authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-09-30 13:02:51-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-09-30 13:36:50-07:00
log52d68a63f8878930b90adbbf4dcdb3753addef12
treed378e222fa69a760d4e07df8d12e0cf401ec903f
parent3c0f71aec0443df1823c3d81c1f020949bbb169e
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU

fix(windows): acrylic and Mica show through the row, live theme switching, Windows 7 glass through the toolbar

- 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.5

12 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,9 +114,12 @@ keyboard, the toolbar and the macOS menu bar all run commands from it.
114 them over the glass; 11 doesn't over the Direct3D surface, so the row114 them over the glass; 11 doesn't over the Direct3D surface, so the row
115 draws them as 11 does, lit where the system reports the pointer. On 8115 draws them as 11 does, lit where the system reports the pointer. On 8
116 and 10, whose frames are opaque, the window has no system frame and the116 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. With117 row draws and runs caption buttons as 10 does, over acrylic on 10. Over
118 Windows 7's basic or classic theme the system draws the title bar and118 7's glass the sidebar stays opaque, and fields and tool buttons take white
119 the row lies beneath it, as on KDE.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- A notebook on a share opens by its UNC path through Windows' own SMB123- A notebook on a share opens by its UNC path through Windows' own SMB
121 client, which takes OneNote's opens and locks natively: `onestore` opens a124 client, which takes OneNote's opens and locks natively: `onestore` opens a
122 section as OneNote does (a reader shares it with everyone, a writer denies125 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,8 +463,10 @@ fn local_access_stays_inside_the_selected_root() {
463}463}
464464
465/// Restores permissions a test took away, so the temporary directory can be removed.465/// Restores permissions a test took away, so the temporary directory can be removed.
466#[cfg(unix)]
466struct Restore(Vec<(std::path::PathBuf, u32)>);467struct Restore(Vec<(std::path::PathBuf, u32)>);
467468
469#[cfg(unix)]
468impl Drop for Restore {470impl Drop for Restore {
469 fn drop(&mut self) {471 fn drop(&mut self) {
470 use std::os::unix::fs::PermissionsExt;472 use std::os::unix::fs::PermissionsExt;
...@@ -474,6 +476,7 @@ impl Drop for Restore {...@@ -474,6 +476,7 @@ impl Drop for Restore {
474 }476 }
475}477}
476478
479#[cfg(unix)]
477#[test]480#[test]
478fn unreadable_children_are_listed_as_unavailable_and_retried() {481fn unreadable_children_are_listed_as_unavailable_and_retried() {
479 use std::os::unix::fs::PermissionsExt;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,9 +196,10 @@ fn saves_wait_for_an_unreachable_file_and_publish_after_relaunch() {
196 let (section, _) = open(&file, &cache);196 let (section, _) = open(&file, &cache);
197 let space = section.pages().unwrap()[0].0;197 let space = section.pages().unwrap()[0].0;
198 let before = section.page(space).unwrap();198 let before = section.page(space).unwrap();
199 use std::os::unix::fs::PermissionsExt;
200 let permissions = std::fs::metadata(&file).unwrap().permissions();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 let after = edited(&before, "Offline ");203 let after = edited(&before, "Offline ");
203 let id = typed(&section, space, &before, 0..0, "Offline ");204 let id = typed(&section, space, &before, 0..0, "Offline ");
204 wait(&section, |event| matches!(event, Event::Unreachable(_)));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,9 +722,10 @@ fn a_conflicting_save_keeps_the_native_page_and_a_conflict_page_the_session_dele
721 let space = section.pages().unwrap()[0].0;722 let space = section.pages().unwrap()[0].0;
722 let before = section.page(space).unwrap();723 let before = section.page(space).unwrap();
723 let text = first_text(&before);724 let text = first_text(&before);
724 use std::os::unix::fs::PermissionsExt;
725 let permissions = std::fs::metadata(&file).unwrap().permissions();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 let mut local = before.clone();729 let mut local = before.clone();
728 model_ops::replace_text(&mut local, text, 0..8, "Local");730 model_ops::replace_text(&mut local, text, 0..8, "Local");
729 let id = typed(&section, space, &before, 0..8, "Local");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,7 +260,6 @@ fn file_id(path: &std::path::Path) -> [u8; 16] {
260260
261#[test]261#[test]
262fn a_page_delete_lists_the_recycle_bin_it_finds_or_makes_and_leaves_an_unreadable_one() {262fn a_page_delete_lists_the_recycle_bin_it_finds_or_makes_and_leaves_an_unreadable_one() {
263 use std::os::unix::fs::PermissionsExt;
264 let temporary = tempfile::tempdir().unwrap();263 let temporary = tempfile::tempdir().unwrap();
265 let root = temporary.path().join("notebook");264 let root = temporary.path().join("notebook");
266 let bin = root.join("OneNote_RecycleBin");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,19 +317,22 @@ fn a_page_delete_lists_the_recycle_bin_it_finds_or_makes_and_leaves_an_unreadabl
318 if let Some(directory) = std::env::var_os("NOTEBOOK_RECYCLE_EXPORT") {317 if let Some(directory) = std::env::var_os("NOTEBOOK_RECYCLE_EXPORT") {
319 copy_dir(&root, std::path::Path::new(&directory));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 // A bin that cannot be read is neither replaced nor added to.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();321 #[cfg(unix)]
328 let denied = notebook.refresh().and_then(|_| recycle(&mut notebook));322 {
329 std::fs::set_permissions(&bin, std::fs::Permissions::from_mode(0o755)).unwrap();323 use std::os::unix::fs::PermissionsExt;
330 assert!(denied.is_err());324 // Permission bits do not bind the superuser.
331 let names: Vec<_> = std::fs::read_dir(&bin)325 if unsafe { libc::geteuid() } == 0 {
332 .unwrap()326 return;
333 .map(|entry| entry.unwrap().file_name().into_string().unwrap())327 }
334 .collect();328 std::fs::set_permissions(&bin, std::fs::Permissions::from_mode(0o000)).unwrap();
335 assert_eq!(names.len(), 2, "{names:?}");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,10 +1060,8 @@ mod tests {
1060 );1060 );
1061 assert_eq!(shown("", "Group/New Section 1.one"), under);1061 assert_eq!(shown("", "Group/New Section 1.one"), under);
1062 assert_eq!(shown("notes/lab", "elsewhere/New Section 1.one"), None);1062 assert_eq!(shown("notes/lab", "elsewhere/New Section 1.one"), None);
1063 assert_eq!(1063 let elsewhere = std::env::temp_dir().join("Section.one");
1064 shown("notes/lab", "/tmp/Section.one"),1064 assert_eq!(library("notes/lab").local(&elsewhere), Some(elsewhere));
1065 Some(PathBuf::from("/tmp/Section.one"))
1066 );
1067 }1065 }
10681066
1069 #[test]1067 #[test]
crates/snowbound/src/linux.rs+5
...@@ -253,6 +253,11 @@ fn desktop() -> Desktop {...@@ -253,6 +253,11 @@ fn desktop() -> Desktop {
253 })253 })
254}254}
255255
256/// The theme over the window's backdrop, as the kit draws it.
257pub fn over_backdrop(theme: ui::Theme) -> ui::Theme {
258 theme.over_backdrop()
259}
260
256/// Menus in `appearance` as the desktop draws its own: libadwaita's popover menus on GNOME261/// Menus in `appearance` as the desktop draws its own: libadwaita's popover menus on GNOME
257/// and Breeze's on KDE, each moving as that desktop moves them.262/// and Breeze's on KDE, each moving as that desktop moves them.
258pub fn menu(appearance: Theme) -> Option<ui::Menu> {263pub 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,6 +246,11 @@ pub fn represent(window: &Window, file: Option<&std::path::Path>) {
246 }246 }
247}247}
248248
249/// The theme over the window's backdrop, as the kit draws it.
250pub fn over_backdrop(theme: ui::Theme) -> ui::Theme {
251 theme.over_backdrop()
252}
253
249/// None: the kit's own menus stand in for AppKit's.254/// None: the kit's own menus stand in for AppKit's.
250pub fn menu(_: winit::window::Theme) -> Option<ui::Menu> {255pub fn menu(_: winit::window::Theme) -> Option<ui::Menu> {
251 None256 None
crates/snowbound/src/main.rs+7-3
...@@ -876,7 +876,9 @@ impl State {...@@ -876,7 +876,9 @@ impl State {
876 theme(appearance, stored.light_pages, backdrop),876 theme(appearance, stored.light_pages, backdrop),
877 platform::double_click_interval(),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 // A window shown but never focused hears no focus event; a hidden one draws as focused.882 // A window shown but never focused hears no focus event; a hidden one draws as focused.
881 ui.window_focused = !visible || window.has_focus();883 ui.window_focused = !visible || window.has_focus();
882 platform::system_interface(&mut ui);884 platform::system_interface(&mut ui);
...@@ -1118,7 +1120,9 @@ impl State {...@@ -1118,7 +1120,9 @@ impl State {
1118 /// Takes `appearance`'s colours.1120 /// Takes `appearance`'s colours.
1119 fn set_appearance(&mut self, appearance: winit::window::Theme) {1121 fn set_appearance(&mut self, appearance: winit::window::Theme) {
1120 self.ui.theme = theme(appearance, self.light_pages, self.surface.translucent());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 }
11231127
1124 /// Takes the colour scheme chosen, or the system's where it follows the system.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,7 +3706,7 @@ fn theme(appearance: winit::window::Theme, light_pages: bool, backdrop: bool) ->
3702 [theme.paper, theme.paper_ink] = [light.paper, light.paper_ink];3706 [theme.paper, theme.paper_ink] = [light.paper, light.paper_ink];
3703 }3707 }
3704 if backdrop {3708 if backdrop {
3705 theme.over_backdrop()3709 platform::over_backdrop(theme)
3706 } else {3710 } else {
3707 theme3711 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,7 +584,7 @@ fn tree_row(ui: &mut Ui, tree: &mut Tree, id: Id, row: Row) -> (Signal, bool) {
584 flags: Flags::CLICKABLE,584 flags: Flags::CLICKABLE,
585 size: [fill(), px(ROW)],585 size: [fill(), px(ROW)],
586 fill: lit,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 radius: 4.0,588 radius: 4.0,
589 pad: [6.0 + INDENT * row.depth as f32, 0.0],589 pad: [6.0 + INDENT * row.depth as f32, 0.0],
590 // The field's text stands where the label did.590 // The field's text stands where the label did.
...@@ -681,7 +681,7 @@ impl crate::State {...@@ -681,7 +681,7 @@ impl crate::State {
681 Spec {681 Spec {
682 flags: Flags::CLIP,682 flags: Flags::CLIP,
683 size: [px(width), fill()],683 size: [px(width), fill()],
684 fill: Some(theme.strip),684 fill: Some(theme.sidebar),
685 ..Spec::default()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,6 +234,12 @@ unsafe extern "system" fn frame_procedure(
234 match message {234 match message {
235 // Accent colours and transparency effects reach the window only as settings.235 // Accent colours and transparency effects reach the window only as settings.
236 wm::WM_SETTINGCHANGE | wm::WM_DWMCOLORIZATIONCOLORCHANGED => {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 if let Some(proxy) = QUIT.get() {243 if let Some(proxy) = QUIT.get() {
238 let _ = proxy.send_event(crate::UserEvent::Appearance);244 let _ = proxy.send_event(crate::UserEvent::Appearance);
239 }245 }
...@@ -252,6 +258,33 @@ unsafe extern "system" fn frame_procedure(...@@ -252,6 +258,33 @@ unsafe extern "system" fn frame_procedure(
252 result.unwrap_or_else(|| previous(message, wparam, lparam))258 result.unwrap_or_else(|| previous(message, wparam, lparam))
253}259}
254260
261/// The setting a `WM_SETTINGCHANGE` names, if any.
262fn 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.
273fn 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
255type Procedure<'a> = &'a dyn Fn(u32, WPARAM, LPARAM) -> LRESULT;288type Procedure<'a> = &'a dyn Fn(u32, WPARAM, LPARAM) -> LRESULT;
256289
257/// The frame over glass or Mica: the system's frame keeps its sides and bottom, the caption290/// 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,7 +416,7 @@ fn drawn_frame(
383 None416 None
384 }417 }
385 wm::WM_EXITSIZEMOVE if ACRYLIC.load(Ordering::Relaxed) => {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 None420 None
388 }421 }
389 _ => None,422 _ => None,
...@@ -409,15 +442,13 @@ pub fn cuts_corners() -> bool {...@@ -409,15 +442,13 @@ pub fn cuts_corners() -> bool {
409 false442 false
410}443}
411444
412/// Whether Windows 10 shows acrylic under the window, which a drag swaps for plain blur,445/// Whether Windows 10 shows acrylic under the window, which a drag swaps for plain blur.
413/// and the tint it shows it with.
414static ACRYLIC: AtomicBool = AtomicBool::new(false);446static ACRYLIC: AtomicBool = AtomicBool::new(false);
415static ACRYLIC_TINT: AtomicU32 = AtomicU32::new(0);
416447
417enum Accent {448enum Accent {
418 Blur,449 Blur,
419 /// Acrylic tinted with an `0xAABBGGRR` colour.450 /// Acrylic in the shell's own tints: Windows 10's light and dark flyouts.
420 Acrylic(u32),451 Acrylic,
421}452}
422453
423/// Windows 10's accent under the window, through the undocumented454/// Windows 10's accent under the window, through the undocumented
...@@ -448,10 +479,15 @@ fn accent(hwnd: HWND, accent: Accent) -> bool {...@@ -448,10 +479,15 @@ fn accent(hwnd: HWND, accent: Accent) -> bool {
448 tint: 0,479 tint: 0,
449 animation: 0,480 animation: 0,
450 },481 },
451 Accent::Acrylic(tint) => Policy {482 Accent::Acrylic => Policy {
452 state: 4,483 state: 4,
453 flags: 2,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 animation: 0,491 animation: 0,
456 },492 },
457 };493 };
...@@ -466,12 +502,11 @@ fn accent(hwnd: HWND, accent: Accent) -> bool {...@@ -466,12 +502,11 @@ fn accent(hwnd: HWND, accent: Accent) -> bool {
466502
467/// Whether Personalization's transparency effects are on, as acrylic and Mica need.503/// Whether Personalization's transparency effects are on, as acrylic and Mica need.
468fn transparency() -> bool {504fn transparency() -> bool {
469 registry_dword(505 registry_dword(PERSONALIZE, "EnableTransparency") != Some(0)
470 r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize",
471 "EnableTransparency",
472 ) != Some(0)
473}506}
474507
508const PERSONALIZE: &str = r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize";
509
475/// Lays the system's material under the window, which shows through the app's transparent510/// Lays the system's material under the window, which shows through the app's transparent
476/// pixels: Aero glass on Windows 7 while the desktop composes, acrylic on Windows 10 (blur511/// pixels: Aero glass on Windows 7 while the desktop composes, acrylic on Windows 10 (blur
477/// before its April 2018 update) and Mica on 11. Windows 8 has none.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,11 +539,7 @@ pub fn install_backdrop(window: &Window) -> bool {
504 }539 }
505 }540 }
506 Caption::Drawn if build >= 17134 => {541 Caption::Drawn if build >= 17134 => {
507 // The shell's own acrylic tints: Windows 10's light and dark flyouts.542 let shown = accent(hwnd, Accent::Acrylic);
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));
512 ACRYLIC.store(shown, Ordering::Relaxed);543 ACRYLIC.store(shown, Ordering::Relaxed);
513 shown544 shown
514 }545 }
...@@ -543,6 +574,23 @@ pub fn titlebar(appearance: Theme) -> Option<[[f32; 4]; 2]> {...@@ -543,6 +574,23 @@ pub fn titlebar(appearance: Theme) -> Option<[[f32; 4]; 2]> {
543 })574 })
544}575}
545576
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.
580pub 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/// None: the kit's own menus.594/// None: the kit's own menus.
547pub fn menu(_: Theme) -> Option<ui::Menu> {595pub fn menu(_: Theme) -> Option<ui::Menu> {
548 None596 None
crates/ui/src/shell.rs+10-3
...@@ -270,7 +270,7 @@ pub fn tool_button(...@@ -270,7 +270,7 @@ pub fn tool_button(
270 size: [px(TOOL), px(TOOL)],270 size: [px(TOOL), px(TOOL)],
271 icon: Some(icon),271 icon: Some(icon),
272 color: Some(tint),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 hover_fill: Some(hover),274 hover_fill: Some(hover),
275 radius: 4.0,275 radius: 4.0,
276 center: true,276 center: true,
...@@ -283,6 +283,11 @@ pub fn tool_button(...@@ -283,6 +283,11 @@ pub fn tool_button(
283 ui.signal(id)283 ui.signal(id)
284}284}
285285
286/// A tool button's face at rest, where the theme gives it one.
287fn rest(theme: &crate::Theme) -> Option<[f32; 4]> {
288 (theme.tool[3] > 0.0).then_some(theme.tool)
289}
290
286/// Shows button `id` as a toggle, pressed or not, where `on` is given.291/// Shows button `id` as a toggle, pressed or not, where `on` is given.
287fn press_state(ui: &mut Ui, id: Id, on: Option<bool>) {292fn press_state(ui: &mut Ui, id: Id, on: Option<bool>) {
288 if let Some(on) = on293 if let Some(on) = on
...@@ -404,7 +409,7 @@ pub fn split_button(...@@ -404,7 +409,7 @@ pub fn split_button(
404 Spec {409 Spec {
405 flags: Flags::FLOAT,410 flags: Flags::FLOAT,
406 size: [px(TOOL + RADIUS), px(TOOL)],411 size: [px(TOOL + RADIUS), px(TOOL)],
407 fill: fade(lit),412 fill: fade(lit).or(rest(&ui.theme)),
408 radius: RADIUS,413 radius: RADIUS,
409 ..Spec::default()414 ..Spec::default()
410 },415 },
...@@ -476,7 +481,9 @@ pub fn menu_button(...@@ -476,7 +481,9 @@ pub fn menu_button(
476 Spec {481 Spec {
477 flags: Flags::CLICKABLE,482 flags: Flags::CLICKABLE,
478 size: [px(TOOL + ARROW), px(TOOL)],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 hover_fill: Some(theme.hover()),487 hover_fill: Some(theme.hover()),
481 radius: 4.0,488 radius: 4.0,
482 role: Some(Role::Button),489 role: Some(Role::Button),
crates/ui/src/theme.rs+11-2
...@@ -8,9 +8,13 @@ pub struct Theme {...@@ -8,9 +8,13 @@ pub struct Theme {
8 pub base: [f32; 4],8 pub base: [f32; 4],
9 /// Side panels.9 /// Side panels.
10 pub panel: [f32; 4],10 pub panel: [f32; 4],
11 /// The window's title bar, toolbar, tab row and sidebar; transparent where the system's11 /// The window's title bar, toolbar and tab row; transparent where the system's backdrop
12 /// backdrop shows through them.12 /// shows through them.
13 pub strip: [f32; 4],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 pub accent: [f32; 4],18 pub accent: [f32; 4],
15 /// The text caret, and selected text's fill with and without keyboard focus; the19 /// The text caret, and selected text's fill with and without keyboard focus; the
16 /// platform's own where it has them.20 /// platform's own where it has them.
...@@ -139,6 +143,8 @@ impl Theme {...@@ -139,6 +143,8 @@ impl Theme {
139 base: srgb(0x19, 0x1b, 0x1c),143 base: srgb(0x19, 0x1b, 0x1c),
140 panel: srgb(0x1f, 0x22, 0x23),144 panel: srgb(0x1f, 0x22, 0x23),
141 strip: srgb(0x27, 0x2a, 0x2b),145 strip: srgb(0x27, 0x2a, 0x2b),
146 sidebar: srgb(0x27, 0x2a, 0x2b),
147 tool: [0.0; 4],
142 accent: srgb(0x00, 0x79, 0xa6),148 accent: srgb(0x00, 0x79, 0xa6),
143 caret,149 caret,
144 selection,150 selection,
...@@ -168,6 +174,8 @@ impl Theme {...@@ -168,6 +174,8 @@ impl Theme {
168 base: srgb(0xfc, 0xfc, 0xfd),174 base: srgb(0xfc, 0xfc, 0xfd),
169 panel: srgb(0xf4, 0xf5, 0xf7),175 panel: srgb(0xf4, 0xf5, 0xf7),
170 strip: srgb(0xeb, 0xed, 0xf0),176 strip: srgb(0xeb, 0xed, 0xf0),
177 sidebar: srgb(0xeb, 0xed, 0xf0),
178 tool: [0.0; 4],
171 accent: srgb(0x00, 0x79, 0xa6),179 accent: srgb(0x00, 0x79, 0xa6),
172 caret,180 caret,
173 selection,181 selection,
...@@ -196,6 +204,7 @@ impl Theme {...@@ -196,6 +204,7 @@ impl Theme {
196 let text = |alpha| [self.text[0], self.text[1], self.text[2], alpha];204 let text = |alpha| [self.text[0], self.text[1], self.text[2], alpha];
197 Self {205 Self {
198 strip: [0.0; 4],206 strip: [0.0; 4],
207 sidebar: [0.0; 4],
199 text_dim: text(0.5),208 text_dim: text(0.5),
200 chip: text(0.1),209 chip: text(0.1),
201 base: text(0.05),210 base: text(0.05),