| 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,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 row | 114 | 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 8 | 115 | 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 the | 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 | 117 | 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 and | 118 | 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 SMB | 123 | - 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 a | 124 | 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 denies | 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,8 +463,10 @@ fn local_access_stays_inside_the_selected_root() { |
| 463 | } | 463 | } |
| 464 | 464 | ||
| 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)] | ||
| 466 | struct Restore(Vec<(std::path::PathBuf, u32)>); | 467 | struct Restore(Vec<(std::path::PathBuf, u32)>); |
| 467 | 468 | ||
| 469 | #[cfg(unix)] | ||
| 468 | impl Drop for Restore { | 470 | impl 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 | } |
| 476 | 478 | ||
| 479 | #[cfg(unix)] | ||
| 477 | #[test] | 480 | #[test] |
| 478 | fn unreadable_children_are_listed_as_unavailable_and_retried() { | 481 | fn 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] { |
| 260 | 260 | ||
| 261 | #[test] | 261 | #[test] |
| 262 | fn a_page_delete_lists_the_recycle_bin_it_finds_or_makes_and_leaves_an_unreadable_one() { | 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 | 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 | } |
| 1068 | 1066 | ||
| 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 | } |
| 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 | /// Menus in `appearance` as the desktop draws its own: libadwaita's popover menus on GNOME | 261 | /// 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. |
| 258 | pub fn menu(appearance: Theme) -> Option<ui::Menu> { | 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,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 | /// None: the kit's own menus stand in for AppKit's. | 254 | /// None: the kit's own menus stand in for AppKit's. |
| 250 | pub fn menu(_: winit::window::Theme) -> Option<ui::Menu> { | 255 | pub fn menu(_: winit::window::Theme) -> Option<ui::Menu> { |
| 251 | None | 256 | 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 | } |
| 1123 | 1127 | ||
| 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 | theme | 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,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 | } |
| 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 | type Procedure<'a> = &'a dyn Fn(u32, WPARAM, LPARAM) -> LRESULT; | 288 | type Procedure<'a> = &'a dyn Fn(u32, WPARAM, LPARAM) -> LRESULT; |
| 256 | 289 | ||
| 257 | /// The frame over glass or Mica: the system's frame keeps its sides and bottom, the caption | 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,7 +416,7 @@ fn drawn_frame( |
| 383 | None | 416 | 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 | None | 420 | 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 | false | 442 | false |
| 410 | } | 443 | } |
| 411 | 444 | ||
| 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. | ||
| 414 | static ACRYLIC: AtomicBool = AtomicBool::new(false); | 446 | static ACRYLIC: AtomicBool = AtomicBool::new(false); |
| 415 | static ACRYLIC_TINT: AtomicU32 = AtomicU32::new(0); | ||
| 416 | 447 | ||
| 417 | enum Accent { | 448 | enum 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 | } |
| 422 | 453 | ||
| 423 | /// Windows 10's accent under the window, through the undocumented | 454 | /// 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 { |
| 466 | 502 | ||
| 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. |
| 468 | fn transparency() -> bool { | 504 | fn 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 | } |
| 474 | 507 | ||
| 508 | const 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 transparent | 510 | /// 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 (blur | 511 | /// 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 | shown | 544 | 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 | } |
| 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 | /// None: the kit's own menus. | 594 | /// None: the kit's own menus. |
| 547 | pub fn menu(_: Theme) -> Option<ui::Menu> { | 595 | pub fn menu(_: Theme) -> Option<ui::Menu> { |
| 548 | None | 596 | 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 | } |
| 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 | /// 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. |
| 287 | fn press_state(ui: &mut Ui, id: Id, on: Option<bool>) { | 292 | fn press_state(ui: &mut Ui, id: Id, on: Option<bool>) { |
| 288 | if let Some(on) = on | 293 | 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's | 11 | /// 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; the | 19 | /// 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), |