improv(keyboard): shortcuts UI improvements

This commit is contained in:
Michael Aaron Murphy 2025-03-17 12:53:25 +01:00 committed by Michael Murphy
parent bcd8293c3e
commit 48e14d4add
20 changed files with 703 additions and 497 deletions

682
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -12,8 +12,8 @@ ashpd = { version = "0.9", default-features = false, features = [
"tokio",
], optional = true }
async-channel = "2.3.1"
chrono = "0.4.39"
clap = { version = "4.5.29", features = ["derive"] }
chrono = "0.4.40"
clap = { version = "4.5.32", features = ["derive"] }
color-eyre = "0.6.3"
cosmic-bg-config.workspace = true
cosmic-comp-config = { workspace = true, optional = true }
@ -34,7 +34,7 @@ derive_setters = "0.1.6"
dirs = "5.0.1"
downcast-rs = "1.2.1"
eyre = "0.6.12"
freedesktop-desktop-entry = "0.7.8"
freedesktop-desktop-entry = "0.7.9"
futures = "0.3.31"
hostname-validator = "1.1.1"
hostname1-zbus = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true }
@ -46,24 +46,24 @@ image = { version = "0.25", default-features = false, features = [
"webp",
"hdr",
] }
indexmap = "2.7.1"
indexmap = "2.8.0"
itertools = "0.13.0"
itoa = "1.0.14"
itoa = "1.0.15"
libcosmic.workspace = true
locale1 = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true }
mime-apps = { package = "cosmic-mime-apps", git = "https://github.com/pop-os/cosmic-mime-apps", optional = true }
notify = "6.1.1"
once_cell = "1.20.3"
once_cell = "1.21.1"
regex = "1.11.1"
ron = "0.8"
rust-embed = "8.5.0"
ron = "0.9.0"
rust-embed = "8.6.0"
sctk = { workspace = true, optional = true }
secure-string = "0.3.0"
serde = { version = "1.0.217", features = ["derive"] }
serde = { version = "1.0.219", features = ["derive"] }
slab = "0.4.9"
slotmap = "1.0.7"
static_init = "1.0.3"
sunrise = "1.0.1"
sunrise = "1.2.1"
tachyonix = "0.3.1"
timedate-zbus = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true }
tokio = { workspace = true, features = ["fs", "io-util", "sync"] }
@ -78,10 +78,10 @@ zbus = { version = "4.4.0", default-features = false, features = [
"tokio",
], optional = true }
zbus_polkit = { version = "4.0.0", optional = true }
fontdb = "0.16.2"
fontdb = "=0.16.2"
fixed_decimal = "0.5.6"
mime = "0.3.17"
rustix = "0.38.44"
rustix = { version = "1.0.3", features = ["process"] }
gettext-rs = { version = "0.7.2", features = [
"gettext-system",
], optional = true }

View file

