#[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum Page { Appearance, General, Theme, Tray, Configuration, Accessibility, Wallpaper, Bar, Notifications, LockScreen, Windows, Motion, Keyboard, Shortcuts, Startup, Displays, Connections, Calendar, Mouse, Touchpad, Shell, } impl Page { pub const NAVIGATION: [(&'static str, &'static [Self]); 5] = [ ("Appearance", &[Self::Appearance, Self::General]), ( "Desktop", &[Self::Theme, Self::Wallpaper, Self::Bar, Self::Tray], ), ( "Interaction", &[ Self::Windows, Self::Keyboard, Self::Mouse, Self::Touchpad, Self::Shortcuts, Self::Accessibility, ], ), ( "System", &[ Self::Displays, Self::Connections, Self::Notifications, Self::Calendar, Self::LockScreen, Self::Startup, ], ), ("Advanced", &[Self::Configuration]), ]; /// Subpages share one entry in navigation and search. pub fn navigation_page(self) -> Self { match self { Self::Motion => Self::Windows, Self::Shell => Self::Bar, _ => self, } } pub fn subpages(self) -> &'static [(Self, &'static str)] { match self.navigation_page() { Self::Windows => &[(Self::Windows, "Windows"), (Self::Motion, "Motion")], Self::Bar => &[(Self::Bar, "Panel"), (Self::Shell, "Shell")], _ => &[], } } pub fn search_destination(self, query: &str) -> Self { if !self.matches_own(query) { for &(page, _) in self.subpages() { if page.matches_own(query) { return page; } } } self } #[cfg(test)] pub const ALL: [Self; 21] = [ Self::Appearance, Self::General, Self::Theme, Self::Tray, Self::Configuration, Self::Accessibility, Self::Wallpaper, Self::Bar, Self::Notifications, Self::LockScreen, Self::Windows, Self::Motion, Self::Keyboard, Self::Shortcuts, Self::Startup, Self::Displays, Self::Connections, Self::Calendar, Self::Mouse, Self::Touchpad, Self::Shell, ]; pub fn title(self) -> &'static str { ferese_config::locale::text(self.title_key()) } fn title_key(self) -> &'static str { match self { Self::Appearance => "Appearance", Self::General => "General", Self::Tray => "Tray icons", Self::Configuration => "All options", Self::Theme => "Theme", Self::Accessibility => "Accessibility", Self::Wallpaper => "Wallpaper", Self::Bar => "Panel & Shell", Self::Notifications => "Notifications", Self::LockScreen => "Lock screen", Self::Windows => "Windows", Self::Motion => "Motion", Self::Keyboard => "Keyboard", Self::Mouse => "Mouse", Self::Touchpad => "Touchpad", Self::Shell => "Shell", Self::Shortcuts => "Shortcuts", Self::Startup => "Login items", Self::Displays => "Displays", Self::Connections => "Connections", Self::Calendar => "Calendar", } } pub fn subtitle(self) -> &'static str { ferese_config::locale::text(match self { Self::Appearance => "All Ferese settings, grouped by function.", Self::General => "Language and desktop preferences.", Self::Tray => "Choose which status and application icons appear in the panel.", Self::Configuration => { "Every supported configuration option, including advanced settings." } Self::Theme => "A desktop that feels like yours.", Self::Accessibility => "Make the desktop easier to see and use.", Self::Wallpaper => "Set the scene for your workspace.", Self::Bar => "Arrange your panel and change the shell's appearance.", Self::Notifications => "Stay informed on your terms.", Self::LockScreen => "Your desktop, safely put away.", Self::Windows => "Window layouts, spacing and focus effects.", Self::Motion => "Animations and transition speed.", Self::Keyboard => "Keyboard layouts, variants and repeat behavior.", Self::Mouse => "Mouse buttons, pointer movement and scrolling.", Self::Touchpad => "Touchpad taps, scrolling and gestures.", Self::Shell => "Shared surfaces, launcher and system menus.", Self::Shortcuts => "Keyboard shortcuts and swipe actions.", Self::Startup => "Ready when you sign in.", Self::Displays => "A place for every screen.", Self::Connections => "Wi-Fi networks and Bluetooth devices.", Self::Calendar => "Calendar, Japanese holiday rules and custom holidays.", }) } pub fn icon(self) -> &'static str { match self { Self::Accessibility => { "M12 3a2 2 0 1 0 0 4a2 2 0 1 0 0-4 M4 9h16 M12 9v6 M12 15l-5 6 M12 15l5 6" } Self::Appearance | Self::General | Self::Configuration => { "M4 5h16 M4 12h16 M4 19h16 M8 3v4 M16 10v4 M10 17v4" } Self::Tray => "M3 5h18v14H3z M6 9h3v3H6z M12 9h3v3h-3z M18 9h.01", Self::Theme => { "M12 3a9 9 0 1 0 0 18h1a2 2 0 0 0 0-4h-1a1 1 0 0 1 0-2h3a6 6 0 0 0 0-12z M7 9h.01 M10 6h.01 M15 6h.01 M18 10h.01" } Self::Wallpaper => "M4 4h16v16H4z M4 16l5-5 4 4 3-3 4 4 M15 8h.01", Self::Bar => "M3 5h18v14H3z M3 9h18 M6 7h.01 M18 7h.01", Self::Notifications => "M6 8a6 6 0 0 1 12 0v6l2 3H4l2-3z M10 21h4", Self::Calendar => { "M4 5h16v16H4z M8 3v4 M16 3v4 M4 10h16 M8 14h.01 M12 14h.01 M16 14h.01 M8 18h.01 M12 18h.01" } Self::LockScreen => "M7 10V7a5 5 0 0 1 10 0v3 M5 10h14v11H5z M12 14v3", Self::Windows => "M3 4h12v12H3z M8 9h13v12H8z", Self::Motion => "M3 8h9a3 3 0 1 0-3-3 M3 12h15a3 3 0 1 1-3 3 M3 16h5", Self::Keyboard => { "M3 6h18v12H3z M6 9h.01 M10 9h.01 M14 9h.01 M18 9h.01 M6 12h.01 M10 12h.01 M14 12h.01 M18 12h.01 M8 15h8" } Self::Mouse => "M8 10V7a4 4 0 0 1 8 0v10a4 4 0 0 1-8 0z M12 3v7 M8 10h8", Self::Touchpad => "M3 5h18v14H3z M3 15h18 M12 15v4", Self::Shell => "M3 4h18v16H3z M3 9h18 M8 9v11", Self::Shortcuts => { "M8 8h8v8H8z M8 8H5a3 3 0 1 1 3-3v3z M16 8V5a3 3 0 1 1 3 3h-3z M16 16h3a3 3 0 1 1-3 3v-3z M8 16v3a3 3 0 1 1-3-3h3z" } Self::Startup => "M12 3v9 M7 5a9 9 0 1 0 10 0", Self::Displays => "M3 4h18v13H3z M12 17v4 M8 21h8", Self::Connections => { "M2 8a16 16 0 0 1 20 0 M5 12a11 11 0 0 1 14 0 M8 16a6 6 0 0 1 8 0 M12 20h.01" } } } pub fn matches(self, query: &str) -> bool { self.matches_own(query) || self .subpages() .iter() .any(|(page, _)| page.matches_own(query)) } fn matches_own(self, query: &str) -> bool { let query = query.to_lowercase(); self.title().to_lowercase().contains(&query) || self.title_key().to_lowercase().contains(&query) || ferese_config::locale::in_language( ferese_config::locale::Language::Chinese, self.title_key(), ) .to_lowercase() .contains(&query) || self.subtitle().to_lowercase().contains(&query) || fields(self).iter().any(|f| { format!( "{} {} {} {} {}", f.label, f.description, f.path, ferese_config::locale::in_language( ferese_config::locale::Language::Chinese, &f.label ), ferese_config::locale::in_language( ferese_config::locale::Language::Chinese, &f.description ) ) .to_lowercase() .contains(&query) }) } } #[derive(Clone, Debug)] pub enum Kind { Toggle(bool), Range { default: f64, min: f64, max: f64, step: f64, suffix: &'static str, integer: bool, }, Choice { default: &'static str, choices: &'static [(&'static str, &'static str)], }, Text { default: &'static str, argv: bool, }, } #[derive(Clone, Debug)] pub struct Field { pub path: String, pub label: String, pub description: String, pub kind: Kind, } impl Field { pub fn new( path: impl Into, label: impl Into, description: impl Into, kind: Kind, ) -> Self { Self { path: path.into(), label: label.into(), description: description.into(), kind, } } } fn toggle(path: &str, label: &str, description: &str, default: bool) -> Field { Field::new(path, label, description, Kind::Toggle(default)) } #[derive(Clone, Copy, Debug)] pub struct RangeSpec { pub default: f64, pub min: f64, pub max: f64, pub step: f64, pub suffix: &'static str, pub integer: bool, } pub fn range( path: impl Into, label: impl Into, description: impl Into, spec: RangeSpec, ) -> Field { let RangeSpec { default, min, max, step, suffix, integer, } = spec; Field::new( path, label, description, Kind::Range { default, min, max, step, suffix, integer, }, ) } pub fn text( path: impl Into, label: impl Into, description: impl Into, default: &'static str, ) -> Field { Field::new( path, label, description, Kind::Text { default, argv: false, }, ) } pub fn choice( path: &str, label: &str, description: &str, default: &'static str, choices: &'static [(&'static str, &'static str)], ) -> Field { Field::new(path, label, description, Kind::Choice { default, choices }) } pub fn fields(page: Page) -> Vec { match page { Page::LockScreen => vec![ range( "lock_screen.dim_after_seconds", "Dim after", "Dim the locked display after inactivity. Zero disables dimming.", RangeSpec { default: 30.0, min: 0.0, max: 600.0, step: 5.0, suffix: "s", integer: true, }, ), range( "lock_screen.sleep_after_seconds", "Display sleep after", "Turn off locked displays after inactivity. Input wakes them; zero disables display sleep.", RangeSpec { default: 120.0, min: 0.0, max: 1800.0, step: 15.0, suffix: "s", integer: true, }, ), toggle( "lock_screen.show_clock", "Show clock", "Display the time above the sign-in card.", true, ), toggle( "lock_screen.show_date", "Show date", "Display the weekday and date.", true, ), choice( "lock_screen.clock_format", "Clock format", "Use a 12-hour or 24-hour clock.", "24h", &[("24h", "24-hour"), ("12h", "12-hour")], ), range( "lock_screen.background_blur", "Wallpaper softness", "Blur the lock screen wallpaper. Zero keeps the original image sharp.", RangeSpec { default: 18.0, min: 0.0, max: 40.0, step: 1.0, suffix: "", integer: false, }, ), range( "lock_screen.background_dim", "Wallpaper dimming", "Darken the wallpaper behind the clock and sign-in card.", RangeSpec { default: 0.48, min: 0.0, max: 0.9, step: 0.02, suffix: "", integer: false, }, ), ], Page::Accessibility => vec![ toggle( "theme.accessibility.increase_contrast", "Increase contrast", "Stronger text and borders.", false, ), toggle( "theme.accessibility.reduce_transparency", "Reduce transparency", "Solid surfaces; background blur is disabled.", false, ), toggle( "animations.reduced_motion", "Reduce motion", "Use immediate changes in place of animations.", false, ), ], Page::Theme => vec![ choice( "theme.mode", "Appearance", "Choose a fixed appearance or let Auto follow a source.", "dark", &[("light", "Light"), ("dark", "Dark"), ("auto", "Auto")], ), text( "theme.file", "Shared theme file", "Optional KDL overrides. Relative paths start in your config folder.", "", ), text( "theme.light.file", "Light theme file", "Optional overrides for Light appearance.", "", ), text( "theme.dark.file", "Dark theme file", "Optional overrides for Dark appearance.", "", ), text( "theme.schedule.light_at", "Light at", "Local time in HH:MM format.", "07:00", ), text( "theme.schedule.dark_at", "Dark at", "Local time in HH:MM format.", "19:00", ), text( "theme.schedule.timezone", "Timezone", "system follows your desktop timezone; or enter an IANA timezone.", "system", ), text( "theme.accent", "Accent color", "Focus rings and selected controls. Use a hex color.", "#3D7BE6", ), choice( "theme.focus_ring.style", "Accent style", "Window focus borders and selected controls. Gradients use the theme colors unless overridden.", "auto", &[("auto", "Gradient"), ("solid", "Solid")], ), range( "theme.material.blur_radius", "Background blur", "Softness behind translucent shell surfaces and windows. Zero disables blur.", RangeSpec { default: 12.0, min: 0.0, max: 32.0, step: 1.0, suffix: " px", integer: false, }, ), text( "theme.typography.font_family", "Interface font", "Use the name of an installed font family.", "Inter", ), ], Page::Wallpaper => vec![ text( "theme.background.path", "Image location", "Choose an image above, or enter its full path.", ferese_config::default_wallpaper(), ), choice( "theme.background.mode", "Image placement", "Fill crops to the screen. Fit keeps the whole image.", "fill", &[("fill", "Fill"), ("fit", "Fit")], ), ], Page::Notifications => vec![ toggle( "notifications.show_popups", "Show popup notifications", "Keep notifications in history when popups are hidden.", true, ), toggle( "notifications.do_not_disturb", "Do Not Disturb", "Silence normal popups; critical notifications can still appear.", false, ), range( "notifications.timeout_ms", "Popup timeout", "Default duration; apps can request another timeout. Pauses while hovered.", RangeSpec { default: 6000.0, min: 1000.0, max: 30000.0, step: 1000.0, suffix: " ms", integer: true, }, ), ], Page::Shell => vec![ range( "theme.geometry.shell_radius", "Shell corner radius", "Rounds the bar, menus, notifications and hover backgrounds. Window corners are separate.", RangeSpec { default: 14.0, min: 0.0, max: 32.0, step: 1.0, suffix: " px", integer: false, }, ), choice( "theme.material.style", "Surface style", "Choose the finish for the panel, launcher, menus and notifications.", "solid", &[("solid", "Solid"), ("translucent", "Translucent")], ), range( "theme.material.opacity", "Shell opacity", "Background opacity for the bar, popovers, menus, notifications and sharing dialog. Text and icons stay opaque.", RangeSpec { default: ferese_config::DEFAULT_MATERIAL_OPACITY, min: 0.0, max: 1.0, step: 0.01, suffix: "", integer: false, }, ), ], Page::Bar => vec![ range( "panels.0.geometry.height", "Height", "Keeps icon and text sizes unchanged.", RangeSpec { default: 36.0, min: 24.0, max: 48.0, step: 1.0, suffix: " px", integer: false, }, ), range( "panels.0.geometry.edge_margin", "Edge margin", "Space from the selected screen edge. Set to zero for a flush panel.", RangeSpec { default: 0.0, min: 0.0, max: 24.0, step: 1.0, suffix: " px", integer: true, }, ), range( "panels.0.geometry.side_margins", "Side margins", "Inset from both edges; stops before extra controls overflow.", RangeSpec { default: 0.0, min: 0.0, max: 4096.0, step: 1.0, suffix: " px", integer: true, }, ), range( "panels.0.geometry.window_clearance", "Window clearance", "Space between the panel and application windows.", RangeSpec { default: 0.0, min: 0.0, max: 24.0, step: 1.0, suffix: " px", integer: true, }, ), ], Page::Windows => vec![ toggle( "workspaces.auto_back_and_forth", "Toggle back with the same workspace shortcut", "Press the current workspace's shortcut again to return to the previous workspace on this monitor.", false, ), choice( "layout.mode", "Layout for new workspaces", "Existing workspaces keep their current layout.", "scrolling", &[("scrolling", "Scrolling"), ("tree", "Tiling")], ), choice( "scrolling.focus_strategy", "Scroll behavior", "How the viewport follows the focused column.", "minimal", &[ ("minimal", "Minimal"), ("center_on_focus", "Centered"), ("paged", "Paged"), ], ), range( "scrolling.default_column_width", "New window width", "Fraction of the available workspace.", RangeSpec { default: 0.5, min: 0.25, max: 1.0, step: 0.05, suffix: "", integer: false, }, ), range( "layout.inner_gap", "Between windows", "Space between neighboring windows.", RangeSpec { default: 10.0, min: 0.0, max: 32.0, step: 1.0, suffix: " px", integer: false, }, ), range( "layout.outer_gap", "Workspace edges", "Space around the outside of the layout.", RangeSpec { default: 4.0, min: 0.0, max: 32.0, step: 1.0, suffix: " px", integer: false, }, ), range( "theme.geometry.window_radius", "Corner radius", "Rounded corners for application windows.", RangeSpec { default: 14.0, min: 0.0, max: 28.0, step: 1.0, suffix: " px", integer: false, }, ), choice( "theme.border.style", "Inactive border style", "Gradients retain the border color's transparency.", "auto", &[("auto", "Gradient"), ("solid", "Solid")], ), range( "theme.geometry.focus_ring_width", "Focus border", "A subtle outline around the active window.", RangeSpec { default: 2.0, min: 0.0, max: 4.0, step: 0.5, suffix: " px", integer: false, }, ), toggle( "input.focus_follows_mouse", "Focus follows pointer", "Focus visible windows without raising or scrolling. Click to bring them into view.", false, ), toggle( "appearance.focus_effect.enabled", "Window focus effects", "Apply window opacity and inactive dimming. Turning this off keeps your settings.", true, ), range( "appearance.focus_effect.active_opacity", "Active window opacity", "Opacity of the active application, including fullscreen windows.", RangeSpec { default: 1.0, min: 0.0, max: 1.0, step: 0.01, suffix: "", integer: false, }, ), range( "appearance.focus_effect.inactive_opacity", "Inactive window opacity", "Opacity of other application windows.", RangeSpec { default: 1.0, min: 0.0, max: 1.0, step: 0.01, suffix: "", integer: false, }, ), toggle( "appearance.focus_effect.blur", "Window blur", "Soften the background behind translucent windows. Adjust the amount in Appearance.", true, ), range( "appearance.focus_effect.inactive_dim", "Inactive window dimming", "Darken inactive windows. Zero disables dimming.", RangeSpec { default: 0.0, min: 0.0, max: 1.0, step: 0.01, suffix: "", integer: false, }, ), ], Page::Motion => vec![ toggle( "animations.enabled", "Animations", "Animate window changes and shell transitions.", true, ), range( "animations.speed", "Animation speed", "Lower values give transitions more time.", RangeSpec { default: 1.0, min: 0.25, max: 2.0, step: 0.05, suffix: "×", integer: false, }, ), range( "appearance.focus_effect.duration_ms", "Focus transition", "Duration of opacity and dimming changes when the active window changes.", RangeSpec { default: 150.0, min: 0.0, max: 600.0, step: 10.0, suffix: " ms", integer: false, }, ), ], Page::Keyboard => vec![ text( "input.xkb_layout", "Keyboard layouts", "Layout codes, separated by commas. For example: us,ru.", "us", ), text( "input.xkb_variant", "Layout variant", "Leave empty to use the standard variant.", "", ), range( "input.repeat_rate", "Key repeat", "Characters repeated each second while a key is held.", RangeSpec { default: 25.0, min: 1.0, max: 70.0, step: 1.0, suffix: " /s", integer: true, }, ), range( "input.repeat_delay_ms", "Delay before repeat", "How long to hold a key before it starts repeating.", RangeSpec { default: 600.0, min: 150.0, max: 1000.0, step: 25.0, suffix: " ms", integer: true, }, ), ], Page::Mouse => vec![ toggle( "input.mouse.natural_scroll", "Mouse natural scrolling", "Reverse the direction of the mouse wheel.", false, ), toggle( "input.mouse.left_handed", "Left-handed mouse", "Swap the primary and secondary mouse buttons.", false, ), toggle( "input.mouse.middle_emulation", "Emulate middle click", "Press both mouse buttons together to middle-click.", false, ), range( "input.mouse.accel_speed", "Mouse speed", "Adjust pointer acceleration for connected mice.", RangeSpec { default: 0.0, min: -1.0, max: 1.0, step: 0.05, suffix: "", integer: false, }, ), range( "input.mouse.scroll_factor", "Mouse scroll speed", "Scale wheel scrolling in applications.", RangeSpec { default: 1.0, min: 0.1, max: 5.0, step: 0.1, suffix: "×", integer: false, }, ), ], Page::Touchpad => vec![ toggle( "input.touchpad.tap", "Tap to click", "Tap the touchpad to click. Updates connected devices immediately.", true, ), toggle( "input.touchpad.natural_scroll", "Natural scrolling", "Move content in the direction of your fingers. Updates immediately.", true, ), toggle( "input.touchpad.disable_while_typing", "Ignore touchpad while typing", "Avoid accidental pointer movement.", true, ), toggle( "input.touchpad.disable_while_mouse_connected", "Disable touchpad with a mouse", "Pause the built-in touchpad while an external mouse is connected.", false, ), range( "input.touchpad.accel_speed", "Touchpad speed", "Adjust pointer acceleration for the touchpad.", RangeSpec { default: 0.0, min: -1.0, max: 1.0, step: 0.05, suffix: "", integer: false, }, ), range( "input.touchpad.swipe_threshold", "Swipe distance", "Distance needed to navigate. Larger values reduce accidental swipes.", RangeSpec { default: 80.0, min: 16.0, max: 1000.0, step: 8.0, suffix: " px", integer: true, }, ), ], Page::Shortcuts => vec![toggle( "status.keybinding_guide", "Show keybinding guide at login", "Keep the shortcut guide until you are comfortable with Ferese.", true, )], Page::General => vec![choice( "ui.language", "Language", "Changes the Ferese interface language. Applies immediately.", "auto", &[ ("auto", "Follow system"), ("zh-CN", "简体中文"), ("en", "English"), ], )], Page::Calendar => vec![toggle( "status.calendar.show_adjacent_days", "Show dates from adjacent months", "Display the previous and next month's dates in the calendar.", true, )], Page::Tray => vec![ toggle( "status.tray.enabled", "Show tray icons", "Show application icons in panel tray items.", true, ), toggle( "status.tray.show_passive", "Show passive icons", "Include applications without active status.", false, ), range( "status.tray.icon_size", "Tray icon size", "Size of application tray icons.", RangeSpec { default: 16., min: 8., max: 48., step: 1., suffix: " px", integer: true, }, ), Field::new( "status.tray.hidden_items", "Hidden applications", "Application IDs to hide, separated by spaces.", Kind::Text { default: "", argv: true, }, ), ], Page::Appearance | Page::Configuration | Page::Startup | Page::Displays | Page::Connections => vec![], } } pub struct FieldGroup { pub title: &'static str, pub fields: Vec, } /// Semantic groups, preserving every field and the declaration order in each group. pub fn grouped_fields(page: Page, fields: Vec) -> Vec { let mut groups: Vec = Vec::new(); for field in fields { let p = field.path.as_str(); let title = match page { Page::General => "Interface", Page::Calendar => "Calendar display", Page::Windows => { if p.starts_with("workspaces.") || p == "layout.mode" || p.starts_with("scrolling.") { "Workspaces and layout" } else if p == "layout.inner_gap" || p == "layout.outer_gap" || p == "theme.geometry.window_radius" { "Spacing and corners" } else { "Focus and window effects" } } Page::Shell => "Shell surfaces", Page::Mouse => "Mouse", Page::Touchpad => "Touchpad", Page::Keyboard => { if p.starts_with("input.xkb_") { "Keyboard layout" } else if p.starts_with("input.repeat_") { "Keyboard repeat" } else if p.starts_with("input.mouse.") { "Mouse" } else { "Touchpad" } } Page::LockScreen => { if p.starts_with("lock_screen.background_") { "Lock background" } else if p.contains("after_seconds") { "Display power" } else { "Clock and date" } } Page::Theme => { if p.ends_with(".file") { "Theme overrides" } else if p == "theme.mode" || p.starts_with("theme.schedule.") { "Automatic appearance" } else if p.starts_with("theme.typography.") { "Typography" } else if p.starts_with("theme.material.") { "Surface and transparency" } else { "Colors and accents" } } Page::Wallpaper => "Wallpaper image", Page::Notifications => { if p.ends_with("timeout_ms") { "Notification timing" } else { "Notification behavior" } } Page::Accessibility => { if p.starts_with("animations.") { "Motion" } else { "Visual accessibility" } } Page::Motion => { if p.starts_with("animations.") { "Animations" } else { "Focus transition" } } Page::Shortcuts => "Shortcut guide", Page::Tray => "Application tray", _ => "General", }; if let Some(group) = groups.iter_mut().find(|group| group.title == title) { group.fields.push(field) } else { groups.push(FieldGroup { title, fields: vec![field], }); } } groups } #[cfg(test)] mod group_tests { use super::*; #[test] fn every_standard_field_and_choice_has_chinese_interface_text() { use ferese_config::locale::{Language, in_language}; for page in Page::ALL { for field in fields(page) { for text in [&field.label, &field.description] { if !text.is_empty() { assert_ne!( in_language(Language::Chinese, text), text, "{}: {text}", field.path ); } } if let Kind::Choice { choices, .. } = field.kind { for (_, text) in choices { if !matches!(*text, "English" | "简体中文") { assert_ne!( in_language(Language::Chinese, text), *text, "{}: {text}", field.path ); } } } } } } #[test] fn semantic_groups_keep_every_field_once_and_separate_input_devices() { for page in Page::ALL { let mut expected = fields(page).into_iter().map(|f| f.path).collect::>(); let groups = grouped_fields(page, fields(page)); assert!(groups.iter().all(|g| !g.fields.is_empty())); let mut actual = groups .into_iter() .flat_map(|g| g.fields.into_iter().map(|f| f.path)) .collect::>(); expected.sort(); actual.sort(); assert_eq!(actual, expected, "{page:?}"); } let mouse = grouped_fields(Page::Mouse, fields(Page::Mouse)); let touchpad = grouped_fields(Page::Touchpad, fields(Page::Touchpad)); assert!( mouse .iter() .find(|g| g.title == "Mouse") .unwrap() .fields .iter() .all(|f| f.path.starts_with("input.mouse.")) ); assert!( touchpad .iter() .find(|g| g.title == "Touchpad") .unwrap() .fields .iter() .all(|f| f.path.starts_with("input.touchpad.")) ); let windows = grouped_fields(Page::Windows, fields(Page::Windows)); assert_eq!( windows.iter().map(|g| g.title).collect::>(), vec![ "Workspaces and layout", "Spacing and corners", "Focus and window effects" ] ); } }