From a223b60a0c60b982d4abf97a9c11ae52d9a392a5 Mon Sep 17 00:00:00 2001 From: Michael Murphy Date: Wed, 2 Aug 2023 11:54:07 +0200 Subject: [PATCH] feat!: implement Application API --- .vscode/settings.json | 5 +- Cargo.toml | 2 +- examples/application/Cargo.toml | 13 ++ examples/application/src/main.rs | 131 ++++++++++++ examples/cosmic-sctk/src/main.rs | 9 +- examples/cosmic-sctk/src/window.rs | 11 +- examples/cosmic/src/main.rs | 6 +- examples/cosmic/src/window.rs | 5 +- examples/cosmic/src/window/demo.rs | 2 +- justfile | 14 ++ src/app/command.rs | 63 ++++++ src/app/core.rs | 144 +++++++++++++ src/app/cosmic.rs | 290 +++++++++++++++++++++++++ src/app/mod.rs | 331 +++++++++++++++++++++++++++++ src/app/settings.rs | 80 +++++++ src/command.rs | 116 ++++++++++ src/icon_theme.rs | 20 ++ src/keyboard_nav.rs | 82 +++---- src/lib.rs | 34 +-- src/settings.rs | 34 --- src/theme/mod.rs | 2 +- src/track.rs | 40 ++++ src/widget/header_bar.rs | 103 +++++++-- src/widget/icon.rs | 4 +- src/widget/nav_bar.rs | 3 + src/widget/nav_bar_toggle.rs | 14 +- 26 files changed, 1420 insertions(+), 138 deletions(-) create mode 100644 examples/application/Cargo.toml create mode 100644 examples/application/src/main.rs create mode 100644 justfile create mode 100644 src/app/command.rs create mode 100644 src/app/core.rs create mode 100644 src/app/cosmic.rs create mode 100644 src/app/mod.rs create mode 100644 src/app/settings.rs create mode 100644 src/command.rs create mode 100644 src/icon_theme.rs delete mode 100644 src/settings.rs create mode 100644 src/track.rs diff --git a/.vscode/settings.json b/.vscode/settings.json index b04a057..ee25874 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,4 @@ { - "rust-analyzer.check.overrideCommand": [ - "cargo", "clippy", "--no-deps", "--message-format=json", "--", "-W", "clippy::pedantic" - ] + "rust-analyzer.check.overrideCommand": ["just", "check-json"], + "git-blame.gitWebUrl": "" } diff --git a/Cargo.toml b/Cargo.toml index 68649e0..dea5d0c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" name = "cosmic" [features] -default = ["winit", "tokio", "a11y"] +default = ["wayland", "tokio", "a11y"] debug = ["iced/debug"] a11y = ["iced/a11y", "iced_accessibility"] wayland = ["iced/wayland", "iced_sctk", "sctk"] diff --git a/examples/application/Cargo.toml b/examples/application/Cargo.toml new file mode 100644 index 0000000..b05d86a --- /dev/null +++ b/examples/application/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "application" +version = "0.1.0" +edition = "2021" + +[dependencies] +tracing = "0.1.37" +tracing-subscriber = "0.3.17" + +[dependencies.libcosmic] +path = "../../" +default-features = false +features = ["debug", "wayland", "tokio"] diff --git a/examples/application/src/main.rs b/examples/application/src/main.rs new file mode 100644 index 0000000..7833201 --- /dev/null +++ b/examples/application/src/main.rs @@ -0,0 +1,131 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +//! Testing ground for improving COSMIC application API ergonomics. + +use cosmic::app::{Command, Core, Settings}; +use cosmic::widget::nav_bar; +use cosmic::{executor, iced, ApplicationExt, Element}; + +/// Runs application with these settings +#[rustfmt::skip] +fn main() -> Result<(), Box> { + let input = vec![ + ("Page 1".into(), "🖖 Hello from libcosmic.".into()), + ("Page 2".into(), "🌟 This is an example application.".into()), + ("Page 3".into(), "🚧 The libcosmic API is not stable yet.".into()), + ("Page 4".into(), "🚀 Copy the source code and experiment today!".into()), + ]; + + let settings = Settings::default() + .antialiasing(true) + .client_decorations(true) + .debug(false) + .default_icon_theme("Pop") + .default_text_size(16.0) + .scale_factor(1.0) + .size((1024, 768)) + .theme(cosmic::Theme::dark()); + + cosmic::app::run::(settings, input)?; + + Ok(()) +} + +/// Messages that are used specifically by our [`App`]. +#[derive(Clone, Debug)] +pub enum Message {} + +/// The [`App`] stores application-specific state. +pub struct App { + core: Core, + nav_model: nav_bar::Model, +} + +/// Implement [`cosmic::Application`] to integrate with COSMIC. +impl cosmic::Application for App { + /// Default async executor to use with the app. + type Executor = executor::Default; + + /// Argument received [`cosmic::Application::new`]. + type Flags = Vec<(String, String)>; + + /// Message type specific to our [`App`]. + type Message = Message; + + const APP_ID: &'static str = "org.cosmic.AppDemo"; + + fn core(&self) -> &Core { + &self.core + } + + fn core_mut(&mut self) -> &mut Core { + &mut self.core + } + + /// Creates the application, and optionally emits command on initialize. + fn init(core: Core, input: Self::Flags) -> (Self, Command) { + let mut nav_model = nav_bar::Model::default(); + + for (title, content) in input { + nav_model.insert().text(title).data(content); + } + + nav_model.activate_position(0); + + let mut app = App { core, nav_model }; + + let command = app.update_title(); + + (app, command) + } + + /// Allows COSMIC to integrate with your application's [`nav_bar::Model`]. + fn nav_model(&self) -> Option<&nav_bar::Model> { + Some(&self.nav_model) + } + + /// Called when a navigation item is selected. + fn on_nav_select(&mut self, id: nav_bar::Id) -> Command { + self.nav_model.activate(id); + self.update_title() + } + + fn update(&mut self, _message: Self::Message) -> Command { + Command::none() + } + + fn view(&self) -> Element { + let page_content = self + .nav_model + .active_data::() + .map(String::as_str) + .unwrap_or("No page selected"); + + let text = cosmic::widget::text(page_content); + + let centered = iced::widget::container(text) + .width(iced::Length::Fill) + .height(iced::Length::Fill) + .align_x(iced::alignment::Horizontal::Center) + .align_y(iced::alignment::Vertical::Center); + + Element::from(centered) + } +} + +impl App +where + Self: cosmic::Application, +{ + fn active_page_title(&mut self) -> &str { + self.nav_model + .text(self.nav_model.active()) + .unwrap_or("Unknown Page") + } + + fn update_title(&mut self) -> Command { + let title = self.active_page_title().to_owned(); + self.set_title(title) + } +} diff --git a/examples/cosmic-sctk/src/main.rs b/examples/cosmic-sctk/src/main.rs index 16c9af7..7ce3ddc 100644 --- a/examples/cosmic-sctk/src/main.rs +++ b/examples/cosmic-sctk/src/main.rs @@ -1,14 +1,11 @@ -use cosmic::{ - iced::{wayland::InitialSurface, Application}, - settings, -}; +use cosmic::iced::{wayland::InitialSurface, Application, Settings}; mod window; pub use window::Window; pub fn main() -> cosmic::iced::Result { - settings::set_default_icon_theme("Pop"); - let mut settings = settings(); + cosmic::icon_theme::set_default("Pop"); + let mut settings = Settings::default(); settings.initial_surface = InitialSurface::XdgWindow(Default::default()); Window::run(settings) } diff --git a/examples/cosmic-sctk/src/window.rs b/examples/cosmic-sctk/src/window.rs index 70ac3bc..76a857d 100644 --- a/examples/cosmic-sctk/src/window.rs +++ b/examples/cosmic-sctk/src/window.rs @@ -6,7 +6,7 @@ use cosmic::{ iced::{ wayland::window::{start_drag_window, toggle_maximize}, widget::{column, container, horizontal_space, pick_list, progress_bar, row, slider}, - window, Color, Event, + window, Color, }, iced_futures::Subscription, iced_style::application, @@ -15,7 +15,7 @@ use cosmic::{ widget::{ button, cosmic_container, header_bar, nav_bar, nav_bar_toggle, rectangle_tracker::{rectangle_tracker_subscription, RectangleTracker, RectangleUpdate}, - scrollable, segmented_button, segmented_selection, settings, toggler, IconSource, + scrollable, segmented_button, segmented_selection, settings, IconSource, }, Element, ElementExt, }; @@ -336,9 +336,8 @@ impl Application for Window { .on_drag(Message::Drag) .start( nav_bar_toggle() - .on_nav_bar_toggled(nav_bar_message) - .nav_bar_active(nav_bar_toggled) - .into(), + .on_toggle(nav_bar_message) + .active(nav_bar_toggled), ); if self.show_maximize { @@ -509,7 +508,7 @@ impl Application for Window { self.theme.clone() } - fn close_requested(&self, id: window::Id) -> Self::Message { + fn close_requested(&self, _id: window::Id) -> Self::Message { Message::Close } fn subscription(&self) -> iced::Subscription { diff --git a/examples/cosmic/src/main.rs b/examples/cosmic/src/main.rs index 5700a59..f180b10 100644 --- a/examples/cosmic/src/main.rs +++ b/examples/cosmic/src/main.rs @@ -1,7 +1,7 @@ // Copyright 2022 System76 // SPDX-License-Identifier: MPL-2.0 -use cosmic::{iced::Application, settings}; +use cosmic::iced::{Application, Settings}; mod window; use env_logger::Env; @@ -13,8 +13,8 @@ pub fn main() -> cosmic::iced::Result { .write_style_or("MY_LOG_STYLE", "always"); env_logger::init_from_env(env); - settings::set_default_icon_theme("Pop"); - let mut settings = settings(); + cosmic::icon_theme::set_default("Pop"); + let mut settings = Settings::default(); settings.window.min_size = Some((600, 300)); Window::run(settings) } diff --git a/examples/cosmic/src/window.rs b/examples/cosmic/src/window.rs index 3352bb8..cccb49d 100644 --- a/examples/cosmic/src/window.rs +++ b/examples/cosmic/src/window.rs @@ -483,9 +483,8 @@ impl Application for Window { .on_drag(Message::Drag) .start( nav_bar_toggle() - .on_nav_bar_toggled(nav_bar_message) - .nav_bar_active(nav_bar_toggled) - .into(), + .on_toggle(nav_bar_message) + .active(nav_bar_toggled), ); if self.show_maximize { diff --git a/examples/cosmic/src/window/demo.rs b/examples/cosmic/src/window/demo.rs index bd0da06..d5402a4 100644 --- a/examples/cosmic/src/window/demo.rs +++ b/examples/cosmic/src/window/demo.rs @@ -184,7 +184,7 @@ impl State { Message::IconTheme(key) => { self.icon_themes.activate(key); if let Some(theme) = self.icon_themes.text(key) { - cosmic::settings::set_default_icon_theme(theme); + cosmic::icon_theme::set_default(theme); } } Message::InputChanged(s) => { diff --git a/justfile b/justfile new file mode 100644 index 0000000..8b01b8a --- /dev/null +++ b/justfile @@ -0,0 +1,14 @@ +# Check for errors and linter warnings +check *args: + cargo clippy --no-deps {{args}} -- -W clippy::pedantic + cargo clippy --no-deps --no-default-features --features="winit,tokio" {{args}} -- -W clippy::pedantic + cargo check -p application {{args}} + cargo check -p cosmic {{args}} + cargo check -p cosmic_sctk {{args}} + +# Runs a check with JSON message format for IDE integration +check-json: (check '--message-format=json') + +# Runs an example of the given {{name}} +example name: + cargo run --release -p {{name}} \ No newline at end of file diff --git a/src/app/command.rs b/src/app/command.rs new file mode 100644 index 0000000..1997b00 --- /dev/null +++ b/src/app/command.rs @@ -0,0 +1,63 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +use std::future::Future; + +use super::{Command, Message}; +use iced_runtime::command::Action; + +/// Yields a command which contains a batch of commands. +pub fn batch(commands: impl IntoIterator>) -> Command { + Command::batch(commands) +} + +/// Yields a command which will run the future on the runtime executor. +pub fn future( + future: impl Future> + Send + 'static, +) -> Command { + Command::single(Action::Future(Box::pin(future))) +} + +/// Creates a command which yields a [`crate::app::Message`]. +pub fn message(message: Message) -> Command { + crate::command::message(message) +} + +/// Convenience methods for building message-based commands. +pub mod message { + /// Creates a command which yields an application message. + pub fn app(message: M) -> crate::app::Command { + super::message(super::Message::App(message)) + } + + /// Creates a command which yields a cosmic message. + pub fn cosmic( + message: crate::app::cosmic::Message, + ) -> crate::app::Command { + super::message(super::Message::Cosmic(message)) + } +} + +pub fn drag() -> iced::Command> { + crate::command::drag().map(Message::Cosmic) +} + +pub fn fullscreen() -> iced::Command> { + crate::command::fullscreen().map(Message::Cosmic) +} + +pub fn minimize() -> iced::Command> { + crate::command::minimize().map(Message::Cosmic) +} + +pub fn set_title(title: String) -> iced::Command> { + crate::command::set_title(title).map(Message::Cosmic) +} + +pub fn set_windowed() -> iced::Command> { + crate::command::set_windowed().map(Message::Cosmic) +} + +pub fn toggle_fullscreen() -> iced::Command> { + crate::command::toggle_fullscreen().map(Message::Cosmic) +} diff --git a/src/app/core.rs b/src/app/core.rs new file mode 100644 index 0000000..73d0534 --- /dev/null +++ b/src/app/core.rs @@ -0,0 +1,144 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +use crate::{theme, Theme}; + +/// Status of the nav bar and its panels. +#[derive(Clone)] +pub struct NavBar { + active: bool, + toggled: bool, + toggled_condensed: bool, +} + +/// COSMIC-specific settings for windows. +#[allow(clippy::struct_excessive_bools)] +#[derive(Clone)] +pub struct Window { + pub can_fullscreen: bool, + pub sharp_corners: bool, + pub show_headerbar: bool, + pub show_window_menu: bool, + pub show_maximize: bool, + pub show_minimize: bool, + height: u32, + width: u32, +} + +/// COSMIC-specific application settings +#[derive(Clone)] +pub struct Core { + /// Enables debug features in cosmic/iced. + pub debug: bool, + + /// Whether the window is too small for the nav bar + main content. + is_condensed: bool, + + /// Current status of the nav bar panel. + nav_bar: NavBar, + + /// Scaling factor used by the application + scale_factor: f32, + + pub theme: Theme, + pub(crate) title: String, + pub window: Window, +} + +impl Default for Core { + fn default() -> Self { + Self { + debug: false, + is_condensed: false, + nav_bar: NavBar { + active: true, + toggled: true, + toggled_condensed: true, + }, + scale_factor: 1.0, + theme: theme::theme(), + title: String::new(), + window: Window { + can_fullscreen: false, + sharp_corners: false, + show_headerbar: true, + show_maximize: true, + show_minimize: true, + show_window_menu: false, + height: 0, + width: 0, + }, + } + } +} + +impl Core { + /// Whether the window is too small for the nav bar + main content. + #[must_use] + pub fn is_condensed(&self) -> bool { + self.is_condensed + } + + /// The scaling factor used by the application. + #[must_use] + pub fn scale_factor(&self) -> f32 { + self.scale_factor + } + + /// Changes the scaling factor used by the application. + pub(crate) fn set_scale_factor(&mut self, factor: f32) { + self.scale_factor = factor; + self.is_condensed_update(); + } + + /// Whether to show or hide the main window's content. + pub(crate) fn show_content(&self) -> bool { + !self.is_condensed || !self.nav_bar.toggled_condensed + } + + /// Call this whenever the scaling factor or window width has changed. + #[allow(clippy::cast_precision_loss)] + fn is_condensed_update(&mut self) { + self.is_condensed = (600.0 * self.scale_factor) > self.window.width as f32; + self.nav_bar_update(); + } + + /// Whether the nav panel is visible or not + #[must_use] + pub fn nav_bar_active(&self) -> bool { + self.nav_bar.active + } + + pub fn nav_bar_toggle(&mut self) { + self.nav_bar.toggled = !self.nav_bar.toggled; + self.nav_bar_set_toggled_condensed(self.nav_bar.toggled); + } + + pub fn nav_bar_toggle_condensed(&mut self) { + self.nav_bar_set_toggled_condensed(!self.nav_bar.toggled_condensed); + } + + pub(crate) fn nav_bar_set_toggled_condensed(&mut self, toggled: bool) { + self.nav_bar.toggled_condensed = toggled; + self.nav_bar_update(); + } + + pub(crate) fn nav_bar_update(&mut self) { + self.nav_bar.active = if self.is_condensed { + self.nav_bar.toggled_condensed + } else { + self.nav_bar.toggled + }; + } + + /// Set the height of the main window. + pub(crate) fn set_window_height(&mut self, new_height: u32) { + self.window.height = new_height; + } + + /// Set the width of the main window. + pub(crate) fn set_window_width(&mut self, new_width: u32) { + self.window.width = new_width; + self.is_condensed_update(); + } +} diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs new file mode 100644 index 0000000..6069d3c --- /dev/null +++ b/src/app/cosmic.rs @@ -0,0 +1,290 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +use super::{command, Application, ApplicationExt, Core, Subscription}; +use crate::theme::{self, Theme}; +use crate::widget::nav_bar; +use crate::{keyboard_nav, Element}; +#[cfg(feature = "wayland")] +use iced::event::wayland::{self, WindowEvent}; +#[cfg(feature = "wayland")] +use iced::event::PlatformSpecific; +use iced::window; +#[cfg(not(feature = "wayland"))] +use iced_runtime::command::Action; +#[cfg(not(feature = "wayland"))] +use iced_runtime::window::Action as WindowAction; +#[cfg(feature = "wayland")] +use sctk::reexports::csd_frame::{WindowManagerCapabilities, WindowState}; + +/// A message managed internally by COSMIC. +#[derive(Clone, Debug)] +pub enum Message { + /// Requests to close the window. + Close, + /// Requests to drag the window. + Drag, + /// Keyboard shortcuts managed by libcosmic. + KeyboardNav(keyboard_nav::Message), + /// Requests to maximize the window. + Maximize, + /// Requests to minimize the window. + Minimize, + /// Activates a navigation element from the nav bar. + NavBar(nav_bar::Id), + /// Set scaling factor + ScaleFactor(f32), + /// Requests theme changes. + ThemeChange(Theme), + /// Toggles visibility of the nav bar. + ToggleNavBar, + /// Toggles the condensed status of the nav bar. + ToggleNavBarCondensed, + /// Updates the tracked window geometry. + WindowResize(window::Id, u32, u32), + /// Tracks updates to window state. + #[cfg(feature = "wayland")] + WindowState(window::Id, WindowState), + /// Capabilities the window manager supports + #[cfg(feature = "wayland")] + WmCapabilities(window::Id, WindowManagerCapabilities), +} + +#[derive(Default)] +pub(crate) struct Cosmic { + pub(crate) app: App, + #[cfg(feature = "wayland")] + pub(crate) should_exit: bool, +} + +impl iced::Application for Cosmic +where + T::Message: Send + 'static, +{ + type Executor = T::Executor; + type Flags = (Core, T::Flags); + type Message = super::Message; + type Theme = Theme; + + fn new((core, flags): Self::Flags) -> (Self, iced::Command) { + let (model, command) = T::init(core, flags); + + (Cosmic::new(model), command) + } + + #[cfg(feature = "wayland")] + fn close_requested(&self, id: window::Id) -> Self::Message { + self.app + .on_close_requested(id) + .map_or(super::Message::None, super::Message::App) + } + + fn title(&self) -> String { + self.app.title().to_string() + } + + fn update(&mut self, message: Self::Message) -> iced::Command { + match message { + super::Message::App(message) => self.app.update(message), + super::Message::Cosmic(message) => self.cosmic_update(message), + super::Message::None => iced::Command::none(), + } + } + + fn scale_factor(&self) -> f64 { + f64::from(self.app.core().scale_factor()) + } + + #[cfg(feature = "wayland")] + fn should_exit(&self) -> bool { + self.should_exit + } + + fn style(&self) -> ::Style { + if self.app.core().window.sharp_corners { + theme::Application::default() + } else { + theme::Application::Custom(Box::new(|theme| iced_style::application::Appearance { + background_color: iced_core::Color::TRANSPARENT, + text_color: theme.cosmic().on_bg_color().into(), + })) + } + } + + fn subscription(&self) -> Subscription { + let window_events = iced::subscription::events_with(|event, _| { + match event { + iced::Event::Window(id, window::Event::Resized { width, height }) => { + return Some(Message::WindowResize(id, width, height)); + } + + #[cfg(feature = "wayland")] + iced::Event::PlatformSpecific(PlatformSpecific::Wayland(event)) => match event { + wayland::Event::Window(WindowEvent::State(state), _surface, id) => { + return Some(Message::WindowState(id, state)); + } + + wayland::Event::Window( + WindowEvent::WmCapabilities(capabilities), + _surface, + id, + ) => { + return Some(Message::WmCapabilities(id, capabilities)); + } + + _ => (), + }, + _ => (), + } + + None + }); + + Subscription::batch(vec![ + self.app.subscription().map(super::Message::App), + keyboard_nav::subscription() + .map(Message::KeyboardNav) + .map(super::Message::Cosmic), + theme::subscription(0) + .map(Message::ThemeChange) + .map(super::Message::Cosmic), + window_events.map(super::Message::Cosmic), + ]) + } + + fn theme(&self) -> Self::Theme { + self.app.core().theme.clone() + } + + #[cfg(feature = "wayland")] + fn view(&self, id: window::Id) -> Element { + if id != window::Id(0) { + return self.app.view_window(id).map(super::Message::App); + } + + self.app.view_main() + } + + #[cfg(not(feature = "wayland"))] + fn view(&self) -> Element { + self.app.view_main() + } +} + +impl Cosmic { + #[cfg(feature = "wayland")] + pub fn close(&mut self) -> iced::Command> { + self.should_exit = true; + iced::Command::none() + } + + #[cfg(not(feature = "wayland"))] + #[allow(clippy::unused_self)] + pub fn close(&mut self) -> iced::Command> { + iced::Command::single(Action::Window(WindowAction::Close)) + } + + fn cosmic_update(&mut self, message: Message) -> iced::Command> { + match message { + Message::WindowResize(id, width, height) => { + if window::Id(0) == id { + self.app.core_mut().set_window_width(width); + self.app.core_mut().set_window_height(height); + } + + self.app.on_window_resize(id, width, height); + } + + #[cfg(feature = "wayland")] + Message::WindowState(id, state) => { + if window::Id(0) == id { + self.app.core_mut().window.sharp_corners = + matches!(state, WindowState::ACTIVATED) + || state.contains(WindowState::TILED); + } + } + + #[cfg(feature = "wayland")] + Message::WmCapabilities(id, capabilities) => { + if window::Id(0) == id { + self.app.core_mut().window.can_fullscreen = + capabilities.contains(WindowManagerCapabilities::FULLSCREEN); + self.app.core_mut().window.show_maximize = + capabilities.contains(WindowManagerCapabilities::MAXIMIZE); + self.app.core_mut().window.show_minimize = + capabilities.contains(WindowManagerCapabilities::MINIMIZE); + self.app.core_mut().window.show_window_menu = + capabilities.contains(WindowManagerCapabilities::WINDOW_MENU); + } + } + + Message::KeyboardNav(message) => match message { + keyboard_nav::Message::Unfocus => { + return keyboard_nav::unfocus().map(super::Message::Cosmic) + } + keyboard_nav::Message::FocusNext => { + return iced::widget::focus_next().map(super::Message::Cosmic) + } + keyboard_nav::Message::FocusPrevious => { + return iced::widget::focus_previous().map(super::Message::Cosmic) + } + keyboard_nav::Message::Escape => return self.app.on_escape(), + keyboard_nav::Message::Search => return self.app.on_search(), + + keyboard_nav::Message::Fullscreen => return command::toggle_fullscreen(), + }, + + Message::Drag => return command::drag(), + + Message::Close => { + self.app.on_app_exit(); + return self.close(); + } + + Message::Minimize => return command::minimize(), + + Message::Maximize => { + if self.app.core().window.sharp_corners { + self.app.core_mut().window.sharp_corners = false; + return command::set_windowed(); + } + + self.app.core_mut().window.sharp_corners = true; + return command::fullscreen(); + } + + Message::NavBar(key) => { + self.app.core_mut().nav_bar_set_toggled_condensed(false); + return self.app.on_nav_select(key); + } + + Message::ToggleNavBar => { + self.app.core_mut().nav_bar_toggle(); + } + + Message::ToggleNavBarCondensed => { + self.app.core_mut().nav_bar_toggle_condensed(); + } + + Message::ThemeChange(theme) => { + self.app.core_mut().theme = theme; + } + + Message::ScaleFactor(factor) => { + self.app.core_mut().set_scale_factor(factor); + } + } + + iced::Command::none() + } +} + +impl Cosmic { + pub fn new(app: App) -> Self { + Self { + app, + #[cfg(feature = "wayland")] + should_exit: false, + } + } +} diff --git a/src/app/mod.rs b/src/app/mod.rs new file mode 100644 index 0000000..6cc6e63 --- /dev/null +++ b/src/app/mod.rs @@ -0,0 +1,331 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +pub mod command; +mod core; +pub mod cosmic; +pub mod settings; + +pub mod message { + #[derive(Clone, Debug)] + #[must_use] + pub enum Message { + /// Messages from the application, for the application. + App(M), + /// Internal messages to be handled by libcosmic. + Cosmic(super::cosmic::Message), + /// Do nothing + None, + } + + pub fn app(message: M) -> Message { + Message::App(message) + } + + pub fn cosmic(message: super::cosmic::Message) -> Message { + Message::Cosmic(message) + } + + pub fn none() -> Message { + Message::None + } +} + +pub use self::core::Core; +pub use self::settings::Settings; +use crate::widget::nav_bar; +use crate::{Element, ElementExt}; +use apply::Apply; +use iced::Subscription; +use iced::{window, Application as IcedApplication}; +pub use message::Message; + +/// Commands for COSMIC applications. +pub type Command = iced::Command>; + +/// Launch the application with the given settings. +/// +/// # Errors +/// +/// Returns error on application failure. +pub fn run(settings: Settings, flags: App::Flags) -> iced::Result { + if let Some(icon_theme) = settings.default_icon_theme { + crate::icon_theme::set_default(icon_theme); + } + + let mut core = Core::default(); + core.debug = settings.debug; + core.set_scale_factor(settings.scale_factor); + core.set_window_width(settings.size.0); + core.set_window_height(settings.size.1); + core.theme = settings.theme; + + let mut iced = iced::Settings::with_flags((core, flags)); + + iced.antialiasing = settings.antialiasing; + iced.default_font = settings.default_font; + iced.default_text_size = settings.default_text_size; + iced.id = Some(App::APP_ID.to_owned()); + + #[cfg(feature = "wayland")] + { + use iced::wayland::actions::window::SctkWindowSettings; + use iced_sctk::settings::InitialSurface; + iced.initial_surface = InitialSurface::XdgWindow(SctkWindowSettings { + app_id: Some(App::APP_ID.to_owned()), + autosize: settings.autosize, + client_decorations: settings.client_decorations, + resizable: settings.resizable, + size: settings.size, + size_limits: settings.size_limits, + title: None, + transparent: settings.transparent, + ..SctkWindowSettings::default() + }); + } + + #[cfg(not(feature = "wayland"))] + { + if let Some(_border_size) = settings.resizable { + // iced.window.border_size = border_size as u32; + iced.window.resizable = true; + } + iced.window.decorations = !settings.client_decorations; + iced.window.size = settings.size; + iced.window.transparent = settings.transparent; + } + + cosmic::Cosmic::::run(iced) +} + +#[allow(unused_variables)] +pub trait Application +where + Self: Sized + 'static, +{ + /// Default async executor to use with the app. + type Executor: iced_futures::Executor; + + /// Argument received [`Application::new`]. + type Flags: Clone; + + /// Message type specific to our app. + type Message: Clone + std::fmt::Debug + Send + 'static; + + /// An ID that uniquely identifies the application. + /// The standard is to pick an ID based on a reverse-domain name notation. + /// IE: `com.system76.Settings` + const APP_ID: &'static str; + + /// Grants access to the COSMIC Core. + fn core(&self) -> &Core; + + /// Grants access to the COSMIC Core. + fn core_mut(&mut self) -> &mut Core; + + /// Creates the application, and optionally emits command on initialize. + fn init(core: Core, flags: Self::Flags) -> (Self, iced::Command>); + + /// Attaches elements to the start section of the header. + fn header_start(&self) -> Vec> { + Vec::new() + } + + /// Attaches elements to the center of the header. + fn header_center(&self) -> Vec> { + Vec::new() + } + + /// Attaches elements to the end section of the header. + fn header_end(&self) -> Vec> { + Vec::new() + } + + /// Allows COSMIC to integrate with your application's [`nav_bar::Model`]. + fn nav_model(&self) -> Option<&nav_bar::Model> { + None + } + + /// Called before closing the application. + fn on_app_exit(&mut self) {} + + /// Called when a window requests to be closed. + fn on_close_requested(&self, id: window::Id) -> Option { + None + } + + /// Called when the escape key is pressed. + fn on_escape(&mut self) -> iced::Command> { + iced::Command::none() + } + + /// Called when a navigation item is selected. + fn on_nav_select(&mut self, id: nav_bar::Id) -> iced::Command> { + iced::Command::none() + } + + /// Called when the search function is requested. + fn on_search(&mut self) -> iced::Command> { + iced::Command::none() + } + + /// Called when a window is resized. + fn on_window_resize(&mut self, id: window::Id, width: u32, height: u32) {} + + /// Event sources that are to be listened to. + fn subscription(&self) -> Subscription { + Subscription::none() + } + + /// Respond to an application-specific message. + fn update(&mut self, message: Self::Message) -> iced::Command> { + iced::Command::none() + } + + /// Constructs the view for the main window. + fn view(&self) -> Element; + + /// Constructs views for other windows. + fn view_window(&self, id: window::Id) -> Element { + panic!("no view for window {}", id.0); + } +} + +/// Methods automatically derived for all types implementing [`Application`]. +pub trait ApplicationExt: Application { + /// Initiates a window drag. + fn drag(&mut self) -> iced::Command>; + + /// Fullscreens the window. + fn fullscreen(&mut self) -> iced::Command>; + + /// Minimizes the window. + fn minimize(&mut self) -> iced::Command>; + + /// Get the title of the main window. + fn title(&self) -> &str; + + /// Set the title of the main window. + fn set_title(&mut self, title: String) -> iced::Command>; + + /// View template for the main window. + fn view_main(&self) -> Element>; +} + +impl ApplicationExt for App { + fn drag(&mut self) -> iced::Command> { + command::drag() + } + + fn fullscreen(&mut self) -> iced::Command> { + command::fullscreen() + } + + fn minimize(&mut self) -> iced::Command> { + command::minimize() + } + + fn title(&self) -> &str { + &self.core().title + } + + #[cfg(feature = "wayland")] + fn set_title(&mut self, title: String) -> iced::Command> { + self.core_mut().title = title.clone(); + command::set_title(title) + } + + #[cfg(not(feature = "wayland"))] + fn set_title(&mut self, title: String) -> iced::Command> { + self.core_mut().title = title.clone(); + iced::Command::none() + } + + fn view_main<'a>(&'a self) -> Element<'a, Message> { + let core = self.core(); + let is_condensed = core.is_condensed(); + let mut main: Vec>> = Vec::with_capacity(2); + + if core.window.show_headerbar { + main.push({ + let mut header = crate::widget::header_bar() + .title(self.title()) + .on_drag(Message::Cosmic(cosmic::Message::Drag)) + .on_close(Message::Cosmic(cosmic::Message::Close)); + + if self.nav_model().is_some() { + let toggle = crate::widget::nav_bar_toggle() + .active(core.nav_bar_active()) + .on_toggle(if is_condensed { + Message::Cosmic(cosmic::Message::ToggleNavBarCondensed) + } else { + Message::Cosmic(cosmic::Message::ToggleNavBar) + }); + + header = header.start(toggle); + } + + if core.window.show_maximize { + header = header.on_maximize(Message::Cosmic(cosmic::Message::Maximize)); + } + + if core.window.show_minimize { + header = header.on_minimize(Message::Cosmic(cosmic::Message::Minimize)); + } + + for element in self.header_start() { + header = header.start(element.map(Message::App)); + } + + for element in self.header_center() { + header = header.center(element.map(Message::App)); + } + + for element in self.header_end() { + header = header.end(element.map(Message::App)); + } + + Element::from(header).debug(core.debug) + }); + } + + // The content element contains every element beneath the header. + main.push( + iced::widget::row({ + let mut widgets = Vec::with_capacity(2); + + // Insert nav bar onto the left side of the window. + if core.nav_bar_active() { + if let Some(nav_model) = self.nav_model() { + let mut nav = crate::widget::nav_bar(nav_model, |entity| { + Message::Cosmic(cosmic::Message::NavBar(entity)) + }); + + if !is_condensed { + nav = nav.max_width(300); + } + + widgets.push(nav.apply(Element::from).debug(core.debug)); + } + } + + if core.show_content() { + let main_content = self.view().debug(core.debug).map(Message::App); + + widgets.push(main_content); + } + + widgets + }) + .spacing(8) + .apply(iced::widget::container) + .padding([0, 8, 8, 8]) + .width(iced::Length::Fill) + .height(iced::Length::Fill) + .style(crate::theme::Container::Background) + .into(), + ); + + iced::widget::column(main).into() + } +} diff --git a/src/app/settings.rs b/src/app/settings.rs new file mode 100644 index 0000000..42b42a1 --- /dev/null +++ b/src/app/settings.rs @@ -0,0 +1,80 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +use crate::{font, Theme}; +#[cfg(feature = "wayland")] +use iced::Limits; +use iced_core::Font; + +#[allow(clippy::struct_excessive_bools)] +#[derive(derive_setters::Setters)] +pub struct Settings { + /// Produces a smoother result in some widgets, at a performance cost. + pub(crate) antialiasing: bool, + + /// Autosize the window to fit its contents + #[cfg(feature = "wayland")] + pub(crate) autosize: bool, + + /// Whether the window should have a border, a title bar, etc. or not. + pub(crate) client_decorations: bool, + + /// Enables debug features in cosmic/iced. + pub(crate) debug: bool, + + /// The default [`Font`] to be used. + pub(crate) default_font: Font, + + /// Name of the icon theme to search by default. + #[setters(strip_option, into)] + pub(crate) default_icon_theme: Option, + + /// Default size of fonts. + pub(crate) default_text_size: f32, + + /// Whether the window should be resizable or not. + /// and the size of the window border which can be dragged for a resize + #[setters(strip_option)] + pub(crate) resizable: Option, + + /// Scale factor to use by default. + pub(crate) scale_factor: f32, + + /// Initial size of the window. + pub(crate) size: (u32, u32), + + /// Limitations of the window size + #[cfg(feature = "wayland")] + pub(crate) size_limits: Limits, + + /// The theme to apply to the application. + pub(crate) theme: Theme, + + /// Whether the window should be transparent. + pub(crate) transparent: bool, +} + +impl Default for Settings { + fn default() -> Self { + Self { + antialiasing: true, + #[cfg(feature = "wayland")] + autosize: false, + client_decorations: true, + debug: false, + default_font: font::FONT, + default_icon_theme: Some(String::from("Pop")), + default_text_size: 14.0, + resizable: Some(8.0), + scale_factor: std::env::var("COSMIC_SCALE") + .ok() + .and_then(|scale| scale.parse::().ok()) + .unwrap_or(1.0), + size: (1024, 768), + #[cfg(feature = "wayland")] + size_limits: Limits::NONE.min_height(1.0).min_width(1.0), + theme: crate::theme::theme(), + transparent: false, + } + } +} diff --git a/src/command.rs b/src/command.rs new file mode 100644 index 0000000..a4e416c --- /dev/null +++ b/src/command.rs @@ -0,0 +1,116 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +#[cfg(feature = "wayland")] +use iced::window; +use iced::Command; +use iced_core::window::Mode; +#[cfg(feature = "wayland")] +use iced_runtime::command::platform_specific::wayland::window::Action as WindowAction; +#[cfg(feature = "wayland")] +use iced_runtime::command::platform_specific::wayland::Action as WaylandAction; +#[cfg(feature = "wayland")] +use iced_runtime::command::platform_specific::Action as PlatformAction; +use iced_runtime::command::Action; +#[cfg(not(feature = "wayland"))] +use iced_runtime::window::Action as WindowAction; +use std::future::Future; + +/// Yields a command which contains a batch of commands. +pub fn batch(commands: impl IntoIterator>) -> Command { + Command::batch(commands) +} + +/// Yields a command which will run the future on the runtime executor. +pub fn future(future: impl Future + Send + 'static) -> Command { + Command::single(Action::Future(Box::pin(future))) +} + +/// Yields a command which will return a message. +pub fn message(message: M) -> Command { + future(async move { message }) +} + +/// Initiates a window drag. +#[cfg(feature = "wayland")] +pub fn drag() -> Command { + iced_sctk::commands::window::start_drag_window(window::Id(0)) +} + +/// Initiates a window drag. +#[cfg(not(feature = "wayland"))] +pub fn drag() -> Command { + iced::Command::none() +} + +/// Fullscreens the window. +#[cfg(feature = "wayland")] +pub fn fullscreen() -> Command { + iced_sctk::commands::window::set_mode_window(window::Id(0), Mode::Fullscreen) +} + +/// Fullscreens the window. +#[cfg(not(feature = "wayland"))] +pub fn fullscreen() -> Command { + iced::Command::single(Action::Window(WindowAction::ChangeMode(Mode::Fullscreen))) +} + +/// Minimizes the window. +#[cfg(feature = "wayland")] +pub fn minimize() -> Command { + iced_sctk::commands::window::set_mode_window(window::Id(0), Mode::Hidden) +} + +/// Minimizes the window. +#[cfg(not(feature = "wayland"))] +pub fn minimize() -> Command { + iced::Command::single(Action::Window(WindowAction::ChangeMode(Mode::Hidden))) +} + +/// Sets the title of a window. +#[cfg(feature = "wayland")] +pub fn set_title(title: String) -> Command { + window_action(WindowAction::Title { + id: window::Id(0), + title, + }) +} + +/// Sets the title of a window. +#[cfg(not(feature = "wayland"))] +#[allow(unused_variables, clippy::needless_pass_by_value)] +pub fn set_title(title: String) -> Command { + Command::none() +} + +/// Sets the window mode to windowed. +#[cfg(feature = "wayland")] +pub fn set_windowed() -> Command { + iced_sctk::commands::window::set_mode_window(window::Id(0), Mode::Windowed) +} + +/// Sets the window mode to windowed. +#[cfg(not(feature = "wayland"))] +pub fn set_windowed() -> Command { + iced::Command::single(Action::Window(WindowAction::ChangeMode(Mode::Windowed))) +} + +/// Toggles the windows' maximization state. +#[cfg(feature = "wayland")] +pub fn toggle_fullscreen() -> Command { + window_action(WindowAction::ToggleFullscreen { id: window::Id(0) }) +} + +/// Toggles the windows' maximization state. +#[cfg(not(feature = "wayland"))] +pub fn toggle_fullscreen() -> Command { + iced::Command::single(Action::Window(WindowAction::ToggleMaximize)) +} + +/// Creates a command to apply an action to a window. +#[cfg(feature = "wayland")] +pub fn window_action(action: WindowAction) -> Command { + Command::single(Action::PlatformSpecific(PlatformAction::Wayland( + WaylandAction::Window(action), + ))) +} diff --git a/src/icon_theme.rs b/src/icon_theme.rs new file mode 100644 index 0000000..58c0564 --- /dev/null +++ b/src/icon_theme.rs @@ -0,0 +1,20 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +use std::cell::RefCell; + +thread_local! { + /// The fallback icon theme to search if no icon theme was specified. + pub(crate) static DEFAULT: RefCell = RefCell::new(String::from("Pop")); +} + +/// The fallback icon theme to search if no icon theme was specified. +#[must_use] +pub fn default() -> String { + DEFAULT.with(|f| f.borrow().clone()) +} + +/// Set the fallback icon theme to search when loading system icons. +pub fn set_default(name: impl Into) { + DEFAULT.with(|f| *f.borrow_mut() = name.into()); +} diff --git a/src/keyboard_nav.rs b/src/keyboard_nav.rs index af4703e..7610328 100644 --- a/src/keyboard_nav.rs +++ b/src/keyboard_nav.rs @@ -1,3 +1,6 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + use iced::{ event, keyboard::{self, KeyCode}, @@ -10,52 +13,57 @@ pub enum Message { Escape, FocusNext, FocusPrevious, + Fullscreen, Unfocus, Search, } pub fn subscription() -> Subscription { - subscription::events_with(|event, status| match (event, status) { - // Focus - ( + subscription::events_with(|event, status| { + if event::Status::Ignored != status { + return None; + } + + match event { Event::Keyboard(keyboard::Event::KeyPressed { - key_code: KeyCode::Tab, + key_code, modifiers, - .. - }), - event::Status::Ignored, - ) => Some(if modifiers.shift() { - Message::FocusPrevious - } else { - Message::FocusNext - }), - // Escape - ( - Event::Keyboard(keyboard::Event::KeyPressed { - key_code: KeyCode::Escape, - .. - }), - _, - ) => Some(Message::Escape), - // Search - ( - Event::Keyboard(keyboard::Event::KeyPressed { - key_code: KeyCode::F, - modifiers, - }), - event::Status::Ignored, - ) => { - if modifiers.control() { - Some(Message::Search) - } else { - None + }) => match key_code { + KeyCode::Tab => { + return Some(if modifiers.shift() { + Message::FocusPrevious + } else { + Message::FocusNext + }); + } + + KeyCode::Escape => { + return Some(Message::Escape); + } + + KeyCode::F11 => { + return Some(Message::Fullscreen); + } + + KeyCode::F => { + return if modifiers.control() { + Some(Message::Search) + } else { + None + }; + } + + _ => (), + }, + + Event::Mouse(mouse::Event::ButtonPressed { .. }) => { + return Some(Message::Unfocus); } + + _ => (), } - // Unfocus - (Event::Mouse(mouse::Event::ButtonPressed { .. }), event::Status::Ignored) => { - Some(Message::Unfocus) - } - _ => None, + + None }) } diff --git a/src/lib.rs b/src/lib.rs index 4dc9219..04b9405 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,8 +3,22 @@ #![allow(clippy::module_name_repetitions)] +pub mod app; +pub use app::{Application, ApplicationExt}; + +pub mod command; pub use cosmic_config; pub use cosmic_theme; + +pub mod executor; +#[cfg(feature = "tokio")] +pub use executor::single::Executor as SingleThreadExecutor; + +mod ext; +pub use ext::ElementExt; + +pub mod font; + pub use iced; pub use iced_core; pub use iced_futures; @@ -16,23 +30,17 @@ pub use iced_style; pub use iced_widget; #[cfg(feature = "winit")] pub use iced_winit; + +pub mod icon_theme; +pub mod keyboard_nav; + #[cfg(feature = "wayland")] pub use sctk; -pub mod executor; -pub mod font; -pub mod keyboard_nav; + pub mod theme; +pub use theme::Theme; + pub mod widget; -#[cfg(feature = "tokio")] -pub use executor::single::Executor as SingleThreadExecutor; - -pub mod settings; -pub use settings::{settings, settings_with_flags}; - -mod ext; -pub use ext::ElementExt; - -pub use theme::Theme; pub type Renderer = iced::Renderer; pub type Element<'a, Message> = iced::Element<'a, Message, Renderer>; diff --git a/src/settings.rs b/src/settings.rs deleted file mode 100644 index 9f9b20f..0000000 --- a/src/settings.rs +++ /dev/null @@ -1,34 +0,0 @@ -use crate::font; -use std::cell::RefCell; - -thread_local! { - /// The fallback icon theme to search if no icon theme was specified. - pub(crate) static DEFAULT_ICON_THEME: RefCell = RefCell::new(String::from("Pop")); -} - -/// The fallback icon theme to search if no icon theme was specified. -#[must_use] -pub fn default_icon_theme() -> String { - DEFAULT_ICON_THEME.with(|f| f.borrow().clone()) -} - -/// Set the fallback icon theme to search when loading system icons. -pub fn set_default_icon_theme(name: impl Into) { - DEFAULT_ICON_THEME.with(|f| *f.borrow_mut() = name.into()); -} - -/// Default iced settings for COSMIC applications. -#[must_use] -pub fn settings() -> iced::Settings { - settings_with_flags(Flags::default()) -} - -/// Default iced settings for COSMIC applications. -#[must_use] -pub fn settings_with_flags(flags: Flags) -> iced::Settings { - iced::Settings { - default_font: font::FONT, - default_text_size: 18.0, - ..iced::Settings::with_flags(flags) - } -} diff --git a/src/theme/mod.rs b/src/theme/mod.rs index 69b945a..8a23a03 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -1221,7 +1221,7 @@ pub fn theme() -> Theme { crate::theme::Theme::custom(Arc::new(t)) } -pub fn theme_subscription(id: u64) -> Subscription { +pub fn subscription(id: u64) -> Subscription { config_subscription::>( id, crate::cosmic_theme::NAME.into(), diff --git a/src/track.rs b/src/track.rs new file mode 100644 index 0000000..104da86 --- /dev/null +++ b/src/track.rs @@ -0,0 +1,40 @@ +/// Records if a change has occurred to its inner value +pub struct Track { + value: T, + changed: bool, +} + +impl Track { + /// Create a new value where changes are tracked. + pub const fn new(value: T) -> Self { + Self { + value, + changed: true, + } + } + + /// Gets the inner value. + pub fn get(&self) -> &T { + &self.value + } + + /// Set a new value, and mark that it has changed. + pub fn set(&mut self, value: T) { + self.value = value; + self.changed = true; + } + + /// Check if value has changed. + pub fn changed(&self) -> bool { + self.changed + } +} + +impl Default for Track +where + T: Default, +{ + fn default() -> Self { + Self::new(T::default()) + } +} diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs index 51fec13..e7c8ca5 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -10,35 +10,80 @@ use std::borrow::Cow; #[must_use] pub fn header_bar<'a, Message>() -> HeaderBar<'a, Message> { HeaderBar { - title: "".into(), + title: Cow::Borrowed(""), on_close: None, on_drag: None, on_maximize: None, on_minimize: None, - start: None, - center: None, - end: None, + start: Vec::new(), + center: Vec::new(), + end: Vec::new(), } } #[derive(Setters)] pub struct HeaderBar<'a, Message> { - #[setters(into)] + /// Defines the title of the window + #[setters(skip)] title: Cow<'a, str>, + + /// A message emitted when the close button is pressed. #[setters(strip_option)] on_close: Option, + + /// A message emitted when dragged. #[setters(strip_option)] on_drag: Option, + + /// A message emitted when the maximize button is pressed. #[setters(strip_option)] on_maximize: Option, + + /// A message emitted when the minimize button is pressed. #[setters(strip_option)] on_minimize: Option, - #[setters(strip_option)] - start: Option>, - #[setters(strip_option)] - center: Option>, - #[setters(strip_option)] - end: Option>, + + /// Elements packed at the start of the headerbar. + #[setters(skip)] + start: Vec>, + + /// Elements packed in the center of the headerbar. + #[setters(skip)] + center: Vec>, + + /// Elements packed at the end of the headerbar. + #[setters(skip)] + end: Vec>, +} + +impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { + /// Defines the title of the window + #[must_use] + pub fn title(mut self, title: impl Into> + 'a) -> Self { + self.title = title.into(); + self + } + + /// Pushes an element to the start region. + #[must_use] + pub fn start(mut self, widget: impl Into> + 'a) -> Self { + self.start.push(widget.into()); + self + } + + /// Pushes an element to the center region. + #[must_use] + pub fn center(mut self, widget: impl Into> + 'a) -> Self { + self.center.push(widget.into()); + self + } + + /// Pushes an element to the end region. + #[must_use] + pub fn end(mut self, widget: impl Into> + 'a) -> Self { + self.end.push(widget.into()); + self + } } impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { @@ -46,16 +91,28 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { pub fn into_element(mut self) -> Element<'a, Message> { let mut packed: Vec> = Vec::with_capacity(4); - if let Some(start) = self.start.take() { + // Take ownership of the regions to be packed. + let start = std::mem::take(&mut self.start); + let center = std::mem::take(&mut self.center); + let mut end = std::mem::take(&mut self.end); + + // If elements exist in the start region, append them here. + if !start.is_empty() { packed.push( - widget::container(start) + iced::widget::row(start) + .align_items(iced::Alignment::Center) + .apply(iced::widget::container) .align_x(iced::alignment::Horizontal::Left) .into(), ); } - packed.push(if let Some(center) = self.center.take() { - widget::container(center) + // If elements exist in the center region, use them here. + // This will otherwise use the title as a widget if a title was defined. + packed.push(if !center.is_empty() { + iced::widget::row(center) + .align_items(iced::Alignment::Center) + .apply(iced::widget::container) .align_x(iced::alignment::Horizontal::Center) .into() } else if self.title.is_empty() { @@ -64,15 +121,17 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { self.title_widget() }); - packed.push(if let Some(end) = self.end.take() { - widget::row(vec![end, self.window_controls()]) + // Also packs the window controls at the very end. + end.push(self.window_controls()); + packed.push( + iced::widget::row(end) + .align_items(iced::Alignment::Center) .apply(widget::container) .align_x(iced::alignment::Horizontal::Right) - .into() - } else { - self.window_controls() - }); + .into(), + ); + // Creates the headerbar widget. let mut widget = widget::row(packed) .height(Length::Fixed(50.0)) .padding(8) @@ -82,10 +141,12 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { .center_y() .apply(widget::mouse_area); + // Assigns a message to emit when the headerbar is dragged. if let Some(message) = self.on_drag.clone() { widget = widget.on_press(message); } + // Assigns a message to emit when the headerbar is double-clicked. if let Some(message) = self.on_maximize.clone() { widget = widget.on_release(message); } diff --git a/src/widget/icon.rs b/src/widget/icon.rs index 9e31eea..cc91dec 100644 --- a/src/widget/icon.rs +++ b/src/widget/icon.rs @@ -260,7 +260,7 @@ impl<'a> Icon<'a> { self.hash(&mut hasher); if self.theme.is_none() { - crate::settings::DEFAULT_ICON_THEME.with(|f| f.borrow().hash(&mut hasher)); + crate::icon_theme::DEFAULT.with(|f| f.borrow().hash(&mut hasher)); } let hash = hasher.finish(); @@ -291,7 +291,7 @@ impl<'a, Message: 'static> From> for Element<'a, Message> { #[must_use] pub fn load_icon(name: &str, size: u16, theme: Option<&str>) -> Option { - let icon = crate::settings::DEFAULT_ICON_THEME.with(|default_theme| { + let icon = crate::icon_theme::DEFAULT.with(|default_theme| { let default_theme = default_theme.borrow(); freedesktop_icons::lookup(name) .with_size(size) diff --git a/src/widget/nav_bar.rs b/src/widget/nav_bar.rs index 2a23646..3b3baf8 100644 --- a/src/widget/nav_bar.rs +++ b/src/widget/nav_bar.rs @@ -14,6 +14,9 @@ use iced_core::Color; use crate::{theme, widget::segmented_button, Theme}; +pub type Id = segmented_button::Entity; +pub type Model = segmented_button::SingleSelectModel; + /// Navigation side panel for switching between views. /// /// For details on the model, see the [`segmented_button`] module for more details. diff --git a/src/widget/nav_bar_toggle.rs b/src/widget/nav_bar_toggle.rs index 2f6012e..ee9d9fe 100644 --- a/src/widget/nav_bar_toggle.rs +++ b/src/widget/nav_bar_toggle.rs @@ -12,23 +12,23 @@ use super::IconSource; #[derive(Setters)] pub struct NavBarToggle { - nav_bar_active: bool, + active: bool, #[setters(strip_option)] - on_nav_bar_toggled: Option, + on_toggle: Option, } #[must_use] pub fn nav_bar_toggle() -> NavBarToggle { NavBarToggle { - nav_bar_active: false, - on_nav_bar_toggled: None, + active: false, + on_toggle: None, } } -impl From> for Element<'static, Message> { +impl<'a, Message: 'static + Clone> From> for Element<'a, Message> { fn from(nav_bar_toggle: NavBarToggle) -> Self { let mut widget = super::icon( - if nav_bar_toggle.nav_bar_active { + if nav_bar_toggle.active { IconSource::svg_from_memory(&include_bytes!("../../res/sidebar-active.svg")[..]) } else { IconSource::from("open-menu-symbolic") @@ -41,7 +41,7 @@ impl From> for Element<'static, .padding([8, 16, 8, 16]) .style(theme::Button::Text); - if let Some(message) = nav_bar_toggle.on_nav_bar_toggled { + if let Some(message) = nav_bar_toggle.on_toggle { widget = widget.on_press(message); }