diff --git a/Cargo.toml b/Cargo.toml index 3d42ed05..738e71fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,11 @@ [package] -name = "libcosmic" -version = "1.0.0" +# Yoda fork: hard-renamed. Every consumer (leyoda/cosmic-files fork + each +# leyoda/cosmic-* app) depends directly on `libcosmic-yoda` by path, bypassing +# pop-os/libcosmic entirely. No [patch] shenanigans needed — transitive deps +# that used to ask for `libcosmic` are replaced by deps on our forks that ask +# for `libcosmic-yoda`. +name = "libcosmic-yoda" +version = "0.1.0-yoda.2" edition = "2024" rust-version = "1.93" @@ -9,12 +14,10 @@ name = "cosmic" [features] default = [ - "winit", "tokio", "a11y", "dbus-config", - "x11", - "iced-wayland", + "wayland", "multi-window", ] advanced-shaping = ["iced/advanced-shaping"] @@ -35,7 +38,6 @@ animated-image = [ autosize = [] applet = [ "autosize", - "winit", "wayland", "tokio", "cosmic-panel-config", @@ -69,7 +71,7 @@ desktop-systemd-scope = ["desktop", "dep:zbus"] # Enables keycode serialization serde-keycode = ["iced_core/serde"] # Prevents multiple separate process instances. -single-instance = ["iced_winit/single-instance", "zbus/blocking-api", "ron"] +single-instance = ["zbus/blocking-api", "ron"] # smol async runtime smol = ["dep:smol", "iced/smol", "zbus?/async-io", "rfd?/async-std"] tokio = [ @@ -81,32 +83,34 @@ tokio = [ "cosmic-config/tokio", ] # Tokio async runtime -# Wayland window support -iced-wayland = [ +# Wayland window support (yoda fork is Wayland-only; always active in default). +# We still need iced/winit because pop-os/iced hosts the runtime dispatcher +# (`iced_winit as shell`) there — the name is a misnomer, it's the same crate +# that provides both the winit path AND the sctk/cctk wayland path. +wayland = [ "ashpd?/wayland", "autosize", + "iced/winit", "iced/wayland", "iced_winit/wayland", - "surface-message", -] -wayland = [ - "iced-wayland", "iced_runtime/cctk", "iced_winit/cctk", "iced_wgpu/cctk", "iced/cctk", + "dep:iced_winit", "dep:cctk", + "surface-message", ] surface-message = [] # multi-window support multi-window = [] # Render with wgpu wgpu = ["iced/wgpu", "iced_wgpu"] -# X11 window support via winit -winit = ["iced/winit", "iced_winit"] -winit_debug = ["winit", "debug"] -winit_tokio = ["winit", "tokio"] -winit_wgpu = ["winit", "wgpu"] +# Compat stubs — kept empty so upstream deps (cosmic-files, cosmic-text, …) +# that still ask for `winit` / `x11` features resolve cleanly against the +# yoda fork. Activating them has no effect: no code is gated on these. +winit = [] +x11 = [] # Enables XDG portal integrations xdg-portal = ["ashpd"] qr_code = ["iced/qr_code"] @@ -119,7 +123,6 @@ async-std = [ "zbus?/async-io", "iced/async-std", ] -x11 = ["iced/x11", "iced_winit/x11"] [dependencies] apply = "0.3.0" @@ -127,10 +130,10 @@ ashpd = { version = "0.12.3", default-features = false, optional = true } async-fs = { version = "2.2", optional = true } async-std = { workspace = true, optional = true } auto_enums = "0.8.8" -cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "c253ec1", optional = true } +cctk = { path = "../cosmic-protocols/client-toolkit", package = "cosmic-client-toolkit", optional = true } jiff = "0.2" cosmic-config = { path = "cosmic-config" } -cosmic-settings-config = { git = "https://github.com/pop-os/cosmic-settings-daemon", optional = true } +cosmic-settings-config = { path = "../cosmic-settings-daemon/config", optional = true } # Internationalization i18n-embed = { version = "0.16.0", features = [ "fluent-system", @@ -174,11 +177,11 @@ ron = { workspace = true, optional = true } # Enable DBus feature on Linux targets [target.'cfg(target_os = "linux")'.dependencies] cosmic-config = { path = "cosmic-config", features = ["dbus"] } -cosmic-settings-daemon = { git = "https://github.com/pop-os/dbus-settings-bindings" } -zbus.workspace = true +cosmic-settings-daemon = { path = "../dbus-settings-bindings/cosmic-settings-daemon" } +zbus = { version = "5.14.0", default-features = false } [target.'cfg(all(unix, not(target_os = "macos")))'.dependencies] -freedesktop-icons = { package = "cosmic-freedesktop-icons", git = "https://github.com/pop-os/freedesktop-icons" } +freedesktop-icons = { package = "cosmic-freedesktop-icons", path = "../cosmic-freedesktop-icons" } freedesktop-desktop-entry = { version = "0.8.1", optional = true } shlex = { version = "1.3.0", optional = true } @@ -224,28 +227,58 @@ optional = true [dependencies.iced_tiny_skia] path = "./iced/tiny_skia" +# Yoda: drop the x11 default → softbuffer no longer pulls tiny-xlib/x11-dl/etc. +default-features = false +features = ["wayland"] [dependencies.iced_winit] path = "./iced/winit" optional = true +# Yoda: drop the x11 default → winit won't pull winit-x11/tiny-xlib/x11-dl. +# Keep wayland + wayland-dlopen (default behaviour minus x11). +default-features = false +features = ["wayland", "wayland-dlopen"] [dependencies.iced_wgpu] path = "./iced/wgpu" optional = true [dependencies.cosmic-panel-config] -git = "https://github.com/pop-os/cosmic-panel" -# path = "../cosmic-panel/cosmic-panel-config" +path = "../cosmic-panel/cosmic-panel-config" optional = true +[patch.'https://github.com/pop-os/freedesktop-icons'] +cosmic-freedesktop-icons = { path = "../cosmic-freedesktop-icons" } + +[patch.'https://github.com/pop-os/softbuffer'] +softbuffer = { path = "../softbuffer" } + +[patch.'https://github.com/pop-os/smithay-clipboard'] +smithay-clipboard = { path = "../smithay-clipboard" } + +[patch.'https://github.com/pop-os/winit.git'] +dpi = { path = "../winit/dpi" } +winit = { path = "../winit/winit" } +winit-android = { path = "../winit/winit-android" } +winit-appkit = { path = "../winit/winit-appkit" } +winit-common = { path = "../winit/winit-common" } +winit-core = { path = "../winit/winit-core" } +winit-orbital = { path = "../winit/winit-orbital" } +winit-uikit = { path = "../winit/winit-uikit" } +winit-wayland = { path = "../winit/winit-wayland" } +winit-web = { path = "../winit/winit-web" } +winit-win32 = { path = "../winit/winit-win32" } +winit-x11 = { path = "../winit/winit-x11" } + [workspace] members = [ "cosmic-config", "cosmic-config-derive", "cosmic-theme", - "examples/*", ] -exclude = ["iced"] +# examples/* excluded — many depend on the removed winit/x11 features. +# They will be revisited and adapted in a later phase. +exclude = ["iced", "examples"] [workspace.dependencies] async-std = "1.13" @@ -258,5 +291,13 @@ tracing = "0.1" tokio = "1.52" zbus = {version = "5.15", default-features = false} +# Speed up snapshot diffing in cosmic-theme tests. Cargo silently ignores +# [profile.*] blocks in non-root manifests, so this lives at the +# workspace root. +[profile.dev.package.insta] +opt-level = 3 +[profile.dev.package.similar] +opt-level = 3 + [dev-dependencies] tempfile = "3.27.0" diff --git a/cosmic-config/Cargo.toml b/cosmic-config/Cargo.toml index 0316c707..a81d13fd 100644 --- a/cosmic-config/Cargo.toml +++ b/cosmic-config/Cargo.toml @@ -10,8 +10,8 @@ macro = ["cosmic-config-derive"] subscription = ["iced_futures"] [dependencies] -cosmic-settings-daemon = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true } -zbus = { workspace = true, default-features = false, optional = true } +cosmic-settings-daemon = { path = "../../dbus-settings-bindings/cosmic-settings-daemon", optional = true } +zbus = { version = "5.14.0", default-features = false, optional = true } atomicwrites = { git = "https://github.com/jackpot51/rust-atomicwrites" } calloop = { version = "0.14.4", optional = true } notify = "8.2.0" diff --git a/cosmic-theme/Cargo.toml b/cosmic-theme/Cargo.toml index 15aee334..b5bffa17 100644 --- a/cosmic-theme/Cargo.toml +++ b/cosmic-theme/Cargo.toml @@ -34,6 +34,3 @@ thiserror.workspace = true [dev-dependencies] insta = "1.47.2" -[profile.dev.package] -insta.opt-level = 3 -similar.opt-level = 3 diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index a403403c..0c06005e 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -955,6 +955,12 @@ impl ThemeBuilder { } #[allow(clippy::too_many_lines)] + // The component_hovered/pressed_overlay vars are seeded once near the + // top of this fn and then reassigned inside each container block + // (primary, secondary, …) before being read again. The initial seed + // is therefore overwritten before any read, which is what the + // unused_assignments lint flags below. + #[allow(unused_assignments)] /// build the theme pub fn build(self) -> Theme { let Self { diff --git a/cosmic-theme/src/output/gtk4_output.rs b/cosmic-theme/src/output/gtk4_output.rs index 16a3c36e..bbb4f246 100644 --- a/cosmic-theme/src/output/gtk4_output.rs +++ b/cosmic-theme/src/output/gtk4_output.rs @@ -1,12 +1,13 @@ -use crate::composite::over; -use crate::steps::steps; -use crate::{Component, Theme}; -use palette::rgb::Rgba; -use palette::{Darken, IntoColor, Lighten, Srgba, WithAlpha}; -use std::fs::{self, File}; -use std::io::{self, Write}; -use std::num::NonZeroUsize; -use std::path::Path; +use crate::{Component, Theme, composite::over, steps::steps}; +use configparser::ini::Ini; +use palette::{Darken, IntoColor, Lighten, Srgba, WithAlpha, rgb::Rgba}; +use std::{ + fs::{self, File}, + io::{self, Write}, + num::NonZeroUsize, + path::Path, + process::Command, +}; use super::{OutputError, to_rgba}; @@ -218,6 +219,50 @@ impl Theme { Ok(()) } + /// Apply the preferred GTK client-side decoration button layout. + /// + /// This writes the GTK 3/4 `settings.ini` value used by GTK header bars and + /// also best-effort updates GNOME's `button-layout` GSettings key for apps + /// that still consult it. + /// + /// # Errors + /// + /// Returns an `OutputError` if the GTK settings files cannot be written. + #[cold] + pub fn apply_gtk_decoration_layout(buttons_at_start: bool) -> Result<(), OutputError> { + let Some(config_dir) = dirs::config_dir() else { + return Err(OutputError::MissingConfigDir); + }; + + let layout = if buttons_at_start { + "close,minimize,maximize:" + } else { + ":minimize,maximize,close" + }; + + for gtk_version in ["gtk-3.0", "gtk-4.0"] { + let gtk_dir = config_dir.join(gtk_version); + fs::create_dir_all(>k_dir).map_err(OutputError::Io)?; + Self::write_gtk_settings_key( + >k_dir.join("settings.ini"), + "gtk-decoration-layout", + layout, + )?; + } + + // best-effort: gsettings is absent on non-GNOME systems + let _ = Command::new("gsettings") + .args([ + "set", + "org.gnome.desktop.wm.preferences", + "button-layout", + layout, + ]) + .status(); + + Ok(()) + } + /// Reset the applied gtk css /// /// # Errors @@ -257,6 +302,20 @@ impl Theme { Ok(()) } + #[cold] + fn write_gtk_settings_key(path: &Path, key: &str, value: &str) -> Result<(), OutputError> { + let mut ini = Ini::new_cs(); + + if path.exists() { + let file_content = fs::read_to_string(path).map_err(OutputError::Io)?; + ini.read(file_content).map_err(OutputError::Ini)?; + } + + ini.setstr("Settings", key, Some(value)); + ini.pretty_write(path, &super::qt_settings_ini_style()) + .map_err(OutputError::Io) + } + fn is_cosmic_css(path: &Path, cosmic_css: &Path) -> io::Result> { if !path.exists() { return Ok(None); diff --git a/cosmic-theme/src/output/vs_code.rs b/cosmic-theme/src/output/vs_code.rs index 43c36bb6..f49c888f 100644 --- a/cosmic-theme/src/output/vs_code.rs +++ b/cosmic-theme/src/output/vs_code.rs @@ -266,6 +266,14 @@ impl From for VsTheme { } impl Theme { + /// Write this theme to VS Code's `settings.json` as a + /// `workbench.colorCustomizations` entry, and enable + /// `window.autoDetectColorScheme` so VS Code follows the system theme. + /// + /// # Errors + /// + /// Returns an `OutputError` if the user config dir is missing, the + /// settings file cannot be read/written, or its JSON is invalid. #[cold] pub fn apply_vs_code(self) -> Result<(), OutputError> { let vs_theme = VsTheme::from(self); @@ -291,6 +299,13 @@ impl Theme { Ok(()) } + /// Remove the `workbench.colorCustomizations` entry previously written + /// by [`Theme::apply_vs_code`] from VS Code's `settings.json`. + /// + /// # Errors + /// + /// Returns an `OutputError` if the user config dir is missing, the + /// settings file cannot be read/written, or its JSON is invalid. #[cold] pub fn reset_vs_code() -> Result<(), OutputError> { let mut config_dir = dirs::config_dir().ok_or(OutputError::MissingConfigDir)?; diff --git a/examples/about/Cargo.toml b/examples/about/Cargo.toml index f980811c..b27b5134 100644 --- a/examples/about/Cargo.toml +++ b/examples/about/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" [dependencies] open = "5.3.3" -[dependencies.libcosmic] +[dependencies.libcosmic-yoda] path = "../../" features = [ "debug", diff --git a/examples/applet/Cargo.toml b/examples/applet/Cargo.toml index 13eff684..265fbe7f 100644 --- a/examples/applet/Cargo.toml +++ b/examples/applet/Cargo.toml @@ -12,7 +12,7 @@ tracing = "0.1" env_logger = "0.10.2" log = "0.4.29" -[dependencies.libcosmic] +[dependencies.libcosmic-yoda] path = "../../" default-features = false features = ["applet-token"] diff --git a/examples/applet/src/window.rs b/examples/applet/src/window.rs index 57546ab1..8722a2a4 100644 --- a/examples/applet/src/window.rs +++ b/examples/applet/src/window.rs @@ -123,8 +123,6 @@ impl cosmic::Application for Window { }, Some(Box::new(move |state: &Window| { let content_list = list_column() - .padding(5) - .spacing(0) .add(settings::item( "Example row", cosmic::widget::container( diff --git a/examples/application/Cargo.toml b/examples/application/Cargo.toml index 7a6083e0..d4c75171 100644 --- a/examples/application/Cargo.toml +++ b/examples/application/Cargo.toml @@ -5,12 +5,12 @@ edition = "2021" [features] default = ["wayland"] -wayland = ["libcosmic/wayland"] +wayland = ["libcosmic-yoda/wayland"] [dependencies] env_logger = "0.11" -[dependencies.libcosmic] +[dependencies.libcosmic-yoda] path = "../../" features = [ "debug", diff --git a/examples/calendar/Cargo.toml b/examples/calendar/Cargo.toml index b7286825..203f7c17 100644 --- a/examples/calendar/Cargo.toml +++ b/examples/calendar/Cargo.toml @@ -8,6 +8,6 @@ edition = "2024" [dependencies] jiff = "0.2" -[dependencies.libcosmic] +[dependencies.libcosmic-yoda] path = "../../" features = ["debug", "winit", "tokio", "xdg-portal", "wgpu"] diff --git a/examples/context-menu/Cargo.toml b/examples/context-menu/Cargo.toml index 39c550f4..4c1eed60 100644 --- a/examples/context-menu/Cargo.toml +++ b/examples/context-menu/Cargo.toml @@ -8,7 +8,7 @@ tracing = "0.1.44" tracing-subscriber = "0.3.22" tracing-log = "0.2.0" -[dependencies.libcosmic] +[dependencies.libcosmic-yoda] path = "../../" features = [ "debug", diff --git a/examples/cosmic/Cargo.toml b/examples/cosmic/Cargo.toml index 8c2a3126..eebf6c31 100644 --- a/examples/cosmic/Cargo.toml +++ b/examples/cosmic/Cargo.toml @@ -8,7 +8,7 @@ publish = false [dependencies] apply = "0.3.0" fraction = "0.15.3" -libcosmic = { path = "../..", features = [ +libcosmic-yoda = { path = "../..", features = [ "debug", "winit", "tokio", diff --git a/examples/image-button/Cargo.toml b/examples/image-button/Cargo.toml index c219a53b..8bc521f7 100644 --- a/examples/image-button/Cargo.toml +++ b/examples/image-button/Cargo.toml @@ -7,6 +7,6 @@ edition = "2021" tracing = "0.1.44" tracing-subscriber = "0.3.22" -[dependencies.libcosmic] +[dependencies.libcosmic-yoda] path = "../../" features = ["debug", "winit", "wgpu", "tokio"] diff --git a/examples/menu/Cargo.toml b/examples/menu/Cargo.toml index 430b26ea..047055e5 100644 --- a/examples/menu/Cargo.toml +++ b/examples/menu/Cargo.toml @@ -8,6 +8,6 @@ tracing = "0.1.44" tracing-subscriber = "0.3.22" tracing-log = "0.2.0" -[dependencies.libcosmic] +[dependencies.libcosmic-yoda] path = "../../" features = ["debug", "winit", "tokio", "xdg-portal", "wgpu"] diff --git a/examples/multi-window/Cargo.toml b/examples/multi-window/Cargo.toml index 0b5440f8..c38595fa 100644 --- a/examples/multi-window/Cargo.toml +++ b/examples/multi-window/Cargo.toml @@ -6,4 +6,4 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -libcosmic = { path = "../..", features = ["debug", "winit", "tokio", "single-instance", "wgpu", "wayland"] } +libcosmic-yoda = { path = "../..", features = ["debug", "winit", "tokio", "single-instance", "wgpu", "wayland"] } diff --git a/examples/nav-context/Cargo.toml b/examples/nav-context/Cargo.toml index d829df0f..ea2bc2b8 100644 --- a/examples/nav-context/Cargo.toml +++ b/examples/nav-context/Cargo.toml @@ -8,6 +8,6 @@ tracing = "0.1.44" tracing-subscriber = "0.3.22" tracing-log = "0.2.0" -[dependencies.libcosmic] +[dependencies.libcosmic-yoda] path = "../../" features = ["debug", "winit", "tokio", "xdg-portal", "wgpu"] diff --git a/examples/open-dialog/Cargo.toml b/examples/open-dialog/Cargo.toml index 94049270..b09b98ca 100644 --- a/examples/open-dialog/Cargo.toml +++ b/examples/open-dialog/Cargo.toml @@ -5,8 +5,8 @@ edition = "2021" [features] default = ["xdg-portal"] -rfd = ["libcosmic/rfd"] -xdg-portal = ["libcosmic/xdg-portal"] +rfd = ["libcosmic-yoda/rfd"] +xdg-portal = ["libcosmic-yoda/xdg-portal"] [dependencies] apply = "0.3.0" @@ -15,6 +15,6 @@ tracing = "0.1.44" tracing-subscriber = "0.3.22" url = "2.5.8" -[dependencies.libcosmic] +[dependencies.libcosmic-yoda] features = ["debug", "winit", "wgpu", "wayland", "tokio"] path = "../../" diff --git a/examples/spin-button/Cargo.toml b/examples/spin-button/Cargo.toml index a522050b..082c0fd5 100644 --- a/examples/spin-button/Cargo.toml +++ b/examples/spin-button/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" [dependencies] fraction = "0.15.3" -[dependencies.libcosmic] +[dependencies.libcosmic-yoda] features = ["debug", "wgpu", "winit", "desktop", "tokio"] path = "../.." default-features = false diff --git a/examples/subscriptions/Cargo.toml b/examples/subscriptions/Cargo.toml index 8eb69ff3..ae31a395 100644 --- a/examples/subscriptions/Cargo.toml +++ b/examples/subscriptions/Cargo.toml @@ -5,6 +5,6 @@ edition = "2024" [dependencies] -[dependencies.libcosmic] +[dependencies.libcosmic-yoda] path = "../../" features = ["debug", "winit", "wgpu", "tokio", "xdg-portal"] diff --git a/examples/table-view/Cargo.toml b/examples/table-view/Cargo.toml index 8ed45928..8f71e5b4 100644 --- a/examples/table-view/Cargo.toml +++ b/examples/table-view/Cargo.toml @@ -9,6 +9,6 @@ tracing-subscriber = "0.3.22" tracing-log = "0.2.0" chrono = "*" -[dependencies.libcosmic] +[dependencies.libcosmic-yoda] features = ["debug", "wgpu", "winit", "desktop", "tokio"] path = "../.." diff --git a/examples/text-input/Cargo.toml b/examples/text-input/Cargo.toml index fe6105c2..69bd2a19 100644 --- a/examples/text-input/Cargo.toml +++ b/examples/text-input/Cargo.toml @@ -8,6 +8,6 @@ tracing = "0.1.44" tracing-subscriber = "0.3.22" tracing-log = "0.2.0" -[dependencies.libcosmic] +[dependencies.libcosmic-yoda] path = "../../" features = ["debug", "winit", "wgpu", "tokio", "xdg-portal"] diff --git a/i18n/af/libcosmic.ftl b/i18n/af/libcosmic_yoda.ftl similarity index 100% rename from i18n/af/libcosmic.ftl rename to i18n/af/libcosmic_yoda.ftl diff --git a/i18n/ar/libcosmic.ftl b/i18n/ar/libcosmic_yoda.ftl similarity index 100% rename from i18n/ar/libcosmic.ftl rename to i18n/ar/libcosmic_yoda.ftl diff --git a/i18n/be/libcosmic.ftl b/i18n/be/libcosmic_yoda.ftl similarity index 100% rename from i18n/be/libcosmic.ftl rename to i18n/be/libcosmic_yoda.ftl diff --git a/i18n/bg/libcosmic.ftl b/i18n/bg/libcosmic_yoda.ftl similarity index 100% rename from i18n/bg/libcosmic.ftl rename to i18n/bg/libcosmic_yoda.ftl diff --git a/i18n/bn/libcosmic.ftl b/i18n/bn/libcosmic_yoda.ftl similarity index 100% rename from i18n/bn/libcosmic.ftl rename to i18n/bn/libcosmic_yoda.ftl diff --git a/i18n/ca/libcosmic.ftl b/i18n/ca/libcosmic_yoda.ftl similarity index 100% rename from i18n/ca/libcosmic.ftl rename to i18n/ca/libcosmic_yoda.ftl diff --git a/i18n/cs/libcosmic.ftl b/i18n/cs/libcosmic_yoda.ftl similarity index 100% rename from i18n/cs/libcosmic.ftl rename to i18n/cs/libcosmic_yoda.ftl diff --git a/i18n/da/libcosmic.ftl b/i18n/da/libcosmic_yoda.ftl similarity index 100% rename from i18n/da/libcosmic.ftl rename to i18n/da/libcosmic_yoda.ftl diff --git a/i18n/de/libcosmic.ftl b/i18n/de/libcosmic_yoda.ftl similarity index 100% rename from i18n/de/libcosmic.ftl rename to i18n/de/libcosmic_yoda.ftl diff --git a/i18n/el/libcosmic.ftl b/i18n/el/libcosmic_yoda.ftl similarity index 100% rename from i18n/el/libcosmic.ftl rename to i18n/el/libcosmic_yoda.ftl diff --git a/i18n/en-GB/libcosmic.ftl b/i18n/en-GB/libcosmic_yoda.ftl similarity index 100% rename from i18n/en-GB/libcosmic.ftl rename to i18n/en-GB/libcosmic_yoda.ftl diff --git a/i18n/en/libcosmic.ftl b/i18n/en/libcosmic_yoda.ftl similarity index 100% rename from i18n/en/libcosmic.ftl rename to i18n/en/libcosmic_yoda.ftl diff --git a/i18n/eo/libcosmic.ftl b/i18n/eo/libcosmic_yoda.ftl similarity index 100% rename from i18n/eo/libcosmic.ftl rename to i18n/eo/libcosmic_yoda.ftl diff --git a/i18n/es-419/libcosmic.ftl b/i18n/es-419/libcosmic_yoda.ftl similarity index 100% rename from i18n/es-419/libcosmic.ftl rename to i18n/es-419/libcosmic_yoda.ftl diff --git a/i18n/es-MX/libcosmic.ftl b/i18n/es-MX/libcosmic_yoda.ftl similarity index 100% rename from i18n/es-MX/libcosmic.ftl rename to i18n/es-MX/libcosmic_yoda.ftl diff --git a/i18n/es/libcosmic.ftl b/i18n/es/libcosmic_yoda.ftl similarity index 100% rename from i18n/es/libcosmic.ftl rename to i18n/es/libcosmic_yoda.ftl diff --git a/i18n/et/libcosmic.ftl b/i18n/et/libcosmic_yoda.ftl similarity index 100% rename from i18n/et/libcosmic.ftl rename to i18n/et/libcosmic_yoda.ftl diff --git a/i18n/eu/libcosmic.ftl b/i18n/eu/libcosmic_yoda.ftl similarity index 100% rename from i18n/eu/libcosmic.ftl rename to i18n/eu/libcosmic_yoda.ftl diff --git a/i18n/fa/libcosmic.ftl b/i18n/fa/libcosmic_yoda.ftl similarity index 100% rename from i18n/fa/libcosmic.ftl rename to i18n/fa/libcosmic_yoda.ftl diff --git a/i18n/fi/libcosmic.ftl b/i18n/fi/libcosmic_yoda.ftl similarity index 100% rename from i18n/fi/libcosmic.ftl rename to i18n/fi/libcosmic_yoda.ftl diff --git a/i18n/fr/libcosmic.ftl b/i18n/fr/libcosmic_yoda.ftl similarity index 100% rename from i18n/fr/libcosmic.ftl rename to i18n/fr/libcosmic_yoda.ftl diff --git a/i18n/fy/libcosmic.ftl b/i18n/fy/libcosmic_yoda.ftl similarity index 100% rename from i18n/fy/libcosmic.ftl rename to i18n/fy/libcosmic_yoda.ftl diff --git a/i18n/ga/libcosmic.ftl b/i18n/ga/libcosmic_yoda.ftl similarity index 100% rename from i18n/ga/libcosmic.ftl rename to i18n/ga/libcosmic_yoda.ftl diff --git a/i18n/gd/libcosmic.ftl b/i18n/gd/libcosmic_yoda.ftl similarity index 100% rename from i18n/gd/libcosmic.ftl rename to i18n/gd/libcosmic_yoda.ftl diff --git a/i18n/gu/libcosmic.ftl b/i18n/gu/libcosmic_yoda.ftl similarity index 100% rename from i18n/gu/libcosmic.ftl rename to i18n/gu/libcosmic_yoda.ftl diff --git a/i18n/he/libcosmic.ftl b/i18n/he/libcosmic_yoda.ftl similarity index 100% rename from i18n/he/libcosmic.ftl rename to i18n/he/libcosmic_yoda.ftl diff --git a/i18n/hi/libcosmic.ftl b/i18n/hi/libcosmic_yoda.ftl similarity index 100% rename from i18n/hi/libcosmic.ftl rename to i18n/hi/libcosmic_yoda.ftl diff --git a/i18n/hr/libcosmic.ftl b/i18n/hr/libcosmic_yoda.ftl similarity index 100% rename from i18n/hr/libcosmic.ftl rename to i18n/hr/libcosmic_yoda.ftl diff --git a/i18n/hu/libcosmic.ftl b/i18n/hu/libcosmic_yoda.ftl similarity index 100% rename from i18n/hu/libcosmic.ftl rename to i18n/hu/libcosmic_yoda.ftl diff --git a/i18n/id/libcosmic.ftl b/i18n/id/libcosmic_yoda.ftl similarity index 100% rename from i18n/id/libcosmic.ftl rename to i18n/id/libcosmic_yoda.ftl diff --git a/i18n/ie/libcosmic.ftl b/i18n/ie/libcosmic_yoda.ftl similarity index 100% rename from i18n/ie/libcosmic.ftl rename to i18n/ie/libcosmic_yoda.ftl diff --git a/i18n/is/libcosmic.ftl b/i18n/is/libcosmic_yoda.ftl similarity index 100% rename from i18n/is/libcosmic.ftl rename to i18n/is/libcosmic_yoda.ftl diff --git a/i18n/it/libcosmic.ftl b/i18n/it/libcosmic_yoda.ftl similarity index 100% rename from i18n/it/libcosmic.ftl rename to i18n/it/libcosmic_yoda.ftl diff --git a/i18n/ja/libcosmic.ftl b/i18n/ja/libcosmic_yoda.ftl similarity index 100% rename from i18n/ja/libcosmic.ftl rename to i18n/ja/libcosmic_yoda.ftl diff --git a/i18n/jv/libcosmic.ftl b/i18n/jv/libcosmic_yoda.ftl similarity index 100% rename from i18n/jv/libcosmic.ftl rename to i18n/jv/libcosmic_yoda.ftl diff --git a/i18n/ka/libcosmic.ftl b/i18n/ka/libcosmic_yoda.ftl similarity index 100% rename from i18n/ka/libcosmic.ftl rename to i18n/ka/libcosmic_yoda.ftl diff --git a/i18n/kab/libcosmic.ftl b/i18n/kab/libcosmic_yoda.ftl similarity index 100% rename from i18n/kab/libcosmic.ftl rename to i18n/kab/libcosmic_yoda.ftl diff --git a/i18n/kk/libcosmic.ftl b/i18n/kk/libcosmic_yoda.ftl similarity index 100% rename from i18n/kk/libcosmic.ftl rename to i18n/kk/libcosmic_yoda.ftl diff --git a/i18n/kmr/libcosmic.ftl b/i18n/kmr/libcosmic_yoda.ftl similarity index 100% rename from i18n/kmr/libcosmic.ftl rename to i18n/kmr/libcosmic_yoda.ftl diff --git a/i18n/kn/libcosmic.ftl b/i18n/kn/libcosmic_yoda.ftl similarity index 100% rename from i18n/kn/libcosmic.ftl rename to i18n/kn/libcosmic_yoda.ftl diff --git a/i18n/ko/libcosmic.ftl b/i18n/ko/libcosmic_yoda.ftl similarity index 100% rename from i18n/ko/libcosmic.ftl rename to i18n/ko/libcosmic_yoda.ftl diff --git a/i18n/li/libcosmic.ftl b/i18n/li/libcosmic_yoda.ftl similarity index 100% rename from i18n/li/libcosmic.ftl rename to i18n/li/libcosmic_yoda.ftl diff --git a/i18n/lt/libcosmic.ftl b/i18n/lt/libcosmic_yoda.ftl similarity index 100% rename from i18n/lt/libcosmic.ftl rename to i18n/lt/libcosmic_yoda.ftl diff --git a/i18n/ml/libcosmic.ftl b/i18n/ml/libcosmic_yoda.ftl similarity index 100% rename from i18n/ml/libcosmic.ftl rename to i18n/ml/libcosmic_yoda.ftl diff --git a/i18n/ms/libcosmic.ftl b/i18n/ms/libcosmic_yoda.ftl similarity index 100% rename from i18n/ms/libcosmic.ftl rename to i18n/ms/libcosmic_yoda.ftl diff --git a/i18n/nb-NO/libcosmic.ftl b/i18n/nb-NO/libcosmic_yoda.ftl similarity index 100% rename from i18n/nb-NO/libcosmic.ftl rename to i18n/nb-NO/libcosmic_yoda.ftl diff --git a/i18n/nl/libcosmic.ftl b/i18n/nl/libcosmic_yoda.ftl similarity index 100% rename from i18n/nl/libcosmic.ftl rename to i18n/nl/libcosmic_yoda.ftl diff --git a/i18n/nn/libcosmic.ftl b/i18n/nn/libcosmic_yoda.ftl similarity index 100% rename from i18n/nn/libcosmic.ftl rename to i18n/nn/libcosmic_yoda.ftl diff --git a/i18n/oc/libcosmic.ftl b/i18n/oc/libcosmic_yoda.ftl similarity index 100% rename from i18n/oc/libcosmic.ftl rename to i18n/oc/libcosmic_yoda.ftl diff --git a/i18n/pa/libcosmic.ftl b/i18n/pa/libcosmic_yoda.ftl similarity index 100% rename from i18n/pa/libcosmic.ftl rename to i18n/pa/libcosmic_yoda.ftl diff --git a/i18n/pl/libcosmic.ftl b/i18n/pl/libcosmic_yoda.ftl similarity index 100% rename from i18n/pl/libcosmic.ftl rename to i18n/pl/libcosmic_yoda.ftl diff --git a/i18n/pt-BR/libcosmic.ftl b/i18n/pt-BR/libcosmic_yoda.ftl similarity index 100% rename from i18n/pt-BR/libcosmic.ftl rename to i18n/pt-BR/libcosmic_yoda.ftl diff --git a/i18n/pt/libcosmic.ftl b/i18n/pt/libcosmic_yoda.ftl similarity index 100% rename from i18n/pt/libcosmic.ftl rename to i18n/pt/libcosmic_yoda.ftl diff --git a/i18n/ro/libcosmic.ftl b/i18n/ro/libcosmic_yoda.ftl similarity index 100% rename from i18n/ro/libcosmic.ftl rename to i18n/ro/libcosmic_yoda.ftl diff --git a/i18n/ru/libcosmic.ftl b/i18n/ru/libcosmic_yoda.ftl similarity index 100% rename from i18n/ru/libcosmic.ftl rename to i18n/ru/libcosmic_yoda.ftl diff --git a/i18n/sk/libcosmic.ftl b/i18n/sk/libcosmic_yoda.ftl similarity index 100% rename from i18n/sk/libcosmic.ftl rename to i18n/sk/libcosmic_yoda.ftl diff --git a/i18n/sl/libcosmic.ftl b/i18n/sl/libcosmic_yoda.ftl similarity index 100% rename from i18n/sl/libcosmic.ftl rename to i18n/sl/libcosmic_yoda.ftl diff --git a/i18n/sr-Cyrl/libcosmic.ftl b/i18n/sr-Cyrl/libcosmic_yoda.ftl similarity index 100% rename from i18n/sr-Cyrl/libcosmic.ftl rename to i18n/sr-Cyrl/libcosmic_yoda.ftl diff --git a/i18n/sr-Latn/libcosmic.ftl b/i18n/sr-Latn/libcosmic_yoda.ftl similarity index 100% rename from i18n/sr-Latn/libcosmic.ftl rename to i18n/sr-Latn/libcosmic_yoda.ftl diff --git a/i18n/sr/libcosmic.ftl b/i18n/sr/libcosmic_yoda.ftl similarity index 100% rename from i18n/sr/libcosmic.ftl rename to i18n/sr/libcosmic_yoda.ftl diff --git a/i18n/sv/libcosmic.ftl b/i18n/sv/libcosmic_yoda.ftl similarity index 100% rename from i18n/sv/libcosmic.ftl rename to i18n/sv/libcosmic_yoda.ftl diff --git a/i18n/ta/libcosmic.ftl b/i18n/ta/libcosmic_yoda.ftl similarity index 100% rename from i18n/ta/libcosmic.ftl rename to i18n/ta/libcosmic_yoda.ftl diff --git a/i18n/th/libcosmic.ftl b/i18n/th/libcosmic_yoda.ftl similarity index 100% rename from i18n/th/libcosmic.ftl rename to i18n/th/libcosmic_yoda.ftl diff --git a/i18n/ti/libcosmic.ftl b/i18n/ti/libcosmic_yoda.ftl similarity index 100% rename from i18n/ti/libcosmic.ftl rename to i18n/ti/libcosmic_yoda.ftl diff --git a/i18n/tr/libcosmic.ftl b/i18n/tr/libcosmic_yoda.ftl similarity index 100% rename from i18n/tr/libcosmic.ftl rename to i18n/tr/libcosmic_yoda.ftl diff --git a/i18n/uk/libcosmic.ftl b/i18n/uk/libcosmic_yoda.ftl similarity index 100% rename from i18n/uk/libcosmic.ftl rename to i18n/uk/libcosmic_yoda.ftl diff --git a/i18n/uz/libcosmic.ftl b/i18n/uz/libcosmic_yoda.ftl similarity index 100% rename from i18n/uz/libcosmic.ftl rename to i18n/uz/libcosmic_yoda.ftl diff --git a/i18n/vi/libcosmic.ftl b/i18n/vi/libcosmic_yoda.ftl similarity index 100% rename from i18n/vi/libcosmic.ftl rename to i18n/vi/libcosmic_yoda.ftl diff --git a/i18n/yue-Hant/libcosmic.ftl b/i18n/yue-Hant/libcosmic_yoda.ftl similarity index 100% rename from i18n/yue-Hant/libcosmic.ftl rename to i18n/yue-Hant/libcosmic_yoda.ftl diff --git a/i18n/zh-Hans/libcosmic.ftl b/i18n/zh-Hans/libcosmic_yoda.ftl similarity index 100% rename from i18n/zh-Hans/libcosmic.ftl rename to i18n/zh-Hans/libcosmic_yoda.ftl diff --git a/i18n/zh-Hant/libcosmic.ftl b/i18n/zh-Hant/libcosmic_yoda.ftl similarity index 100% rename from i18n/zh-Hant/libcosmic.ftl rename to i18n/zh-Hant/libcosmic_yoda.ftl diff --git a/iced b/iced index 98fb6a0f..d9fe129f 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 98fb6a0fba98cfdb6c04f33ccf0be6abad7fcfff +Subproject commit d9fe129f7316cea3ef6d2576e9421a305759b8bf diff --git a/justfile b/justfile index 4653434e..7d9aecee 100644 --- a/justfile +++ b/justfile @@ -23,6 +23,12 @@ check-json: (check '--message-format=json') clean: cargo clean +# Generate documentation +doc: + env RUSTDOCFLAGS="--cfg docsrs" cargo +nightly-2026-04-27 doc --no-deps --verbose \ + -p cosmic-client-toolkit -p cosmic-protocols -p libcosmic \ + --features tokio,winit,wayland,desktop,single-instance,applet,xdg-portal,multi-window + # Also remove .cargo and vendored dependencies clean-dist: clean rm -rf .cargo vendor vendor.tar target diff --git a/src/action.rs b/src/action.rs index b7162896..19e228b8 100644 --- a/src/action.rs +++ b/src/action.rs @@ -1,7 +1,6 @@ // Copyright 2023 System76 // SPDX-License-Identifier: MPL-2.0 -#[cfg(feature = "winit")] use crate::app; #[cfg(feature = "single-instance")] use crate::dbus_activation; @@ -9,7 +8,6 @@ use crate::dbus_activation; pub const fn app(message: M) -> Action { Action::App(message) } -#[cfg(feature = "winit")] pub const fn cosmic(message: app::Action) -> Action { Action::Cosmic(message) } @@ -23,7 +21,6 @@ pub const fn none() -> Action { pub enum Action { /// Messages from the application, for the application. App(M), - #[cfg(feature = "winit")] /// Internal messages to be handled by libcosmic. Cosmic(app::Action), #[cfg(feature = "single-instance")] diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 030ed041..86af0992 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -100,9 +100,10 @@ impl Cosmic where T::Message: Send + 'static, { - pub fn init( - (mut core, flags): (Core, T::Flags), - ) -> (Self, iced::Task>) { + pub fn init((core, flags): (Core, T::Flags)) -> (Self, iced::Task>) { + #[cfg(all(feature = "dbus-config", target_os = "linux"))] + let mut core = core; + #[cfg(all(feature = "dbus-config", target_os = "linux"))] { use iced_futures::futures::executor::block_on; @@ -364,7 +365,6 @@ where crate::surface::Action::Task(f) => { f().map(|sm| crate::Action::Cosmic(Action::Surface(sm))) } - _ => iced::Task::none(), } #[cfg(not(feature = "surface-message"))] @@ -408,7 +408,7 @@ where f64::from(self.app.core().scale_factor()) } - pub fn style(&self, theme: &Theme) -> theme::Style { + pub fn style(&self, _theme: &Theme) -> theme::Style { if let Some(style) = self.app.style() { style } else if self.app.core().window.is_maximized { @@ -480,12 +480,11 @@ where .into_iter() .filter(cosmic_config::Error::is_err) { - if let cosmic_config::Error::GetKey(_, err) = &why { - if err.kind() == std::io::ErrorKind::NotFound { + if let cosmic_config::Error::GetKey(_, err) = &why + && err.kind() == std::io::ErrorKind::NotFound { // No system default config installed; don't error continue; } - } tracing::error!(?why, "cosmic toolkit config update error"); } @@ -621,15 +620,15 @@ impl Cosmic { #[allow(clippy::too_many_lines)] fn cosmic_update(&mut self, message: Action) -> iced::Task> { match message { - Action::WindowMaximized(id, maximized) => { + Action::WindowMaximized(_id, _maximized) => { #[cfg(not(all(feature = "wayland", target_os = "linux")))] if self .app .core() .main_window_id() - .is_some_and(|main_id| main_id == id) + .is_some_and(|main_id| main_id == _id) { - self.app.core_mut().window.sharp_corners = maximized; + self.app.core_mut().window.sharp_corners = _maximized; } } diff --git a/src/app/mod.rs b/src/app/mod.rs index 264f1d16..b2d82dcd 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -136,7 +136,7 @@ pub fn run(settings: Settings, flags: App::Flags) -> iced::Res crate::malloc::limit_mmap_threshold(threshold); } - let default_font = settings.default_font; + let _default_font = settings.default_font; let (settings, (mut core, flags), window_settings) = iced_settings::(settings, flags); #[cfg(not(feature = "multi-window"))] { @@ -284,7 +284,7 @@ where // app = app.window(window_settings); core.main_window = Some(iced_core::window::Id::RESERVED); } - let mut app = iced::daemon( + let app = iced::daemon( BootData(Rc::new(RefCell::new(Some(BootDataInner:: { flags, core, diff --git a/src/applet/row.rs b/src/applet/row.rs index 718f366c..888c68ef 100644 --- a/src/applet/row.rs +++ b/src/applet/row.rs @@ -1,14 +1,13 @@ //! Distribute content horizontally. use crate::iced; use iced::core::alignment::{self, Alignment}; -use iced::core::event::{self, Event}; +use iced::core::event::Event; use iced::core::layout::{self, Layout}; use iced::core::widget::{Operation, Tree}; use iced::core::{ Clipboard, Element, Length, Padding, Pixels, Rectangle, Shell, Size, Vector, Widget, mouse, overlay, renderer, widget, }; -use iced::touch; /// A container that distributes its contents horizontally. /// diff --git a/src/command.rs b/src/command.rs index 1d6f635c..c5c1c62b 100644 --- a/src/command.rs +++ b/src/command.rs @@ -27,12 +27,10 @@ pub fn set_title(id: window::Id, title: String) -> iced::Task(factor: f32) -> iced::Task> { iced::Task::done(crate::app::Action::ScaleFactor(factor)).map(crate::Action::Cosmic) } -#[cfg(feature = "winit")] pub fn set_theme(theme: crate::Theme) -> iced::Task> { iced::Task::done(crate::app::Action::AppThemeChange(theme)).map(crate::Action::Cosmic) } diff --git a/src/config/mod.rs b/src/config/mod.rs index 9807961c..08a4c0ca 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -4,6 +4,7 @@ //! Configurations available to libcosmic applications. use crate::cosmic_theme::Density; +use crate::widget::WindowControlsPosition; use cosmic_config::cosmic_config_derive::CosmicConfigEntry; use cosmic_config::{Config, CosmicConfigEntry}; use serde::{Deserialize, Serialize}; @@ -22,12 +23,11 @@ pub static COSMIC_TK: LazyLock> = LazyLock::new(|| { .map(|c| { CosmicTk::get_entry(&c).unwrap_or_else(|(errors, mode)| { for why in errors.into_iter().filter(cosmic_config::Error::is_err) { - if let cosmic_config::Error::GetKey(_, err) = &why { - if err.kind() == std::io::ErrorKind::NotFound { + if let cosmic_config::Error::GetKey(_, err) = &why + && err.kind() == std::io::ErrorKind::NotFound { // No system default config installed; don't error continue; } - } tracing::error!(?why, "CosmicTk config entry error"); } mode @@ -67,6 +67,12 @@ pub fn header_size() -> Density { COSMIC_TK.read().unwrap().header_size } +/// Position of the window control buttons (close / minimize / maximize). +#[allow(clippy::missing_panics_doc)] +pub fn window_controls_position() -> WindowControlsPosition { + COSMIC_TK.read().unwrap().window_controls_position +} + /// Interface density. #[allow(clippy::missing_panics_doc)] pub fn interface_density() -> Density { @@ -109,6 +115,10 @@ pub struct CosmicTk { /// Mono font family pub monospace_font: FontConfig, + + /// Side on which window control buttons (close / minimize / maximize) + /// are placed. `End` = right (Linux / GNOME), `Start` = left (macOS). + pub window_controls_position: WindowControlsPosition, } impl Default for CosmicTk { @@ -132,6 +142,7 @@ impl Default for CosmicTk { stretch: iced::font::Stretch::Normal, style: iced::font::Style::Normal, }, + window_controls_position: WindowControlsPosition::default(), } } } diff --git a/src/core.rs b/src/core.rs index 970a5351..f80954f9 100644 --- a/src/core.rs +++ b/src/core.rs @@ -78,8 +78,6 @@ pub struct Core { pub(super) portal_accent: Option, - pub(super) portal_is_high_contrast: Option, - pub(super) title: HashMap, pub window: Window, @@ -155,7 +153,6 @@ impl Default for Core { settings_daemon: None, portal_is_dark: None, portal_accent: None, - portal_is_high_contrast: None, main_window: None, exit_on_main_window_closed: true, menu_bars: HashMap::new(), @@ -432,7 +429,6 @@ impl Core { id } - #[cfg(feature = "winit")] pub fn drag(&self, id: Option) -> crate::app::Task { let Some(id) = id.or(self.main_window) else { return iced::Task::none(); @@ -440,7 +436,6 @@ impl Core { crate::command::drag(id) } - #[cfg(feature = "winit")] pub fn maximize( &self, id: Option, @@ -452,7 +447,6 @@ impl Core { crate::command::maximize(id, maximized) } - #[cfg(feature = "winit")] pub fn minimize(&self, id: Option) -> crate::app::Task { let Some(id) = id.or(self.main_window) else { return iced::Task::none(); @@ -460,7 +454,6 @@ impl Core { crate::command::minimize(id) } - #[cfg(feature = "winit")] pub fn set_title( &self, id: Option, @@ -472,7 +465,6 @@ impl Core { crate::command::set_title(id, title) } - #[cfg(feature = "winit")] pub fn set_windowed(&self, id: Option) -> crate::app::Task { let Some(id) = id.or(self.main_window) else { return iced::Task::none(); @@ -480,7 +472,6 @@ impl Core { crate::command::set_windowed(id) } - #[cfg(feature = "winit")] pub fn toggle_maximize( &self, id: Option, diff --git a/src/dbus_activation.rs b/src/dbus_activation.rs index e7bc6900..880db634 100644 --- a/src/dbus_activation.rs +++ b/src/dbus_activation.rs @@ -43,7 +43,7 @@ pub fn subscription() -> Subscription( exec: S, env_vars: I, - app_id: Option<&str>, + _app_id: Option<&str>, terminal: bool, ) where S: AsRef, @@ -816,13 +816,17 @@ pub async fn spawn_desktop_exec( // https://systemd.io/DESKTOP_ENVIRONMENTS // // Similar to what Gnome sets, for now. - if let Some(pid) = crate::process::spawn(cmd).await { + if let Some(_pid) = crate::process::spawn(cmd).await { #[cfg(feature = "desktop-systemd-scope")] if let Ok(session) = zbus::Connection::session().await { if let Ok(systemd_manager) = SystemdMangerProxy::new(&session).await { let _ = systemd_manager .start_transient_unit( - &format!("app-cosmic-{}-{}.scope", app_id.unwrap_or(&executable), pid), + &format!( + "app-cosmic-{}-{}.scope", + _app_id.unwrap_or(&executable), + _pid + ), "fail", &[ ( @@ -833,7 +837,7 @@ pub async fn spawn_desktop_exec( ), ( "PIDs".to_string(), - zbus::zvariant::Value::from(vec![pid]) + zbus::zvariant::Value::from(vec![_pid]) .try_to_owned() .unwrap(), ), diff --git a/src/ext.rs b/src/ext.rs index 8eb749e5..65ca0145 100644 --- a/src/ext.rs +++ b/src/ext.rs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: MPL-2.0 use iced::Color; -use iced_core::Widget; pub trait ElementExt { #[must_use] diff --git a/src/lib.rs b/src/lib.rs index 875d9148..e22a3a1f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,9 +5,91 @@ #![cfg_attr(target_os = "redox", feature(lazy_cell))] #![cfg_attr(docsrs, feature(doc_cfg))] +//! # The COSMIC Toolkit +//! +//! Quickly code your project with modular elements created with customization in mind. +//! Panels, applets, theming, tiling, launcher, app library, keyboard shortcuts and +//! dynamic or pinned workspaces are all made flexible to bend to your users needs. +//! +//! [COSMIC](https://system76.com/cosmic) empowers frictionless development by being +//! approachable, easy to maintain, and modular. Use the same language and toolkit to +//! build apps and applets, with helpful templates provided. Even shell components and +//! the compositor use the same toolkit. Learn once and use your knowledge anywhere in +//! the desktop. +//! +//! ## Architecture +//! +//! Based on [iced](https://iced.rs/), COSMIC apps and applets are modeled with the +//! MVU (Model-View-Update) pattern from [The Elm Architecture](https://guide.elm-lang.org/architecture/). +//! For more details, see the [architecture page of the iced book](https://book.iced.rs/architecture.html). +//! +//! An application will consist of: +//! +//! * An application **model** for holding persistent application state which implements +//! the [Application] trait. +//! * A [view](Application::view) function which borrows data from the model to construct +//! a view with stateless widgets +//! * An [update](Application::update) function which receives application messages from +//! widgets, tasks, and subscriptions. +//! +//! Messages handled by the update function are used to update the +//! application model and spawn background tasks that can emit messages back to the +//! app's update function. +//! +//! ### Tasks +//! +//! Tasks returned by the update function are scheduled for concurrent execution on a +//! background thread managed by the application's async executor, which is tokio by default. +//! They can be constructed from futures and may also stream events back to the application +//! asynchronously. +//! +//! ### Subscriptions +//! +//! Applications may also use a [subscription](Application::subscription) function to subscribe to external +//! asynchronous event streams. These can run perpetually from application start; or +//! optionally started, stopped, and restarted based on changes to the application model +//! between updates. Such as: +//! +//! * Conditionally starting and stopping a subscription based on the state of a boolean +//! value or enum +//! * Restarting a subscription when the hash of its borrowed data changes +//! * Dynamically spawning a subscription for each item in a list. +//! +//! ## Templates +//! +//! Get started using [cargo-generate](https://github.com/cargo-generate/cargo-generate) +//! with one of the following templates. The app template is for developing desktop +//! applications and the applet template is for developing COSMIC applets. +//! +//! - [App Template](https://github.com/pop-os/cosmic-app-template/) +//! - [Applet Template](https://github.com/pop-os/cosmic-applet-template/) +//! +//! ## Widgets +//! +//! Reference the [`widget`] module for available widgets for use in the view function. +//! Widgets are composable and can be configured through chainable builder methods. +//! Compose widgets together to create complex interfaces and higher level widgets. +//! +//! Composed widgets may be managed by their own custom type with its own view and +//! update functions. It is a common pattern to use these functions within the +//! application's own view and update functions. If using a custom type, implement +//! [From] for the [Element] type to have API treat a composed widget the same as a +//! native custom widget. +//! +//! If a widget does not exist for a specific use case, use the [Widget](iced::advanced::Widget) +//! trait to create an advanced custom widget. This can then be used to composed higher +//! level widgets by chaining composable widgets together. +//! +//! ## Core +//! +//! Every application model requires a [cosmic::Core](app::Core). This contains +//! application state which is managed by libcosmic's runtime for its generated +//! interfaces. Such as the context drawner, nav bar, and the headerbar. This can be +//! used by the app to subscribe to configuration changes and to emit events to the +//! libcosmic-managed portion of the application's state and view. + /// Recommended default imports. pub mod prelude { - #[cfg(feature = "winit")] pub use crate::ApplicationExt; pub use crate::ext::*; pub use crate::{Also, Apply, Element, Renderer, Task, Theme}; @@ -21,9 +103,7 @@ pub use action::Action; pub mod anim; -#[cfg(feature = "winit")] pub mod app; -#[cfg(feature = "winit")] #[doc(inline)] pub use app::{Application, ApplicationExt}; @@ -66,6 +146,17 @@ pub mod font; #[doc(inline)] pub use iced; +#[doc(inline)] +pub use iced_core; +#[doc(inline)] +pub use iced_futures; +#[doc(inline)] +pub use iced_runtime; +#[doc(inline)] +pub use iced_widget; +#[doc(inline)] +#[cfg(feature = "wayland")] +pub use iced_winit; pub mod icon_theme; pub mod keyboard_nav; diff --git a/src/malloc.rs b/src/malloc.rs index b99a66f4..bc5e8353 100644 --- a/src/malloc.rs +++ b/src/malloc.rs @@ -1,21 +1,49 @@ // Copyright 2025 System76 // SPDX-License-Identifier: MPL-2.0 +use std::cell::Cell; use std::os::raw::c_int; +use std::time::{Duration, Instant}; const M_MMAP_THRESHOLD: c_int = -3; +/// Minimum interval between two actual `malloc_trim` calls. +/// +/// `trim` is called at the end of every `update()` and `view()`, which can +/// reach 60-200 Hz during typical scrolling, resize, or animation. Each +/// `malloc_trim` walks the glibc heap (10 µs to several ms depending on +/// fragmentation), so calling it at render frequency can consume a +/// substantial fraction of the frame budget. Throttling to 1 Hz keeps RSS +/// bounded while removing the per-frame syscall from the hot path. +const TRIM_MIN_INTERVAL: Duration = Duration::from_millis(1000); + +thread_local! { + static LAST_TRIM: Cell> = const { Cell::new(None) }; +} + unsafe extern "C" { fn malloc_trim(pad: usize); fn mallopt(param: c_int, value: c_int) -> c_int; } +/// Throttled wrapper over `malloc_trim`. Safe to call at render frequency: +/// consecutive calls within `TRIM_MIN_INTERVAL` (per-thread) skip the syscall. #[inline] pub fn trim(pad: usize) { - unsafe { - malloc_trim(pad); - } + LAST_TRIM.with(|last| { + let now = Instant::now(); + let should_trim = match last.get() { + None => true, + Some(prev) => now.duration_since(prev) >= TRIM_MIN_INTERVAL, + }; + if should_trim { + last.set(Some(now)); + unsafe { + malloc_trim(pad); + } + } + }); } /// Prevents glibc from hoarding memory via memory fragmentation. diff --git a/src/scroll.rs b/src/scroll.rs index b6d42378..67947399 100644 --- a/src/scroll.rs +++ b/src/scroll.rs @@ -1,4 +1,3 @@ -use iced::Task; use iced::mouse::ScrollDelta; use std::time::{Duration, Instant}; @@ -95,7 +94,7 @@ impl Scroll { } else { // Return integer part of scroll, and keep remainder self.scroll = Some((scroll.fract(), Instant::now())); - let mut discrete = scroll.trunc() as isize; + let discrete = scroll.trunc() as isize; if discrete != 0 { self.last_discrete = Some(Instant::now()); } diff --git a/src/surface/action.rs b/src/surface/action.rs index 5bd5a294..16816fff 100644 --- a/src/surface/action.rs +++ b/src/surface/action.rs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: MPL-2.0 use super::Action; -#[cfg(feature = "winit")] use crate::Application; use iced::window; @@ -28,7 +27,7 @@ pub fn destroy_window(id: iced_core::window::Id) -> Action { Action::DestroyWindow(id) } -#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] +#[cfg(all(feature = "wayland", target_os = "linux"))] #[must_use] pub fn app_window( settings: impl Fn(&mut App) -> window::Settings + Send + Sync + 'static, @@ -61,7 +60,7 @@ pub fn app_window( } /// Used to create a window message from within a widget. -#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] +#[cfg(all(feature = "wayland", target_os = "linux"))] #[must_use] pub fn simple_window( settings: impl Fn() -> window::Settings + Send + Sync + 'static, @@ -93,7 +92,7 @@ pub fn simple_window( ) } -#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] +#[cfg(all(feature = "wayland", target_os = "linux"))] #[must_use] pub fn app_popup( settings: impl Fn(&mut App) -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings @@ -127,7 +126,7 @@ pub fn app_popup( } /// Used to create a subsurface message from within a widget. -#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] +#[cfg(all(feature = "wayland", target_os = "linux"))] #[must_use] pub fn simple_subsurface( settings: impl Fn() -> iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings @@ -156,7 +155,7 @@ pub fn simple_subsurface( } /// Used to create a popup message from within a widget. -#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] +#[cfg(all(feature = "wayland", target_os = "linux"))] #[must_use] pub fn simple_popup( settings: impl Fn() -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings @@ -187,7 +186,7 @@ pub fn simple_popup( ) } -#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] +#[cfg(all(feature = "wayland", target_os = "linux"))] #[must_use] pub fn subsurface( settings: impl Fn( diff --git a/src/theme/style/iced.rs b/src/theme/style/iced.rs index 131538e4..8f9fa461 100644 --- a/src/theme/style/iced.rs +++ b/src/theme/style/iced.rs @@ -170,18 +170,15 @@ impl Button { * TODO: Checkbox */ #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Default)] pub enum Checkbox { + #[default] Primary, Secondary, Success, Danger, } -impl Default for Checkbox { - fn default() -> Self { - Self::Primary - } -} impl iced_checkbox::Catalog for Theme { type Class<'a> = Checkbox; @@ -791,7 +788,7 @@ impl menu::Catalog for Theme { fn default<'a>() -> ::Class<'a> {} - fn style(&self, class: &::Class<'_>) -> menu::Style { + fn style(&self, _class: &::Class<'_>) -> menu::Style { let cosmic = self.cosmic(); menu::Style { @@ -815,7 +812,7 @@ impl pick_list::Catalog for Theme { fn style( &self, - class: &::Class<'_>, + _class: &::Class<'_>, status: pick_list::Status, ) -> pick_list::Style { let cosmic = &self.cosmic(); @@ -856,7 +853,7 @@ impl radio::Catalog for Theme { fn default<'a>() -> Self::Class<'a> {} - fn style(&self, class: &Self::Class<'_>, status: radio::Status) -> radio::Style { + fn style(&self, _class: &Self::Class<'_>, status: radio::Status) -> radio::Style { let cur_container = self.current_container(); let theme = self.cosmic(); @@ -909,7 +906,7 @@ impl toggler::Catalog for Theme { fn default<'a>() -> Self::Class<'a> {} - fn style(&self, class: &Self::Class<'_>, status: toggler::Status) -> toggler::Style { + fn style(&self, _class: &Self::Class<'_>, status: toggler::Status) -> toggler::Style { let cosmic = self.cosmic(); const HANDLE_MARGIN: f32 = 2.0; let neutral_10 = cosmic.palette.neutral_10.with_alpha(0.1); @@ -937,8 +934,8 @@ impl toggler::Catalog for Theme { padding_ratio: 0.0, }; match status { - toggler::Status::Active { is_toggled } => active, - toggler::Status::Hovered { is_toggled } => { + toggler::Status::Active { is_toggled: _ } => active, + toggler::Status::Hovered { is_toggled: _ } => { let is_active = matches!(status, toggler::Status::Hovered { is_toggled: true }); toggler::Style { background: if is_active { @@ -957,7 +954,7 @@ impl toggler::Catalog for Theme { ..active } } - toggler::Status::Disabled { is_toggled } => { + toggler::Status::Disabled { is_toggled: _ } => { active.background = active.background.scale_alpha(0.5); active.foreground = active.foreground.scale_alpha(0.5); active @@ -974,7 +971,7 @@ impl pane_grid::Catalog for Theme { fn default<'a>() -> ::Class<'a> {} - fn style(&self, class: &::Class<'_>) -> pane_grid::Style { + fn style(&self, _class: &::Class<'_>) -> pane_grid::Style { let theme = self.cosmic(); pane_grid::Style { @@ -1142,8 +1139,8 @@ impl scrollable::Catalog for Theme { fn style(&self, class: &Self::Class<'_>, status: scrollable::Status) -> scrollable::Style { match status { scrollable::Status::Active { - is_horizontal_scrollbar_disabled, - is_vertical_scrollbar_disabled, + is_horizontal_scrollbar_disabled: _, + is_vertical_scrollbar_disabled: _, } => { let cosmic = self.cosmic(); let neutral_5 = cosmic.palette.neutral_5.with_alpha(0.7); @@ -1192,7 +1189,7 @@ impl scrollable::Catalog for Theme { background: Color::TRANSPARENT.into(), border: Border::default(), shadow: Shadow::default(), - icon: Color::TRANSPARENT.into(), + icon: Color::TRANSPARENT, }, }; let small_widget_container = self.current_container().small_widget.with_alpha(0.7); @@ -1260,7 +1257,7 @@ impl scrollable::Catalog for Theme { background: Color::TRANSPARENT.into(), border: Border::default(), shadow: Shadow::default(), - icon: Color::TRANSPARENT.into(), + icon: Color::TRANSPARENT, }, }; @@ -1302,7 +1299,7 @@ impl svg::Catalog for Theme { Svg::default() } - fn style(&self, class: &Self::Class<'_>, status: svg::Status) -> svg::Style { + fn style(&self, class: &Self::Class<'_>, _status: svg::Status) -> svg::Style { #[allow(clippy::match_same_arms)] match class { Svg::Default => svg::Style::default(), @@ -1439,7 +1436,7 @@ impl text_input::Catalog for Theme { }, } } - text_input::Status::Focused { is_hovered } => { + text_input::Status::Focused { is_hovered: _ } => { let bg = self.current_container().small_widget.with_alpha(0.25); match class { @@ -1516,7 +1513,7 @@ impl iced_widget::text_editor::Catalog for Theme { let selection = cosmic.accent.base.into(); let value = cosmic.palette.neutral_9.into(); let placeholder = cosmic.palette.neutral_9.with_alpha(0.7).into(); - let icon: Color = cosmic.background.on.into(); + let _icon: Color = cosmic.background.on.into(); // TODO do we need to add icon color back? match status { @@ -1533,7 +1530,7 @@ impl iced_widget::text_editor::Catalog for Theme { value, selection, }, - iced_widget::text_editor::Status::Focused { is_hovered } => { + iced_widget::text_editor::Status::Focused { is_hovered: _ } => { iced_widget::text_editor::Style { background: iced::Color::from(cosmic.bg_color()).into(), border: Border { @@ -1636,8 +1633,8 @@ impl Base for Theme { crate::theme::ThemeType::Light => "Cosmic Light Theme", crate::theme::ThemeType::HighContrastDark => "Cosmic High Contrast Dark Theme", crate::theme::ThemeType::HighContrastLight => "Cosmic High Contrast Light Theme", - crate::theme::ThemeType::Custom(theme) => "Custom Cosmic Theme", - crate::theme::ThemeType::System { prefer_dark, theme } => &theme.name, + crate::theme::ThemeType::Custom(_theme) => "Custom Cosmic Theme", + crate::theme::ThemeType::System { prefer_dark: _, theme } => &theme.name, } } } diff --git a/src/theme/style/mod.rs b/src/theme/style/mod.rs index bc648a73..cc489312 100644 --- a/src/theme/style/mod.rs +++ b/src/theme/style/mod.rs @@ -32,7 +32,7 @@ mod text_input; #[doc(inline)] pub use self::text_input::TextInput; -#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] +#[cfg(all(feature = "wayland", target_os = "linux"))] pub mod tooltip; -#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] +#[cfg(all(feature = "wayland", target_os = "linux"))] pub use tooltip::Tooltip; diff --git a/src/theme/style/segmented_button.rs b/src/theme/style/segmented_button.rs index 381c4a05..45fcc0b8 100644 --- a/src/theme/style/segmented_button.rs +++ b/src/theme/style/segmented_button.rs @@ -185,7 +185,7 @@ mod horizontal { pub fn selection_active( cosmic: &cosmic_theme::Theme, - component: &Component, + _component: &Component, ) -> ItemStatusAppearance { let rad_xl = cosmic.corner_radii.radius_xl; let rad_0 = cosmic.corner_radii.radius_0; @@ -280,7 +280,7 @@ mod vertical { pub fn selection_active( cosmic: &cosmic_theme::Theme, - component: &Component, + _component: &Component, ) -> ItemStatusAppearance { let rad_0 = cosmic.corner_radii.radius_0; let rad_xl = cosmic.corner_radii.radius_xl; diff --git a/src/widget/button/icon.rs b/src/widget/button/icon.rs index 6be13008..47713536 100644 --- a/src/widget/button/icon.rs +++ b/src/widget/button/icon.rs @@ -156,7 +156,7 @@ impl<'a, Message: Clone + 'static> From> for Element<'a, Mes ); } - let mut button = if builder.variant.vertical { + let button = if builder.variant.vertical { crate::widget::column::with_children(content) .padding(builder.padding) .spacing(builder.spacing) @@ -173,9 +173,11 @@ impl<'a, Message: Clone + 'static> From> for Element<'a, Mes }; #[cfg(feature = "a11y")] - { + let button = { + let mut button = button; button = button.name(builder.name).description(builder.description); - } + button + }; let button = button .padding(0) diff --git a/src/widget/button/image.rs b/src/widget/button/image.rs index 3b382035..915cc532 100644 --- a/src/widget/button/image.rs +++ b/src/widget/button/image.rs @@ -84,7 +84,7 @@ where .width(builder.width) .height(builder.height); - let mut button = super::custom_image_button(content, builder.variant.on_remove) + let button = super::custom_image_button(content, builder.variant.on_remove) .padding(0) .selected(builder.variant.selected) .id(builder.id) @@ -92,9 +92,11 @@ where .class(builder.class); #[cfg(feature = "a11y")] - { + let button = { + let mut button = button; button = button.name(builder.name).description(builder.description); - } + button + }; button.into() } diff --git a/src/widget/button/link.rs b/src/widget/button/link.rs index c0afd327..e6f8ac7e 100644 --- a/src/widget/button/link.rs +++ b/src/widget/button/link.rs @@ -67,7 +67,7 @@ pub fn icon() -> Handle { impl<'a, Message: Clone + 'static> From> for Element<'a, Message> { fn from(mut builder: Button<'a, Message>) -> Element<'a, Message> { - let mut button: super::Button<'a, Message> = row::with_capacity(2) + let button: super::Button<'a, Message> = row::with_capacity(2) .push({ // TODO: Avoid allocation crate::widget::text(builder.label.to_string()) @@ -95,13 +95,15 @@ impl<'a, Message: Clone + 'static> From> for Element<'a, Mes .class(builder.class); #[cfg(feature = "a11y")] - { + let button = { + let mut button = button; if !builder.label.is_empty() { button = button.name(builder.label); } button = button.description(builder.description); - } + button + }; if builder.tooltip.is_empty() { button.into() diff --git a/src/widget/button/text.rs b/src/widget/button/text.rs index 52ab6145..55623ff8 100644 --- a/src/widget/button/text.rs +++ b/src/widget/button/text.rs @@ -122,7 +122,7 @@ impl<'a, Message: Clone + 'static> From> for Element<'a, Mes .into() }); - let mut button: super::Button<'a, Message> = row::with_capacity(3) + let button: super::Button<'a, Message> = row::with_capacity(3) // Optional icon to place before label. .push_maybe(leading_icon) // Optional label between icons. @@ -141,13 +141,15 @@ impl<'a, Message: Clone + 'static> From> for Element<'a, Mes .class(builder.class); #[cfg(feature = "a11y")] - { + let button = { + let mut button = button; if !builder.label.is_empty() { button = button.name(builder.label) } button = button.description(builder.description); - } + button + }; if builder.tooltip.is_empty() { button.into() diff --git a/src/widget/button/widget.rs b/src/widget/button/widget.rs index cca54fd2..e6b83d7b 100644 --- a/src/widget/button/widget.rs +++ b/src/widget/button/widget.rs @@ -9,7 +9,7 @@ use iced_runtime::core::widget::Id; use iced_runtime::{Action, Task, keyboard, task}; -use iced_core::event::{self, Event}; +use iced_core::event::Event; use iced_core::renderer::{self, Quad, Renderer}; use iced_core::widget::Operation; use iced_core::widget::tree::{self, Tree}; @@ -378,13 +378,12 @@ impl<'a, Message: 'a + Clone> Widget match event { Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { - if let Some(position) = cursor.position() { - if removal_bounds(layout.bounds(), 4.0).contains(position) { + if let Some(position) = cursor.position() + && removal_bounds(layout.bounds(), 4.0).contains(position) { shell.publish(on_remove.clone()); shell.capture_event(); return; } - } } _ => (), @@ -557,9 +556,9 @@ impl<'a, Message: 'a + Clone> Widget } } - if on_remove.is_some() { - if let Some(position) = cursor.position() { - if bounds.contains(position) { + if on_remove.is_some() + && let Some(position) = cursor.position() + && bounds.contains(position) { let bounds = removal_bounds(layout.bounds(), 4.0); renderer.fill_quad( renderer::Quad { @@ -591,8 +590,6 @@ impl<'a, Message: 'a + Clone> Widget }, ); } - } - } }); } } @@ -662,7 +659,7 @@ impl<'a, Message: 'a + Clone> Widget height, } = layout.bounds(); let bounds = Rect::new(x as f64, y as f64, (x + width) as f64, (y + height) as f64); - let is_hovered = state.state.downcast_ref::().is_hovered; + let _is_hovered = state.state.downcast_ref::().is_hovered; let mut node = Node::new(Role::Button); node.add_action(Action::Focus); @@ -743,12 +740,6 @@ impl State { self.is_focused } - /// Returns whether the [`Button`] is currently hovered or not. - #[inline] - pub fn is_hovered(self) -> bool { - self.is_hovered - } - /// Focuses the [`Button`]. #[inline] pub fn focus(&mut self) { @@ -794,7 +785,6 @@ pub fn update<'a, Message: Clone>( } shell.capture_event(); - return; } } } @@ -814,7 +804,6 @@ pub fn update<'a, Message: Clone>( } shell.capture_event(); - return; } } else if on_press_down.is_some() { let state = state(); @@ -822,7 +811,7 @@ pub fn update<'a, Message: Clone>( } } #[cfg(feature = "a11y")] - Event::A11y(event_id, iced_accessibility::accesskit::ActionRequest { action, .. }) => { + Event::A11y(_event_id, iced_accessibility::accesskit::ActionRequest { action, .. }) => { let state = state(); if let Some(on_press) = matches!(action, iced_accessibility::accesskit::Action::Click) .then_some(on_press) @@ -834,7 +823,6 @@ pub fn update<'a, Message: Clone>( shell.publish(msg); } shell.capture_event(); - return; } Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => { if let Some(on_press) = on_press { @@ -845,7 +833,6 @@ pub fn update<'a, Message: Clone>( shell.publish(msg); shell.capture_event(); - return; } } } @@ -865,7 +852,7 @@ pub fn draw( viewport_bounds: Rectangle, styling: &super::style::Style, draw_contents: impl FnOnce(&mut Renderer, &Style), - is_image: bool, + _is_image: bool, ) where Theme: super::style::Catalog, { diff --git a/src/widget/cards.rs b/src/widget/cards.rs index 00014740..4d6770fa 100644 --- a/src/widget/cards.rs +++ b/src/widget/cards.rs @@ -93,7 +93,7 @@ where /// Get an expandable stack of cards #[allow(clippy::too_many_arguments)] pub fn new( - id: widget::Id, + _id: widget::Id, card_inner_elements: Vec>, on_clear_all: Message, on_show_more: Option, @@ -527,7 +527,7 @@ where let c_layout = layout.next().unwrap(); let state = clear_all_state.unwrap(); self.clear_all_button.as_widget_mut().update( - state, &event, c_layout, cursor, renderer, clipboard, shell, viewport, + state, event, c_layout, cursor, renderer, clipboard, shell, viewport, ); } @@ -537,7 +537,7 @@ where for ((inner, layout), c_state) in self.elements.iter_mut().zip(layout).zip(tree_children) { inner.as_widget_mut().update( - c_state, &event, layout, cursor, renderer, clipboard, shell, viewport, + c_state, event, layout, cursor, renderer, clipboard, shell, viewport, ); if shell.is_event_captured() || fully_unexpanded { break; diff --git a/src/widget/color_picker/mod.rs b/src/widget/color_picker/mod.rs index 66ffd847..879cc604 100644 --- a/src/widget/color_picker/mod.rs +++ b/src/widget/color_picker/mod.rs @@ -17,7 +17,7 @@ use crate::widget::segmented_button::Entity; use crate::widget::{container, slider}; use derive_setters::Setters; use iced::Task; -use iced_core::event::{self, Event}; +use iced_core::event::Event; use iced_core::gradient::{ColorStop, Linear}; use iced_core::renderer::Quad; use iced_core::widget::{Tree, tree}; @@ -619,8 +619,10 @@ where let bounds = canvas_layout.bounds(); // Draw the handle on the saturation value canvas - let t = THEME.lock().unwrap().clone(); - let t = t.cosmic(); + // Yoda: use the Theme passed into draw() instead of locking the global + // THEME Mutex and cloning the whole Theme. Fires on every color-picker + // redraw, so saving the Mutex lock + full Theme clone adds up. + let t = theme.cosmic(); let handle_radius = f32::from(t.space_xs()) / 2.0; let (x, y) = ( self.active_color @@ -743,7 +745,7 @@ where let column_tree = &mut tree.children[0]; self.inner.as_widget_mut().update( column_tree, - &event, + event, column_layout, cursor, renderer, @@ -756,22 +758,19 @@ where return; } - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { - let bounds = column_layout.children().nth(1).unwrap().bounds(); - if let Some(point) = cursor.position_over(bounds) { - let relative_pos = point - bounds.position(); - let (s, v) = ( - relative_pos.x / bounds.width, - 1.0 - relative_pos.y / bounds.height, - ); - state.dragging = true; - let hsv: palette::Hsv = palette::Hsv::new(self.active_color.hue, s, v); - shell.publish((self.on_update)(ColorPickerUpdate::ActiveColor(hsv))); - shell.capture_event(); - } + if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) = event { + let bounds = column_layout.children().nth(1).unwrap().bounds(); + if let Some(point) = cursor.position_over(bounds) { + let relative_pos = point - bounds.position(); + let (s, v) = ( + relative_pos.x / bounds.width, + 1.0 - relative_pos.y / bounds.height, + ); + state.dragging = true; + let hsv: palette::Hsv = palette::Hsv::new(self.active_color.hue, s, v); + shell.publish((self.on_update)(ColorPickerUpdate::ActiveColor(hsv))); + shell.capture_event(); } - _ => {} } } diff --git a/src/widget/context_menu.rs b/src/widget/context_menu.rs index bd950bff..9256c954 100644 --- a/src/widget/context_menu.rs +++ b/src/widget/context_menu.rs @@ -6,7 +6,6 @@ #[cfg(all( feature = "wayland", target_os = "linux", - feature = "winit", feature = "surface-message" ))] use crate::app::cosmic::{WINDOWING_SYSTEM, WindowingSystem}; @@ -67,7 +66,6 @@ impl ContextMenu<'_, Message> { #[cfg(all( feature = "wayland", target_os = "linux", - feature = "winit", feature = "surface-message" ))] #[allow(clippy::too_many_lines)] @@ -91,7 +89,7 @@ impl ContextMenu<'_, Message> { bounds.x = my_state.context_cursor.x; bounds.y = my_state.context_cursor.y; - let (id, root_list) = my_state.menu_bar_state.inner.with_data_mut(|state| { + let (id, _root_list) = my_state.menu_bar_state.inner.with_data_mut(|state| { if let Some(id) = state.popup_id.get(&self.window_id).copied() { // close existing popups state.menu_states.clear(); @@ -149,7 +147,7 @@ impl ContextMenu<'_, Message> { layout.bounds(), -bounds.height, ); - let (anchor_rect, gravity) = my_state.menu_bar_state.inner.with_data_mut(|state| { + let (anchor_rect, _gravity) = my_state.menu_bar_state.inner.with_data_mut(|state| { use iced::Rectangle; state.popup_id.insert(self.window_id, id); @@ -378,7 +376,6 @@ impl Widget #[cfg(all( feature = "wayland", target_os = "linux", - feature = "winit", feature = "surface-message" ))] if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) @@ -422,7 +419,6 @@ impl Widget #[cfg(all( feature = "wayland", target_os = "linux", - feature = "winit", feature = "surface-message" ))] if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) { @@ -444,7 +440,6 @@ impl Widget #[cfg(all( feature = "wayland", target_os = "linux", - feature = "winit", feature = "surface-message" ))] if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) @@ -483,7 +478,6 @@ impl Widget #[cfg(all( feature = "wayland", target_os = "linux", - feature = "winit", feature = "surface-message" ))] if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) diff --git a/src/widget/dnd_destination.rs b/src/widget/dnd_destination.rs index 0ebff3dc..afe135bc 100644 --- a/src/widget/dnd_destination.rs +++ b/src/widget/dnd_destination.rs @@ -357,7 +357,7 @@ impl Widget x, y, mime_types, .. }, )) if *id == Some(my_id) => { - if !self.mime_matches(&mime_types) { + if !self.mime_matches(mime_types) { log::trace!( target: DND_DEST_LOG_TARGET, "offer enter id={my_id:?} ignored (mimes={mime_types:?} not in {:?})", @@ -396,7 +396,6 @@ impl Widget ); } shell.capture_event(); - return; } Event::Dnd(DndEvent::Offer(_, OfferEvent::Leave)) => { log::trace!( @@ -424,7 +423,6 @@ impl Widget viewport, ); } - return; } Event::Dnd(DndEvent::Offer(id, OfferEvent::Motion { x, y })) if *id == Some(my_id) => { log::trace!( @@ -459,7 +457,6 @@ impl Widget ); } shell.capture_event(); - return; } Event::Dnd(DndEvent::Offer(_, OfferEvent::LeaveDestination)) => { log::trace!( @@ -472,7 +469,6 @@ impl Widget { shell.publish(msg); } - return; } Event::Dnd(DndEvent::Offer(id, OfferEvent::Drop)) if *id == Some(my_id) => { log::trace!( @@ -485,7 +481,6 @@ impl Widget shell.publish(msg); } shell.capture_event(); - return; } Event::Dnd(DndEvent::Offer(id, OfferEvent::SelectedAction(action))) if *id == Some(my_id) => @@ -503,7 +498,6 @@ impl Widget shell.publish(msg); } shell.capture_event(); - return; } Event::Dnd(DndEvent::Offer(id, OfferEvent::Data { data, mime_type })) if *id == Some(my_id) => @@ -543,7 +537,6 @@ impl Widget return; } shell.capture_event(); - return; } _ => {} } diff --git a/src/widget/dropdown/menu/mod.rs b/src/widget/dropdown/menu/mod.rs index 0c96c1c6..99bee955 100644 --- a/src/widget/dropdown/menu/mod.rs +++ b/src/widget/dropdown/menu/mod.rs @@ -8,9 +8,8 @@ use std::sync::{Arc, Mutex}; pub use appearance::{Appearance, StyleSheet}; -use crate::surface; use crate::widget::{Container, RcWrapper, icon}; -use iced_core::event::{self, Event}; +use iced_core::event::Event; use iced_core::layout::{self, Layout}; use iced_core::text::{self, Text}; use iced_core::widget::Tree; @@ -391,7 +390,7 @@ impl<'a, Message: Clone + 'a> crate::widget::Widget { let hovered_guard = self.hovered_option.lock().unwrap(); - if cursor.is_over(layout.bounds()) { - if let Some(index) = *hovered_guard { + if cursor.is_over(layout.bounds()) + && let Some(index) = *hovered_guard { shell.publish((self.on_selected)(index)); if let Some(close_on_selected) = self.close_on_selected.as_ref() { shell.publish(close_on_selected.clone()); } shell.capture_event(); - return; } - } } Event::Mouse(mouse::Event::CursorMoved { .. }) => { if let Some(cursor_position) = cursor.position_in(layout.bounds()) { @@ -499,11 +496,10 @@ where let new_hovered_option = (cursor_position.y / option_height) as usize; let mut hovered_guard = self.hovered_option.lock().unwrap(); - if let Some(on_option_hovered) = self.on_option_hovered { - if *hovered_guard != Some(new_hovered_option) { + if let Some(on_option_hovered) = self.on_option_hovered + && *hovered_guard != Some(new_hovered_option) { shell.publish(on_option_hovered(new_hovered_option)); } - } *hovered_guard = Some(new_hovered_option); } @@ -527,7 +523,6 @@ where shell.publish(close_on_selected.clone()); } shell.capture_event(); - return; } } } @@ -554,12 +549,12 @@ where fn draw( &self, - state: &Tree, + _state: &Tree, renderer: &mut crate::Renderer, theme: &crate::Theme, - style: &renderer::Style, + _style: &renderer::Style, layout: Layout<'_>, - cursor: mouse::Cursor, + _cursor: mouse::Cursor, viewport: &Rectangle, ) { let appearance = theme.appearance(&()); diff --git a/src/widget/dropdown/mod.rs b/src/widget/dropdown/mod.rs index b5fd4c06..0ea3a212 100644 --- a/src/widget/dropdown/mod.rs +++ b/src/widget/dropdown/mod.rs @@ -50,7 +50,7 @@ pub fn popup_dropdown< let dropdown: Dropdown<'_, S, Message, AppMessage> = Dropdown::new(selections.into(), selected, on_selected); - #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] + #[cfg(all(feature = "wayland", target_os = "linux"))] let dropdown = dropdown.with_popup(_parent_id, _on_surface_action, _map_action); dropdown diff --git a/src/widget/dropdown/multi/menu.rs b/src/widget/dropdown/multi/menu.rs index 0a761097..883b8ada 100644 --- a/src/widget/dropdown/multi/menu.rs +++ b/src/widget/dropdown/multi/menu.rs @@ -2,7 +2,7 @@ use super::Model; pub use crate::widget::dropdown::menu::{Appearance, StyleSheet}; use crate::widget::Container; -use iced_core::event::{self, Event}; +use iced_core::event::Event; use iced_core::layout::{self, Layout}; use iced_core::text::{self, Text}; use iced_core::widget::Tree; @@ -343,13 +343,11 @@ where match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { - if cursor.is_over(bounds) { - if let Some(item) = self.hovered_option.as_ref() { + if cursor.is_over(bounds) + && let Some(item) = self.hovered_option.as_ref() { shell.publish((self.on_selected)(item.clone())); shell.capture_event(); - return; } - } } Event::Mouse(mouse::Event::CursorMoved { .. }) => { if let Some(cursor_position) = cursor.position_in(bounds) { diff --git a/src/widget/dropdown/multi/widget.rs b/src/widget/dropdown/multi/widget.rs index 3c01f5f8..c7a46592 100644 --- a/src/widget/dropdown/multi/widget.rs +++ b/src/widget/dropdown/multi/widget.rs @@ -5,15 +5,14 @@ use super::menu::{self, Menu}; use crate::widget::icon; use derive_setters::Setters; -use iced_core::event::{self, Event}; -use iced_core::text::{self, Paragraph, Text}; +use iced_core::event::Event; +use iced_core::text::{self, Text}; use iced_core::widget::tree::{self, Tree}; use iced_core::{ Clipboard, Layout, Length, Padding, Pixels, Rectangle, Shadow, Shell, Size, Vector, Widget, alignment, keyboard, layout, mouse, overlay, renderer, svg, touch, }; use iced_widget::pick_list; -use std::ffi::OsStr; pub use iced_widget::pick_list::{Catalog, Style}; @@ -128,7 +127,7 @@ impl<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static> _viewport: &Rectangle, ) { update( - &event, + event, layout, cursor, shell, @@ -253,7 +252,7 @@ impl Default for State { /// Computes the layout of a [`Dropdown`]. #[allow(clippy::too_many_arguments)] pub fn layout( - renderer: &crate::Renderer, + _renderer: &crate::Renderer, limits: &layout::Limits, width: Length, gap: f32, @@ -376,7 +375,7 @@ pub fn mouse_interaction(layout: Layout<'_>, cursor: mouse::Cursor) -> mouse::In #[allow(clippy::too_many_arguments)] pub fn overlay<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static>( layout: Layout<'_>, - renderer: &crate::Renderer, + _renderer: &crate::Renderer, state: &'a mut State, gap: f32, padding: Padding, diff --git a/src/widget/dropdown/operation.rs b/src/widget/dropdown/operation.rs index 1a4e1a9f..4cd266d4 100644 --- a/src/widget/dropdown/operation.rs +++ b/src/widget/dropdown/operation.rs @@ -2,9 +2,6 @@ // SPDX-License-Identifier: MPL-2.0 AND MIT //! Operate on dropdown widgets. -use super::State; -use iced::Rectangle; -use iced_core::widget::{Id, Operation}; pub trait Dropdown { fn close(&mut self); diff --git a/src/widget/dropdown/widget.rs b/src/widget/dropdown/widget.rs index 046cf84d..ebcf175a 100644 --- a/src/widget/dropdown/widget.rs +++ b/src/widget/dropdown/widget.rs @@ -8,7 +8,7 @@ use crate::widget::icon::{self, Handle}; use crate::{Element, surface}; use derive_setters::Setters; use iced::window; -use iced_core::event::{self, Event}; +use iced_core::event::Event; use iced_core::text::{self, Paragraph, Text}; use iced_core::widget::tree::{self, Tree}; use iced_core::{ @@ -17,7 +17,6 @@ use iced_core::{ }; use iced_widget::pick_list::{self, Catalog}; use std::borrow::Cow; -use std::ffi::OsStr; use std::hash::{DefaultHasher, Hash, Hasher}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, LazyLock, Mutex}; @@ -60,7 +59,7 @@ where action_map: Option AppMessage + 'static + Send + Sync>>, #[setters(strip_option)] window_id: Option, - #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] + #[cfg(all(feature = "wayland", target_os = "linux"))] positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, } @@ -96,14 +95,14 @@ where text_line_height: text::LineHeight::Relative(1.2), font: None, window_id: None, - #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] + #[cfg(all(feature = "wayland", target_os = "linux"))] positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner::default(), on_surface_action: None, action_map: None, } } - #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] + #[cfg(all(feature = "wayland", target_os = "linux"))] /// Handle dropdown requests for popup creation. /// Intended to be used with [`crate::app::message::get_popup`] pub fn with_popup( @@ -154,7 +153,7 @@ where self } - #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] + #[cfg(all(feature = "wayland", target_os = "linux"))] pub fn with_positioner( mut self, positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, @@ -264,11 +263,11 @@ where _viewport: &Rectangle, ) { update::( - &event, + event, layout, cursor, shell, - #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] + #[cfg(all(feature = "wayland", target_os = "linux"))] self.positioner.clone(), self.on_selected.clone(), self.selected, @@ -328,10 +327,10 @@ where fn operate( &mut self, - tree: &mut Tree, + _tree: &mut Tree, _layout: Layout<'_>, _renderer: &crate::Renderer, - operation: &mut dyn iced_core::widget::Operation, + _operation: &mut dyn iced_core::widget::Operation, ) { // TODO: double check operation handling // let state = tree.state.downcast_mut::(); @@ -343,10 +342,10 @@ where tree: &'b mut Tree, layout: Layout<'b>, renderer: &crate::Renderer, - viewport: &Rectangle, + _viewport: &Rectangle, translation: Vector, ) -> Option> { - #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] + #[cfg(all(feature = "wayland", target_os = "linux"))] if self.window_id.is_some() || self.on_surface_action.is_some() { return None; } @@ -452,7 +451,7 @@ impl super::operation::Dropdown for State { /// Computes the layout of a [`Dropdown`]. #[allow(clippy::too_many_arguments)] pub fn layout( - renderer: &crate::Renderer, + _renderer: &crate::Renderer, limits: &layout::Limits, width: Length, gap: f32, @@ -545,8 +544,8 @@ pub fn update< layout: Layout<'_>, cursor: mouse::Cursor, shell: &mut Shell<'_, Message>, - #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] - positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, + #[cfg(all(feature = "wayland", target_os = "linux"))] + _positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, on_selected: Arc Message + Send + Sync + 'static>, selected: Option, selections: &[S], @@ -558,7 +557,7 @@ pub fn update< gap: f32, padding: Padding, text_size: Option, - font: Option, + _font: Option, selected_option: Option, ) { let state = state(); @@ -571,7 +570,7 @@ pub fn update< *hovered_guard = selected; let id = window::Id::unique(); state.popup_id = id; - #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] + #[cfg(all(feature = "wayland", target_os = "linux"))] if let Some(((on_surface_action, parent), action_map)) = on_surface_action .as_ref() .zip(_window_id) @@ -658,7 +657,7 @@ pub fn update< state.close_operation = false; state.is_open.store(false, Ordering::SeqCst); if is_open { - #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] + #[cfg(all(feature = "wayland", target_os = "linux"))] if let Some(ref on_close) = on_surface_action { shell.publish(on_close(surface::action::destroy_popup(state.popup_id))); } @@ -681,7 +680,7 @@ pub fn update< // Event wasn't processed by overlay, so cursor was clicked either outside it's // bounds or on the drop-down, either way we close the overlay. state.is_open.store(false, Ordering::Relaxed); - #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] + #[cfg(all(feature = "wayland", target_os = "linux"))] if let Some(on_close) = on_surface_action { shell.publish(on_close(surface::action::destroy_popup(state.popup_id))); } @@ -726,7 +725,7 @@ pub fn mouse_interaction(layout: Layout<'_>, cursor: mouse::Cursor) -> mouse::In } } -#[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] +#[cfg(all(feature = "wayland", target_os = "linux"))] /// Returns the current menu widget of a [`Dropdown`]. #[allow(clippy::too_many_arguments)] pub fn menu_widget< diff --git a/src/widget/flex_row/widget.rs b/src/widget/flex_row/widget.rs index 0b2e6e13..e5c0b615 100644 --- a/src/widget/flex_row/widget.rs +++ b/src/widget/flex_row/widget.rs @@ -3,7 +3,7 @@ use crate::{Element, Renderer}; use derive_setters::Setters; -use iced_core::event::{self, Event}; +use iced_core::event::Event; use iced_core::widget::{Operation, Tree}; use iced_core::{ Clipboard, Layout, Length, Padding, Rectangle, Shell, Vector, Widget, layout, mouse, overlay, diff --git a/src/widget/grid/widget.rs b/src/widget/grid/widget.rs index e59ba90d..55ce3c91 100644 --- a/src/widget/grid/widget.rs +++ b/src/widget/grid/widget.rs @@ -3,7 +3,7 @@ use crate::{Element, Renderer}; use derive_setters::Setters; -use iced_core::event::{self, Event}; +use iced_core::event::Event; use iced_core::widget::{Operation, Tree}; use iced_core::{ Alignment, Clipboard, Layout, Length, Padding, Rectangle, Shell, Vector, Widget, layout, mouse, diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs index 556466fd..82a7e8e9 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -27,9 +27,32 @@ pub fn header_bar<'a, Message>() -> HeaderBar<'a, Message> { sharp_corners: false, is_ssd: false, on_double_click: None, + transparent: false, + controls_position: None, } } +/// Position of the window control buttons (close/min/max) within the headerbar. +#[derive( + Clone, + Copy, + Debug, + Default, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, +)] +pub enum WindowControlsPosition { + /// Controls packed at the start (left on LTR) — macOS style. + /// Internal icon order becomes close → minimize → maximize. + Start, + /// Controls packed at the end (right on LTR) — Linux / GNOME style. + /// Internal icon order is minimize → maximize → close. + #[default] + End, +} + #[derive(Setters)] pub struct HeaderBar<'a, Message> { /// Defines the title of the window @@ -88,6 +111,17 @@ pub struct HeaderBar<'a, Message> { /// HeaderBar used for server-side decorations is_ssd: bool, + + /// Whether the headerbar should be transparent + transparent: bool, + + /// Side on which the window control buttons (close / minimize / maximize) + /// are rendered. `None` falls back to the user's CosmicTk config + /// (`crate::config::window_controls_position()`). `Some` overrides it. + /// `End` matches Linux / GNOME conventions; `Start` provides macOS-style + /// left-side controls. + #[setters(strip_option)] + controls_position: Option, } impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { @@ -370,12 +404,20 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { let is_ssd = self.is_ssd; // Take ownership of the regions to be packed. - let start = std::mem::take(&mut self.start); + let mut start = std::mem::take(&mut self.start); let center = std::mem::take(&mut self.center); let mut end = std::mem::take(&mut self.end); - // Also packs the window controls at the very end. - end.push(self.window_controls(space_xxs)); + // Pack window controls on the configured side (reads CosmicTk + // config when the builder did not set an explicit override). + let controls_position = self + .controls_position + .unwrap_or_else(crate::config::window_controls_position); + let controls = self.window_controls(space_xxs, controls_position); + match controls_position { + WindowControlsPosition::End => end.push(controls), + WindowControlsPosition::Start => start.insert(0, controls), + } let padding = if is_ssd { [2, 8, 2, 8] @@ -445,7 +487,11 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { } /// Creates the widget for window controls. - fn window_controls(&mut self, spacing: u16) -> Element<'a, Message> { + fn window_controls( + &mut self, + spacing: u16, + controls_position: WindowControlsPosition, + ) -> Element<'a, Message> { macro_rules! icon { ($name:expr, $size:expr, $on_press:expr) => {{ widget::icon::from_name($name) @@ -458,25 +504,37 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { }}; } - widget::row::with_capacity(3) - .push_maybe( - self.on_minimize - .take() - .map(|m| icon!("window-minimize-symbolic", 16, m)), - ) - .push_maybe(self.on_maximize.take().map(|m| { - if self.maximized { - icon!("window-restore-symbolic", 16, m) - } else { - icon!("window-maximize-symbolic", 16, m) - } - })) - .push_maybe( - self.on_close - .take() - .map(|m| icon!("window-close-symbolic", 16, m)), - ) - .spacing(spacing) + let minimize = self + .on_minimize + .take() + .map(|m| icon!("window-minimize-symbolic", 16, m)); + let maximize = self.on_maximize.take().map(|m| { + if self.maximized { + icon!("window-restore-symbolic", 16, m) + } else { + icon!("window-maximize-symbolic", 16, m) + } + }); + let close = self + .on_close + .take() + .map(|m| icon!("window-close-symbolic", 16, m)); + + // Icon order follows the OS convention for the chosen side: + // End → minimize, maximize, close (Linux / GNOME) + // Start → close, minimize, maximize (macOS) + let row = widget::row::with_capacity(3); + let row = match controls_position { + WindowControlsPosition::End => row + .push_maybe(minimize) + .push_maybe(maximize) + .push_maybe(close), + WindowControlsPosition::Start => row + .push_maybe(close) + .push_maybe(minimize) + .push_maybe(maximize), + }; + row.spacing(spacing) .align_y(iced::Alignment::Center) .into() } diff --git a/src/widget/icon/bundle.rs b/src/widget/icon/bundle.rs index bb6ce244..30a9938a 100644 --- a/src/widget/icon/bundle.rs +++ b/src/widget/icon/bundle.rs @@ -5,7 +5,7 @@ /// Icon bundling is not enabled on unix platforms. #[cfg(all(unix, not(target_os = "macos")))] -pub fn get(icon_name: &str) -> Option { +pub fn get(_icon_name: &str) -> Option { None } diff --git a/src/widget/menu/flex.rs b/src/widget/menu/flex.rs index 213ee694..e4a32870 100644 --- a/src/widget/menu/flex.rs +++ b/src/widget/menu/flex.rs @@ -48,6 +48,7 @@ impl Axis { /// padding and alignment to the items as needed. /// /// It returns a new layout [`Node`]. +#[allow(dead_code)] // kept as public helper; not currently called by libcosmic pub fn resolve<'a, E, Message, Renderer>( axis: &Axis, renderer: &Renderer, @@ -246,7 +247,7 @@ pub fn resolve_wrapper<'a, Message>( if align_items == Alignment::Center { let mut fill_cross = axis.cross(limits.min()); - for (child, tree) in items.into_iter().zip(tree.iter_mut()) { + for (child, tree) in items.iter_mut().zip(tree.iter_mut()) { let c_size = child.size(); let cross_fill_factor = match axis { Axis::Horizontal => c_size.height, @@ -269,7 +270,7 @@ pub fn resolve_wrapper<'a, Message>( cross = fill_cross; } - for (i, (child, tree)) in items.into_iter().zip(tree.iter_mut()).enumerate() { + for (i, (child, tree)) in items.iter_mut().zip(tree.iter_mut()).enumerate() { let c_size = child.size(); let fill_factor = match axis { Axis::Horizontal => c_size.width, @@ -312,7 +313,7 @@ pub fn resolve_wrapper<'a, Message>( let remaining = available.max(0.0); - for (i, (child, tree)) in items.into_iter().zip(tree.iter_mut()).enumerate() { + for (i, (child, tree)) in items.iter_mut().zip(tree.iter_mut()).enumerate() { let c_size = child.size(); let fill_factor = match axis { Axis::Horizontal => c_size.width, diff --git a/src/widget/menu/key_bind.rs b/src/widget/menu/key_bind.rs index ef87b344..b764fde7 100644 --- a/src/widget/menu/key_bind.rs +++ b/src/widget/menu/key_bind.rs @@ -51,9 +51,10 @@ impl KeyBind { physical_key: Option<&Physical>, ) -> bool { let key_eq = self.key_eq(key) - || physical_key - .and_then(physical_key_to_latin) - .is_some_and(|latin| self.key_eq(&latin)); + || (!is_latin_shortcut_key(key) + && physical_key + .and_then(physical_key_to_latin) + .is_some_and(|latin| self.key_eq(&latin))); key_eq && modifiers.logo() == self.modifiers.contains(&Modifier::Super) && modifiers.control() == self.modifiers.contains(&Modifier::Ctrl) @@ -70,6 +71,19 @@ impl KeyBind { } } +fn is_latin_shortcut_key(key: &Key) -> bool { + let Key::Character(s) = key else { + return false; + }; + + let mut chars = s.chars(); + let Some(ch) = chars.next() else { + return false; + }; + + chars.next().is_none() && (ch.is_ascii_graphic() || ch == ' ') +} + /// Converts a physical key code to the corresponding US-layout Latin `Key`. /// /// This mapping is intentionally limited to keys that may produce different @@ -138,3 +152,92 @@ impl fmt::Display for KeyBind { } } } + +#[cfg(test)] +mod test { + use super::*; + + fn bind_ctrl_w() -> KeyBind { + KeyBind { + modifiers: vec![Modifier::Ctrl], + key: Key::Character("w".into()), + } + } + + #[test] + fn ctrl_w() { + assert!(bind_ctrl_w().matches( + Modifiers::CTRL, + &Key::Character("w".into()), + Some(&Physical::Code(Code::KeyW)), + )); + } + + #[test] + fn ctrl_w_no_fallback_to_dvorak_comma() { + assert!(!bind_ctrl_w().matches( + Modifiers::CTRL, + &Key::Character(",".into()), + Some(&Physical::Code(Code::KeyW)), + )); + } + + #[test] + fn non_latin_layout_fallback() { + assert!(bind_ctrl_w().matches( + Modifiers::CTRL, + &Key::Character("ц".into()), + Some(&Physical::Code(Code::KeyW)), + )); + + let bind = KeyBind { + modifiers: vec![Modifier::Ctrl], + key: Key::Character("s".into()), + }; + + assert!(bind.matches( + Modifiers::CTRL, + &Key::Character("ы".into()), + Some(&Physical::Code(Code::KeyS)), + )); + + assert!(!bind.matches( + Modifiers::CTRL, + &Key::Character("ц".into()), + Some(&Physical::Code(Code::KeyQ)), + )); + } + + #[test] + fn ctrl_space() { + let bind = KeyBind { + modifiers: vec![Modifier::Ctrl], + key: Key::Character(" ".into()), + }; + + assert!(bind.matches(Modifiers::CTRL, &Key::Character(" ".into()), None,)); + } + + #[test] + fn ctrl_space_no_fallback() { + assert!(!bind_ctrl_w().matches( + Modifiers::CTRL, + &Key::Character(" ".into()), + Some(&Physical::Code(Code::KeyW)), + )); + } + + #[test] + fn ctrl_a_no_fallback_to_french_azerty_q() { + let bind = KeyBind { + modifiers: vec![Modifier::Ctrl], + key: Key::Character("a".into()), + }; + + assert!(!bind.matches( + Modifiers::CTRL, + &Key::Character("q".into()), + Some(&Physical::Code(Code::KeyA)), + )); + } +} diff --git a/src/widget/menu/menu_bar.rs b/src/widget/menu/menu_bar.rs index b5ffca42..3843c349 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -13,7 +13,6 @@ use crate::Renderer; feature = "multi-window", feature = "wayland", target_os = "linux", - feature = "winit", feature = "surface-message" ))] use crate::app::cosmic::{WINDOWING_SYSTEM, WindowingSystem}; @@ -51,7 +50,6 @@ pub(crate) struct MenuBarStateInner { pub(crate) tree: Tree, pub(crate) popup_id: HashMap, pub(crate) pressed: bool, - pub(crate) bar_pressed: bool, pub(crate) view_cursor: Cursor, pub(crate) open: bool, pub(crate) active_root: Vec, @@ -88,7 +86,6 @@ impl Default for MenuBarStateInner { vertical_direction: Direction::Positive, menu_states: Vec::new(), popup_id: HashMap::new(), - bar_pressed: false, } } } @@ -165,14 +162,6 @@ where } } -pub fn get_mut_or_default(vec: &mut Vec, index: usize) -> &mut T { - if index < vec.len() { - &mut vec[index] - } else { - vec.resize_with(index + 1, T::default); - &mut vec[index] - } -} /// A `MenuBar` collects `MenuTree`s and handles all the layout, event processing, and drawing. #[allow(missing_debug_implementations)] @@ -194,7 +183,6 @@ pub struct MenuBar { #[cfg(all( feature = "multi-window", feature = "wayland", - feature = "winit", target_os = "linux" ))] positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, @@ -234,7 +222,6 @@ where #[cfg(all( feature = "multi-window", feature = "wayland", - feature = "winit", target_os = "linux" ))] positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner::default(), @@ -333,7 +320,6 @@ where #[cfg(all( feature = "multi-window", feature = "wayland", - feature = "winit", target_os = "linux" ))] pub fn with_positioner( @@ -371,7 +357,6 @@ where feature = "multi-window", feature = "wayland", target_os = "linux", - feature = "winit", feature = "surface-message" ))] #[allow(clippy::too_many_lines)] @@ -610,14 +595,12 @@ where .with_data(|d| !d.open && !d.active_root.is_empty()); let open = my_state.inner.with_data_mut(|state| { - if reset { - if let Some(popup_id) = state.popup_id.get(&self.window_id).copied() { - if let Some(handler) = self.on_surface_action.as_ref() { + if reset + && let Some(popup_id) = state.popup_id.get(&self.window_id).copied() + && let Some(handler) = self.on_surface_action.as_ref() { shell.publish((handler)(crate::surface::Action::DestroyPopup(popup_id))); state.reset(); } - } - } state.open }); @@ -643,7 +626,6 @@ where #[cfg(all( feature = "wayland", target_os = "linux", - feature = "winit", feature = "surface-message" ))] { @@ -667,7 +649,6 @@ where feature = "multi-window", feature = "wayland", target_os = "linux", - feature = "winit", feature = "surface-message" ))] if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) { @@ -682,7 +663,6 @@ where feature = "multi-window", feature = "wayland", target_os = "linux", - feature = "winit", feature = "surface-message" ))] if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) { @@ -765,7 +745,6 @@ where feature = "multi-window", feature = "wayland", target_os = "linux", - feature = "winit", feature = "surface-message" ))] if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index 6211e201..c534c5b7 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -10,7 +10,6 @@ use super::menu_tree::MenuTree; feature = "multi-window", feature = "wayland", target_os = "linux", - feature = "winit", feature = "surface-message" ))] use crate::app::cosmic::{WINDOWING_SYSTEM, WindowingSystem}; @@ -680,7 +679,6 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { feature = "multi-window", feature = "wayland", target_os = "linux", - feature = "winit", feature = "surface-message" ))] if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) @@ -965,7 +963,6 @@ impl Widget( feature = "multi-window", feature = "wayland", target_os = "linux", - feature = "winit", feature = "surface-message" ))] pub(super) fn init_root_popup_menu( @@ -1525,15 +1521,14 @@ where .as_ref() .is_some_and(|i| *i != new_index && !active_menu[*i].children.is_empty()); - #[cfg(all(feature = "multi-window", feature = "wayland",target_os = "linux", feature = "winit", feature = "surface-message"))] - if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) && remove { - if let Some(id) = state.popup_id.remove(&menu.window_id) { + #[cfg(all(feature = "multi-window", feature = "wayland", target_os = "linux", feature = "surface-message"))] + if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) && remove + && let Some(id) = state.popup_id.remove(&menu.window_id) { state.active_root.truncate(menu.depth + 1); shell.publish((menu.on_surface_action.as_ref().unwrap())({ crate::surface::action::destroy_popup(id) })); } - } let item = &active_menu[new_index]; // set new index let old_index = last_menu_state.index.replace(new_index); diff --git a/src/widget/mod.rs b/src/widget/mod.rs index 5089b997..5764ab44 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -119,8 +119,6 @@ pub mod calendar; pub use calendar::{Calendar, calendar}; pub mod card; -#[doc(inline)] -pub use card::*; pub mod color_picker; #[doc(inline)] @@ -225,7 +223,7 @@ pub use grid::{Grid, grid}; mod header_bar; #[doc(inline)] -pub use header_bar::{HeaderBar, header_bar}; +pub use header_bar::{HeaderBar, WindowControlsPosition, header_bar}; pub mod icon; #[doc(inline)] @@ -312,7 +310,7 @@ pub use toggler::{Toggler, toggler}; #[doc(inline)] pub use tooltip::{Tooltip, tooltip}; -#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] +#[cfg(all(feature = "wayland", target_os = "linux"))] pub mod wayland; pub mod tooltip { diff --git a/src/widget/popover.rs b/src/widget/popover.rs index 57e4568a..138d5a19 100644 --- a/src/widget/popover.rs +++ b/src/widget/popover.rs @@ -160,8 +160,8 @@ where shell.capture_event(); return; } - } else if let Some(on_close) = self.on_close.as_ref() { - if matches!( + } else if let Some(on_close) = self.on_close.as_ref() + && matches!( event, Event::Mouse(mouse::Event::ButtonPressed(_)) | Event::Touch(touch::Event::FingerPressed { .. }) @@ -169,7 +169,6 @@ where { shell.publish(on_close.clone()); } - } } // Hide cursor from background content when modal popup is active @@ -472,12 +471,6 @@ where } } -/// The local state of a [`Popover`]. -#[derive(Debug, Default)] -struct State { - is_open: bool, -} - /// The first child in [`Popover::children`] is always the wrapped content. fn content_tree(tree: &Tree) -> &Tree { &tree.children[0] diff --git a/src/widget/progress_bar/circular.rs b/src/widget/progress_bar/circular.rs index 3a3fedc4..3318d7a3 100644 --- a/src/widget/progress_bar/circular.rs +++ b/src/widget/progress_bar/circular.rs @@ -9,7 +9,8 @@ use iced::{Element, Event, Length, Radians, Rectangle, Renderer, Size, Vector, m use std::f32::consts::PI; use std::time::Duration; -const MIN_ANGLE: Radians = Radians(PI / 8.0); +const MIN_GAP_ANGLE: Radians = Radians(PI / 4.0); +const MAX_WRAP: f32 = 1.0 - MIN_GAP_ANGLE.0 / (2.0 * PI); #[must_use] pub struct Circular @@ -76,12 +77,6 @@ where self.progress = Some(progress.clamp(0.0, 1.0)); self } - - fn min_wrap(&self, track_radius: f32) -> (f32, f32) { - let cap_angle = self.bar_height / track_radius; - let gap = MIN_ANGLE.0.max(cap_angle); - ((gap - cap_angle) / (2.0 * PI), 1.0 - gap / PI) - } } impl Default for Circular @@ -148,11 +143,12 @@ where shell.request_redraw(); } } else { - let (_, wrap) = self.min_wrap(self.size / 2.0 - self.bar_height); - state.animation = - state - .animation - .timed_transition(self.cycle_duration, self.period, wrap, *now); + state.animation = state.animation.timed_transition( + self.cycle_duration, + self.period, + MAX_WRAP, + *now, + ); state.cache.clear(); shell.request_redraw(); } @@ -191,25 +187,6 @@ where // Converts a track fraction to an angle in radians, with 0 being top of circle let to_angle = |t: f32| t * 2.0 * PI - PI / 2.0; - - let draw_cap = |frame: &mut canvas::Frame, t: f32, flip: bool| { - let angle = to_angle(t); - let center = frame.center() + Vector::new(angle.cos(), angle.sin()) * track_radius; - let (start_angle, end_angle) = if flip { - (angle - PI, angle) - } else { - (angle, angle + PI) - }; - let mut builder = canvas::path::Builder::new(); - builder.arc(canvas::path::Arc { - center, - radius: self.bar_height / 2.0, - start_angle: Radians(start_angle), - end_angle: Radians(end_angle), - }); - frame.fill(&builder.build(), custom_style.bar_color); - }; - let draw_bar = |frame: &mut canvas::Frame, start: f32, end: f32| { let mut builder = canvas::path::Builder::new(); builder.arc(canvas::path::Arc { @@ -222,10 +199,9 @@ where &builder.build(), canvas::Stroke::default() .with_color(custom_style.bar_color) - .with_width(self.bar_height), + .with_width(self.bar_height) + .with_line_cap(canvas::LineCap::Round), ); - draw_cap(frame, end, false); - draw_cap(frame, start, true); }; if self.progress.is_some() { @@ -243,10 +219,11 @@ where } draw_bar(frame, 0.0, state.progress.current); } else { - let (min, wrap) = self.min_wrap(track_radius); - let (start, end) = state - .animation - .bar_positions(self.cycle_duration, min, wrap); + // f32::EPSILON prevents flicker when wrap angle is 0.0 + let (start, end) = + state + .animation + .bar_positions(self.cycle_duration, f32::EPSILON, MAX_WRAP); draw_bar(frame, start, end); } }); diff --git a/src/widget/progress_bar/linear.rs b/src/widget/progress_bar/linear.rs index 0ebe402f..b8e92453 100644 --- a/src/widget/progress_bar/linear.rs +++ b/src/widget/progress_bar/linear.rs @@ -3,7 +3,7 @@ use super::animation::{Animation, Progress}; use super::style::StyleSheet; use iced::advanced::widget::tree::{self, Tree}; use iced::advanced::{self, Clipboard, Layout, Shell, Widget, layout, renderer}; -use iced::{Background, Element, Event, Length, Pixels, Rectangle, Size, mouse, window}; +use iced::{Element, Event, Length, Pixels, Rectangle, Size, mouse, window}; use std::time::Duration; @@ -39,7 +39,7 @@ where period: Duration::from_secs(2), progress: None, markers: Vec::new(), - segment_spacing: 0.0, + segment_spacing: 1.0, } } @@ -81,11 +81,11 @@ where } /// Sets the markers of a determinate progress bar, which divide the bar into segments. - /// Each value is a progress fraction between `0.0` and `1.0 at which a visual gap is inserted. + /// Each marker is a value between `0.0` and `1.0` that defines the position of a visual gap. pub fn markers(mut self, markers: impl Into>) -> Self { let mut markers = markers.into(); - for bp in &mut markers { - *bp = bp.clamp(0.0, 1.0); + for marker in &mut markers { + *marker = marker.clamp(0.0, 1.0); } markers.sort_by(f32::total_cmp); markers.dedup(); @@ -96,7 +96,7 @@ where /// Sets the spacing between segments at each marker. pub fn segment_spacing(mut self, spacing: impl Into) -> Self { - self.segment_spacing = spacing.into().0; + self.segment_spacing = spacing.into().0.max(1.0); self } } @@ -197,110 +197,144 @@ where let border_color = custom_style.border_color.unwrap_or(custom_style.bar_color); let radius = custom_style.border_radius; - let mut draw_quad = |x: f32, width: f32, color: iced::Color, border: iced::Border| { - // don't draw if width is less than 0.1 pixels - if width * bounds.width > 0.1 { - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x + x * bounds.width, - y: bounds.y, - width: width * bounds.width, - height: bounds.height, - }, - border, - snap: true, - ..renderer::Quad::default() - }, - Background::Color(color), - ); + let mut draw_quad = |x: f32, + width: f32, + color: iced::Color, + mut border: iced::Border, + is_track: bool, + total_progress_width: f32| { + let mut height = bounds.height; + if !is_track { + // For progress that is at the end of completion + if total_progress_width > bounds.width - radius { + let border_radius = + radius.min(bounds.height / 2.0) - (bounds.width - total_progress_width); + border.radius.top_right = border_radius; + border.radius.bottom_right = border_radius; + } else { + border.radius.top_right = 0.0; + border.radius.bottom_right = 0.0; + } + + // For indeterminate mode or when progress has just started + if x < radius.min(bounds.height / 2.0) { + let border_radius = radius.min(bounds.height / 2.0) - x; + border.radius.top_left = border_radius; + border.radius.bottom_left = border_radius; + + if total_progress_width < radius.min(bounds.height / 2.0) { + height = bounds.height - 2.0 * radius.min(bounds.height / 2.0) + + total_progress_width * 2.0; + } + } else { + border.radius.top_left = 0.0; + border.radius.bottom_left = 0.0; + } + + if x > bounds.width - radius.min(bounds.height / 2.0) { + height = bounds.height - 2.0 * radius.min(bounds.height / 2.0) + width * 2.0; + } } + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x + x, + y: bounds.y + (bounds.height - height) / 2.0, + width, + height, + }, + border, + snap: true, + ..renderer::Quad::default() + }, + color, + ); }; if self.progress.is_some() { - let spacing = self.segment_spacing.max(1.0); + let current_p = state.progress.current; + let len = self.markers.len(); + let spacing = self.segment_spacing; let radius_inner = radius.min(spacing); - let gap = if self.markers.is_empty() { - 0.0 - } else { + let gap = if len != 0 { spacing / bounds.width + } else { + 0.0 }; - let drawable = 1.0 - gap * self.markers.len() as f32; - let num_segments = self.markers.len() + 1; + let drawable = 1.0 - gap * len as f32; - let segment_bounds = |i: usize| { - let seg_lo = if i == 0 { 0.0 } else { self.markers[i - 1] }; - let seg_hi = if i == num_segments - 1 { - 1.0 + let mut absolute_width = 0.0; + for i in 0..=len { + let (seg_lo, r_left) = if i == 0 { + (0.0, radius) } else { - self.markers[i] + (self.markers[i - 1], radius_inner) }; - (seg_lo, seg_hi) - }; - let get_radius = |i: usize| { - let r_left = if i == 0 { radius } else { radius_inner }; - let r_right = if i == num_segments - 1 { - radius + let (seg_hi, r_right) = if i == len { + (1.0, radius) } else { - radius_inner + (self.markers[i], radius_inner) }; - [r_left, r_right, r_right, r_left].into() - }; - - // draw track segments - for i in 0..num_segments { - let (seg_lo, seg_hi) = segment_bounds(i); let x_start = seg_lo * drawable + i as f32 * gap; let x_width = (seg_hi - seg_lo) * drawable; + let mut segment_radius = if i == 0 && len == 0 { + [r_left, r_right, r_right, r_left].into() + } else if i == 0 { + [r_left, 0.0, 0.0, r_left].into() + } else if i == len { + [0.0, r_right, r_right, 0.0].into() + } else { + [0.0, 0.0, 0.0, 0.0].into() + }; + + // draw track segment draw_quad( - x_start, - x_width, + x_start * bounds.width, + x_width * bounds.width, custom_style.track_color, iced::Border { width: border_width, color: border_color, - radius: get_radius(i), + radius: segment_radius, }, + true, + bounds.width, ); - } - // draw bar segments - let current_p = state.progress.current; - for i in 0..num_segments { - let (seg_lo, seg_hi) = segment_bounds(i); - - // don't iterate over non-filled segments - if current_p < seg_lo { - break; + // draw bar segment + if current_p > seg_lo { + let fill = ((current_p - seg_lo) / (seg_hi - seg_lo)).min(1.0); + absolute_width += x_width * fill + if i == 0 { 0.0 } else { gap }; + segment_radius = [r_left, r_right, r_right, r_left].into(); + draw_quad( + x_start * bounds.width, + x_width * fill * bounds.width, + custom_style.bar_color, + iced::Border { + radius: segment_radius, + ..iced::Border::default() + }, + false, + absolute_width * bounds.width, + ); } - - let x_start = seg_lo * drawable + i as f32 * gap; - let x_width = (seg_hi - seg_lo) * drawable; - let fill = ((current_p - seg_lo) / (seg_hi - seg_lo)).clamp(0.0, 1.0); - - draw_quad( - x_start, - x_width * fill, - custom_style.bar_color, - iced::Border { - radius: get_radius(i), - ..iced::Border::default() - }, - ); } } else { // draw track draw_quad( 0.0, - 1.0, + bounds.width, custom_style.track_color, iced::Border { width: border_width, color: border_color, radius: radius.into(), }, + true, + bounds.width, ); // draw bar @@ -312,24 +346,26 @@ where let start = bar_start % 1.0; let right_width = (1.0 - start).min(length); let left_width = length - right_width; + let border = iced::Border { + radius: radius.into(), + ..iced::Border::default() + }; draw_quad( - start, - right_width, + start * bounds.width, + right_width * bounds.width, custom_style.bar_color, - iced::Border { - radius: radius.into(), - ..iced::Border::default() - }, + border, + false, + (right_width + start) * bounds.width, ); draw_quad( 0.0, - left_width, + left_width * bounds.width, custom_style.bar_color, - iced::Border { - radius: radius.into(), - ..iced::Border::default() - }, + border, + false, + left_width * bounds.width, ); } } diff --git a/src/widget/radio.rs b/src/widget/radio.rs index 7e6ef424..f8e174e1 100644 --- a/src/widget/radio.rs +++ b/src/widget/radio.rs @@ -262,7 +262,6 @@ where if cursor.is_over(layout.bounds()) { shell.publish(self.on_click.clone()); shell.capture_event(); - return; } } _ => {} diff --git a/src/widget/reorderable_flex_row/widget.rs b/src/widget/reorderable_flex_row/widget.rs index ed95a34f..03789877 100644 --- a/src/widget/reorderable_flex_row/widget.rs +++ b/src/widget/reorderable_flex_row/widget.rs @@ -21,6 +21,7 @@ use std::time::{Duration, Instant}; const DEFAULT_ANIMATION_DURATION: Duration = Duration::from_millis(180); const DEFAULT_DRAG_LIFT: f32 = 10.0; const DEFAULT_DRAG_THRESHOLD: f32 = 6.0; +#[cfg(feature = "wgpu")] const SHADOW_BLUR_RADIUS: f32 = 20.0; const POSITION_EPSILON: f32 = 0.5; diff --git a/src/widget/responsive_menu_bar.rs b/src/widget/responsive_menu_bar.rs index 2cae68d8..de323c13 100644 --- a/src/widget/responsive_menu_bar.rs +++ b/src/widget/responsive_menu_bar.rs @@ -23,7 +23,7 @@ impl Default for ResponsiveMenuBar { fn default() -> ResponsiveMenuBar { ResponsiveMenuBar { collapsed_item_width: { - #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] + #[cfg(all(feature = "wayland", target_os = "linux"))] if matches!( crate::app::cosmic::WINDOWING_SYSTEM.get(), Some(crate::app::cosmic::WindowingSystem::Wayland) @@ -32,7 +32,7 @@ impl Default for ResponsiveMenuBar { } else { ItemWidth::Static(84) } - #[cfg(not(all(feature = "winit", feature = "wayland", target_os = "linux")))] + #[cfg(not(all(feature = "wayland", target_os = "linux")))] { ItemWidth::Static(84) } diff --git a/src/widget/segmented_button/model/mod.rs b/src/widget/segmented_button/model/mod.rs index e0dd8c54..fff1cf6b 100644 --- a/src/widget/segmented_button/model/mod.rs +++ b/src/widget/segmented_button/model/mod.rs @@ -132,7 +132,10 @@ where /// ``` #[inline] pub fn clear(&mut self) { - for entity in self.order.clone() { + // `remove()` mutates `self.order`, so transfer ownership first: + // the inner `self.order.remove(index)` then no-ops because + // `position()` can't find the entity in the empty VecDeque. + for entity in std::mem::take(&mut self.order) { self.remove(entity); } } diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index edd58eb7..5e0b4f7f 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -175,6 +175,10 @@ where pub(super) on_context: Option Message + 'static>>, #[setters(skip)] pub(super) on_middle_press: Option Message + 'static>>, + /// Emits the ID of the item that was double-clicked with the left button. + /// Fires in addition to `on_activate` (which keeps firing on each click). + #[setters(skip)] + pub(super) on_double_click: Option Message + 'static>>, #[setters(skip)] pub(super) on_dnd_drop: Option, String, DndAction) -> Message + 'static>>, @@ -234,6 +238,7 @@ where on_close: None, on_context: None, on_middle_press: None, + on_double_click: None, on_dnd_drop: None, on_dnd_enter: None, on_dnd_leave: None, @@ -356,6 +361,16 @@ where self } + /// Emitted when a tab is double-clicked with the left mouse button. + /// Fires in addition to `on_activate`, which keeps firing on each click. + pub fn on_double_click(mut self, on_double_click: T) -> Self + where + T: Fn(Entity) -> Message + 'static, + { + self.on_double_click = Some(Box::new(on_double_click)); + self + } + /// Enable drag-and-drop support for tabs using the provided payload builder. pub fn enable_tab_drag(mut self, mime: String) -> Self { self.tab_drag = Some(TabDragSource::new(mime)); @@ -393,11 +408,12 @@ where { return None; } - let position = state - .drop_hint - .filter(|hint| hint.entity == target) - .map(|hint| InsertPosition::from(hint.side)) - .unwrap_or_else(|| self.default_insert_position(dragged, target)); + // Always use positional swap (Konsole/Firefox/Chrome semantics): + // dropping onto any part of a different tab swaps it with the dragged + // tab. drop_hint.side-based Before/After is counter-intuitive: dropping + // A (pos 0) on the left half of B (pos 1) resolves to "Before B" which, + // after removing A, lands at pos 0 — so the tab appears not to move. + let position = self.default_insert_position(dragged, target); Some(ReorderEvent { dragged, target, @@ -678,11 +694,10 @@ where if let Some(icon) = self.model.icon(key) { non_text_width += f32::from(icon.size) + f32::from(self.button_spacing); - } else if self.model.is_active(key) { - if let crate::theme::SegmentedButton::Control = self.style { + } else if self.model.is_active(key) + && let crate::theme::SegmentedButton::Control = self.style { non_text_width += 16.0 + f32::from(self.button_spacing); } - } if self.model.is_closable(key) { non_text_width += @@ -914,6 +929,7 @@ where hovered: Default::default(), known_length: Default::default(), middle_clicked: Default::default(), + last_click: None, internal_layout: Default::default(), context_cursor: Point::default(), show_context: Default::default(), @@ -1068,11 +1084,10 @@ where { state.drop_hint = None; self.emit_drop_hint(shell, state.drop_hint); - if let Some(Some(entity)) = entity { - if let Some(on_dnd_leave) = self.on_dnd_leave.as_ref() { + if let Some(Some(entity)) = entity + && let Some(on_dnd_leave) = self.on_dnd_leave.as_ref() { shell.publish(on_dnd_leave(entity)); } - } log::trace!( target: TAB_REORDER_LOG_TARGET, "offer leave id={my_id:?} entity={entity:?}" @@ -1148,11 +1163,10 @@ where None:: Message>, None, ); - if let Some(on_dnd_leave) = self.on_dnd_leave.as_ref() { - if let Some(Some(entity)) = entity { + if let Some(on_dnd_leave) = self.on_dnd_leave.as_ref() + && let Some(Some(entity)) = entity { shell.publish(on_dnd_leave(entity)); } - } } } DndEvent::Offer(id, OfferEvent::Drop) if Some(my_id) == *id => { @@ -1187,7 +1201,14 @@ where .dnd_state .drag_offer .as_ref() - .is_some_and(|offer| offer.selected_action.contains(DndAction::Move)); + .is_some_and(|offer| offer.selected_action.contains(DndAction::Move)) + // Self-drop fallback: some compositors (cosmic-comp + // observed) do not emit OfferEvent::SelectedAction for + // internal drags, leaving selected_action empty. + // dragging_tab is only set by start_tab_drag on this + // same widget, so this covers the self-drop case + // safely; mime and on_reorder are checked below. + || state.dragging_tab.is_some(); let pending_reorder = if allow_reorder && self.on_reorder.is_some() && self.tab_drag.as_ref().is_some_and(|d| d.mime == *mime_type) @@ -1318,8 +1339,8 @@ where // Emit close message if the close button is pressed. if let Some(on_close) = self.on_close.as_ref() { if over_close_button - && (left_button_released(&event) - || (touch_lifted(&event) && fingers_pressed == 1)) + && (left_button_released(event) + || (touch_lifted(event) && fingers_pressed == 1)) { shell.publish(on_close(key)); shell.capture_event(); @@ -1373,18 +1394,45 @@ where } } - if is_lifted(&event) { + if is_lifted(event) { state.unfocus(); } if let Some(on_activate) = self.on_activate.as_ref() { if is_pressed(event) { state.pressed_item = Some(Item::Tab(key)); - } else if is_lifted(&event) && self.button_is_pressed(state, key) { + } else if is_lifted(event) && self.button_is_pressed(state, key) { shell.publish(on_activate(key)); state.set_focused(); state.focused_item = Item::Tab(key); state.pressed_item = None; + + // Double-click detection on the same entity + // within 400 ms — fires after on_activate so + // the tab is already focused when the handler + // runs. + if let Some(on_double_click) = + self.on_double_click.as_ref() + { + let now = Instant::now(); + let is_double = match state.last_click { + Some((prev, t)) => { + prev == key + && now.duration_since(t) + < Duration::from_millis(400) + } + None => false, + }; + state.last_click = if is_double { + None + } else { + Some((key, now)) + }; + if is_double { + shell.publish(on_double_click(key)); + } + } + shell.capture_event(); return; } @@ -1393,8 +1441,8 @@ where // Present a context menu on a right click event. if self.context_menu.is_some() && let Some(on_context) = self.on_context.as_ref() - && (right_button_released(&event) - || (touch_lifted(&event) && fingers_pressed == 2)) + && (right_button_released(event) + || (touch_lifted(event) && fingers_pressed == 2)) { state.show_context = Some(key); state.context_cursor = cursor_position.position().unwrap_or_default(); @@ -1492,12 +1540,12 @@ where } if state.is_focused() { // Unfocus on clicks outside of the boundaries of the segmented button. - if is_pressed(&event) { + if is_pressed(event) { state.unfocus(); state.pressed_item = None; return; } - } else if is_lifted(&event) { + } else if is_lifted(event) { state.pressed_item = None; } } @@ -2116,8 +2164,8 @@ where ); } - if show_drop_hint_marker { - if matches!( + if show_drop_hint_marker + && matches!( drop_hint_marker, Some(DropHint { entity, @@ -2132,7 +2180,6 @@ where appearance.active.text_color, ); } - } nth += 1; }); @@ -2317,7 +2364,7 @@ where } } -struct TabDragSource { +pub(super) struct TabDragSource { mime: String, threshold: f32, _marker: PhantomData, @@ -2368,7 +2415,6 @@ struct TabDragCandidate { #[derive(Debug, Clone, Copy)] struct Focus { updated_at: Instant, - now: Instant, } /// State that is maintained by each individual widget. @@ -2391,6 +2437,9 @@ pub struct LocalState { hovered: Item, /// The ID of the button that was middle-clicked, but not yet released. middle_clicked: Option, + /// Entity and timestamp of the most recent left-click activation, used + /// to detect double-clicks on the same tab. + last_click: Option<(Entity, Instant)>, /// Last known length of the model. pub(super) known_length: usize, /// Dimensions of internal buttons when shrinking @@ -2438,7 +2487,6 @@ impl LocalState { self.focused = Some(Focus { updated_at: now, - now, }); } } @@ -2536,6 +2584,7 @@ mod tests { hovered: Item::default(), known_length: 0, middle_clicked: None, + last_click: None, internal_layout: Vec::new(), context_cursor: Point::ORIGIN, show_context: None, diff --git a/src/widget/spin_button.rs b/src/widget/spin_button.rs index 71933124..b2528861 100644 --- a/src/widget/spin_button.rs +++ b/src/widget/spin_button.rs @@ -23,7 +23,7 @@ pub fn spin_button<'a, T, M>( where T: Copy + Sub + Add + PartialOrd, { - let mut button = SpinButton::new( + let button = SpinButton::new( label, value, step, @@ -34,9 +34,11 @@ where ); #[cfg(feature = "a11y")] - { + let button = { + let mut button = button; button = button.name(name.into()); - } + button + }; button } @@ -54,20 +56,22 @@ pub fn vertical<'a, T, M>( where T: Copy + Sub + Add + PartialOrd, { - let mut button = SpinButton::new( + let button = SpinButton::new( label, value, step, min, max, - Orientation::Horizontal, + Orientation::Vertical, on_press, ); #[cfg(feature = "a11y")] - { + let button = { + let mut button = button; button = button.name(name.into()); - } + button + }; button } diff --git a/src/widget/table/model/mod.rs b/src/widget/table/model/mod.rs index 93998f9f..f0e5e61c 100644 --- a/src/widget/table/model/mod.rs +++ b/src/widget/table/model/mod.rs @@ -98,7 +98,10 @@ where /// model.clear(); /// ``` pub fn clear(&mut self) { - for entity in self.order.clone() { + // `remove()` mutates `self.order`, so transfer ownership first: + // the inner `self.order.remove(index)` then no-ops because + // `position()` can't find the entity in the empty VecDeque. + for entity in std::mem::take(&mut self.order) { self.remove(entity); } } diff --git a/src/widget/table/widget/standard.rs b/src/widget/table/widget/standard.rs index 0e206238..e21cd22f 100644 --- a/src/widget/table/widget/standard.rs +++ b/src/widget/table/widget/standard.rs @@ -84,15 +84,14 @@ where let mut sort_state = 0; - if let Some(sort) = val.model.sort { - if sort.0 == category { + if let Some(sort) = val.model.sort + && sort.0 == category { if sort.1 { sort_state = 1; } else { sort_state = 2; } - } - }; + }; // Build the category header widget::row::with_capacity(2) diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index ea1122ea..730fae25 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -21,12 +21,12 @@ use apply::Apply; use iced::Limits; use iced::clipboard::dnd::{DndAction, DndEvent, OfferEvent, SourceEvent}; use iced::clipboard::mime::AsMimeTypes; -use iced_core::event::{self, Event}; +use iced_core::event::Event; use iced_core::input_method::{self, InputMethod, Preedit}; use iced_core::mouse::{self, click}; use iced_core::overlay::Group; use iced_core::renderer::{self, Renderer as CoreRenderer}; -use iced_core::text::{self, Affinity, Paragraph, Renderer, Text}; +use iced_core::text::{self, Paragraph, Renderer, Text}; use iced_core::time::{Duration, Instant}; use iced_core::widget::Id; use iced_core::widget::operation::{self, Operation}; @@ -666,18 +666,15 @@ where let old_value = Value::new(&old_value); if state.is_focused() && let cursor::State::Index(index) = state.cursor.state(&old_value) - { - if index == old_value.len() { + && index == old_value.len() { state.cursor.move_to(self.value.len()); } - } - if let Some(f) = state.is_focused.as_ref().filter(|f| f.focused) { - if f.updated_at != LAST_FOCUS_UPDATE.with(|f| f.get()) { + if let Some(f) = state.is_focused.as_ref().filter(|f| f.focused) + && f.updated_at != LAST_FOCUS_UPDATE.with(|f| f.get()) { state.unfocus(); state.emit_unfocus = true; } - } if self.is_editable_variant { if !state.is_focused() { @@ -825,7 +822,7 @@ where &mut self, tree: &mut Tree, layout: Layout<'_>, - renderer: &crate::Renderer, + _renderer: &crate::Renderer, operation: &mut dyn Operation, ) { operation.container(Some(&self.id), layout.bounds()); @@ -891,8 +888,8 @@ where let line_height = self.line_height; // Disables editing of the editable variant when clicking outside of, or for tab focus changes. - if self.is_editable_variant { - if let Some(ref on_edit) = self.on_toggle_edit { + if self.is_editable_variant + && let Some(ref on_edit) = self.on_toggle_edit { let state = tree.state.downcast_mut::(); if !state.is_read_only && state.is_focused.is_some_and(|f| !f.focused) { state.is_read_only = true; @@ -904,7 +901,6 @@ where shell.publish((on_edit)(f.focused)); } } - } // Calculates the layout of the trailing icon button element. if !tree.children.is_empty() { @@ -915,9 +911,9 @@ where trailing_icon_layout = Some(text_layout.children().last().unwrap()); // Enable custom buttons defined on the trailing icon position to be handled. - if !self.is_editable_variant { - if let Some(trailing_layout) = trailing_icon_layout { - let res = trailing_icon.as_widget_mut().update( + if !self.is_editable_variant + && let Some(trailing_layout) = trailing_icon_layout { + trailing_icon.as_widget_mut().update( tree, event, trailing_layout, @@ -932,18 +928,16 @@ where return; } } - } } } let state = tree.state.downcast_mut::(); - if let Some(on_unfocus) = self.on_unfocus.as_ref() { - if state.emit_unfocus { + if let Some(on_unfocus) = self.on_unfocus.as_ref() + && state.emit_unfocus { state.emit_unfocus = false; shell.publish(on_unfocus.clone()); } - } let dnd_id = self.dnd_id(); let id = Widget::id(self); @@ -1652,11 +1646,10 @@ pub fn update<'a, Message: Clone + 'static>( if matches!(state.dragging_state, None | Some(DraggingState::Selection)) && (!state.is_focused() || (is_editable_variant && state.is_read_only)) { - if !state.is_focused() { - if let Some(on_focus) = on_focus { + if !state.is_focused() + && let Some(on_focus) = on_focus { shell.publish(on_focus.clone()); } - } if state.is_read_only { state.is_read_only = false; @@ -1681,7 +1674,6 @@ pub fn update<'a, Message: Clone + 'static>( state.last_click = Some(click); shell.capture_event(); - return; } else { state.unfocus(); @@ -1717,7 +1709,6 @@ pub fn update<'a, Message: Clone + 'static>( if cursor.is_over(layout.bounds()) { shell.capture_event(); } - return; } Event::Mouse(mouse::Event::CursorMoved { position }) | Event::Touch(touch::Event::FingerMoved { position, .. }) => { @@ -1800,7 +1791,6 @@ pub fn update<'a, Message: Clone + 'static>( } shell.capture_event(); - return; } } Event::Keyboard(keyboard::Event::KeyPressed { @@ -1825,14 +1815,13 @@ pub fn update<'a, Message: Clone + 'static>( if state.keyboard_modifiers.command() { match key.to_latin(*physical_key) { Some('c') => { - if !is_secure { - if let Some((start, end)) = state.cursor.selection(value) { + if !is_secure + && let Some((start, end)) = state.cursor.selection(value) { clipboard.write( iced_core::clipboard::Kind::Standard, value.select(start, end).to_string(), ); } - } } // XXX if we want to allow cutting of secure text, we need to // update the cache and decide which value to cut @@ -2087,7 +2076,6 @@ pub fn update<'a, Message: Clone + 'static>( } shell.capture_event(); - return; } } Event::Keyboard(keyboard::Event::KeyReleased { key, .. }) => { @@ -2107,7 +2095,6 @@ pub fn update<'a, Message: Clone + 'static>( } shell.capture_event(); - return; } } Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { @@ -2123,7 +2110,6 @@ pub fn update<'a, Message: Clone + 'static>( state.preedit = matches!(event, input_method::Event::Opened) .then(input_method::Preedit::new); shell.capture_event(); - return; } input_method::Event::Preedit(content, selection) => { if state.is_focused() { @@ -2133,7 +2119,6 @@ pub fn update<'a, Message: Clone + 'static>( text_size: Some(size.into()), }); shell.capture_event(); - return; } } input_method::Event::Commit(text) => { @@ -2151,7 +2136,7 @@ pub fn update<'a, Message: Clone + 'static>( LAST_FOCUS_UPDATE.with(|x| x.set(focus.updated_at)); let mut editor = Editor::new(unsecured_value, &mut state.cursor); - editor.paste(Value::new(&text)); + editor.paste(Value::new(text)); let contents = editor.contents(); let unsecured_value = Value::new(&contents); @@ -2171,7 +2156,6 @@ pub fn update<'a, Message: Clone + 'static>( update_cache(state, &value); shell.capture_event(); - return; } } } @@ -2201,7 +2185,6 @@ pub fn update<'a, Message: Clone + 'static>( // TODO: restore value in text input state.dragging_state = None; shell.capture_event(); - return; } } #[cfg(all(feature = "wayland", target_os = "linux"))] @@ -2211,12 +2194,12 @@ pub fn update<'a, Message: Clone + 'static>( x, y, mime_types, - surface, + surface: _surface, }, )) if *rectangle == Some(dnd_id) => { cold(); let state = state(); - let is_clicked = text_layout.bounds().contains(Point { + let _is_clicked = text_layout.bounds().contains(Point { x: *x as f32, y: *y as f32, }); @@ -2224,7 +2207,7 @@ pub fn update<'a, Message: Clone + 'static>( let mut accepted = false; for m in mime_types { if SUPPORTED_TEXT_MIME_TYPES.contains(&m.as_str()) { - let clone = m.clone(); + let _clone = m.clone(); accepted = true; } } @@ -2251,11 +2234,10 @@ pub fn update<'a, Message: Clone + 'static>( state.cursor.set_affinity(affinity); state.cursor.move_to(position); shell.capture_event(); - return; } } #[cfg(all(feature = "wayland", target_os = "linux"))] - Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Motion { x, y })) + Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Motion { x, y: _ })) if *rectangle == Some(dnd_id) => { let state = state(); @@ -2280,14 +2262,13 @@ pub fn update<'a, Message: Clone + 'static>( state.cursor.set_affinity(affinity); state.cursor.move_to(position); shell.capture_event(); - return; } #[cfg(all(feature = "wayland", target_os = "linux"))] Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Drop)) if *rectangle == Some(dnd_id) => { cold(); let state = state(); if let DndOfferState::HandlingOffer(mime_types, _action) = state.dnd_offer.clone() { - let Some(mime_type) = SUPPORTED_TEXT_MIME_TYPES + let Some(_mime_type) = SUPPORTED_TEXT_MIME_TYPES .iter() .find(|&&m| mime_types.iter().any(|t| t == m)) else { @@ -2297,14 +2278,12 @@ pub fn update<'a, Message: Clone + 'static>( }; state.dnd_offer = DndOfferState::Dropped; } - - return; } #[cfg(all(feature = "wayland", target_os = "linux"))] Event::Dnd(DndEvent::Offer(id, OfferEvent::LeaveDestination)) if Some(dnd_id) != *id => {} #[cfg(all(feature = "wayland", target_os = "linux"))] Event::Dnd(DndEvent::Offer( - rectangle, + _rectangle, OfferEvent::Leave | OfferEvent::LeaveDestination, )) => { cold(); @@ -2318,7 +2297,6 @@ pub fn update<'a, Message: Clone + 'static>( } }; shell.capture_event(); - return; } #[cfg(all(feature = "wayland", target_os = "linux"))] Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Data { data, mime_type })) @@ -2355,9 +2333,7 @@ pub fn update<'a, Message: Clone + 'static>( }; update_cache(state, &value); shell.capture_event(); - return; } - return; } _ => {} } @@ -2607,7 +2583,7 @@ pub fn draw<'a, Message>( let handling_dnd_offer = !matches!(state.dnd_offer, DndOfferState::None); #[cfg(not(all(feature = "wayland", target_os = "linux")))] let handling_dnd_offer = false; - let (cursors, offset, is_selecting) = if let Some(focus) = + let (cursors, offset, _is_selecting) = if let Some(focus) = state.is_focused.filter(|f| f.focused).or_else(|| { let now = Instant::now(); handling_dnd_offer.then_some(Focus { @@ -2790,11 +2766,11 @@ pub fn draw<'a, Message>( // draw the end icon in the text input if let (Some(icon), Some(tree)) = (trailing_icon, trailing_icon_tree) { let mut children = text_layout.children(); - let mut icon_layout = children.next().unwrap(); + children.next().unwrap(); // skip text layout if has_start_icon { - icon_layout = children.next().unwrap(); + children.next().unwrap(); // skip start-icon layout } - icon_layout = children.next().unwrap(); + let icon_layout = children.next().unwrap(); // trailing-icon layout icon.as_widget().draw( tree, diff --git a/src/widget/text_input/value.rs b/src/widget/text_input/value.rs index 3f7b8d73..59e77752 100644 --- a/src/widget/text_input/value.rs +++ b/src/widget/text_input/value.rs @@ -45,9 +45,7 @@ impl Value { pub fn previous_start_of_word(&self, index: usize) -> usize { let previous_string = &self.graphemes[..index.min(self.graphemes.len())].concat(); - UnicodeSegmentation::split_word_bound_indices(previous_string as &str) - .filter(|(_, word)| !word.trim_start().is_empty()) - .next_back() + UnicodeSegmentation::split_word_bound_indices(previous_string as &str).rfind(|(_, word)| !word.trim_start().is_empty()) .map_or(0, |(i, previous_word)| { index - UnicodeSegmentation::graphemes(previous_word, true).count() diff --git a/src/widget/toggler.rs b/src/widget/toggler.rs index 2dad2ed9..2254759e 100644 --- a/src/widget/toggler.rs +++ b/src/widget/toggler.rs @@ -182,7 +182,8 @@ impl<'a, Message> Widget for Toggler<'a, ) -> layout::Node { let limits = limits.width(self.width); - let res = next_to_each_other( + + next_to_each_other( &limits, self.spacing, |limits| { @@ -221,8 +222,7 @@ impl<'a, Message> Widget for Toggler<'a, } }, |_| layout::Node::new(Size::new(48., 24.)), - ); - res + ) } fn update(