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

View file

@ -754,10 +754,7 @@ impl cosmic::Application for SettingsApp {
self.context_title = Some(title.to_string()); self.context_title = Some(title.to_string());
} }
Message::CloseContextDrawer => { Message::CloseContextDrawer => return self.close_context_drawer(),
self.core.window.show_context = false;
self.active_context_page = None;
}
Message::Error(error) => { Message::Error(error) => {
tracing::error!(error, "error occurred"); tracing::error!(error, "error occurred");
@ -869,7 +866,9 @@ impl SettingsApp {
if current_page != page { if current_page != page {
self.loaded_pages.remove(&current_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 leave_task = self
.pages .pages
.on_leave(current_page) .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. /// Adds a main page to the settings application.
fn insert_page<P: page::AutoBind<crate::pages::Message>>( fn insert_page<P: page::AutoBind<crate::pages::Message>>(
&mut self, &mut self,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -88,10 +88,6 @@ impl Page {
match message { match message {
Message::HostnameEdit(editing) => { Message::HostnameEdit(editing) => {
self.editing_device_name = editing; self.editing_device_name = editing;
if !editing {
return self.hostname_submit();
}
} }
Message::HostnameInput(hostname) => { Message::HostnameInput(hostname) => {
@ -119,6 +115,7 @@ impl Page {
} }
fn hostname_submit(&mut self) -> cosmic::app::Task<crate::app::Message> { fn hostname_submit(&mut self) -> cosmic::app::Task<crate::app::Message> {
eprintln!("hostname submit");
if self.hostname_input == self.info.device_name { if self.hostname_input == self.info.device_name {
return Task::none(); return Task::none();
} }
@ -182,6 +179,7 @@ fn device() -> Section<crate::pages::Message> {
) )
.width(250) .width(250)
.on_input(Message::HostnameInput) .on_input(Message::HostnameInput)
.on_unfocus(Message::HostnameSubmit)
.on_submit(|_| Message::HostnameSubmit); .on_submit(|_| Message::HostnameSubmit);
let device_name = settings::item::builder(&*desc[device]) 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 keyboard-shortcuts = Keyboard Shortcuts
.desc = View and customize shortcuts .desc = View and customize shortcuts
add-keybinding = Add keybinding add-another-keybinding = Add another keybinding
cancel = Cancel cancel = Cancel
command = Command command = Command
custom = Custom custom = Custom

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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