cosmic-settings/cosmic-settings/src/pages/input/keyboard/mod.rs

948 lines
34 KiB
Rust

// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
pub mod shortcuts;
use std::cmp;
use cosmic::app::{ContextDrawer, context_drawer};
use cosmic::cosmic_config::{self, ConfigSet};
use cosmic::iced::{Alignment, Length};
use cosmic::widget::{self, ListColumn, button, container, icon, list, row, settings};
use cosmic::{Apply, Element, Task, theme};
use cosmic_comp_config::{KeyboardConfig, NumlockState, XkbConfig};
use cosmic_settings_page::{self as page, Section, section};
use itertools::Itertools;
use slotmap::{DefaultKey, Key, SlotMap};
/// Contains all options for mapping a [SpecialKey].
/// The available options differ for each key being mapped.
enum SpecialKeyAlternative {
AltLeft,
AltRight,
Ctrl,
CtrlRight,
SwapWithCtrl,
Super,
SuperLeft,
SuperRight,
CapsLock,
ScrollLock,
MenuKey,
Backspace,
Escape,
SwapWithEscape,
PrintScreen,
None,
}
impl SpecialKeyAlternative {
fn display_text(&self) -> String {
match self {
Self::AltLeft => fl!("keyboard-special-char", "alt-left"),
Self::AltRight => fl!("keyboard-special-char", "alt-right"),
Self::Ctrl => fl!("keyboard-special-char", "ctrl"),
Self::CtrlRight => fl!("keyboard-special-char", "ctrl-right"),
Self::SwapWithCtrl => fl!("keyboard-special-char", "swap-with-ctrl"),
Self::Super => fl!("keyboard-special-char", "super"),
Self::SuperLeft => fl!("keyboard-special-char", "super-left"),
Self::SuperRight => fl!("keyboard-special-char", "super-right"),
Self::CapsLock => fl!("keyboard-special-char", "caps"),
Self::ScrollLock => fl!("keyboard-special-char", "scroll-lock"),
Self::MenuKey => fl!("keyboard-special-char", "menu"),
Self::Backspace => fl!("keyboard-special-char", "backspace"),
Self::Escape => fl!("keyboard-special-char", "escape"),
Self::SwapWithEscape => fl!("keyboard-special-char", "swap-with-escape"),
Self::PrintScreen => fl!("keyboard-special-char", "print-screen"),
Self::None => fl!("keyboard-special-char", "none"),
}
}
}
/// The second/right value is the key identifier, which is how it will be saved to the configuration.
/// These values are different for each [SpecialKey], even for the same [SpecialKeyAlternative].
/// The identifier starts with a prefix defined by [SpecialKey::prefixes].
type SpecialKeyOption = (SpecialKeyAlternative, &'static str);
/// Available options for [SpecialKey::Compose].
static COMPOSE_OPTIONS: &[SpecialKeyOption] = &[
// ("Left Alt", "compose:lalt"), XXX?
(SpecialKeyAlternative::AltRight, "compose:ralt"),
(SpecialKeyAlternative::SuperLeft, "compose:lwin"),
(SpecialKeyAlternative::SuperRight, "compose:rwin"),
(SpecialKeyAlternative::MenuKey, "compose:menu"),
(SpecialKeyAlternative::CtrlRight, "compose:rctrl"),
(SpecialKeyAlternative::CapsLock, "compose:caps"),
(SpecialKeyAlternative::ScrollLock, "compose:sclk"),
(SpecialKeyAlternative::PrintScreen, "compose:prsc"),
];
/// Available options for [SpecialKey::AlternateCharacters].
static ALTERNATE_CHARACTER_OPTIONS: &[SpecialKeyOption] = &[
(SpecialKeyAlternative::AltLeft, "lv3:lalt_switch"),
(SpecialKeyAlternative::AltRight, "lv3:ralt_switch"),
(SpecialKeyAlternative::SuperLeft, "lv3:lwin_switch"),
(SpecialKeyAlternative::SuperRight, "lv3:rwin_switch"),
(SpecialKeyAlternative::MenuKey, "lv3:menu_switch"),
// ("Right Ctrl", "lv3:"), XXX
(SpecialKeyAlternative::CapsLock, "lv3:caps_switch"),
// ("Scroll Lock", "lv3:"), XXX
// ("Print Screen", "lv3"), XXX
];
/// Available options for [SpecialKey::CapsLock].
static CAPS_LOCK_OPTIONS: &[SpecialKeyOption] = &[
(SpecialKeyAlternative::Escape, "caps:escape"),
(SpecialKeyAlternative::SwapWithEscape, "caps:swapescape"),
(SpecialKeyAlternative::Backspace, "caps:backspace"),
(SpecialKeyAlternative::Super, "caps:super"),
(SpecialKeyAlternative::Ctrl, "caps:ctrl_modifier"),
(SpecialKeyAlternative::SwapWithCtrl, "ctrl:swapcaps"),
];
#[derive(Clone, Debug)]
pub enum Message {
ExpandInputSourcePopover(Option<DefaultKey>),
InputSourceSearch(String),
OpenSpecialCharacterContext(SpecialKey),
OpenNumlockContext,
ShowInputSourcesContext,
SourceAdd(DefaultKey),
SourceContext(SourceContext),
SpecialCharacterSelect(Option<&'static str>),
SetRepeatKeysDelay(u32),
SetRepeatKeysRate(u32),
SetShowExtendedInputSources(bool),
SetNumlockState(NumlockState),
}
#[derive(Clone, Debug)]
pub enum SourceContext {
MoveDown(DefaultKey),
MoveUp(DefaultKey),
Remove(DefaultKey),
Settings(DefaultKey),
ViewLayout(DefaultKey),
}
pub type Locale = String;
pub type Variant = String;
pub type Description = String;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum LayoutSource {
Base,
Extra,
Custom,
}
const KB_REPEAT_DELAY_DEFAULT: u32 = 600;
const KB_REPEAT_RATE_DEFAULT: u32 = 25;
const KB_REPEAT_DELAY_MAX: u32 = 1000;
const KB_REPEAT_DELAY_MIN: u32 = 200;
const KB_REPEAT_RATE_MAX: u32 = 45;
const KB_REPEAT_RATE_MIN: u32 = 5;
const COSMIC_COMP_CONFIG: &str = "com.system76.CosmicComp";
const COSMIC_COMP_CONFIG_VERSION: u64 = 1;
pub struct Page {
entity: page::Entity,
config: cosmic_config::Config,
context: Option<Context>,
input_source_search: String,
xkb: XkbConfig,
keyboard_config: KeyboardConfig,
keyboard_layouts: SlotMap<DefaultKey, (Locale, Variant, Description, LayoutSource)>,
active_layouts: Vec<DefaultKey>,
expanded_source_popover: Option<DefaultKey>,
show_extended_input_sources: bool,
}
impl Default for Page {
fn default() -> Self {
let config =
cosmic_config::Config::new(COSMIC_COMP_CONFIG, COSMIC_COMP_CONFIG_VERSION).unwrap();
Self {
entity: page::Entity::null(),
context: None,
expanded_source_popover: None,
keyboard_layouts: SlotMap::new(),
active_layouts: Vec::new(),
xkb: XkbConfig::default(),
keyboard_config: KeyboardConfig::default(),
input_source_search: String::new(),
show_extended_input_sources: false,
config,
}
}
}
enum Context {
ShowInputSourcesContext,
SpecialCharacter(SpecialKey),
NumlockState,
}
#[derive(Copy, Clone, Debug)]
pub enum SpecialKey {
AlternateCharacters,
Compose,
CapsLock,
}
impl SpecialKey {
pub fn title(self) -> String {
match self {
Self::Compose => fl!("keyboard-special-char", "compose"),
Self::AlternateCharacters => fl!("keyboard-special-char", "alternate"),
Self::CapsLock => fl!("keyboard-special-char", "caps"),
}
}
pub fn prefixes(self) -> &'static [&'static str] {
match self {
Self::Compose => &["compose:"],
Self::AlternateCharacters => &["lv3:"],
Self::CapsLock => &["caps:", "ctrl:"],
}
}
}
fn popover_menu_row(
id: DefaultKey,
label: String,
message: impl Fn(DefaultKey) -> SourceContext + 'static,
) -> cosmic::Element<'static, Message> {
let spacing = theme::spacing();
widget::text::body(label)
.align_y(Alignment::Center)
.apply(button::custom)
.padding([spacing.space_xxxs, spacing.space_xs])
.width(Length::Fill)
.class(theme::Button::MenuItem)
.on_press(Message::SourceContext(message(id)))
.apply(Element::from)
}
fn popover_menu(id: DefaultKey) -> cosmic::Element<'static, Message> {
widget::column::with_children([
popover_menu_row(
id,
fl!("keyboard-sources", "move-up"),
SourceContext::MoveUp,
),
popover_menu_row(
id,
fl!("keyboard-sources", "move-down"),
SourceContext::MoveDown,
),
widget::divider::horizontal::default()
.apply(widget::container)
.padding(8)
.into(),
popover_menu_row(
id,
fl!("keyboard-sources", "settings"),
SourceContext::Settings,
),
popover_menu_row(
id,
fl!("keyboard-sources", "view-layout"),
SourceContext::ViewLayout,
),
popover_menu_row(id, fl!("keyboard-sources", "remove"), SourceContext::Remove),
])
.width(Length::Fixed(200.0))
.apply(widget::container)
.padding(theme::spacing().space_xxs)
.class(theme::Container::Dropdown)
.into()
}
fn popover_button(id: DefaultKey, expanded: bool) -> cosmic::Element<'static, Message> {
let on_press = Message::ExpandInputSourcePopover(if expanded { None } else { Some(id) });
let button = button::icon(icon::from_name("view-more-symbolic"))
.extra_small()
.on_press(on_press);
if expanded {
widget::popover(button)
.position(widget::popover::Position::Bottom)
.popup(popover_menu(id))
.on_close(Message::ExpandInputSourcePopover(None))
.into()
} else {
button.into()
}
}
fn input_source(
id: DefaultKey,
description: &str,
expanded_source_popover: Option<DefaultKey>,
) -> cosmic::Element<'_, Message> {
let expanded = expanded_source_popover.is_some_and(|expanded_id| expanded_id == id);
settings::item(description, popover_button(id, expanded)).into()
}
fn special_char_radio_row<'a>(
desc: String,
value: Option<&'static str>,
current_value: Option<&'a str>,
) -> list::ListButton<'a, Message> {
settings::item::builder(desc).radio(value, Some(current_value), |_| {
Message::SpecialCharacterSelect(value)
})
}
impl page::Page<crate::pages::Message> for Page {
fn set_id(&mut self, entity: page::Entity) {
self.entity = entity;
}
fn content(
&self,
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
) -> Option<page::Content> {
Some(vec![
sections.insert(input_sources()),
sections.insert(special_character_entry()),
sections.insert(keyboard_shortcuts()),
sections.insert(keyboard_typing_assist()),
sections.insert(keyboard_num_lock()),
])
}
fn info(&self) -> page::Info {
page::Info::new("keyboard", "input-keyboard-symbolic")
.title(fl!("keyboard"))
.description(fl!("xdg-entry-keyboard-comment"))
}
fn context_drawer(&self) -> Option<ContextDrawer<'_, crate::pages::Message>> {
self.context.as_ref().map(|context| match context {
Context::ShowInputSourcesContext => {
let search = widget::search_input("", &self.input_source_search)
.on_input(Message::InputSourceSearch)
.on_clear(Message::InputSourceSearch(String::new()))
.apply(Element::from)
.map(crate::pages::Message::Keyboard);
context_drawer(
self.add_input_source_view(),
crate::pages::Message::CloseContextDrawer,
)
.title(fl!("keyboard-sources", "add"))
.header(search)
}
Context::SpecialCharacter(special_key) => context_drawer(
self.special_character_key_view(*special_key)
.map(crate::pages::Message::Keyboard),
crate::pages::Message::CloseContextDrawer,
)
.title(special_key.title()),
Context::NumlockState => context_drawer(
self.numlock_state_view()
.map(crate::pages::Message::Keyboard),
crate::pages::Message::CloseContextDrawer,
)
.title(fl!("keyboard-numlock-boot", "set")),
})
}
fn on_enter(&mut self) -> Task<crate::pages::Message> {
self.xkb = super::get_config(&self.config, "xkb_config");
self.keyboard_config = super::get_config(&self.config, "keyboard_config");
match (
xkb_data::keyboard_layouts(),
xkb_data::extra_keyboard_layouts(),
) {
(Ok(base_layouts), Ok(extra_layouts)) => {
self.active_layouts.clear();
self.keyboard_layouts.clear();
let mut sorted_layouts = base_layouts
.layouts()
.iter()
.map(|layout| (layout, LayoutSource::Base))
.chain(
extra_layouts
.layouts()
.iter()
.map(|layout| (layout, LayoutSource::Extra)),
)
.collect::<Vec<_>>();
// Add user-defined layouts if any are found
let user_layouts = xkb_data::user_keyboard_layouts();
match user_layouts {
Ok(ref custom) => {
sorted_layouts.extend(
custom
.layouts()
.iter()
.map(|layout| (layout, LayoutSource::Custom)),
);
}
Err(why) => {
tracing::error!(?why, "failed to get user keyboard layouts");
}
}
sorted_layouts.sort_unstable_by(|(a, _), (b, _)| {
match (a.name(), b.name()) {
// Place US at the top of the list as it's the default
("us", _) => cmp::Ordering::Less,
(_, "us") => cmp::Ordering::Greater,
// Place custom at the bottom
("custom", _) => cmp::Ordering::Greater,
(_, "custom") => cmp::Ordering::Less,
// Compare everything else by description because it looks nicer (e.g. all
// English grouped together)
_ => a.description().cmp(b.description()),
}
});
for (layout, source) in sorted_layouts {
self.keyboard_layouts.insert((
layout.name().to_owned(),
String::new(),
gettextrs::dgettext("xkeyboard-config", layout.description()),
source.clone(),
));
if let Some(variants) = layout.variants().map(|variants| {
variants.iter().map(|variant| {
(
layout.name().to_owned(),
variant.name().to_owned(),
gettextrs::dgettext("xkeyboard-config", variant.description()),
source.clone(),
)
})
}) {
let mut variants: Vec<_> = variants.collect();
variants.sort_unstable_by(|(_, _, desc_a, _), (_, _, desc_b, _)| {
desc_a.cmp(desc_b)
});
for (layout_name, name, description, source) in variants {
self.keyboard_layouts
.insert((layout_name, name, description, source));
}
}
}
// Xkb layouts currently enabled.
let layouts = if self.xkb.layout.is_empty() {
"us"
} else {
&self.xkb.layout
}
.split_terminator(',');
// Xkb variants for each layout. Repeat empty strings in case there's more layouts than variants.
let variants = self
.xkb
.variant
.split_terminator(',')
.chain(std::iter::repeat(""));
for (layout, variant) in layouts.zip(variants) {
for (id, (xkb_layout, xkb_variant, _desc, _source)) in &self.keyboard_layouts {
if layout == xkb_layout && variant == xkb_variant {
self.active_layouts.push(id);
break;
}
}
}
}
(Err(why), _) | (_, Err(why)) => {
tracing::error!(?why, "failed to get keyboard layouts");
}
}
Task::none()
}
fn on_leave(&mut self) -> Task<crate::pages::Message> {
self.active_layouts = Vec::new();
self.keyboard_layouts = SlotMap::new();
self.input_source_search = String::new();
Task::none()
}
}
impl Page {
pub fn update(&mut self, message: Message) -> Task<crate::app::Message> {
match message {
Message::InputSourceSearch(search) => {
self.input_source_search = search;
}
Message::SourceAdd(id) => {
self.context = None;
if !self.active_layouts.contains(&id) {
self.active_layouts.push(id);
self.update_xkb_config();
}
}
Message::SourceContext(context_message) => {
self.expanded_source_popover = None;
match context_message {
SourceContext::MoveDown(id) => {
if let Some(pos) =
self.active_layouts.iter().position(|&active| active == id)
&& pos + 1 < self.active_layouts.len()
{
self.active_layouts.swap(pos, pos + 1);
self.update_xkb_config();
}
}
SourceContext::MoveUp(id) => {
if let Some(pos) =
self.active_layouts.iter().position(|&active| active == id)
&& pos > 0
{
self.active_layouts.swap(pos, pos - 1);
self.update_xkb_config();
}
}
SourceContext::Remove(id) => {
if let Some(pos) =
self.active_layouts.iter().position(|&active| active == id)
{
let _removed = self.active_layouts.remove(pos);
self.update_xkb_config();
}
}
SourceContext::Settings(_id) => {
eprintln!("settings not implemented");
}
SourceContext::ViewLayout(_id) => {
eprintln!("view layout not implemented");
}
}
}
Message::ShowInputSourcesContext => {
self.context = Some(Context::ShowInputSourcesContext);
return cosmic::task::message(crate::app::Message::OpenContextDrawer(self.entity));
}
Message::ExpandInputSourcePopover(value) => {
self.expanded_source_popover = value;
}
Message::OpenSpecialCharacterContext(key) => {
self.context = Some(Context::SpecialCharacter(key));
return cosmic::task::message(crate::app::Message::OpenContextDrawer(self.entity));
}
Message::OpenNumlockContext => {
self.context = Some(Context::NumlockState);
return cosmic::task::message(crate::app::Message::OpenContextDrawer(self.entity));
}
Message::SpecialCharacterSelect(id) => {
if let Some(Context::SpecialCharacter(special_key)) = self.context {
let options = self.xkb.options.as_deref().unwrap_or_default();
let prefixes = special_key.prefixes();
let new_options = options
.split(',')
.filter(|x| !prefixes.iter().any(|prefix| x.starts_with(prefix)))
.chain(id)
.join(",");
self.xkb.options = Some(new_options).filter(|x| !x.is_empty());
if let Err(err) = self.config.set("xkb_config", &self.xkb) {
tracing::error!(?err, "Failed to set config 'xkb_config'");
}
}
}
Message::SetRepeatKeysDelay(delay) => {
self.xkb.repeat_delay = delay;
self.update_xkb_config();
}
Message::SetRepeatKeysRate(rate) => {
self.xkb.repeat_rate = rate;
self.update_xkb_config();
}
Message::SetShowExtendedInputSources(value) => {
self.show_extended_input_sources = value;
}
Message::SetNumlockState(numlock_state) => {
self.keyboard_config.numlock_state = numlock_state;
if let Err(err) = self.config.set("keyboard_config", &self.keyboard_config) {
tracing::error!(?err, "Failed to set config 'keyboard_config'");
}
}
}
Task::none()
}
pub fn add_input_source_view(&self) -> Element<'_, crate::pages::Message> {
let space_l = theme::spacing().space_l;
let toggler = settings::section().add(
settings::item::builder(fl!("show-extended-input-sources")).toggler(
self.show_extended_input_sources,
Message::SetShowExtendedInputSources,
),
);
let mut list = widget::list_column();
let search_input = &self.input_source_search.trim().to_lowercase();
for (id, (_locale, variant, description, source)) in &self.keyboard_layouts {
if (search_input.is_empty() || description.to_lowercase().contains(search_input))
&& (source != &LayoutSource::Extra || self.show_extended_input_sources)
{
list = list.add(self.input_source_item(id, description, !variant.is_empty()));
}
}
widget::column::with_capacity(2)
.spacing(space_l)
.push(toggler)
.push(list)
.apply(Element::from)
.map(crate::pages::Message::Keyboard)
}
fn input_source_item<'a>(
&self,
id: DefaultKey,
description: &'a str,
indent: bool,
) -> Element<'a, Message> {
let is_added = self.active_layouts.contains(&id);
let button_text = if is_added { fl!("added") } else { fl!("add") };
let add_button = widget::button::text(button_text).on_press_maybe(if is_added {
None
} else {
Some(Message::SourceAdd(id))
});
let button = widget::settings::item::builder(description).control(add_button);
if indent {
container(button).padding([0, 0, 0, 16]).into()
} else {
button.into()
}
}
fn special_character_key_view(&self, special_key: SpecialKey) -> cosmic::Element<'_, Message> {
let (options, description) = match special_key {
SpecialKey::Compose => (
COMPOSE_OPTIONS,
Some(fl!("keyboard-special-char", "compose-desc")),
),
SpecialKey::AlternateCharacters => (ALTERNATE_CHARACTER_OPTIONS, None),
SpecialKey::CapsLock => (CAPS_LOCK_OPTIONS, None),
};
let prefixes = special_key.prefixes();
let current = self
.xkb
.options
.iter()
.flat_map(|x| x.split(','))
.find(|x| prefixes.iter().any(|prefix| x.starts_with(prefix)));
// TODO layout default
let mut list = cosmic::widget::list_column();
if matches!(special_key, SpecialKey::CapsLock) {
list = list.add(special_char_radio_row(
SpecialKeyAlternative::CapsLock.display_text(),
None,
current,
));
} else {
list = list.add(special_char_radio_row(
SpecialKeyAlternative::None.display_text(),
None,
current,
));
}
list = options
.iter()
.map(|(key, id)| special_char_radio_row(key.display_text(), Some(id), current))
.fold(list, ListColumn::add);
widget::column::with_capacity(2)
.spacing(theme::spacing().space_m)
.push_maybe(description.map(widget::text::body))
.push(list)
.into()
}
fn numlock_state_view(&self) -> cosmic::Element<'_, Message> {
let current = self.keyboard_config.numlock_state;
let options = [
(fl!("keyboard-numlock-boot", "off"), NumlockState::BootOff),
(fl!("keyboard-numlock-boot", "on"), NumlockState::BootOn),
(
fl!("keyboard-numlock-boot", "last-boot"),
NumlockState::LastBoot,
),
];
let mut list = cosmic::widget::list_column();
for (desc, state) in options {
list = list.add(settings::item::builder(desc).radio(
Some(state),
Some(Some(current)),
|_| Message::SetNumlockState(state),
));
}
list.into()
}
fn update_xkb_config(&mut self) {
let result = update_xkb_config(
&self.config,
&mut self.xkb,
&mut self
.active_layouts
.iter()
.filter_map(|id| self.keyboard_layouts.get(*id))
.map(|(locale, variant, _description, _source)| {
(locale.as_str(), variant.as_str())
}),
);
if let Err(why) = result {
tracing::error!(?why, "Failed to set config 'xkb_config'");
}
}
}
impl page::AutoBind<crate::pages::Message> for Page {
fn sub_pages(page: page::Insert<crate::pages::Message>) -> page::Insert<crate::pages::Message> {
page.sub_page::<shortcuts::Page>()
}
}
fn input_sources() -> Section<crate::pages::Message> {
Section::default()
.title(fl!("keyboard-sources"))
.view::<Page>(move |_binder, page, section| {
// TODO Need something more custom, with drag and drop
let mut section = settings::section().title(&section.title);
for id in &page.active_layouts {
if let Some((_locale, _variant, description, _source)) =
page.keyboard_layouts.get(*id)
{
section =
section.add(input_source(*id, description, page.expanded_source_popover));
}
}
let add_input_source = widget::button::standard(fl!("keyboard-sources", "add"))
.on_press(Message::ShowInputSourcesContext);
widget::column::with_capacity(2)
.spacing(theme::spacing().space_xxs)
.push(section)
.push(
widget::container(add_input_source)
.width(Length::Fill)
.align_x(Alignment::End),
)
.apply(Element::from)
.map(crate::pages::Message::Keyboard)
})
}
fn special_character_entry() -> Section<crate::pages::Message> {
crate::slab!(descriptions {
alternate = fl!("keyboard-special-char", "alternate");
compose = fl!("keyboard-special-char", "compose");
caps = fl!("keyboard-special-char", "caps");
});
Section::default()
.title(fl!("keyboard-special-char"))
.descriptions(descriptions)
.view::<Page>(move |_binder, _page, section| {
let descriptions = &section.descriptions;
settings::section()
.title(&section.title)
.add(crate::widget::go_next_item(
&descriptions[alternate],
Message::OpenSpecialCharacterContext(SpecialKey::AlternateCharacters),
))
.add(crate::widget::go_next_item(
&descriptions[compose],
Message::OpenSpecialCharacterContext(SpecialKey::Compose),
))
.add(crate::widget::go_next_item(
&descriptions[caps],
Message::OpenSpecialCharacterContext(SpecialKey::CapsLock),
))
.apply(cosmic::Element::from)
.map(crate::pages::Message::Keyboard)
})
}
fn keyboard_shortcuts() -> Section<crate::pages::Message> {
crate::slab!(descriptions {
shortcuts_desc = fl!("keyboard-shortcuts", "desc");
});
Section::default()
.title(fl!("keyboard-shortcuts"))
.descriptions(descriptions)
.view::<Page>(move |binder, _page, section| {
let descriptions = &section.descriptions;
let mut section = settings::section().title(&section.title);
if let Some((shortcuts_entity, _)) = binder
.info
.iter()
.find(|(_, v)| v.id == "keyboard-shortcuts")
{
section = section.add(crate::widget::go_next_item(
&descriptions[shortcuts_desc],
crate::pages::Message::Page(shortcuts_entity),
));
}
section.apply(cosmic::Element::from)
})
}
fn keyboard_typing_assist() -> Section<crate::pages::Message> {
crate::slab!(descriptions {
repeat_delay = fl!("keyboard-typing-assist", "repeat-delay");
repeat_rate = fl!("keyboard-typing-assist", "repeat-rate");
short = fl!("short");
long = fl!("long");
slow = fl!("slow");
fast = fl!("fast");
});
Section::default()
.title(fl!("keyboard-typing-assist"))
.descriptions(descriptions)
.view::<Page>(move |_binder, page, section| {
let descriptions = &section.descriptions;
settings::section()
.title(&section.title)
.add(
settings::item::builder(&descriptions[repeat_delay])
.flex_control({
// Delay
let delay_slider = cosmic::widget::slider(
KB_REPEAT_DELAY_MIN..=KB_REPEAT_DELAY_MAX,
page.xkb.repeat_delay,
Message::SetRepeatKeysDelay,
)
.width(Length::Fill)
.breakpoints(&[KB_REPEAT_DELAY_DEFAULT])
.step(50_u32)
.apply(widget::container)
.max_width(250);
row::with_capacity(3)
.align_y(Alignment::Center)
.spacing(theme::spacing().space_s)
.push(widget::text::body(&descriptions[short]))
.push(delay_slider)
.push(widget::text::body(&descriptions[long]))
})
.align_items(Alignment::Center),
)
.add(
settings::item::builder(&descriptions[repeat_rate])
.flex_control({
// Repeat rate
let rate_slider = cosmic::widget::slider(
KB_REPEAT_RATE_MIN..=KB_REPEAT_RATE_MAX,
page.xkb.repeat_rate,
Message::SetRepeatKeysRate,
)
.width(Length::Fill)
.breakpoints(&[KB_REPEAT_RATE_DEFAULT])
.step(5_u32)
.apply(widget::container)
.max_width(250);
row::with_capacity(3)
.align_y(Alignment::Center)
.spacing(theme::spacing().space_s)
.push(widget::text::body(&descriptions[slow]))
.push(rate_slider)
.push(widget::text::body(&descriptions[fast]))
})
.align_items(Alignment::Center),
)
.apply(cosmic::Element::from)
.map(crate::pages::Message::Keyboard)
})
}
fn keyboard_num_lock() -> Section<crate::pages::Message> {
crate::slab!(descriptions {
boot_state = fl!("keyboard-numlock-boot", "boot-state");
});
Section::default()
.title(fl!("keyboard-numlock-boot"))
.descriptions(descriptions)
.view::<Page>(move |_binder, _page, section| {
let descriptions = &section.descriptions;
settings::section()
.title(&section.title)
.add(crate::widget::go_next_item(
&descriptions[boot_state],
Message::OpenNumlockContext,
))
.apply(cosmic::Element::from)
.map(crate::pages::Message::Keyboard)
})
}
fn update_xkb_config(
config: &cosmic_config::Config,
xkb: &mut XkbConfig,
active_layouts: &mut dyn Iterator<Item = (&str, &str)>,
) -> Result<(), cosmic_config::Error> {
let mut new_layout = String::new();
let mut new_variant = String::new();
for (locale, variant) in active_layouts {
new_layout.push_str(locale);
new_layout.push(',');
new_variant.push_str(variant);
new_variant.push(',');
}
let _excess_comma = new_layout.pop();
let _excess_comma = new_variant.pop();
xkb.layout = new_layout;
xkb.variant = new_variant;
config.set("xkb_config", xkb)
}