@ -754,10 +754,7 @@ impl cosmic::Application for SettingsApp {
self.context_title = Some(title.to_string());
}
Message::CloseContextDrawer => {
self.core.window.show_context = false;
self.active_context_page = None;
}
Message::CloseContextDrawer => return self.close_context_drawer(),
Message::Error(error) => {
tracing::error!(error, "error occurred");
@ -869,7 +866,9 @@ impl SettingsApp {
if current_page != page {
self.loaded_pages.remove(&current_page);
close_context_drawer_task = cosmic::task::message(Message::CloseContextDrawer);
close_context_drawer_task = self.close_context_drawer();
leave_task = self
.pages
.on_leave(current_page)
@ -920,6 +919,16 @@ impl SettingsApp {
}
}
fn close_context_drawer(&mut self) -> Task<Message> {
self.core.window.show_context = false;
self.active_context_page = None;
self.pages
.on_context_drawer_close(self.active_page)
.unwrap_or(iced::Task::none())
.map(Message::PageMessage)
.map(Into::into)
}
/// Adds a main page to the settings application.
fn insert_page<P: page::AutoBind<crate::pages::Message>>(
&mut self,

View file

@ -39,6 +39,7 @@ macro_rules! fl {
// Get the `Localizer` to be used for localizing this library.
#[must_use]
#[inline(always)]
pub fn localizer() -> Box<dyn Localizer> {
Box::from(DefaultLocalizer::new(&*LANGUAGE_LOADER, &Localizations))
}

View file

@ -3,7 +3,7 @@
use cosmic::iced::{Alignment, Length};
use cosmic::widget::{self, button, icon, settings, text};
use cosmic::{theme, Apply, Element, Task};
use cosmic::{Apply, Element, Task, theme};
use cosmic_config::{ConfigGet, ConfigSet};
use cosmic_settings_config::shortcuts::{self, Action, Binding, Shortcuts};
use cosmic_settings_page as page;
@ -15,7 +15,7 @@ use std::str::FromStr;
#[derive(Clone, Debug)]
pub enum ShortcutMessage {
AddKeybinding,
AddAnotherKeybinding,
ApplyReplace,
CancelReplace,
DeleteBinding(usize),
@ -32,8 +32,18 @@ pub struct ShortcutBinding {
pub id: widget::Id,
pub binding: Binding,
pub input: String,
pub editing: bool,
pub is_default: bool,
pub is_saved: bool,
}
impl ShortcutBinding {
pub fn reset(&mut self) {
self.input = if self.is_saved {
self.binding.to_string()
} else {
String::new()
};
}
}
#[must_use]
@ -57,8 +67,8 @@ impl ShortcutModel {
id: widget::Id::unique(),
binding: binding.clone(),
input: String::new(),
editing: false,
is_default,
is_saved: true,
});
(slab, if is_default { modified } else { modified + 1 })
@ -94,10 +104,13 @@ impl ShortcutModel {
#[must_use]
pub struct Model {
pub entity: page::Entity,
pub add_keybindings_button_id: cosmic::widget::Id,
pub defaults: Shortcuts,
pub editing: Option<usize>,
pub replace_dialog: Option<(usize, Binding, Action, String)>,
pub shortcut_models: Slab<ShortcutModel>,
pub shortcut_context: Option<usize>,
pub shortcut_title: String,
pub config: cosmic_config::Config,
pub custom: bool,
pub actions: fn(&Shortcuts, &Shortcuts) -> Slab<ShortcutModel>,
@ -107,10 +120,13 @@ impl Default for Model {
fn default() -> Self {
Self {
entity: page::Entity::null(),
add_keybindings_button_id: widget::Id::unique(),
defaults: Shortcuts::default(),
editing: None,
replace_dialog: None,
shortcut_models: Slab::new(),
shortcut_context: None,
shortcut_title: String::new(),
config: shortcuts::context().unwrap(),
custom: false,
actions: |_, _| Slab::new(),
@ -153,9 +169,16 @@ impl Model {
}
pub(super) fn context_drawer(&self) -> Option<Element<'_, ShortcutMessage>> {
self.shortcut_context
.as_ref()
.map(|id| context_drawer(&self.shortcut_models, *id, self.custom))
self.shortcut_context.as_ref().map(|id| {
context_drawer(
&self.shortcut_title,
&self.shortcut_models,
self.editing,
self.add_keybindings_button_id.clone(),
*id,
self.custom,
)
})
}
pub(super) fn dialog(&self) -> Option<Element<'_, ShortcutMessage>> {
@ -207,6 +230,25 @@ impl Model {
}
self.shortcut_models = (self.actions)(&self.defaults, &shortcuts);
self.shortcut_context = None;
self.editing = None;
}
pub(super) fn on_context_drawer_close(&mut self) {
if let Some(short_id) = self.shortcut_context.take() {
if let Some(model) = self.shortcut_models.get_mut(short_id) {
if let Some(remove_id) = model
.bindings
.iter()
.find(|(_, binding)| !binding.is_saved)
.map(|(id, _)| id)
{
model.bindings.remove(remove_id);
}
}
}
self.editing = None;
}
pub(super) fn on_clear(&mut self) {
@ -249,17 +291,18 @@ impl Model {
#[allow(clippy::too_many_lines)]
pub(super) fn update(&mut self, message: ShortcutMessage) -> Task<crate::app::Message> {
match message {
ShortcutMessage::AddKeybinding => {
ShortcutMessage::AddAnotherKeybinding => {
if let Some(short_id) = self.shortcut_context {
if let Some(model) = self.shortcut_models.get_mut(short_id) {
// If an empty entry exists, focus it instead of creating a new input.
for (_, shortcut) in &mut model.bindings {
for (binding_id, shortcut) in &mut model.bindings {
if shortcut.binding.is_set()
|| Binding::from_str(&shortcut.input).is_ok()
{
continue;
}
self.editing = Some(binding_id);
shortcut.input.clear();
return widget::text_input::focus(shortcut.id.clone());
@ -267,13 +310,13 @@ impl Model {
// Create a new input and focus it.
let id = widget::Id::unique();
model.bindings.insert(ShortcutBinding {
self.editing = Some(model.bindings.insert(ShortcutBinding {
id: id.clone(),
binding: Binding::default(),
input: String::new(),
editing: true,
is_default: false,
});
is_saved: false,
}));
return widget::text_input::focus(id);
}
@ -306,7 +349,10 @@ impl Model {
shortcut.binding = new_binding.clone();
shortcut.input.clear();
shortcut.editing = false;
if self.editing == Some(id) {
self.editing = None;
}
let action = model.action.clone();
self.config_remove(&prev_binding);
@ -319,7 +365,18 @@ impl Model {
}
}
ShortcutMessage::CancelReplace => self.replace_dialog = None,
ShortcutMessage::CancelReplace => {
if let Some(((id, _, _, _), short_id)) =
self.replace_dialog.take().zip(self.shortcut_context)
{
if let Some(model) = self.shortcut_models.get_mut(short_id) {
if let Some(binding) = model.bindings.get_mut(id) {
binding.reset();
return cosmic::widget::text_input::focus(binding.id.clone());
}
}
}
}
ShortcutMessage::DeleteBinding(id) => {
if let Some(short_id) = self.shortcut_context {
@ -328,14 +385,8 @@ impl Model {
if shortcut.is_default {
self.config_add(Action::Disable, shortcut.binding.clone());
} else {
// if last keybind deleted, clear shortcut context
if model.bindings.is_empty() {
self.shortcut_context = None;
}
self.config_remove(&shortcut.binding);
}
self.on_enter();
}
}
}
@ -344,7 +395,6 @@ impl Model {
let model = self.shortcut_models.remove(id);
for (_, shortcut) in model.bindings {
self.config_remove(&shortcut.binding);
self.on_enter();
}
}
@ -352,10 +402,12 @@ impl Model {
if let Some(short_id) = self.shortcut_context {
if let Some(model) = self.shortcut_models.get_mut(short_id) {
if let Some(shortcut) = model.bindings.get_mut(id) {
shortcut.editing = enable;
if enable {
self.editing = Some(id);
shortcut.input = shortcut.binding.to_string();
return widget::text_input::select_all(shortcut.id.clone());
} else if self.editing == Some(id) {
self.editing = None;
}
}
}
@ -395,14 +447,16 @@ impl Model {
ShortcutMessage::ShowShortcut(id, description) => {
self.shortcut_context = Some(id);
self.shortcut_title = description;
self.replace_dialog = None;
let mut tasks = vec![cosmic::task::message(
crate::app::Message::OpenContextDrawer(self.entity, description.into()),
crate::app::Message::OpenContextDrawer(self.entity, "".into()),
)];
if let Some(model) = self.shortcut_models.get(0) {
if let Some(shortcut) = model.bindings.get(0) {
self.editing = Some(0);
tasks.push(widget::text_input::focus(shortcut.id.clone()));
tasks.push(widget::text_input::select_all(shortcut.id.clone()));
}
@ -411,59 +465,7 @@ impl Model {
return Task::batch(tasks);
}
ShortcutMessage::SubmitBinding(id) => {
if let Some(short_id) = self.shortcut_context {
let mut apply_binding = None;
// Check for conflicts with the new binding.
if let Some(model) = self.shortcut_models.get_mut(short_id) {
if let Some(shortcut) = model.bindings.get_mut(id) {
match Binding::from_str(&shortcut.input) {
Ok(new_binding) => {
if !new_binding.is_set() {
shortcut.input.clear();
return Task::none();
}
if let Some(action) = self.config_contains(&new_binding) {
let action_str = if let Action::Spawn(_) = &action {
super::localize_custom_action(&action, &new_binding)
} else {
super::localize_action(&action)
};
self.replace_dialog =
Some((id, new_binding, action, action_str));
return Task::none();
}
apply_binding = Some(new_binding);
}
Err(why) => {
tracing::error!(why, "keybinding input invalid");
}
}
}
}
// Apply if no conflict was found.
if let Some(new_binding) = apply_binding {
if let Some(model) = self.shortcut_models.get_mut(short_id) {
if let Some(shortcut) = model.bindings.get_mut(id) {
let prev_binding = shortcut.binding.clone();
shortcut.binding = new_binding.clone();
shortcut.input.clear();
shortcut.editing = false;
let action = model.action.clone();
self.config_remove(&prev_binding);
self.config_add(action, new_binding);
self.on_enter();
}
}
}
}
}
ShortcutMessage::SubmitBinding(id) => return self.submit_binding(id),
}
Task::none()
@ -476,13 +478,87 @@ impl Model {
.fold(widget::list_column(), widget::ListColumn::add)
.into()
}
fn submit_binding(&mut self, id: usize) -> Task<crate::app::Message> {
if let Some(short_id) = self.shortcut_context {
let mut apply_binding = None;
// Check for conflicts with the new binding.
if let Some(model) = self.shortcut_models.get_mut(short_id) {
if let Some(shortcut) = model.bindings.get_mut(id) {
if shortcut.input.is_empty() {
return Task::none();
}
match Binding::from_str(&shortcut.input) {
Ok(new_binding) => {
if shortcut.binding == new_binding {
return Task::none();
}
if !new_binding.is_set() {
shortcut.input.clear();
return Task::none();
}
if let Some(action) = self.config_contains(&new_binding) {
let action_str = if let Action::Spawn(_) = &action {
super::localize_custom_action(&action, &new_binding)
} else {
super::localize_action(&action)
};
self.replace_dialog = Some((id, new_binding, action, action_str));
return Task::none();
}
apply_binding = Some(new_binding);
}
Err(why) => {
tracing::error!(why, "keybinding input invalid");
shortcut.reset();
}
}
}
}
// Apply if no conflict was found.
if let Some(new_binding) = apply_binding {
if let Some(model) = self.shortcut_models.get_mut(short_id) {
if let Some(shortcut) = model.bindings.get_mut(id) {
let prev_binding = shortcut.binding.clone();
shortcut.binding = new_binding.clone();
shortcut.is_saved = true;
shortcut.input.clear();
if self.editing == Some(id) {
self.editing = None;
}
let action = model.action.clone();
self.config_remove(&prev_binding);
self.config_add(action, new_binding);
return cosmic::widget::text_input::focus(
self.add_keybindings_button_id.clone(),
);
}
}
}
}
Task::none()
}
}
fn context_drawer(
shortcuts: &Slab<ShortcutModel>,
fn context_drawer<'a>(
title: &'a str,
shortcuts: &'a Slab<ShortcutModel>,
editing: Option<usize>,
add_keybindings_id: widget::Id,
id: usize,
show_action: bool,
) -> Element<ShortcutMessage> {
) -> Element<'a, ShortcutMessage> {
let cosmic::cosmic_theme::Spacing {
space_xxs,
space_xs,
@ -505,35 +581,39 @@ fn context_drawer(
let bindings = model.bindings.iter().enumerate().fold(
widget::list_column().spacing(space_xxs),
|section, (_, (bind_id, shortcut))| {
let text: Cow<'_, str> = if !shortcut.editing && shortcut.binding.is_set() {
let editing = editing == Some(bind_id);
let text: Cow<'_, str> = if !editing && shortcut.binding.is_set() {
Cow::Owned(shortcut.binding.to_string())
} else {
Cow::Borrowed(&shortcut.input)
};
let input = widget::editable_input("", text, shortcut.editing, move |enable| {
let input = widget::editable_input("", text, editing, move |enable| {
ShortcutMessage::EditBinding(bind_id, enable)
})
.select_on_focus(true)
.on_input(move |text| ShortcutMessage::InputBinding(bind_id, text))
.on_unfocus(ShortcutMessage::SubmitBinding(bind_id))
.on_submit(move |_| ShortcutMessage::SubmitBinding(bind_id))
.padding([0, space_xs])
.id(shortcut.id.clone())
.into();
let delete_button = widget::button::icon(icon::from_name("edit-delete-symbolic"))
.on_press(ShortcutMessage::DeleteBinding(bind_id))
.into();
let mut children = Vec::with_capacity(2);
children.push(input);
let flex_control =
settings::item_row(vec![input, delete_button]).align_y(Alignment::Center);
if shortcut.is_saved {
let delete_button = widget::button::icon(icon::from_name("edit-delete-symbolic"))
.on_press(ShortcutMessage::DeleteBinding(bind_id))
.into();
children.push(delete_button);
}
section.add(flex_control)
section.add(settings::item_row(children).align_y(Alignment::Center))
},
);
// TODO: Detect when it is necessary
let reset_keybinding_button = if show_action {
let reset_keybinding_button = if model.modified == 0 || show_action {
None
} else {
let button = widget::button::standard(fl!("reset-to-default"))
@ -541,8 +621,13 @@ fn context_drawer(
Some(button)
};
let add_keybinding_button =
widget::button::standard(fl!("add-keybinding")).on_press(ShortcutMessage::AddKeybinding);
let add_keybinding_button = widget::button::standard(fl!("add-another-keybinding"))
.id(add_keybindings_id)
.on_press_maybe(if model.bindings.iter().any(|(_, b)| !b.is_saved) {
None
} else {
Some(ShortcutMessage::AddAnotherKeybinding)
});
let button_container = widget::row::with_capacity(2)
.push_maybe(reset_keybinding_button)
@ -552,7 +637,8 @@ fn context_drawer(
.width(Length::Fill)
.align_x(Alignment::End);
widget::column::with_capacity(if show_action { 3 } else { 2 })
widget::column::with_capacity(if show_action { 4 } else { 3 })
.push(widget::text::heading(title))
.spacing(space_l)
.push_maybe(action)
.push(bindings)

View file

@ -67,9 +67,10 @@ pub enum Message {
#[derive(Default)]
struct AddShortcut {
pub active: bool,
pub editing: Option<usize>,
pub name: String,
pub task: String,
pub keys: Slab<(String, widget::Id, bool)>,
pub keys: Slab<(String, widget::Id)>,
}
impl AddShortcut {
@ -79,8 +80,7 @@ impl AddShortcut {
self.task.clear();
if self.keys.is_empty() {
self.keys
.insert((String::new(), widget::Id::unique(), false));
self.keys.insert((String::new(), widget::Id::unique()));
} else {
while self.keys.len() > 1 {
self.keys.remove(self.keys.len() - 1);
@ -103,34 +103,20 @@ impl Page {
}
Message::KeyEditing(id, enable) => {
self.add_shortcut.keys[id].2 = enable;
if enable {
self.add_shortcut.editing = Some(id)
} else if self.add_shortcut.editing == Some(id) {
let task = self.add_keybinding();
self.add_shortcut.editing = None;
return task;
}
}
Message::NameInput(text) => {
self.add_shortcut.name = text;
}
Message::AddKeybinding => {
// If an empty entry exists, focus it instead of creating a new input.
for (_, (binding, id, _)) in &mut self.add_shortcut.keys {
if Binding::from_str(binding).is_ok() {
continue;
}
binding.clear();
return widget::text_input::focus(id.clone());
}
let new_id = widget::Id::unique();
self.add_shortcut
.keys
.insert((String::new(), new_id.clone(), true));
return Task::batch(vec![
widget::text_input::focus(new_id.clone()),
widget::text_input::select_all(new_id),
]);
}
Message::AddKeybinding => return self.add_keybinding(),
Message::AddShortcut => {
let name = self.add_shortcut.name.trim();
@ -172,12 +158,13 @@ impl Page {
}
Message::EditCombination => {
let (_, id, editing) = &mut self.add_shortcut.keys[0];
*editing = true;
return Task::batch(vec![
widget::text_input::focus(id.clone()),
widget::text_input::select_all(id.clone()),
]);
if let Some((slab_index, (_, id))) = self.add_shortcut.keys.iter().next() {
self.add_shortcut.editing = Some(slab_index);
return Task::batch(vec![
widget::text_input::focus(id.clone()),
widget::text_input::select_all(id.clone()),
]);
}
}
Message::NameSubmit => {
@ -227,6 +214,31 @@ impl Page {
Task::none()
}
fn add_keybinding(&mut self) -> Task<crate::app::Message> {
// If an empty entry exists, focus it instead of creating a new input.
for (_, (binding, id)) in &mut self.add_shortcut.keys {
if Binding::from_str(binding).is_ok() {
continue;
}
binding.clear();
return widget::text_input::focus(id.clone());
}
let new_id = widget::Id::unique();
self.add_shortcut.editing = Some(
self.add_shortcut
.keys
.insert((String::new(), new_id.clone())),
);
Task::batch(vec![
widget::text_input::focus(new_id.clone()),
widget::text_input::select_all(new_id),
])
}
fn add_keybinding_context(&self) -> Element<'_, Message> {
let name_input = widget::text_input("", &self.add_shortcut.name)
.padding([6, 12])
@ -258,15 +270,17 @@ impl Page {
let keys = self.add_shortcut.keys.iter().fold(
widget::list_column().spacing(0),
|column, (id, (text, widget_id, editing))| {
|column, (id, (text, widget_id))| {
let key_combination = widget::editable_input(
fl!("type-key-combination"),
text,
*editing,
self.add_shortcut.editing == Some(id),
move |enable| Message::KeyEditing(id, enable),
)
.select_on_focus(true)
.padding([0, 12])
.on_input(move |input| Message::KeyInput(id, input))
.on_unfocus(Message::AddKeybinding)
.on_submit(|_| Message::AddKeybinding)
.id(widget_id.clone())
.apply(widget::container)
@ -278,7 +292,7 @@ impl Page {
let controls = widget::list_column().add(input_fields).add(keys).spacing(0);
let add_keybinding_button = widget::button::standard(fl!("add-keybinding"))
let add_keybinding_button = widget::button::standard(fl!("add-another-keybinding"))
.on_press(Message::AddShortcut)
.apply(widget::container)
.width(Length::Fill)
@ -358,6 +372,11 @@ impl page::Page<crate::pages::Message> for Page {
.map(|el| el.map(crate::pages::Message::CustomShortcuts))
}
fn on_context_drawer_close(&mut self) -> Task<crate::pages::Message> {
self.model.on_context_drawer_close();
Task::none()
}
fn on_enter(&mut self) -> Task<crate::pages::Message> {
self.model.on_enter();
Task::none()
@ -385,8 +404,8 @@ fn bindings(_defaults: &Shortcuts, keybindings: &Shortcuts) -> Slab<ShortcutMode
id: widget::Id::unique(),
binding: binding.clone(),
input: String::new(),
editing: false,
is_default: false,
is_saved: true,
};
if let Some((_, existing_model)) =

View file

@ -59,9 +59,13 @@ impl page::Page<crate::pages::Message> for Page {
.map(|el| el.map(crate::pages::Message::ManageWindowShortcuts))
}
fn on_context_drawer_close(&mut self) -> Task<crate::pages::Message> {
self.model.on_context_drawer_close();
Task::none()
}
fn on_enter(&mut self) -> Task<crate::pages::Message> {
self.model.on_enter();
Task::none()
}

View file

@ -14,13 +14,13 @@ pub mod tiling;
use cosmic::iced::Length;
use cosmic::widget::{self, icon, settings, text};
use cosmic::{theme, Apply, Element, Task};
use cosmic::{Apply, Element, Task, theme};
use cosmic_config::ConfigGet;
use cosmic_settings_config::Binding;
use cosmic_settings_config::shortcuts::action::{
Direction, FocusDirection, Orientation, ResizeDirection,
};
use cosmic_settings_config::shortcuts::{self, Action, Shortcuts};
use cosmic_settings_config::Binding;
use cosmic_settings_page::Section;
use cosmic_settings_page::{self as page, section};
use itertools::Itertools;
@ -476,25 +476,21 @@ fn all_system_actions() -> &'static [Action] {
Action::Focus(FocusDirection::Up),
Action::LastWorkspace,
Action::Maximize,
Action::MigrateWorkspaceToNextOutput,
Action::MigrateWorkspaceToOutput(Direction::Down),
Action::MigrateWorkspaceToOutput(Direction::Left),
Action::MigrateWorkspaceToOutput(Direction::Right),
Action::MigrateWorkspaceToOutput(Direction::Up),
Action::MigrateWorkspaceToPreviousOutput,
Action::Minimize,
Action::Move(Direction::Down),
Action::Move(Direction::Left),
Action::Move(Direction::Right),
Action::Move(Direction::Up),
Action::MoveToLastWorkspace,
Action::MoveToNextOutput,
Action::MoveToNextWorkspace,
Action::MoveToOutput(Direction::Down),
Action::MoveToOutput(Direction::Left),
Action::MoveToOutput(Direction::Right),
Action::MoveToOutput(Direction::Up),
Action::MoveToPreviousOutput,
Action::MoveToPreviousWorkspace,
Action::MoveToWorkspace(1),
Action::MoveToWorkspace(2),
@ -505,11 +501,9 @@ fn all_system_actions() -> &'static [Action] {
Action::MoveToWorkspace(7),
Action::MoveToWorkspace(8),
Action::MoveToWorkspace(9),
Action::NextOutput,
Action::NextWorkspace,
Action::Orientation(Orientation::Horizontal),
Action::Orientation(Orientation::Vertical),
Action::PreviousOutput,
Action::PreviousWorkspace,
Action::Resizing(ResizeDirection::Inwards),
Action::Resizing(ResizeDirection::Outwards),

View file

@ -59,9 +59,13 @@ impl page::Page<crate::pages::Message> for Page {
.map(|el| el.map(crate::pages::Message::MoveWindowShortcuts))
}
fn on_context_drawer_close(&mut self) -> Task<crate::pages::Message> {
self.model.on_context_drawer_close();
Task::none()
}
fn on_enter(&mut self) -> Task<crate::pages::Message> {
self.model.on_enter();
Task::none()
}

View file

@ -59,9 +59,13 @@ impl page::Page<crate::pages::Message> for Page {
.map(|el| el.map(crate::pages::Message::NavShortcuts))
}
fn on_context_drawer_close(&mut self) -> Task<crate::pages::Message> {
self.model.on_context_drawer_close();
Task::none()
}
fn on_enter(&mut self) -> Task<crate::pages::Message> {
self.model.on_enter();
Task::none()
}

View file

@ -59,9 +59,13 @@ impl page::Page<crate::pages::Message> for Page {
.map(|el| el.map(crate::pages::Message::SystemShortcuts))
}
fn on_context_drawer_close(&mut self) -> Task<crate::pages::Message> {
self.model.on_context_drawer_close();
Task::none()
}
fn on_enter(&mut self) -> Task<crate::pages::Message> {
self.model.on_enter();
Task::none()
}

View file

@ -59,9 +59,13 @@ impl page::Page<crate::pages::Message> for Page {
.map(|el| el.map(crate::pages::Message::TilingShortcuts))
}
fn on_context_drawer_close(&mut self) -> Task<crate::pages::Message> {
self.model.on_context_drawer_close();
Task::none()
}
fn on_enter(&mut self) -> Task<crate::pages::Message> {
self.model.on_enter();
Task::none()
}

View file

@ -88,10 +88,6 @@ impl Page {
match message {
Message::HostnameEdit(editing) => {
self.editing_device_name = editing;
if !editing {
return self.hostname_submit();
}
}
Message::HostnameInput(hostname) => {
@ -119,6 +115,7 @@ impl Page {
}
fn hostname_submit(&mut self) -> cosmic::app::Task<crate::app::Message> {
eprintln!("hostname submit");
if self.hostname_input == self.info.device_name {
return Task::none();
}
@ -182,6 +179,7 @@ fn device() -> Section<crate::pages::Message> {
)
.width(250)
.on_input(Message::HostnameInput)
.on_unfocus(Message::HostnameSubmit)
.on_submit(|_| Message::HostnameSubmit);
let device_name = settings::item::builder(&*desc[device])

View file

@ -577,7 +577,7 @@ show-extended-input-sources = Show extended input sources
keyboard-shortcuts = Keyboard Shortcuts
.desc = View and customize shortcuts
add-keybinding = Add keybinding
add-another-keybinding = Add another keybinding
cancel = Cancel
command = Command
custom = Custom

View file

@ -9,7 +9,6 @@ regex = "1.11.1"
slotmap = "1.0.7"
libcosmic = { workspace = true }
downcast-rs = "1.2.1"
once_cell = "1.20.3"
tokio.workspace = true
url = "2.5.4"
slab = "0.4.9"

View file

@ -25,6 +25,7 @@ pub struct Binder<Message> {
}
impl<Message> Default for Binder<Message> {
#[inline]
fn default() -> Self {
Self {
content: SparseSecondaryMap::new(),
@ -42,12 +43,14 @@ impl<Message> Default for Binder<Message> {
impl<Message: 'static> Binder<Message> {
/// Check if a page exists in the model.
#[must_use]
#[inline]
pub fn contains_item(&self, id: crate::Entity) -> bool {
self.info.contains_key(id)
}
/// Returns the content of a page, if it has any.
#[must_use]
#[inline]
pub fn content(&self, page: crate::Entity) -> Option<&[section::Entity]> {
self.content.get(page).map(Vec::as_slice)
}
@ -87,6 +90,7 @@ impl<Message: 'static> Binder<Message> {
}
#[must_use]
#[inline]
pub fn find_page_by_id(&self, id: &str) -> Option<(crate::Entity, &Info)> {
self.info.iter().find(|(_id, info)| info.id == id)
}
@ -117,22 +121,26 @@ impl<Message: 'static> Binder<Message> {
}
#[must_use]
#[inline]
pub fn model(&self, id: crate::Entity) -> Option<&dyn Page<Message>> {
self.page.get(id).map(AsRef::as_ref)
}
#[must_use]
#[inline]
pub fn model_mut(&mut self, id: crate::Entity) -> Option<&mut dyn Page<Message>> {
self.page.get_mut(id).map(AsMut::as_mut)
}
/// Get entity ID of page by its type ID.
#[inline]
pub fn page_id<P: Page<Message>>(&self) -> Option<crate::Entity> {
self.typed_page_ids.get(&TypeId::of::<P>()).copied()
}
/// Obtain a reference to a page by its type ID.
#[must_use]
#[inline]
pub fn page<P: Page<Message>>(&self) -> Option<&P> {
let page = self.page.get(self.page_id::<P>()?)?;
page.downcast_ref::<P>()
@ -140,6 +148,7 @@ impl<Message: 'static> Binder<Message> {
/// Create a context drawer for the given page.
#[must_use]
#[inline]
pub fn context_drawer(&self, id: crate::Entity) -> Option<Element<'_, Message>> {
let page = self.page.get(id)?;
page.context_drawer()
@ -147,6 +156,7 @@ impl<Message: 'static> Binder<Message> {
/// Create a dialog for the given page.
#[must_use]
#[inline]
pub fn dialog(&self, id: crate::Entity) -> Option<Element<'_, Message>> {
let page = self.page.get(id)?;
page.dialog()
@ -154,12 +164,23 @@ impl<Message: 'static> Binder<Message> {
/// Obtain a reference to a page by its type ID.
#[must_use]
#[inline]
pub fn page_mut<P: Page<Message>>(&mut self) -> Option<&mut P> {
let page = self.page.get_mut(self.page_id::<P>()?)?;
page.downcast_mut::<P>()
}
/// Returns a Task when a context drawer is closed.
#[inline]
pub fn on_context_drawer_close(&mut self, id: crate::Entity) -> Option<Task<Message>> {
if let Some(page) = self.page.get_mut(id) {
return Some(page.on_context_drawer_close());
}
None
}
/// Returns a Task when a page is left
#[inline]
pub fn on_leave(&mut self, id: crate::Entity) -> Option<Task<Message>> {
if let Some(page) = self.page.get_mut(id) {
return Some(page.on_leave());
@ -168,6 +189,7 @@ impl<Message: 'static> Binder<Message> {
}
/// Calls a page's load function to refresh its data.
#[inline]
pub fn on_enter(&mut self, id: crate::Entity) -> Task<Message> {
if let Some(page) = self.page.get_mut(id) {
return page.on_enter();
@ -204,13 +226,14 @@ impl<Message: 'static> Binder<Message> {
) -> impl Iterator<Item = (crate::Entity, section::Entity)> + 'a {
self.content.iter().flat_map(move |(page, sections)| {
sections
.into_iter()
.iter()
.filter(|&id| self.sections[*id].search_matches(rule))
.map(move |&id| (page, id))
})
}
/// Returns the sub-pages of a page, if it has any.
#[inline]
pub fn sub_pages(&self, page: crate::Entity) -> Option<&[crate::Entity]> {
self.sub_pages.get(page).map(AsRef::as_ref)
}
@ -219,6 +242,7 @@ impl<Message: 'static> Binder<Message> {
pub trait AutoBind<Message: 'static>: Page<Message> + Default + 'static {
/// Attaches sub-pages to the page.
#[allow(clippy::must_use_candidate)]
#[inline]
fn sub_pages(page: crate::Insert<Message>) -> crate::Insert<Message> {
page
}

View file

@ -9,13 +9,15 @@ pub struct Insert<'a, Message> {
pub id: Entity,
}
impl<'a, Message: 'static> Insert<'a, Message> {
impl<Message: 'static> Insert<'_, Message> {
#[must_use]
#[inline]
pub fn id(self) -> Entity {
self.id
}
#[must_use]
#[inline]
pub fn content(self, content: Content) -> Self {
self.model.content.insert(self.id, content);
self
@ -26,7 +28,11 @@ impl<'a, Message: 'static> Insert<'a, Message> {
#[allow(clippy::must_use_candidate)]
pub fn sub_page<P: AutoBind<Message>>(self) -> Self {
let sub_page = self.model.register::<P>().id();
self.sub_page_inner(sub_page)
}
#[inline(never)]
fn sub_page_inner(self, sub_page: Entity) -> Self {
self.model.info[sub_page].parent = Some(self.id);
self.model
@ -43,7 +49,11 @@ impl<'a, Message: 'static> Insert<'a, Message> {
#[allow(clippy::must_use_candidate)]
pub fn sub_page_with_id<P: AutoBind<Message>>(&mut self) -> Entity {
let sub_page = self.model.register::<P>().id();
self.sub_page_with_id_inner(sub_page)
}
#[inline(never)]
fn sub_page_with_id_inner(&mut self, sub_page: Entity) -> Entity {
self.model.info[sub_page].parent = Some(self.id);
self.model

View file

@ -6,7 +6,7 @@ pub use binder::{AutoBind, Binder};
mod insert;
use cosmic::{Element, Task};
use downcast_rs::{impl_downcast, Downcast};
use downcast_rs::{Downcast, impl_downcast};
pub use insert::Insert;
pub mod section;
@ -30,6 +30,7 @@ pub trait Page<Message: 'static>: Downcast {
/// Initialize the sections used by this page.
#[must_use]
#[inline]
fn content(
&self,
_sections: &mut SlotMap<section::Entity, Section<Message>>,
@ -39,46 +40,62 @@ pub trait Page<Message: 'static>: Downcast {
/// Display a context drawer for the page.
#[must_use]
#[inline]
fn context_drawer(&self) -> Option<Element<'_, Message>> {
None
}
/// Set a custom page header
#[inline]
fn header(&self) -> Option<Element<'_, Message>> {
None
}
/// Display an inner app dialog for the page.
#[inline]
fn dialog(&self) -> Option<Element<'_, Message>> {
None
}
/// Response from a file chooser dialog request.
#[inline]
fn file_chooser(&mut self, _selected: Vec<url::Url>) -> Task<Message> {
Task::none()
}
/// Alter the contents of the page's header view.
#[inline]
fn header_view(&self) -> Option<Element<'_, Message>> {
None
}
/// Emit on the context drawer being closed
#[allow(unused)]
#[inline]
fn on_context_drawer_close(&mut self) -> Task<Message> {
Task::none()
}
/// Reload page metadata via a Task.
#[allow(unused)]
#[inline]
fn on_enter(&mut self) -> Task<Message> {
Task::none()
}
/// Emit a command when the page is left
#[inline]
fn on_leave(&mut self) -> Task<Message> {
Task::none()
}
/// Assigns the entity ID of the page to the page.
#[allow(unused)]
#[inline]
fn set_id(&mut self, entity: Entity) {}
/// The title to display in the page header.
#[inline]
fn title(&self) -> Option<&str> {
None
}
@ -112,6 +129,7 @@ pub struct Info {
}
impl Info {
#[inline]
pub fn new(id: impl Into<Cow<'static, str>>, icon_name: impl Into<Cow<'static, str>>) -> Self {
Self {
title: String::new(),

View file

@ -54,6 +54,7 @@ impl<Message: 'static> Default for Section<Message> {
impl<Message: 'static> Section<Message> {
#[must_use]
#[inline]
pub fn search_matches(&self, rule: &Regex) -> bool {
if self.search_ignore {
return false;
@ -72,6 +73,7 @@ impl<Message: 'static> Section<Message> {
false
}
#[inline]
pub fn show_while<Model: Page<Message>>(
mut self,
func: impl for<'a> Fn(&'a Model) -> bool + 'static,
@ -92,6 +94,7 @@ impl<Message: 'static> Section<Message> {
/// # Panics
///
/// Will panic if the `Model` type does not match the page type.
#[inline]
pub fn view<Model: Page<Message>>(
mut self,
func: impl for<'a> Fn(
@ -116,6 +119,7 @@ impl<Message: 'static> Section<Message> {
}
#[must_use]
#[inline]
pub fn unimplemented<'a, Message: 'static>(
_binder: &'a Binder<Message>,
_page: &'a dyn Page<Message>,

View file

@ -20,6 +20,6 @@ futures-lite = "2.6.0"
futures-util = "0.3.31"
image = "0.25.5"
infer = "0.16.0"
jxl-oxide = "0.11.1"
tokio = { version = "1.43.0", features = ["sync"] }
jxl-oxide = "0.11.3"
tokio = { version = "1.44.1", features = ["sync"] }
tracing = "0.1.41"