| author | |
| committer | |
| log | 52d68a63f8878930b90adbbf4dcdb3753addef12 |
| tree | d378e222fa69a760d4e07df8d12e0cf401ec903f |
| parent | 3c0f71aec0443df1823c3d81c1f020949bbb169e |
| signature | Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU |
- The title bar no longer paints an opaque fill over a translucent surface, so
Acrylic on 10 and Mica on 11 show.
- A system theme change re-reads the app colour mode before winit handles it;
the acrylic tint follows.
- On Windows 7 glass the sidebar stays opaque and toolbar fields and buttons
get light faces.
- Tests that assumed Unix paths or permission bits are portable or Unix-only.
Assisted-by: claude-opus-5.512 files changed, 139 insertions(+), 53 deletions(-)
arc/platforms.md+6-3| ... | ... | @@ -114,9 +114,12 @@ keyboard, the toolbar and the macOS menu bar all run commands from it. |
| 114 | 114 | them over the glass; 11 doesn't over the Direct3D surface, so the row |
| 115 | 115 | draws them as 11 does, lit where the system reports the pointer. On 8 |
| 116 | 116 | and 10, whose frames are opaque, the window has no system frame and the |
| 117 | row draws and runs caption buttons as 10 does, over acrylic on 10. With | |
| 118 | Windows 7's basic or classic theme the system draws the title bar and | |
| 119 | the row lies beneath it, as on KDE. | |
| 117 | row draws and runs caption buttons as 10 does, over acrylic on 10. Over | |
| 118 | 7's glass the sidebar stays opaque, and fields and tool buttons take white | |
| 119 | faces, as Internet Explorer's do there. The theme and material follow the | |
| 120 | system's colour mode as it changes. With Windows 7's basic or classic | |
| 121 | theme the system draws the title bar and the row lies beneath it, as on | |
| 122 | KDE. | |
| 120 | 123 | - A notebook on a share opens by its UNC path through Windows' own SMB |
| 121 | 124 | client, which takes OneNote's opens and locks natively: `onestore` opens a |
| 122 | 125 | section as OneNote does (a reader shares it with everyone, a writer denies |
crates/notebook/tests/discovery.rs+3| ... | ... | @@ -463,8 +463,10 @@ fn local_access_stays_inside_the_selected_root() { |
| 463 | 463 | } |
| 464 | 464 | |
| 465 | 465 | /// Restores permissions a test took away, so the temporary directory can be removed. |
| 466 | #[cfg(unix)] | |
| 466 | 467 | struct Restore(Vec<(std::path::PathBuf, u32)>); |
| 467 | 468 | |
| 469 | #[cfg(unix)] | |
| 468 | 470 | impl Drop for Restore { |
| 469 | 471 | fn drop(&mut self) { |
| 470 | 472 | use std::os::unix::fs::PermissionsExt; |
| ... | ... | @@ -474,6 +476,7 @@ impl Drop for Restore { |
| 474 | 476 | } |
| 475 | 477 | } |
| 476 | 478 | |
| 479 | #[cfg(unix)] | |
| 477 | 480 | #[test] |
| 478 | 481 | fn unreadable_children_are_listed_as_unavailable_and_retried() { |
| 479 | 482 | use std::os::unix::fs::PermissionsExt; |
crates/notebook/tests/session.rs+6-4| ... | ... | @@ -196,9 +196,10 @@ fn saves_wait_for_an_unreachable_file_and_publish_after_relaunch() { |
| 196 | 196 | let (section, _) = open(&file, &cache); |
| 197 | 197 | let space = section.pages().unwrap()[0].0; |
| 198 | 198 | let before = section.page(space).unwrap(); |
| 199 | use std::os::unix::fs::PermissionsExt; | |
| 200 | 199 | let permissions = std::fs::metadata(&file).unwrap().permissions(); |
| 201 | std::fs::set_permissions(&file, std::fs::Permissions::from_mode(0o444)).unwrap(); | |
| 200 | let mut read_only = permissions.clone(); | |
| 201 | read_only.set_readonly(true); | |
| 202 | std::fs::set_permissions(&file, read_only).unwrap(); | |
| 202 | 203 | let after = edited(&before, "Offline "); |
| 203 | 204 | let id = typed(&section, space, &before, 0..0, "Offline "); |
| 204 | 205 | wait(&section, |event| matches!(event, Event::Unreachable(_))); |
| ... | ... | @@ -721,9 +722,10 @@ fn a_conflicting_save_keeps_the_native_page_and_a_conflict_page_the_session_dele |
| 721 | 722 | let space = section.pages().unwrap()[0].0; |
| 722 | 723 | let before = section.page(space).unwrap(); |
| 723 | 724 | let text = first_text(&before); |
| 724 | use std::os::unix::fs::PermissionsExt; | |
| 725 | 725 | let permissions = std::fs::metadata(&file).unwrap().permissions(); |
| 726 | std::fs::set_permissions(&file, std::fs::Permissions::from_mode(0o444)).unwrap(); | |
| 726 | let mut read_only = permissions.clone(); | |
| 727 | read_only.set_readonly(true); | |
| 728 | std::fs::set_permissions(&file, read_only).unwrap(); | |
| 727 | 729 | let mut local = before.clone(); |
| 728 | 730 | model_ops::replace_text(&mut local, text, 0..8, "Local"); |
| 729 | 731 | let id = typed(&section, space, &before, 0..8, "Local"); |
crates/notebook/tests/structure.rs+17-15| ... | ... | @@ -260,7 +260,6 @@ fn file_id(path: &std::path::Path) -> [u8; 16] { |
| 260 | 260 | |
| 261 | 261 | #[test] |
| 262 | 262 | fn a_page_delete_lists_the_recycle_bin_it_finds_or_makes_and_leaves_an_unreadable_one() { |
| 263 | use std::os::unix::fs::PermissionsExt; | |
| 264 | 263 | let temporary = tempfile::tempdir().unwrap(); |
| 265 | 264 | let root = temporary.path().join("notebook"); |
| 266 | 265 | let bin = root.join("OneNote_RecycleBin"); |
| ... | ... | @@ -318,19 +317,22 @@ fn a_page_delete_lists_the_recycle_bin_it_finds_or_makes_and_leaves_an_unreadabl |
| 318 | 317 | if let Some(directory) = std::env::var_os("NOTEBOOK_RECYCLE_EXPORT") { |
| 319 | 318 | copy_dir(&root, std::path::Path::new(&directory)); |
| 320 | 319 | } |
| 321 | // Permission bits do not bind the superuser. | |
| 322 | if unsafe { libc::geteuid() } == 0 { | |
| 323 | return; | |
| 324 | } | |
| 325 | ||
| 326 | 320 | // A bin that cannot be read is neither replaced nor added to. |
| 327 | std::fs::set_permissions(&bin, std::fs::Permissions::from_mode(0o000)).unwrap(); | |
| 328 | let denied = notebook.refresh().and_then(|_| recycle(&mut notebook)); | |
| 329 | std::fs::set_permissions(&bin, std::fs::Permissions::from_mode(0o755)).unwrap(); | |
| 330 | assert!(denied.is_err()); | |
| 331 | let names: Vec<_> = std::fs::read_dir(&bin) | |
| 332 | .unwrap() | |
| 333 | .map(|entry| entry.unwrap().file_name().into_string().unwrap()) | |
| 334 | .collect(); | |
| 335 | assert_eq!(names.len(), 2, "{names:?}"); | |
| 321 | #[cfg(unix)] | |
| 322 | { | |
| 323 | use std::os::unix::fs::PermissionsExt; | |
| 324 | // Permission bits do not bind the superuser. | |
| 325 | if unsafe { libc::geteuid() } == 0 { | |
| 326 | return; | |
| 327 | } | |
| 328 | std::fs::set_permissions(&bin, std::fs::Permissions::from_mode(0o000)).unwrap(); | |
| 329 | let denied = notebook.refresh().and_then(|_| recycle(&mut notebook)); | |
| 330 | std::fs::set_permissions(&bin, std::fs::Permissions::from_mode(0o755)).unwrap(); | |
| 331 | assert!(denied.is_err()); | |
| 332 | let names: Vec<_> = std::fs::read_dir(&bin) | |
| 333 | .unwrap() | |
| 334 | .map(|entry| entry.unwrap().file_name().into_string().unwrap()) | |
| 335 | .collect(); | |
| 336 | assert_eq!(names.len(), 2, "{names:?}"); | |
| 337 | } | |
| 336 | 338 | } |
crates/snowbound/src/library.rs+2-4| ... | ... | @@ -1060,10 +1060,8 @@ mod tests { |
| 1060 | 1060 | ); |
| 1061 | 1061 | assert_eq!(shown("", "Group/New Section 1.one"), under); |
| 1062 | 1062 | assert_eq!(shown("notes/lab", "elsewhere/New Section 1.one"), None); |
| 1063 | assert_eq!( | |
| 1064 | shown("notes/lab", "/tmp/Section.one"), | |
| 1065 | Some(PathBuf::from("/tmp/Section.one")) | |
| 1066 | ); | |
| 1063 | let elsewhere = std::env::temp_dir().join("Section.one"); | |
| 1064 | assert_eq!(library("notes/lab").local(&elsewhere), Some(elsewhere)); | |
| 1067 | 1065 | } |
| 1068 | 1066 | |
| 1069 | 1067 | #[test] |
crates/snowbound/src/linux.rs+5| ... | ... | @@ -253,6 +253,11 @@ fn desktop() -> Desktop { |
| 253 | 253 | }) |
| 254 | 254 | } |
| 255 | 255 | |
| 256 | /// The theme over the window's backdrop, as the kit draws it. | |
| 257 | pub fn over_backdrop(theme: ui::Theme) -> ui::Theme { | |
| 258 | theme.over_backdrop() | |
| 259 | } | |
| 260 | ||
| 256 | 261 | /// Menus in `appearance` as the desktop draws its own: libadwaita's popover menus on GNOME |
| 257 | 262 | /// and Breeze's on KDE, each moving as that desktop moves them. |
| 258 | 263 | pub fn menu(appearance: Theme) -> Option<ui::Menu> { |
crates/snowbound/src/macos.rs+5| ... | ... | @@ -246,6 +246,11 @@ pub fn represent(window: &Window, file: Option<&std::path::Path>) { |
| 246 | 246 | } |
| 247 | 247 | } |
| 248 | 248 | |
| 249 | /// The theme over the window's backdrop, as the kit draws it. | |
| 250 | pub fn over_backdrop(theme: ui::Theme) -> ui::Theme { | |
| 251 | theme.over_backdrop() | |
| 252 | } | |
| 253 | ||
| 249 | 254 | /// None: the kit's own menus stand in for AppKit's. |
| 250 | 255 | pub fn menu(_: winit::window::Theme) -> Option<ui::Menu> { |
| 251 | 256 | None |
crates/snowbound/src/main.rs+7-3| ... | ... | @@ -876,7 +876,9 @@ impl State { |
| 876 | 876 | theme(appearance, stored.light_pages, backdrop), |
| 877 | 877 | platform::double_click_interval(), |
| 878 | 878 | ); |
| 879 | let titlebar = platform::titlebar(appearance).unwrap_or([ui.theme.strip; 2]); | |
| 879 | let titlebar = platform::titlebar(appearance) | |
| 880 | .filter(|_| !backdrop) | |
| 881 | .unwrap_or([ui.theme.strip; 2]); | |
| 880 | 882 | // A window shown but never focused hears no focus event; a hidden one draws as focused. |
| 881 | 883 | ui.window_focused = !visible || window.has_focus(); |
| 882 | 884 | platform::system_interface(&mut ui); |
| ... | ... | @@ -1118,7 +1120,9 @@ impl State { |
| 1118 | 1120 | /// Takes `appearance`'s colours. |
| 1119 | 1121 | fn set_appearance(&mut self, appearance: winit::window::Theme) { |
| 1120 | 1122 | self.ui.theme = theme(appearance, self.light_pages, self.surface.translucent()); |
| 1121 | self.titlebar = platform::titlebar(appearance).unwrap_or([self.ui.theme.strip; 2]); | |
| 1123 | self.titlebar = platform::titlebar(appearance) | |
| 1124 | .filter(|_| !self.surface.translucent()) | |
| 1125 | .unwrap_or([self.ui.theme.strip; 2]); | |
| 1122 | 1126 | } |
| 1123 | 1127 | |
| 1124 | 1128 | /// Takes the colour scheme chosen, or the system's where it follows the system. |
| ... | ... | @@ -3702,7 +3706,7 @@ fn theme(appearance: winit::window::Theme, light_pages: bool, backdrop: bool) -> |
| 3702 | 3706 | [theme.paper, theme.paper_ink] = [light.paper, light.paper_ink]; |
| 3703 | 3707 | } |
| 3704 | 3708 | if backdrop { |
| 3705 | theme.over_backdrop() | |
| 3709 | platform::over_backdrop(theme) | |
| 3706 | 3710 | } else { |
| 3707 | 3711 | theme |
| 3708 | 3712 | } |
crates/snowbound/src/sidebar.rs+2-2| ... | ... | @@ -584,7 +584,7 @@ fn tree_row(ui: &mut Ui, tree: &mut Tree, id: Id, row: Row) -> (Signal, bool) { |
| 584 | 584 | flags: Flags::CLICKABLE, |
| 585 | 585 | size: [fill(), px(ROW)], |
| 586 | 586 | fill: lit, |
| 587 | hover_fill: Some(ui::mix(lit.unwrap_or(theme.strip), theme.hover(), 0.6)), | |
| 587 | hover_fill: Some(ui::mix(lit.unwrap_or(theme.sidebar), theme.hover(), 0.6)), | |
| 588 | 588 | radius: 4.0, |
| 589 | 589 | pad: [6.0 + INDENT * row.depth as f32, 0.0], |
| 590 | 590 | // The field's text stands where the label did. |
| ... | ... | @@ -681,7 +681,7 @@ impl crate::State { |
| 681 | 681 | Spec { |
| 682 | 682 | flags: Flags::CLIP, |
| 683 | 683 | size: [px(width), fill()], |
| 684 | fill: Some(theme.strip), | |
| 684 | fill: Some(theme.sidebar), | |
| 685 | 685 | ..Spec::default() |
| 686 | 686 | }, |
| 687 | 687 | ); |
crates/snowbound/src/windows.rs+65-17| ... | ... | @@ -234,6 +234,12 @@ unsafe extern "system" fn frame_procedure( |
| 234 | 234 | match message { |
| 235 | 235 | // Accent colours and transparency effects reach the window only as settings. |
| 236 | 236 | wm::WM_SETTINGCHANGE | wm::WM_DWMCOLORIZATIONCOLORCHANGED => { |
| 237 | if message == wm::WM_SETTINGCHANGE && setting(lparam) == "ImmersiveColorSet" { | |
| 238 | refresh_color_policy(); | |
| 239 | if ACRYLIC.load(Ordering::Relaxed) { | |
| 240 | accent(hwnd, Accent::Acrylic); | |
| 241 | } | |
| 242 | } | |
| 237 | 243 | if let Some(proxy) = QUIT.get() { |
| 238 | 244 | let _ = proxy.send_event(crate::UserEvent::Appearance); |
| 239 | 245 | } |
| ... | ... | @@ -252,6 +258,33 @@ unsafe extern "system" fn frame_procedure( |
| 252 | 258 | result.unwrap_or_else(|| previous(message, wparam, lparam)) |
| 253 | 259 | } |
| 254 | 260 | |
| 261 | /// The setting a `WM_SETTINGCHANGE` names, if any. | |
| 262 | fn setting(lparam: LPARAM) -> String { | |
| 263 | if lparam == 0 { | |
| 264 | return String::new(); | |
| 265 | } | |
| 266 | let text = lparam as *const u16; | |
| 267 | let length = (0..).take_while(|&i| unsafe { *text.add(i) } != 0).count(); | |
| 268 | String::from_utf16_lossy(unsafe { std::slice::from_raw_parts(text, length) }) | |
| 269 | } | |
| 270 | ||
| 271 | /// Has uxtheme read the apps' colour mode again: it answers winit's `ShouldAppsUseDarkMode` | |
| 272 | /// from a cache that a change of mode otherwise leaves stale. | |
| 273 | fn refresh_color_policy() { | |
| 274 | // RefreshImmersiveColorPolicyState is exported by ordinal alone, and 104 only from 1809. | |
| 275 | if version() < (10, 0, 17763) { | |
| 276 | return; | |
| 277 | } | |
| 278 | let refresh = unsafe { | |
| 279 | let module = LoadLibraryW(wide("uxtheme.dll").as_ptr()); | |
| 280 | GetProcAddress(module, 104 as *const u8) | |
| 281 | }; | |
| 282 | if let Some(refresh) = refresh { | |
| 283 | let refresh: unsafe extern "system" fn() = unsafe { std::mem::transmute(refresh) }; | |
| 284 | unsafe { refresh() }; | |
| 285 | } | |
| 286 | } | |
| 287 | ||
| 255 | 288 | type Procedure<'a> = &'a dyn Fn(u32, WPARAM, LPARAM) -> LRESULT; |
| 256 | 289 | |
| 257 | 290 | /// The frame over glass or Mica: the system's frame keeps its sides and bottom, the caption |
| ... | ... | @@ -383,7 +416,7 @@ fn drawn_frame( |
| 383 | 416 | None |
| 384 | 417 | } |
| 385 | 418 | wm::WM_EXITSIZEMOVE if ACRYLIC.load(Ordering::Relaxed) => { |
| 386 | accent(hwnd, Accent::Acrylic(ACRYLIC_TINT.load(Ordering::Relaxed))); | |
| 419 | accent(hwnd, Accent::Acrylic); | |
| 387 | 420 | None |
| 388 | 421 | } |
| 389 | 422 | _ => None, |
| ... | ... | @@ -409,15 +442,13 @@ pub fn cuts_corners() -> bool { |
| 409 | 442 | false |
| 410 | 443 | } |
| 411 | 444 | |
| 412 | /// Whether Windows 10 shows acrylic under the window, which a drag swaps for plain blur, | |
| 413 | /// and the tint it shows it with. | |
| 445 | /// Whether Windows 10 shows acrylic under the window, which a drag swaps for plain blur. | |
| 414 | 446 | static ACRYLIC: AtomicBool = AtomicBool::new(false); |
| 415 | static ACRYLIC_TINT: AtomicU32 = AtomicU32::new(0); | |
| 416 | 447 | |
| 417 | 448 | enum Accent { |
| 418 | 449 | Blur, |
| 419 | /// Acrylic tinted with an `0xAABBGGRR` colour. | |
| 420 | Acrylic(u32), | |
| 450 | /// Acrylic in the shell's own tints: Windows 10's light and dark flyouts. | |
| 451 | Acrylic, | |
| 421 | 452 | } |
| 422 | 453 | |
| 423 | 454 | /// Windows 10's accent under the window, through the undocumented |
| ... | ... | @@ -448,10 +479,15 @@ fn accent(hwnd: HWND, accent: Accent) -> bool { |
| 448 | 479 | tint: 0, |
| 449 | 480 | animation: 0, |
| 450 | 481 | }, |
| 451 | Accent::Acrylic(tint) => Policy { | |
| 482 | Accent::Acrylic => Policy { | |
| 452 | 483 | state: 4, |
| 453 | 484 | flags: 2, |
| 454 | tint, | |
| 485 | // 0xAABBGGRR, after the apps' colour mode. | |
| 486 | tint: if registry_dword(PERSONALIZE, "AppsUseLightTheme") == Some(0) { | |
| 487 | 0xcc20_2020 | |
| 488 | } else { | |
| 489 | 0xccf3_f3f3 | |
| 490 | }, | |
| 455 | 491 | animation: 0, |
| 456 | 492 | }, |
| 457 | 493 | }; |
| ... | ... | @@ -466,12 +502,11 @@ fn accent(hwnd: HWND, accent: Accent) -> bool { |
| 466 | 502 | |
| 467 | 503 | /// Whether Personalization's transparency effects are on, as acrylic and Mica need. |
| 468 | 504 | fn transparency() -> bool { |
| 469 | registry_dword( | |
| 470 | r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize", | |
| 471 | "EnableTransparency", | |
| 472 | ) != Some(0) | |
| 505 | registry_dword(PERSONALIZE, "EnableTransparency") != Some(0) | |
| 473 | 506 | } |
| 474 | 507 | |
| 508 | const PERSONALIZE: &str = r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"; | |
| 509 | ||
| 475 | 510 | /// Lays the system's material under the window, which shows through the app's transparent |
| 476 | 511 | /// pixels: Aero glass on Windows 7 while the desktop composes, acrylic on Windows 10 (blur |
| 477 | 512 | /// before its April 2018 update) and Mica on 11. Windows 8 has none. |
| ... | ... | @@ -504,11 +539,7 @@ pub fn install_backdrop(window: &Window) -> bool { |
| 504 | 539 | } |
| 505 | 540 | } |
| 506 | 541 | Caption::Drawn if build >= 17134 => { |
| 507 | // The shell's own acrylic tints: Windows 10's light and dark flyouts. | |
| 508 | let dark = appearance(window) == Theme::Dark; | |
| 509 | let tint = if dark { 0xcc20_2020 } else { 0xccf3_f3f3 }; | |
| 510 | ACRYLIC_TINT.store(tint, Ordering::Relaxed); | |
| 511 | let shown = accent(hwnd, Accent::Acrylic(tint)); | |
| 542 | let shown = accent(hwnd, Accent::Acrylic); | |
| 512 | 543 | ACRYLIC.store(shown, Ordering::Relaxed); |
| 513 | 544 | shown |
| 514 | 545 | } |
| ... | ... | @@ -543,6 +574,23 @@ pub fn titlebar(appearance: Theme) -> Option<[[f32; 4]; 2]> { |
| 543 | 574 | }) |
| 544 | 575 | } |
| 545 | 576 | |
| 577 | /// The theme over the window's material. Over Windows 7's glass, fields and tool buttons | |
| 578 | /// take white faces, as Internet Explorer's do there, so their text and icons stay legible, | |
| 579 | /// and the sidebar stays opaque. | |
| 580 | pub fn over_backdrop(theme: ui::Theme) -> ui::Theme { | |
| 581 | let sidebar = theme.sidebar; | |
| 582 | let theme = theme.over_backdrop(); | |
| 583 | if caption() != Caption::Glass || eleven() { | |
| 584 | return theme; | |
| 585 | } | |
| 586 | ui::Theme { | |
| 587 | sidebar, | |
| 588 | base: [1.0, 1.0, 1.0, 0.8], | |
| 589 | tool: [1.0, 1.0, 1.0, 0.35], | |
| 590 | ..theme | |
| 591 | } | |
| 592 | } | |
| 593 | ||
| 546 | 594 | /// None: the kit's own menus. |
| 547 | 595 | pub fn menu(_: Theme) -> Option<ui::Menu> { |
| 548 | 596 | None |
crates/ui/src/shell.rs+10-3| ... | ... | @@ -270,7 +270,7 @@ pub fn tool_button( |
| 270 | 270 | size: [px(TOOL), px(TOOL)], |
| 271 | 271 | icon: Some(icon), |
| 272 | 272 | color: Some(tint), |
| 273 | fill: (on == Some(true)).then_some(hover), | |
| 273 | fill: (on == Some(true)).then_some(hover).or(rest(&ui.theme)), | |
| 274 | 274 | hover_fill: Some(hover), |
| 275 | 275 | radius: 4.0, |
| 276 | 276 | center: true, |
| ... | ... | @@ -283,6 +283,11 @@ pub fn tool_button( |
| 283 | 283 | ui.signal(id) |
| 284 | 284 | } |
| 285 | 285 | |
| 286 | /// A tool button's face at rest, where the theme gives it one. | |
| 287 | fn rest(theme: &crate::Theme) -> Option<[f32; 4]> { | |
| 288 | (theme.tool[3] > 0.0).then_some(theme.tool) | |
| 289 | } | |
| 290 | ||
| 286 | 291 | /// Shows button `id` as a toggle, pressed or not, where `on` is given. |
| 287 | 292 | fn press_state(ui: &mut Ui, id: Id, on: Option<bool>) { |
| 288 | 293 | if let Some(on) = on |
| ... | ... | @@ -404,7 +409,7 @@ pub fn split_button( |
| 404 | 409 | Spec { |
| 405 | 410 | flags: Flags::FLOAT, |
| 406 | 411 | size: [px(TOOL + RADIUS), px(TOOL)], |
| 407 | fill: fade(lit), | |
| 412 | fill: fade(lit).or(rest(&ui.theme)), | |
| 408 | 413 | radius: RADIUS, |
| 409 | 414 | ..Spec::default() |
| 410 | 415 | }, |
| ... | ... | @@ -476,7 +481,9 @@ pub fn menu_button( |
| 476 | 481 | Spec { |
| 477 | 482 | flags: Flags::CLICKABLE, |
| 478 | 483 | size: [px(TOOL + ARROW), px(TOOL)], |
| 479 | fill: (ui.popup_open(menu) || on == Some(true)).then(|| theme.hover()), | |
| 484 | fill: (ui.popup_open(menu) || on == Some(true)) | |
| 485 | .then(|| theme.hover()) | |
| 486 | .or(rest(&theme)), | |
| 480 | 487 | hover_fill: Some(theme.hover()), |
| 481 | 488 | radius: 4.0, |
| 482 | 489 | role: Some(Role::Button), |
crates/ui/src/theme.rs+11-2| ... | ... | @@ -8,9 +8,13 @@ pub struct Theme { |
| 8 | 8 | pub base: [f32; 4], |
| 9 | 9 | /// Side panels. |
| 10 | 10 | pub panel: [f32; 4], |
| 11 | /// The window's title bar, toolbar, tab row and sidebar; transparent where the system's | |
| 12 | /// backdrop shows through them. | |
| 11 | /// The window's title bar, toolbar and tab row; transparent where the system's backdrop | |
| 12 | /// shows through them. | |
| 13 | 13 | pub strip: [f32; 4], |
| 14 | /// The notebook sidebar: the strip's colour, unless kept opaque over a backdrop. | |
| 15 | pub sidebar: [f32; 4], | |
| 16 | /// Toolbar buttons at rest: clear, or a face where the backdrop would hide their icons. | |
| 17 | pub tool: [f32; 4], | |
| 14 | 18 | pub accent: [f32; 4], |
| 15 | 19 | /// The text caret, and selected text's fill with and without keyboard focus; the |
| 16 | 20 | /// platform's own where it has them. |
| ... | ... | @@ -139,6 +143,8 @@ impl Theme { |
| 139 | 143 | base: srgb(0x19, 0x1b, 0x1c), |
| 140 | 144 | panel: srgb(0x1f, 0x22, 0x23), |
| 141 | 145 | strip: srgb(0x27, 0x2a, 0x2b), |
| 146 | sidebar: srgb(0x27, 0x2a, 0x2b), | |
| 147 | tool: [0.0; 4], | |
| 142 | 148 | accent: srgb(0x00, 0x79, 0xa6), |
| 143 | 149 | caret, |
| 144 | 150 | selection, |
| ... | ... | @@ -168,6 +174,8 @@ impl Theme { |
| 168 | 174 | base: srgb(0xfc, 0xfc, 0xfd), |
| 169 | 175 | panel: srgb(0xf4, 0xf5, 0xf7), |
| 170 | 176 | strip: srgb(0xeb, 0xed, 0xf0), |
| 177 | sidebar: srgb(0xeb, 0xed, 0xf0), | |
| 178 | tool: [0.0; 4], | |
| 171 | 179 | accent: srgb(0x00, 0x79, 0xa6), |
| 172 | 180 | caret, |
| 173 | 181 | selection, |
| ... | ... | @@ -196,6 +204,7 @@ impl Theme { |
| 196 | 204 | let text = |alpha| [self.text[0], self.text[1], self.text[2], alpha]; |
| 197 | 205 | Self { |
| 198 | 206 | strip: [0.0; 4], |
| 207 | sidebar: [0.0; 4], | |
| 199 | 208 | text_dim: text(0.5), |
| 200 | 209 | chip: text(0.1), |
| 201 | 210 | base: text(0.05), |