Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/panel-sizing.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,10 @@ $env:SWITCHIFY_PANEL_FIXTURES = Join-Path $PWD '.cache/panel-fixtures'
cargo test --manifest-path src-tauri/Cargo.toml export_panel_size_fixtures -- --ignored --nocapture
```

Each panel is exported twice: dark as `name-Size.png` and light as `name-Size-light.png`, so the Panel theme palettes can be checked side by side.

```powershell
# Example: .cache/panel-fixtures/mouse-Medium.png and mouse-Medium-light.png
```

Windows Small and Medium mouse and Medium keyboard fixtures were visually checked for labels, spacing, direction alignment and reserved prediction slots. macOS visual inspection is not available on this Windows machine; cross-platform compilation and tests are covered by CI. This change does not modify platform focus or click-through behavior.
1 change: 1 addition & 0 deletions site/src/content/docs/using/scanning-settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Choosing **Back** in a menu opens the menu above it afresh, scanning from the be
- **Scanner colour**: red, green, blue, yellow or white.
- **Highlight thickness**: thin, standard or thick.
- **Panel size**: small, medium or large.
- **Panel theme**: match system, light or dark. This colours the keyboard, the mouse panel, Home and the action menus. **Match system** follows your computer's light or dark setting, and the panels change when it does. On light panels the scanner colour is drawn in a deeper shade so the highlight stays easy to see, and white becomes dark grey.
- Panels can move away from the pointer.

## Different settings for menus and the keyboard
Expand Down
14 changes: 14 additions & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ mod modifier_overlay;
mod mouse_repeat;
mod overlay;
mod panel_position;
mod panel_theme;
mod point_scan;
mod point_scan_activation;
mod point_scan_runtime;
Expand Down Expand Up @@ -1896,6 +1897,12 @@ pub fn run() {
.telemetry
.start(&state.version, &state.capabilities.platform);
}
if let Some(theme) = app
.get_webview_window("main")
.and_then(|window| window.theme().ok())
{
panel_theme::set_system_light(theme == tauri::Theme::Light);
}
app.manage(overlay::CursorOverlay::install(
app.handle().clone(),
overlay_shared.clone(),
Expand Down Expand Up @@ -1951,6 +1958,13 @@ pub fn run() {
Ok(())
})
.on_window_event(|window, event| {
// The panels follow the computer's light or dark setting through
// the main window, which reports the change even while hidden.
if let tauri::WindowEvent::ThemeChanged(theme) = event {
if window.label() == "main" {
panel_theme::set_system_light(*theme == tauri::Theme::Light);
}
}
if window.label() == "main" && matches!(event, tauri::WindowEvent::Focused(false)) {
window
.app_handle()
Expand Down
42 changes: 30 additions & 12 deletions src-tauri/src/modifier_overlay_windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,7 @@ fn present(window: HWND, labels: &[String], layout: &Layout) -> Result<(), Strin
pixmap.data(),
labels.iter().any(|label| label.contains('\n')),
false,
[255, 255, 255],
)
}

Expand Down Expand Up @@ -474,6 +475,7 @@ fn present_pixmap_with_text(
rgba: &[u8],
wrap: bool,
center_wrapped: bool,
text_rgb: [u8; 3],
) -> Result<(), String> {
unsafe {
let screen = ScreenDc(GetDC(None));
Expand Down Expand Up @@ -540,7 +542,12 @@ fn present_pixmap_with_text(
previous: SelectObject(memory.0, font.0),
};
SetBkMode(memory.0, TRANSPARENT);
SetTextColor(memory.0, COLORREF(0x00ff_ffff));
SetTextColor(
memory.0,
COLORREF(
u32::from(text_rgb[0]) | u32::from(text_rgb[1]) << 8 | u32::from(text_rgb[2]) << 16,
),
);
for (label, chip) in labels.iter().zip(&layout.chips) {
if label.is_empty() {
continue;
Expand Down Expand Up @@ -572,16 +579,19 @@ fn present_pixmap_with_text(
);
}

// GDI writes text with alpha 0. The layered window composites
// premultiplied alpha, so such a pixel adds its colour to whatever is
// behind the panel: white text happened to stay white, but dark text
// on a light panel would vanish over a light window. Text only lands
// on opaque key faces, so the source alpha is the right alpha.
for (pixel, source) in output.chunks_exact_mut(4).zip(rgba.chunks_exact(4)) {
pixel[3] = source[3];
}

#[cfg(test)]
if PANEL_FIXTURE.with(|fixture| fixture.borrow().is_some()) {
let mut pixels = vec![0; output.len()];
copy_rgba_to_bgra(output, &mut pixels)?;
// GDI clears alpha on text pixels; the panel underneath is opaque.
for pixel in pixels.chunks_exact_mut(4) {
if pixel[0] != 0 || pixel[1] != 0 || pixel[2] != 0 {
pixel[3] = 255;
}
}
let tile = Pixmap::from_vec(
pixels,
tiny_skia::IntSize::from_wh(layout.width as u32, layout.height as u32).unwrap(),
Expand Down Expand Up @@ -673,6 +683,7 @@ pub(crate) fn present_scan_tile(
pixmap.data(),
true,
true,
crate::scan_tile::text_rgb(tile),
)
}

Expand Down Expand Up @@ -728,6 +739,7 @@ mod tests {
&pixels,
wrapped,
wrapped,
[255, 255, 255],
);
assert!(result.is_err());
}
Expand Down Expand Up @@ -888,7 +900,7 @@ mod tests {
fn export_panel_size_fixtures() {
use crate::{
scan_panel::Dock,
scan_preferences::{PanelSize, Resolved},
scan_preferences::{PanelSize, PanelTheme, Resolved},
scanning::{Rect, ScannerColor},
};
let directory =
Expand All @@ -900,10 +912,14 @@ mod tests {
width: 1920.0,
height: 1080.0,
};
for size in [PanelSize::Small, PanelSize::Medium, PanelSize::Large] {
let presets = [PanelSize::Small, PanelSize::Medium, PanelSize::Large]
.into_iter()
.flat_map(|size| [(size, PanelTheme::Dark), (size, PanelTheme::Light)]);
for (size, theme) in presets {
let mut mouse = crate::scan_mouse::MousePanel::new(
Resolved {
panel_size: size,
panel_theme: theme,
..Default::default()
},
2,
Expand All @@ -913,6 +929,7 @@ mod tests {
let mut keyboard = crate::scan_keyboard::Keyboard::new(false);
keyboard.dock = mouse.dock;
keyboard.set_panel_size(size);
keyboard.set_panel_theme(theme);
keyboard.enable_predictions(true);
let keyboard_frame = keyboard.frame(screen, 1.0, ScannerColor::default());
let mut page = |page| {
Expand Down Expand Up @@ -951,9 +968,10 @@ mod tests {
.borrow_mut()
.take()
.unwrap()
.save_png(
std::path::Path::new(&directory).join(format!("{name}-{size:?}.png")),
)
.save_png(std::path::Path::new(&directory).join(match theme {
PanelTheme::Light => format!("{name}-{size:?}-light.png"),
_ => format!("{name}-{size:?}.png"),
}))
.unwrap()
});
}
Expand Down
158 changes: 158 additions & 0 deletions src-tauri/src/panel_theme.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
//! Light and dark colours for the native panels: the keyboard, the mouse
//! panel, Home and the action menus. Point scanning lines, pointer markers
//! and the floating prompt keep their own colours.
use crate::scanning::ScannerColor;
use serde::{Deserialize, Serialize};
use std::sync::atomic::{AtomicBool, Ordering};

/// Whether the computer is using a light appearance. Set from the main
/// window's theme at startup and whenever the system setting changes;
/// read when a panel frame is built, so panels follow the change on their
/// next tick without being restarted.
static SYSTEM_LIGHT: AtomicBool = AtomicBool::new(false);

pub fn set_system_light(light: bool) {
SYSTEM_LIGHT.store(light, Ordering::Relaxed);
}
pub fn system_light() -> bool {
SYSTEM_LIGHT.load(Ordering::Relaxed)
}

/// The saved choice. `System` follows the computer's light or dark setting.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum PanelTheme {
#[default]
System,
Light,
Dark,
}
impl PanelTheme {
pub fn appearance(self) -> Appearance {
self.appearance_when(system_light())
}
/// Pure, so tests never have to touch the shared system flag.
pub fn appearance_when(self, system_light: bool) -> Appearance {
match self {
Self::Light => Appearance::Light,
Self::Dark => Appearance::Dark,
Self::System if system_light => Appearance::Light,
Self::System => Appearance::Dark,
}
}
}

/// What a panel is drawn with once the system setting has been consulted.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum Appearance {
#[default]
Dark,
Light,
}
impl Appearance {
pub fn palette(self) -> Palette {
match self {
Self::Dark => Palette {
panel: [20, 24, 32],
band: [30, 36, 48],
character: [43, 51, 66],
utility: [34, 41, 54],
menu_bar: [52, 61, 79],
other: [25, 30, 40],
border: [62, 72, 89],
band_border: [74, 86, 108],
text: [255, 255, 255],
highlight_base: [30, 35, 46],
},
Self::Light => Palette {
panel: [240, 242, 246],
band: [226, 230, 237],
character: [255, 255, 255],
utility: [236, 239, 244],
menu_bar: [214, 219, 228],
other: [244, 246, 249],
border: [196, 203, 214],
band_border: [176, 185, 200],
text: [20, 24, 32],
highlight_base: [255, 255, 255],
},
}
}
/// The scanner colour as drawn on panels of this appearance. Light
/// panels use a deeper shade so every choice stays visible; white
/// becomes dark grey.
pub fn highlight(self, color: ScannerColor) -> [u8; 3] {
match self {
Self::Dark => color.rgb(),
Self::Light => match color {
ScannerColor::Red => [198, 40, 40],
ScannerColor::Green => [27, 140, 60],
ScannerColor::Blue => [25, 103, 210],
ScannerColor::Yellow => [176, 112, 0],
ScannerColor::White => [58, 64, 76],
},
}
}
/// The fill of a highlighted key: the panel's base tinted with the highlight.
pub fn highlight_fill(self, color: ScannerColor) -> [u8; 3] {
let base = self.palette().highlight_base.map(u16::from);
let highlight = self.highlight(color).map(u16::from);
std::array::from_fn(|i| ((base[i] * 3 + highlight[i]) / 4) as u8)
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Palette {
pub panel: [u8; 3],
/// The menu bar band along the top of every panel.
pub band: [u8; 3],
pub character: [u8; 3],
pub utility: [u8; 3],
pub menu_bar: [u8; 3],
pub other: [u8; 3],
pub border: [u8; 3],
pub band_border: [u8; 3],
pub text: [u8; 3],
highlight_base: [u8; 3],
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn dark_highlights_are_the_scanner_colours_and_light_ones_are_deeper() {
for color in [
ScannerColor::Red,
ScannerColor::Green,
ScannerColor::Blue,
ScannerColor::Yellow,
ScannerColor::White,
] {
assert_eq!(Appearance::Dark.highlight(color), color.rgb());
let [r, g, b] = color.rgb().map(u16::from);
assert_eq!(
Appearance::Dark.highlight_fill(color),
[(90 + r) / 4, (105 + g) / 4, (138 + b) / 4].map(|c| c as u8)
);
let light = Appearance::Light.highlight(color);
let luma =
|[r, g, b]: [u8; 3]| u32::from(r) * 299 + u32::from(g) * 587 + u32::from(b) * 114;
// Visible against a white key and darker than the dark-panel shade.
assert!(luma(light) < luma([200, 200, 200]), "{color:?}");
assert!(luma(light) < luma(color.rgb()), "{color:?}");
assert_ne!(Appearance::Light.highlight_fill(color), [255, 255, 255]);
}
}

#[test]
fn the_chosen_theme_wins_and_system_follows_the_computer() {
assert_eq!(PanelTheme::Light.appearance(), Appearance::Light);
assert_eq!(PanelTheme::Dark.appearance(), Appearance::Dark);
assert_eq!(PanelTheme::default(), PanelTheme::System);
assert_eq!(PanelTheme::System.appearance_when(true), Appearance::Light);
assert_eq!(PanelTheme::System.appearance_when(false), Appearance::Dark);
assert_eq!(PanelTheme::Dark.appearance_when(true), Appearance::Dark);
assert_eq!(PanelTheme::Light.appearance_when(false), Appearance::Light);
}
}
6 changes: 6 additions & 0 deletions src-tauri/src/point_workflow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,12 @@ impl Workflow {
.set_panel_size(self.point.config.mouse_scan.panel_size);
self.menu
.set_panel_size(self.point.config.menu_scan.panel_size);
self.keyboard
.set_panel_theme(self.point.config.keyboard_scan.panel_theme);
self.mouse
.set_panel_theme(self.point.config.mouse_scan.panel_theme);
self.menu
.set_panel_theme(self.point.config.menu_scan.panel_theme);

self.keyboard.set_layout(self.point.config.keyboard_layout);
self.menu
Expand Down
Loading
Loading