diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7897eb01..a822642e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,17 +33,16 @@ jobs: strategy: fail-fast: false matrix: - test_args: - - --no-default-features --features "" # for cosmic-comp, don't remove! - - --no-default-features --features "winit_debug" - - --no-default-features --features "winit_tokio" - - --no-default-features --features "winit" - - --no-default-features --features "winit_wgpu" - - --no-default-features --features "wayland" - - --no-default-features --features "applet" - - --no-default-features --features "desktop,smol" - - --no-default-features --features "desktop,tokio" - - -p cosmic-theme + features: + - "" # for cosmic-comp, don't remove! + - 'winit_debug' + - 'winit_tokio' + - winit + - winit_wgpu + - wayland + - applet + - desktop,smol + - desktop,tokio runs-on: ubuntu-22.04 steps: - name: Checkout sources @@ -67,7 +66,7 @@ jobs: - name: Rust toolchain uses: dtolnay/rust-toolchain@stable - name: Test features - run: cargo test ${{ matrix.test_args }} -- --test-threads=1 + run: cargo test --no-default-features --features "${{ matrix.features }}" -- --test-threads=1 env: RUST_BACKTRACE: full @@ -104,7 +103,7 @@ jobs: run: sudo apt-get update; sudo apt-get install -y libxkbcommon-dev libwayland-dev - name: Rust toolchain uses: dtolnay/rust-toolchain@stable - - name: Check example + - name: Test example run: cargo check -p "${{ matrix.examples }}" env: RUST_BACKTRACE: full diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 3e3a042e..46d53ad2 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -7,30 +7,19 @@ on: jobs: pages: + runs-on: ubuntu-latest steps: - - name: Checkout sources - uses: actions/checkout@v3 - with: - submodules: recursive - - name: Install Rust nightly - uses: dtolnay/rust-toolchain@master - with: - toolchain: nightly-2025-07-31 - - name: System dependencies - run: sudo apt-get update; sudo apt-get install -y libxkbcommon-dev libwayland-dev - - name: Build documentation - run: | - RUSTDOCFLAGS="--cfg docsrs" \ - cargo +nightly-2025-07-31 doc --no-deps \ - -p cosmic-client-toolkit \ - -p cosmic-protocols \ - -p libcosmic \ - --verbose --features tokio,winit,wayland,desktop,single-instance,applet,xdg-portal,multi-window - - name: Deploy documentation - uses: peaceiris/actions-gh-pages@v3 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./target/doc - force_orphan: true + - name: Checkout sources + uses: actions/checkout@v3 + with: + submodules: recursive + - name: Build documentation + run: cargo doc --verbose --features tokio,winit + - name: Deploy documentation + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./target/doc + force_orphan: true diff --git a/Cargo.toml b/Cargo.toml index d73da2dc..ecb84bb5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,10 +14,9 @@ default = [ "a11y", "dbus-config", "x11", - "iced-wayland", + "wayland", "multi-window", -] -advanced-shaping = ["iced/advanced-shaping"] +] # default = ["dbus-config", "multi-window", "a11y"] # Accessibility support a11y = ["iced/a11y", "iced_accessibility"] # Enable about widget @@ -58,7 +57,6 @@ desktop = [ "process", "dep:cosmic-settings-config", "dep:freedesktop-desktop-entry", - "dep:image-extras", "dep:mime", "dep:shlex", "tokio?/io-util", @@ -82,21 +80,15 @@ tokio = [ ] # Tokio async runtime # Wayland window support -iced-wayland = [ +wayland = [ "ashpd?/wayland", "autosize", + "iced_runtime/wayland", "iced/wayland", "iced_winit/wayland", + "cctk", "surface-message", ] -wayland = [ - "iced-wayland", - "iced_runtime/cctk", - "iced_winit/cctk", - "iced_wgpu/cctk", - "iced/cctk", - "dep:cctk", -] surface-message = [] # multi-window support multi-window = [] @@ -123,10 +115,10 @@ x11 = ["iced/x11", "iced_winit/x11"] [dependencies] apply = "0.3.0" -ashpd = { version = "0.12.3", default-features = false, optional = true } +ashpd = { version = "0.12.1", default-features = false, optional = true } async-fs = { version = "2.2", optional = true } async-std = { version = "1.13", optional = true } -auto_enums = "0.8.8" +auto_enums = "0.8.7" cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "160b086", optional = true } jiff = "0.2" cosmic-config = { path = "cosmic-config" } @@ -139,21 +131,17 @@ i18n-embed = { version = "0.16.0", features = [ i18n-embed-fl = "0.10" rust-embed = "8.11.0" css-color = "0.2.8" -derive_setters = "0.1.9" +derive_setters = "0.1.8" futures = "0.3" -image = { version = "0.25.10", default-features = false, features = [ - "ico", +image = { version = "0.25.9", default-features = false, features = [ "jpeg", "png", ] } -image-extras = { version = "0.1.0", default-features = false, features = [ - "xpm", - "xbm", -], optional = true } -libc = { version = "0.2.183", optional = true } +libc = { version = "0.2.180", optional = true } log = "0.4" mime = { version = "0.3.17", optional = true } palette = "0.7.6" +raw-window-handle = "0.6" rfd = { version = "0.16.0", default-features = false, features = [ "xdg-portal", ], optional = true } @@ -163,25 +151,25 @@ slotmap = "1.1.1" smol = { version = "2.0.2", optional = true } thiserror = "2.0.18" taffy = { version = "0.9.2", features = ["grid"] } -tokio = { version = "1.50.0", optional = true } +tokio = { version = "1.49.0", optional = true } tracing = "0.1.44" unicode-segmentation = "1.12" url = "2.5.8" -zbus = { version = "5.14.0", default-features = false, optional = true } +zbus = { version = "5.13.2", default-features = false, optional = true } float-cmp = "0.10.0" # 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 = { version = "5.14.0", default-features = false } +zbus = { version = "5.13.2", default-features = false } -[target.'cfg(all(unix, not(target_os = "macos")))'.dependencies] +[target.'cfg(unix)'.dependencies] freedesktop-icons = { package = "cosmic-freedesktop-icons", git = "https://github.com/pop-os/freedesktop-icons" } freedesktop-desktop-entry = { version = "0.8.1", optional = true } shlex = { version = "1.3.0", optional = true } -[target.'cfg(any(not(unix), target_os = "macos"))'.dependencies] +[target.'cfg(not(unix))'.dependencies] # Used to embed bundled icons for non-unix platforms. phf = { version = "0.13.1", features = ["macros"] } @@ -254,4 +242,4 @@ exclude = ["iced"] dirs = "6.0.0" [dev-dependencies] -tempfile = "3.27.0" +tempfile = "3.24.0" diff --git a/build.rs b/build.rs index 4ce0aa9e..c69feaf5 100644 --- a/build.rs +++ b/build.rs @@ -3,9 +3,7 @@ use std::env; fn main() { println!("cargo::rerun-if-changed=build.rs"); - if env::var_os("CARGO_CFG_UNIX").is_none() - || env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("macos") - { + if env::var_os("CARGO_CFG_UNIX").is_none() { generate_bundled_icons(); } } diff --git a/cosmic-config/Cargo.toml b/cosmic-config/Cargo.toml index 0a7653e0..6103c15e 100644 --- a/cosmic-config/Cargo.toml +++ b/cosmic-config/Cargo.toml @@ -11,9 +11,9 @@ subscription = ["iced_futures"] [dependencies] cosmic-settings-daemon = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true } -zbus = { version = "5.14.0", default-features = false, optional = true } +zbus = { version = "5.13.2", default-features = false, optional = true } atomicwrites = { git = "https://github.com/jackpot51/rust-atomicwrites" } -calloop = { version = "0.14.4", optional = true } +calloop = { version = "0.14.3", optional = true } notify = "8.2.0" ron = "0.12.0" serde = "1.0.228" @@ -22,7 +22,7 @@ iced = { path = "../iced/", default-features = false, optional = true } iced_futures = { path = "../iced/futures/", default-features = false, optional = true } futures-util = { version = "0.3", optional = true } dirs.workspace = true -tokio = { version = "1.50", optional = true, features = ["time"] } +tokio = { version = "1.49", optional = true, features = ["time"] } async-std = { version = "1.13", optional = true } tracing = "0.1" @@ -30,4 +30,4 @@ tracing = "0.1" xdg = "3.0" [target.'cfg(windows)'.dependencies] -known-folders = "1.4.2" +known-folders = "1.4.0" diff --git a/cosmic-theme/Cargo.toml b/cosmic-theme/Cargo.toml index 7e408d8d..80f4805d 100644 --- a/cosmic-theme/Cargo.toml +++ b/cosmic-theme/Cargo.toml @@ -22,7 +22,7 @@ serde_json = { version = "1.0.149", optional = true, features = [ "preserve_order", ] } ron = "0.12.0" -csscolorparser = { version = "0.8.3", features = ["serde"] } +csscolorparser = { version = "0.8.1", features = ["serde"] } cosmic-config = { path = "../cosmic-config/", default-features = false, features = [ "subscription", "macro", @@ -30,10 +30,3 @@ cosmic-config = { path = "../cosmic-config/", default-features = false, features configparser = "3.1.0" dirs.workspace = true thiserror = "2.0.18" - -[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 5db0f32c..8e1cd9f7 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -986,19 +986,19 @@ impl ThemeBuilder { let success = if let Some(success) = success { success.into_color() } else { - palette.as_ref().bright_green + palette.as_ref().accent_green }; let warning = if let Some(warning) = warning { warning.into_color() } else { - palette.as_ref().bright_orange + palette.as_ref().accent_yellow }; let destructive = if let Some(destructive) = destructive { destructive.into_color() } else { - palette.as_ref().bright_red + palette.as_ref().accent_red }; let text_steps_array = text_tint.map(|c| steps(c, NonZeroUsize::new(100).unwrap())); diff --git a/cosmic-theme/src/output/mod.rs b/cosmic-theme/src/output/mod.rs index 19f7bc5b..b2474dc1 100644 --- a/cosmic-theme/src/output/mod.rs +++ b/cosmic-theme/src/output/mod.rs @@ -46,10 +46,8 @@ impl Theme { pub fn write_exports(&self) -> Result<(), OutputError> { let gtk_res = self.write_gtk4(); let qt_res = self.write_qt(); - let qt56ct_res = self.write_qt56ct(); gtk_res?; qt_res?; - qt56ct_res?; Ok(()) } @@ -58,10 +56,8 @@ impl Theme { pub fn reset_exports() -> Result<(), OutputError> { let gtk_res = Theme::reset_gtk(); let qt_res = Theme::reset_qt(); - let qt56ct_res = Theme::reset_qt56ct(); gtk_res?; qt_res?; - qt56ct_res?; Ok(()) } } diff --git a/cosmic-theme/src/output/qt56ct_output.rs b/cosmic-theme/src/output/qt56ct_output.rs index 43a45470..552e7fec 100644 --- a/cosmic-theme/src/output/qt56ct_output.rs +++ b/cosmic-theme/src/output/qt56ct_output.rs @@ -1,11 +1,8 @@ use crate::Theme; use configparser::ini::Ini; -use palette::{Mix, Srgba, WithAlpha, blend::Compose, rgb::Rgba}; use std::{ fs::{self, File}, - io::Write, path::PathBuf, - vec, }; use super::{OutputError, qt_settings_ini_style}; @@ -18,117 +15,7 @@ impl Theme { /// Increment this value when changes to qt{5,6}ct.conf are needed. /// If the config's version is outdated, we update several sections. /// Otherwise, only the light/dark mode is updated. - const COSMIC_QT_VERSION: u64 = 2; - - /// Produces a QPalette ini file for qt5ct and qt6ct. - /// - /// Example file: https://github.com/trialuser02/qt6ct/blob/master/colors/airy.conf - #[must_use] - #[cold] - pub fn as_qpalette(&self) -> String { - let lightest = if self.is_dark { - self.background.on - } else { - self.background.base - }; - let darkest = if self.is_dark { - self.background.base - } else { - self.background.on - }; - let active = QPaletteGroup { - window_text: self.background.on, - button: self.button.base, - light: self.button.base.mix(lightest, 0.1), - midlight: self.button.base.mix(lightest, 0.05), - dark: self.button.base.mix(darkest, 0.1), - mid: self.button.base.mix(darkest, 0.05), - text: self.background.component.on, - bright_text: lightest, - button_text: self.button.on, - base: self.background.component.base, - window: self.background.base, - shadow: darkest, - // selection colors are swapped to fix menu bar contrast - highlight: self.background.component.selected_text, - highlighted_text: self.background.component.selected, - link: self.link_button.on, - link_visited: self.link_button.on.mix(self.secondary.component.base, 0.2), - alternate_base: self.background.base.mix(self.accent.base, 0.05), - no_role: self.background.component.disabled, - tool_tip_base: self.background.component.base, - tool_tip_text: self.background.component.on, - placeholder_text: self.background.component.on.with_alpha(0.5), - }; - let inactive = QPaletteGroup { - window_text: active.window_text.with_alpha(0.8), - text: active.text.with_alpha(0.8), - highlighted_text: active.highlighted_text.with_alpha(0.8), - tool_tip_text: active.tool_tip_text.with_alpha(0.8), - ..active - }; - let disabled = QPaletteGroup { - button: self.button.disabled, - text: self.background.component.on_disabled, - button_text: self.button.on_disabled, - base: self.background.component.disabled, - highlighted_text: active.highlighted_text.with_alpha(0.5), - link: self.link_button.on_disabled, - link_visited: self - .link_button - .on_disabled - .mix(self.secondary.component.disabled, 0.2), - alternate_base: self.background.base.mix(self.accent.disabled, 0.05), - tool_tip_base: self.background.component.disabled, - tool_tip_text: self.background.component.on_disabled, - placeholder_text: self.background.component.on_disabled.with_alpha(0.5), - ..inactive - }; - - format!( - r#"# GENERATED BY COSMIC - -[ColorScheme] -active_colors={} -disabled_colors={} -inactive_colors={} -"#, - active.as_list(), - disabled.as_list(), - inactive.as_list(), - ) - } - - /// Writes the QPalette ini files to: - /// - `~/.config/qt6ct/colors/` - /// - `~/.config/qt5ct/colors/` - #[cold] - pub fn write_qt56ct(&self) -> Result<(), OutputError> { - let qpalette = self.as_qpalette(); - let qt5ct_res = self.write_ct("qt5ct", &qpalette); - let qt6ct_res = self.write_ct("qt6ct", &qpalette); - qt5ct_res?; - qt6ct_res?; - Ok(()) - } - #[must_use] - #[cold] - fn write_ct(&self, ct: &str, qpalette: &str) -> Result<(), OutputError> { - let file_path = Self::get_qpalette_path(ct, self.is_dark)?; - let tmp_file_path = file_path.with_extension("conf.new"); - - let mut tmp_file = File::create(&tmp_file_path).map_err(OutputError::Io)?; - let res = tmp_file - .write_all(qpalette.as_bytes()) - .and_then(|_| tmp_file.flush()) - .and_then(|_| std::fs::rename(&tmp_file_path, file_path)); - if let Err(e) = res { - _ = std::fs::remove_file(&tmp_file_path); - return Err(OutputError::Io(e)); - } - - Ok(()) - } + const COSMIC_QT_VERSION: u64 = 1; /// Edits qt{5,6}ct.conf to use COSMIC styles if needed. #[cold] @@ -152,7 +39,7 @@ inactive_colors={} .map_err(OutputError::Ini)? .unwrap_or_default(); - let color_scheme_path = Self::get_qpalette_path(ct, is_dark)?; + let color_scheme_path = Self::get_qt_colors_path(is_dark)?; let icon_theme = if is_dark { "breeze-dark" } else { "breeze" }; ini.set( @@ -204,48 +91,11 @@ inactive_colors={} Ok(()) } - /// Reset the applied qt56ct config by removing COSMIC-specific entries from the config file. - #[cold] - pub fn reset_qt56ct() -> Result<(), OutputError> { - let qt5ct_res = Self::reset_ct("qt5ct"); - let qt6ct_res = Self::reset_ct("qt6ct"); - qt5ct_res?; - qt6ct_res?; - Ok(()) - } - #[must_use] - #[cold] - fn reset_ct(ct: &str) -> Result<(), OutputError> { - let path = Self::get_conf_path(ct)?; - let file_content = fs::read_to_string(&path).map_err(OutputError::Io)?; - let mut ini = Ini::new_cs(); - ini.read(file_content).map_err(OutputError::Ini)?; - - let old_version = ini - .getuint("Appearance", "cosmic_qt_version") - .map_err(OutputError::Ini)? - .unwrap_or_default(); - if old_version == 0 { - return Ok(()); - } - - ini.remove_key("Appearance", "cosmic_qt_version"); - ini.remove_key("Appearance", "color_scheme_path"); - ini.remove_key("Appearance", "icon_theme"); - - ini.pretty_write(path, &qt_settings_ini_style()) - .map_err(OutputError::Io)?; - Ok(()) - } - /// Returns the file paths of the form `~/.config/ct/ct.conf`: /// e.g. `~/.config/qt6ct/qt6ct.conf`. /// /// The file and its parent directory are created if they don't exist. - #[cold] fn get_conf_path(ct: &str) -> Result { - assert!(ct == "qt5ct" || ct == "qt6ct"); - let Some(mut config_dir) = dirs::config_dir() else { return Err(OutputError::MissingConfigDir); }; @@ -261,155 +111,4 @@ inactive_colors={} Ok(file_path) } - - /// Gets a path like `~/.config/qt6ct/colors/CosmicDark.conf` - /// - /// Its parent directory is created if it doesn't exist. - #[cold] - fn get_qpalette_path(ct: &str, is_dark: bool) -> Result { - assert!(ct == "qt5ct" || ct == "qt6ct"); - - let Some(mut config_dir) = dirs::config_dir() else { - return Err(OutputError::MissingConfigDir); - }; - config_dir.push(&ct); - config_dir.push("colors"); - if !config_dir.exists() { - fs::create_dir_all(&config_dir).map_err(OutputError::Io)?; - } - - let file_name = if is_dark { - "CosmicDark.conf" - } else { - "CosmicLight.conf" - }; - - Ok(config_dir.join(file_name)) - } -} - -/// Defines the different symbolic color roles used in current GUIs. -/// -/// qt5ct and qt6ct consume this as a list of colors, ordered by ColorRole: -/// - https://doc.qt.io/qt-6/qpalette.html#ColorRole-enum -/// - https://doc.qt.io/archives/qt-5.15/qpalette.html#ColorRole-enum -struct QPaletteGroup { - /// A general foreground color. - window_text: Srgba, - /// The general button background color. - button: Srgba, - /// Lighter than [button] color, used mostly for 3D bevel and shadow effects. - light: Srgba, - /// Between [button] and [light], used mostly for 3D bevel and shadow effects. - midlight: Srgba, - /// Darker than [button], used mostly for 3D bevel and shadow effects. - dark: Srgba, - /// Between [button] and [dark], used mostly for 3D bevel and shadow effects. - mid: Srgba, - /// The foreground color used with [base]. - text: Srgba, - /// A text color that is very different from [window_text], and contrasts well with e.g. [dark]. - /// Typically used for text that needs to be drawn where [text] or [window_text] would give poor contrast, such as on pressed push buttons. - bright_text: Srgba, - /// A foreground color used with the [button] color. - button_text: Srgba, - /// Used mostly as the background color for text entry widgets, but can also be used for other painting - - /// such as the background of combobox drop down lists and toolbar handles. - base: Srgba, - /// A general background color. - window: Srgba, - /// A very dark color, used mostly for 3D bevel and shadow effects. - /// Opaque black by default. - shadow: Srgba, - /// A color to indicate a selected item or the current item. - highlight: Srgba, - /// A text color that contrasts with [highlight]. - highlighted_text: Srgba, - /// A text color used for unvisited hyperlinks. - link: Srgba, - /// A text color used for already visited hyperlinks. - link_visited: Srgba, - /// Used as the alternate background color in views with alternating row colors. - alternate_base: Srgba, - /// No role; this special role is often used to indicate that a role has not been assigned. - no_role: Srgba, - /// Used as the background color for QToolTip and QWhatsThis. - /// Tool tips use the inactive color group of QPalette, because tool tips are not active windows. - tool_tip_base: Srgba, - /// Used as the foreground color for QToolTip and QWhatsThis. - /// Tool tips use the inactive color group of QPalette, because tool tips are not active windows. - tool_tip_text: Srgba, - /// Used as the placeholder color for various text input widgets. - placeholder_text: Srgba, - // /// [accent] only exists since Qt 6.6. Including it here breaks qt5ct. - // /// When omitted, it defaults to [highlight]. - // accent: Srgba, -} - -impl QPaletteGroup { - /// Returns a comma-separated list of the colors as hex codes. - /// E.g. `#ff000000, #ffdcdcdc, ...` - /// - /// Any transparent colors are flattened with [base] to avoid issues with - /// the Fusion style. - fn as_list(&self) -> String { - let colors = vec![ - to_argb_hex(self.window_text.over(self.base)), - to_argb_hex(self.button.over(self.base)), - to_argb_hex(self.light.over(self.base)), - to_argb_hex(self.midlight.over(self.base)), - to_argb_hex(self.dark.over(self.base)), - to_argb_hex(self.mid.over(self.base)), - to_argb_hex(self.text.over(self.base)), - to_argb_hex(self.bright_text.over(self.base)), - to_argb_hex(self.button_text.over(self.base)), - to_argb_hex(self.base.over(self.base)), - to_argb_hex(self.window.over(self.base)), - to_argb_hex(self.shadow.over(self.base)), - to_argb_hex(self.highlight.over(self.base)), - to_argb_hex(self.highlighted_text.over(self.base)), - to_argb_hex(self.link.over(self.base)), - to_argb_hex(self.link_visited.over(self.base)), - to_argb_hex(self.alternate_base.over(self.base)), - to_argb_hex(self.no_role.over(self.base)), - to_argb_hex(self.tool_tip_base.over(self.base)), - to_argb_hex(self.tool_tip_text.over(self.base)), - to_argb_hex(self.placeholder_text.over(self.base)), - ]; - colors.join(", ") - } -} - -/// Converts a color to a hex string in the format `#AARRGGBB`. -/// Do not use [to_hex] since that uses the format `RRGGBBAA`. -fn to_argb_hex(c: Srgba) -> String { - let c_u8: Rgba = c.into_format(); - format!( - "#{:02x}{:02x}{:02x}{:02x}", - c_u8.alpha, c_u8.red, c_u8.green, c_u8.blue - ) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_color_to_argb_hex() { - let color = Srgba::new(0x33, 0x55, 0x77, 0xff); - let argb = to_argb_hex(color.into()); - assert_eq!(argb, "#ff335577"); - } - - #[test] - fn test_light_default_qpalette() { - let light_default_qpalette = Theme::light_default().as_qpalette(); - insta::assert_snapshot!(light_default_qpalette); - } - - #[test] - fn test_dark_default_qpalette() { - let dark_default_qpalette = Theme::dark_default().as_qpalette(); - insta::assert_snapshot!(dark_default_qpalette); - } } diff --git a/cosmic-theme/src/output/qt_output.rs b/cosmic-theme/src/output/qt_output.rs index d42d553b..9bca3d18 100644 --- a/cosmic-theme/src/output/qt_output.rs +++ b/cosmic-theme/src/output/qt_output.rs @@ -14,11 +14,10 @@ impl Theme { /// Produces a color scheme ini file for Qt. /// /// Some high-level documentation for this file can be found at: - /// - https://api.kde.org/kcolorscheme.html - /// - https://web.archive.org/web/20250402234329/https://docs.kde.org/stable5/en/plasma-workspace/kcontrol/colors/ + /// https://web.archive.org/web/20250402234329/https://docs.kde.org/stable5/en/plasma-workspace/kcontrol/colors/ #[must_use] #[cold] - pub fn as_kcolorscheme(&self) -> String { + pub fn as_qt(&self) -> String { // Usually, disabled elements will have strongly reduced contrast and are often notably darker or lighter let disabled_color_effects = IniColorEffects { color: self.button.disabled, @@ -42,7 +41,7 @@ impl Theme { let bg = self.background.base; // the background container - let window_colors = IniColors { + let view_colors = IniColors { background_alternate: bg.mix(self.accent.base, 0.05), background_normal: bg, decoration_focus: self.accent_text_color(), @@ -57,17 +56,16 @@ impl Theme { foreground_visited: self.accent_text_color(), }; // components inside the background container - let view_colors = IniColors { + let window_colors = IniColors { background_alternate: self.background.component.base.mix(self.accent.base, 0.05), background_normal: self.background.component.base, - ..window_colors + ..view_colors }; // selected text and items let selection_colors = { - // selection colors are swapped to fix menu bar contrast - let selected = self.background.component.selected_text; - let selected_text = self.background.component.selected; + let selected = self.background.component.selected; + let selected_text = self.background.component.selected_text; IniColors { background_alternate: selected.mix(bg, 0.5), background_normal: selected, @@ -94,11 +92,8 @@ impl Theme { let complementary_colors = { let dark = if self.is_dark { self.clone() - } else if cfg!(test) { - // For reproducible results in tests, use the default dark theme - Theme::dark_default() } else { - Theme::dark_config() + Theme::light_config() .ok() .as_ref() .and_then(|conf| Theme::get_entry(conf).ok()) @@ -121,10 +116,10 @@ impl Theme { }; // headers in cosmic don't have a background - let header_colors = &window_colors; - let header_colors_inactive = &window_colors; + let header_colors = &view_colors; + let header_colors_inactive = &view_colors; // tool tips, "What's This" tips, and similar elements - let tooltip_colors = &view_colors; + let tooltip_colors = &window_colors; let general_color_scheme = if self.is_dark { "CosmicDark" @@ -203,7 +198,7 @@ widgetStyle=qt6ct-style format_ini_colors(&tooltip_colors, bg), format_ini_colors(&view_colors, bg), format_ini_colors(&window_colors, bg), - format_ini_wm_colors(&window_colors, self.is_dark), + format_ini_wm_colors(&view_colors, self.is_dark), ) } @@ -217,14 +212,14 @@ widgetStyle=qt6ct-style /// Returns an `OutputError` if there is an error writing the colors file. #[cold] pub fn write_qt(&self) -> Result<(), OutputError> { - let kcolorscheme = self.as_kcolorscheme(); - let file_path = Self::get_kcolorscheme_path(self.is_dark)?; + let colors = self.as_qt(); + let file_path = Self::get_qt_colors_path(self.is_dark)?; let tmp_file_path = file_path.with_extension("colors.new"); // Write to tmp_file_path first, then move it to file_path let mut tmp_file = File::create(&tmp_file_path).map_err(OutputError::Io)?; let res = tmp_file - .write_all(kcolorscheme.as_bytes()) + .write_all(colors.as_bytes()) .and_then(|_| tmp_file.flush()) .and_then(|_| std::fs::rename(&tmp_file_path, file_path)); if let Err(e) = res { @@ -250,7 +245,7 @@ widgetStyle=qt6ct-style let kdeglobals_file = config_dir.join("kdeglobals"); let mut kdeglobals_ini = Self::read_ini(&kdeglobals_file)?; - let src_file = Self::get_kcolorscheme_path(is_dark)?; + let src_file = Self::get_qt_colors_path(is_dark)?; let src_ini = Self::read_ini(&src_file)?; Self::backup_non_cosmic_kdeglobals(&kdeglobals_ini, &kdeglobals_file) @@ -293,7 +288,7 @@ widgetStyle=qt6ct-style } let is_dark = false; // doesn't matter since we're only reading keys - let src_file = Self::get_kcolorscheme_path(is_dark)?; + let src_file = Self::get_qt_colors_path(is_dark)?; let src_ini = Self::read_ini(&src_file)?; for (section, key_value) in src_ini.get_map_ref() { @@ -308,8 +303,8 @@ widgetStyle=qt6ct-style Ok(()) } - /// Gets a path like `~/.local/share/color-schemes/CosmicDark.colors` - fn get_kcolorscheme_path(is_dark: bool) -> Result { + /// Gets a path like `~/.config/color-schemes/CosmicDark.colors` + pub fn get_qt_colors_path(is_dark: bool) -> Result { let Some(mut data_dir) = dirs::data_dir() else { return Err(OutputError::MissingDataDir); }; @@ -525,44 +520,3 @@ impl ColorEffect { } } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_opaque_color_to_rgb() { - let color = Srgba::new(30.0 / 255.0, 50.0 / 255.0, 70.0 / 255.0, 1.0); - let bg = Srgba::new(1.0, 1.0, 1.0, 1.0); - let result = to_rgb(color, bg); - assert_eq!(result, "30,50,70"); - } - - #[test] - fn test_transparent_color_to_rgb() { - let color = Srgba::new(0.0, 0.0, 0.0, 0.0); - let bg = Srgba::new(1.0, 1.0, 1.0, 1.0); - let result = to_rgb(color, bg); - assert_eq!(result, "255,255,255"); - } - - #[test] - fn test_translucent_color_to_rgb() { - let color = Srgba::new(0.0, 0.0, 0.0, 0.9); - let bg = Srgba::new(1.0, 1.0, 1.0, 1.0); - let result = to_rgb(color, bg); - assert_eq!(result, "26,26,26"); - } - - #[test] - fn test_light_default_kcolorscheme() { - let light_default_kcolorscheme = Theme::light_default().as_kcolorscheme(); - insta::assert_snapshot!(light_default_kcolorscheme); - } - - #[test] - fn test_dark_default_kcolorscheme() { - let dark_default_kcolorscheme = Theme::dark_default().as_kcolorscheme(); - insta::assert_snapshot!(dark_default_kcolorscheme); - } -} diff --git a/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt56ct_output__tests__dark_default_qpalette.snap b/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt56ct_output__tests__dark_default_qpalette.snap deleted file mode 100644 index 15746fd0..00000000 --- a/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt56ct_output__tests__dark_default_qpalette.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: cosmic-theme/src/output/qt56ct_output.rs -expression: dark_default_qpalette ---- -# GENERATED BY COSMIC - -[ColorScheme] -active_colors=#ffe7e7e7, #ff4a4a4a, #ff555555, #ff505050, #ff4f4f4f, #ff4d4d4d, #ffc0c0c0, #ffe7e7e7, #ffc0c0c0, #ff2e2e2e, #ff1b1b1b, #ff1b1b1b, #ff63d0df, #ff434343, #ff63d0df, #ff5bb2be, #ff1f2425, #ff2e2e2e, #ff2e2e2e, #ffc0c0c0, #ff777777 -disabled_colors=#e6d3d3d3, #8f474747, #a9696969, #a4626262, #a95f5f5f, #a45d5d5d, #d2a1a1a1, #ffe7e7e7, #d2a1a1a1, #bf2e2e2e, #ff1b1b1b, #ff1b1b1b, #ff63d0df, #bf3c3c3c, #bf30555a, #bf324f53, #ff1f2425, #bf2e2e2e, #bf2e2e2e, #d2a1a1a1, #bf909090 -inactive_colors=#ffc2c2c2, #ff4a4a4a, #ff555555, #ff505050, #ff4f4f4f, #ff4d4d4d, #ffa3a3a3, #ffe7e7e7, #ffc0c0c0, #ff2e2e2e, #ff1b1b1b, #ff1b1b1b, #ff63d0df, #ff3f3f3f, #ff63d0df, #ff5bb2be, #ff1f2425, #ff2e2e2e, #ff2e2e2e, #ffa3a3a3, #ff777777 diff --git a/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt56ct_output__tests__light_default_qpalette.snap b/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt56ct_output__tests__light_default_qpalette.snap deleted file mode 100644 index c79b2c55..00000000 --- a/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt56ct_output__tests__light_default_qpalette.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: cosmic-theme/src/output/qt56ct_output.rs -expression: light_default_qpalette ---- -# GENERATED BY COSMIC - -[ColorScheme] -active_colors=#ff121212, #ffc3c3c3, #ffbababa, #ffbebebe, #ffb3b3b3, #ffbbbbbb, #ff272727, #ffd7d7d7, #ff272727, #fff5f5f5, #ffd7d7d7, #ff121212, #ff00525a, #fff6f6f6, #ff00525a, #ff317379, #ffccd0d1, #fff5f5f5, #fff5f5f5, #ff272727, #ff8e8e8e -disabled_colors=#e62b2b2b, #8fc9c9c9, #a99b9b9b, #a4a0a0a0, #a9929292, #a49b9b9b, #d2535353, #ffd7d7d7, #d2535353, #bff5f5f5, #ffd7d7d7, #ff121212, #ff00525a, #bff6f6f6, #bf526d70, #bf72888a, #ffccd0d1, #bff5f5f5, #bff5f5f5, #d2535353, #bf6c6c6c -inactive_colors=#ff3f3f3f, #ffc3c3c3, #ffbababa, #ffbebebe, #ffb3b3b3, #ffbbbbbb, #ff505050, #ffd7d7d7, #ff272727, #fff5f5f5, #ffd7d7d7, #ff121212, #ff00525a, #fff6f6f6, #ff00525a, #ff317379, #ffccd0d1, #fff5f5f5, #fff5f5f5, #ff505050, #ff8e8e8e diff --git a/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__dark_default_kcolorscheme.snap b/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__dark_default_kcolorscheme.snap deleted file mode 100644 index c50f95dc..00000000 --- a/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__dark_default_kcolorscheme.snap +++ /dev/null @@ -1,157 +0,0 @@ ---- -source: cosmic-theme/src/output/qt_output.rs -expression: dark_default_kcolorscheme ---- -# GENERATED BY COSMIC - -[ColorEffects:Disabled] -Color=43,43,43 -ColorAmount=0 -ColorEffect=0 -ContrastAmount=0.65 -ContrastEffect=1 -IntensityAmount=0.1 -IntensityEffect=2 - -[ColorEffects:Inactive] -ChangeSelectionColor=false -Enable=false -Color=27,27,27 -ColorAmount=0.025 -ColorEffect=2 -ContrastAmount=0.1 -ContrastEffect=2 -IntensityAmount=0 -IntensityEffect=0 - -[Colors:Button] -BackgroundAlternate=99,208,223 -BackgroundNormal=60,60,60 -DecorationFocus=99,208,223 -DecorationHover=99,208,223 -ForegroundActive=99,208,223 -ForegroundInactive=211,211,211 -ForegroundLink=99,208,223 -ForegroundNegative=255,160,154 -ForegroundNeutral=255,163,125 -ForegroundNormal=231,231,231 -ForegroundPositive=94,219,140 -ForegroundVisited=99,208,223 - -[Colors:Complementary] -BackgroundAlternate=99,208,223 -BackgroundNormal=27,27,27 -DecorationFocus=99,208,223 -DecorationHover=99,208,223 -ForegroundActive=99,208,223 -ForegroundInactive=211,211,211 -ForegroundLink=99,208,223 -ForegroundNegative=255,160,154 -ForegroundNeutral=255,163,125 -ForegroundNormal=231,231,231 -ForegroundPositive=94,219,140 -ForegroundVisited=99,208,223 - -[Colors:Header] -BackgroundAlternate=31,36,37 -BackgroundNormal=27,27,27 -DecorationFocus=99,208,223 -DecorationHover=99,208,223 -ForegroundActive=99,208,223 -ForegroundInactive=211,211,211 -ForegroundLink=99,208,223 -ForegroundNegative=255,160,154 -ForegroundNeutral=255,163,125 -ForegroundNormal=231,231,231 -ForegroundPositive=94,219,140 -ForegroundVisited=99,208,223 - -[Colors:Header][Inactive] -BackgroundAlternate=31,36,37 -BackgroundNormal=27,27,27 -DecorationFocus=99,208,223 -DecorationHover=99,208,223 -ForegroundActive=99,208,223 -ForegroundInactive=211,211,211 -ForegroundLink=99,208,223 -ForegroundNegative=255,160,154 -ForegroundNeutral=255,163,125 -ForegroundNormal=231,231,231 -ForegroundPositive=94,219,140 -ForegroundVisited=99,208,223 - -[Colors:Selection] -BackgroundAlternate=63,118,125 -BackgroundNormal=99,208,223 -DecorationFocus=99,208,223 -DecorationHover=99,208,223 -ForegroundActive=67,67,67 -ForegroundInactive=83,138,145 -ForegroundLink=27,27,27 -ForegroundNegative=255,160,154 -ForegroundNeutral=255,163,125 -ForegroundNormal=67,67,67 -ForegroundPositive=94,219,140 -ForegroundVisited=99,208,223 - -[Colors:Tooltip] -BackgroundAlternate=49,55,55 -BackgroundNormal=46,46,46 -DecorationFocus=99,208,223 -DecorationHover=99,208,223 -ForegroundActive=99,208,223 -ForegroundInactive=211,211,211 -ForegroundLink=99,208,223 -ForegroundNegative=255,160,154 -ForegroundNeutral=255,163,125 -ForegroundNormal=231,231,231 -ForegroundPositive=94,219,140 -ForegroundVisited=99,208,223 - -[Colors:View] -BackgroundAlternate=49,55,55 -BackgroundNormal=46,46,46 -DecorationFocus=99,208,223 -DecorationHover=99,208,223 -ForegroundActive=99,208,223 -ForegroundInactive=211,211,211 -ForegroundLink=99,208,223 -ForegroundNegative=255,160,154 -ForegroundNeutral=255,163,125 -ForegroundNormal=231,231,231 -ForegroundPositive=94,219,140 -ForegroundVisited=99,208,223 - -[Colors:Window] -BackgroundAlternate=31,36,37 -BackgroundNormal=27,27,27 -DecorationFocus=99,208,223 -DecorationHover=99,208,223 -ForegroundActive=99,208,223 -ForegroundInactive=211,211,211 -ForegroundLink=99,208,223 -ForegroundNegative=255,160,154 -ForegroundNeutral=255,163,125 -ForegroundNormal=231,231,231 -ForegroundPositive=94,219,140 -ForegroundVisited=99,208,223 - -[General] -ColorScheme=CosmicDark -Name=COSMIC Dark -shadeSortColumn=true - -[Icons] -Theme=breeze-dark - -[KDE] -contrast=4 -widgetStyle=qt6ct-style - -[WM] -activeBackground=27,27,27 -activeBlend=99,208,223 -activeForeground=99,208,223 -inactiveBackground=27,27,27 -inactiveBlend=99,208,223 -inactiveForeground=99,208,223 diff --git a/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__light_default_kcolorscheme.snap b/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__light_default_kcolorscheme.snap deleted file mode 100644 index ae2bcb66..00000000 --- a/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__light_default_kcolorscheme.snap +++ /dev/null @@ -1,157 +0,0 @@ ---- -source: cosmic-theme/src/output/qt_output.rs -expression: light_default_kcolorscheme ---- -# GENERATED BY COSMIC - -[ColorEffects:Disabled] -Color=194,194,194 -ColorAmount=0 -ColorEffect=0 -ContrastAmount=0.65 -ContrastEffect=1 -IntensityAmount=0.1 -IntensityEffect=2 - -[ColorEffects:Inactive] -ChangeSelectionColor=false -Enable=false -Color=215,215,215 -ColorAmount=0.025 -ColorEffect=2 -ContrastAmount=0.1 -ContrastEffect=2 -IntensityAmount=0 -IntensityEffect=0 - -[Colors:Button] -BackgroundAlternate=0,82,90 -BackgroundNormal=173,173,173 -DecorationFocus=0,82,90 -DecorationHover=0,82,90 -ForegroundActive=0,82,90 -ForegroundInactive=38,38,38 -ForegroundLink=0,82,90 -ForegroundNegative=137,4,24 -ForegroundNeutral=121,44,0 -ForegroundNormal=18,18,18 -ForegroundPositive=0,87,44 -ForegroundVisited=0,82,90 - -[Colors:Complementary] -BackgroundAlternate=99,208,223 -BackgroundNormal=27,27,27 -DecorationFocus=99,208,223 -DecorationHover=99,208,223 -ForegroundActive=99,208,223 -ForegroundInactive=211,211,211 -ForegroundLink=99,208,223 -ForegroundNegative=255,160,154 -ForegroundNeutral=255,163,125 -ForegroundNormal=231,231,231 -ForegroundPositive=94,219,140 -ForegroundVisited=99,208,223 - -[Colors:Header] -BackgroundAlternate=204,208,209 -BackgroundNormal=215,215,215 -DecorationFocus=0,82,90 -DecorationHover=0,82,90 -ForegroundActive=0,82,90 -ForegroundInactive=38,38,38 -ForegroundLink=0,82,90 -ForegroundNegative=137,4,24 -ForegroundNeutral=121,44,0 -ForegroundNormal=18,18,18 -ForegroundPositive=0,87,44 -ForegroundVisited=0,82,90 - -[Colors:Header][Inactive] -BackgroundAlternate=204,208,209 -BackgroundNormal=215,215,215 -DecorationFocus=0,82,90 -DecorationHover=0,82,90 -ForegroundActive=0,82,90 -ForegroundInactive=38,38,38 -ForegroundLink=0,82,90 -ForegroundNegative=137,4,24 -ForegroundNeutral=121,44,0 -ForegroundNormal=18,18,18 -ForegroundPositive=0,87,44 -ForegroundVisited=0,82,90 - -[Colors:Selection] -BackgroundAlternate=108,149,152 -BackgroundNormal=0,82,90 -DecorationFocus=0,82,90 -DecorationHover=0,82,90 -ForegroundActive=246,246,246 -ForegroundInactive=123,164,168 -ForegroundLink=215,215,215 -ForegroundNegative=137,4,24 -ForegroundNeutral=121,44,0 -ForegroundNormal=246,246,246 -ForegroundPositive=0,87,44 -ForegroundVisited=0,82,90 - -[Colors:Tooltip] -BackgroundAlternate=233,237,237 -BackgroundNormal=245,245,245 -DecorationFocus=0,82,90 -DecorationHover=0,82,90 -ForegroundActive=0,82,90 -ForegroundInactive=38,38,38 -ForegroundLink=0,82,90 -ForegroundNegative=137,4,24 -ForegroundNeutral=121,44,0 -ForegroundNormal=18,18,18 -ForegroundPositive=0,87,44 -ForegroundVisited=0,82,90 - -[Colors:View] -BackgroundAlternate=233,237,237 -BackgroundNormal=245,245,245 -DecorationFocus=0,82,90 -DecorationHover=0,82,90 -ForegroundActive=0,82,90 -ForegroundInactive=38,38,38 -ForegroundLink=0,82,90 -ForegroundNegative=137,4,24 -ForegroundNeutral=121,44,0 -ForegroundNormal=18,18,18 -ForegroundPositive=0,87,44 -ForegroundVisited=0,82,90 - -[Colors:Window] -BackgroundAlternate=204,208,209 -BackgroundNormal=215,215,215 -DecorationFocus=0,82,90 -DecorationHover=0,82,90 -ForegroundActive=0,82,90 -ForegroundInactive=38,38,38 -ForegroundLink=0,82,90 -ForegroundNegative=137,4,24 -ForegroundNeutral=121,44,0 -ForegroundNormal=18,18,18 -ForegroundPositive=0,87,44 -ForegroundVisited=0,82,90 - -[General] -ColorScheme=CosmicLight -Name=COSMIC Light -shadeSortColumn=true - -[Icons] -Theme=breeze - -[KDE] -contrast=4 -widgetStyle=qt6ct-style - -[WM] -activeBackground=215,215,215 -activeBlend=215,215,215 -activeForeground=0,82,90 -inactiveBackground=215,215,215 -inactiveBlend=215,215,215 -inactiveForeground=0,82,90 diff --git a/cosmic-theme/src/steps.rs b/cosmic-theme/src/steps.rs index 6ebf1015..143cf532 100644 --- a/cosmic-theme/src/steps.rs +++ b/cosmic-theme/src/steps.rs @@ -145,6 +145,7 @@ pub fn is_valid_srgb(c: Srgba) -> bool { #[cfg(test)] mod tests { + use almost::equal; use palette::{OklabHue, Srgba}; use super::{is_valid_srgb, oklch_to_srgba_nearest_chroma}; @@ -172,57 +173,57 @@ mod tests { fn test_conversion_boundaries() { let c1 = palette::Oklcha::new(0.0, 0.288, OklabHue::from_degrees(0.0), 1.0); let srgb = oklch_to_srgba_nearest_chroma(c1); - almost::zero(srgb.red); - almost::zero(srgb.blue); - almost::zero(srgb.green); + equal(srgb.red, 0.0); + equal(srgb.blue, 0.0); + equal(srgb.green, 0.0); let c1 = palette::Oklcha::new(1.0, 0.288, OklabHue::from_degrees(0.0), 1.0); let srgb = oklch_to_srgba_nearest_chroma(c1); - almost::equal(srgb.red, 1.0); - almost::equal(srgb.blue, 1.0); - almost::equal(srgb.green, 1.0); + equal(srgb.red, 1.0); + equal(srgb.blue, 1.0); + equal(srgb.green, 1.0); } #[test] fn test_conversion_colors() { let c1 = palette::Oklcha::new(0.4608, 0.11111, OklabHue::new(57.31), 1.0); let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); - assert_eq!(srgb.red, 133); - assert_eq!(srgb.green, 69); - assert_eq!(srgb.blue, 0); + assert!(srgb.red == 133); + assert!(srgb.green == 69); + assert!(srgb.blue == 0); let c1 = palette::Oklcha::new(0.30, 0.08, OklabHue::new(35.0), 1.0); let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); - assert_eq!(srgb.red, 78); - assert_eq!(srgb.green, 27); - assert_eq!(srgb.blue, 15); + assert!(srgb.red == 78); + assert!(srgb.green == 27); + assert!(srgb.blue == 15); let c1 = palette::Oklcha::new(0.757, 0.146, OklabHue::new(301.2), 1.0); let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); - assert_eq!(srgb.red, 192); - assert_eq!(srgb.green, 153); - assert_eq!(srgb.blue, 253); + assert!(srgb.red == 192); + assert!(srgb.green == 153); + assert!(srgb.blue == 253); } #[test] fn test_conversion_fallback_colors() { let c1 = palette::Oklcha::new(0.70, 0.284, OklabHue::new(35.0), 1.0); let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); - assert_eq!(srgb.red, 255); - assert_eq!(srgb.green, 102); - assert_eq!(srgb.blue, 65); + assert!(srgb.red == 255); + assert!(srgb.green == 103); + assert!(srgb.blue == 65); let c1 = palette::Oklcha::new(0.757, 0.239, OklabHue::new(301.2), 1.0); let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); - assert_eq!(srgb.red, 193); - assert_eq!(srgb.green, 152); - assert_eq!(srgb.blue, 255); + assert!(srgb.red == 193); + assert!(srgb.green == 152); + assert!(srgb.blue == 255); let c1 = palette::Oklcha::new(0.163, 0.333, OklabHue::new(141.0), 1.0); let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); - assert_eq!(srgb.red, 1); - assert_eq!(srgb.green, 19); - assert_eq!(srgb.blue, 0); + assert!(srgb.red == 1); + assert!(srgb.green == 19); + assert!(srgb.blue == 0); } } diff --git a/examples/about/src/main.rs b/examples/about/src/main.rs index c25a9b9a..50f25da4 100644 --- a/examples/about/src/main.rs +++ b/examples/about/src/main.rs @@ -132,7 +132,7 @@ impl cosmic::Application for App { fn view(&self) -> Element<'_, Self::Message> { let show_about_button = widget::button::text("Show about").on_press(Message::ToggleAbout); let centered = cosmic::widget::container( - widget::column::with_capacity(1) + widget::column() .push(show_about_button) .width(Length::Fill) .height(Length::Shrink) diff --git a/examples/applet/Cargo.toml b/examples/applet/Cargo.toml index 13eff684..f97bff44 100644 --- a/examples/applet/Cargo.toml +++ b/examples/applet/Cargo.toml @@ -13,6 +13,6 @@ env_logger = "0.10.2" log = "0.4.29" [dependencies.libcosmic] -path = "../../" +git = "https://github.com/pop-os/libcosmic" default-features = false features = ["applet-token"] diff --git a/examples/applet/src/window.rs b/examples/applet/src/window.rs index 22903eac..4e05c70a 100644 --- a/examples/applet/src/window.rs +++ b/examples/applet/src/window.rs @@ -1,8 +1,8 @@ use cosmic::app::{Core, Task}; -use cosmic::iced::core::window; use cosmic::iced::window::Id; use cosmic::iced::{Length, Rectangle}; +use cosmic::iced_runtime::core::window; use cosmic::surface::action::{app_popup, destroy_popup}; use cosmic::widget::{dropdown::popup_dropdown, list_column, settings, toggler}; use cosmic::Element; @@ -159,7 +159,7 @@ impl cosmic::Application for Window { "oops".into() } - fn style(&self) -> Option { + fn style(&self) -> Option { Some(cosmic::applet::style()) } } diff --git a/examples/application/Cargo.toml b/examples/application/Cargo.toml index 7a6083e0..c842c79f 100644 --- a/examples/application/Cargo.toml +++ b/examples/application/Cargo.toml @@ -11,15 +11,15 @@ wayland = ["libcosmic/wayland"] env_logger = "0.11" [dependencies.libcosmic] -path = "../../" +git = "https://github.com/pop-os/libcosmic" features = [ "debug", "winit", "tokio", "xdg-portal", "a11y", + "wgpu", "single-instance", "surface-message", "multi-window", - "wgpu", ] diff --git a/examples/application/src/main.rs b/examples/application/src/main.rs index f6e571e0..831a47f1 100644 --- a/examples/application/src/main.rs +++ b/examples/application/src/main.rs @@ -54,7 +54,7 @@ impl widget::menu::Action for Action { /// Runs application with these settings #[rustfmt::skip] fn main() -> Result<(), Box> { - + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init(); @@ -82,7 +82,6 @@ pub enum Message { Hi, Hi2, Hi3, - Tick, } /// The [`App`] stores application-specific state. @@ -93,7 +92,6 @@ pub struct App { input_2: String, hidden: bool, keybinds: HashMap, - progress: f32, } /// Implement [`cosmic::Application`] to integrate with COSMIC. @@ -135,7 +133,6 @@ impl cosmic::Application for App { input_2: String::new(), hidden: true, keybinds: HashMap::new(), - progress: 0.0, }; let command = app.update_title(); @@ -181,17 +178,10 @@ impl cosmic::Application for App { Message::Hi3 => { dbg!("hi 3"); } - Message::Tick => { - self.progress = (self.progress + 0.01) % 1.0; - } } Task::none() } - fn subscription(&self) -> iced::Subscription { - iced::time::every(std::time::Duration::from_millis(64)).map(|_| Message::Tick) - } - /// Creates a view after each update. fn view(&self) -> Element<'_, Self::Message> { let page_content = self @@ -200,7 +190,7 @@ impl cosmic::Application for App { .map_or("No page selected", String::as_str); let centered = widget::container( - widget::column::with_capacity(14) + widget::column() .push(widget::text::body(page_content)) .push( widget::text_input::text_input("", &self.input_1) @@ -222,47 +212,6 @@ impl cosmic::Application for App { .on_input(Message::Input2) .on_clear(Message::Ignore), ) - .push(widget::progress_bar::circular::Circular::new().size(50.0)) - .push(widget::progress_bar::circular::Circular::new().size(20.0)) - .push( - widget::progress_bar::linear::Linear::new() - .girth(10.0) - .width(Length::Fill), - ) - .push( - widget::progress_bar::circular::Circular::new() - .bar_height(10.0) - .size(50.0) - .progress(self.progress), - ) - .push( - widget::progress_bar::linear::Linear::new() - .girth(10.0) - .progress(self.progress) - .width(Length::Fill), - ) - .push( - widget::progress_bar::circular::Circular::new() - .size(50.0) - .progress(0.0), - ) - .push( - widget::progress_bar::linear::Linear::new() - .girth(10.0) - .progress(0.0) - .width(Length::Fill), - ) - .push( - widget::progress_bar::circular::Circular::new() - .size(50.0) - .progress(1.0), - ) - .push( - widget::progress_bar::linear::Linear::new() - .girth(10.0) - .progress(1.0) - .width(Length::Fill), - ) .spacing(cosmic::theme::spacing().space_s) .width(Length::Fill) .height(Length::Shrink) diff --git a/examples/calendar/src/main.rs b/examples/calendar/src/main.rs index 494087d1..240684c6 100644 --- a/examples/calendar/src/main.rs +++ b/examples/calendar/src/main.rs @@ -85,6 +85,8 @@ impl cosmic::Application for App { /// Creates a view after each update. fn view(&self) -> Element<'_, Self::Message> { + let mut content = cosmic::widget::column().spacing(12); + let calendar = cosmic::widget::calendar( &self.calendar_model, |date| Message::DateSelected(date), @@ -93,7 +95,9 @@ impl cosmic::Application for App { Weekday::Sunday, ); - let centered = cosmic::widget::container(calendar) + content = content.push(calendar); + + let centered = cosmic::widget::container(content) .width(iced::Length::Fill) .height(iced::Length::Shrink) .align_x(iced::Alignment::Center) diff --git a/examples/context-menu/src/main.rs b/examples/context-menu/src/main.rs index e5ca5878..db66ba1b 100644 --- a/examples/context-menu/src/main.rs +++ b/examples/context-menu/src/main.rs @@ -4,7 +4,7 @@ //! Application API example use cosmic::app::{Core, Settings, Task}; -use cosmic::iced::Size; +use cosmic::iced_core::Size; use cosmic::widget::menu; use cosmic::{executor, iced, ApplicationExt, Element}; use std::collections::HashMap; diff --git a/examples/cosmic/src/window/bluetooth.rs b/examples/cosmic/src/window/bluetooth.rs index 1b5892f6..44fe7d6c 100644 --- a/examples/cosmic/src/window/bluetooth.rs +++ b/examples/cosmic/src/window/bluetooth.rs @@ -28,14 +28,13 @@ impl State { column!( list_column().add(settings::item( "Bluetooth", - toggler(self.enabled).on_toggle(Message::Enable) + toggler(None, self.enabled, Message::Enable) )), text("Now visible as \"TODO\", just kidding") ) .spacing(8) .into(), - settings::section() - .title("Devices") + settings::view_section("Devices") .add(settings::item("No devices found", text(""))) .into(), ]) diff --git a/examples/cosmic/src/window/demo.rs b/examples/cosmic/src/window/demo.rs index 0d31fa93..9ca84ef7 100644 --- a/examples/cosmic/src/window/demo.rs +++ b/examples/cosmic/src/window/demo.rs @@ -258,13 +258,12 @@ impl State { match self.tab_bar.active_data() { None => panic!("no tab is active"), Some(DemoView::TabA) => settings::view_column(vec![ - settings::section() - .title("Debug") + settings::view_section("Debug") .add(settings::item("Debug theme", choose_theme)) .add(settings::item("Debug icon theme", choose_icon_theme)) .add(settings::item( "Debug layout", - toggler(window.debug).on_toggle(Message::Debug), + toggler(None, window.debug, Message::Debug), )) .add(settings::item( "Scaling Factor", @@ -277,11 +276,10 @@ impl State { .into(), ])) .into(), - settings::section() - .title("Controls") + settings::view_section("Controls") .add(settings::item( "Toggler", - toggler(self.toggler_value).on_toggle(Message::TogglerToggled), + toggler(None, self.toggler_value, Message::TogglerToggled), )) .add(settings::item( "Pick List (TODO)", @@ -301,13 +299,15 @@ impl State { .add(settings::item( "Progress", progress_bar(0.0..=100.0, self.slider_value) - .length(Length::Fixed(250.0)) - .girth(Length::Fixed(4.0)), + .width(Length::Fixed(250.0)) + .height(Length::Fixed(4.0)), )) - .add(settings::item_row(vec![checkbox(self.checkbox_value) - .label("Checkbox") - .on_toggle(Message::CheckboxToggled) - .into()])) + .add(settings::item_row(vec![checkbox( + "Checkbox", + self.checkbox_value, + Message::CheckboxToggled, + ) + .into()])) .add(settings::item( format!( "Spin Button (Range {}:{})", @@ -354,7 +354,8 @@ impl State { .width(Length::Shrink) .on_activate(Message::MultiSelection) .apply(container) - .center_x(Length::Fill) + .center_x() + .width(Length::Fill) .into(), text("Vertical With Spacing").into(), cosmic::iced::widget::row(vec![ @@ -423,12 +424,13 @@ impl State { ]) .padding(0) .into(), - Some(DemoView::TabC) => settings::view_column(vec![settings::section() - .title("Tab C") - .add(text("Nothing here yet").width(Length::Fill)) - .into()]) - .padding(0) - .into(), + Some(DemoView::TabC) => { + settings::view_column(vec![settings::view_section("Tab C") + .add(text("Nothing here yet").width(Length::Fill)) + .into()]) + .padding(0) + .into() + } }, container(text("Background container with some text").size(24)) .layer(cosmic_theme::Layer::Background) diff --git a/examples/cosmic/src/window/desktop.rs b/examples/cosmic/src/window/desktop.rs index 46a4e5b8..4fa726d8 100644 --- a/examples/cosmic/src/window/desktop.rs +++ b/examples/cosmic/src/window/desktop.rs @@ -147,8 +147,7 @@ impl State { fn view_desktop_options<'a>(&'a self, window: &'a Window) -> Element<'a, Message> { settings::view_column(vec![ window.parent_page_button(DesktopPage::DesktopOptions), - settings::section() - .title("Super Key Action") + settings::view_section("Super Key Action") .add(settings::item("Launcher", horizontal_space(Length::Fill))) .add(settings::item("Workspaces", horizontal_space(Length::Fill))) .add(settings::item( @@ -156,34 +155,38 @@ impl State { horizontal_space(Length::Fill), )) .into(), - settings::section() - .title("Hot Corner") + settings::view_section("Hot Corner") .add(settings::item( "Enable top-left hot corner for Workspaces", - toggler(self.top_left_hot_corner).on_toggle(Message::TopLeftHotCorner), + toggler(None, self.top_left_hot_corner, Message::TopLeftHotCorner), )) .into(), - settings::section() - .title("Top Panel") + settings::view_section("Top Panel") .add(settings::item( "Show Workspaces Button", - toggler(self.show_workspaces_button).on_toggle(Message::ShowWorkspacesButton), + toggler( + None, + self.show_workspaces_button, + Message::ShowWorkspacesButton, + ), )) .add(settings::item( "Show Applications Button", - toggler(self.show_applications_button) - .on_toggle(Message::ShowApplicationsButton), + toggler( + None, + self.show_applications_button, + Message::ShowApplicationsButton, + ), )) .into(), - settings::section() - .title("Window Controls") + settings::view_section("Window Controls") .add(settings::item( "Show Minimize Button", - toggler(self.show_minimize_button).on_toggle(Message::ShowMinimizeButton), + toggler(None, self.show_minimize_button, Message::ShowMinimizeButton), )) .add(settings::item( "Show Maximize Button", - toggler(self.show_maximize_button).on_toggle(Message::ShowMaximizeButton), + toggler(None, self.show_maximize_button, Message::ShowMaximizeButton), )) .into(), ]) @@ -242,12 +245,12 @@ impl State { list_column() .add(settings::item( "Same background on all displays", - toggler(self.same_background).on_toggle(Message::SameBackground), + toggler(None, self.same_background, Message::SameBackground), )) .add(settings::item("Background fit", text("TODO"))) .add(settings::item( "Slideshow", - toggler(self.slideshow).on_toggle(Message::Slideshow), + toggler(None, self.slideshow, Message::Slideshow), )) .into(), column(image_column).spacing(16).into(), @@ -258,8 +261,7 @@ impl State { fn view_desktop_workspaces<'a>(&'a self, window: &'a Window) -> Element<'a, Message> { settings::view_column(vec![ window.parent_page_button(DesktopPage::Wallpaper), - settings::section() - .title("Workspace Behavior") + settings::view_section("Workspace Behavior") .add(settings::item( "Dynamic workspaces", horizontal_space(Length::Fill), @@ -269,8 +271,7 @@ impl State { horizontal_space(Length::Fill), )) .into(), - settings::section() - .title("Multi-monitor Behavior") + settings::view_section("Multi-monitor Behavior") .add(settings::item( "Workspaces Span Displays", horizontal_space(Length::Fill), diff --git a/examples/cosmic/src/window/system_and_accounts.rs b/examples/cosmic/src/window/system_and_accounts.rs index ed1bd004..e42e643c 100644 --- a/examples/cosmic/src/window/system_and_accounts.rs +++ b/examples/cosmic/src/window/system_and_accounts.rs @@ -69,16 +69,14 @@ impl State { list_column() .add(settings::item("Device name", text("TODO"))) .into(), - settings::section() - .title("Hardware") + settings::view_section("Hardware") .add(settings::item("Hardware model", text("TODO"))) .add(settings::item("Memory", text("TODO"))) .add(settings::item("Processor", text("TODO"))) .add(settings::item("Graphics", text("TODO"))) .add(settings::item("Disk Capacity", text("TODO"))) .into(), - settings::section() - .title("Operating System") + settings::view_section("Operating System") .add(settings::item("Operating system", text("TODO"))) .add(settings::item( "Operating system architecture", @@ -87,8 +85,7 @@ impl State { .add(settings::item("Desktop environment", text("TODO"))) .add(settings::item("Windowing system", text("TODO"))) .into(), - settings::section() - .title("Related settings") + settings::view_section("Related settings") .add(settings::item("Get support", text("TODO"))) .into(), ]) diff --git a/examples/image-button/src/main.rs b/examples/image-button/src/main.rs index c68c7070..0ac906ca 100644 --- a/examples/image-button/src/main.rs +++ b/examples/image-button/src/main.rs @@ -80,7 +80,7 @@ impl cosmic::Application for App { /// Creates a view after each update. fn view(&self) -> Element<'_, Self::Message> { - let mut content = cosmic::widget::column::with_capacity(self.images.len()).spacing(12); + let mut content = cosmic::widget::column().spacing(12); for (id, image) in self.images.iter().enumerate() { content = content.push( diff --git a/examples/menu/src/main.rs b/examples/menu/src/main.rs index da0c3231..8b5a1cb7 100644 --- a/examples/menu/src/main.rs +++ b/examples/menu/src/main.rs @@ -7,10 +7,10 @@ use std::collections::HashMap; use std::{env, process}; use cosmic::app::{Core, Settings, Task}; -use cosmic::iced::alignment::{Horizontal, Vertical}; -use cosmic::iced::keyboard::Key; use cosmic::iced::window; -use cosmic::iced::{Length, Size}; +use cosmic::iced_core::alignment::{Horizontal, Vertical}; +use cosmic::iced_core::keyboard::Key; +use cosmic::iced_core::{Length, Size}; use cosmic::widget::menu::action::MenuAction; use cosmic::widget::menu::key_bind::KeyBind; use cosmic::widget::menu::key_bind::Modifier; diff --git a/examples/multi-window/src/window.rs b/examples/multi-window/src/window.rs index 754a0d86..74ab5386 100644 --- a/examples/multi-window/src/window.rs +++ b/examples/multi-window/src/window.rs @@ -2,9 +2,9 @@ use std::collections::HashMap; use cosmic::{ app::Core, - iced::core::{id, Alignment, Length, Point}, - iced::widget::{column, container, scrollable, text}, iced::{self, event, window, Subscription}, + iced_core::{id, Alignment, Length, Point}, + iced_widget::{column, container, scrollable, text}, prelude::*, widget::{button, header_bar}, }; diff --git a/examples/nav-context/src/main.rs b/examples/nav-context/src/main.rs index 1992066f..fdfb90f9 100644 --- a/examples/nav-context/src/main.rs +++ b/examples/nav-context/src/main.rs @@ -6,7 +6,7 @@ use std::collections::HashMap; use cosmic::app::{Core, Settings, Task}; -use cosmic::iced::Size; +use cosmic::iced_core::Size; use cosmic::widget::{menu, nav_bar}; use cosmic::{executor, iced, ApplicationExt, Element}; diff --git a/examples/open-dialog/src/main.rs b/examples/open-dialog/src/main.rs index b4b5343f..29061534 100644 --- a/examples/open-dialog/src/main.rs +++ b/examples/open-dialog/src/main.rs @@ -6,7 +6,7 @@ use apply::Apply; use cosmic::app::{Core, Settings, Task}; use cosmic::dialog::file_chooser::{self, FileFilter}; -use cosmic::iced::Length; +use cosmic::iced_core::Length; use cosmic::widget::button; use cosmic::{executor, iced, ApplicationExt, Element}; use std::sync::Arc; diff --git a/examples/subscriptions/src/main.rs b/examples/subscriptions/src/main.rs index 17e630aa..47bd3772 100644 --- a/examples/subscriptions/src/main.rs +++ b/examples/subscriptions/src/main.rs @@ -64,7 +64,7 @@ impl cosmic::Application for App { /// Creates a view after each update. fn view(&self) -> Element<'_, Self::Message> { - widget::Row::new().into() + widget::row().into() } } diff --git a/examples/table-view/src/main.rs b/examples/table-view/src/main.rs index d2478429..bbd9cf5b 100644 --- a/examples/table-view/src/main.rs +++ b/examples/table-view/src/main.rs @@ -7,7 +7,7 @@ use std::collections::HashMap; use chrono::Datelike; use cosmic::app::{Core, Settings, Task}; -use cosmic::iced::Size; +use cosmic::iced_core::Size; use cosmic::prelude::*; use cosmic::widget::table; use cosmic::widget::{self, nav_bar}; diff --git a/examples/text-input/src/main.rs b/examples/text-input/src/main.rs index c17fcd5c..ea99666c 100644 --- a/examples/text-input/src/main.rs +++ b/examples/text-input/src/main.rs @@ -99,9 +99,7 @@ impl cosmic::Application for App { let inline = cosmic::widget::inline_input("", &self.input).on_input(Message::Input); - let column = cosmic::widget::column::with_capacity(2) - .push(editable) - .push(inline); + let column = cosmic::widget::column().push(editable).push(inline); let centered = cosmic::widget::container(column.width(200)) .width(iced::Length::Fill) diff --git a/i18n/de/libcosmic.ftl b/i18n/de/libcosmic.ftl index 2d3704a6..238000f5 100644 --- a/i18n/de/libcosmic.ftl +++ b/i18n/de/libcosmic.ftl @@ -6,7 +6,7 @@ links = Links developers = Entwickler(innen) designers = Designer(innen) artists = Künstler(innen) -translators = Übersetzer(innen) +translators = Übersetzer*innen documenters = Dokumentierer(innen) # Calendar january = Januar { $year } diff --git a/i18n/eu/libcosmic.ftl b/i18n/eu/libcosmic.ftl deleted file mode 100644 index e69de29b..00000000 diff --git a/i18n/kab/libcosmic.ftl b/i18n/kab/libcosmic.ftl index 6eac2bc7..e69de29b 100644 --- a/i18n/kab/libcosmic.ftl +++ b/i18n/kab/libcosmic.ftl @@ -1,33 +0,0 @@ -close = Mdel -license = Turagt -links = Iseɣwan -developers = Ineflayen -artists = Inaẓuren -translators = Imsuqlen -january = Yennayer { $year } -february = Fuṛar { $year } -march = Meɣres { $year } -april = Yebrir { $year } -may = Mayyu { $year } -june = Yunyu { $year } -july = Yulyu { $year } -august = Ɣuct { $year } -september = Ctembeṛ { $year } -october = Tubeṛ { $year } -november = Wambeṛ { $year } -december = Dujembeṛ { $year } -documenters = Imeskaren -monday = Arim -mon = Ari -tuesday = Aram -tue = Ara -wednesday = Ahad -wed = Aha -thursday = Amhad -thu = Amh -friday = Sem -fri = Sm -saturday = Sed -sat = Sd -sunday = Acer -sun = Ace diff --git a/i18n/ko/libcosmic.ftl b/i18n/ko/libcosmic.ftl index 6cc0adbc..8d499756 100644 --- a/i18n/ko/libcosmic.ftl +++ b/i18n/ko/libcosmic.ftl @@ -2,33 +2,26 @@ february = { $year }년 2월 close = 닫기 documenters = 문서 작성자 november = { $year }년 11월 -friday = 금요일 -tuesday = 화요일 +friday = 금 +tuesday = 화 may = { $year }년 5월 -wednesday = 수요일 +wednesday = 수 april = { $year }년 4월 -monday = 월요일 +monday = 월 translators = 번역가 artists = 아티스트 license = 라이선스 december = { $year }년 12월 -sunday = 일요일 +sunday = 일 links = 링크 march = { $year }년 3월 june = { $year }년 6월 -saturday = 토요일 +saturday = 토 august = { $year }년 8월 developers = 개발자 july = { $year }년 7월 -thursday = 목요일 +thursday = 목 september = { $year }년 9월 designers = 디자이너 october = { $year }년 10월 january = { $year }년 1월 -mon = 월 -tue = 화 -wed = 수 -thu = 목 -fri = 금 -sat = 토 -sun = 일 diff --git a/i18n/zh-Hant/libcosmic.ftl b/i18n/zh-Hant/libcosmic.ftl index 8c9b201c..e69de29b 100644 --- a/i18n/zh-Hant/libcosmic.ftl +++ b/i18n/zh-Hant/libcosmic.ftl @@ -1,34 +0,0 @@ -close = 關閉 -developers = 開發人員 -designers = 設計人員 -artists = 美編設計 -translators = 翻譯人員 -documenters = 文件編輯人員 -january = { $year } 年 1 月 -monday = 星期一 -tuesday = 星期二 -wednesday = 星期三 -thursday = 星期四 -friday = 星期五 -saturday = 星期六 -sunday = 星期日 -mon = 週一 -tue = 週二 -wed = 週三 -thu = 週四 -fri = 週五 -sat = 週六 -sun = 週日 -license = 授權 -links = 連結 -february = { $year } 年 2 月 -march = { $year } 年 3 月 -april = { $year } 年 4 月 -may = { $year } 年 5 月 -june = { $year } 年 6 月 -july = { $year } 年 7 月 -august = { $year } 年 8 月 -september = { $year } 年 9 月 -october = { $year } 年 10 月 -november = { $year } 年 11 月 -december = { $year } 年 12 月 diff --git a/iced b/iced index 78caabba..4020ad70 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 78caabba7ef91cd1030da6f70b41d266704ffece +Subproject commit 4020ad70b64432ccbb694ce321a49a9193a6053d diff --git a/src/app/action.rs b/src/app/action.rs index fb982acb..05fc7cbe 100644 --- a/src/app/action.rs +++ b/src/app/action.rs @@ -5,7 +5,7 @@ use crate::surface; use crate::theme::Theme; use crate::widget::nav_bar; use crate::{config::CosmicTk, keyboard_nav}; -#[cfg(all(feature = "wayland", target_os = "linux"))] +#[cfg(feature = "wayland")] use cctk::sctk::reexports::csd_frame::{WindowManagerCapabilities, WindowState}; use cosmic_theme::ThemeMode; @@ -69,10 +69,10 @@ pub enum Action { /// Updates the tracked window geometry. WindowResize(iced::window::Id, f32, f32), /// Tracks updates to window state. - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] WindowState(iced::window::Id, WindowState), /// Capabilities the window manager supports - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] WmCapabilities(iced::window::Id, WindowManagerCapabilities), #[cfg(feature = "xdg-portal")] DesktopSettings(crate::theme::portal::Desktop), diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 030ed041..edd7b157 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -8,16 +8,16 @@ use std::sync::Arc; use super::{Action, Application, ApplicationExt, Subscription}; use crate::theme::{THEME, Theme, ThemeType}; use crate::{Core, Element, keyboard_nav}; -#[cfg(all(feature = "wayland", target_os = "linux"))] +#[cfg(feature = "wayland")] use cctk::sctk::reexports::csd_frame::{WindowManagerCapabilities, WindowState}; use cosmic_theme::ThemeMode; -#[cfg(not(any(feature = "multi-window", feature = "wayland", target_os = "linux")))] +#[cfg(not(any(feature = "multi-window", feature = "wayland")))] use iced::Application as IcedApplication; -#[cfg(all(feature = "wayland", target_os = "linux"))] +#[cfg(feature = "wayland")] use iced::event::wayland; use iced::{Task, theme, window}; use iced_futures::event::listen_with; -#[cfg(all(feature = "wayland", target_os = "linux"))] +#[cfg(feature = "wayland")] use iced_winit::SurfaceIdWrapper; use palette::color_difference::EuclideanDistance; @@ -49,8 +49,8 @@ pub fn windowing_system() -> Option { WINDOWING_SYSTEM.get().copied() } -fn init_windowing_system(handle: window::raw_window_handle::WindowHandle) -> crate::Action { - let raw = handle.as_ref(); +fn init_windowing_system(handle: raw_window_handle::WindowHandle) -> crate::Action { + let raw: &raw_window_handle::RawWindowHandle = handle.as_ref(); let system = match raw { window::raw_window_handle::RawWindowHandle::UiKit(_) => WindowingSystem::UiKit, window::raw_window_handle::RawWindowHandle::AppKit(_) => WindowingSystem::AppKit, @@ -83,7 +83,7 @@ fn init_windowing_system(handle: window::raw_window_handle::WindowHandle) -> #[derive(Default)] pub struct Cosmic { pub app: App, - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] pub surface_views: HashMap< window::Id, ( @@ -138,7 +138,7 @@ where ) -> iced::Task> { #[cfg(feature = "surface-message")] match _surface_message { - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] crate::surface::Action::AppSubsurface(settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings) .ok() @@ -168,7 +168,7 @@ where iced_winit::commands::subsurface::get_subsurface(settings(&mut self.app)) } } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] crate::surface::Action::Subsurface(settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings) .ok() @@ -196,7 +196,7 @@ where iced_winit::commands::subsurface::get_subsurface(settings()) } } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] crate::surface::Action::AppPopup(settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings) .ok() @@ -225,26 +225,15 @@ where iced_winit::commands::popup::get_popup(settings(&mut self.app)) } } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] crate::surface::Action::DestroyPopup(id) => { iced_winit::commands::popup::destroy_popup(id) } - #[cfg(all(feature = "wayland", target_os = "linux"))] - crate::surface::Action::DestroyTooltipPopup => { - #[cfg(feature = "applet")] - { - iced_winit::commands::popup::destroy_popup(*crate::applet::TOOLTIP_WINDOW_ID) - } - #[cfg(not(feature = "applet"))] - { - Task::none() - } - } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] crate::surface::Action::DestroySubsurface(id) => { iced_winit::commands::subsurface::destroy_subsurface(id) } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] crate::surface::Action::DestroyWindow(id) => iced::window::close(id), crate::surface::Action::ResponsiveMenuBar { menu_bar, @@ -255,7 +244,7 @@ where core.menu_bars.insert(menu_bar, (limits, size)); iced::Task::none() } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] crate::surface::Action::Popup(settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings) .ok() @@ -282,7 +271,7 @@ where iced_winit::commands::popup::get_popup(settings()) } } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] crate::surface::Action::AppWindow(id, settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings).ok().and_then(|s| { s.downcast:: iced::window::Settings + Send + Sync>>() @@ -321,7 +310,7 @@ where .discard() } } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] crate::surface::Action::Window(id, settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings).ok().and_then(|s| { s.downcast:: iced::window::Settings + Send + Sync>>() @@ -441,7 +430,7 @@ where } iced::Event::Window(window::Event::Focused) => return Some(Action::Focus(id)), iced::Event::Window(window::Event::Unfocused) => return Some(Action::Unfocus(id)), - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] iced::Event::PlatformSpecific(iced::event::PlatformSpecific::Wayland(event)) => { match event { wayland::Event::Popup(wayland::PopupEvent::Done, _, id) @@ -454,7 +443,7 @@ where ) => { return Some(Action::SuggestedBounds(b)); } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] wayland::Event::Window(iced::event::wayland::WindowEvent::WindowState( s, )) => { @@ -571,7 +560,7 @@ where #[cfg(feature = "multi-window")] pub fn view(&self, id: window::Id) -> Element<'_, crate::Action> { - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] if let Some((_, _, v)) = self.surface_views.get(&id) { return v(&self.app); } @@ -622,7 +611,7 @@ impl Cosmic { fn cosmic_update(&mut self, message: Action) -> iced::Task> { match message { Action::WindowMaximized(id, maximized) => { - #[cfg(not(all(feature = "wayland", target_os = "linux")))] + #[cfg(not(feature = "wayland"))] if self .app .core() @@ -652,7 +641,7 @@ impl Cosmic { }); } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] Action::WindowState(id, state) => { if self .app @@ -704,7 +693,7 @@ impl Cosmic { } } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] Action::WmCapabilities(id, capabilities) => { if self .app @@ -811,7 +800,7 @@ impl Cosmic { new_theme.theme_type.prefer_dark(prefer_dark); cosmic_theme.set_theme(new_theme.theme_type); - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] if self.app.core().sync_window_border_radii_to_theme() { use iced_runtime::platform_specific::wayland::CornerRadius; use iced_winit::platform_specific::commands::corner_radius::corner_radius; @@ -957,7 +946,7 @@ impl Cosmic { // Only apply update if the theme is set to load a system theme if let ThemeType::System { .. } = cosmic_theme.theme_type { cosmic_theme.set_theme(new_theme.theme_type); - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] if self.app.core().sync_window_border_radii_to_theme() { use iced_runtime::platform_specific::wayland::CornerRadius; use iced_winit::platform_specific::commands::corner_radius::corner_radius; @@ -1051,7 +1040,7 @@ impl Cosmic { // Unminimize window before requesting to activate it. let mut task = iced_runtime::window::minimize(id, false); - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] { task = task.chain( iced_winit::platform_specific::commands::activation::activate( @@ -1062,7 +1051,7 @@ impl Cosmic { ) } - #[cfg(not(all(feature = "wayland", target_os = "linux")))] + #[cfg(not(feature = "wayland"))] { task = task.chain(iced_runtime::window::gain_focus(id)); } @@ -1079,7 +1068,7 @@ impl Cosmic { *v == 0 }) { self.opened_surfaces.remove(&id); - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] self.surface_views.remove(&id); self.tracked_windows.remove(&id); } @@ -1201,8 +1190,7 @@ impl Cosmic { #[cfg(all( feature = "wayland", feature = "multi-window", - feature = "surface-message", - target_os = "linux" + feature = "surface-message" ))] if let Some(( parent, @@ -1247,7 +1235,7 @@ impl Cosmic { core.applet.suggested_bounds = b; } Action::Opened(id) => { - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] if self.app.core().sync_window_border_radii_to_theme() { use iced_runtime::platform_specific::wayland::CornerRadius; use iced_winit::platform_specific::commands::corner_radius::corner_radius; @@ -1296,14 +1284,14 @@ impl Cosmic { pub fn new(app: App) -> Self { Self { app, - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] surface_views: HashMap::new(), tracked_windows: HashSet::new(), opened_surfaces: HashMap::new(), } } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] /// Create a subsurface pub fn get_subsurface( &mut self, @@ -1326,7 +1314,7 @@ impl Cosmic { get_subsurface(settings) } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] /// Create a subsurface pub fn get_popup( &mut self, @@ -1348,7 +1336,7 @@ impl Cosmic { get_popup(settings) } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] /// Create a window surface pub fn get_window( &mut self, diff --git a/src/app/mod.rs b/src/app/mod.rs index f78beac7..b36ec4f6 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -128,9 +128,6 @@ impl BootFn, crate::Action(settings: Settings, flags: App::Flags) -> iced::Result { - #[cfg(feature = "desktop")] - image_extras::register(); - #[cfg(all(target_env = "gnu", not(target_os = "windows")))] if let Some(threshold) = settings.default_mmap_threshold { crate::malloc::limit_mmap_threshold(threshold); @@ -197,9 +194,6 @@ where App::Flags: CosmicFlags, App::Message: Clone + std::fmt::Debug + Send + 'static, { - #[cfg(feature = "desktop")] - image_extras::register(); - use std::collections::HashMap; let activation_token = std::env::var("XDG_ACTIVATION_TOKEN").ok(); @@ -748,6 +742,7 @@ impl ApplicationExt for App { })); let content: Element<_> = if content_container { content_col + .apply(container) .width(iced::Length::Fill) .height(iced::Length::Fill) .apply(|w| id_container(w, iced_core::id::Id::new("COSMIC_content_container"))) @@ -777,7 +772,8 @@ impl ApplicationExt for App { .title(&core.window.header_title) .on_drag(crate::Action::Cosmic(Action::Drag)) .on_right_click(crate::Action::Cosmic(Action::ShowWindowMenu)) - .on_double_click(crate::Action::Cosmic(Action::Maximize)); + .on_double_click(crate::Action::Cosmic(Action::Maximize)) + .is_condensed(is_condensed); if self.nav_model().is_some() { let toggle = crate::widget::nav_bar_toggle() diff --git a/src/app/settings.rs b/src/app/settings.rs index 5c903f09..926181e1 100644 --- a/src/app/settings.rs +++ b/src/app/settings.rs @@ -16,7 +16,7 @@ pub struct Settings { pub(crate) antialiasing: bool, /// Autosize the window to fit its contents - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] pub(crate) autosize: bool, /// Set the application to not create a main window @@ -80,7 +80,7 @@ impl Default for Settings { fn default() -> Self { Self { antialiasing: true, - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] autosize: false, no_main_window: false, client_decorations: true, diff --git a/src/applet/mod.rs b/src/applet/mod.rs index 48721e1c..a3f5228b 100644 --- a/src/applet/mod.rs +++ b/src/applet/mod.rs @@ -6,6 +6,13 @@ use crate::{ Application, Element, Renderer, app::iced_settings, cctk::sctk, + iced::{ + self, Color, Length, Limits, Rectangle, + alignment::{Alignment, Horizontal, Vertical}, + widget::Container, + window, + }, + iced_widget, theme::{self, Button, THEME, system_dark, system_light}, widget::{ self, @@ -17,15 +24,8 @@ use crate::{ space::vertical, }, }; - pub use cosmic_panel_config; use cosmic_panel_config::{CosmicPanelBackground, PanelAnchor, PanelSize}; -use iced::{ - self, Color, Length, Limits, Rectangle, - alignment::{Alignment, Horizontal, Vertical}, - widget::Container, - window, -}; use iced_core::{Padding, Shadow}; use iced_runtime::platform_specific::wayland::popup::{SctkPopupSettings, SctkPositioner}; use iced_widget::Text; @@ -42,7 +42,7 @@ static AUTOSIZE_ID: LazyLock = static AUTOSIZE_MAIN_ID: LazyLock = LazyLock::new(|| iced::id::Id::new("cosmic-applet-autosize-main")); static TOOLTIP_ID: LazyLock = LazyLock::new(|| iced::id::Id::new("subsurface")); -pub(crate) static TOOLTIP_WINDOW_ID: LazyLock = LazyLock::new(window::Id::unique); +static TOOLTIP_WINDOW_ID: LazyLock = LazyLock::new(window::Id::unique); #[derive(Debug, Clone)] pub struct Context { @@ -226,7 +226,7 @@ impl Context { let symbolic = icon.symbolic; let icon = widget::icon(icon) .class(if symbolic { - theme::Svg::Custom(Rc::new(|theme| iced_widget::svg::Style { + theme::Svg::Custom(Rc::new(|theme| crate::iced_widget::svg::Style { color: Some(theme.cosmic().background.on.into()), })) } else { diff --git a/src/applet/token/subscription.rs b/src/applet/token/subscription.rs index 07c528ea..82763303 100644 --- a/src/applet/token/subscription.rs +++ b/src/applet/token/subscription.rs @@ -1,11 +1,11 @@ use crate::iced; +use crate::iced_futures::futures; use cctk::sctk::reexports::calloop; use futures::{ SinkExt, StreamExt, channel::mpsc::{UnboundedReceiver, unbounded}, }; use iced::Subscription; -use iced_futures::futures; use iced_futures::stream; use std::{fmt::Debug, hash::Hash, thread::JoinHandle}; diff --git a/src/core.rs b/src/core.rs index 970a5351..4d50e764 100644 --- a/src/core.rs +++ b/src/core.rs @@ -99,7 +99,7 @@ pub struct Core { pub(crate) menu_bars: HashMap, - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] pub(crate) sync_window_border_radii_to_theme: bool, } @@ -159,7 +159,7 @@ impl Default for Core { main_window: None, exit_on_main_window_closed: true, menu_bars: HashMap::new(), - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] sync_window_border_radii_to_theme: true, } } @@ -493,12 +493,12 @@ impl Core { } // TODO should we emit tasks setting the corner radius or unsetting it if this is changed? - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] pub fn set_sync_window_border_radii_to_theme(&mut self, sync: bool) { self.sync_window_border_radii_to_theme = sync; } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] pub fn sync_window_border_radii_to_theme(&self) -> bool { self.sync_window_border_radii_to_theme } diff --git a/src/desktop.rs b/src/desktop.rs index 98ce7d4b..fe32f286 100644 --- a/src/desktop.rs +++ b/src/desktop.rs @@ -789,7 +789,7 @@ pub async fn spawn_desktop_exec( }) .unwrap_or_else(|| String::from("cosmic-term")); - term_exec = format!("{term} -e {}", exec.as_ref()); + term_exec = format!("{term} -- {}", exec.as_ref()); &term_exec } else { exec.as_ref() diff --git a/src/ext.rs b/src/ext.rs index 8eb749e5..c85e6e86 100644 --- a/src/ext.rs +++ b/src/ext.rs @@ -19,6 +19,72 @@ impl ElementExt for crate::Element<'_, Message> { } } +/// Additional methods for the [`Column`] and [`Row`] widgets. +pub trait CollectionWidget<'a, Message: 'a>: + Widget +where + Self: Sized, +{ + /// Moves all the elements of `other` into `self`, leaving `other` empty. + #[must_use] + fn append(self, other: &mut Vec) -> Self + where + E: Into>; + + /// Appends all elements in an iterator to the widget. + #[must_use] + fn extend(mut self, iterator: impl Iterator) -> Self + where + E: Into>, + { + for item in iterator { + self = self.push(item.into()); + } + + self + } + + /// Pushes an element into the widget. + #[must_use] + fn push(self, element: impl Into>) -> Self; + + /// Conditionally pushes an element to the widget. + #[must_use] + fn push_maybe(self, element: Option>>) -> Self { + if let Some(element) = element { + self.push(element.into()) + } else { + self + } + } +} + +impl<'a, Message: 'a> CollectionWidget<'a, Message> for crate::widget::Column<'a, Message> { + fn append(self, other: &mut Vec) -> Self + where + E: Into>, + { + self.extend(other.drain(..).map(Into::into)) + } + + fn push(self, element: impl Into>) -> Self { + self.push(element) + } +} + +impl<'a, Message: 'a> CollectionWidget<'a, Message> for crate::widget::Row<'a, Message> { + fn append(self, other: &mut Vec) -> Self + where + E: Into>, + { + self.extend(other.drain(..).map(Into::into)) + } + + fn push(self, element: impl Into>) -> Self { + self.push(element) + } +} + pub trait ColorExt { /// Combines color with background to create appearance of transparency. #[must_use] diff --git a/src/lib.rs b/src/lib.rs index 02623799..1a579f96 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,7 +3,6 @@ #![allow(clippy::module_name_repetitions)] #![cfg_attr(target_os = "redox", feature(lazy_cell))] -#![cfg_attr(docsrs, feature(doc_auto_cfg))] /// Recommended default imports. pub mod prelude { @@ -67,6 +66,29 @@ 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_renderer; + +#[doc(inline)] +pub use iced_runtime; + +#[doc(inline)] +pub use iced_widget; + +#[doc(inline)] +#[cfg(feature = "winit")] +pub use iced_winit; + +#[doc(inline)] +#[cfg(feature = "wgpu")] +pub use iced_wgpu; + pub mod icon_theme; pub mod keyboard_nav; @@ -78,8 +100,7 @@ pub(crate) mod malloc; #[cfg(all(feature = "process", not(windows)))] pub mod process; -#[doc(inline)] -#[cfg(all(feature = "wayland", target_os = "linux"))] +#[cfg(feature = "wayland")] pub use cctk; pub mod surface; diff --git a/src/surface/action.rs b/src/surface/action.rs index 50e2b4a9..3a078ca3 100644 --- a/src/surface/action.rs +++ b/src/surface/action.rs @@ -9,25 +9,25 @@ use iced::window; use std::{any::Any, sync::Arc}; /// Used to produce a destroy popup message from within a widget. -#[cfg(all(feature = "wayland", target_os = "linux"))] +#[cfg(feature = "wayland")] #[must_use] pub fn destroy_popup(id: iced_core::window::Id) -> Action { Action::DestroyPopup(id) } -#[cfg(all(feature = "wayland", target_os = "linux"))] +#[cfg(feature = "wayland")] #[must_use] pub fn destroy_subsurface(id: iced_core::window::Id) -> Action { Action::DestroySubsurface(id) } -#[cfg(all(feature = "wayland", target_os = "linux"))] +#[cfg(feature = "wayland")] #[must_use] 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", feature = "winit"))] #[must_use] pub fn app_window( settings: impl Fn(&mut App) -> window::Settings + Send + Sync + 'static, @@ -60,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", feature = "winit"))] #[must_use] pub fn simple_window( settings: impl Fn() -> window::Settings + Send + Sync + 'static, @@ -92,7 +92,7 @@ pub fn simple_window( ) } -#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] +#[cfg(all(feature = "wayland", feature = "winit"))] #[must_use] pub fn app_popup( settings: impl Fn(&mut App) -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings @@ -126,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", feature = "winit"))] #[must_use] pub fn simple_subsurface( settings: impl Fn() -> iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings @@ -155,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", feature = "winit"))] #[must_use] pub fn simple_popup( settings: impl Fn() -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings @@ -186,7 +186,7 @@ pub fn simple_popup( ) } -#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] +#[cfg(all(feature = "wayland", feature = "winit"))] #[must_use] pub fn subsurface( settings: impl Fn( diff --git a/src/surface/mod.rs b/src/surface/mod.rs index 0dad6459..4598ac7c 100644 --- a/src/surface/mod.rs +++ b/src/surface/mod.rs @@ -36,8 +36,6 @@ pub enum Action { ), /// Destroy a subsurface with a view function DestroyPopup(iced::window::Id), - /// Destroys the global tooltip popup subsurface - DestroyTooltipPopup, /// Create a window with a view function accepting the App as a parameter AppWindow( @@ -87,7 +85,6 @@ impl std::fmt::Debug for Action { } Self::Popup(arg0, arg1) => f.debug_tuple("Popup").field(arg0).field(arg1).finish(), Self::DestroyPopup(arg0) => f.debug_tuple("DestroyPopup").field(arg0).finish(), - Self::DestroyTooltipPopup => f.debug_tuple("DestroyTooltipPopup").finish(), Self::ResponsiveMenuBar { menu_bar, limits, diff --git a/src/theme/mod.rs b/src/theme/mod.rs index 093bac05..b7e85237 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -307,7 +307,7 @@ impl DefaultStyle for Theme { fn default_style(&self) -> Appearance { let cosmic = self.cosmic(); Appearance { - icon_color: cosmic.on_bg_color().into(), + icon_color: cosmic.bg_color().into(), background_color: cosmic.bg_color().into(), text_color: cosmic.on_bg_color().into(), } diff --git a/src/theme/style/button.rs b/src/theme/style/button.rs index bb52d9a6..0575ce67 100644 --- a/src/theme/style/button.rs +++ b/src/theme/style/button.rs @@ -27,7 +27,7 @@ pub enum Button { IconVertical, Image, Link, - ListItem([f32; 4]), + ListItem, MenuFolder, MenuItem, MenuRoot, @@ -148,8 +148,8 @@ pub fn appearance( appearance.text_color = Some(component.on.into()); corner_radii = &cosmic.corner_radii.radius_s; } - Button::ListItem(radii) => { - corner_radii = radii; + Button::ListItem => { + corner_radii = &[0.0; 4]; let (background, text, icon) = color(&cosmic.background.component); if selected { @@ -197,7 +197,7 @@ impl Catalog for crate::Theme { return active(focused, self); } - let mut s = appearance(self, focused, selected, false, style, move |component| { + appearance(self, focused, selected, false, style, move |component| { let text_color = if matches!( style, Button::Icon | Button::IconVertical | Button::HeaderBar @@ -209,15 +209,7 @@ impl Catalog for crate::Theme { }; (component.base.into(), text_color, text_color) - }); - - if let Button::ListItem(_) = style { - if !selected { - s.background = None; - } - } - - s + }) } fn disabled(&self, style: &Self::Class) -> Style { @@ -245,7 +237,7 @@ impl Catalog for crate::Theme { return hovered(focused, self); } - let mut s = appearance( + appearance( self, focused || matches!(style, Button::Image), selected, @@ -264,15 +256,7 @@ impl Catalog for crate::Theme { (component.hover.into(), text_color, text_color) }, - ); - - if let Button::ListItem(_) = style { - if !selected { - s.background = None; - } - } - - s + ) } fn pressed(&self, focused: bool, selected: bool, style: &Self::Class) -> Style { diff --git a/src/theme/style/iced.rs b/src/theme/style/iced.rs index aa6f4b33..4633477d 100644 --- a/src/theme/style/iced.rs +++ b/src/theme/style/iced.rs @@ -43,7 +43,7 @@ pub mod application { iced::theme::Style { background_color: cosmic.bg_color().into(), text_color: cosmic.on_bg_color().into(), - icon_color: cosmic.on_bg_color().into(), + icon_color: cosmic.bg_color().into(), } } } diff --git a/src/theme/style/mod.rs b/src/theme/style/mod.rs index bc648a73..a187374c 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", feature = "winit"))] pub mod tooltip; -#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] +#[cfg(all(feature = "wayland", feature = "winit"))] pub use tooltip::Tooltip; diff --git a/src/widget/about.rs b/src/widget/about.rs index 9b21e93a..ba88e03a 100644 --- a/src/widget/about.rs +++ b/src/widget/about.rs @@ -1,9 +1,8 @@ use crate::{ Apply, Element, fl, iced::{Alignment, Length}, - widget::{self, list}, + widget::{self, space}, }; -use std::rc::Rc; #[derive(Debug, Default, Clone, derive_setters::Setters)] #[setters(into, strip_option)] @@ -48,40 +47,32 @@ pub struct About { fn add_contributors(contributors: Vec<(&str, &str)>) -> Vec<(String, String)> { contributors .into_iter() - .map(|(name, email)| (name.into(), format!("mailto:{email}"))) + .map(|(name, email)| (name.to_string(), format!("mailto:{email}"))) .collect() } +macro_rules! set_contributors { + ($field:ident, $doc:expr) => { + #[doc = $doc] + pub fn $field(mut self, contributors: impl Into>) -> Self { + self.$field = add_contributors(contributors.into()); + self + } + }; +} + impl<'a> About { - /// Artists who contributed to the application. - pub fn artists(mut self, contributors: impl Into>) -> Self { - self.artists = add_contributors(contributors.into()); - self - } - - /// Designers who contributed to the application. - pub fn designers(mut self, contributors: impl Into>) -> Self { - self.designers = add_contributors(contributors.into()); - self - } - - /// Developers who contributed to the application. - pub fn developers(mut self, contributors: impl Into>) -> Self { - self.developers = add_contributors(contributors.into()); - self - } - - /// Documenters who contributed to the application. - pub fn documenters(mut self, contributors: impl Into>) -> Self { - self.documenters = add_contributors(contributors.into()); - self - } - - /// Translators who contributed to the application. - pub fn translators(mut self, contributors: impl Into>) -> Self { - self.translators = add_contributors(contributors.into()); - self - } + set_contributors!(artists, "Artists who contributed to the application."); + set_contributors!(designers, "Designers who contributed to the application."); + set_contributors!(developers, "Developers who contributed to the application."); + set_contributors!( + documenters, + "Documenters who contributed to the application." + ); + set_contributors!( + translators, + "Translators who contributed to the application." + ); /// Links associated with the application. pub fn links, V: Into>( @@ -105,23 +96,19 @@ pub fn about<'a, Message: Clone + 'static>( space_xxs, space_m, .. } = crate::theme::spacing(); - let svg_accent = Rc::new(|theme: &crate::Theme| widget::svg::Style { - color: Some(theme.cosmic().accent_text_color().into()), - }); - - let section_button = |name: &'a str, url: &'a str| -> list::ListButton<'a, Message> { - widget::row::with_capacity(2) - .push(widget::text::body(name).width(Length::Fill)) + let section_button = |name: &'a str, url: &'a str| -> Element<'a, Message> { + widget::row() + .push(widget::text(name)) + .push(space::horizontal()) .push_maybe( - (!url.is_empty()).then_some( - widget::icon::from_name("link-symbolic") - .icon() - .class(crate::theme::Svg::Custom(svg_accent.clone())), - ), + (!url.is_empty()).then_some(crate::widget::icon::from_name("link-symbolic").icon()), ) .align_y(Alignment::Center) - .apply(list::button) + .apply(widget::button::custom) + .class(crate::theme::Button::Link) .on_press(on_url_press(url)) + .width(Length::Fill) + .into() }; let section = |list: &'a Vec<(String, String)>, title: String| { @@ -171,7 +158,7 @@ pub fn about<'a, Message: Clone + 'static>( let copyright = about.copyright.as_ref().map(widget::text::body); let comments = about.comments.as_ref().map(widget::text::body); - widget::column::with_capacity(10) + widget::column() .push_maybe(header) .push_maybe(links_section) .push_maybe(developers_section) diff --git a/src/widget/autosize.rs b/src/widget/autosize.rs index 69fd9c83..937aabf9 100644 --- a/src/widget/autosize.rs +++ b/src/widget/autosize.rs @@ -170,7 +170,7 @@ where shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) { - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] if matches!( event, Event::PlatformSpecific(event::PlatformSpecific::Wayland( diff --git a/src/widget/button/icon.rs b/src/widget/button/icon.rs index 04d2bdd5..edb54272 100644 --- a/src/widget/button/icon.rs +++ b/src/widget/button/icon.rs @@ -3,7 +3,10 @@ use super::{Builder, ButtonClass}; use crate::Element; -use crate::widget::{icon::Handle, tooltip}; +use crate::widget::{ + icon::{self, Handle}, + tooltip, +}; use apply::Apply; use iced_core::{Alignment, Length, Padding, font::Weight, text::LineHeight, widget::Id}; use std::borrow::Cow; @@ -130,7 +133,7 @@ impl Button<'_, Message> { } impl<'a, Message: Clone + 'static> From> for Element<'a, Message> { - fn from(builder: Button<'a, Message>) -> Element<'a, Message> { + fn from(mut builder: Button<'a, Message>) -> Element<'a, Message> { let mut content = Vec::with_capacity(2); content.push( diff --git a/src/widget/button/widget.rs b/src/widget/button/widget.rs index 4acf3f2d..a4e32378 100644 --- a/src/widget/button/widget.rs +++ b/src/widget/button/widget.rs @@ -357,8 +357,6 @@ impl<'a, Message: 'a + Clone> Widget operation, ); }); - let state = tree.state.downcast_mut::(); - operation.focusable(Some(&self.id), layout.bounds(), state); } fn update( diff --git a/src/widget/calendar.rs b/src/widget/calendar.rs index 91c601d3..7c09d39c 100644 --- a/src/widget/calendar.rs +++ b/src/widget/calendar.rs @@ -4,10 +4,10 @@ //! A widget that displays an interactive calendar. use crate::fl; +use crate::iced_core::{Alignment, Length}; use crate::widget::{button, column, grid, icon, row, text}; use apply::Apply; use iced::alignment::Vertical; -use iced_core::{Alignment, Length}; use jiff::{ ToSpan, civil::{Date, Weekday}, @@ -212,7 +212,7 @@ where let content_list = column::with_children([ row::with_children([ - column([date.into(), day.into()]).into(), + column().push(date).push(day).into(), crate::widget::space::horizontal() .width(Length::Fill) .into(), diff --git a/src/widget/cards.rs b/src/widget/cards.rs index 66267a73..b8e17636 100644 --- a/src/widget/cards.rs +++ b/src/widget/cards.rs @@ -1,8 +1,13 @@ //! An expandable stack of cards use std::time::Duration; +use self::iced_core::{ + Element, Event, Length, Size, Vector, Widget, border::Radius, id::Id, layout::Node, + renderer::Quad, widget::Tree, +}; use crate::{ anim, + iced_core::{self, Border, Shadow}, widget::{ button, card::style::Style, @@ -13,10 +18,6 @@ use crate::{ }; use float_cmp::approx_eq; use iced::widget; -use iced_core::{ - Border, Element, Event, Length, Shadow, Size, Vector, Widget, border::Radius, id::Id, - layout::Node, renderer::Quad, widget::Tree, -}; use iced_core::{widget::tree, window}; const ICON_SIZE: u16 = 16; diff --git a/src/widget/color_picker/mod.rs b/src/widget/color_picker/mod.rs index 318e943b..d484bb62 100644 --- a/src/widget/color_picker/mod.rs +++ b/src/widget/color_picker/mod.rs @@ -4,6 +4,7 @@ //! Widgets for selecting colors with a color picker. use std::borrow::Cow; +use std::iter; use std::rc::Rc; use std::sync::LazyLock; use std::sync::atomic::{AtomicBool, Ordering}; @@ -92,6 +93,8 @@ pub struct ColorPickerModel { #[setters(skip)] active_color: palette::Hsv, #[setters(skip)] + save_next: Option, + #[setters(skip)] input_color: String, #[setters(skip)] applied_color: Option, @@ -125,6 +128,7 @@ impl ColorPickerModel { .insert(move |b| b.text(rgb.clone())) .build(), active_color: hsv, + save_next: None, input_color: color_to_string(hsv, true), applied_color: initial, fallback_color, @@ -155,26 +159,22 @@ impl ColorPickerModel { ) } - fn update_recent_colors(&mut self, new_color: Color) { - if let Some(pos) = self.recent_colors.iter().position(|c| *c == new_color) { - self.recent_colors.remove(pos); - } - self.recent_colors.insert(0, new_color); - self.recent_colors.truncate(MAX_RECENT); - } - pub fn update(&mut self, update: ColorPickerUpdate) -> Task { match update { ColorPickerUpdate::ActiveColor(c) => { self.must_clear_cache.store(true, Ordering::SeqCst); self.input_color = color_to_string(c, self.is_hex()); + if let Some(to_save) = self.save_next.take() { + self.recent_colors.insert(0, to_save); + self.recent_colors.truncate(MAX_RECENT); + } self.active_color = c; self.copied_at = None; } - ColorPickerUpdate::AppliedColor | ColorPickerUpdate::ActionFinished => { + ColorPickerUpdate::AppliedColor => { let srgb = palette::Srgb::from_color(self.active_color); if let Some(applied_color) = self.applied_color.take() { - self.update_recent_colors(applied_color); + self.recent_colors.push(applied_color); } self.applied_color = Some(Color::from(srgb)); self.active = false; @@ -215,12 +215,21 @@ impl ColorPickerModel { palette::Hsv::from_color(palette::Srgb::new(c.red, c.green, c.blue)); } } + ColorPickerUpdate::ActionFinished => { + let srgb = palette::Srgb::from_color(self.active_color); + if let Some(applied_color) = self.applied_color.take() { + self.recent_colors.push(applied_color); + } + self.applied_color = Some(Color::from(srgb)); + self.active = false; + self.save_next = Some(Color::from(srgb)); + } ColorPickerUpdate::ToggleColorPicker => { self.must_clear_cache.store(true, Ordering::SeqCst); self.active = !self.active; self.copied_at = None; } - } + }; Task::none() } @@ -386,8 +395,7 @@ where text_input("", self.input_color) .on_input(move |s| on_update(ColorPickerUpdate::Input(s))) .on_paste(move |s| on_update(ColorPickerUpdate::Input(s))) - .on_submit(move |_| on_update(ColorPickerUpdate::ActionFinished)) - // .on_unfocus(on_update(ColorPickerUpdate::ActionFinished)) Somehow this is called even when the field wasn't previously focused + .on_submit(move |_| on_update(ColorPickerUpdate::AppliedColor)) .leading_icon( color_button( None, diff --git a/src/widget/context_menu.rs b/src/widget/context_menu.rs index 3f35f04a..200021c3 100644 --- a/src/widget/context_menu.rs +++ b/src/widget/context_menu.rs @@ -3,12 +3,7 @@ //! A context menu is a menu in a graphical user interface that appears upon user interaction, such as a right-click mouse operation. -#[cfg(all( - feature = "wayland", - target_os = "linux", - feature = "winit", - feature = "surface-message" -))] +#[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] use crate::app::cosmic::{WINDOWING_SYSTEM, WindowingSystem}; use crate::widget::menu::{ self, CloseCondition, Direction, ItemHeight, ItemWidth, MenuBarState, PathHighlight, @@ -32,7 +27,7 @@ pub fn context_menu<'a, Message: 'static + Clone>( content: content.into(), context_menu: context_menu.map(|menus| { vec![menu::Tree::with_children( - crate::Element::from(crate::widget::Row::new()), + crate::Element::from(crate::widget::row::<'static, Message>()), menus, )] }), @@ -64,12 +59,7 @@ pub struct ContextMenu<'a, Message> { } impl ContextMenu<'_, Message> { - #[cfg(all( - feature = "wayland", - target_os = "linux", - feature = "winit", - feature = "surface-message" - ))] + #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] #[allow(clippy::too_many_lines)] fn create_popup( &mut self, @@ -374,12 +364,7 @@ impl Widget state.active_root.clear(); state.open = false; - #[cfg(all( - feature = "wayland", - target_os = "linux", - feature = "winit", - feature = "surface-message" - ))] + #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) && let Some(id) = state.popup_id.remove(&self.window_id) { @@ -418,12 +403,7 @@ impl Widget state.open = true; state.view_cursor = cursor; }); - #[cfg(all( - feature = "wayland", - target_os = "linux", - feature = "winit", - feature = "surface-message" - ))] + #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) { self.create_popup(layout, cursor, renderer, shell, viewport, state); } @@ -442,7 +422,6 @@ impl Widget #[cfg(all( feature = "wayland", - target_os = "linux", feature = "winit", feature = "surface-message" ))] @@ -479,12 +458,7 @@ impl Widget _viewport: &iced::Rectangle, translation: Vector, ) -> Option> { - #[cfg(all( - feature = "wayland", - target_os = "linux", - feature = "winit", - feature = "surface-message" - ))] + #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) && self.window_id != window::Id::NONE && self.on_surface_action.is_some() diff --git a/src/widget/dnd_destination.rs b/src/widget/dnd_destination.rs index 10bf7a8b..a77101b9 100644 --- a/src/widget/dnd_destination.rs +++ b/src/widget/dnd_destination.rs @@ -7,22 +7,21 @@ use iced::Vector; use crate::{ Element, - widget::{Id, Widget}, -}; - -use iced::{ - Event, Length, Rectangle, - clipboard::{ - dnd::{self, DndAction, DndDestinationRectangle, DndEvent, OfferEvent}, - mime::AllowedMimeTypes, + iced::{ + Event, Length, Rectangle, + clipboard::{ + dnd::{self, DndAction, DndDestinationRectangle, DndEvent, OfferEvent}, + mime::AllowedMimeTypes, + }, + event, + id::Internal, + mouse, overlay, }, - event, - id::Internal, - mouse, overlay, -}; -use iced_core::{ - self, Clipboard, Shell, layout, - widget::{Tree, tree}, + iced_core::{ + self, Clipboard, Shell, layout, + widget::{Tree, tree}, + }, + widget::{Id, Widget}, }; pub fn dnd_destination<'a, Message: 'static>( diff --git a/src/widget/dnd_source.rs b/src/widget/dnd_source.rs index 980723e3..25900a66 100644 --- a/src/widget/dnd_source.rs +++ b/src/widget/dnd_source.rs @@ -4,17 +4,17 @@ use iced_core::{widget::Operation, window}; use crate::{ Element, + iced::{ + Event, Length, Point, Rectangle, Vector, + clipboard::dnd::{DndAction, DndEvent, SourceEvent}, + event, mouse, overlay, + }, + iced_core::{ + self, Clipboard, Shell, layout, renderer, + widget::{Tree, tree}, + }, widget::{Id, Widget, container}, }; -use iced::{ - Event, Length, Point, Rectangle, Vector, - clipboard::dnd::{DndAction, DndEvent, SourceEvent}, - event, mouse, overlay, -}; -use iced_core::{ - self, Clipboard, Shell, layout, renderer, - widget::{Tree, tree}, -}; pub fn dnd_source< 'a, diff --git a/src/widget/dropdown/mod.rs b/src/widget/dropdown/mod.rs index b5fd4c06..b2d3fbed 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 = "winit", feature = "wayland"))] let dropdown = dropdown.with_popup(_parent_id, _on_surface_action, _map_action); dropdown diff --git a/src/widget/dropdown/widget.rs b/src/widget/dropdown/widget.rs index 2ff9c92f..b6244c07 100644 --- a/src/widget/dropdown/widget.rs +++ b/src/widget/dropdown/widget.rs @@ -60,7 +60,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 = "winit", feature = "wayland"))] positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, } @@ -96,14 +96,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 = "winit", feature = "wayland"))] 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 = "winit", feature = "wayland"))] /// Handle dropdown requests for popup creation. /// Intended to be used with [`crate::app::message::get_popup`] pub fn with_popup( @@ -154,7 +154,7 @@ where self } - #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] + #[cfg(all(feature = "winit", feature = "wayland"))] pub fn with_positioner( mut self, positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, @@ -268,7 +268,7 @@ where layout, cursor, shell, - #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] + #[cfg(all(feature = "winit", feature = "wayland"))] self.positioner.clone(), self.on_selected.clone(), self.selected, @@ -346,7 +346,7 @@ where viewport: &Rectangle, translation: Vector, ) -> Option> { - #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] + #[cfg(all(feature = "winit", feature = "wayland"))] if self.window_id.is_some() || self.on_surface_action.is_some() { return None; } @@ -545,7 +545,7 @@ pub fn update< layout: Layout<'_>, cursor: mouse::Cursor, shell: &mut Shell<'_, Message>, - #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] + #[cfg(all(feature = "winit", feature = "wayland"))] positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, on_selected: Arc Message + Send + Sync + 'static>, selected: Option, @@ -571,7 +571,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 = "winit", feature = "wayland"))] if let Some(((on_surface_action, parent), action_map)) = on_surface_action .as_ref() .zip(_window_id) @@ -658,7 +658,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 = "winit", feature = "wayland"))] if let Some(ref on_close) = on_surface_action { shell.publish(on_close(surface::action::destroy_popup(state.popup_id))); } @@ -681,7 +681,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 = "winit", feature = "wayland"))] if let Some(on_close) = on_surface_action { shell.publish(on_close(surface::action::destroy_popup(state.popup_id))); } @@ -726,7 +726,7 @@ pub fn mouse_interaction(layout: Layout<'_>, cursor: mouse::Cursor) -> mouse::In } } -#[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] +#[cfg(all(feature = "winit", feature = "wayland"))] /// Returns the current menu widget of a [`Dropdown`]. #[allow(clippy::too_many_arguments)] pub fn menu_widget< diff --git a/src/widget/flex_row/layout.rs b/src/widget/flex_row/layout.rs index 166b47f4..ae0c28d6 100644 --- a/src/widget/flex_row/layout.rs +++ b/src/widget/flex_row/layout.rs @@ -162,14 +162,9 @@ pub fn resolve( }); }); - let actual_height = nodes - .iter() - .map(|node| node.bounds().y + node.bounds().height) - .fold(0.0f32, f32::max); - let size = Size { width: flex_layout.content_size.width, - height: actual_height.max(flex_layout.content_size.height), + height: flex_layout.content_size.height, }; Node::with_children(size, nodes) diff --git a/src/widget/frames.rs b/src/widget/frames.rs index a542cec6..056a55ba 100644 --- a/src/widget/frames.rs +++ b/src/widget/frames.rs @@ -14,10 +14,10 @@ use iced_core::image::Renderer as ImageRenderer; use iced_core::mouse::Cursor; use iced_core::widget::{Tree, tree}; use iced_core::{ - Clipboard, ContentFit, Element, Event, Layout, Length, Rectangle, Rotation, Shell, Size, - Widget, event, layout, renderer, window, + Clipboard, ContentFit, Element, Event, Layout, Length, Rectangle, Shell, Size, Vector, Widget, + event, layout, renderer, window, }; -use iced_widget::image::{self, FilterMethod, Handle}; +use iced_widget::image::{self, Handle}; use image_rs::AnimationDecoder; use image_rs::codecs::gif::GifDecoder; use image_rs::codecs::png::PngDecoder; @@ -146,7 +146,7 @@ impl Frames { match image_type { ImageType::Gif => Self::from_decoder(GifDecoder::new(io::Cursor::new(bytes))?), - ImageType::Apng => Self::from_decoder(PngDecoder::new(io::Cursor::new(bytes))?.apng()?), + ImageType::Apng => Self::from_decoder(PngDecoder::new(io::Cursor::new(bytes))?.apng()), ImageType::WebP => Self::from_decoder(WebPDecoder::new(io::Cursor::new(bytes))?), } } @@ -168,10 +168,10 @@ impl Frames { let first = frames.first().cloned().unwrap(); let total_bytes = frames .iter() - .map(|f| match &f.handle { - Handle::Path(..) => 0, - Handle::Bytes(_, b) => b.len(), - Handle::Rgba { pixels, .. } => pixels.len(), + .map(|f| match f.handle.data() { + iced_core::image::Handle::Path(..) => 0, + iced_core::image::Handle::Bytes(_, b) => b.len(), + iced_core::image::Handle::Rgba { pixels, .. } => pixels.len(), }) .sum::() .try_into() @@ -324,11 +324,7 @@ where &self.frames.first.handle, self.width, self.height, - None, self.content_fit, - Rotation::default(), - false, - [0.0; 4], ) } @@ -375,18 +371,37 @@ where ) { let state = tree.state.downcast_ref::(); - iced_widget::image::draw( - renderer, - layout, - &state.current.frame.handle, - None, - iced_core::border::Radius::default(), - self.content_fit, - FilterMethod::default(), - Rotation::default(), - 1.0, - 1.0, - ); + // Pulled from iced_native::widget::::draw + // + // TODO: export iced_native::widget::image::draw as standalone function + { + let Size { width, height } = renderer.dimensions(&state.current.frame.handle); + let image_size = Size::new(width as f32, height as f32); + + let bounds = layout.bounds(); + let adjusted_fit = self.content_fit.fit(image_size, bounds.size()); + + let render = |renderer: &mut Renderer| { + let offset = Vector::new( + (bounds.width - adjusted_fit.width).max(0.0) / 2.0, + (bounds.height - adjusted_fit.height).max(0.0) / 2.0, + ); + + let drawing_bounds = Rectangle { + width: adjusted_fit.width, + height: adjusted_fit.height, + ..bounds + }; + + renderer.draw(state.current.frame.handle.clone(), drawing_bounds + offset); + }; + + if adjusted_fit.width > bounds.width || adjusted_fit.height > bounds.height { + renderer.with_layer(bounds, render); + } else { + render(renderer); + } + } } } diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs index a772f7d2..1465a9d7 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -5,8 +5,9 @@ use crate::cosmic_theme::{Density, Spacing}; use crate::{Element, theme, widget}; use apply::Apply; use derive_setters::Setters; -use iced_core::{Length, Size, Vector, Widget, layout, text, widget::tree}; -use std::borrow::Cow; +use iced::{Length, mouse}; +use iced_core::{Vector, Widget, widget::tree}; +use std::{borrow::Cow, cmp}; #[must_use] pub fn header_bar<'a, Message>() -> HeaderBar<'a, Message> { @@ -26,6 +27,7 @@ pub fn header_bar<'a, Message>() -> HeaderBar<'a, Message> { sharp_corners: false, is_ssd: false, on_double_click: None, + is_condensed: false, transparent: false, } } @@ -89,6 +91,9 @@ pub struct HeaderBar<'a, Message> { /// HeaderBar used for server-side decorations is_ssd: bool, + /// Whether the headerbar should be compact + is_condensed: bool, + /// Whether the headerbar should be transparent transparent: bool, } @@ -121,116 +126,48 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { self.end.push(widget.into()); self } + + /// Build the widget + #[must_use] + #[inline] + pub fn build(self) -> HeaderBarWidget<'a, Message> { + HeaderBarWidget { + header_bar_inner: self.view(), + } + } } pub struct HeaderBarWidget<'a, Message> { - start: Element<'a, Message>, - center: Option>, - end: Element<'a, Message>, + header_bar_inner: Element<'a, Message>, } -impl<'a, Message> HeaderBarWidget<'a, Message> { - pub fn new( - start: Element<'a, Message>, - center: Option>, - end: Element<'a, Message>, - ) -> Self { - Self { start, center, end } - } - - fn elems(&self) -> impl Iterator> { - std::iter::once(&self.start) - .chain(std::iter::once(&self.end)) - .chain(self.center.as_ref()) - } - - fn elems_mut(&mut self) -> impl Iterator> { - std::iter::once(&mut self.start) - .chain(std::iter::once(&mut self.end)) - .chain(self.center.as_mut()) - } -} - -impl<'a, Message: Clone + 'static> Widget - for HeaderBarWidget<'a, Message> +impl Widget + for HeaderBarWidget<'_, Message> { fn diff(&mut self, tree: &mut tree::Tree) { - if let Some(center) = &mut self.center { - tree.diff_children(&mut [&mut self.start, &mut self.end, center]); - } else { - tree.diff_children(&mut [&mut self.start, &mut self.end]); - } + tree.diff_children(&mut [&mut self.header_bar_inner]); } fn children(&self) -> Vec { - self.elems().map(tree::Tree::new).collect() + vec![tree::Tree::new(&self.header_bar_inner)] } - fn size(&self) -> Size { - Size { - width: Length::Fill, - height: Length::Shrink, - } + fn size(&self) -> iced_core::Size { + self.header_bar_inner.as_widget().size() } fn layout( &mut self, tree: &mut tree::Tree, renderer: &crate::Renderer, - limits: &layout::Limits, - ) -> layout::Node { - let width = limits.max().width; - let height = limits.max().height; - let gap = 8.0; - - let end_node = - self.end - .as_widget_mut() - .layout(&mut tree.children[1], renderer, &limits.loose()); - let end_width = end_node.size().width; - - let start_available = (width - end_width - gap).max(0.0); - let start_node = self.start.as_widget_mut().layout( - &mut tree.children[0], - renderer, - &layout::Limits::new(Size::ZERO, Size::new(start_available, height)), - ); - let start_width = start_node.size().width; - - let vcenter = |node: layout::Node, x: f32| -> layout::Node { - let dy = ((height - node.size().height) / 2.0).max(0.0); - node.translate(Vector::new(x, dy)) - }; - - let mut child_nodes = Vec::with_capacity(3); - child_nodes.push(vcenter(start_node, 0.0)); - child_nodes.push(vcenter(end_node, width - end_width)); - - if let Some(center) = &mut self.center { - let slot_start = start_width + gap; - let slot_end = (width - end_width - gap).max(slot_start); - let slot_width = slot_end - slot_start; - // this instead of `node.size().width` prevents center jitter as text ellipsizes - let natural_width = center - .as_widget_mut() - .layout(&mut tree.children[2], renderer, &limits.loose()) - .size() - .width; - - let node = center.as_widget_mut().layout( - &mut tree.children[2], - renderer, - &layout::Limits::new(Size::ZERO, Size::new(slot_width, height)), - ); - - let ideal_x = (width - natural_width) / 2.0; - let max_x = (width - end_width - gap - natural_width).max(slot_start); - let center_x = ideal_x.clamp(slot_start, max_x); - - child_nodes.push(vcenter(node, center_x)) - } - - layout::Node::with_children(Size::new(width, height), child_nodes) + limits: &iced_core::layout::Limits, + ) -> iced_core::layout::Node { + let child_tree = &mut tree.children[0]; + let child = self + .header_bar_inner + .as_widget_mut() + .layout(child_tree, renderer, limits); + iced_core::layout::Node::with_children(child.size(), vec![child]) } fn draw( @@ -243,13 +180,17 @@ impl<'a, Message: Clone + 'static> Widget Widget, viewport: &iced_core::Rectangle, ) { - self.elems_mut() - .zip(&mut state.children) - .zip(layout.children()) - .for_each(|((e, s), l)| { - e.as_widget_mut() - .update(s, event, l, cursor, renderer, clipboard, shell, viewport); - }); + let child_state = &mut state.children[0]; + let child_layout = layout.children().next().unwrap(); + + self.header_bar_inner.as_widget_mut().update( + child_state, + event, + child_layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ); } fn mouse_interaction( @@ -280,15 +227,15 @@ impl<'a, Message: Clone + 'static> Widget iced_core::mouse::Interaction { - self.elems() - .zip(&state.children) - .zip(layout.children()) - .map(|((e, s), l)| { - e.as_widget() - .mouse_interaction(s, l, cursor, viewport, renderer) - }) - .max() - .unwrap_or(iced_core::mouse::Interaction::None) + let child_tree = &state.children[0]; + let child_layout = layout.children().next().unwrap(); + self.header_bar_inner.as_widget().mouse_interaction( + child_tree, + child_layout, + cursor, + viewport, + renderer, + ) } fn operate( @@ -298,12 +245,14 @@ impl<'a, Message: Clone + 'static> Widget, ) { - self.elems_mut() - .zip(&mut state.children) - .zip(layout.children()) - .for_each(|((e, s), l)| { - e.as_widget_mut().operate(s, l, renderer, operation); - }); + let child_tree = &mut state.children[0]; + let child_layout = layout.children().next().unwrap(); + self.header_bar_inner.as_widget_mut().operate( + child_tree, + child_layout, + renderer, + operation, + ); } fn overlay<'b>( @@ -314,13 +263,15 @@ impl<'a, Message: Clone + 'static> Widget Option> { - self.elems_mut() - .zip(&mut state.children) - .zip(layout.children()) - .find_map(|((e, s), l)| { - e.as_widget_mut() - .overlay(s, l, renderer, viewport, translation) - }) + let child_tree = &mut state.children[0]; + let child_layout = layout.children().next().unwrap(); + self.header_bar_inner.as_widget_mut().overlay( + child_tree, + child_layout, + renderer, + viewport, + translation, + ) } fn drag_destinations( @@ -330,13 +281,16 @@ impl<'a, Message: Clone + 'static> Widget Widget iced_accessibility::A11yTree { - iced_accessibility::A11yTree::join( - self.elems() - .zip(&state.children) - .zip(layout.children()) - .map(|((e, s), l)| e.as_widget().a11y_nodes(l, s, p)), - ) - } -} - -impl<'a, Message: Clone + 'static> From> for Element<'a, Message> { - fn from(w: HeaderBarWidget<'a, Message>) -> Self { - Element::new(w) + let c_layout = layout.children().next().unwrap(); + let c_state = &state.children[0]; + self.header_bar_inner + .as_widget() + .a11y_nodes(c_layout, c_state, p) } } impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { + #[allow(clippy::too_many_lines)] /// Converts the headerbar builder into an Iced element. pub fn view(mut self) -> Element<'a, Message> { let Spacing { @@ -376,84 +324,154 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { let center = std::mem::take(&mut self.center); let mut end = std::mem::take(&mut self.end); + let window_control_cnt = self.on_close.is_some() as usize + + self.on_maximize.is_some() as usize + + self.on_minimize.is_some() as usize; // Also packs the window controls at the very end. - end.push(self.window_controls(space_xxs)); + end.push(self.window_controls()); - let padding = if self.is_ssd { - [2, 8, 2, 8] - } else { - match ( - self.density.unwrap_or_else(crate::config::header_size), - self.maximized, // window border handling - ) { - (Density::Compact, true) => [4, 8, 4, 8], - (Density::Compact, false) => [3, 7, 4, 7], - (_, true) => [8, 8, 8, 8], - (_, false) => [7, 7, 8, 7], + // Center content depending on window border + let padding = match self.density.unwrap_or_else(crate::config::header_size) { + Density::Compact => { + if self.maximized { + [4, 8, 4, 8] + } else { + [3, 7, 4, 7] + } + } + _ => { + if self.maximized { + [8, 8, 8, 8] + } else { + [7, 7, 8, 7] + } } }; - let start = widget::row::with_children(start) - .spacing(space_xxxs) - .align_y(iced::Alignment::Center) - .into(); - let center = if !center.is_empty() { - Some( - widget::row::with_children(center) + let acc_count = |v: &[Element<'a, Message>]| { + v.iter().fold(0, |acc, e| { + acc + match e.as_widget().size().width { + Length::Fixed(w) if w > 30. => (w / 30.0).ceil() as usize, + _ => 1, + } + }) + }; + + let left_len = acc_count(&start); + let right_len = acc_count(&end); + + let portion = ((left_len.max(right_len + window_control_cnt) as f32 + / center.len().max(1) as f32) + .round() as u16) + .max(1); + let (left_portion, right_portion) = + if center.is_empty() && (self.title.is_empty() || self.is_condensed) { + let left_to_right_ratio = left_len as f32 / right_len.max(1) as f32; + let right_to_left_ratio = right_len as f32 / left_len.max(1) as f32; + if right_to_left_ratio > 2. || left_len < 1 { + (1, 2) + } else if left_to_right_ratio > 2. || right_len < 1 { + (2, 1) + } else { + (left_len as u16, (right_len + window_control_cnt) as u16) + } + } else { + (portion, portion) + }; + let title_portion = cmp::max(left_portion, right_portion) * 2; + // Creates the headerbar widget. + let mut widget = widget::row::with_capacity(3) + // If elements exist in the start region, append them here. + .push( + widget::row::with_children(start) .spacing(space_xxxs) .align_y(iced::Alignment::Center) - .into(), + .apply(widget::container) + .align_x(iced::Alignment::Start) + .width(Length::FillPortion(left_portion)), ) - } else if !self.title.is_empty() { - Some( - widget::text::heading(self.title) - .wrapping(text::Wrapping::None) - .ellipsize(text::Ellipsize::End(text::EllipsizeHeightLimit::Lines(1))) - .into(), + // If elements exist in the center region, use them here. + // This will otherwise use the title as a widget if a title was defined. + .push_maybe(if !center.is_empty() { + Some( + widget::row::with_children(center) + .spacing(space_xxxs) + .align_y(iced::Alignment::Center) + .apply(widget::container) + .center_x(Length::Fill) + .into(), + ) + } else if !self.title.is_empty() && !self.is_condensed { + Some(self.title_widget(title_portion)) + } else { + None + }) + .push( + widget::row::with_children(end) + .spacing(space_xxs) + .align_y(iced::Alignment::Center) + .apply(widget::container) + .align_x(iced::Alignment::End) + .width(Length::FillPortion(right_portion)), ) - } else { - None - }; - let end = widget::row::with_children(end) - .spacing(space_xxs) .align_y(iced::Alignment::Center) - .into(); - - let mut widget = HeaderBarWidget::new(start, center, end) + .height(Length::Fixed(32.0 + padding[0] as f32 + padding[2] as f32)) + .padding(if self.is_ssd { [0, 8, 0, 8] } else { padding }) + .spacing(8) .apply(widget::container) - .class(theme::Container::HeaderBar { + .class(crate::theme::Container::HeaderBar { focused: self.focused, sharp_corners: self.sharp_corners, transparent: self.transparent, }) - .height(Length::Fixed(32.0 + padding[0] as f32 + padding[2] as f32)) - .padding(padding) + .center_y(Length::Shrink) .apply(widget::mouse_area); - if let Some(message) = self.on_drag { + // Assigns a message to emit when the headerbar is dragged. + if let Some(message) = self.on_drag.clone() { widget = widget.on_drag(message); } - if let Some(message) = self.on_maximize { + + // Assigns a message to emit when the headerbar is double-clicked. + if let Some(message) = self.on_maximize.clone() { widget = widget.on_release(message); } - if let Some(message) = self.on_double_click { + + if let Some(message) = self.on_double_click.clone() { widget = widget.on_double_press(message); } - if let Some(message) = self.on_right_click { + if let Some(message) = self.on_right_click.clone() { widget = widget.on_right_press(message); } widget.into() } + fn title_widget(&mut self, title_portion: u16) -> Element<'a, Message> { + let mut title = Cow::default(); + std::mem::swap(&mut title, &mut self.title); + + widget::text::heading(title) + .wrapping(iced_core::text::Wrapping::None) + .ellipsize(iced_core::text::Ellipsize::End( + iced_core::text::EllipsizeHeightLimit::Lines(1), + )) + .apply(widget::container) + .center(Length::FillPortion(title_portion)) + .into() + } + /// Creates the widget for window controls. - fn window_controls(&mut self, spacing: u16) -> Element<'a, Message> { + fn window_controls(&mut self) -> Element<'a, Message> { macro_rules! icon { ($name:expr, $size:expr, $on_press:expr) => {{ - widget::icon::from_name($name) - .apply(widget::button::icon) - .padding(8) - .class(theme::Button::HeaderBar) + let icon = { + widget::icon::from_name($name) + .apply(widget::button::icon) + .padding(8) + }; + + icon.class(crate::theme::Button::HeaderBar) .selected(self.focused) .icon_size($size) .on_press($on_press) @@ -464,7 +482,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { .push_maybe( self.on_minimize .take() - .map(|m| icon!("window-minimize-symbolic", 16, m)), + .map(|m: Message| icon!("window-minimize-symbolic", 16, m)), ) .push_maybe(self.on_maximize.take().map(|m| { if self.maximized { @@ -478,14 +496,21 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { .take() .map(|m| icon!("window-close-symbolic", 16, m)), ) - .spacing(spacing) - .align_y(iced::Alignment::Center) + .spacing(theme::spacing().space_xxs) + .apply(widget::container) + .center_y(Length::Fill) .into() } } impl<'a, Message: Clone + 'static> From> for Element<'a, Message> { fn from(headerbar: HeaderBar<'a, Message>) -> Self { - headerbar.view() + Element::new(headerbar.build()) + } +} + +impl<'a, Message: Clone + 'static> From> for Element<'a, Message> { + fn from(headerbar: HeaderBarWidget<'a, Message>) -> Self { + Element::new(headerbar) } } diff --git a/src/widget/icon/bundle.rs b/src/widget/icon/bundle.rs index bb6ce244..9d0877d0 100644 --- a/src/widget/icon/bundle.rs +++ b/src/widget/icon/bundle.rs @@ -4,12 +4,12 @@ //! Embedded icons for platforms which do not support icon themes yet. /// Icon bundling is not enabled on unix platforms. -#[cfg(all(unix, not(target_os = "macos")))] +#[cfg(unix)] pub fn get(icon_name: &str) -> Option { None } -#[cfg(any(not(unix), target_os = "macos"))] +#[cfg(not(unix))] /// Get a bundled icon on non-unix platforms. pub fn get(icon_name: &str) -> Option { ICONS @@ -17,5 +17,5 @@ pub fn get(icon_name: &str) -> Option { .map(|bytes| super::Data::Svg(crate::iced::widget::svg::Handle::from_memory(*bytes))) } -#[cfg(any(not(unix), target_os = "macos"))] +#[cfg(not(unix))] include!(concat!(env!("OUT_DIR"), "/bundled_icons.rs")); diff --git a/src/widget/icon/named.rs b/src/widget/icon/named.rs index dfd66cf5..8405e080 100644 --- a/src/widget/icon/named.rs +++ b/src/widget/icon/named.rs @@ -52,7 +52,7 @@ impl Named { } } - #[cfg(all(unix, not(target_os = "macos")))] + #[cfg(not(windows))] #[must_use] pub fn path(self) -> Option { let name = &*self.name; @@ -107,7 +107,7 @@ impl Named { result } - #[cfg(any(not(unix), target_os = "macos"))] + #[cfg(windows)] #[must_use] pub fn path(self) -> Option { //TODO: implement icon lookup for Windows diff --git a/src/widget/list/column.rs b/src/widget/list/column.rs new file mode 100644 index 00000000..136b49ea --- /dev/null +++ b/src/widget/list/column.rs @@ -0,0 +1,128 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + +use iced_core::Padding; +use iced_widget::container::Catalog; + +use crate::{ + Apply, Element, theme, + widget::{container, divider, space::vertical}, +}; + +#[inline] +pub fn list_column<'a, Message: 'static>() -> ListColumn<'a, Message> { + ListColumn::default() +} + +#[must_use] +pub struct ListColumn<'a, Message> { + spacing: u16, + padding: Padding, + list_item_padding: Padding, + divider_padding: u16, + style: theme::Container<'a>, + children: Vec>, +} + +impl Default for ListColumn<'_, Message> { + fn default() -> Self { + let cosmic_theme::Spacing { + space_xxs, space_m, .. + } = theme::spacing(); + + Self { + spacing: 0, + padding: Padding::from(0), + divider_padding: 16, + list_item_padding: [space_xxs, space_m].into(), + style: theme::Container::List, + children: Vec::with_capacity(4), + } + } +} + +impl<'a, Message: 'static> ListColumn<'a, Message> { + #[inline] + pub fn new() -> Self { + Self::default() + } + + #[allow(clippy::should_implement_trait)] + pub fn add(self, item: impl Into>) -> Self { + #[inline(never)] + fn inner<'a, Message: 'static>( + mut this: ListColumn<'a, Message>, + item: Element<'a, Message>, + ) -> ListColumn<'a, Message> { + if !this.children.is_empty() { + this.children.push( + container(divider::horizontal::default()) + .padding([0, this.divider_padding]) + .into(), + ); + } + + // Ensure a minimum height of 32. + let list_item = iced::widget::row![ + container(item).align_y(iced::Alignment::Center), + vertical().height(iced::Length::Fixed(32.)) + ] + .padding(this.list_item_padding) + .align_y(iced::Alignment::Center); + + this.children.push(list_item.into()); + this + } + + inner(self, item.into()) + } + + #[inline] + pub fn spacing(mut self, spacing: u16) -> Self { + self.spacing = spacing; + self + } + + /// Sets the style variant of this [`Circular`]. + #[inline] + pub fn style(mut self, style: ::Class<'a>) -> Self { + self.style = style; + self + } + + #[inline] + pub fn padding(mut self, padding: impl Into) -> Self { + self.padding = padding.into(); + self + } + + #[inline] + pub fn divider_padding(mut self, padding: u16) -> Self { + self.divider_padding = padding; + self + } + + pub fn list_item_padding(mut self, padding: impl Into) -> Self { + self.list_item_padding = padding.into(); + self + } + + #[must_use] + pub fn into_element(self) -> Element<'a, Message> { + crate::widget::column::with_children(self.children) + .spacing(self.spacing) + .padding(self.padding) + .width(iced::Length::Fill) + .apply(container) + .padding([self.spacing, 0]) + .class(self.style) + .width(iced::Length::Fill) + .into() + } +} + +impl<'a, Message: 'static> From> for Element<'a, Message> { + fn from(column: ListColumn<'a, Message>) -> Self { + column.into_element() + } +} diff --git a/src/widget/list/list_column.rs b/src/widget/list/list_column.rs deleted file mode 100644 index 4ef3fc01..00000000 --- a/src/widget/list/list_column.rs +++ /dev/null @@ -1,213 +0,0 @@ -// Copyright 2022 System76 -// SPDX-License-Identifier: MPL-2.0 - -use crate::widget::container::Catalog; -use crate::widget::{button, column, container, divider, row, space::vertical}; -use crate::{Apply, Element, theme}; -use iced::{Length, Padding}; - -/// A button list item for use in a [`ListColumn`]. -pub struct ListButton<'a, Message> { - content: Element<'a, Message>, - on_press: Option, - selected: bool, -} - -/// Creates a [`ListButton`] with the given content. -pub fn button<'a, Message>(content: impl Into>) -> ListButton<'a, Message> { - ListButton { - content: content.into(), - on_press: None, - selected: false, - } -} - -impl<'a, Message: 'static> ListButton<'a, Message> { - pub fn on_press(mut self, on_press: Message) -> Self { - self.on_press = Some(on_press); - self - } - - pub fn on_press_maybe(mut self, on_press: Option) -> Self { - self.on_press = on_press; - self - } - - pub fn selected(mut self, selected: bool) -> Self { - self.selected = selected; - self - } -} - -pub enum ListItem<'a, Message> { - Element(Element<'a, Message>), - Button(ListButton<'a, Message>), -} - -/// A trait for types that can be added to a [`ListColumn`]. -pub trait IntoListItem<'a, Message> { - fn into_list_item(self) -> ListItem<'a, Message>; -} - -impl<'a, Message, T> IntoListItem<'a, Message> for T -where - T: Into>, -{ - fn into_list_item(self) -> ListItem<'a, Message> { - ListItem::Element(self.into()) - } -} - -impl<'a, Message> IntoListItem<'a, Message> for ListButton<'a, Message> { - fn into_list_item(self) -> ListItem<'a, Message> { - ListItem::Button(self) - } -} - -// Snapshots the padding values at the moment an item is added -struct ListEntry<'a, Message> { - item: ListItem<'a, Message>, - item_padding: Padding, - divider_padding: u16, -} - -#[must_use] -pub struct ListColumn<'a, Message> { - list_item_padding: Padding, - divider_padding: u16, - style: theme::Container<'a>, - children: Vec>, -} - -#[inline] -pub fn list_column<'a, Message: 'static>() -> ListColumn<'a, Message> { - ListColumn::default() -} - -pub fn with_capacity<'a, Message: 'static>(capacity: usize) -> ListColumn<'a, Message> { - let cosmic_theme::Spacing { - space_xxs, space_m, .. - } = theme::spacing(); - - ListColumn { - list_item_padding: [space_xxs, space_m].into(), - divider_padding: 0, - style: theme::Container::List, - children: Vec::with_capacity(capacity), - } -} - -impl Default for ListColumn<'_, Message> { - fn default() -> Self { - with_capacity(4) - } -} - -impl<'a, Message: Clone + 'static> ListColumn<'a, Message> { - #[inline] - pub fn new() -> Self { - Self::default() - } - - /// Adds a [`ListItem`] to the [`ListColumn`]. - #[allow(clippy::should_implement_trait)] - pub fn add(mut self, item: impl IntoListItem<'a, Message>) -> Self { - self.children.push(ListEntry { - item: item.into_list_item(), - item_padding: self.list_item_padding, - divider_padding: self.divider_padding, - }); - self - } - - /// Sets the style variant of this [`ListColumn`]. - #[inline] - pub fn style(mut self, style: ::Class<'a>) -> Self { - self.style = style; - self - } - - pub fn list_item_padding(mut self, padding: impl Into) -> Self { - self.list_item_padding = padding.into(); - self - } - - #[inline] - pub fn divider_padding(mut self, padding: u16) -> Self { - self.divider_padding = padding; - self - } - - #[must_use] - pub fn into_element(self) -> Element<'a, Message> { - let count = self.children.len(); - let last_index = count.saturating_sub(1); - let radius_s = theme::active().cosmic().radius_s(); - let mut col = column::with_capacity((2 * count).saturating_sub(1)); - - // Ensure minimum height of 32 - let content_row = |content| { - row![container(content), vertical().height(32)].align_y(iced::Alignment::Center) - }; - - for ( - i, - ListEntry { - item, - item_padding, - divider_padding, - }, - ) in self.children.into_iter().enumerate() - { - if i > 0 { - col = col - .push(container(divider::horizontal::default()).padding([0, divider_padding])); - } - - col = match item { - ListItem::Element(content) => col.push( - content_row(content) - .padding(item_padding) - .width(Length::Fill), - ), - ListItem::Button(ListButton { - content, - on_press, - selected, - }) => col.push( - content_row(content) - .apply(button::custom) - .padding(item_padding) - .width(Length::Fill) - .on_press_maybe(on_press) - .selected(selected) - .class(theme::Button::ListItem(get_radius( - radius_s, - i == 0, - i == last_index, - ))), - ), - }; - } - - col.width(Length::Fill) - .apply(container) - .class(self.style) - .into() - } -} - -impl<'a, Message: Clone + 'static> From> for Element<'a, Message> { - fn from(column: ListColumn<'a, Message>) -> Self { - column.into_element() - } -} - -fn get_radius(radius: [f32; 4], first: bool, last: bool) -> [f32; 4] { - match (first, last) { - (true, true) => radius, - (true, false) => [radius[0], radius[1], 0.0, 0.0], - (false, true) => [0.0, 0.0, radius[2], radius[3]], - (false, false) => [0.0, 0.0, 0.0, 0.0], - } -} diff --git a/src/widget/list/mod.rs b/src/widget/list/mod.rs index 71eda086..c6e2051c 100644 --- a/src/widget/list/mod.rs +++ b/src/widget/list/mod.rs @@ -1,6 +1,6 @@ // Copyright 2022 System76 // SPDX-License-Identifier: MPL-2.0 -pub mod list_column; +pub mod column; -pub use self::list_column::{ListButton, ListColumn, button, list_column}; +pub use self::column::{ListColumn, list_column}; diff --git a/src/widget/menu/menu_bar.rs b/src/widget/menu/menu_bar.rs index 981446e8..7007befb 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -12,7 +12,6 @@ use super::{ #[cfg(all( feature = "multi-window", feature = "wayland", - target_os = "linux", feature = "winit", feature = "surface-message" ))] @@ -196,12 +195,7 @@ pub struct MenuBar { menu_roots: Vec>, style: ::Style, window_id: window::Id, - #[cfg(all( - feature = "multi-window", - feature = "wayland", - feature = "winit", - target_os = "linux" - ))] + #[cfg(all(feature = "multi-window", feature = "wayland", feature = "winit"))] positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, pub(crate) on_surface_action: Option Message + Send + Sync + 'static>>, @@ -236,12 +230,7 @@ where menu_roots, style: ::Style::default(), window_id: window::Id::NONE, - #[cfg(all( - feature = "multi-window", - feature = "wayland", - feature = "winit", - target_os = "linux" - ))] + #[cfg(all(feature = "multi-window", feature = "wayland", feature = "winit"))] positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner::default(), on_surface_action: None, } @@ -335,12 +324,7 @@ where self } - #[cfg(all( - feature = "multi-window", - feature = "wayland", - feature = "winit", - target_os = "linux" - ))] + #[cfg(all(feature = "multi-window", feature = "wayland", feature = "winit"))] pub fn with_positioner( mut self, positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, @@ -375,7 +359,6 @@ where #[cfg(all( feature = "multi-window", feature = "wayland", - target_os = "linux", feature = "winit", feature = "surface-message" ))] @@ -646,7 +629,6 @@ where state.open = false; #[cfg(all( feature = "wayland", - target_os = "linux", feature = "winit", feature = "surface-message" ))] @@ -670,7 +652,6 @@ where #[cfg(all( feature = "multi-window", feature = "wayland", - target_os = "linux", feature = "winit", feature = "surface-message" ))] @@ -685,7 +666,6 @@ where #[cfg(all( feature = "multi-window", feature = "wayland", - target_os = "linux", feature = "winit", feature = "surface-message" ))] @@ -768,7 +748,6 @@ where #[cfg(all( feature = "multi-window", feature = "wayland", - target_os = "linux", feature = "winit", feature = "surface-message" ))] diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index 74afe60f..d23a1599 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -7,7 +7,6 @@ use super::{menu_bar::MenuBarState, menu_tree::MenuTree}; #[cfg(all( feature = "multi-window", feature = "wayland", - target_os = "linux", feature = "winit", feature = "surface-message" ))] @@ -681,7 +680,6 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { #[cfg(all( feature = "multi-window", feature = "wayland", - target_os = "linux", feature = "winit", feature = "surface-message" ))] @@ -767,13 +765,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { PathHighlight::OmitActive => { !indices.is_empty() && i < indices.len() - 1 } - PathHighlight::MenuActive => { - !indices.is_empty() - && i < indices.len() - && menu_roots.len() > indices[i] - && (i < indices.len() - 1 - || !menu_roots[indices[i]].children.is_empty()) - } + PathHighlight::MenuActive => self.depth == state.active_root.len() - 1, }); // react only to the last menu @@ -968,8 +960,7 @@ impl Widget( #[cfg(all( feature = "multi-window", feature = "wayland", - target_os = "linux", feature = "winit", feature = "surface-message" ))] @@ -1527,7 +1517,7 @@ 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"))] + #[cfg(all(feature = "multi-window", feature = "wayland", 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) { state.active_root.truncate(menu.depth + 1); diff --git a/src/widget/menu/menu_tree.rs b/src/widget/menu/menu_tree.rs index 41cf1dff..bd182b9c 100644 --- a/src/widget/menu/menu_tree.rs +++ b/src/widget/menu/menu_tree.rs @@ -9,11 +9,11 @@ use std::rc::Rc; use iced::advanced::widget::text::Style as TextStyle; use iced_widget::core::{Element, renderer}; +use crate::iced_core::{Alignment, Length}; use crate::widget::menu::action::MenuAction; use crate::widget::menu::key_bind::KeyBind; use crate::widget::{Button, RcElementWrapper, icon}; use crate::{theme, widget}; -use iced_core::{Alignment, Length}; /// Nested menu is essentially a tree of items, a menu is a collection of items /// a menu itself can also be an item of another menu. @@ -252,18 +252,9 @@ pub fn menu_items< let l: Cow<'static, str> = label.into(); let key = find_key(&action, key_binds); let mut items = vec![ - widget::text(l) - .ellipsize(iced_core::text::Ellipsize::Middle( - iced_core::text::EllipsizeHeightLimit::Lines(1), - )) - .into(), + widget::text(l).into(), widget::space::horizontal().into(), - widget::text(key) - .class(key_class) - .ellipsize(iced_core::text::Ellipsize::Middle( - iced_core::text::EllipsizeHeightLimit::Lines(1), - )) - .into(), + widget::text(key).class(key_class).into(), ]; if let Some(icon) = icon { @@ -284,18 +275,9 @@ pub fn menu_items< let key = find_key(&action, key_binds); let mut items = vec![ - widget::text(l) - .ellipsize(iced_core::text::Ellipsize::Middle( - iced_core::text::EllipsizeHeightLimit::Lines(1), - )) - .into(), + widget::text(l).into(), widget::space::horizontal().into(), - widget::text(key) - .ellipsize(iced_core::text::Ellipsize::Middle( - iced_core::text::EllipsizeHeightLimit::Lines(1), - )) - .class(key_class) - .into(), + widget::text(key).class(key_class).into(), ]; if let Some(icon) = icon { @@ -330,19 +312,9 @@ pub fn menu_items< .into() }, widget::space::horizontal().width(spacing.space_xxs).into(), - widget::text(label) - .ellipsize(iced_core::text::Ellipsize::Middle( - iced_core::text::EllipsizeHeightLimit::Lines(1), - )) - .align_x(iced::Alignment::Start) - .into(), + widget::text(label).align_x(iced::Alignment::Start).into(), widget::space::horizontal().into(), - widget::text(key) - .class(key_class) - .ellipsize(iced_core::text::Ellipsize::Middle( - iced_core::text::EllipsizeHeightLimit::Lines(1), - )) - .into(), + widget::text(key).class(key_class).into(), ]; if let Some(icon) = icon { @@ -363,11 +335,7 @@ pub fn menu_items< trees.push(MenuTree::::with_children( RcElementWrapper::new(crate::Element::from( menu_button::<'static, _>(vec![ - widget::text(l.clone()) - .ellipsize(iced_core::text::Ellipsize::Middle( - iced_core::text::EllipsizeHeightLimit::Lines(1), - )) - .into(), + widget::text(l.clone()).into(), widget::space::horizontal().into(), widget::icon::from_name("pan-end-symbolic") .size(16) diff --git a/src/widget/mod.rs b/src/widget/mod.rs index f442b0da..73004597 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -24,7 +24,7 @@ //! .on_press(Message::LaunchUrl(REPOSITORY)) //! .padding(0); //! -//! let content = widget::column::with_capacity(3) +//! let content = widget::column() //! .push(widget::icon::from_name("my-app-icon")) //! .push(widget::text::title3("My App Name")) //! .push(link) @@ -53,9 +53,6 @@ pub use iced::widget::{Canvas, canvas}; #[doc(inline)] pub use iced::widget::{Checkbox, checkbox}; -#[doc(inline)] -pub use iced::widget::{Column, column}; - #[doc(inline)] pub use iced::widget::{ComboBox, combo_box}; @@ -78,10 +75,10 @@ pub use iced::widget::{MouseArea, mouse_area}; pub use iced::widget::{PaneGrid, pane_grid}; #[doc(inline)] -pub use iced::widget::{Responsive, responsive}; +pub use iced::widget::{ProgressBar, progress_bar}; #[doc(inline)] -pub use iced::widget::{Row, row}; +pub use iced::widget::{Responsive, responsive}; #[doc(inline)] pub use iced::widget::{Slider, VerticalSlider, slider, vertical_slider}; @@ -138,6 +135,34 @@ pub mod context_drawer; #[doc(inline)] pub use context_drawer::{ContextDrawer, context_drawer}; +#[doc(inline)] +pub use column::{Column, column}; +pub mod column { + //! A container which aligns its children in a column. + + pub type Column<'a, Message> = iced::widget::Column<'a, Message, crate::Theme, crate::Renderer>; + + #[must_use] + /// A container which aligns its children in a column. + pub fn column<'a, Message>() -> Column<'a, Message> { + Column::new() + } + + #[must_use] + /// A pre-allocated [`column`]. + pub fn with_capacity<'a, Message>(capacity: usize) -> Column<'a, Message> { + Column::with_capacity(capacity) + } + + #[must_use] + /// A [`column`] that will be assigned an [`Iterator`] of children. + pub fn with_children<'a, Message>( + children: impl IntoIterator>, + ) -> Column<'a, Message> { + Column::with_children(children) + } +} + pub mod layer_container; #[doc(inline)] pub use layer_container::{LayerContainer, layer_container}; @@ -254,13 +279,6 @@ pub mod popover; #[doc(inline)] pub use popover::{Popover, popover}; -pub mod progress_bar; -#[doc(inline)] -pub use progress_bar::{ - circular, circular::Circular, determinate_circular, determinate_linear, indeterminate_circular, - indeterminate_linear, linear, linear::Linear, style, -}; - pub mod radio; #[doc(inline)] pub use radio::{Radio, radio}; @@ -269,6 +287,35 @@ pub mod rectangle_tracker; #[doc(inline)] pub use rectangle_tracker::{RectangleTracker, rectangle_tracking_container}; +#[doc(inline)] +pub use row::{Row, row}; + +pub mod row { + //! A container which aligns its children in a row. + + pub type Row<'a, Message> = iced::widget::Row<'a, Message, crate::Theme, crate::Renderer>; + + #[must_use] + /// A container which aligns its children in a row. + pub fn row<'a, Message>() -> Row<'a, Message> { + Row::new() + } + + #[must_use] + /// A pre-allocated [`row`]. + pub fn with_capacity<'a, Message>(capacity: usize) -> Row<'a, Message> { + Row::with_capacity(capacity) + } + + #[must_use] + /// A [`row`] that will be assigned an [`Iterator`] of children. + pub fn with_children<'a, Message>( + children: impl IntoIterator>, + ) -> Row<'a, Message> { + Row::with_children(children) + } +} + pub mod scrollable; #[doc(inline)] pub use scrollable::scrollable; @@ -308,7 +355,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", feature = "winit"))] pub mod wayland; pub mod tooltip { diff --git a/src/widget/popover.rs b/src/widget/popover.rs index af5370a8..7a82cd86 100644 --- a/src/widget/popover.rs +++ b/src/widget/popover.rs @@ -138,10 +138,6 @@ where renderer: &Renderer, operation: &mut dyn Operation, ) { - // Skip operating on background content, prevents Tab from escaping - if self.modal && self.popup.is_some() { - return; - } self.content .as_widget_mut() .operate(content_tree_mut(tree), layout, renderer, operation); @@ -176,17 +172,11 @@ where } } - // Hide cursor from background content when modal popup is active - let cursor = if self.modal && self.popup.is_some() { - mouse::Cursor::Unavailable - } else { - cursor_position - }; self.content.as_widget_mut().update( &mut tree.children[0], event, layout, - cursor, + cursor_position, renderer, clipboard, shell, @@ -224,19 +214,13 @@ where cursor_position: mouse::Cursor, viewport: &Rectangle, ) { - // Hide cursor from background content when a modal popup is active - let cursor = if self.modal && self.popup.is_some() { - mouse::Cursor::Unavailable - } else { - cursor_position - }; self.content.as_widget().draw( content_tree(tree), renderer, theme, renderer_style, layout, - cursor, + cursor_position, viewport, ); } diff --git a/src/widget/progress_bar/circular.rs b/src/widget/progress_bar/circular.rs deleted file mode 100644 index fa8c38fe..00000000 --- a/src/widget/progress_bar/circular.rs +++ /dev/null @@ -1,462 +0,0 @@ -//! Show a circular progress indicator. -use super::style::StyleSheet; -use crate::anim::smootherstep; -use iced::advanced::layout; -use iced::advanced::renderer; -use iced::advanced::widget::tree::{self, Tree}; -use iced::advanced::{self, Clipboard, Layout, Shell, Widget}; -use iced::mouse; -use iced::time::Instant; -use iced::widget::canvas; -use iced::window; -use iced::{Element, Event, Length, Radians, Rectangle, Renderer, Size, Vector}; - -use std::f32::consts::PI; -use std::time::Duration; - -const MIN_ANGLE: Radians = Radians(PI / 8.0); - -#[must_use] -pub struct Circular -where - Theme: StyleSheet, -{ - size: f32, - bar_height: f32, - style: ::Style, - cycle_duration: Duration, - rotation_duration: Duration, - progress: Option, -} - -impl Circular -where - Theme: StyleSheet, -{ - /// Creates a new [`Circular`] with the given content. - pub fn new() -> Self { - Circular { - size: 40.0, - bar_height: 4.0, - style: ::Style::default(), - cycle_duration: Duration::from_millis(1500), - rotation_duration: Duration::from_secs(2), - progress: None, - } - } - - /// Sets the size of the [`Circular`]. - pub fn size(mut self, size: f32) -> Self { - self.size = size; - self - } - - /// Sets the bar height of the [`Circular`]. - pub fn bar_height(mut self, bar_height: f32) -> Self { - self.bar_height = bar_height; - self - } - - /// Sets the style variant of this [`Circular`]. - pub fn style(mut self, style: ::Style) -> Self { - self.style = style; - self - } - - /// Sets the cycle duration of this [`Circular`]. - pub fn cycle_duration(mut self, duration: Duration) -> Self { - self.cycle_duration = duration / 2; - self - } - - /// Sets the base rotation duration of this [`Circular`]. This is the duration that a full - /// rotation would take if the cycle rotation were set to 0.0 (no expanding or contracting) - pub fn rotation_duration(mut self, duration: Duration) -> Self { - self.rotation_duration = duration; - self - } - - /// Override the default behavior by providing a determinate progress value between `0.0` and `1.0`. - pub fn progress(mut self, progress: f32) -> Self { - self.progress = Some(progress.clamp(0.0, 1.0)); - self - } - - fn min_wrap_angle(&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 - gap * 2.0) - } -} - -impl Default for Circular -where - Theme: StyleSheet, -{ - fn default() -> Self { - Self::new() - } -} - -#[derive(Clone, Copy)] -enum Animation { - Expanding { - start: Instant, - progress: f32, - rotation: u32, - last: Instant, - }, - Contracting { - start: Instant, - progress: f32, - rotation: u32, - last: Instant, - }, -} - -impl Default for Animation { - fn default() -> Self { - Self::Expanding { - start: Instant::now(), - progress: 0.0, - rotation: 0, - last: Instant::now(), - } - } -} - -impl Animation { - fn next(&self, additional_rotation: u32, wrap_angle: f32, now: Instant) -> Self { - match self { - Self::Expanding { rotation, .. } => Self::Contracting { - start: now, - progress: 0.0, - rotation: rotation.wrapping_add(additional_rotation), - last: now, - }, - Self::Contracting { rotation, .. } => Self::Expanding { - start: now, - progress: 0.0, - rotation: rotation.wrapping_add( - (f64::from((wrap_angle) / (2.0 * PI)) * f64::from(u32::MAX)) as u32, - ), - last: now, - }, - } - } - - fn start(&self) -> Instant { - match self { - Self::Expanding { start, .. } | Self::Contracting { start, .. } => *start, - } - } - - fn last(&self) -> Instant { - match self { - Self::Expanding { last, .. } | Self::Contracting { last, .. } => *last, - } - } - - fn timed_transition( - &self, - cycle_duration: Duration, - rotation_duration: Duration, - wrap_angle: f32, - now: Instant, - ) -> Self { - let elapsed = now.duration_since(self.start()); - let additional_rotation = ((now - self.last()).as_secs_f32() - / rotation_duration.as_secs_f32() - * (u32::MAX) as f32) as u32; - - match elapsed { - elapsed if elapsed > cycle_duration => self.next(additional_rotation, wrap_angle, now), - _ => self.with_elapsed(cycle_duration, additional_rotation, elapsed, now), - } - } - - fn with_elapsed( - &self, - cycle_duration: Duration, - additional_rotation: u32, - elapsed: Duration, - now: Instant, - ) -> Self { - let progress = elapsed.as_secs_f32() / cycle_duration.as_secs_f32(); - match self { - Self::Expanding { - start, rotation, .. - } => Self::Expanding { - start: *start, - progress, - rotation: rotation.wrapping_add(additional_rotation), - last: now, - }, - Self::Contracting { - start, rotation, .. - } => Self::Contracting { - start: *start, - progress, - rotation: rotation.wrapping_add(additional_rotation), - last: now, - }, - } - } - - fn rotation(&self) -> f32 { - match self { - Self::Expanding { rotation, .. } | Self::Contracting { rotation, .. } => { - *rotation as f32 / u32::MAX as f32 - } - } - } -} - -#[derive(Default)] -struct State { - animation: Animation, - cache: canvas::Cache, - progress: Option, -} - -impl Widget for Circular -where - Message: Clone, - Theme: StyleSheet, -{ - fn tag(&self) -> tree::Tag { - tree::Tag::of::() - } - - fn state(&self) -> tree::State { - tree::State::new(State::default()) - } - - fn size(&self) -> Size { - Size { - width: Length::Fixed(self.size), - height: Length::Fixed(self.size), - } - } - - fn layout( - &mut self, - _tree: &mut Tree, - _renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - layout::atomic(limits, self.size, self.size) - } - - fn update( - &mut self, - tree: &mut Tree, - event: &Event, - _layout: Layout<'_>, - _cursor: mouse::Cursor, - _renderer: &Renderer, - _clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - _viewport: &Rectangle, - ) { - let state = tree.state.downcast_mut::(); - if self.progress.is_some() { - if !float_cmp::approx_eq!( - f32, - state.progress.unwrap_or_default(), - self.progress.unwrap_or_default() - ) { - state.progress = self.progress; - state.cache.clear(); - } - return; - } - if let Event::Window(window::Event::RedrawRequested(now)) = event { - let (_, wrap_angle) = self.min_wrap_angle(self.size / 2.0 - self.bar_height); - state.animation = state.animation.timed_transition( - self.cycle_duration, - self.rotation_duration, - wrap_angle, - *now, - ); - - state.cache.clear(); - shell.request_redraw(); - } - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Theme, - _style: &renderer::Style, - layout: Layout<'_>, - _cursor: mouse::Cursor, - _viewport: &Rectangle, - ) { - use advanced::Renderer as _; - - let state = tree.state.downcast_ref::(); - let bounds = layout.bounds(); - let custom_style = - ::appearance(theme, &self.style, self.progress.is_some(), true); - - let geometry = state.cache.draw(renderer, bounds.size(), |frame| { - let track_radius = frame.width() / 2.0 - self.bar_height; - let track_path = canvas::Path::circle(frame.center(), track_radius); - - frame.stroke( - &track_path, - canvas::Stroke::default() - .with_color(custom_style.track_color) - .with_width(self.bar_height), - ); - - if let Some(progress) = self.progress { - // outer border - if let Some(border_color) = custom_style.border_color { - let border_path = - canvas::Path::circle(frame.center(), track_radius + self.bar_height / 2.0); - - frame.stroke( - &border_path, - canvas::Stroke::default() - .with_color(border_color) - .with_width(1.0), - ); - } - - // inner border - if let Some(border_color) = custom_style.border_color { - let border_path = - canvas::Path::circle(frame.center(), track_radius - self.bar_height / 2.0); - - frame.stroke( - &border_path, - canvas::Stroke::default() - .with_color(border_color) - .with_width(1.0), - ); - } - - // bar - let mut builder = canvas::path::Builder::new(); - - builder.arc(canvas::path::Arc { - center: frame.center(), - radius: track_radius, - start_angle: Radians(-PI / 2.0), - end_angle: Radians(-PI / 2.0 + progress * 2.0 * PI), - }); - - let bar_path = builder.build(); - - frame.stroke( - &bar_path, - canvas::Stroke::default() - .with_color(custom_style.bar_color) - .with_width(self.bar_height), - ); - - let mut builder = canvas::path::Builder::new(); - - // get center of end of arc for rounded cap - let end_angle = -PI / 2.0 + progress * 2.0 * PI; - let end_center = - frame.center() + Vector::new(end_angle.cos(), end_angle.sin()) * track_radius; - builder.arc(canvas::path::Arc { - center: end_center, - radius: self.bar_height / 2.0, - start_angle: Radians(end_angle), - end_angle: Radians(end_angle + PI), - }); - - // get center of start of arc for rounded cap - let start_angle = -PI / 2.0; - let start_center = frame.center() - + Vector::new(start_angle.cos(), start_angle.sin()) * track_radius; - builder.arc(canvas::path::Arc { - center: start_center, - radius: self.bar_height / 2.0, - start_angle: Radians(start_angle - PI), - end_angle: Radians(start_angle), - }); - - let cap_path = builder.build(); - frame.fill(&cap_path, custom_style.bar_color); - } else { - let mut builder = canvas::path::Builder::new(); - - let start = state.animation.rotation() * 2.0 * PI; - let (min_angle, wrap_angle) = self.min_wrap_angle(track_radius); - let (start_angle, end_angle) = match state.animation { - Animation::Expanding { progress, .. } => ( - start, - start + min_angle + wrap_angle * smootherstep(progress), - ), - Animation::Contracting { progress, .. } => ( - start + wrap_angle * smootherstep(progress), - start + min_angle + wrap_angle, - ), - }; - builder.arc(canvas::path::Arc { - center: frame.center(), - radius: track_radius, - start_angle: Radians(start_angle), - end_angle: Radians(end_angle), - }); - - let bar_path = builder.build(); - - frame.stroke( - &bar_path, - canvas::Stroke::default() - .with_color(custom_style.bar_color) - .with_width(self.bar_height), - ); - - let mut builder = canvas::path::Builder::new(); - - // get center of end of arc for rounded cap - let end_center = - frame.center() + Vector::new(end_angle.cos(), end_angle.sin()) * track_radius; - builder.arc(canvas::path::Arc { - center: end_center, - radius: self.bar_height / 2.0, - start_angle: Radians(end_angle), - end_angle: Radians(end_angle + PI), - }); - - // get center of start of arc for rounded cap - let start_center = frame.center() - + Vector::new(start_angle.cos(), start_angle.sin()) * track_radius; - builder.arc(canvas::path::Arc { - center: start_center, - radius: self.bar_height / 2.0, - start_angle: Radians(start_angle - PI), - end_angle: Radians(start_angle), - }); - - let cap_path = builder.build(); - frame.fill(&cap_path, custom_style.bar_color); - } - }); - - renderer.with_translation(Vector::new(bounds.x, bounds.y), |renderer| { - use iced::advanced::graphics::geometry::Renderer as _; - - renderer.draw_geometry(geometry); - }); - } -} - -impl<'a, Message, Theme> From> for Element<'a, Message, Theme, Renderer> -where - Message: Clone + 'a, - Theme: StyleSheet + 'a, -{ - fn from(circular: Circular) -> Self { - Self::new(circular) - } -} diff --git a/src/widget/progress_bar/linear.rs b/src/widget/progress_bar/linear.rs deleted file mode 100644 index 226b2b5f..00000000 --- a/src/widget/progress_bar/linear.rs +++ /dev/null @@ -1,306 +0,0 @@ -//! Show a linear progress indicator. -use iced::advanced::layout; -use iced::advanced::renderer::{self, Quad}; -use iced::advanced::widget::tree::{self, Tree}; -use iced::advanced::{self, Clipboard, Layout, Shell, Widget}; -use iced::mouse; -use iced::time::Instant; -use iced::window; -use iced::{Background, Element, Event, Length, Rectangle, Size}; - -use crate::anim::smootherstep; - -use super::style::StyleSheet; - -use std::time::Duration; - -#[must_use] -pub struct Linear -where - Theme: StyleSheet, -{ - width: Length, - girth: Length, - style: Theme::Style, - cycle_duration: Duration, - progress: Option, -} - -impl Linear -where - Theme: StyleSheet, -{ - /// Creates a new [`Linear`] with the given content. - pub fn new() -> Self { - Linear { - width: Length::Fixed(100.0), - girth: Length::Fixed(4.0), - style: Theme::Style::default(), - cycle_duration: Duration::from_millis(1500), - progress: None, - } - } - - /// Sets the width of the [`Linear`]. - pub fn width(mut self, width: impl Into) -> Self { - self.width = width.into(); - self - } - - /// Sets the girth of the [`Linear`]. - pub fn girth(mut self, girth: impl Into) -> Self { - self.girth = girth.into(); - self - } - - /// Sets the style variant of this [`Linear`]. - pub fn style(mut self, style: impl Into) -> Self { - self.style = style.into(); - self - } - - /// Sets the cycle duration of this [`Linear`]. - pub fn cycle_duration(mut self, duration: Duration) -> Self { - self.cycle_duration = duration / 2; - self - } - - /// Override the default behavior by providing a determinate progress value between `0.0` and `1.0`. - pub fn progress(mut self, progress: f32) -> Self { - self.progress = Some(progress.clamp(0.0, 1.0)); - self - } -} - -impl Default for Linear -where - Theme: StyleSheet, -{ - fn default() -> Self { - Self::new() - } -} - -#[derive(Clone, Copy)] -enum State { - Expanding { start: Instant, progress: f32 }, - Contracting { start: Instant, progress: f32 }, -} - -impl Default for State { - fn default() -> Self { - Self::Expanding { - start: Instant::now(), - progress: 0.0, - } - } -} - -impl State { - fn next(&self, now: Instant) -> Self { - match self { - Self::Expanding { .. } => Self::Contracting { - start: now, - progress: 0.0, - }, - Self::Contracting { .. } => Self::Expanding { - start: now, - progress: 0.0, - }, - } - } - - fn start(&self) -> Instant { - match self { - Self::Expanding { start, .. } | Self::Contracting { start, .. } => *start, - } - } - - fn timed_transition(&self, cycle_duration: Duration, now: Instant) -> Self { - let elapsed = now.duration_since(self.start()); - - match elapsed { - elapsed if elapsed > cycle_duration => self.next(now), - _ => self.with_elapsed(cycle_duration, elapsed), - } - } - - fn with_elapsed(&self, cycle_duration: Duration, elapsed: Duration) -> Self { - let progress = elapsed.as_secs_f32() / cycle_duration.as_secs_f32(); - match self { - Self::Expanding { start, .. } => Self::Expanding { - start: *start, - progress, - }, - Self::Contracting { start, .. } => Self::Contracting { - start: *start, - progress, - }, - } - } -} - -impl Widget for Linear -where - Message: Clone, - Theme: StyleSheet, - Renderer: advanced::Renderer, -{ - fn tag(&self) -> tree::Tag { - tree::Tag::of::() - } - - fn state(&self) -> tree::State { - tree::State::new(State::default()) - } - - fn size(&self) -> Size { - Size { - width: self.width, - height: self.girth, - } - } - - fn layout( - &mut self, - _tree: &mut Tree, - _renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - layout::atomic(limits, self.width, self.girth) - } - - fn update( - &mut self, - tree: &mut Tree, - event: &Event, - _layout: Layout<'_>, - _cursor: mouse::Cursor, - _renderer: &Renderer, - _clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - _viewport: &Rectangle, - ) { - if self.progress.is_some() { - return; - } - - let state = tree.state.downcast_mut::(); - - if let Event::Window(window::Event::RedrawRequested(now)) = event { - *state = state.timed_transition(self.cycle_duration, *now); - - shell.request_redraw(); - } - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Theme, - _style: &renderer::Style, - layout: Layout<'_>, - _cursor: mouse::Cursor, - _viewport: &Rectangle, - ) { - let bounds = layout.bounds(); - let custom_style = theme.appearance(&self.style, self.progress.is_some(), false); - let state = tree.state.downcast_ref::(); - - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x, - y: bounds.y, - width: bounds.width, - height: bounds.height, - }, - border: iced::Border { - width: if custom_style.border_color.is_some() { - 1.0 - } else { - 0.0 - }, - color: custom_style.border_color.unwrap_or(custom_style.bar_color), - radius: custom_style.border_radius.into(), - }, - snap: true, - ..renderer::Quad::default() - }, - Background::Color(custom_style.track_color), - ); - - if let Some(progress) = self.progress { - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x, - y: bounds.y, - width: progress * bounds.width, - height: bounds.height, - }, - border: iced::Border { - width: 0., - color: iced::Color::TRANSPARENT, - radius: custom_style.border_radius.into(), - }, - snap: true, - ..renderer::Quad::default() - }, - Background::Color(custom_style.bar_color), - ); - } else { - match state { - State::Expanding { progress, .. } => renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x, - y: bounds.y, - width: smootherstep(*progress) * bounds.width, - height: bounds.height, - }, - border: iced::Border { - width: 0., - color: iced::Color::TRANSPARENT, - radius: custom_style.border_radius.into(), - }, - snap: true, - ..renderer::Quad::default() - }, - Background::Color(custom_style.bar_color), - ), - - State::Contracting { progress, .. } => renderer.fill_quad( - Quad { - bounds: Rectangle { - x: bounds.x + smootherstep(*progress) * bounds.width, - y: bounds.y, - width: (1.0 - smootherstep(*progress)) * bounds.width, - height: bounds.height, - }, - border: iced::Border { - width: 0., - color: iced::Color::TRANSPARENT, - radius: custom_style.border_radius.into(), - }, - snap: true, - ..renderer::Quad::default() - }, - Background::Color(custom_style.bar_color), - ), - } - } - } -} - -impl<'a, Message, Theme, Renderer> From> for Element<'a, Message, Theme, Renderer> -where - Message: Clone + 'a, - Theme: StyleSheet + 'a, - Renderer: iced::advanced::Renderer + 'a, -{ - fn from(linear: Linear) -> Self { - Self::new(linear) - } -} diff --git a/src/widget/progress_bar/mod.rs b/src/widget/progress_bar/mod.rs deleted file mode 100644 index 4e277b0a..00000000 --- a/src/widget/progress_bar/mod.rs +++ /dev/null @@ -1,23 +0,0 @@ -pub mod circular; -pub mod linear; -pub mod style; - -/// A spinner / throbber widget that can be used to indicate that some operation is in progress. -pub fn indeterminate_circular() -> circular::Circular { - circular::Circular::new() -} - -/// A linear throbber widget that can be used to indicate that some operation is in progress. -pub fn indeterminate_linear() -> linear::Linear { - linear::Linear::new() -} - -/// A circular progress spinner widget that can be used to indicate the progress of some operation. -pub fn determinate_circular(progress: f32) -> circular::Circular { - circular::Circular::new().progress(progress) -} - -/// A linear progress bar widget that can be used to indicate the progress of some operation. -pub fn determinate_linear(progress: f32) -> linear::Linear { - linear::Linear::new().progress(progress) -} diff --git a/src/widget/progress_bar/style.rs b/src/widget/progress_bar/style.rs deleted file mode 100644 index db2fe64d..00000000 --- a/src/widget/progress_bar/style.rs +++ /dev/null @@ -1,105 +0,0 @@ -use iced::Color; - -#[derive(Debug, Clone, Copy)] -pub struct Appearance { - /// The track [`Color`] of the progress indicator. - pub track_color: Color, - /// The bar [`Color`] of the progress indicator. - pub bar_color: Color, - /// The border [`Color`] of the progress indicator. - pub border_color: Option, - /// The border radius of the progress indicator. - pub border_radius: f32, -} - -impl std::default::Default for Appearance { - fn default() -> Self { - Self { - track_color: Color::TRANSPARENT, - bar_color: Color::BLACK, - border_color: None, - border_radius: 0.0, - } - } -} - -/// A set of rules that dictate the style of an indicator. -pub trait StyleSheet { - /// The supported style of the [`StyleSheet`]. - type Style: Default; - - /// Produces the active [`Appearance`] of a indicator. - fn appearance( - &self, - style: &Self::Style, - is_determinate: bool, - is_circular: bool, - ) -> Appearance; -} - -impl StyleSheet for iced::Theme { - type Style = (); - - fn appearance( - &self, - _style: &Self::Style, - _is_determinate: bool, - _is_circular: bool, - ) -> Appearance { - let palette = self.extended_palette(); - - Appearance { - track_color: palette.background.weak.color, - bar_color: palette.primary.base.color, - border_color: None, - border_radius: 0.0, - } - } -} - -impl StyleSheet for crate::Theme { - type Style = (); - - fn appearance( - &self, - _style: &Self::Style, - is_determinate: bool, - is_circular: bool, - ) -> Appearance { - let cur = self.current_container(); - let mut cur_divider = cur.divider; - cur_divider.alpha = 0.5; - let theme = self.cosmic(); - - let (mut track_color, bar_color) = if theme.is_dark && theme.is_high_contrast { - ( - theme.palette.neutral_6.into(), - theme.accent_text_color().into(), - ) - } else if theme.is_dark { - (theme.palette.neutral_5.into(), theme.accent_color().into()) - } else if theme.is_high_contrast { - ( - theme.palette.neutral_4.into(), - theme.accent_text_color().into(), - ) - } else { - (theme.palette.neutral_3.into(), theme.accent_color().into()) - }; - - if !is_determinate && is_circular { - track_color = Color::TRANSPARENT; - } - - Appearance { - track_color, - bar_color, - border_color: if is_determinate && theme.is_high_contrast { - Some(cur_divider.into()) - } else { - None - }, - border_radius: theme.corner_radii.radius_xl[0], - } - } -} diff --git a/src/widget/radio.rs b/src/widget/radio.rs index c3f115c0..338c0a4e 100644 --- a/src/widget/radio.rs +++ b/src/widget/radio.rs @@ -1,5 +1,5 @@ //! Create choices using radio buttons. -use crate::{Theme, theme}; +use crate::Theme; use iced::border; use iced_core::event::{self, Event}; use iced_core::layout; @@ -92,7 +92,7 @@ where { is_selected: bool, on_click: Message, - label: Option>, + label: Element<'a, Message, Theme, Renderer>, width: Length, size: f32, spacing: f32, @@ -106,6 +106,9 @@ where /// The default size of a [`Radio`] button. pub const DEFAULT_SIZE: f32 = 16.0; + /// The default spacing of a [`Radio`] button. + pub const DEFAULT_SPACING: f32 = 8.0; + /// Creates a new [`Radio`] button. /// /// It expects: @@ -123,29 +126,10 @@ where Radio { is_selected: Some(value) == selected, on_click: f(value), - label: Some(label.into()), + label: label.into(), width: Length::Shrink, size: Self::DEFAULT_SIZE, - spacing: theme::spacing().space_xs as f32, - } - } - - /// Creates a new [`Radio`] button without a label. - /// - /// This is intended for internal use with the settings item builder, - /// where the label comes from the settings item title instead. - pub(crate) fn new_no_label(value: V, selected: Option, f: F) -> Self - where - V: Eq + Copy, - F: FnOnce(V) -> Message, - { - Radio { - is_selected: Some(value) == selected, - on_click: f(value), - label: None, - width: Length::Shrink, - size: Self::DEFAULT_SIZE, - spacing: theme::spacing().space_xs as f32, + spacing: Self::DEFAULT_SPACING, } } @@ -177,17 +161,11 @@ where Renderer: iced_core::Renderer, { fn children(&self) -> Vec { - if let Some(label) = &self.label { - vec![Tree::new(label)] - } else { - vec![] - } + vec![Tree::new(&self.label)] } fn diff(&mut self, tree: &mut Tree) { - if let Some(label) = &mut self.label { - tree.diff_children(std::slice::from_mut(label)); - } + tree.diff_children(std::slice::from_mut(&mut self.label)); } fn size(&self) -> Size { Size { @@ -202,20 +180,16 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - if let Some(label) = &mut self.label { - layout::next_to_each_other( - &limits.width(self.width), - self.spacing, - |_| layout::Node::new(Size::new(self.size, self.size)), - |limits| { - label - .as_widget_mut() - .layout(&mut tree.children[0], renderer, limits) - }, - ) - } else { - layout::Node::new(Size::new(self.size, self.size)) - } + layout::next_to_each_other( + &limits.width(self.width), + self.spacing, + |_| layout::Node::new(Size::new(self.size, self.size)), + |limits| { + self.label + .as_widget_mut() + .layout(&mut tree.children[0], renderer, limits) + }, + ) } fn operate( @@ -225,14 +199,12 @@ where renderer: &Renderer, operation: &mut dyn iced_core::widget::Operation<()>, ) { - if let Some(label) = &mut self.label { - label.as_widget_mut().operate( - &mut tree.children[0], - layout.children().nth(1).unwrap(), - renderer, - operation, - ); - } + self.label.as_widget_mut().operate( + &mut tree.children[0], + layout.children().nth(1).unwrap(), + renderer, + operation, + ); } fn update( @@ -246,25 +218,24 @@ where shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) { - if let Some(label) = &mut self.label { - label.as_widget_mut().update( - &mut tree.children[0], - event, - layout.children().nth(1).unwrap(), - cursor, - renderer, - clipboard, - shell, - viewport, - ); - } + self.label.as_widget_mut().update( + &mut tree.children[0], + event, + layout.children().nth(1).unwrap(), + cursor, + renderer, + clipboard, + shell, + viewport, + ); if !shell.is_event_captured() { match event { - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerLifted { .. }) => { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { if cursor.is_over(layout.bounds()) { shell.publish(self.on_click.clone()); + shell.capture_event(); return; } @@ -282,17 +253,13 @@ where viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { - let interaction = if let Some(label) = &self.label { - label.as_widget().mouse_interaction( - &tree.children[0], - layout.children().nth(1).unwrap(), - cursor, - viewport, - renderer, - ) - } else { - mouse::Interaction::default() - }; + let interaction = self.label.as_widget().mouse_interaction( + &tree.children[0], + layout.children().nth(1).unwrap(), + cursor, + viewport, + renderer, + ); if interaction == mouse::Interaction::default() { if cursor.is_over(layout.bounds()) { @@ -317,6 +284,8 @@ where ) { let is_mouse_over = cursor.is_over(layout.bounds()); + let mut children = layout.children(); + let custom_style = if is_mouse_over { theme.style( &(), @@ -333,21 +302,16 @@ where ) }; - let (dot_bounds, label_layout) = if self.label.is_some() { - let mut children = layout.children(); - let dot_bounds = children.next().unwrap().bounds(); - (dot_bounds, children.next()) - } else { - (layout.bounds(), None) - }; - { - let size = dot_bounds.width; + let layout = children.next().unwrap(); + let bounds = layout.bounds(); + + let size = bounds.width; let dot_size = 6.0; renderer.fill_quad( renderer::Quad { - bounds: dot_bounds, + bounds, border: Border { radius: (size / 2.0).into(), width: custom_style.border_width, @@ -362,8 +326,8 @@ where renderer.fill_quad( renderer::Quad { bounds: Rectangle { - x: dot_bounds.x + (size - dot_size) / 2.0, - y: dot_bounds.y + (size - dot_size) / 2.0, + x: bounds.x + (size - dot_size) / 2.0, + y: bounds.y + (size - dot_size) / 2.0, width: dot_size, height: dot_size, }, @@ -375,8 +339,9 @@ where } } - if let (Some(label), Some(label_layout)) = (&self.label, label_layout) { - label.as_widget().draw( + { + let label_layout = children.next().unwrap(); + self.label.as_widget().draw( &tree.children[0], renderer, theme, @@ -396,7 +361,7 @@ where viewport: &Rectangle, translation: Vector, ) -> Option> { - self.label.as_mut()?.as_widget_mut().overlay( + self.label.as_widget_mut().overlay( &mut tree.children[0], layout.children().nth(1).unwrap(), renderer, @@ -412,14 +377,12 @@ where renderer: &Renderer, dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, ) { - if let Some(label) = &self.label { - label.as_widget().drag_destinations( - &state.children[0], - layout.children().nth(1).unwrap(), - renderer, - dnd_rectangles, - ); - } + self.label.as_widget().drag_destinations( + &state.children[0], + layout.children().nth(1).unwrap(), + renderer, + dnd_rectangles, + ); } } diff --git a/src/widget/responsive_menu_bar.rs b/src/widget/responsive_menu_bar.rs index b5dd556d..5f855260 100644 --- a/src/widget/responsive_menu_bar.rs +++ b/src/widget/responsive_menu_bar.rs @@ -25,7 +25,7 @@ impl Default for ResponsiveMenuBar { fn default() -> ResponsiveMenuBar { ResponsiveMenuBar { collapsed_item_width: { - #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] + #[cfg(all(feature = "winit", feature = "wayland"))] if matches!( crate::app::cosmic::WINDOWING_SYSTEM.get(), Some(crate::app::cosmic::WindowingSystem::Wayland) @@ -34,7 +34,7 @@ impl Default for ResponsiveMenuBar { } else { ItemWidth::Static(84) } - #[cfg(not(all(feature = "winit", feature = "wayland", target_os = "linux")))] + #[cfg(not(all(feature = "winit", feature = "wayland")))] { ItemWidth::Static(84) } diff --git a/src/widget/segmented_button/horizontal.rs b/src/widget/segmented_button/horizontal.rs index 5fd67649..3e46dd5e 100644 --- a/src/widget/segmented_button/horizontal.rs +++ b/src/widget/segmented_button/horizontal.rs @@ -213,18 +213,6 @@ where state.buttons_offset = num - state.buttons_visible; } - // Resize paragraph bounds so that text ellipsis can take effect. - if !matches!(self.width, Length::Shrink) || state.collapsed { - let num = state.buttons_visible.max(1) as f32; - let spacing = f32::from(self.spacing); - let mut width_offset = 0.0; - if state.collapsed { - width_offset = f32::from(self.button_height) * 2.0; - } - let button_width = ((num).mul_add(-spacing, size.width - width_offset) + spacing) / num; - self.resize_paragraphs(state, button_width); - } - size } } diff --git a/src/widget/segmented_button/vertical.rs b/src/widget/segmented_button/vertical.rs index 5458cd0a..7963e9c8 100644 --- a/src/widget/segmented_button/vertical.rs +++ b/src/widget/segmented_button/vertical.rs @@ -117,15 +117,10 @@ where height += item_height; } - let size = limits.height(Length::Fixed(height)).resolve( + limits.height(Length::Fixed(height)).resolve( self.width, self.height, Size::new(width, height), - ); - - // Resize paragraph bounds so that text ellipsis can take effect. - self.resize_paragraphs(state, size.width); - - size + ) } } diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 44ca8574..857d6371 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -3,6 +3,7 @@ use super::model::{Entity, Model, Selectable}; use super::{InsertPosition, ReorderEvent}; +use crate::iced_core::id::Internal; use crate::theme::{SegmentedButton as Style, THEME}; use crate::widget::dnd_destination::DragId; use crate::widget::menu::{ @@ -21,7 +22,6 @@ use iced::{ Alignment, Background, Color, Event, Length, Padding, Rectangle, Size, Task, Vector, alignment, keyboard, mouse, touch, window, }; -use iced_core::id::Internal; use iced_core::mouse::ScrollDelta; use iced_core::text::{self, Ellipsize, LineHeight, Renderer as TextRenderer, Shaping, Wrapping}; use iced_core::widget::operation::Focusable; @@ -156,8 +156,6 @@ where pub(super) spacing: u16, /// LineHeight of the font. pub(super) line_height: LineHeight, - /// Ellipsize strategy for button text. - pub(super) ellipsize: Ellipsize, /// Style to draw the widget in. #[setters(into)] pub(super) style: Style, @@ -218,14 +216,13 @@ where maximum_button_width: u16::MAX, indent_spacing: 16, font_active: crate::font::semibold(), - font_hovered: crate::font::default(), + font_hovered: crate::font::semibold(), font_inactive: crate::font::default(), font_size: 14.0, height: Length::Shrink, width: Length::Fill, spacing: 0, line_height: LineHeight::default(), - ellipsize: Ellipsize::default(), style: Style::default(), context_menu: None, on_activate: None, @@ -246,13 +243,12 @@ where fn update_entity_paragraph(&mut self, state: &mut LocalState, key: Entity) { if let Some(text) = self.model.text.get(key) { - let font = if self.button_is_focused(state, key) - || state.show_context == Some(key) - || self.model.is_active(key) - { + let font = if self.button_is_focused(state, key) { self.font_active - } else if self.button_is_hovered(state, key) { + } else if state.show_context == Some(key) || self.button_is_hovered(state, key) { self.font_hovered + } else if self.model.is_active(key) { + self.font_active } else { self.font_inactive }; @@ -262,10 +258,10 @@ where font.hash(&mut hasher); let text_hash = hasher.finish(); - if let Some(prev_hash) = state.text_hashes.insert(key, text_hash) - && prev_hash == text_hash - { - return; + if let Some(prev_hash) = state.text_hashes.insert(key, text_hash) { + if prev_hash == text_hash { + return; + } } if let Some(paragraph) = state.paragraphs.get_mut(key) { @@ -279,7 +275,7 @@ where shaping: Shaping::Advanced, wrapping: Wrapping::None, line_height: self.line_height, - ellipsize: self.ellipsize, + ellipsize: Ellipsize::default(), }; paragraph.update(text); } else { @@ -293,7 +289,7 @@ where shaping: Shaping::Advanced, wrapping: Wrapping::None, line_height: self.line_height, - ellipsize: self.ellipsize, + ellipsize: Ellipsize::default(), }; state.paragraphs.insert(key, crate::Plain::new(text)); } @@ -306,7 +302,7 @@ where { self.context_menu = context_menu.map(|menus| { vec![menu::Tree::with_children( - crate::Element::from(crate::widget::Row::new()), + crate::Element::from(crate::widget::row::<'static, Message>()), menus, )] }); @@ -625,7 +621,7 @@ where align_y: alignment::Vertical::Center, shaping: Shaping::Advanced, wrapping: Wrapping::default(), - ellipsize: self.ellipsize, + ellipsize: Ellipsize::default(), line_height: self.line_height, }) }); @@ -661,50 +657,6 @@ where (width, f32::from(self.button_height)) } - /// Resizes paragraph bounds based on the actual available button width so that - /// text ellipsis can take effect. Call this after `variant_layout` has populated - /// `state.internal_layout` with final button sizes. - pub(super) fn resize_paragraphs(&self, state: &mut LocalState, available_width: f32) { - if matches!(self.ellipsize, Ellipsize::None) { - return; - } - - for (nth, key) in self.model.order.iter().copied().enumerate() { - if self.model.text(key).is_some_and(|text| !text.is_empty()) { - let mut non_text_width = - f32::from(self.button_padding[0]) + f32::from(self.button_padding[2]); - - 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 { - non_text_width += 16.0 + f32::from(self.button_spacing); - } - } - - if self.model.is_closable(key) { - non_text_width += - f32::from(self.close_icon.size) + f32::from(self.button_spacing); - } - - let text_width = (available_width - non_text_width).max(0.0); - - if let Some(paragraph) = state.paragraphs.get_mut(key) { - paragraph.resize(Size::new(text_width, f32::INFINITY)); - - // Update internal_layout actual content width so that - // button_alignment centering uses the ellipsized size. - let content_width = paragraph.min_bounds().width + non_text_width - - f32::from(self.button_padding[0]) - - f32::from(self.button_padding[2]); - if let Some(entry) = state.internal_layout.get_mut(nth) { - entry.1.width = content_width; - } - } - } - } - } - pub(super) fn max_button_dimensions( &self, state: &mut LocalState, @@ -928,6 +880,7 @@ where fn diff(&mut self, tree: &mut Tree) { let state = tree.state.downcast_mut::(); + for key in self.model.order.iter().copied() { self.update_entity_paragraph(state, key); } @@ -1481,7 +1434,7 @@ where } } } else { - if let Item::Tab(_key) = std::mem::replace(&mut state.hovered, Item::None) { + if let Item::Tab(key) = std::mem::replace(&mut state.hovered, Item::None) { for key in self.model.order.iter().copied() { self.update_entity_paragraph(state, key); } @@ -1643,7 +1596,7 @@ where } } - iced_core::mouse::Interaction::default() + iced_core::mouse::Interaction::Idle } #[allow(clippy::too_many_lines)] @@ -1985,9 +1938,7 @@ where // Align contents of the button to the requested `button_alignment`. { - // Avoid shifting content outside the left edge when the measured content is - // wider than the available button bounds (for example, non-ellipsized text). - let actual_width = state.internal_layout[nth].1.width.min(bounds.width); + let actual_width = state.internal_layout[nth].1.width; let offset = match self.button_alignment { Alignment::Start => None, @@ -2043,10 +1994,10 @@ where ..image_bounds }, crate::widget::icon(match crate::widget::common::object_select().data() { - iced_core::svg::Data::Bytes(bytes) => { + crate::iced_core::svg::Data::Bytes(bytes) => { crate::widget::icon::from_svg_bytes(bytes.as_ref()).symbolic(true) } - iced_core::svg::Data::Path(path) => { + crate::iced_core::svg::Data::Path(path) => { crate::widget::icon::from_path(path.clone()) } }), @@ -2139,7 +2090,7 @@ where tree: &'b mut Tree, layout: iced_core::Layout<'b>, _renderer: &Renderer, - _viewport: &iced_core::Rectangle, + viewport: &iced_core::Rectangle, translation: Vector, ) -> Option> { let state = tree.state.downcast_mut::(); diff --git a/src/widget/settings/item.rs b/src/widget/settings/item.rs index 5abb464c..110ab7b7 100644 --- a/src/widget/settings/item.rs +++ b/src/widget/settings/item.rs @@ -4,8 +4,8 @@ use std::borrow::Cow; use crate::{ - Element, Theme, theme, - widget::{FlexRow, Row, column, container, flex_row, list, row, text}, + Element, theme, + widget::{FlexRow, Row, column, container, flex_row, row, text}, }; use derive_setters::Setters; use iced_core::{Length, text::Wrapping}; @@ -18,12 +18,12 @@ use taffy::AlignContent; pub fn item<'a, Message: 'static>( title: impl Into> + 'a, widget: impl Into> + 'a, -) -> Row<'a, Message, Theme> { +) -> Row<'a, Message> { #[inline(never)] fn inner<'a, Message: 'static>( title: Cow<'a, str>, widget: Element<'a, Message>, - ) -> Row<'a, Message, Theme> { + ) -> Row<'a, Message> { item_row(vec![ text(title).wrapping(Wrapping::Word).into(), space::horizontal().into(), @@ -37,7 +37,7 @@ pub fn item<'a, Message: 'static>( /// A settings item aligned in a row #[must_use] #[allow(clippy::module_name_repetitions)] -pub fn item_row(children: Vec>) -> Row { +pub fn item_row(children: Vec>) -> Row { row::with_children(children) .spacing(theme::spacing().space_xs) .align_y(iced::Alignment::Center) @@ -103,9 +103,9 @@ pub struct Item<'a, Message> { icon: Option>, } -impl<'a, Message: Clone + 'static> Item<'a, Message> { +impl<'a, Message: 'static> Item<'a, Message> { /// Assigns a control to the item. - pub fn control(self, widget: impl Into>) -> Row<'a, Message, Theme> { + pub fn control(self, widget: impl Into>) -> Row<'a, Message> { item_row(self.control_(widget.into())) } @@ -114,109 +114,39 @@ impl<'a, Message: Clone + 'static> Item<'a, Message> { flex_item_row(self.control_(widget.into())) } - fn label(self) -> Element<'a, Message> { + #[inline(never)] + fn control_(self, widget: Element<'a, Message>) -> Vec> { + let mut contents = Vec::with_capacity(4); + + if let Some(icon) = self.icon { + contents.push(icon); + } + if let Some(description) = self.description { - column::with_capacity(2) + let column = column::with_capacity(2) .spacing(2) .push(text::body(self.title).wrapping(Wrapping::Word)) .push(text::caption(description).wrapping(Wrapping::Word)) - .width(Length::Fill) - .into() - } else { - text(self.title).width(Length::Fill).into() - } - } + .width(Length::Fill); - #[inline(never)] - fn control_(mut self, widget: Element<'a, Message>) -> Vec> { - let mut contents = Vec::with_capacity(3); - if let Some(icon) = self.icon.take() { - contents.push(icon); + contents.push(column.into()); + } else { + contents.push(text(self.title).width(Length::Fill).into()); } - contents.push(self.label()); + contents.push(widget); contents } - fn control_start(self, widget: impl Into>) -> Row<'a, Message, Theme> { - item_row(vec![widget.into(), self.label()]) - } - pub fn toggler( self, is_checked: bool, message: impl Fn(bool) -> Message + 'static, - ) -> list::ListButton<'a, Message> { - let on_press = message(!is_checked); - list::button( - self.control( - crate::widget::toggler(is_checked) - .width(Length::Shrink) - .on_toggle(message), - ), + ) -> Row<'a, Message> { + self.control( + crate::widget::toggler(is_checked) + .width(Length::Shrink) + .on_toggle(message), ) - .on_press(on_press) - } - - pub fn toggler_maybe( - self, - is_checked: bool, - message: Option Message + 'static>, - ) -> list::ListButton<'a, Message> { - let on_press = message.as_ref().map(|f| f(!is_checked)); - list::button( - self.control( - crate::widget::toggler(is_checked) - .width(Length::Shrink) - .on_toggle_maybe(message), - ), - ) - .on_press_maybe(on_press) - } - - pub fn checkbox( - self, - is_checked: bool, - message: impl Fn(bool) -> Message + 'static, - ) -> list::ListButton<'a, Message> { - let on_press = message(!is_checked); - list::button( - self.control_start( - crate::widget::checkbox(is_checked) - .width(Length::Shrink) - .on_toggle(message), - ), - ) - .on_press(on_press) - } - - pub fn checkbox_maybe( - self, - is_checked: bool, - message: Option Message + 'static>, - ) -> list::ListButton<'a, Message> { - let on_press = message.as_ref().map(|f| f(!is_checked)); - list::button( - self.control_start( - crate::widget::checkbox(is_checked) - .width(Length::Shrink) - .on_toggle_maybe(message), - ), - ) - .on_press_maybe(on_press) - } - - pub fn radio(self, value: V, selected: Option, f: F) -> list::ListButton<'a, Message> - where - V: Eq + Copy, - F: Fn(V) -> Message, - { - let on_press = f(value); - list::button( - self.control_start(crate::widget::radio::Radio::new_no_label( - value, selected, f, - )), - ) - .on_press(on_press) } } diff --git a/src/widget/settings/mod.rs b/src/widget/settings/mod.rs index 79d81697..597d9bdd 100644 --- a/src/widget/settings/mod.rs +++ b/src/widget/settings/mod.rs @@ -8,10 +8,10 @@ pub use self::item::{flex_item, flex_item_row, item, item_row}; pub use self::section::{Section, section}; use crate::widget::{Column, column}; -use crate::{Element, Theme, theme}; +use crate::{Element, theme}; /// A column with a predefined style for creating a settings panel #[must_use] -pub fn view_column(children: Vec>) -> Column { +pub fn view_column(children: Vec>) -> Column { column::with_children(children).spacing(theme::spacing().space_m) } diff --git a/src/widget/settings/section.rs b/src/widget/settings/section.rs index 3dddb1a1..899826dc 100644 --- a/src/widget/settings/section.rs +++ b/src/widget/settings/section.rs @@ -2,24 +2,22 @@ // SPDX-License-Identifier: MPL-2.0 use crate::Element; -use crate::widget::list_column::IntoListItem; -use crate::widget::{ListColumn, column, list_column, text}; +use crate::widget::{ListColumn, column, text}; use std::borrow::Cow; /// A section within a settings view column. -pub fn section<'a, Message: Clone + 'static>() -> Section<'a, Message> { +#[deprecated(note = "use `settings::section().title()` instead")] +pub fn view_section<'a, Message: 'static>(title: impl Into>) -> Section<'a, Message> { + section().title(title) +} + +/// A section within a settings view column. +pub fn section<'a, Message: 'static>() -> Section<'a, Message> { with_column(ListColumn::default()) } -/// A section with a pre-defined list column of a given capacity. -pub fn with_capacity<'a, Message: Clone + 'static>(capacity: usize) -> Section<'a, Message> { - with_column(list_column::with_capacity(capacity)) -} - /// A section with a pre-defined list column. -pub fn with_column( - children: ListColumn<'_, Message>, -) -> Section<'_, Message> { +pub fn with_column(children: ListColumn<'_, Message>) -> Section<'_, Message> { Section { header: None, children, @@ -32,9 +30,9 @@ pub struct Section<'a, Message> { children: ListColumn<'a, Message>, } -impl<'a, Message: Clone + 'static> Section<'a, Message> { +impl<'a, Message: 'static> Section<'a, Message> { /// Define an optional title for the section. - pub fn title(self, title: impl Into>) -> Self { + pub fn title(mut self, title: impl Into>) -> Self { self.header(text::heading(title.into())) } @@ -46,13 +44,13 @@ impl<'a, Message: Clone + 'static> Section<'a, Message> { /// Add a child element to the section's list column. #[allow(clippy::should_implement_trait)] - pub fn add(mut self, item: impl IntoListItem<'a, Message>) -> Self { - self.children = self.children.add(item); + pub fn add(mut self, item: impl Into>) -> Self { + self.children = self.children.add(item.into()); self } /// Add a child element to the section's list column, if `Some`. - pub fn add_maybe(self, item: Option>) -> Self { + pub fn add_maybe(self, item: Option>>) -> Self { if let Some(item) = item { self.add(item) } else { @@ -63,13 +61,13 @@ impl<'a, Message: Clone + 'static> Section<'a, Message> { /// Extends the [`Section`] with the given children. pub fn extend( self, - children: impl IntoIterator>, + children: impl IntoIterator>>, ) -> Self { children.into_iter().fold(self, Self::add) } } -impl<'a, Message: Clone + 'static> From> for Element<'a, Message> { +impl<'a, Message: 'static> From> for Element<'a, Message> { fn from(data: Section<'a, Message>) -> Self { column::with_capacity(2) .spacing(8) diff --git a/src/widget/table/widget/compact.rs b/src/widget/table/widget/compact.rs index 65ac9058..85b5cfce 100644 --- a/src/widget/table/widget/compact.rs +++ b/src/widget/table/widget/compact.rs @@ -65,7 +65,7 @@ where let selected = val.model.is_active(entity); let context_menu = (val.item_context_builder)(item); - widget::column::with_capacity(2) + widget::column() .spacing(val.item_spacing) .push( widget::divider::horizontal::default() @@ -73,7 +73,7 @@ where .padding(val.divider_padding), ) .push( - widget::row::with_capacity(2) + widget::row() .spacing(space_xxxs) .align_y(Alignment::Center) .push_maybe( @@ -81,7 +81,7 @@ where .map(|icon| icon.size(val.icon_size)), ) .push( - widget::column::with_capacity(2) + widget::column() .push(widget::text::body(item.get_text(Category::default()))) .push({ let mut elements = val @@ -145,7 +145,7 @@ where }) // Double click .apply(|mouse_area| { - if let Some(ref on_item_mb) = val.on_item_mb_double { + if let Some(ref on_item_mb) = val.on_item_mb_left { mouse_area.on_double_click((on_item_mb)(entity)) } else { mouse_area diff --git a/src/widget/table/widget/standard.rs b/src/widget/table/widget/standard.rs index 9ab76c9d..79107074 100644 --- a/src/widget/table/widget/standard.rs +++ b/src/widget/table/widget/standard.rs @@ -99,7 +99,7 @@ where }; // Build the category header - widget::row::with_capacity(2) + widget::row() .spacing(val.icon_spacing) .push(widget::text::heading(category.to_string())) .push_maybe(match sort_state { @@ -152,7 +152,7 @@ where categories .iter() .map(|category| { - widget::row::with_capacity(2) + widget::row() .spacing(val.icon_spacing) .push_maybe( item.get_icon(*category) @@ -206,7 +206,7 @@ where }) // Double click .apply(|mouse_area| { - if let Some(ref on_item_mb) = val.on_item_mb_double { + if let Some(ref on_item_mb) = val.on_item_mb_left { mouse_area.on_double_click((on_item_mb)(entity)) } else { mouse_area diff --git a/src/widget/text_input/cursor.rs b/src/widget/text_input/cursor.rs index 3ffb535c..42f52da1 100644 --- a/src/widget/text_input/cursor.rs +++ b/src/widget/text_input/cursor.rs @@ -3,19 +3,16 @@ // SPDX-License-Identifier: MIT //! Track the cursor of a text input. -use iced_core::text::Affinity; - use super::value::Value; /// The cursor of a text input. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[derive(Debug, Copy, Clone)] pub struct Cursor { state: State, - affinity: Affinity, } /// The state of a [`Cursor`]. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[derive(Debug, Copy, Clone)] pub enum State { /// Cursor without a selection Index(usize), @@ -34,7 +31,6 @@ impl Default for Cursor { fn default() -> Self { Self { state: State::Index(0), - affinity: Affinity::Before, } } } @@ -197,37 +193,4 @@ impl Cursor { State::Selection { start, end } => start.max(end), } } - - /// Returns the current cursor [`Affinity`]. - #[must_use] - pub fn affinity(&self) -> Affinity { - self.affinity - } - - /// Sets the cursor [`Affinity`]. - pub fn set_affinity(&mut self, affinity: Affinity) { - self.affinity = affinity; - } - - /// Moves the cursor in a visual direction, accounting for RTL text. - /// - /// `forward` = `true` is visually rightward. - pub fn move_visual(&mut self, forward: bool, by_words: bool, rtl: bool, value: &Value) { - match (forward ^ rtl, by_words) { - (true, false) => self.move_right(value), - (true, true) => self.move_right_by_words(value), - (false, false) => self.move_left(value), - (false, true) => self.move_left_by_words(value), - } - } - - /// Extends the selection in a visual direction, accounting for RTL text. - pub fn select_visual(&mut self, forward: bool, by_words: bool, rtl: bool, value: &Value) { - match (forward ^ rtl, by_words) { - (true, false) => self.select_right(value), - (true, true) => self.select_right_by_words(value), - (false, false) => self.select_left(value), - (false, true) => self.select_left_by_words(value), - } - } } diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index 4336c757..3960cee1 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -22,11 +22,10 @@ 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::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::touch; use iced_core::widget::Id; @@ -67,20 +66,18 @@ pub fn editable_input<'a, Message: Clone + 'static>( editing: bool, on_toggle_edit: impl Fn(bool) -> Message + 'a, ) -> TextInput<'a, Message> { - // The trailing icon is a placeholder; diff() rebuilds it reactively - // based on the current is_read_only state and value content. + let icon = crate::widget::icon::from_name(if editing { + "edit-clear-symbolic" + } else { + "edit-symbolic" + }); + TextInput::new(placeholder, text) .style(crate::theme::TextInput::EditableText) .editable() .editing(editing) .on_toggle_edit(on_toggle_edit) - .trailing_icon( - crate::widget::icon::from_name("edit-symbolic") - .size(16) - .apply(crate::widget::container) - .padding(8) - .into(), - ) + .trailing_icon(icon.size(16).into()) } /// Creates a new search [`TextInput`]. @@ -188,7 +185,6 @@ pub struct TextInput<'a, Message> { is_editable_variant: bool, is_read_only: bool, select_on_focus: bool, - double_click_select_delimiter: Option, font: Option<::Font>, width: Length, padding: Padding, @@ -239,7 +235,6 @@ where is_editable_variant: false, is_read_only: false, select_on_focus: false, - double_click_select_delimiter: None, font: None, width: Length::Fill, padding: spacing.into(), @@ -345,17 +340,6 @@ where self } - /// Sets a delimiter character for double-click selection behavior. - /// - /// When set, double-clicking before the last occurrence of this character - /// selects from the start to that character. Double-clicking after the - /// delimiter uses normal word selection. - #[inline] - pub const fn double_click_select_delimiter(mut self, delimiter: char) -> Self { - self.double_click_select_delimiter = Some(delimiter); - self - } - /// Emits a message when an unfocused text input has been focused by click. /// /// This will not trigger if the input was focused externally by the application. @@ -529,7 +513,7 @@ where } /// Sets the start dnd handler of the [`TextInput`]. - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] pub fn on_start_dnd(mut self, on_start_dnd: impl Fn(State) -> Message + 'a) -> Self { self.on_create_dnd_source = Some(Box::new(on_start_dnd)); self @@ -611,7 +595,6 @@ where self.value = state.tracked_value.clone(); // std::mem::swap(&mut state.tracked_value, &mut self.value); } - state.double_click_select_delimiter = self.double_click_select_delimiter; // Unfocus text input if it becomes disabled if self.on_input.is_none() && !self.manage_value { state.last_click = None; @@ -683,36 +666,7 @@ where } } - if self.is_editable_variant { - if !state.is_focused() { - // Not yet interacted, use the widget's value - state.is_read_only = self.is_read_only; - } else { - // Already interacted, use the state - self.is_read_only = state.is_read_only; - } - - let editing = !self.is_read_only; - let icon_name = if editing { - if self.value.is_empty() { - "window-close-symbolic" - } else { - "edit-clear-symbolic" - } - } else { - "edit-symbolic" - }; - - self.trailing_icon = Some( - crate::widget::icon::from_name(icon_name) - .size(16) - .apply(crate::widget::container) - .padding(8) - .into(), - ); - } else { - self.is_read_only = state.is_read_only; - } + self.is_read_only = state.is_read_only; // Stop pasting if input becomes disabled if !self.manage_value && self.on_input.is_none() { @@ -901,6 +855,9 @@ where if !state.is_read_only && state.is_focused.is_some_and(|f| !f.focused) { state.is_read_only = true; shell.publish((on_edit)(false)); + } else if state.is_focused() && state.is_read_only { + state.is_read_only = false; + shell.publish((on_edit)(true)); } else if let Some(f) = state.is_focused.as_mut().filter(|f| f.needs_update) { // TODO do we want to just move this to on_focus or on_unfocus for all inputs? f.needs_update = false; @@ -980,18 +937,6 @@ where self.drag_threshold, self.always_active, ); - - let state = tree.state.downcast_mut::(); - let value = if self.is_secure { - self.value.secure() - } else { - self.value.clone() - }; - state.scroll_offset = offset( - text_layout.children().next().unwrap().bounds(), - &value, - state, - ); } #[inline] @@ -1061,7 +1006,9 @@ where index += 1; } - if self.trailing_icon.is_some() { + if let (Some(trailing_icon), Some(tree)) = + (self.trailing_icon.as_ref(), state.children.get(index)) + { let mut children = layout.children(); children.next(); // skip if there is no leading icon @@ -1071,21 +1018,13 @@ where let trailing_icon_layout = children.next().unwrap(); if cursor_position.is_over(trailing_icon_layout.bounds()) { - if self.is_editable_variant { - return mouse::Interaction::Pointer; - } - - if let Some((trailing_icon, tree)) = - self.trailing_icon.as_ref().zip(state.children.get(index)) - { - return trailing_icon.as_widget().mouse_interaction( - tree, - layout, - cursor_position, - viewport, - renderer, - ); - } + return trailing_icon.as_widget().mouse_interaction( + tree, + layout, + cursor_position, + viewport, + renderer, + ); } } let mut children = layout.children(); @@ -1186,22 +1125,6 @@ pub fn select_all(id: Id) -> Task { task::effect(Action::widget(operation::text_input::select_all(id))) } -/// Produces a [`Task`] that selects a range of the content of the [`TextInput`] with the given -/// [`Id`]. -pub fn select_range(id: Id, start: usize, end: usize) -> Task { - task::effect(Action::widget(operation::text_input::select_range( - id, start, end, - ))) -} - -/// Produces a [`Task`] that selects from the front to the last occurrence of the given character -/// in the [`TextInput`] with the given [`Id`], or selects all if not found. -pub fn select_until_last(id: Id, value: &str, ch: char) -> Task { - let v = Value::new(value); - let end = v.rfind_char(ch).unwrap_or(v.len()); - select_range(id, 0, end) -} - /// Computes the layout of a [`TextInput`]. #[allow(clippy::cast_precision_loss)] #[allow(clippy::too_many_arguments)] @@ -1483,71 +1406,28 @@ pub fn update<'a, Message: Clone + 'static>( && edit_button_layout.is_some_and(|l| cursor.is_over(l.bounds())) { if is_editable_variant { - let has_content = !unsecured_value.is_empty(); - let is_editing = !state.is_read_only; + state.is_read_only = !state.is_read_only; + state.move_cursor_to_end(); - if is_editing && has_content { - if let Some(on_input) = on_input { - shell.publish((on_input)(String::new())); - } - - if manage_value { - *unsecured_value = Value::new(""); - state.tracked_value = unsecured_value.clone(); - - let cleared_value = if is_secure { - unsecured_value.secure() - } else { - unsecured_value.clone() - }; - - update_cache(state, &cleared_value); - } - - state.move_cursor_to_end(); - } else if is_editing { - // Close: toggle back to read-only and unfocus. - state.is_read_only = true; - state.unfocus(); - - if let Some(on_toggle_edit) = on_toggle_edit { - shell.publish(on_toggle_edit(false)); - } - } else { - // Edit: toggle to editing, select all, and focus. - state.is_read_only = false; - state.cursor.select_range(0, value.len()); - - if let Some(on_toggle_edit) = on_toggle_edit { - shell.publish(on_toggle_edit(true)); - } - - let now = Instant::now(); - LAST_FOCUS_UPDATE.with(|x| x.set(now)); - state.is_focused = Some(Focus { - updated_at: now, - now, - focused: true, - needs_update: false, - }); + if let Some(on_toggle_edit) = on_toggle_edit { + shell.publish(on_toggle_edit(!state.is_read_only)); } + + let now = Instant::now(); + LAST_FOCUS_UPDATE.with(|x| x.set(now)); + state.is_focused = Some(Focus { + updated_at: now, + now, + focused: true, + needs_update: false, + }); } shell.capture_event(); return; } - let target = { - let text_bounds = text_layout.bounds(); - - let alignment_offset = alignment_offset( - text_bounds.width, - state.value.raw().min_width(), - effective_alignment(state.value.raw()), - ); - - cursor_position.x - text_bounds.x - alignment_offset - }; + let target = cursor_position.x - text_layout.bounds().x; let click = mouse::Click::new(cursor_position, mouse::Button::Left, state.last_click); @@ -1557,7 +1437,7 @@ pub fn update<'a, Message: Clone + 'static>( click.kind(), state.cursor().state(value), ) { - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] (None, click::Kind::Single, cursor::State::Selection { start, end }) => { let left = start.min(end); let right = end.max(start); @@ -1566,30 +1446,17 @@ pub fn update<'a, Message: Clone + 'static>( state.value.raw(), text_layout.bounds(), left, - value, - state.cursor.affinity(), - state.scroll_offset, ); let (right_position, _right_offset) = measure_cursor_and_scroll_offset( state.value.raw(), text_layout.bounds(), right, - value, - state.cursor.affinity(), - state.scroll_offset, ); - let selection_start = left_position.min(right_position); - let width = (right_position - left_position).abs(); - let alignment_offset = alignment_offset( - text_layout.bounds().width, - state.value.raw().min_width(), - effective_alignment(state.value.raw()), - ); + let width = right_position - left_position; let selection_bounds = Rectangle { - x: text_layout.bounds().x + alignment_offset + selection_start - - state.scroll_offset, + x: text_layout.bounds().x + left_position, y: text_layout.bounds().y, width, height: text_layout.bounds().height, @@ -1617,28 +1484,14 @@ pub fn update<'a, Message: Clone + 'static>( if is_secure { state.cursor.select_all(value); } else { - let (position, affinity) = + let position = find_cursor_position(text_layout.bounds(), value, state, target) - .unwrap_or((0, text::Affinity::Before)); + .unwrap_or(0); - state.cursor.set_affinity(affinity); - - if let Some(delimiter) = state.double_click_select_delimiter { - if let Some(delim_pos) = value.rfind_char(delimiter) { - if position <= delim_pos { - state.cursor.select_range(0, delim_pos); - } else { - state.cursor.select_range(delim_pos + 1, value.len()); - } - } else { - state.cursor.select_all(value); - } - } else { - state.cursor.select_range( - value.previous_start_of_word(position), - value.next_end_of_word(position), - ); - } + state.cursor.select_range( + value.previous_start_of_word(position), + value.next_end_of_word(position), + ); } state.dragging_state = Some(DraggingState::Selection); } @@ -1653,18 +1506,15 @@ pub fn update<'a, Message: Clone + 'static>( } // Focus on click of the text input, and ensure that the input is writable. - if matches!(state.dragging_state, None | Some(DraggingState::Selection)) - && (!state.is_focused() || (is_editable_variant && state.is_read_only)) + if !state.is_focused() + && matches!(state.dragging_state, None | Some(DraggingState::Selection)) { - if !state.is_focused() { - if let Some(on_focus) = on_focus { - shell.publish(on_focus.clone()); - } + if let Some(on_focus) = on_focus { + shell.publish(on_focus.clone()); } if state.is_read_only { state.is_read_only = false; - state.cursor.select_range(0, value.len()); if let Some(on_toggle_edit) = on_toggle_edit { let message = (on_toggle_edit)(true); shell.publish(message); @@ -1698,22 +1548,12 @@ pub fn update<'a, Message: Clone + 'static>( | Event::Touch(touch::Event::FingerLifted { .. } | touch::Event::FingerLost { .. }) => { cold(); let state = state(); - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] if matches!(state.dragging_state, Some(DraggingState::PrepareDnd(_))) { // clear selection and place cursor at click position update_cache(state, value); if let Some(position) = cursor.position_over(layout.bounds()) { - let target = { - let text_bounds = text_layout.bounds(); - - let alignment_offset = alignment_offset( - text_bounds.width, - state.value.raw().min_width(), - effective_alignment(state.value.raw()), - ); - - position.x - text_bounds.x - alignment_offset - }; + let target = position.x - text_layout.bounds().x; state.setting_selection(value, text_layout.bounds(), target); } } @@ -1728,24 +1568,12 @@ pub fn update<'a, Message: Clone + 'static>( let state = state(); if matches!(state.dragging_state, Some(DraggingState::Selection)) { - let target = { - let text_bounds = text_layout.bounds(); - - let alignment_offset = alignment_offset( - text_bounds.width, - state.value.raw().min_width(), - effective_alignment(state.value.raw()), - ); - - position.x - text_bounds.x - alignment_offset - }; + let target = position.x - text_layout.bounds().x; update_cache(state, value); - let (position, affinity) = - find_cursor_position(text_layout.bounds(), value, state, target) - .unwrap_or((0, text::Affinity::Before)); + let position = + find_cursor_position(text_layout.bounds(), value, state, target).unwrap_or(0); - state.cursor.set_affinity(affinity); state .cursor .select_range(state.cursor.start(value), position); @@ -1753,7 +1581,7 @@ pub fn update<'a, Message: Clone + 'static>( shell.capture_event(); return; } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] if let Some(DraggingState::PrepareDnd(start_position)) = state.dragging_state { let distance = ((position.x - start_position.x).powi(2) + (position.y - start_position.y).powi(2)) @@ -1825,10 +1653,13 @@ pub fn update<'a, Message: Clone + 'static>( focus.updated_at = Instant::now(); LAST_FOCUS_UPDATE.with(|x| x.set(focus.updated_at)); - // Check if Ctrl/Command+A/C/V/X was pressed. - if state.keyboard_modifiers.command() { - match key.to_latin(*physical_key) { - Some('c') => { + // Check if Ctrl+A/C/V/X was pressed. + if state.keyboard_modifiers == keyboard::Modifiers::COMMAND + || state.keyboard_modifiers + == keyboard::Modifiers::COMMAND | keyboard::Modifiers::CAPS_LOCK + { + match key.as_ref() { + keyboard::Key::Character("c") | keyboard::Key::Character("C") => { if !is_secure { if let Some((start, end)) = state.cursor.selection(value) { clipboard.write( @@ -1840,7 +1671,7 @@ pub fn update<'a, Message: Clone + 'static>( } // XXX if we want to allow cutting of secure text, we need to // update the cache and decide which value to cut - Some('x') => { + keyboard::Key::Character("x") | keyboard::Key::Character("X") => { if !is_secure { if let Some((start, end)) = state.cursor.selection(value) { clipboard.write( @@ -1859,7 +1690,7 @@ pub fn update<'a, Message: Clone + 'static>( } } } - Some('v') => { + keyboard::Key::Character("v") | keyboard::Key::Character("V") => { let content = if let Some(content) = state.is_pasting.take() { content } else { @@ -1904,7 +1735,7 @@ pub fn update<'a, Message: Clone + 'static>( return; } - Some('a') => { + keyboard::Key::Character("a") | keyboard::Key::Character("A") => { state.cursor.select_all(value); shell.capture_event(); return; @@ -2021,23 +1852,29 @@ pub fn update<'a, Message: Clone + 'static>( update_cache(state, &value); } keyboard::Key::Named(keyboard::key::Named::ArrowLeft) => { - let rtl = state.value.raw().is_rtl(0).unwrap_or(false); - let by_words = platform::is_jump_modifier_pressed(modifiers) && !is_secure; - - if modifiers.shift() { - state.cursor.select_visual(false, by_words, rtl, value); + if platform::is_jump_modifier_pressed(modifiers) && !is_secure { + if modifiers.shift() { + state.cursor.select_left_by_words(value); + } else { + state.cursor.move_left_by_words(value); + } + } else if modifiers.shift() { + state.cursor.select_left(value); } else { - state.cursor.move_visual(false, by_words, rtl, value); + state.cursor.move_left(value); } } keyboard::Key::Named(keyboard::key::Named::ArrowRight) => { - let rtl = state.value.raw().is_rtl(0).unwrap_or(false); - let by_words = platform::is_jump_modifier_pressed(modifiers) && !is_secure; - - if modifiers.shift() { - state.cursor.select_visual(true, by_words, rtl, value); + if platform::is_jump_modifier_pressed(modifiers) && !is_secure { + if modifiers.shift() { + state.cursor.select_right_by_words(value); + } else { + state.cursor.move_right_by_words(value); + } + } else if modifiers.shift() { + state.cursor.select_right(value); } else { - state.cursor.move_visual(true, by_words, rtl, value); + state.cursor.move_right(value); } } keyboard::Key::Named(keyboard::key::Named::Home) => { @@ -2119,66 +1956,6 @@ pub fn update<'a, Message: Clone + 'static>( state.keyboard_modifiers = *modifiers; } - Event::InputMethod(event) => { - let state = state(); - - match event { - input_method::Event::Opened | input_method::Event::Closed => { - 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() { - state.preedit = Some(input_method::Preedit { - content: content.to_owned(), - selection: selection.clone(), - text_size: Some(size.into()), - }); - shell.capture_event(); - return; - } - } - input_method::Event::Commit(text) => { - let Some(focus) = state.is_focused.as_mut().filter(|f| f.focused) else { - return; - }; - let Some(on_input) = on_input else { - return; - }; - if state.is_read_only { - return; - } - - focus.updated_at = Instant::now(); - 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)); - - let contents = editor.contents(); - let unsecured_value = Value::new(&contents); - let message = if let Some(paste) = &on_paste { - (paste)(contents) - } else { - (on_input)(contents) - }; - shell.publish(message); - - state.is_pasting = None; - let value = if is_secure { - unsecured_value.secure() - } else { - unsecured_value - }; - - update_cache(state, &value); - shell.capture_event(); - return; - } - } - } Event::Window(window::Event::RedrawRequested(now)) => { let state = state(); @@ -2191,13 +1968,11 @@ pub fn update<'a, Message: Clone + 'static>( now.checked_add(Duration::from_millis(millis_until_redraw as u64)) .unwrap_or(*now), )); - - shell.request_input_method(&input_method(state, text_layout, unsecured_value)); } else if always_active { shell.request_redraw(); } } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] Event::Dnd(DndEvent::Source(SourceEvent::Finished | SourceEvent::Cancelled)) => { cold(); let state = state(); @@ -2208,7 +1983,7 @@ pub fn update<'a, Message: Clone + 'static>( return; } } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] Event::Dnd(DndEvent::Offer( rectangle, OfferEvent::Enter { @@ -2233,60 +2008,42 @@ pub fn update<'a, Message: Clone + 'static>( } } if accepted { - let target = { - let text_bounds = text_layout.bounds(); - - let alignment_offset = alignment_offset( - text_bounds.width, - state.value.raw().min_width(), - effective_alignment(state.value.raw()), - ); - - *x as f32 - text_bounds.x - alignment_offset - }; + let target = *x as f32 - text_layout.bounds().x; state.dnd_offer = DndOfferState::HandlingOffer(mime_types.clone(), DndAction::empty()); // existing logic for setting the selection - update_cache(state, value); - let (position, affinity) = + let position = if target > 0.0 { + update_cache(state, value); find_cursor_position(text_layout.bounds(), value, state, target) - .unwrap_or((0, text::Affinity::Before)); + } else { + None + }; - state.cursor.set_affinity(affinity); - state.cursor.move_to(position); + state.cursor.move_to(position.unwrap_or(0)); shell.capture_event(); return; } } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Motion { x, y })) if *rectangle == Some(dnd_id) => { let state = state(); - let target = { - let text_bounds = text_layout.bounds(); - - let alignment_offset = alignment_offset( - text_bounds.width, - state.value.raw().min_width(), - effective_alignment(state.value.raw()), - ); - - *x as f32 - text_bounds.x - alignment_offset - }; + let target = *x as f32 - text_layout.bounds().x; // existing logic for setting the selection - update_cache(state, value); - let (position, affinity) = + let position = if target > 0.0 { + update_cache(state, value); find_cursor_position(text_layout.bounds(), value, state, target) - .unwrap_or((0, text::Affinity::Before)); + } else { + None + }; - state.cursor.set_affinity(affinity); - state.cursor.move_to(position); + state.cursor.move_to(position.unwrap_or(0)); shell.capture_event(); return; } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Drop)) if *rectangle == Some(dnd_id) => { cold(); let state = state(); @@ -2304,9 +2061,9 @@ pub fn update<'a, Message: Clone + 'static>( return; } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] Event::Dnd(DndEvent::Offer(id, OfferEvent::LeaveDestination)) if Some(dnd_id) != *id => {} - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] Event::Dnd(DndEvent::Offer( rectangle, OfferEvent::Leave | OfferEvent::LeaveDestination, @@ -2324,7 +2081,7 @@ pub fn update<'a, Message: Clone + 'static>( shell.capture_event(); return; } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Data { data, mime_type })) if *rectangle == Some(dnd_id) => { @@ -2367,42 +2124,6 @@ pub fn update<'a, Message: Clone + 'static>( } } -fn input_method<'b>( - state: &'b State, - text_layout: Layout<'_>, - value: &Value, -) -> InputMethod<&'b str> { - if !state.is_focused() { - return InputMethod::Disabled; - }; - - let text_bounds = text_layout.bounds(); - let cursor_index = match state.cursor.state(value) { - cursor::State::Index(position) => position, - cursor::State::Selection { start, end } => start.min(end), - }; - let (cursor, offset) = measure_cursor_and_scroll_offset( - state.value.raw(), - text_bounds, - cursor_index, - value, - state.cursor.affinity(), - state.scroll_offset, - ); - InputMethod::Enabled { - cursor: Rectangle::new( - Point::new(text_bounds.x + cursor - offset, text_bounds.y), - Size::new(1.0, text_bounds.height), - ), - purpose: if state.is_secure { - input_method::Purpose::Secure - } else { - input_method::Purpose::Normal - }, - preedit: state.preedit.as_ref().map(input_method::Preedit::as_ref), - } -} - /// Draws the [`TextInput`] with the given [`Renderer`], overriding its /// [`Value`] if provided. /// @@ -2607,11 +2328,11 @@ pub fn draw<'a, Message>( let actual_width = text_width.max(text_bounds.width); let radius_0 = THEME.lock().unwrap().cosmic().corner_radii.radius_0.into(); - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] let handling_dnd_offer = !matches!(state.dnd_offer, DndOfferState::None); - #[cfg(not(all(feature = "wayland", target_os = "linux")))] + #[cfg(not(feature = "wayland"))] let handling_dnd_offer = false; - let (cursors, offset, is_selecting) = if let Some(focus) = + let (cursor, offset) = if let Some(focus) = state.is_focused.filter(|f| f.focused).or_else(|| { let now = Instant::now(); handling_dnd_offer.then_some(Focus { @@ -2623,78 +2344,27 @@ pub fn draw<'a, Message>( }) { match state.cursor.state(value) { cursor::State::Index(position) => { - let (text_value_width, _) = measure_cursor_and_scroll_offset( - state.value.raw(), - text_bounds, - position, - value, - state.cursor.affinity(), - state.scroll_offset, - ); + let (text_value_width, offset) = + measure_cursor_and_scroll_offset(state.value.raw(), text_bounds, position); let is_cursor_visible = handling_dnd_offer || ((focus.now - focus.updated_at).as_millis() / CURSOR_BLINK_INTERVAL_MILLIS) .is_multiple_of(2); - - if is_cursor_visible && !dnd_icon { - ( - vec![( - renderer::Quad { - bounds: Rectangle { - x: (text_bounds.x + text_value_width).floor(), - y: text_bounds.y, - width: 1.0, - height: text_bounds.height, - }, - border: Border { - width: 0.0, - color: Color::TRANSPARENT, - radius: radius_0, - }, - shadow: Shadow { - offset: Vector::ZERO, - color: Color::TRANSPARENT, - blur_radius: 0.0, - }, - snap: true, - }, - text_color, - )], - state.scroll_offset, - false, - ) - } else { - ( - Vec::<(renderer::Quad, Color)>::new(), - if dnd_icon { 0.0 } else { state.scroll_offset }, - false, - ) - } - } - cursor::State::Selection { start, end } => { - let left = start.min(end); - let right = end.max(start); - - if dnd_icon { - (Vec::<(renderer::Quad, Color)>::new(), 0.0, true) - } else { - let lo_byte = value.byte_index_at_grapheme(left); - let hi_byte = value.byte_index_at_grapheme(right); - - let rects = state.value.raw().highlight( - 0, - (lo_byte, text::Affinity::After), - (hi_byte, text::Affinity::Before), - ); - - let cursors: Vec<(renderer::Quad, Color)> = rects - .into_iter() - .map(|r| { - ( + if is_cursor_visible { + if dnd_icon { + (None, 0.0) + } else { + ( + Some(( renderer::Quad { bounds: Rectangle { - x: text_bounds.x + r.x, + x: text_bounds.x + text_value_width - offset + + if text_value_width < 0. { + actual_width + } else { + 0. + }, y: text_bounds.y, - width: r.width, + width: 1.0, height: text_bounds.height, }, border: Border { @@ -2709,49 +2379,81 @@ pub fn draw<'a, Message>( }, snap: true, }, - appearance.selected_fill, - ) - }) - .collect(); + text_color, + )), + offset, + ) + } + } else { + (None, offset) + } + } + cursor::State::Selection { start, end } => { + let left = start.min(end); + let right = end.max(start); - (cursors, state.scroll_offset, true) + let value_paragraph = &state.value; + let (left_position, left_offset) = + measure_cursor_and_scroll_offset(value_paragraph.raw(), text_bounds, left); + + let (right_position, right_offset) = + measure_cursor_and_scroll_offset(value_paragraph.raw(), text_bounds, right); + + let width = right_position - left_position; + if dnd_icon { + (None, 0.0) + } else { + ( + Some(( + renderer::Quad { + bounds: Rectangle { + x: text_bounds.x + + left_position + + if left_position < 0. || right_position < 0. { + actual_width + } else { + 0. + }, + y: text_bounds.y, + width, + height: text_bounds.height, + }, + border: Border { + width: 0.0, + color: Color::TRANSPARENT, + radius: radius_0, + }, + shadow: Shadow { + offset: Vector::ZERO, + color: Color::TRANSPARENT, + blur_radius: 0.0, + }, + snap: true, + }, + appearance.selected_fill, + )), + if end == right { + right_offset + } else { + left_offset + }, + ) } } } } else { - let unfocused_offset = match effective_alignment(state.value.raw()) { - alignment::Horizontal::Right => { - (state.value.raw().min_width() - text_bounds.width).max(0.0) - } - _ => 0.0, - }; - - ( - Vec::<(renderer::Quad, Color)>::new(), - unfocused_offset, - false, - ) + (None, 0.0) }; let render = |renderer: &mut crate::Renderer| { - let alignment_offset = alignment_offset( - text_bounds.width, - state.value.raw().min_width(), - effective_alignment(state.value.raw()), - ); - - if cursors.is_empty() { - renderer.with_translation(Vector::ZERO, |_| {}); + if let Some((cursor, color)) = cursor { + renderer.fill_quad(cursor, color); } else { - renderer.with_translation(Vector::new(alignment_offset - offset, 0.0), |renderer| { - for (quad, color) in &cursors { - renderer.fill_quad(*quad, *color); - } - }); + renderer.with_translation(Vector::ZERO, |_| {}); } let bounds = Rectangle { - x: text_bounds.x + alignment_offset - offset, + x: text_bounds.x - offset, y: text_bounds.center_y(), width: actual_width, ..text_bounds @@ -2772,7 +2474,7 @@ pub fn draw<'a, Message>( font, bounds: bounds.size(), size: iced::Pixels(size), - align_x: text::Alignment::Default, + align_x: text::Alignment::Left, align_y: alignment::Vertical::Center, line_height: text::LineHeight::default(), shaping: text::Shaping::Advanced, @@ -2785,8 +2487,6 @@ pub fn draw<'a, Message>( ); }; - // FIXME: we always must clip with a layer because of what appears to be a tiny-skia text clipping issue. - // Otherwise overflowing text escapes the bounds of the input. renderer.with_layer(text_bounds, render); let trailing_icon_tree = children.get(child_index); @@ -2859,7 +2559,7 @@ pub fn mouse_interaction( #[derive(Debug, Clone)] pub struct TextInputString(pub String); -#[cfg(all(feature = "wayland", target_os = "linux"))] +#[cfg(feature = "wayland")] impl AsMimeTypes for TextInputString { fn available(&self) -> Cow<'static, [String]> { Cow::Owned( @@ -2883,13 +2583,13 @@ impl AsMimeTypes for TextInputString { #[derive(Debug, Clone, PartialEq)] pub(crate) enum DraggingState { Selection, - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] PrepareDnd(Point), - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] Dnd(DndAction, String), } -#[cfg(all(feature = "wayland", target_os = "linux"))] +#[cfg(feature = "wayland")] #[derive(Debug, Default, Clone)] pub(crate) enum DndOfferState { #[default] @@ -2898,7 +2598,7 @@ pub(crate) enum DndOfferState { Dropped, } #[derive(Debug, Default, Clone)] -#[cfg(not(all(feature = "wayland", target_os = "linux")))] +#[cfg(not(feature = "wayland"))] pub(crate) struct DndOfferState; /// The state of a [`TextInput`]. @@ -2915,16 +2615,14 @@ pub struct State { pub is_read_only: bool, pub emit_unfocus: bool, select_on_focus: bool, - double_click_select_delimiter: Option, is_focused: Option, dragging_state: Option, dnd_offer: DndOfferState, is_pasting: Option, last_click: Option, cursor: Cursor, - preedit: Option, keyboard_modifiers: keyboard::Modifiers, - scroll_offset: f32, + // TODO: Add stateful horizontal scrolling offset } #[derive(Debug, Clone, Copy)] @@ -2974,7 +2672,7 @@ impl State { } } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] /// Returns the current value of the dragged text in the [`TextInput`]. #[must_use] pub fn dragged_text(&self) -> Option { @@ -2997,15 +2695,12 @@ impl State { emit_unfocus: false, is_focused: None, select_on_focus: false, - double_click_select_delimiter: None, dragging_state: None, dnd_offer: DndOfferState::default(), is_pasting: None, last_click: None, cursor: Cursor::default(), - preedit: None, keyboard_modifiers: keyboard::Modifiers::default(), - scroll_offset: 0.0, dirty: false, } } @@ -3087,18 +2782,14 @@ impl State { self.cursor.select_range(0, usize::MAX); } - /// Selects a range of the content of the [`TextInput`]. - #[inline] - pub fn select_range(&mut self, start: usize, end: usize) { - self.cursor.select_range(start, end); - } - pub(super) fn setting_selection(&mut self, value: &Value, bounds: Rectangle, target: f32) { - let (position, affinity) = find_cursor_position(bounds, value, self, target) - .unwrap_or((0, text::Affinity::Before)); + let position = if target > 0.0 { + find_cursor_position(bounds, value, self, target) + } else { + None + }; - self.cursor.set_affinity(affinity); - self.cursor.move_to(position); + self.cursor.move_to(position.unwrap_or(0)); self.dragging_state = Some(DraggingState::Selection); } } @@ -3151,9 +2842,8 @@ impl operation::TextInput for State { todo!() } - #[inline] fn select_range(&mut self, start: usize, end: usize) { - Self::select_range(self, start, end); + todo!() } } @@ -3162,33 +2852,14 @@ fn measure_cursor_and_scroll_offset( paragraph: &impl text::Paragraph, text_bounds: Rectangle, cursor_index: usize, - value: &Value, - affinity: text::Affinity, - current_offset: f32, ) -> (f32, f32) { - let byte_index = value.byte_index_at_grapheme(cursor_index); - let position = paragraph - .cursor_position(0, byte_index, affinity) + let grapheme_position = paragraph + .grapheme_position(0, cursor_index) .unwrap_or(Point::ORIGIN); - // The visible window in paragraph coordinates is: - // [current_offset, current_offset + text_bounds.width] - // Keep the cursor visible with a 5px margin on each side. - let offset = if position.x > current_offset + text_bounds.width - 5.0 { - // Cursor past right edge of visible window → scroll left - (position.x + 5.0) - text_bounds.width - } else if position.x < current_offset + 5.0 { - // Cursor past left edge of visible window → scroll right - position.x - 5.0 - } else { - // Cursor is within visible window → keep current scroll - current_offset - }; + let offset = ((grapheme_position.x + 5.0) - text_bounds.width).max(0.0); - let max_offset = (paragraph.min_width() - text_bounds.width).max(0.0); - let offset = offset.clamp(0.0, max_offset); - - (position.x, offset) + (grapheme_position.x, offset) } /// Computes the position of the text cursor at the given X coordinate of @@ -3199,23 +2870,23 @@ fn find_cursor_position( value: &Value, state: &State, x: f32, -) -> Option<(usize, text::Affinity)> { - let value_str = value.to_string(); +) -> Option { + let offset = offset(text_bounds, value, state); + let value = value.to_string(); - let hit = state.value.raw().hit_test(Point::new( - x + state.scroll_offset, - text_bounds.height / 2.0, - ))?; - let char_offset = hit.cursor(); - let affinity = hit.affinity(); + let char_offset = state + .value + .raw() + .hit_test(Point::new(x + offset, text_bounds.height / 2.0)) + .map(text::Hit::cursor)?; - let grapheme_count = unicode_segmentation::UnicodeSegmentation::graphemes( - &value_str[..char_offset.min(value_str.len())], - true, + Some( + unicode_segmentation::UnicodeSegmentation::graphemes( + &value[..char_offset.min(value.len())], + true, + ) + .count(), ) - .count(); - - Some((grapheme_count, affinity)) } #[inline(never)] @@ -3242,7 +2913,7 @@ fn replace_paragraph( content: value.to_string(), bounds, size: text_size, - align_x: text::Alignment::Default, + align_x: text::Alignment::Left, align_y: alignment::Vertical::Top, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::None, @@ -3275,48 +2946,11 @@ fn offset(text_bounds: Rectangle, value: &Value, state: &State) -> f32 { cursor::State::Selection { end, .. } => end, }; - let (_, offset) = measure_cursor_and_scroll_offset( - state.value.raw(), - text_bounds, - focus_position, - value, - state.cursor().affinity(), - state.scroll_offset, - ); + let (_, offset) = + measure_cursor_and_scroll_offset(state.value.raw(), text_bounds, focus_position); offset } else { - match effective_alignment(state.value.raw()) { - alignment::Horizontal::Right => { - (state.value.raw().min_width() - text_bounds.width).max(0.0) - } - _ => 0.0, - } - } -} - -#[inline(never)] -fn alignment_offset( - text_bounds_width: f32, - text_min_width: f32, - alignment: alignment::Horizontal, -) -> f32 { - if text_min_width > text_bounds_width { 0.0 - } else { - match alignment { - alignment::Horizontal::Left => 0.0, - alignment::Horizontal::Center => (text_bounds_width - text_min_width) / 2.0, - alignment::Horizontal::Right => text_bounds_width - text_min_width, - } - } -} - -#[inline(never)] -fn effective_alignment(paragraph: &impl text::Paragraph) -> alignment::Horizontal { - if paragraph.is_rtl(0).unwrap_or(false) { - alignment::Horizontal::Right - } else { - alignment::Horizontal::Left } } diff --git a/src/widget/text_input/value.rs b/src/widget/text_input/value.rs index 3f7b8d73..900aac0f 100644 --- a/src/widget/text_input/value.rs +++ b/src/widget/text_input/value.rs @@ -132,42 +132,11 @@ impl Value { graphemes: std::iter::repeat_n(String::from("•"), self.graphemes.len()).collect(), } } - - /// Converts a grapheme index to a byte index in the underlying string. - #[must_use] - pub fn byte_index_at_grapheme(&self, grapheme_index: usize) -> usize { - self.graphemes[..grapheme_index.min(self.graphemes.len())] - .iter() - .map(|g| g.len()) - .sum() - } - - /// Returns the grapheme index of the last occurrence of the given character, - /// searching from the end. - #[must_use] - pub fn rfind_char(&self, ch: char) -> Option { - let needle = ch.to_string(); - self.graphemes.iter().rposition(|g| g == &needle) - } - - /// Converts a byte index to a grapheme index. - #[must_use] - pub fn grapheme_index_at_byte(&self, byte_index: usize) -> usize { - let mut bytes = 0; - for (i, g) in self.graphemes.iter().enumerate() { - if bytes >= byte_index { - return i; - } - bytes += g.len(); - } - - self.graphemes.len() - } } -impl std::fmt::Display for Value { +impl ToString for Value { #[inline] - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(&self.graphemes.concat()) + fn to_string(&self) -> String { + self.graphemes.concat() } } diff --git a/src/widget/toaster/mod.rs b/src/widget/toaster/mod.rs index bafaa9f9..efd93a9d 100644 --- a/src/widget/toaster/mod.rs +++ b/src/widget/toaster/mod.rs @@ -34,10 +34,10 @@ pub fn toaster<'a, Message: Clone + 'static>( } = theme.cosmic().spacing; let make_toast = move |(id, toast): (ToastId, &'a Toast)| { - let row = row::with_capacity(2) + let row = row() .push(text(&toast.message)) .push( - row::with_capacity(2) + row() .push_maybe(toast.action.as_ref().map(|action| { button::text(&action.description).on_press((action.message)(id)) })) diff --git a/src/widget/toggler.rs b/src/widget/toggler.rs index b95b596e..12bb8950 100644 --- a/src/widget/toggler.rs +++ b/src/widget/toggler.rs @@ -2,18 +2,18 @@ use std::time::{Duration, Instant}; -use crate::{Element, anim}; +use crate::{Element, anim, iced_core::Border, iced_widget::toggler::Status}; use iced_core::{ - Border, Clipboard, Event, Layout, Length, Pixels, Rectangle, Shell, Size, Widget, alignment, - event, layout, mouse, + Clipboard, Event, Layout, Length, Pixels, Rectangle, Shell, Size, Widget, alignment, event, + layout, mouse, renderer::{self, Renderer}, - text, touch, + text, widget::{self, Tree, tree}, window, }; -use iced_widget::{Id, toggler::Status}; +use iced_widget::Id; -pub use iced_widget::toggler::{Catalog, Style}; +pub use crate::iced_widget::toggler::{Catalog, Style}; pub fn toggler<'a, Message>(is_checked: bool) -> Toggler<'a, Message> { Toggler::new(is_checked) @@ -161,10 +161,7 @@ impl<'a, Message> Widget for Toggler<'a, } fn state(&self) -> tree::State { - tree::State::new(State { - prev_toggled: self.is_toggled, - ..State::default() - }) + tree::State::new(State::default()) } fn id(&self) -> Option { @@ -203,7 +200,7 @@ impl<'a, Message> Widget for Toggler<'a, align_x: self.text_alignment, align_y: alignment::Vertical::Top, shaping: self.text_shaping, - wrapping: iced_core::text::Wrapping::default(), + wrapping: crate::iced_core::text::Wrapping::default(), ellipsize: self.ellipsize, }, ); @@ -241,23 +238,13 @@ impl<'a, Message> Widget for Toggler<'a, return; }; let state = tree.state.downcast_mut::(); - - // animate external changes - if state.prev_toggled != self.is_toggled { - state.anim.changed(self.duration); - shell.request_redraw(); - state.prev_toggled = self.is_toggled; - } - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { let mouse_over = cursor_position.is_over(layout.bounds()); if mouse_over { shell.publish((on_toggle)(!self.is_toggled)); state.anim.changed(self.duration); - state.prev_toggled = !self.is_toggled; shell.capture_event(); } } @@ -442,5 +429,4 @@ pub fn next_to_each_other( pub struct State { text: widget::text::State<::Paragraph>, anim: anim::State, - prev_toggled: bool, } diff --git a/src/widget/wrapper.rs b/src/widget/wrapper.rs index 133f9b87..73e476fa 100644 --- a/src/widget/wrapper.rs +++ b/src/widget/wrapper.rs @@ -93,8 +93,8 @@ impl Widget for RcElementWrapper { &mut self, tree: &mut tree::Tree, renderer: &crate::Renderer, - limits: &iced_core::layout::Limits, - ) -> iced_core::layout::Node { + limits: &crate::iced_core::layout::Limits, + ) -> crate::iced_core::layout::Node { self.element .with_data_mut(|e| e.as_widget_mut().layout(tree, renderer, limits)) } @@ -104,9 +104,9 @@ impl Widget for RcElementWrapper { tree: &tree::Tree, renderer: &mut crate::Renderer, theme: &crate::Theme, - style: &iced_core::renderer::Style, - layout: iced_core::Layout<'_>, - cursor: iced_core::mouse::Cursor, + style: &crate::iced_core::renderer::Style, + layout: crate::iced_core::Layout<'_>, + cursor: crate::iced_core::mouse::Cursor, viewport: &Rectangle, ) { self.element.with_data(move |e| { @@ -134,7 +134,7 @@ impl Widget for RcElementWrapper { fn operate( &mut self, state: &mut tree::Tree, - layout: iced_core::Layout<'_>, + layout: crate::iced_core::Layout<'_>, renderer: &crate::Renderer, operation: &mut dyn widget::Operation, ) { @@ -148,11 +148,11 @@ impl Widget for RcElementWrapper { &mut self, state: &mut tree::Tree, event: &crate::iced::Event, - layout: iced_core::Layout<'_>, - cursor: iced_core::mouse::Cursor, + layout: crate::iced_core::Layout<'_>, + cursor: crate::iced_core::mouse::Cursor, renderer: &crate::Renderer, - clipboard: &mut dyn iced_core::Clipboard, - shell: &mut iced_core::Shell<'_, M>, + clipboard: &mut dyn crate::iced_core::Clipboard, + shell: &mut crate::iced_core::Shell<'_, M>, viewport: &Rectangle, ) { self.element.with_data_mut(|e| { @@ -165,11 +165,11 @@ impl Widget for RcElementWrapper { fn mouse_interaction( &self, state: &tree::Tree, - layout: iced_core::Layout<'_>, - cursor: iced_core::mouse::Cursor, + layout: crate::iced_core::Layout<'_>, + cursor: crate::iced_core::mouse::Cursor, viewport: &Rectangle, renderer: &crate::Renderer, - ) -> iced_core::mouse::Interaction { + ) -> crate::iced_core::mouse::Interaction { self.element.with_data(|e| { e.as_widget() .mouse_interaction(state, layout, cursor, viewport, renderer) @@ -179,11 +179,11 @@ impl Widget for RcElementWrapper { fn overlay<'a>( &'a mut self, state: &'a mut tree::Tree, - layout: iced_core::Layout<'a>, + layout: crate::iced_core::Layout<'a>, renderer: &crate::Renderer, viewport: &Rectangle, - translation: iced_core::Vector, - ) -> Option> { + translation: crate::iced_core::Vector, + ) -> Option> { assert_eq!(self.element.thread_id, thread::current().id()); Rc::get_mut(&mut self.element.data).and_then(|e| { e.get_mut() @@ -203,9 +203,9 @@ impl Widget for RcElementWrapper { fn drag_destinations( &self, state: &tree::Tree, - layout: iced_core::Layout<'_>, + layout: crate::iced_core::Layout<'_>, renderer: &crate::Renderer, - dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, + dnd_rectangles: &mut crate::iced_core::clipboard::DndDestinationRectangles, ) { self.element.with_data_mut(|e| { e.as_widget_mut()