Compare commits

..

1 commit

Author SHA1 Message Date
Ashley Wulber
9f213013dc fix: if not in bounds, return default mouse interaction 2026-03-17 15:18:09 -04:00
101 changed files with 1153 additions and 3587 deletions

View file

@ -33,17 +33,16 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
test_args: features:
- --no-default-features --features "" # for cosmic-comp, don't remove! - "" # for cosmic-comp, don't remove!
- --no-default-features --features "winit_debug" - 'winit_debug'
- --no-default-features --features "winit_tokio" - 'winit_tokio'
- --no-default-features --features "winit" - winit
- --no-default-features --features "winit_wgpu" - winit_wgpu
- --no-default-features --features "wayland" - wayland
- --no-default-features --features "applet" - applet
- --no-default-features --features "desktop,smol" - desktop,smol
- --no-default-features --features "desktop,tokio" - desktop,tokio
- -p cosmic-theme
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- name: Checkout sources - name: Checkout sources
@ -67,7 +66,7 @@ jobs:
- name: Rust toolchain - name: Rust toolchain
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
- name: Test features - 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: env:
RUST_BACKTRACE: full RUST_BACKTRACE: full
@ -104,7 +103,7 @@ jobs:
run: sudo apt-get update; sudo apt-get install -y libxkbcommon-dev libwayland-dev run: sudo apt-get update; sudo apt-get install -y libxkbcommon-dev libwayland-dev
- name: Rust toolchain - name: Rust toolchain
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
- name: Check example - name: Test example
run: cargo check -p "${{ matrix.examples }}" run: cargo check -p "${{ matrix.examples }}"
env: env:
RUST_BACKTRACE: full RUST_BACKTRACE: full

View file

@ -7,6 +7,7 @@ on:
jobs: jobs:
pages: pages:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@ -14,20 +15,8 @@ jobs:
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
submodules: recursive 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 - name: Build documentation
run: | run: cargo doc --verbose --features tokio,winit
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 - name: Deploy documentation
uses: peaceiris/actions-gh-pages@v3 uses: peaceiris/actions-gh-pages@v3
with: with:

View file

@ -14,10 +14,9 @@ default = [
"a11y", "a11y",
"dbus-config", "dbus-config",
"x11", "x11",
"iced-wayland", "wayland",
"multi-window", "multi-window",
] ] # default = ["dbus-config", "multi-window", "a11y"]
advanced-shaping = ["iced/advanced-shaping"]
# Accessibility support # Accessibility support
a11y = ["iced/a11y", "iced_accessibility"] a11y = ["iced/a11y", "iced_accessibility"]
# Enable about widget # Enable about widget
@ -58,7 +57,6 @@ desktop = [
"process", "process",
"dep:cosmic-settings-config", "dep:cosmic-settings-config",
"dep:freedesktop-desktop-entry", "dep:freedesktop-desktop-entry",
"dep:image-extras",
"dep:mime", "dep:mime",
"dep:shlex", "dep:shlex",
"tokio?/io-util", "tokio?/io-util",
@ -82,21 +80,15 @@ tokio = [
] ]
# Tokio async runtime # Tokio async runtime
# Wayland window support # Wayland window support
iced-wayland = [ wayland = [
"ashpd?/wayland", "ashpd?/wayland",
"autosize", "autosize",
"iced_runtime/wayland",
"iced/wayland", "iced/wayland",
"iced_winit/wayland", "iced_winit/wayland",
"cctk",
"surface-message", "surface-message",
] ]
wayland = [
"iced-wayland",
"iced_runtime/cctk",
"iced_winit/cctk",
"iced_wgpu/cctk",
"iced/cctk",
"dep:cctk",
]
surface-message = [] surface-message = []
# multi-window support # multi-window support
multi-window = [] multi-window = []
@ -123,10 +115,10 @@ x11 = ["iced/x11", "iced_winit/x11"]
[dependencies] [dependencies]
apply = "0.3.0" 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-fs = { version = "2.2", optional = true }
async-std = { version = "1.13", 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 } cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "160b086", optional = true }
jiff = "0.2" jiff = "0.2"
cosmic-config = { path = "cosmic-config" } cosmic-config = { path = "cosmic-config" }
@ -139,21 +131,17 @@ i18n-embed = { version = "0.16.0", features = [
i18n-embed-fl = "0.10" i18n-embed-fl = "0.10"
rust-embed = "8.11.0" rust-embed = "8.11.0"
css-color = "0.2.8" css-color = "0.2.8"
derive_setters = "0.1.9" derive_setters = "0.1.8"
futures = "0.3" futures = "0.3"
image = { version = "0.25.10", default-features = false, features = [ image = { version = "0.25.9", default-features = false, features = [
"ico",
"jpeg", "jpeg",
"png", "png",
] } ] }
image-extras = { version = "0.1.0", default-features = false, features = [ libc = { version = "0.2.180", optional = true }
"xpm",
"xbm",
], optional = true }
libc = { version = "0.2.183", optional = true }
log = "0.4" log = "0.4"
mime = { version = "0.3.17", optional = true } mime = { version = "0.3.17", optional = true }
palette = "0.7.6" palette = "0.7.6"
raw-window-handle = "0.6"
rfd = { version = "0.16.0", default-features = false, features = [ rfd = { version = "0.16.0", default-features = false, features = [
"xdg-portal", "xdg-portal",
], optional = true } ], optional = true }
@ -163,25 +151,25 @@ slotmap = "1.1.1"
smol = { version = "2.0.2", optional = true } smol = { version = "2.0.2", optional = true }
thiserror = "2.0.18" thiserror = "2.0.18"
taffy = { version = "0.9.2", features = ["grid"] } 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" tracing = "0.1.44"
unicode-segmentation = "1.12" unicode-segmentation = "1.12"
url = "2.5.8" 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" float-cmp = "0.10.0"
# Enable DBus feature on Linux targets # Enable DBus feature on Linux targets
[target.'cfg(target_os = "linux")'.dependencies] [target.'cfg(target_os = "linux")'.dependencies]
cosmic-config = { path = "cosmic-config", features = ["dbus"] } cosmic-config = { path = "cosmic-config", features = ["dbus"] }
cosmic-settings-daemon = { git = "https://github.com/pop-os/dbus-settings-bindings" } 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-icons = { package = "cosmic-freedesktop-icons", git = "https://github.com/pop-os/freedesktop-icons" }
freedesktop-desktop-entry = { version = "0.8.1", optional = true } freedesktop-desktop-entry = { version = "0.8.1", optional = true }
shlex = { version = "1.3.0", 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. # Used to embed bundled icons for non-unix platforms.
phf = { version = "0.13.1", features = ["macros"] } phf = { version = "0.13.1", features = ["macros"] }
@ -254,4 +242,4 @@ exclude = ["iced"]
dirs = "6.0.0" dirs = "6.0.0"
[dev-dependencies] [dev-dependencies]
tempfile = "3.27.0" tempfile = "3.24.0"

View file

@ -3,9 +3,7 @@ use std::env;
fn main() { fn main() {
println!("cargo::rerun-if-changed=build.rs"); println!("cargo::rerun-if-changed=build.rs");
if env::var_os("CARGO_CFG_UNIX").is_none() if env::var_os("CARGO_CFG_UNIX").is_none() {
|| env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("macos")
{
generate_bundled_icons(); generate_bundled_icons();
} }
} }

View file

@ -11,9 +11,9 @@ subscription = ["iced_futures"]
[dependencies] [dependencies]
cosmic-settings-daemon = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true } 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" } 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" notify = "8.2.0"
ron = "0.12.0" ron = "0.12.0"
serde = "1.0.228" 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 } iced_futures = { path = "../iced/futures/", default-features = false, optional = true }
futures-util = { version = "0.3", optional = true } futures-util = { version = "0.3", optional = true }
dirs.workspace = 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 } async-std = { version = "1.13", optional = true }
tracing = "0.1" tracing = "0.1"
@ -30,4 +30,4 @@ tracing = "0.1"
xdg = "3.0" xdg = "3.0"
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
known-folders = "1.4.2" known-folders = "1.4.0"

View file

@ -22,7 +22,7 @@ serde_json = { version = "1.0.149", optional = true, features = [
"preserve_order", "preserve_order",
] } ] }
ron = "0.12.0" 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 = [ cosmic-config = { path = "../cosmic-config/", default-features = false, features = [
"subscription", "subscription",
"macro", "macro",
@ -30,10 +30,3 @@ cosmic-config = { path = "../cosmic-config/", default-features = false, features
configparser = "3.1.0" configparser = "3.1.0"
dirs.workspace = true dirs.workspace = true
thiserror = "2.0.18" thiserror = "2.0.18"
[dev-dependencies]
insta = "1.47.2"
[profile.dev.package]
insta.opt-level = 3
similar.opt-level = 3

View file

@ -986,19 +986,19 @@ impl ThemeBuilder {
let success = if let Some(success) = success { let success = if let Some(success) = success {
success.into_color() success.into_color()
} else { } else {
palette.as_ref().bright_green palette.as_ref().accent_green
}; };
let warning = if let Some(warning) = warning { let warning = if let Some(warning) = warning {
warning.into_color() warning.into_color()
} else { } else {
palette.as_ref().bright_orange palette.as_ref().accent_yellow
}; };
let destructive = if let Some(destructive) = destructive { let destructive = if let Some(destructive) = destructive {
destructive.into_color() destructive.into_color()
} else { } 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())); let text_steps_array = text_tint.map(|c| steps(c, NonZeroUsize::new(100).unwrap()));

View file

@ -46,10 +46,8 @@ impl Theme {
pub fn write_exports(&self) -> Result<(), OutputError> { pub fn write_exports(&self) -> Result<(), OutputError> {
let gtk_res = self.write_gtk4(); let gtk_res = self.write_gtk4();
let qt_res = self.write_qt(); let qt_res = self.write_qt();
let qt56ct_res = self.write_qt56ct();
gtk_res?; gtk_res?;
qt_res?; qt_res?;
qt56ct_res?;
Ok(()) Ok(())
} }
@ -58,10 +56,8 @@ impl Theme {
pub fn reset_exports() -> Result<(), OutputError> { pub fn reset_exports() -> Result<(), OutputError> {
let gtk_res = Theme::reset_gtk(); let gtk_res = Theme::reset_gtk();
let qt_res = Theme::reset_qt(); let qt_res = Theme::reset_qt();
let qt56ct_res = Theme::reset_qt56ct();
gtk_res?; gtk_res?;
qt_res?; qt_res?;
qt56ct_res?;
Ok(()) Ok(())
} }
} }

View file

@ -1,11 +1,8 @@
use crate::Theme; use crate::Theme;
use configparser::ini::Ini; use configparser::ini::Ini;
use palette::{Mix, Srgba, WithAlpha, blend::Compose, rgb::Rgba};
use std::{ use std::{
fs::{self, File}, fs::{self, File},
io::Write,
path::PathBuf, path::PathBuf,
vec,
}; };
use super::{OutputError, qt_settings_ini_style}; 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. /// Increment this value when changes to qt{5,6}ct.conf are needed.
/// If the config's version is outdated, we update several sections. /// If the config's version is outdated, we update several sections.
/// Otherwise, only the light/dark mode is updated. /// Otherwise, only the light/dark mode is updated.
const COSMIC_QT_VERSION: u64 = 2; const COSMIC_QT_VERSION: u64 = 1;
/// 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(())
}
/// Edits qt{5,6}ct.conf to use COSMIC styles if needed. /// Edits qt{5,6}ct.conf to use COSMIC styles if needed.
#[cold] #[cold]
@ -152,7 +39,7 @@ inactive_colors={}
.map_err(OutputError::Ini)? .map_err(OutputError::Ini)?
.unwrap_or_default(); .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" }; let icon_theme = if is_dark { "breeze-dark" } else { "breeze" };
ini.set( ini.set(
@ -204,48 +91,11 @@ inactive_colors={}
Ok(()) 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`: /// Returns the file paths of the form `~/.config/ct/ct.conf`:
/// e.g. `~/.config/qt6ct/qt6ct.conf`. /// e.g. `~/.config/qt6ct/qt6ct.conf`.
/// ///
/// The file and its parent directory are created if they don't exist. /// The file and its parent directory are created if they don't exist.
#[cold]
fn get_conf_path(ct: &str) -> Result<PathBuf, OutputError> { fn get_conf_path(ct: &str) -> Result<PathBuf, OutputError> {
assert!(ct == "qt5ct" || ct == "qt6ct");
let Some(mut config_dir) = dirs::config_dir() else { let Some(mut config_dir) = dirs::config_dir() else {
return Err(OutputError::MissingConfigDir); return Err(OutputError::MissingConfigDir);
}; };
@ -261,155 +111,4 @@ inactive_colors={}
Ok(file_path) 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<PathBuf, OutputError> {
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<palette::encoding::Srgb, u8> = 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);
}
} }

View file

@ -14,11 +14,10 @@ impl Theme {
/// Produces a color scheme ini file for Qt. /// Produces a color scheme ini file for Qt.
/// ///
/// Some high-level documentation for this file can be found at: /// 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] #[must_use]
#[cold] #[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 // Usually, disabled elements will have strongly reduced contrast and are often notably darker or lighter
let disabled_color_effects = IniColorEffects { let disabled_color_effects = IniColorEffects {
color: self.button.disabled, color: self.button.disabled,
@ -42,7 +41,7 @@ impl Theme {
let bg = self.background.base; let bg = self.background.base;
// the background container // the background container
let window_colors = IniColors { let view_colors = IniColors {
background_alternate: bg.mix(self.accent.base, 0.05), background_alternate: bg.mix(self.accent.base, 0.05),
background_normal: bg, background_normal: bg,
decoration_focus: self.accent_text_color(), decoration_focus: self.accent_text_color(),
@ -57,17 +56,16 @@ impl Theme {
foreground_visited: self.accent_text_color(), foreground_visited: self.accent_text_color(),
}; };
// components inside the background container // 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_alternate: self.background.component.base.mix(self.accent.base, 0.05),
background_normal: self.background.component.base, background_normal: self.background.component.base,
..window_colors ..view_colors
}; };
// selected text and items // selected text and items
let selection_colors = { let selection_colors = {
// selection colors are swapped to fix menu bar contrast let selected = self.background.component.selected;
let selected = self.background.component.selected_text; let selected_text = self.background.component.selected_text;
let selected_text = self.background.component.selected;
IniColors { IniColors {
background_alternate: selected.mix(bg, 0.5), background_alternate: selected.mix(bg, 0.5),
background_normal: selected, background_normal: selected,
@ -94,11 +92,8 @@ impl Theme {
let complementary_colors = { let complementary_colors = {
let dark = if self.is_dark { let dark = if self.is_dark {
self.clone() self.clone()
} else if cfg!(test) {
// For reproducible results in tests, use the default dark theme
Theme::dark_default()
} else { } else {
Theme::dark_config() Theme::light_config()
.ok() .ok()
.as_ref() .as_ref()
.and_then(|conf| Theme::get_entry(conf).ok()) .and_then(|conf| Theme::get_entry(conf).ok())
@ -121,10 +116,10 @@ impl Theme {
}; };
// headers in cosmic don't have a background // headers in cosmic don't have a background
let header_colors = &window_colors; let header_colors = &view_colors;
let header_colors_inactive = &window_colors; let header_colors_inactive = &view_colors;
// tool tips, "What's This" tips, and similar elements // 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 { let general_color_scheme = if self.is_dark {
"CosmicDark" "CosmicDark"
@ -203,7 +198,7 @@ widgetStyle=qt6ct-style
format_ini_colors(&tooltip_colors, bg), format_ini_colors(&tooltip_colors, bg),
format_ini_colors(&view_colors, bg), format_ini_colors(&view_colors, bg),
format_ini_colors(&window_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. /// Returns an `OutputError` if there is an error writing the colors file.
#[cold] #[cold]
pub fn write_qt(&self) -> Result<(), OutputError> { pub fn write_qt(&self) -> Result<(), OutputError> {
let kcolorscheme = self.as_kcolorscheme(); let colors = self.as_qt();
let file_path = Self::get_kcolorscheme_path(self.is_dark)?; let file_path = Self::get_qt_colors_path(self.is_dark)?;
let tmp_file_path = file_path.with_extension("colors.new"); let tmp_file_path = file_path.with_extension("colors.new");
// Write to tmp_file_path first, then move it to file_path // 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 mut tmp_file = File::create(&tmp_file_path).map_err(OutputError::Io)?;
let res = tmp_file let res = tmp_file
.write_all(kcolorscheme.as_bytes()) .write_all(colors.as_bytes())
.and_then(|_| tmp_file.flush()) .and_then(|_| tmp_file.flush())
.and_then(|_| std::fs::rename(&tmp_file_path, file_path)); .and_then(|_| std::fs::rename(&tmp_file_path, file_path));
if let Err(e) = res { if let Err(e) = res {
@ -250,7 +245,7 @@ widgetStyle=qt6ct-style
let kdeglobals_file = config_dir.join("kdeglobals"); let kdeglobals_file = config_dir.join("kdeglobals");
let mut kdeglobals_ini = Self::read_ini(&kdeglobals_file)?; 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)?; let src_ini = Self::read_ini(&src_file)?;
Self::backup_non_cosmic_kdeglobals(&kdeglobals_ini, &kdeglobals_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 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)?; let src_ini = Self::read_ini(&src_file)?;
for (section, key_value) in src_ini.get_map_ref() { for (section, key_value) in src_ini.get_map_ref() {
@ -308,8 +303,8 @@ widgetStyle=qt6ct-style
Ok(()) Ok(())
} }
/// Gets a path like `~/.local/share/color-schemes/CosmicDark.colors` /// Gets a path like `~/.config/color-schemes/CosmicDark.colors`
fn get_kcolorscheme_path(is_dark: bool) -> Result<PathBuf, OutputError> { pub fn get_qt_colors_path(is_dark: bool) -> Result<PathBuf, OutputError> {
let Some(mut data_dir) = dirs::data_dir() else { let Some(mut data_dir) = dirs::data_dir() else {
return Err(OutputError::MissingDataDir); 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);
}
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -145,6 +145,7 @@ pub fn is_valid_srgb(c: Srgba) -> bool {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use almost::equal;
use palette::{OklabHue, Srgba}; use palette::{OklabHue, Srgba};
use super::{is_valid_srgb, oklch_to_srgba_nearest_chroma}; use super::{is_valid_srgb, oklch_to_srgba_nearest_chroma};
@ -172,57 +173,57 @@ mod tests {
fn test_conversion_boundaries() { fn test_conversion_boundaries() {
let c1 = palette::Oklcha::new(0.0, 0.288, OklabHue::from_degrees(0.0), 1.0); let c1 = palette::Oklcha::new(0.0, 0.288, OklabHue::from_degrees(0.0), 1.0);
let srgb = oklch_to_srgba_nearest_chroma(c1); let srgb = oklch_to_srgba_nearest_chroma(c1);
almost::zero(srgb.red); equal(srgb.red, 0.0);
almost::zero(srgb.blue); equal(srgb.blue, 0.0);
almost::zero(srgb.green); equal(srgb.green, 0.0);
let c1 = palette::Oklcha::new(1.0, 0.288, OklabHue::from_degrees(0.0), 1.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); let srgb = oklch_to_srgba_nearest_chroma(c1);
almost::equal(srgb.red, 1.0); equal(srgb.red, 1.0);
almost::equal(srgb.blue, 1.0); equal(srgb.blue, 1.0);
almost::equal(srgb.green, 1.0); equal(srgb.green, 1.0);
} }
#[test] #[test]
fn test_conversion_colors() { fn test_conversion_colors() {
let c1 = palette::Oklcha::new(0.4608, 0.11111, OklabHue::new(57.31), 1.0); 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::<u8, u8>(); let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::<u8, u8>();
assert_eq!(srgb.red, 133); assert!(srgb.red == 133);
assert_eq!(srgb.green, 69); assert!(srgb.green == 69);
assert_eq!(srgb.blue, 0); assert!(srgb.blue == 0);
let c1 = palette::Oklcha::new(0.30, 0.08, OklabHue::new(35.0), 1.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::<u8, u8>(); let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::<u8, u8>();
assert_eq!(srgb.red, 78); assert!(srgb.red == 78);
assert_eq!(srgb.green, 27); assert!(srgb.green == 27);
assert_eq!(srgb.blue, 15); assert!(srgb.blue == 15);
let c1 = palette::Oklcha::new(0.757, 0.146, OklabHue::new(301.2), 1.0); 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::<u8, u8>(); let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::<u8, u8>();
assert_eq!(srgb.red, 192); assert!(srgb.red == 192);
assert_eq!(srgb.green, 153); assert!(srgb.green == 153);
assert_eq!(srgb.blue, 253); assert!(srgb.blue == 253);
} }
#[test] #[test]
fn test_conversion_fallback_colors() { fn test_conversion_fallback_colors() {
let c1 = palette::Oklcha::new(0.70, 0.284, OklabHue::new(35.0), 1.0); 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::<u8, u8>(); let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::<u8, u8>();
assert_eq!(srgb.red, 255); assert!(srgb.red == 255);
assert_eq!(srgb.green, 102); assert!(srgb.green == 103);
assert_eq!(srgb.blue, 65); assert!(srgb.blue == 65);
let c1 = palette::Oklcha::new(0.757, 0.239, OklabHue::new(301.2), 1.0); 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::<u8, u8>(); let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::<u8, u8>();
assert_eq!(srgb.red, 193); assert!(srgb.red == 193);
assert_eq!(srgb.green, 152); assert!(srgb.green == 152);
assert_eq!(srgb.blue, 255); assert!(srgb.blue == 255);
let c1 = palette::Oklcha::new(0.163, 0.333, OklabHue::new(141.0), 1.0); 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::<u8, u8>(); let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::<u8, u8>();
assert_eq!(srgb.red, 1); assert!(srgb.red == 1);
assert_eq!(srgb.green, 19); assert!(srgb.green == 19);
assert_eq!(srgb.blue, 0); assert!(srgb.blue == 0);
} }
} }

View file

@ -132,7 +132,7 @@ impl cosmic::Application for App {
fn view(&self) -> Element<'_, Self::Message> { fn view(&self) -> Element<'_, Self::Message> {
let show_about_button = widget::button::text("Show about").on_press(Message::ToggleAbout); let show_about_button = widget::button::text("Show about").on_press(Message::ToggleAbout);
let centered = cosmic::widget::container( let centered = cosmic::widget::container(
widget::column::with_capacity(1) widget::column()
.push(show_about_button) .push(show_about_button)
.width(Length::Fill) .width(Length::Fill)
.height(Length::Shrink) .height(Length::Shrink)

View file

@ -13,6 +13,6 @@ env_logger = "0.10.2"
log = "0.4.29" log = "0.4.29"
[dependencies.libcosmic] [dependencies.libcosmic]
path = "../../" git = "https://github.com/pop-os/libcosmic"
default-features = false default-features = false
features = ["applet-token"] features = ["applet-token"]

View file

@ -1,8 +1,8 @@
use cosmic::app::{Core, Task}; use cosmic::app::{Core, Task};
use cosmic::iced::core::window;
use cosmic::iced::window::Id; use cosmic::iced::window::Id;
use cosmic::iced::{Length, Rectangle}; use cosmic::iced::{Length, Rectangle};
use cosmic::iced_runtime::core::window;
use cosmic::surface::action::{app_popup, destroy_popup}; use cosmic::surface::action::{app_popup, destroy_popup};
use cosmic::widget::{dropdown::popup_dropdown, list_column, settings, toggler}; use cosmic::widget::{dropdown::popup_dropdown, list_column, settings, toggler};
use cosmic::Element; use cosmic::Element;
@ -159,7 +159,7 @@ impl cosmic::Application for Window {
"oops".into() "oops".into()
} }
fn style(&self) -> Option<cosmic::iced::theme::Style> { fn style(&self) -> Option<cosmic::iced_core::theme::Style> {
Some(cosmic::applet::style()) Some(cosmic::applet::style())
} }
} }

View file

@ -11,15 +11,15 @@ wayland = ["libcosmic/wayland"]
env_logger = "0.11" env_logger = "0.11"
[dependencies.libcosmic] [dependencies.libcosmic]
path = "../../" git = "https://github.com/pop-os/libcosmic"
features = [ features = [
"debug", "debug",
"winit", "winit",
"tokio", "tokio",
"xdg-portal", "xdg-portal",
"a11y", "a11y",
"wgpu",
"single-instance", "single-instance",
"surface-message", "surface-message",
"multi-window", "multi-window",
"wgpu",
] ]

View file

@ -82,7 +82,6 @@ pub enum Message {
Hi, Hi,
Hi2, Hi2,
Hi3, Hi3,
Tick,
} }
/// The [`App`] stores application-specific state. /// The [`App`] stores application-specific state.
@ -93,7 +92,6 @@ pub struct App {
input_2: String, input_2: String,
hidden: bool, hidden: bool,
keybinds: HashMap<KeyBind, Action>, keybinds: HashMap<KeyBind, Action>,
progress: f32,
} }
/// Implement [`cosmic::Application`] to integrate with COSMIC. /// Implement [`cosmic::Application`] to integrate with COSMIC.
@ -135,7 +133,6 @@ impl cosmic::Application for App {
input_2: String::new(), input_2: String::new(),
hidden: true, hidden: true,
keybinds: HashMap::new(), keybinds: HashMap::new(),
progress: 0.0,
}; };
let command = app.update_title(); let command = app.update_title();
@ -181,17 +178,10 @@ impl cosmic::Application for App {
Message::Hi3 => { Message::Hi3 => {
dbg!("hi 3"); dbg!("hi 3");
} }
Message::Tick => {
self.progress = (self.progress + 0.01) % 1.0;
}
} }
Task::none() Task::none()
} }
fn subscription(&self) -> iced::Subscription<Self::Message> {
iced::time::every(std::time::Duration::from_millis(64)).map(|_| Message::Tick)
}
/// Creates a view after each update. /// Creates a view after each update.
fn view(&self) -> Element<'_, Self::Message> { fn view(&self) -> Element<'_, Self::Message> {
let page_content = self let page_content = self
@ -200,7 +190,7 @@ impl cosmic::Application for App {
.map_or("No page selected", String::as_str); .map_or("No page selected", String::as_str);
let centered = widget::container( let centered = widget::container(
widget::column::with_capacity(14) widget::column()
.push(widget::text::body(page_content)) .push(widget::text::body(page_content))
.push( .push(
widget::text_input::text_input("", &self.input_1) widget::text_input::text_input("", &self.input_1)
@ -222,47 +212,6 @@ impl cosmic::Application for App {
.on_input(Message::Input2) .on_input(Message::Input2)
.on_clear(Message::Ignore), .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) .spacing(cosmic::theme::spacing().space_s)
.width(Length::Fill) .width(Length::Fill)
.height(Length::Shrink) .height(Length::Shrink)

View file

@ -85,6 +85,8 @@ impl cosmic::Application for App {
/// Creates a view after each update. /// Creates a view after each update.
fn view(&self) -> Element<'_, Self::Message> { fn view(&self) -> Element<'_, Self::Message> {
let mut content = cosmic::widget::column().spacing(12);
let calendar = cosmic::widget::calendar( let calendar = cosmic::widget::calendar(
&self.calendar_model, &self.calendar_model,
|date| Message::DateSelected(date), |date| Message::DateSelected(date),
@ -93,7 +95,9 @@ impl cosmic::Application for App {
Weekday::Sunday, Weekday::Sunday,
); );
let centered = cosmic::widget::container(calendar) content = content.push(calendar);
let centered = cosmic::widget::container(content)
.width(iced::Length::Fill) .width(iced::Length::Fill)
.height(iced::Length::Shrink) .height(iced::Length::Shrink)
.align_x(iced::Alignment::Center) .align_x(iced::Alignment::Center)

View file

@ -4,7 +4,7 @@
//! Application API example //! Application API example
use cosmic::app::{Core, Settings, Task}; use cosmic::app::{Core, Settings, Task};
use cosmic::iced::Size; use cosmic::iced_core::Size;
use cosmic::widget::menu; use cosmic::widget::menu;
use cosmic::{executor, iced, ApplicationExt, Element}; use cosmic::{executor, iced, ApplicationExt, Element};
use std::collections::HashMap; use std::collections::HashMap;

View file

@ -28,14 +28,13 @@ impl State {
column!( column!(
list_column().add(settings::item( list_column().add(settings::item(
"Bluetooth", "Bluetooth",
toggler(self.enabled).on_toggle(Message::Enable) toggler(None, self.enabled, Message::Enable)
)), )),
text("Now visible as \"TODO\", just kidding") text("Now visible as \"TODO\", just kidding")
) )
.spacing(8) .spacing(8)
.into(), .into(),
settings::section() settings::view_section("Devices")
.title("Devices")
.add(settings::item("No devices found", text(""))) .add(settings::item("No devices found", text("")))
.into(), .into(),
]) ])

View file

@ -258,13 +258,12 @@ impl State {
match self.tab_bar.active_data() { match self.tab_bar.active_data() {
None => panic!("no tab is active"), None => panic!("no tab is active"),
Some(DemoView::TabA) => settings::view_column(vec![ Some(DemoView::TabA) => settings::view_column(vec![
settings::section() settings::view_section("Debug")
.title("Debug")
.add(settings::item("Debug theme", choose_theme)) .add(settings::item("Debug theme", choose_theme))
.add(settings::item("Debug icon theme", choose_icon_theme)) .add(settings::item("Debug icon theme", choose_icon_theme))
.add(settings::item( .add(settings::item(
"Debug layout", "Debug layout",
toggler(window.debug).on_toggle(Message::Debug), toggler(None, window.debug, Message::Debug),
)) ))
.add(settings::item( .add(settings::item(
"Scaling Factor", "Scaling Factor",
@ -277,11 +276,10 @@ impl State {
.into(), .into(),
])) ]))
.into(), .into(),
settings::section() settings::view_section("Controls")
.title("Controls")
.add(settings::item( .add(settings::item(
"Toggler", "Toggler",
toggler(self.toggler_value).on_toggle(Message::TogglerToggled), toggler(None, self.toggler_value, Message::TogglerToggled),
)) ))
.add(settings::item( .add(settings::item(
"Pick List (TODO)", "Pick List (TODO)",
@ -301,12 +299,14 @@ impl State {
.add(settings::item( .add(settings::item(
"Progress", "Progress",
progress_bar(0.0..=100.0, self.slider_value) progress_bar(0.0..=100.0, self.slider_value)
.length(Length::Fixed(250.0)) .width(Length::Fixed(250.0))
.girth(Length::Fixed(4.0)), .height(Length::Fixed(4.0)),
)) ))
.add(settings::item_row(vec![checkbox(self.checkbox_value) .add(settings::item_row(vec![checkbox(
.label("Checkbox") "Checkbox",
.on_toggle(Message::CheckboxToggled) self.checkbox_value,
Message::CheckboxToggled,
)
.into()])) .into()]))
.add(settings::item( .add(settings::item(
format!( format!(
@ -354,7 +354,8 @@ impl State {
.width(Length::Shrink) .width(Length::Shrink)
.on_activate(Message::MultiSelection) .on_activate(Message::MultiSelection)
.apply(container) .apply(container)
.center_x(Length::Fill) .center_x()
.width(Length::Fill)
.into(), .into(),
text("Vertical With Spacing").into(), text("Vertical With Spacing").into(),
cosmic::iced::widget::row(vec![ cosmic::iced::widget::row(vec![
@ -423,12 +424,13 @@ impl State {
]) ])
.padding(0) .padding(0)
.into(), .into(),
Some(DemoView::TabC) => settings::view_column(vec![settings::section() Some(DemoView::TabC) => {
.title("Tab C") settings::view_column(vec![settings::view_section("Tab C")
.add(text("Nothing here yet").width(Length::Fill)) .add(text("Nothing here yet").width(Length::Fill))
.into()]) .into()])
.padding(0) .padding(0)
.into(), .into()
}
}, },
container(text("Background container with some text").size(24)) container(text("Background container with some text").size(24))
.layer(cosmic_theme::Layer::Background) .layer(cosmic_theme::Layer::Background)

View file

@ -147,8 +147,7 @@ impl State {
fn view_desktop_options<'a>(&'a self, window: &'a Window) -> Element<'a, Message> { fn view_desktop_options<'a>(&'a self, window: &'a Window) -> Element<'a, Message> {
settings::view_column(vec![ settings::view_column(vec![
window.parent_page_button(DesktopPage::DesktopOptions), window.parent_page_button(DesktopPage::DesktopOptions),
settings::section() settings::view_section("Super Key Action")
.title("Super Key Action")
.add(settings::item("Launcher", horizontal_space(Length::Fill))) .add(settings::item("Launcher", horizontal_space(Length::Fill)))
.add(settings::item("Workspaces", horizontal_space(Length::Fill))) .add(settings::item("Workspaces", horizontal_space(Length::Fill)))
.add(settings::item( .add(settings::item(
@ -156,34 +155,38 @@ impl State {
horizontal_space(Length::Fill), horizontal_space(Length::Fill),
)) ))
.into(), .into(),
settings::section() settings::view_section("Hot Corner")
.title("Hot Corner")
.add(settings::item( .add(settings::item(
"Enable top-left hot corner for Workspaces", "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(), .into(),
settings::section() settings::view_section("Top Panel")
.title("Top Panel")
.add(settings::item( .add(settings::item(
"Show Workspaces Button", "Show Workspaces Button",
toggler(self.show_workspaces_button).on_toggle(Message::ShowWorkspacesButton), toggler(
None,
self.show_workspaces_button,
Message::ShowWorkspacesButton,
),
)) ))
.add(settings::item( .add(settings::item(
"Show Applications Button", "Show Applications Button",
toggler(self.show_applications_button) toggler(
.on_toggle(Message::ShowApplicationsButton), None,
self.show_applications_button,
Message::ShowApplicationsButton,
),
)) ))
.into(), .into(),
settings::section() settings::view_section("Window Controls")
.title("Window Controls")
.add(settings::item( .add(settings::item(
"Show Minimize Button", "Show Minimize Button",
toggler(self.show_minimize_button).on_toggle(Message::ShowMinimizeButton), toggler(None, self.show_minimize_button, Message::ShowMinimizeButton),
)) ))
.add(settings::item( .add(settings::item(
"Show Maximize Button", "Show Maximize Button",
toggler(self.show_maximize_button).on_toggle(Message::ShowMaximizeButton), toggler(None, self.show_maximize_button, Message::ShowMaximizeButton),
)) ))
.into(), .into(),
]) ])
@ -242,12 +245,12 @@ impl State {
list_column() list_column()
.add(settings::item( .add(settings::item(
"Same background on all displays", "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("Background fit", text("TODO")))
.add(settings::item( .add(settings::item(
"Slideshow", "Slideshow",
toggler(self.slideshow).on_toggle(Message::Slideshow), toggler(None, self.slideshow, Message::Slideshow),
)) ))
.into(), .into(),
column(image_column).spacing(16).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> { fn view_desktop_workspaces<'a>(&'a self, window: &'a Window) -> Element<'a, Message> {
settings::view_column(vec![ settings::view_column(vec![
window.parent_page_button(DesktopPage::Wallpaper), window.parent_page_button(DesktopPage::Wallpaper),
settings::section() settings::view_section("Workspace Behavior")
.title("Workspace Behavior")
.add(settings::item( .add(settings::item(
"Dynamic workspaces", "Dynamic workspaces",
horizontal_space(Length::Fill), horizontal_space(Length::Fill),
@ -269,8 +271,7 @@ impl State {
horizontal_space(Length::Fill), horizontal_space(Length::Fill),
)) ))
.into(), .into(),
settings::section() settings::view_section("Multi-monitor Behavior")
.title("Multi-monitor Behavior")
.add(settings::item( .add(settings::item(
"Workspaces Span Displays", "Workspaces Span Displays",
horizontal_space(Length::Fill), horizontal_space(Length::Fill),

View file

@ -69,16 +69,14 @@ impl State {
list_column() list_column()
.add(settings::item("Device name", text("TODO"))) .add(settings::item("Device name", text("TODO")))
.into(), .into(),
settings::section() settings::view_section("Hardware")
.title("Hardware")
.add(settings::item("Hardware model", text("TODO"))) .add(settings::item("Hardware model", text("TODO")))
.add(settings::item("Memory", text("TODO"))) .add(settings::item("Memory", text("TODO")))
.add(settings::item("Processor", text("TODO"))) .add(settings::item("Processor", text("TODO")))
.add(settings::item("Graphics", text("TODO"))) .add(settings::item("Graphics", text("TODO")))
.add(settings::item("Disk Capacity", text("TODO"))) .add(settings::item("Disk Capacity", text("TODO")))
.into(), .into(),
settings::section() settings::view_section("Operating System")
.title("Operating System")
.add(settings::item("Operating system", text("TODO"))) .add(settings::item("Operating system", text("TODO")))
.add(settings::item( .add(settings::item(
"Operating system architecture", "Operating system architecture",
@ -87,8 +85,7 @@ impl State {
.add(settings::item("Desktop environment", text("TODO"))) .add(settings::item("Desktop environment", text("TODO")))
.add(settings::item("Windowing system", text("TODO"))) .add(settings::item("Windowing system", text("TODO")))
.into(), .into(),
settings::section() settings::view_section("Related settings")
.title("Related settings")
.add(settings::item("Get support", text("TODO"))) .add(settings::item("Get support", text("TODO")))
.into(), .into(),
]) ])

View file

@ -80,7 +80,7 @@ impl cosmic::Application for App {
/// Creates a view after each update. /// Creates a view after each update.
fn view(&self) -> Element<'_, Self::Message> { 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() { for (id, image) in self.images.iter().enumerate() {
content = content.push( content = content.push(

View file

@ -7,10 +7,10 @@ use std::collections::HashMap;
use std::{env, process}; use std::{env, process};
use cosmic::app::{Core, Settings, Task}; use cosmic::app::{Core, Settings, Task};
use cosmic::iced::alignment::{Horizontal, Vertical};
use cosmic::iced::keyboard::Key;
use cosmic::iced::window; 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::action::MenuAction;
use cosmic::widget::menu::key_bind::KeyBind; use cosmic::widget::menu::key_bind::KeyBind;
use cosmic::widget::menu::key_bind::Modifier; use cosmic::widget::menu::key_bind::Modifier;

View file

@ -2,9 +2,9 @@ use std::collections::HashMap;
use cosmic::{ use cosmic::{
app::Core, app::Core,
iced::core::{id, Alignment, Length, Point},
iced::widget::{column, container, scrollable, text},
iced::{self, event, window, Subscription}, iced::{self, event, window, Subscription},
iced_core::{id, Alignment, Length, Point},
iced_widget::{column, container, scrollable, text},
prelude::*, prelude::*,
widget::{button, header_bar}, widget::{button, header_bar},
}; };

View file

@ -6,7 +6,7 @@
use std::collections::HashMap; use std::collections::HashMap;
use cosmic::app::{Core, Settings, Task}; use cosmic::app::{Core, Settings, Task};
use cosmic::iced::Size; use cosmic::iced_core::Size;
use cosmic::widget::{menu, nav_bar}; use cosmic::widget::{menu, nav_bar};
use cosmic::{executor, iced, ApplicationExt, Element}; use cosmic::{executor, iced, ApplicationExt, Element};

View file

@ -6,7 +6,7 @@
use apply::Apply; use apply::Apply;
use cosmic::app::{Core, Settings, Task}; use cosmic::app::{Core, Settings, Task};
use cosmic::dialog::file_chooser::{self, FileFilter}; use cosmic::dialog::file_chooser::{self, FileFilter};
use cosmic::iced::Length; use cosmic::iced_core::Length;
use cosmic::widget::button; use cosmic::widget::button;
use cosmic::{executor, iced, ApplicationExt, Element}; use cosmic::{executor, iced, ApplicationExt, Element};
use std::sync::Arc; use std::sync::Arc;

View file

@ -64,7 +64,7 @@ impl cosmic::Application for App {
/// Creates a view after each update. /// Creates a view after each update.
fn view(&self) -> Element<'_, Self::Message> { fn view(&self) -> Element<'_, Self::Message> {
widget::Row::new().into() widget::row().into()
} }
} }

View file

@ -7,7 +7,7 @@ use std::collections::HashMap;
use chrono::Datelike; use chrono::Datelike;
use cosmic::app::{Core, Settings, Task}; use cosmic::app::{Core, Settings, Task};
use cosmic::iced::Size; use cosmic::iced_core::Size;
use cosmic::prelude::*; use cosmic::prelude::*;
use cosmic::widget::table; use cosmic::widget::table;
use cosmic::widget::{self, nav_bar}; use cosmic::widget::{self, nav_bar};

View file

@ -99,9 +99,7 @@ impl cosmic::Application for App {
let inline = cosmic::widget::inline_input("", &self.input).on_input(Message::Input); let inline = cosmic::widget::inline_input("", &self.input).on_input(Message::Input);
let column = cosmic::widget::column::with_capacity(2) let column = cosmic::widget::column().push(editable).push(inline);
.push(editable)
.push(inline);
let centered = cosmic::widget::container(column.width(200)) let centered = cosmic::widget::container(column.width(200))
.width(iced::Length::Fill) .width(iced::Length::Fill)

View file

@ -6,7 +6,7 @@ links = Links
developers = Entwickler(innen) developers = Entwickler(innen)
designers = Designer(innen) designers = Designer(innen)
artists = Künstler(innen) artists = Künstler(innen)
translators = Übersetzer(innen) translators = Übersetzer*innen
documenters = Dokumentierer(innen) documenters = Dokumentierer(innen)
# Calendar # Calendar
january = Januar { $year } january = Januar { $year }

View file

View file

@ -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

View file

@ -2,33 +2,26 @@ february = { $year }년 2월
close = 닫기 close = 닫기
documenters = 문서 작성자 documenters = 문서 작성자
november = { $year }년 11월 november = { $year }년 11월
friday = 금요일 friday = 금
tuesday = 화요일 tuesday = 화
may = { $year }년 5월 may = { $year }년 5월
wednesday = 수요일 wednesday = 수
april = { $year }년 4월 april = { $year }년 4월
monday = 월요일 monday = 월
translators = 번역가 translators = 번역가
artists = 아티스트 artists = 아티스트
license = 라이선스 license = 라이선스
december = { $year }년 12월 december = { $year }년 12월
sunday = 일요일 sunday = 일
links = 링크 links = 링크
march = { $year }년 3월 march = { $year }년 3월
june = { $year }년 6월 june = { $year }년 6월
saturday = 토요일 saturday = 토
august = { $year }년 8월 august = { $year }년 8월
developers = 개발자 developers = 개발자
july = { $year }년 7월 july = { $year }년 7월
thursday = 목요일 thursday = 목
september = { $year }년 9월 september = { $year }년 9월
designers = 디자이너 designers = 디자이너
october = { $year }년 10월 october = { $year }년 10월
january = { $year }년 1월 january = { $year }년 1월
mon = 월
tue = 화
wed = 수
thu = 목
fri = 금
sat = 토
sun = 일

View file

@ -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 月

2
iced

@ -1 +1 @@
Subproject commit 78caabba7ef91cd1030da6f70b41d266704ffece Subproject commit 7491547d7078c8bad54cf350b1276c7f32e50df5

View file

@ -5,7 +5,7 @@ use crate::surface;
use crate::theme::Theme; use crate::theme::Theme;
use crate::widget::nav_bar; use crate::widget::nav_bar;
use crate::{config::CosmicTk, keyboard_nav}; 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 cctk::sctk::reexports::csd_frame::{WindowManagerCapabilities, WindowState};
use cosmic_theme::ThemeMode; use cosmic_theme::ThemeMode;
@ -69,10 +69,10 @@ pub enum Action {
/// Updates the tracked window geometry. /// Updates the tracked window geometry.
WindowResize(iced::window::Id, f32, f32), WindowResize(iced::window::Id, f32, f32),
/// Tracks updates to window state. /// Tracks updates to window state.
#[cfg(all(feature = "wayland", target_os = "linux"))] #[cfg(feature = "wayland")]
WindowState(iced::window::Id, WindowState), WindowState(iced::window::Id, WindowState),
/// Capabilities the window manager supports /// Capabilities the window manager supports
#[cfg(all(feature = "wayland", target_os = "linux"))] #[cfg(feature = "wayland")]
WmCapabilities(iced::window::Id, WindowManagerCapabilities), WmCapabilities(iced::window::Id, WindowManagerCapabilities),
#[cfg(feature = "xdg-portal")] #[cfg(feature = "xdg-portal")]
DesktopSettings(crate::theme::portal::Desktop), DesktopSettings(crate::theme::portal::Desktop),

View file

@ -8,16 +8,16 @@ use std::sync::Arc;
use super::{Action, Application, ApplicationExt, Subscription}; use super::{Action, Application, ApplicationExt, Subscription};
use crate::theme::{THEME, Theme, ThemeType}; use crate::theme::{THEME, Theme, ThemeType};
use crate::{Core, Element, keyboard_nav}; 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 cctk::sctk::reexports::csd_frame::{WindowManagerCapabilities, WindowState};
use cosmic_theme::ThemeMode; 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; use iced::Application as IcedApplication;
#[cfg(all(feature = "wayland", target_os = "linux"))] #[cfg(feature = "wayland")]
use iced::event::wayland; use iced::event::wayland;
use iced::{Task, theme, window}; use iced::{Task, theme, window};
use iced_futures::event::listen_with; use iced_futures::event::listen_with;
#[cfg(all(feature = "wayland", target_os = "linux"))] #[cfg(feature = "wayland")]
use iced_winit::SurfaceIdWrapper; use iced_winit::SurfaceIdWrapper;
use palette::color_difference::EuclideanDistance; use palette::color_difference::EuclideanDistance;
@ -49,8 +49,8 @@ pub fn windowing_system() -> Option<WindowingSystem> {
WINDOWING_SYSTEM.get().copied() WINDOWING_SYSTEM.get().copied()
} }
fn init_windowing_system<M>(handle: window::raw_window_handle::WindowHandle) -> crate::Action<M> { fn init_windowing_system<M>(handle: raw_window_handle::WindowHandle) -> crate::Action<M> {
let raw = handle.as_ref(); let raw: &raw_window_handle::RawWindowHandle = handle.as_ref();
let system = match raw { let system = match raw {
window::raw_window_handle::RawWindowHandle::UiKit(_) => WindowingSystem::UiKit, window::raw_window_handle::RawWindowHandle::UiKit(_) => WindowingSystem::UiKit,
window::raw_window_handle::RawWindowHandle::AppKit(_) => WindowingSystem::AppKit, window::raw_window_handle::RawWindowHandle::AppKit(_) => WindowingSystem::AppKit,
@ -83,7 +83,7 @@ fn init_windowing_system<M>(handle: window::raw_window_handle::WindowHandle) ->
#[derive(Default)] #[derive(Default)]
pub struct Cosmic<App: Application> { pub struct Cosmic<App: Application> {
pub app: App, pub app: App,
#[cfg(all(feature = "wayland", target_os = "linux"))] #[cfg(feature = "wayland")]
pub surface_views: HashMap< pub surface_views: HashMap<
window::Id, window::Id,
( (
@ -138,7 +138,7 @@ where
) -> iced::Task<crate::Action<T::Message>> { ) -> iced::Task<crate::Action<T::Message>> {
#[cfg(feature = "surface-message")] #[cfg(feature = "surface-message")]
match _surface_message { match _surface_message {
#[cfg(all(feature = "wayland", target_os = "linux"))] #[cfg(feature = "wayland")]
crate::surface::Action::AppSubsurface(settings, view) => { crate::surface::Action::AppSubsurface(settings, view) => {
let Some(settings) = std::sync::Arc::try_unwrap(settings) let Some(settings) = std::sync::Arc::try_unwrap(settings)
.ok() .ok()
@ -168,7 +168,7 @@ where
iced_winit::commands::subsurface::get_subsurface(settings(&mut self.app)) 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) => { crate::surface::Action::Subsurface(settings, view) => {
let Some(settings) = std::sync::Arc::try_unwrap(settings) let Some(settings) = std::sync::Arc::try_unwrap(settings)
.ok() .ok()
@ -196,7 +196,7 @@ where
iced_winit::commands::subsurface::get_subsurface(settings()) iced_winit::commands::subsurface::get_subsurface(settings())
} }
} }
#[cfg(all(feature = "wayland", target_os = "linux"))] #[cfg(feature = "wayland")]
crate::surface::Action::AppPopup(settings, view) => { crate::surface::Action::AppPopup(settings, view) => {
let Some(settings) = std::sync::Arc::try_unwrap(settings) let Some(settings) = std::sync::Arc::try_unwrap(settings)
.ok() .ok()
@ -225,26 +225,15 @@ where
iced_winit::commands::popup::get_popup(settings(&mut self.app)) 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) => { crate::surface::Action::DestroyPopup(id) => {
iced_winit::commands::popup::destroy_popup(id) iced_winit::commands::popup::destroy_popup(id)
} }
#[cfg(all(feature = "wayland", target_os = "linux"))] #[cfg(feature = "wayland")]
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"))]
crate::surface::Action::DestroySubsurface(id) => { crate::surface::Action::DestroySubsurface(id) => {
iced_winit::commands::subsurface::destroy_subsurface(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::DestroyWindow(id) => iced::window::close(id),
crate::surface::Action::ResponsiveMenuBar { crate::surface::Action::ResponsiveMenuBar {
menu_bar, menu_bar,
@ -255,7 +244,7 @@ where
core.menu_bars.insert(menu_bar, (limits, size)); core.menu_bars.insert(menu_bar, (limits, size));
iced::Task::none() iced::Task::none()
} }
#[cfg(all(feature = "wayland", target_os = "linux"))] #[cfg(feature = "wayland")]
crate::surface::Action::Popup(settings, view) => { crate::surface::Action::Popup(settings, view) => {
let Some(settings) = std::sync::Arc::try_unwrap(settings) let Some(settings) = std::sync::Arc::try_unwrap(settings)
.ok() .ok()
@ -282,7 +271,7 @@ where
iced_winit::commands::popup::get_popup(settings()) iced_winit::commands::popup::get_popup(settings())
} }
} }
#[cfg(all(feature = "wayland", target_os = "linux"))] #[cfg(feature = "wayland")]
crate::surface::Action::AppWindow(id, settings, view) => { crate::surface::Action::AppWindow(id, settings, view) => {
let Some(settings) = std::sync::Arc::try_unwrap(settings).ok().and_then(|s| { let Some(settings) = std::sync::Arc::try_unwrap(settings).ok().and_then(|s| {
s.downcast::<Box<dyn Fn(&mut T) -> iced::window::Settings + Send + Sync>>() s.downcast::<Box<dyn Fn(&mut T) -> iced::window::Settings + Send + Sync>>()
@ -321,7 +310,7 @@ where
.discard() .discard()
} }
} }
#[cfg(all(feature = "wayland", target_os = "linux"))] #[cfg(feature = "wayland")]
crate::surface::Action::Window(id, settings, view) => { crate::surface::Action::Window(id, settings, view) => {
let Some(settings) = std::sync::Arc::try_unwrap(settings).ok().and_then(|s| { let Some(settings) = std::sync::Arc::try_unwrap(settings).ok().and_then(|s| {
s.downcast::<Box<dyn Fn() -> iced::window::Settings + Send + Sync>>() s.downcast::<Box<dyn Fn() -> 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::Focused) => return Some(Action::Focus(id)),
iced::Event::Window(window::Event::Unfocused) => return Some(Action::Unfocus(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)) => { iced::Event::PlatformSpecific(iced::event::PlatformSpecific::Wayland(event)) => {
match event { match event {
wayland::Event::Popup(wayland::PopupEvent::Done, _, id) wayland::Event::Popup(wayland::PopupEvent::Done, _, id)
@ -454,7 +443,7 @@ where
) => { ) => {
return Some(Action::SuggestedBounds(b)); return Some(Action::SuggestedBounds(b));
} }
#[cfg(all(feature = "wayland", target_os = "linux"))] #[cfg(feature = "wayland")]
wayland::Event::Window(iced::event::wayland::WindowEvent::WindowState( wayland::Event::Window(iced::event::wayland::WindowEvent::WindowState(
s, s,
)) => { )) => {
@ -571,7 +560,7 @@ where
#[cfg(feature = "multi-window")] #[cfg(feature = "multi-window")]
pub fn view(&self, id: window::Id) -> Element<'_, crate::Action<T::Message>> { pub fn view(&self, id: window::Id) -> Element<'_, crate::Action<T::Message>> {
#[cfg(all(feature = "wayland", target_os = "linux"))] #[cfg(feature = "wayland")]
if let Some((_, _, v)) = self.surface_views.get(&id) { if let Some((_, _, v)) = self.surface_views.get(&id) {
return v(&self.app); return v(&self.app);
} }
@ -622,7 +611,7 @@ impl<T: Application> Cosmic<T> {
fn cosmic_update(&mut self, message: Action) -> iced::Task<crate::Action<T::Message>> { fn cosmic_update(&mut self, message: Action) -> iced::Task<crate::Action<T::Message>> {
match message { match message {
Action::WindowMaximized(id, maximized) => { Action::WindowMaximized(id, maximized) => {
#[cfg(not(all(feature = "wayland", target_os = "linux")))] #[cfg(not(feature = "wayland"))]
if self if self
.app .app
.core() .core()
@ -652,7 +641,7 @@ impl<T: Application> Cosmic<T> {
}); });
} }
#[cfg(all(feature = "wayland", target_os = "linux"))] #[cfg(feature = "wayland")]
Action::WindowState(id, state) => { Action::WindowState(id, state) => {
if self if self
.app .app
@ -704,7 +693,7 @@ impl<T: Application> Cosmic<T> {
} }
} }
#[cfg(all(feature = "wayland", target_os = "linux"))] #[cfg(feature = "wayland")]
Action::WmCapabilities(id, capabilities) => { Action::WmCapabilities(id, capabilities) => {
if self if self
.app .app
@ -811,7 +800,7 @@ impl<T: Application> Cosmic<T> {
new_theme.theme_type.prefer_dark(prefer_dark); new_theme.theme_type.prefer_dark(prefer_dark);
cosmic_theme.set_theme(new_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() { if self.app.core().sync_window_border_radii_to_theme() {
use iced_runtime::platform_specific::wayland::CornerRadius; use iced_runtime::platform_specific::wayland::CornerRadius;
use iced_winit::platform_specific::commands::corner_radius::corner_radius; use iced_winit::platform_specific::commands::corner_radius::corner_radius;
@ -957,7 +946,7 @@ impl<T: Application> Cosmic<T> {
// Only apply update if the theme is set to load a system theme // Only apply update if the theme is set to load a system theme
if let ThemeType::System { .. } = cosmic_theme.theme_type { if let ThemeType::System { .. } = cosmic_theme.theme_type {
cosmic_theme.set_theme(new_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() { if self.app.core().sync_window_border_radii_to_theme() {
use iced_runtime::platform_specific::wayland::CornerRadius; use iced_runtime::platform_specific::wayland::CornerRadius;
use iced_winit::platform_specific::commands::corner_radius::corner_radius; use iced_winit::platform_specific::commands::corner_radius::corner_radius;
@ -1051,7 +1040,7 @@ impl<T: Application> Cosmic<T> {
// Unminimize window before requesting to activate it. // Unminimize window before requesting to activate it.
let mut task = iced_runtime::window::minimize(id, false); let mut task = iced_runtime::window::minimize(id, false);
#[cfg(all(feature = "wayland", target_os = "linux"))] #[cfg(feature = "wayland")]
{ {
task = task.chain( task = task.chain(
iced_winit::platform_specific::commands::activation::activate( iced_winit::platform_specific::commands::activation::activate(
@ -1062,7 +1051,7 @@ impl<T: Application> Cosmic<T> {
) )
} }
#[cfg(not(all(feature = "wayland", target_os = "linux")))] #[cfg(not(feature = "wayland"))]
{ {
task = task.chain(iced_runtime::window::gain_focus(id)); task = task.chain(iced_runtime::window::gain_focus(id));
} }
@ -1079,7 +1068,7 @@ impl<T: Application> Cosmic<T> {
*v == 0 *v == 0
}) { }) {
self.opened_surfaces.remove(&id); self.opened_surfaces.remove(&id);
#[cfg(all(feature = "wayland", target_os = "linux"))] #[cfg(feature = "wayland")]
self.surface_views.remove(&id); self.surface_views.remove(&id);
self.tracked_windows.remove(&id); self.tracked_windows.remove(&id);
} }
@ -1201,8 +1190,7 @@ impl<T: Application> Cosmic<T> {
#[cfg(all( #[cfg(all(
feature = "wayland", feature = "wayland",
feature = "multi-window", feature = "multi-window",
feature = "surface-message", feature = "surface-message"
target_os = "linux"
))] ))]
if let Some(( if let Some((
parent, parent,
@ -1247,7 +1235,7 @@ impl<T: Application> Cosmic<T> {
core.applet.suggested_bounds = b; core.applet.suggested_bounds = b;
} }
Action::Opened(id) => { Action::Opened(id) => {
#[cfg(all(feature = "wayland", target_os = "linux"))] #[cfg(feature = "wayland")]
if self.app.core().sync_window_border_radii_to_theme() { if self.app.core().sync_window_border_radii_to_theme() {
use iced_runtime::platform_specific::wayland::CornerRadius; use iced_runtime::platform_specific::wayland::CornerRadius;
use iced_winit::platform_specific::commands::corner_radius::corner_radius; use iced_winit::platform_specific::commands::corner_radius::corner_radius;
@ -1296,14 +1284,14 @@ impl<App: Application> Cosmic<App> {
pub fn new(app: App) -> Self { pub fn new(app: App) -> Self {
Self { Self {
app, app,
#[cfg(all(feature = "wayland", target_os = "linux"))] #[cfg(feature = "wayland")]
surface_views: HashMap::new(), surface_views: HashMap::new(),
tracked_windows: HashSet::new(), tracked_windows: HashSet::new(),
opened_surfaces: HashMap::new(), opened_surfaces: HashMap::new(),
} }
} }
#[cfg(all(feature = "wayland", target_os = "linux"))] #[cfg(feature = "wayland")]
/// Create a subsurface /// Create a subsurface
pub fn get_subsurface( pub fn get_subsurface(
&mut self, &mut self,
@ -1326,7 +1314,7 @@ impl<App: Application> Cosmic<App> {
get_subsurface(settings) get_subsurface(settings)
} }
#[cfg(all(feature = "wayland", target_os = "linux"))] #[cfg(feature = "wayland")]
/// Create a subsurface /// Create a subsurface
pub fn get_popup( pub fn get_popup(
&mut self, &mut self,
@ -1348,7 +1336,7 @@ impl<App: Application> Cosmic<App> {
get_popup(settings) get_popup(settings)
} }
#[cfg(all(feature = "wayland", target_os = "linux"))] #[cfg(feature = "wayland")]
/// Create a window surface /// Create a window surface
pub fn get_window( pub fn get_window(
&mut self, &mut self,

View file

@ -128,9 +128,6 @@ impl<A: crate::app::Application> BootFn<cosmic::Cosmic<A>, crate::Action<A::Mess
/// ///
/// Returns error on application failure. /// Returns error on application failure.
pub fn run<App: Application>(settings: Settings, flags: App::Flags) -> iced::Result { pub fn run<App: Application>(settings: Settings, flags: App::Flags) -> iced::Result {
#[cfg(feature = "desktop")]
image_extras::register();
#[cfg(all(target_env = "gnu", not(target_os = "windows")))] #[cfg(all(target_env = "gnu", not(target_os = "windows")))]
if let Some(threshold) = settings.default_mmap_threshold { if let Some(threshold) = settings.default_mmap_threshold {
crate::malloc::limit_mmap_threshold(threshold); crate::malloc::limit_mmap_threshold(threshold);
@ -197,9 +194,6 @@ where
App::Flags: CosmicFlags, App::Flags: CosmicFlags,
App::Message: Clone + std::fmt::Debug + Send + 'static, App::Message: Clone + std::fmt::Debug + Send + 'static,
{ {
#[cfg(feature = "desktop")]
image_extras::register();
use std::collections::HashMap; use std::collections::HashMap;
let activation_token = std::env::var("XDG_ACTIVATION_TOKEN").ok(); let activation_token = std::env::var("XDG_ACTIVATION_TOKEN").ok();
@ -748,8 +742,6 @@ impl<App: Application> ApplicationExt for App {
})); }));
let content: Element<_> = if content_container { let content: Element<_> = if content_container {
content_col content_col
.width(iced::Length::Fill)
.height(iced::Length::Fill)
.apply(|w| id_container(w, iced_core::id::Id::new("COSMIC_content_container"))) .apply(|w| id_container(w, iced_core::id::Id::new("COSMIC_content_container")))
.into() .into()
} else { } else {

View file

@ -16,7 +16,7 @@ pub struct Settings {
pub(crate) antialiasing: bool, pub(crate) antialiasing: bool,
/// Autosize the window to fit its contents /// Autosize the window to fit its contents
#[cfg(all(feature = "wayland", target_os = "linux"))] #[cfg(feature = "wayland")]
pub(crate) autosize: bool, pub(crate) autosize: bool,
/// Set the application to not create a main window /// Set the application to not create a main window
@ -80,7 +80,7 @@ impl Default for Settings {
fn default() -> Self { fn default() -> Self {
Self { Self {
antialiasing: true, antialiasing: true,
#[cfg(all(feature = "wayland", target_os = "linux"))] #[cfg(feature = "wayland")]
autosize: false, autosize: false,
no_main_window: false, no_main_window: false,
client_decorations: true, client_decorations: true,

View file

@ -6,6 +6,13 @@ use crate::{
Application, Element, Renderer, Application, Element, Renderer,
app::iced_settings, app::iced_settings,
cctk::sctk, 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}, theme::{self, Button, THEME, system_dark, system_light},
widget::{ widget::{
self, self,
@ -17,15 +24,8 @@ use crate::{
space::vertical, space::vertical,
}, },
}; };
pub use cosmic_panel_config; pub use cosmic_panel_config;
use cosmic_panel_config::{CosmicPanelBackground, PanelAnchor, PanelSize}; 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_core::{Padding, Shadow};
use iced_runtime::platform_specific::wayland::popup::{SctkPopupSettings, SctkPositioner}; use iced_runtime::platform_specific::wayland::popup::{SctkPopupSettings, SctkPositioner};
use iced_widget::Text; use iced_widget::Text;
@ -42,7 +42,7 @@ static AUTOSIZE_ID: LazyLock<iced::id::Id> =
static AUTOSIZE_MAIN_ID: LazyLock<iced::id::Id> = static AUTOSIZE_MAIN_ID: LazyLock<iced::id::Id> =
LazyLock::new(|| iced::id::Id::new("cosmic-applet-autosize-main")); LazyLock::new(|| iced::id::Id::new("cosmic-applet-autosize-main"));
static TOOLTIP_ID: LazyLock<crate::widget::Id> = LazyLock::new(|| iced::id::Id::new("subsurface")); static TOOLTIP_ID: LazyLock<crate::widget::Id> = LazyLock::new(|| iced::id::Id::new("subsurface"));
pub(crate) static TOOLTIP_WINDOW_ID: LazyLock<window::Id> = LazyLock::new(window::Id::unique); static TOOLTIP_WINDOW_ID: LazyLock<window::Id> = LazyLock::new(window::Id::unique);
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Context { pub struct Context {
@ -226,7 +226,7 @@ impl Context {
let symbolic = icon.symbolic; let symbolic = icon.symbolic;
let icon = widget::icon(icon) let icon = widget::icon(icon)
.class(if symbolic { .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()), color: Some(theme.cosmic().background.on.into()),
})) }))
} else { } else {

View file

@ -1,11 +1,11 @@
use crate::iced; use crate::iced;
use crate::iced_futures::futures;
use cctk::sctk::reexports::calloop; use cctk::sctk::reexports::calloop;
use futures::{ use futures::{
SinkExt, StreamExt, SinkExt, StreamExt,
channel::mpsc::{UnboundedReceiver, unbounded}, channel::mpsc::{UnboundedReceiver, unbounded},
}; };
use iced::Subscription; use iced::Subscription;
use iced_futures::futures;
use iced_futures::stream; use iced_futures::stream;
use std::{fmt::Debug, hash::Hash, thread::JoinHandle}; use std::{fmt::Debug, hash::Hash, thread::JoinHandle};

View file

@ -99,7 +99,7 @@ pub struct Core {
pub(crate) menu_bars: HashMap<crate::widget::Id, (Limits, Size)>, pub(crate) menu_bars: HashMap<crate::widget::Id, (Limits, Size)>,
#[cfg(all(feature = "wayland", target_os = "linux"))] #[cfg(feature = "wayland")]
pub(crate) sync_window_border_radii_to_theme: bool, pub(crate) sync_window_border_radii_to_theme: bool,
} }
@ -159,7 +159,7 @@ impl Default for Core {
main_window: None, main_window: None,
exit_on_main_window_closed: true, exit_on_main_window_closed: true,
menu_bars: HashMap::new(), menu_bars: HashMap::new(),
#[cfg(all(feature = "wayland", target_os = "linux"))] #[cfg(feature = "wayland")]
sync_window_border_radii_to_theme: true, 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? // 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) { pub fn set_sync_window_border_radii_to_theme(&mut self, sync: bool) {
self.sync_window_border_radii_to_theme = sync; 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 { pub fn sync_window_border_radii_to_theme(&self) -> bool {
self.sync_window_border_radii_to_theme self.sync_window_border_radii_to_theme
} }

View file

@ -789,7 +789,7 @@ pub async fn spawn_desktop_exec<S, I, K, V>(
}) })
.unwrap_or_else(|| String::from("cosmic-term")); .unwrap_or_else(|| String::from("cosmic-term"));
term_exec = format!("{term} -e {}", exec.as_ref()); term_exec = format!("{term} -- {}", exec.as_ref());
&term_exec &term_exec
} else { } else {
exec.as_ref() exec.as_ref()

View file

@ -19,6 +19,72 @@ impl<Message: 'static> ElementExt for crate::Element<'_, Message> {
} }
} }
/// Additional methods for the [`Column`] and [`Row`] widgets.
pub trait CollectionWidget<'a, Message: 'a>:
Widget<Message, crate::Theme, crate::Renderer>
where
Self: Sized,
{
/// Moves all the elements of `other` into `self`, leaving `other` empty.
#[must_use]
fn append<E>(self, other: &mut Vec<E>) -> Self
where
E: Into<crate::Element<'a, Message>>;
/// Appends all elements in an iterator to the widget.
#[must_use]
fn extend<E>(mut self, iterator: impl Iterator<Item = E>) -> Self
where
E: Into<crate::Element<'a, Message>>,
{
for item in iterator {
self = self.push(item.into());
}
self
}
/// Pushes an element into the widget.
#[must_use]
fn push(self, element: impl Into<crate::Element<'a, Message>>) -> Self;
/// Conditionally pushes an element to the widget.
#[must_use]
fn push_maybe(self, element: Option<impl Into<crate::Element<'a, Message>>>) -> 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<E>(self, other: &mut Vec<E>) -> Self
where
E: Into<crate::Element<'a, Message>>,
{
self.extend(other.drain(..).map(Into::into))
}
fn push(self, element: impl Into<crate::Element<'a, Message>>) -> Self {
self.push(element)
}
}
impl<'a, Message: 'a> CollectionWidget<'a, Message> for crate::widget::Row<'a, Message> {
fn append<E>(self, other: &mut Vec<E>) -> Self
where
E: Into<crate::Element<'a, Message>>,
{
self.extend(other.drain(..).map(Into::into))
}
fn push(self, element: impl Into<crate::Element<'a, Message>>) -> Self {
self.push(element)
}
}
pub trait ColorExt { pub trait ColorExt {
/// Combines color with background to create appearance of transparency. /// Combines color with background to create appearance of transparency.
#[must_use] #[must_use]

View file

@ -3,7 +3,6 @@
#![allow(clippy::module_name_repetitions)] #![allow(clippy::module_name_repetitions)]
#![cfg_attr(target_os = "redox", feature(lazy_cell))] #![cfg_attr(target_os = "redox", feature(lazy_cell))]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
/// Recommended default imports. /// Recommended default imports.
pub mod prelude { pub mod prelude {
@ -67,6 +66,29 @@ pub mod font;
#[doc(inline)] #[doc(inline)]
pub use iced; 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 icon_theme;
pub mod keyboard_nav; pub mod keyboard_nav;
@ -78,8 +100,7 @@ pub(crate) mod malloc;
#[cfg(all(feature = "process", not(windows)))] #[cfg(all(feature = "process", not(windows)))]
pub mod process; pub mod process;
#[doc(inline)] #[cfg(feature = "wayland")]
#[cfg(all(feature = "wayland", target_os = "linux"))]
pub use cctk; pub use cctk;
pub mod surface; pub mod surface;

View file

@ -9,25 +9,25 @@ use iced::window;
use std::{any::Any, sync::Arc}; use std::{any::Any, sync::Arc};
/// Used to produce a destroy popup message from within a widget. /// Used to produce a destroy popup message from within a widget.
#[cfg(all(feature = "wayland", target_os = "linux"))] #[cfg(feature = "wayland")]
#[must_use] #[must_use]
pub fn destroy_popup(id: iced_core::window::Id) -> Action { pub fn destroy_popup(id: iced_core::window::Id) -> Action {
Action::DestroyPopup(id) Action::DestroyPopup(id)
} }
#[cfg(all(feature = "wayland", target_os = "linux"))] #[cfg(feature = "wayland")]
#[must_use] #[must_use]
pub fn destroy_subsurface(id: iced_core::window::Id) -> Action { pub fn destroy_subsurface(id: iced_core::window::Id) -> Action {
Action::DestroySubsurface(id) Action::DestroySubsurface(id)
} }
#[cfg(all(feature = "wayland", target_os = "linux"))] #[cfg(feature = "wayland")]
#[must_use] #[must_use]
pub fn destroy_window(id: iced_core::window::Id) -> Action { pub fn destroy_window(id: iced_core::window::Id) -> Action {
Action::DestroyWindow(id) Action::DestroyWindow(id)
} }
#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] #[cfg(all(feature = "wayland", feature = "winit"))]
#[must_use] #[must_use]
pub fn app_window<App: Application>( pub fn app_window<App: Application>(
settings: impl Fn(&mut App) -> window::Settings + Send + Sync + 'static, settings: impl Fn(&mut App) -> window::Settings + Send + Sync + 'static,
@ -60,7 +60,7 @@ pub fn app_window<App: Application>(
} }
/// Used to create a window message from within a widget. /// 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] #[must_use]
pub fn simple_window<Message: 'static>( pub fn simple_window<Message: 'static>(
settings: impl Fn() -> window::Settings + Send + Sync + 'static, settings: impl Fn() -> window::Settings + Send + Sync + 'static,
@ -92,7 +92,7 @@ pub fn simple_window<Message: 'static>(
) )
} }
#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] #[cfg(all(feature = "wayland", feature = "winit"))]
#[must_use] #[must_use]
pub fn app_popup<App: Application>( pub fn app_popup<App: Application>(
settings: impl Fn(&mut App) -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings settings: impl Fn(&mut App) -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings
@ -126,7 +126,7 @@ pub fn app_popup<App: Application>(
} }
/// Used to create a subsurface message from within a widget. /// 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] #[must_use]
pub fn simple_subsurface<Message: 'static, V>( pub fn simple_subsurface<Message: 'static, V>(
settings: impl Fn() -> iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings settings: impl Fn() -> iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings
@ -155,7 +155,7 @@ pub fn simple_subsurface<Message: 'static, V>(
} }
/// Used to create a popup message from within a widget. /// 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] #[must_use]
pub fn simple_popup<Message: 'static>( pub fn simple_popup<Message: 'static>(
settings: impl Fn() -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings settings: impl Fn() -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings
@ -186,7 +186,7 @@ pub fn simple_popup<Message: 'static>(
) )
} }
#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] #[cfg(all(feature = "wayland", feature = "winit"))]
#[must_use] #[must_use]
pub fn subsurface<App: Application>( pub fn subsurface<App: Application>(
settings: impl Fn( settings: impl Fn(

View file

@ -36,8 +36,6 @@ pub enum Action {
), ),
/// Destroy a subsurface with a view function /// Destroy a subsurface with a view function
DestroyPopup(iced::window::Id), DestroyPopup(iced::window::Id),
/// Destroys the global tooltip popup subsurface
DestroyTooltipPopup,
/// Create a window with a view function accepting the App as a parameter /// Create a window with a view function accepting the App as a parameter
AppWindow( 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::Popup(arg0, arg1) => f.debug_tuple("Popup").field(arg0).field(arg1).finish(),
Self::DestroyPopup(arg0) => f.debug_tuple("DestroyPopup").field(arg0).finish(), Self::DestroyPopup(arg0) => f.debug_tuple("DestroyPopup").field(arg0).finish(),
Self::DestroyTooltipPopup => f.debug_tuple("DestroyTooltipPopup").finish(),
Self::ResponsiveMenuBar { Self::ResponsiveMenuBar {
menu_bar, menu_bar,
limits, limits,

View file

@ -307,7 +307,7 @@ impl DefaultStyle for Theme {
fn default_style(&self) -> Appearance { fn default_style(&self) -> Appearance {
let cosmic = self.cosmic(); let cosmic = self.cosmic();
Appearance { Appearance {
icon_color: cosmic.on_bg_color().into(), icon_color: cosmic.bg_color().into(),
background_color: cosmic.bg_color().into(), background_color: cosmic.bg_color().into(),
text_color: cosmic.on_bg_color().into(), text_color: cosmic.on_bg_color().into(),
} }

View file

@ -27,7 +27,7 @@ pub enum Button {
IconVertical, IconVertical,
Image, Image,
Link, Link,
ListItem([f32; 4]), ListItem,
MenuFolder, MenuFolder,
MenuItem, MenuItem,
MenuRoot, MenuRoot,
@ -148,8 +148,8 @@ pub fn appearance(
appearance.text_color = Some(component.on.into()); appearance.text_color = Some(component.on.into());
corner_radii = &cosmic.corner_radii.radius_s; corner_radii = &cosmic.corner_radii.radius_s;
} }
Button::ListItem(radii) => { Button::ListItem => {
corner_radii = radii; corner_radii = &[0.0; 4];
let (background, text, icon) = color(&cosmic.background.component); let (background, text, icon) = color(&cosmic.background.component);
if selected { if selected {
@ -197,7 +197,7 @@ impl Catalog for crate::Theme {
return active(focused, self); 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!( let text_color = if matches!(
style, style,
Button::Icon | Button::IconVertical | Button::HeaderBar Button::Icon | Button::IconVertical | Button::HeaderBar
@ -209,15 +209,7 @@ impl Catalog for crate::Theme {
}; };
(component.base.into(), text_color, text_color) (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 { fn disabled(&self, style: &Self::Class) -> Style {
@ -245,7 +237,7 @@ impl Catalog for crate::Theme {
return hovered(focused, self); return hovered(focused, self);
} }
let mut s = appearance( appearance(
self, self,
focused || matches!(style, Button::Image), focused || matches!(style, Button::Image),
selected, selected,
@ -264,15 +256,7 @@ impl Catalog for crate::Theme {
(component.hover.into(), text_color, text_color) (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 { fn pressed(&self, focused: bool, selected: bool, style: &Self::Class) -> Style {

View file

@ -43,7 +43,7 @@ pub mod application {
iced::theme::Style { iced::theme::Style {
background_color: cosmic.bg_color().into(), background_color: cosmic.bg_color().into(),
text_color: cosmic.on_bg_color().into(), text_color: cosmic.on_bg_color().into(),
icon_color: cosmic.on_bg_color().into(), icon_color: cosmic.bg_color().into(),
} }
} }
} }

View file

@ -32,7 +32,7 @@ mod text_input;
#[doc(inline)] #[doc(inline)]
pub use self::text_input::TextInput; pub use self::text_input::TextInput;
#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] #[cfg(all(feature = "wayland", feature = "winit"))]
pub mod tooltip; pub mod tooltip;
#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] #[cfg(all(feature = "wayland", feature = "winit"))]
pub use tooltip::Tooltip; pub use tooltip::Tooltip;

View file

@ -1,9 +1,8 @@
use crate::{ use crate::{
Apply, Element, fl, Apply, Element, fl,
iced::{Alignment, Length}, iced::{Alignment, Length},
widget::{self, list}, widget::{self, space},
}; };
use std::rc::Rc;
#[derive(Debug, Default, Clone, derive_setters::Setters)] #[derive(Debug, Default, Clone, derive_setters::Setters)]
#[setters(into, strip_option)] #[setters(into, strip_option)]
@ -48,40 +47,32 @@ pub struct About {
fn add_contributors(contributors: Vec<(&str, &str)>) -> Vec<(String, String)> { fn add_contributors(contributors: Vec<(&str, &str)>) -> Vec<(String, String)> {
contributors contributors
.into_iter() .into_iter()
.map(|(name, email)| (name.into(), format!("mailto:{email}"))) .map(|(name, email)| (name.to_string(), format!("mailto:{email}")))
.collect() .collect()
} }
macro_rules! set_contributors {
($field:ident, $doc:expr) => {
#[doc = $doc]
pub fn $field(mut self, contributors: impl Into<Vec<(&'a str, &'a str)>>) -> Self {
self.$field = add_contributors(contributors.into());
self
}
};
}
impl<'a> About { impl<'a> About {
/// Artists who contributed to the application. set_contributors!(artists, "Artists who contributed to the application.");
pub fn artists(mut self, contributors: impl Into<Vec<(&'a str, &'a str)>>) -> Self { set_contributors!(designers, "Designers who contributed to the application.");
self.artists = add_contributors(contributors.into()); set_contributors!(developers, "Developers who contributed to the application.");
self set_contributors!(
} documenters,
"Documenters who contributed to the application."
/// Designers who contributed to the application. );
pub fn designers(mut self, contributors: impl Into<Vec<(&'a str, &'a str)>>) -> Self { set_contributors!(
self.designers = add_contributors(contributors.into()); translators,
self "Translators who contributed to the application."
} );
/// Developers who contributed to the application.
pub fn developers(mut self, contributors: impl Into<Vec<(&'a str, &'a str)>>) -> Self {
self.developers = add_contributors(contributors.into());
self
}
/// Documenters who contributed to the application.
pub fn documenters(mut self, contributors: impl Into<Vec<(&'a str, &'a str)>>) -> Self {
self.documenters = add_contributors(contributors.into());
self
}
/// Translators who contributed to the application.
pub fn translators(mut self, contributors: impl Into<Vec<(&'a str, &'a str)>>) -> Self {
self.translators = add_contributors(contributors.into());
self
}
/// Links associated with the application. /// Links associated with the application.
pub fn links<K: Into<String>, V: Into<String>>( pub fn links<K: Into<String>, V: Into<String>>(
@ -105,23 +96,19 @@ pub fn about<'a, Message: Clone + 'static>(
space_xxs, space_m, .. space_xxs, space_m, ..
} = crate::theme::spacing(); } = crate::theme::spacing();
let svg_accent = Rc::new(|theme: &crate::Theme| widget::svg::Style { let section_button = |name: &'a str, url: &'a str| -> Element<'a, Message> {
color: Some(theme.cosmic().accent_text_color().into()), widget::row()
}); .push(widget::text(name))
.push(space::horizontal())
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))
.push_maybe( .push_maybe(
(!url.is_empty()).then_some( (!url.is_empty()).then_some(crate::widget::icon::from_name("link-symbolic").icon()),
widget::icon::from_name("link-symbolic")
.icon()
.class(crate::theme::Svg::Custom(svg_accent.clone())),
),
) )
.align_y(Alignment::Center) .align_y(Alignment::Center)
.apply(list::button) .apply(widget::button::custom)
.class(crate::theme::Button::Link)
.on_press(on_url_press(url)) .on_press(on_url_press(url))
.width(Length::Fill)
.into()
}; };
let section = |list: &'a Vec<(String, String)>, title: String| { 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 copyright = about.copyright.as_ref().map(widget::text::body);
let comments = about.comments.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(header)
.push_maybe(links_section) .push_maybe(links_section)
.push_maybe(developers_section) .push_maybe(developers_section)

View file

@ -170,7 +170,7 @@ where
shell: &mut Shell<'_, Message>, shell: &mut Shell<'_, Message>,
viewport: &Rectangle, viewport: &Rectangle,
) { ) {
#[cfg(all(feature = "wayland", target_os = "linux"))] #[cfg(feature = "wayland")]
if matches!( if matches!(
event, event,
Event::PlatformSpecific(event::PlatformSpecific::Wayland( Event::PlatformSpecific(event::PlatformSpecific::Wayland(

View file

@ -3,7 +3,10 @@
use super::{Builder, ButtonClass}; use super::{Builder, ButtonClass};
use crate::Element; use crate::Element;
use crate::widget::{icon::Handle, tooltip}; use crate::widget::{
icon::{self, Handle},
tooltip,
};
use apply::Apply; use apply::Apply;
use iced_core::{Alignment, Length, Padding, font::Weight, text::LineHeight, widget::Id}; use iced_core::{Alignment, Length, Padding, font::Weight, text::LineHeight, widget::Id};
use std::borrow::Cow; use std::borrow::Cow;
@ -130,7 +133,7 @@ impl<Message> Button<'_, Message> {
} }
impl<'a, Message: Clone + 'static> From<Button<'a, Message>> for Element<'a, Message> { impl<'a, Message: Clone + 'static> From<Button<'a, Message>> 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); let mut content = Vec::with_capacity(2);
content.push( content.push(

View file

@ -357,8 +357,6 @@ impl<'a, Message: 'a + Clone> Widget<Message, crate::Theme, crate::Renderer>
operation, operation,
); );
}); });
let state = tree.state.downcast_mut::<State>();
operation.focusable(Some(&self.id), layout.bounds(), state);
} }
fn update( fn update(

View file

@ -4,10 +4,10 @@
//! A widget that displays an interactive calendar. //! A widget that displays an interactive calendar.
use crate::fl; use crate::fl;
use crate::iced_core::{Alignment, Length};
use crate::widget::{button, column, grid, icon, row, text}; use crate::widget::{button, column, grid, icon, row, text};
use apply::Apply; use apply::Apply;
use iced::alignment::Vertical; use iced::alignment::Vertical;
use iced_core::{Alignment, Length};
use jiff::{ use jiff::{
ToSpan, ToSpan,
civil::{Date, Weekday}, civil::{Date, Weekday},
@ -212,7 +212,7 @@ where
let content_list = column::with_children([ let content_list = column::with_children([
row::with_children([ row::with_children([
column([date.into(), day.into()]).into(), column().push(date).push(day).into(),
crate::widget::space::horizontal() crate::widget::space::horizontal()
.width(Length::Fill) .width(Length::Fill)
.into(), .into(),

View file

@ -1,8 +1,13 @@
//! An expandable stack of cards //! An expandable stack of cards
use std::time::Duration; 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::{ use crate::{
anim, anim,
iced_core::{self, Border, Shadow},
widget::{ widget::{
button, button,
card::style::Style, card::style::Style,
@ -13,10 +18,6 @@ use crate::{
}; };
use float_cmp::approx_eq; use float_cmp::approx_eq;
use iced::widget; 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}; use iced_core::{widget::tree, window};
const ICON_SIZE: u16 = 16; const ICON_SIZE: u16 = 16;

View file

@ -4,6 +4,7 @@
//! Widgets for selecting colors with a color picker. //! Widgets for selecting colors with a color picker.
use std::borrow::Cow; use std::borrow::Cow;
use std::iter;
use std::rc::Rc; use std::rc::Rc;
use std::sync::LazyLock; use std::sync::LazyLock;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
@ -92,6 +93,8 @@ pub struct ColorPickerModel {
#[setters(skip)] #[setters(skip)]
active_color: palette::Hsv, active_color: palette::Hsv,
#[setters(skip)] #[setters(skip)]
save_next: Option<Color>,
#[setters(skip)]
input_color: String, input_color: String,
#[setters(skip)] #[setters(skip)]
applied_color: Option<Color>, applied_color: Option<Color>,
@ -125,6 +128,7 @@ impl ColorPickerModel {
.insert(move |b| b.text(rgb.clone())) .insert(move |b| b.text(rgb.clone()))
.build(), .build(),
active_color: hsv, active_color: hsv,
save_next: None,
input_color: color_to_string(hsv, true), input_color: color_to_string(hsv, true),
applied_color: initial, applied_color: initial,
fallback_color, 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<Message>(&mut self, update: ColorPickerUpdate) -> Task<Message> { pub fn update<Message>(&mut self, update: ColorPickerUpdate) -> Task<Message> {
match update { match update {
ColorPickerUpdate::ActiveColor(c) => { ColorPickerUpdate::ActiveColor(c) => {
self.must_clear_cache.store(true, Ordering::SeqCst); self.must_clear_cache.store(true, Ordering::SeqCst);
self.input_color = color_to_string(c, self.is_hex()); 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.active_color = c;
self.copied_at = None; self.copied_at = None;
} }
ColorPickerUpdate::AppliedColor | ColorPickerUpdate::ActionFinished => { ColorPickerUpdate::AppliedColor => {
let srgb = palette::Srgb::from_color(self.active_color); let srgb = palette::Srgb::from_color(self.active_color);
if let Some(applied_color) = self.applied_color.take() { 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.applied_color = Some(Color::from(srgb));
self.active = false; self.active = false;
@ -215,12 +215,21 @@ impl ColorPickerModel {
palette::Hsv::from_color(palette::Srgb::new(c.red, c.green, c.blue)); 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 => { ColorPickerUpdate::ToggleColorPicker => {
self.must_clear_cache.store(true, Ordering::SeqCst); self.must_clear_cache.store(true, Ordering::SeqCst);
self.active = !self.active; self.active = !self.active;
self.copied_at = None; self.copied_at = None;
} }
} };
Task::none() Task::none()
} }
@ -386,8 +395,7 @@ where
text_input("", self.input_color) text_input("", self.input_color)
.on_input(move |s| on_update(ColorPickerUpdate::Input(s))) .on_input(move |s| on_update(ColorPickerUpdate::Input(s)))
.on_paste(move |s| on_update(ColorPickerUpdate::Input(s))) .on_paste(move |s| on_update(ColorPickerUpdate::Input(s)))
.on_submit(move |_| on_update(ColorPickerUpdate::ActionFinished)) .on_submit(move |_| on_update(ColorPickerUpdate::AppliedColor))
// .on_unfocus(on_update(ColorPickerUpdate::ActionFinished)) Somehow this is called even when the field wasn't previously focused
.leading_icon( .leading_icon(
color_button( color_button(
None, None,

View file

@ -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. //! 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( #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))]
feature = "wayland",
target_os = "linux",
feature = "winit",
feature = "surface-message"
))]
use crate::app::cosmic::{WINDOWING_SYSTEM, WindowingSystem}; use crate::app::cosmic::{WINDOWING_SYSTEM, WindowingSystem};
use crate::widget::menu::{ use crate::widget::menu::{
self, CloseCondition, Direction, ItemHeight, ItemWidth, MenuBarState, PathHighlight, self, CloseCondition, Direction, ItemHeight, ItemWidth, MenuBarState, PathHighlight,
@ -32,7 +27,7 @@ pub fn context_menu<'a, Message: 'static + Clone>(
content: content.into(), content: content.into(),
context_menu: context_menu.map(|menus| { context_menu: context_menu.map(|menus| {
vec![menu::Tree::with_children( vec![menu::Tree::with_children(
crate::Element::from(crate::widget::Row::new()), crate::Element::from(crate::widget::row::<'static, Message>()),
menus, menus,
)] )]
}), }),
@ -64,12 +59,7 @@ pub struct ContextMenu<'a, Message> {
} }
impl<Message: Clone + 'static> ContextMenu<'_, Message> { impl<Message: Clone + 'static> ContextMenu<'_, Message> {
#[cfg(all( #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))]
feature = "wayland",
target_os = "linux",
feature = "winit",
feature = "surface-message"
))]
#[allow(clippy::too_many_lines)] #[allow(clippy::too_many_lines)]
fn create_popup( fn create_popup(
&mut self, &mut self,
@ -374,12 +364,7 @@ impl<Message: 'static + Clone> Widget<Message, crate::Theme, crate::Renderer>
state.active_root.clear(); state.active_root.clear();
state.open = false; state.open = false;
#[cfg(all( #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))]
feature = "wayland",
target_os = "linux",
feature = "winit",
feature = "surface-message"
))]
if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland))
&& let Some(id) = state.popup_id.remove(&self.window_id) && let Some(id) = state.popup_id.remove(&self.window_id)
{ {
@ -418,12 +403,7 @@ impl<Message: 'static + Clone> Widget<Message, crate::Theme, crate::Renderer>
state.open = true; state.open = true;
state.view_cursor = cursor; state.view_cursor = cursor;
}); });
#[cfg(all( #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))]
feature = "wayland",
target_os = "linux",
feature = "winit",
feature = "surface-message"
))]
if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) { if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) {
self.create_popup(layout, cursor, renderer, shell, viewport, state); self.create_popup(layout, cursor, renderer, shell, viewport, state);
} }
@ -442,7 +422,6 @@ impl<Message: 'static + Clone> Widget<Message, crate::Theme, crate::Renderer>
#[cfg(all( #[cfg(all(
feature = "wayland", feature = "wayland",
target_os = "linux",
feature = "winit", feature = "winit",
feature = "surface-message" feature = "surface-message"
))] ))]
@ -479,12 +458,7 @@ impl<Message: 'static + Clone> Widget<Message, crate::Theme, crate::Renderer>
_viewport: &iced::Rectangle, _viewport: &iced::Rectangle,
translation: Vector, translation: Vector,
) -> Option<iced_core::overlay::Element<'b, Message, crate::Theme, crate::Renderer>> { ) -> Option<iced_core::overlay::Element<'b, Message, crate::Theme, crate::Renderer>> {
#[cfg(all( #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))]
feature = "wayland",
target_os = "linux",
feature = "winit",
feature = "surface-message"
))]
if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland))
&& self.window_id != window::Id::NONE && self.window_id != window::Id::NONE
&& self.on_surface_action.is_some() && self.on_surface_action.is_some()

View file

@ -7,10 +7,7 @@ use iced::Vector;
use crate::{ use crate::{
Element, Element,
widget::{Id, Widget}, iced::{
};
use iced::{
Event, Length, Rectangle, Event, Length, Rectangle,
clipboard::{ clipboard::{
dnd::{self, DndAction, DndDestinationRectangle, DndEvent, OfferEvent}, dnd::{self, DndAction, DndDestinationRectangle, DndEvent, OfferEvent},
@ -19,10 +16,12 @@ use iced::{
event, event,
id::Internal, id::Internal,
mouse, overlay, mouse, overlay,
}; },
use iced_core::{ iced_core::{
self, Clipboard, Shell, layout, self, Clipboard, Shell, layout,
widget::{Tree, tree}, widget::{Tree, tree},
},
widget::{Id, Widget},
}; };
pub fn dnd_destination<'a, Message: 'static>( pub fn dnd_destination<'a, Message: 'static>(

View file

@ -4,16 +4,16 @@ use iced_core::{widget::Operation, window};
use crate::{ use crate::{
Element, Element,
widget::{Id, Widget, container}, iced::{
};
use iced::{
Event, Length, Point, Rectangle, Vector, Event, Length, Point, Rectangle, Vector,
clipboard::dnd::{DndAction, DndEvent, SourceEvent}, clipboard::dnd::{DndAction, DndEvent, SourceEvent},
event, mouse, overlay, event, mouse, overlay,
}; },
use iced_core::{ iced_core::{
self, Clipboard, Shell, layout, renderer, self, Clipboard, Shell, layout, renderer,
widget::{Tree, tree}, widget::{Tree, tree},
},
widget::{Id, Widget, container},
}; };
pub fn dnd_source< pub fn dnd_source<

View file

@ -50,7 +50,7 @@ pub fn popup_dropdown<
let dropdown: Dropdown<'_, S, Message, AppMessage> = let dropdown: Dropdown<'_, S, Message, AppMessage> =
Dropdown::new(selections.into(), selected, on_selected); 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); let dropdown = dropdown.with_popup(_parent_id, _on_surface_action, _map_action);
dropdown dropdown

View file

@ -60,7 +60,7 @@ where
action_map: Option<Arc<dyn Fn(Message) -> AppMessage + 'static + Send + Sync>>, action_map: Option<Arc<dyn Fn(Message) -> AppMessage + 'static + Send + Sync>>,
#[setters(strip_option)] #[setters(strip_option)]
window_id: Option<window::Id>, window_id: Option<window::Id>,
#[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] #[cfg(all(feature = "winit", feature = "wayland"))]
positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner,
} }
@ -96,14 +96,14 @@ where
text_line_height: text::LineHeight::Relative(1.2), text_line_height: text::LineHeight::Relative(1.2),
font: None, font: None,
window_id: 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(), positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner::default(),
on_surface_action: None, on_surface_action: None,
action_map: 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. /// Handle dropdown requests for popup creation.
/// Intended to be used with [`crate::app::message::get_popup`] /// Intended to be used with [`crate::app::message::get_popup`]
pub fn with_popup<NewAppMessage>( pub fn with_popup<NewAppMessage>(
@ -154,7 +154,7 @@ where
self self
} }
#[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] #[cfg(all(feature = "winit", feature = "wayland"))]
pub fn with_positioner( pub fn with_positioner(
mut self, mut self,
positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner,
@ -268,7 +268,7 @@ where
layout, layout,
cursor, cursor,
shell, shell,
#[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] #[cfg(all(feature = "winit", feature = "wayland"))]
self.positioner.clone(), self.positioner.clone(),
self.on_selected.clone(), self.on_selected.clone(),
self.selected, self.selected,
@ -346,7 +346,7 @@ where
viewport: &Rectangle, viewport: &Rectangle,
translation: Vector, translation: Vector,
) -> Option<overlay::Element<'b, Message, crate::Theme, crate::Renderer>> { ) -> Option<overlay::Element<'b, Message, crate::Theme, crate::Renderer>> {
#[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() { if self.window_id.is_some() || self.on_surface_action.is_some() {
return None; return None;
} }
@ -545,7 +545,7 @@ pub fn update<
layout: Layout<'_>, layout: Layout<'_>,
cursor: mouse::Cursor, cursor: mouse::Cursor,
shell: &mut Shell<'_, Message>, 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, positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner,
on_selected: Arc<dyn Fn(usize) -> Message + Send + Sync + 'static>, on_selected: Arc<dyn Fn(usize) -> Message + Send + Sync + 'static>,
selected: Option<usize>, selected: Option<usize>,
@ -571,7 +571,7 @@ pub fn update<
*hovered_guard = selected; *hovered_guard = selected;
let id = window::Id::unique(); let id = window::Id::unique();
state.popup_id = id; 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 if let Some(((on_surface_action, parent), action_map)) = on_surface_action
.as_ref() .as_ref()
.zip(_window_id) .zip(_window_id)
@ -658,7 +658,7 @@ pub fn update<
state.close_operation = false; state.close_operation = false;
state.is_open.store(false, Ordering::SeqCst); state.is_open.store(false, Ordering::SeqCst);
if is_open { 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 { if let Some(ref on_close) = on_surface_action {
shell.publish(on_close(surface::action::destroy_popup(state.popup_id))); 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 // 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. // bounds or on the drop-down, either way we close the overlay.
state.is_open.store(false, Ordering::Relaxed); 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 { if let Some(on_close) = on_surface_action {
shell.publish(on_close(surface::action::destroy_popup(state.popup_id))); 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`]. /// Returns the current menu widget of a [`Dropdown`].
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn menu_widget< pub fn menu_widget<

View file

@ -162,14 +162,9 @@ pub fn resolve<Message>(
}); });
}); });
let actual_height = nodes
.iter()
.map(|node| node.bounds().y + node.bounds().height)
.fold(0.0f32, f32::max);
let size = Size { let size = Size {
width: flex_layout.content_size.width, 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) Node::with_children(size, nodes)

View file

@ -14,10 +14,10 @@ use iced_core::image::Renderer as ImageRenderer;
use iced_core::mouse::Cursor; use iced_core::mouse::Cursor;
use iced_core::widget::{Tree, tree}; use iced_core::widget::{Tree, tree};
use iced_core::{ use iced_core::{
Clipboard, ContentFit, Element, Event, Layout, Length, Rectangle, Rotation, Shell, Size, Clipboard, ContentFit, Element, Event, Layout, Length, Rectangle, Shell, Size, Vector, Widget,
Widget, event, layout, renderer, window, event, layout, renderer, window,
}; };
use iced_widget::image::{self, FilterMethod, Handle}; use iced_widget::image::{self, Handle};
use image_rs::AnimationDecoder; use image_rs::AnimationDecoder;
use image_rs::codecs::gif::GifDecoder; use image_rs::codecs::gif::GifDecoder;
use image_rs::codecs::png::PngDecoder; use image_rs::codecs::png::PngDecoder;
@ -146,7 +146,7 @@ impl Frames {
match image_type { match image_type {
ImageType::Gif => Self::from_decoder(GifDecoder::new(io::Cursor::new(bytes))?), 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))?), ImageType::WebP => Self::from_decoder(WebPDecoder::new(io::Cursor::new(bytes))?),
} }
} }
@ -168,10 +168,10 @@ impl Frames {
let first = frames.first().cloned().unwrap(); let first = frames.first().cloned().unwrap();
let total_bytes = frames let total_bytes = frames
.iter() .iter()
.map(|f| match &f.handle { .map(|f| match f.handle.data() {
Handle::Path(..) => 0, iced_core::image::Handle::Path(..) => 0,
Handle::Bytes(_, b) => b.len(), iced_core::image::Handle::Bytes(_, b) => b.len(),
Handle::Rgba { pixels, .. } => pixels.len(), iced_core::image::Handle::Rgba { pixels, .. } => pixels.len(),
}) })
.sum::<usize>() .sum::<usize>()
.try_into() .try_into()
@ -324,11 +324,7 @@ where
&self.frames.first.handle, &self.frames.first.handle,
self.width, self.width,
self.height, self.height,
None,
self.content_fit, self.content_fit,
Rotation::default(),
false,
[0.0; 4],
) )
} }
@ -375,18 +371,37 @@ where
) { ) {
let state = tree.state.downcast_ref::<State>(); let state = tree.state.downcast_ref::<State>();
iced_widget::image::draw( // Pulled from iced_native::widget::<Image as Widget>::draw
renderer, //
layout, // TODO: export iced_native::widget::image::draw as standalone function
&state.current.frame.handle, {
None, let Size { width, height } = renderer.dimensions(&state.current.frame.handle);
iced_core::border::Radius::default(), let image_size = Size::new(width as f32, height as f32);
self.content_fit,
FilterMethod::default(), let bounds = layout.bounds();
Rotation::default(), let adjusted_fit = self.content_fit.fit(image_size, bounds.size());
1.0,
1.0, 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);
}
}
} }
} }

View file

@ -197,16 +197,7 @@ impl<'a, Message: Clone + 'static> Widget<Message, crate::Theme, crate::Renderer
); );
let start_width = start_node.size().width; let start_width = start_node.size().width;
let vcenter = |node: layout::Node, x: f32| -> layout::Node { let (center_node, center_x) = if let Some(center) = &mut self.center {
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_start = start_width + gap;
let slot_end = (width - end_width - gap).max(slot_start); let slot_end = (width - end_width - gap).max(slot_start);
let slot_width = slot_end - slot_start; let slot_width = slot_end - slot_start;
@ -226,8 +217,21 @@ impl<'a, Message: Clone + 'static> Widget<Message, crate::Theme, crate::Renderer
let ideal_x = (width - natural_width) / 2.0; let ideal_x = (width - natural_width) / 2.0;
let max_x = (width - end_width - gap - natural_width).max(slot_start); let max_x = (width - end_width - gap - natural_width).max(slot_start);
let center_x = ideal_x.clamp(slot_start, max_x); let center_x = ideal_x.clamp(slot_start, max_x);
(Some(node), center_x)
} else {
(None, 0.0)
};
child_nodes.push(vcenter(node, center_x)) 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(cn) = center_node {
child_nodes.push(vcenter(cn, center_x));
} }
layout::Node::with_children(Size::new(width, height), child_nodes) layout::Node::with_children(Size::new(width, height), child_nodes)
@ -243,13 +247,10 @@ impl<'a, Message: Clone + 'static> Widget<Message, crate::Theme, crate::Renderer
cursor: iced_core::mouse::Cursor, cursor: iced_core::mouse::Cursor,
viewport: &iced_core::Rectangle, viewport: &iced_core::Rectangle,
) { ) {
self.elems() for ((e, s), l) in self.elems().zip(&tree.children).zip(layout.children()) {
.zip(&tree.children)
.zip(layout.children())
.for_each(|((e, s), l)| {
e.as_widget() e.as_widget()
.draw(s, renderer, theme, style, l, cursor, viewport); .draw(s, renderer, theme, style, l, cursor, viewport);
}); }
} }
fn update( fn update(
@ -263,13 +264,14 @@ impl<'a, Message: Clone + 'static> Widget<Message, crate::Theme, crate::Renderer
shell: &mut iced_core::Shell<'_, Message>, shell: &mut iced_core::Shell<'_, Message>,
viewport: &iced_core::Rectangle, viewport: &iced_core::Rectangle,
) { ) {
self.elems_mut() for ((e, s), l) in self
.elems_mut()
.zip(&mut state.children) .zip(&mut state.children)
.zip(layout.children()) .zip(layout.children())
.for_each(|((e, s), l)| { {
e.as_widget_mut() e.as_widget_mut()
.update(s, event, l, cursor, renderer, clipboard, shell, viewport); .update(s, event, l, cursor, renderer, clipboard, shell, viewport);
}); }
} }
fn mouse_interaction( fn mouse_interaction(
@ -298,12 +300,13 @@ impl<'a, Message: Clone + 'static> Widget<Message, crate::Theme, crate::Renderer
renderer: &crate::Renderer, renderer: &crate::Renderer,
operation: &mut dyn iced_core::widget::Operation<()>, operation: &mut dyn iced_core::widget::Operation<()>,
) { ) {
self.elems_mut() for ((e, s), l) in self
.elems_mut()
.zip(&mut state.children) .zip(&mut state.children)
.zip(layout.children()) .zip(layout.children())
.for_each(|((e, s), l)| { {
e.as_widget_mut().operate(s, l, renderer, operation); e.as_widget_mut().operate(s, l, renderer, operation);
}); }
} }
fn overlay<'b>( fn overlay<'b>(
@ -314,13 +317,27 @@ impl<'a, Message: Clone + 'static> Widget<Message, crate::Theme, crate::Renderer
viewport: &iced_core::Rectangle, viewport: &iced_core::Rectangle,
translation: Vector, translation: Vector,
) -> Option<iced_core::overlay::Element<'b, Message, crate::Theme, crate::Renderer>> { ) -> Option<iced_core::overlay::Element<'b, Message, crate::Theme, crate::Renderer>> {
self.elems_mut() let mut layouts = layout.children();
.zip(&mut state.children) let mut try_overlay = |elem: &'b mut Element<'a, Message>,
.zip(layout.children()) state: &'b mut tree::Tree|
.find_map(|((e, s), l)| { -> Option<
e.as_widget_mut() iced_core::overlay::Element<'b, Message, crate::Theme, crate::Renderer>,
.overlay(s, l, renderer, viewport, translation) > {
}) elem.as_widget_mut()
.overlay(state, layouts.next()?, renderer, viewport, translation)
};
if let Some(center) = &mut self.center {
let (start_slice, end_center) = state.children.split_at_mut(1);
let (end_slice, center_slice) = end_center.split_at_mut(1);
try_overlay(&mut self.start, &mut start_slice[0])
.or_else(|| try_overlay(&mut self.end, &mut end_slice[0]))
.or_else(|| try_overlay(center, &mut center_slice[0]))
} else {
let (start_slice, end_slice) = state.children.split_at_mut(1);
try_overlay(&mut self.start, &mut start_slice[0])
.or_else(|| try_overlay(&mut self.end, &mut end_slice[0]))
}
} }
fn drag_destinations( fn drag_destinations(
@ -330,13 +347,10 @@ impl<'a, Message: Clone + 'static> Widget<Message, crate::Theme, crate::Renderer
renderer: &crate::Renderer, renderer: &crate::Renderer,
dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles,
) { ) {
self.elems() for ((e, s), l) in self.elems().zip(&state.children).zip(layout.children()) {
.zip(&state.children)
.zip(layout.children())
.for_each(|((e, s), l)| {
e.as_widget() e.as_widget()
.drag_destinations(s, l, renderer, dnd_rectangles); .drag_destinations(s, l, renderer, dnd_rectangles);
}); }
} }
#[cfg(feature = "a11y")] #[cfg(feature = "a11y")]
@ -384,7 +398,8 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
} else { } else {
match ( match (
self.density.unwrap_or_else(crate::config::header_size), self.density.unwrap_or_else(crate::config::header_size),
self.maximized, // window border handling // Center content depending on window border
self.maximized,
) { ) {
(Density::Compact, true) => [4, 8, 4, 8], (Density::Compact, true) => [4, 8, 4, 8],
(Density::Compact, false) => [3, 7, 4, 7], (Density::Compact, false) => [3, 7, 4, 7],
@ -421,7 +436,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
let mut widget = HeaderBarWidget::new(start, center, end) let mut widget = HeaderBarWidget::new(start, center, end)
.apply(widget::container) .apply(widget::container)
.class(theme::Container::HeaderBar { .class(crate::theme::Container::HeaderBar {
focused: self.focused, focused: self.focused,
sharp_corners: self.sharp_corners, sharp_corners: self.sharp_corners,
transparent: self.transparent, transparent: self.transparent,
@ -453,7 +468,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
widget::icon::from_name($name) widget::icon::from_name($name)
.apply(widget::button::icon) .apply(widget::button::icon)
.padding(8) .padding(8)
.class(theme::Button::HeaderBar) .class(crate::theme::Button::HeaderBar)
.selected(self.focused) .selected(self.focused)
.icon_size($size) .icon_size($size)
.on_press($on_press) .on_press($on_press)

View file

@ -4,12 +4,12 @@
//! Embedded icons for platforms which do not support icon themes yet. //! Embedded icons for platforms which do not support icon themes yet.
/// Icon bundling is not enabled on unix platforms. /// Icon bundling is not enabled on unix platforms.
#[cfg(all(unix, not(target_os = "macos")))] #[cfg(unix)]
pub fn get(icon_name: &str) -> Option<super::Data> { pub fn get(icon_name: &str) -> Option<super::Data> {
None None
} }
#[cfg(any(not(unix), target_os = "macos"))] #[cfg(not(unix))]
/// Get a bundled icon on non-unix platforms. /// Get a bundled icon on non-unix platforms.
pub fn get(icon_name: &str) -> Option<super::Data> { pub fn get(icon_name: &str) -> Option<super::Data> {
ICONS ICONS
@ -17,5 +17,5 @@ pub fn get(icon_name: &str) -> Option<super::Data> {
.map(|bytes| super::Data::Svg(crate::iced::widget::svg::Handle::from_memory(*bytes))) .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")); include!(concat!(env!("OUT_DIR"), "/bundled_icons.rs"));

View file

@ -52,7 +52,7 @@ impl Named {
} }
} }
#[cfg(all(unix, not(target_os = "macos")))] #[cfg(not(windows))]
#[must_use] #[must_use]
pub fn path(self) -> Option<PathBuf> { pub fn path(self) -> Option<PathBuf> {
let name = &*self.name; let name = &*self.name;
@ -107,7 +107,7 @@ impl Named {
result result
} }
#[cfg(any(not(unix), target_os = "macos"))] #[cfg(windows)]
#[must_use] #[must_use]
pub fn path(self) -> Option<PathBuf> { pub fn path(self) -> Option<PathBuf> {
//TODO: implement icon lookup for Windows //TODO: implement icon lookup for Windows

128
src/widget/list/column.rs Normal file
View file

@ -0,0 +1,128 @@
// Copyright 2022 System76 <info@system76.com>
// 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<Element<'a, Message>>,
}
impl<Message: 'static> 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<Element<'a, Message>>) -> 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: <crate::Theme as Catalog>::Class<'a>) -> Self {
self.style = style;
self
}
#[inline]
pub fn padding(mut self, padding: impl Into<Padding>) -> 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<Padding>) -> 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<ListColumn<'a, Message>> for Element<'a, Message> {
fn from(column: ListColumn<'a, Message>) -> Self {
column.into_element()
}
}

View file

@ -1,213 +0,0 @@
// Copyright 2022 System76 <info@system76.com>
// 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<Message>,
selected: bool,
}
/// Creates a [`ListButton`] with the given content.
pub fn button<'a, Message>(content: impl Into<Element<'a, Message>>) -> 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<Message>) -> 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<Element<'a, Message>>,
{
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<ListEntry<'a, Message>>,
}
#[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<Message: 'static> 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: <crate::Theme as Catalog>::Class<'a>) -> Self {
self.style = style;
self
}
pub fn list_item_padding(mut self, padding: impl Into<Padding>) -> 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<ListColumn<'a, Message>> 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],
}
}

View file

@ -1,6 +1,6 @@
// Copyright 2022 System76 <info@system76.com> // Copyright 2022 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0 // 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};

View file

@ -12,7 +12,6 @@ use super::{
#[cfg(all( #[cfg(all(
feature = "multi-window", feature = "multi-window",
feature = "wayland", feature = "wayland",
target_os = "linux",
feature = "winit", feature = "winit",
feature = "surface-message" feature = "surface-message"
))] ))]
@ -196,12 +195,7 @@ pub struct MenuBar<Message> {
menu_roots: Vec<MenuTree<Message>>, menu_roots: Vec<MenuTree<Message>>,
style: <crate::Theme as StyleSheet>::Style, style: <crate::Theme as StyleSheet>::Style,
window_id: window::Id, window_id: window::Id,
#[cfg(all( #[cfg(all(feature = "multi-window", feature = "wayland", feature = "winit"))]
feature = "multi-window",
feature = "wayland",
feature = "winit",
target_os = "linux"
))]
positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner,
pub(crate) on_surface_action: pub(crate) on_surface_action:
Option<Arc<dyn Fn(crate::surface::Action) -> Message + Send + Sync + 'static>>, Option<Arc<dyn Fn(crate::surface::Action) -> Message + Send + Sync + 'static>>,
@ -236,12 +230,7 @@ where
menu_roots, menu_roots,
style: <crate::Theme as StyleSheet>::Style::default(), style: <crate::Theme as StyleSheet>::Style::default(),
window_id: window::Id::NONE, window_id: window::Id::NONE,
#[cfg(all( #[cfg(all(feature = "multi-window", feature = "wayland", feature = "winit"))]
feature = "multi-window",
feature = "wayland",
feature = "winit",
target_os = "linux"
))]
positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner::default(), positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner::default(),
on_surface_action: None, on_surface_action: None,
} }
@ -335,12 +324,7 @@ where
self self
} }
#[cfg(all( #[cfg(all(feature = "multi-window", feature = "wayland", feature = "winit"))]
feature = "multi-window",
feature = "wayland",
feature = "winit",
target_os = "linux"
))]
pub fn with_positioner( pub fn with_positioner(
mut self, mut self,
positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner,
@ -375,7 +359,6 @@ where
#[cfg(all( #[cfg(all(
feature = "multi-window", feature = "multi-window",
feature = "wayland", feature = "wayland",
target_os = "linux",
feature = "winit", feature = "winit",
feature = "surface-message" feature = "surface-message"
))] ))]
@ -646,7 +629,6 @@ where
state.open = false; state.open = false;
#[cfg(all( #[cfg(all(
feature = "wayland", feature = "wayland",
target_os = "linux",
feature = "winit", feature = "winit",
feature = "surface-message" feature = "surface-message"
))] ))]
@ -670,7 +652,6 @@ where
#[cfg(all( #[cfg(all(
feature = "multi-window", feature = "multi-window",
feature = "wayland", feature = "wayland",
target_os = "linux",
feature = "winit", feature = "winit",
feature = "surface-message" feature = "surface-message"
))] ))]
@ -685,7 +666,6 @@ where
#[cfg(all( #[cfg(all(
feature = "multi-window", feature = "multi-window",
feature = "wayland", feature = "wayland",
target_os = "linux",
feature = "winit", feature = "winit",
feature = "surface-message" feature = "surface-message"
))] ))]
@ -768,7 +748,6 @@ where
#[cfg(all( #[cfg(all(
feature = "multi-window", feature = "multi-window",
feature = "wayland", feature = "wayland",
target_os = "linux",
feature = "winit", feature = "winit",
feature = "surface-message" feature = "surface-message"
))] ))]

View file

@ -7,7 +7,6 @@ use super::{menu_bar::MenuBarState, menu_tree::MenuTree};
#[cfg(all( #[cfg(all(
feature = "multi-window", feature = "multi-window",
feature = "wayland", feature = "wayland",
target_os = "linux",
feature = "winit", feature = "winit",
feature = "surface-message" feature = "surface-message"
))] ))]
@ -681,7 +680,6 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> {
#[cfg(all( #[cfg(all(
feature = "multi-window", feature = "multi-window",
feature = "wayland", feature = "wayland",
target_os = "linux",
feature = "winit", feature = "winit",
feature = "surface-message" feature = "surface-message"
))] ))]
@ -767,13 +765,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> {
PathHighlight::OmitActive => { PathHighlight::OmitActive => {
!indices.is_empty() && i < indices.len() - 1 !indices.is_empty() && i < indices.len() - 1
} }
PathHighlight::MenuActive => { PathHighlight::MenuActive => self.depth == state.active_root.len() - 1,
!indices.is_empty()
&& i < indices.len()
&& menu_roots.len() > indices[i]
&& (i < indices.len() - 1
|| !menu_roots[indices[i]].children.is_empty())
}
}); });
// react only to the last menu // react only to the last menu
@ -968,8 +960,7 @@ impl<Message: std::clone::Clone + 'static> Widget<Message, crate::Theme, crate::
feature = "multi-window", feature = "multi-window",
feature = "wayland", feature = "wayland",
feature = "winit", feature = "winit",
feature = "surface-message", feature = "surface-message"
target_os = "linux"
))] ))]
if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland))
&& let Some((new_root, new_ms)) = new_root && let Some((new_root, new_ms)) = new_root
@ -1229,7 +1220,6 @@ pub(crate) fn init_root_menu<Message: Clone>(
#[cfg(all( #[cfg(all(
feature = "multi-window", feature = "multi-window",
feature = "wayland", feature = "wayland",
target_os = "linux",
feature = "winit", feature = "winit",
feature = "surface-message" feature = "surface-message"
))] ))]
@ -1527,7 +1517,7 @@ where
.as_ref() .as_ref()
.is_some_and(|i| *i != new_index && !active_menu[*i].children.is_empty()); .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 matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) && remove {
if let Some(id) = state.popup_id.remove(&menu.window_id) { if let Some(id) = state.popup_id.remove(&menu.window_id) {
state.active_root.truncate(menu.depth + 1); state.active_root.truncate(menu.depth + 1);

View file

@ -9,11 +9,11 @@ use std::rc::Rc;
use iced::advanced::widget::text::Style as TextStyle; use iced::advanced::widget::text::Style as TextStyle;
use iced_widget::core::{Element, renderer}; use iced_widget::core::{Element, renderer};
use crate::iced_core::{Alignment, Length};
use crate::widget::menu::action::MenuAction; use crate::widget::menu::action::MenuAction;
use crate::widget::menu::key_bind::KeyBind; use crate::widget::menu::key_bind::KeyBind;
use crate::widget::{Button, RcElementWrapper, icon}; use crate::widget::{Button, RcElementWrapper, icon};
use crate::{theme, widget}; use crate::{theme, widget};
use iced_core::{Alignment, Length};
/// Nested menu is essentially a tree of items, a menu is a collection of items /// 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. /// 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 l: Cow<'static, str> = label.into();
let key = find_key(&action, key_binds); let key = find_key(&action, key_binds);
let mut items = vec![ let mut items = vec![
widget::text(l) widget::text(l).into(),
.ellipsize(iced_core::text::Ellipsize::Middle(
iced_core::text::EllipsizeHeightLimit::Lines(1),
))
.into(),
widget::space::horizontal().into(), widget::space::horizontal().into(),
widget::text(key) widget::text(key).class(key_class).into(),
.class(key_class)
.ellipsize(iced_core::text::Ellipsize::Middle(
iced_core::text::EllipsizeHeightLimit::Lines(1),
))
.into(),
]; ];
if let Some(icon) = icon { if let Some(icon) = icon {
@ -284,18 +275,9 @@ pub fn menu_items<
let key = find_key(&action, key_binds); let key = find_key(&action, key_binds);
let mut items = vec![ let mut items = vec![
widget::text(l) widget::text(l).into(),
.ellipsize(iced_core::text::Ellipsize::Middle(
iced_core::text::EllipsizeHeightLimit::Lines(1),
))
.into(),
widget::space::horizontal().into(), widget::space::horizontal().into(),
widget::text(key) widget::text(key).class(key_class).into(),
.ellipsize(iced_core::text::Ellipsize::Middle(
iced_core::text::EllipsizeHeightLimit::Lines(1),
))
.class(key_class)
.into(),
]; ];
if let Some(icon) = icon { if let Some(icon) = icon {
@ -330,19 +312,9 @@ pub fn menu_items<
.into() .into()
}, },
widget::space::horizontal().width(spacing.space_xxs).into(), widget::space::horizontal().width(spacing.space_xxs).into(),
widget::text(label) widget::text(label).align_x(iced::Alignment::Start).into(),
.ellipsize(iced_core::text::Ellipsize::Middle(
iced_core::text::EllipsizeHeightLimit::Lines(1),
))
.align_x(iced::Alignment::Start)
.into(),
widget::space::horizontal().into(), widget::space::horizontal().into(),
widget::text(key) widget::text(key).class(key_class).into(),
.class(key_class)
.ellipsize(iced_core::text::Ellipsize::Middle(
iced_core::text::EllipsizeHeightLimit::Lines(1),
))
.into(),
]; ];
if let Some(icon) = icon { if let Some(icon) = icon {
@ -363,11 +335,7 @@ pub fn menu_items<
trees.push(MenuTree::<Message>::with_children( trees.push(MenuTree::<Message>::with_children(
RcElementWrapper::new(crate::Element::from( RcElementWrapper::new(crate::Element::from(
menu_button::<'static, _>(vec![ menu_button::<'static, _>(vec![
widget::text(l.clone()) widget::text(l.clone()).into(),
.ellipsize(iced_core::text::Ellipsize::Middle(
iced_core::text::EllipsizeHeightLimit::Lines(1),
))
.into(),
widget::space::horizontal().into(), widget::space::horizontal().into(),
widget::icon::from_name("pan-end-symbolic") widget::icon::from_name("pan-end-symbolic")
.size(16) .size(16)

View file

@ -24,7 +24,7 @@
//! .on_press(Message::LaunchUrl(REPOSITORY)) //! .on_press(Message::LaunchUrl(REPOSITORY))
//! .padding(0); //! .padding(0);
//! //!
//! let content = widget::column::with_capacity(3) //! let content = widget::column()
//! .push(widget::icon::from_name("my-app-icon")) //! .push(widget::icon::from_name("my-app-icon"))
//! .push(widget::text::title3("My App Name")) //! .push(widget::text::title3("My App Name"))
//! .push(link) //! .push(link)
@ -53,9 +53,6 @@ pub use iced::widget::{Canvas, canvas};
#[doc(inline)] #[doc(inline)]
pub use iced::widget::{Checkbox, checkbox}; pub use iced::widget::{Checkbox, checkbox};
#[doc(inline)]
pub use iced::widget::{Column, column};
#[doc(inline)] #[doc(inline)]
pub use iced::widget::{ComboBox, combo_box}; 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}; pub use iced::widget::{PaneGrid, pane_grid};
#[doc(inline)] #[doc(inline)]
pub use iced::widget::{Responsive, responsive}; pub use iced::widget::{ProgressBar, progress_bar};
#[doc(inline)] #[doc(inline)]
pub use iced::widget::{Row, row}; pub use iced::widget::{Responsive, responsive};
#[doc(inline)] #[doc(inline)]
pub use iced::widget::{Slider, VerticalSlider, slider, vertical_slider}; pub use iced::widget::{Slider, VerticalSlider, slider, vertical_slider};
@ -138,6 +135,34 @@ pub mod context_drawer;
#[doc(inline)] #[doc(inline)]
pub use context_drawer::{ContextDrawer, context_drawer}; 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<Item = crate::Element<'a, Message>>,
) -> Column<'a, Message> {
Column::with_children(children)
}
}
pub mod layer_container; pub mod layer_container;
#[doc(inline)] #[doc(inline)]
pub use layer_container::{LayerContainer, layer_container}; pub use layer_container::{LayerContainer, layer_container};
@ -254,13 +279,6 @@ pub mod popover;
#[doc(inline)] #[doc(inline)]
pub use popover::{Popover, popover}; 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; pub mod radio;
#[doc(inline)] #[doc(inline)]
pub use radio::{Radio, radio}; pub use radio::{Radio, radio};
@ -269,6 +287,35 @@ pub mod rectangle_tracker;
#[doc(inline)] #[doc(inline)]
pub use rectangle_tracker::{RectangleTracker, rectangle_tracking_container}; 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<Item = crate::Element<'a, Message>>,
) -> Row<'a, Message> {
Row::with_children(children)
}
}
pub mod scrollable; pub mod scrollable;
#[doc(inline)] #[doc(inline)]
pub use scrollable::scrollable; pub use scrollable::scrollable;
@ -308,7 +355,7 @@ pub use toggler::{Toggler, toggler};
#[doc(inline)] #[doc(inline)]
pub use tooltip::{Tooltip, tooltip}; 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 wayland;
pub mod tooltip { pub mod tooltip {

View file

@ -138,10 +138,6 @@ where
renderer: &Renderer, renderer: &Renderer,
operation: &mut dyn Operation, operation: &mut dyn Operation,
) { ) {
// Skip operating on background content, prevents Tab from escaping
if self.modal && self.popup.is_some() {
return;
}
self.content self.content
.as_widget_mut() .as_widget_mut()
.operate(content_tree_mut(tree), layout, renderer, operation); .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( self.content.as_widget_mut().update(
&mut tree.children[0], &mut tree.children[0],
event, event,
layout, layout,
cursor, cursor_position,
renderer, renderer,
clipboard, clipboard,
shell, shell,
@ -224,19 +214,13 @@ where
cursor_position: mouse::Cursor, cursor_position: mouse::Cursor,
viewport: &Rectangle, 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( self.content.as_widget().draw(
content_tree(tree), content_tree(tree),
renderer, renderer,
theme, theme,
renderer_style, renderer_style,
layout, layout,
cursor, cursor_position,
viewport, viewport,
); );
} }

View file

@ -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<Theme>
where
Theme: StyleSheet,
{
size: f32,
bar_height: f32,
style: <Theme as StyleSheet>::Style,
cycle_duration: Duration,
rotation_duration: Duration,
progress: Option<f32>,
}
impl<Theme> Circular<Theme>
where
Theme: StyleSheet,
{
/// Creates a new [`Circular`] with the given content.
pub fn new() -> Self {
Circular {
size: 40.0,
bar_height: 4.0,
style: <Theme as StyleSheet>::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: <Theme as StyleSheet>::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<Theme> Default for Circular<Theme>
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<f32>,
}
impl<Message, Theme> Widget<Message, Theme, Renderer> for Circular<Theme>
where
Message: Clone,
Theme: StyleSheet,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<State>()
}
fn state(&self) -> tree::State {
tree::State::new(State::default())
}
fn size(&self) -> Size<Length> {
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::<State>();
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::<State>();
let bounds = layout.bounds();
let custom_style =
<Theme as StyleSheet>::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<Circular<Theme>> for Element<'a, Message, Theme, Renderer>
where
Message: Clone + 'a,
Theme: StyleSheet + 'a,
{
fn from(circular: Circular<Theme>) -> Self {
Self::new(circular)
}
}

View file

@ -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<Theme>
where
Theme: StyleSheet,
{
width: Length,
girth: Length,
style: Theme::Style,
cycle_duration: Duration,
progress: Option<f32>,
}
impl<Theme> Linear<Theme>
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<Length>) -> Self {
self.width = width.into();
self
}
/// Sets the girth of the [`Linear`].
pub fn girth(mut self, girth: impl Into<Length>) -> Self {
self.girth = girth.into();
self
}
/// Sets the style variant of this [`Linear`].
pub fn style(mut self, style: impl Into<Theme::Style>) -> 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<Theme> Default for Linear<Theme>
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<Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Linear<Theme>
where
Message: Clone,
Theme: StyleSheet,
Renderer: advanced::Renderer,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<State>()
}
fn state(&self) -> tree::State {
tree::State::new(State::default())
}
fn size(&self) -> Size<Length> {
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::<State>();
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::<State>();
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<Linear<Theme>> for Element<'a, Message, Theme, Renderer>
where
Message: Clone + 'a,
Theme: StyleSheet + 'a,
Renderer: iced::advanced::Renderer + 'a,
{
fn from(linear: Linear<Theme>) -> Self {
Self::new(linear)
}
}

View file

@ -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<crate::Theme> {
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<crate::Theme> {
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<crate::Theme> {
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<crate::Theme> {
linear::Linear::new().progress(progress)
}

View file

@ -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<Color>,
/// 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],
}
}
}

View file

@ -1,5 +1,5 @@
//! Create choices using radio buttons. //! Create choices using radio buttons.
use crate::{Theme, theme}; use crate::Theme;
use iced::border; use iced::border;
use iced_core::event::{self, Event}; use iced_core::event::{self, Event};
use iced_core::layout; use iced_core::layout;
@ -92,7 +92,7 @@ where
{ {
is_selected: bool, is_selected: bool,
on_click: Message, on_click: Message,
label: Option<Element<'a, Message, Theme, Renderer>>, label: Element<'a, Message, Theme, Renderer>,
width: Length, width: Length,
size: f32, size: f32,
spacing: f32, spacing: f32,
@ -106,6 +106,9 @@ where
/// The default size of a [`Radio`] button. /// The default size of a [`Radio`] button.
pub const DEFAULT_SIZE: f32 = 16.0; 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. /// Creates a new [`Radio`] button.
/// ///
/// It expects: /// It expects:
@ -123,29 +126,10 @@ where
Radio { Radio {
is_selected: Some(value) == selected, is_selected: Some(value) == selected,
on_click: f(value), on_click: f(value),
label: Some(label.into()), label: label.into(),
width: Length::Shrink, width: Length::Shrink,
size: Self::DEFAULT_SIZE, size: Self::DEFAULT_SIZE,
spacing: theme::spacing().space_xs as f32, spacing: Self::DEFAULT_SPACING,
}
}
/// 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<V, F>(value: V, selected: Option<V>, 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,
} }
} }
@ -177,17 +161,11 @@ where
Renderer: iced_core::Renderer, Renderer: iced_core::Renderer,
{ {
fn children(&self) -> Vec<Tree> { fn children(&self) -> Vec<Tree> {
if let Some(label) = &self.label { vec![Tree::new(&self.label)]
vec![Tree::new(label)]
} else {
vec![]
}
} }
fn diff(&mut self, tree: &mut Tree) { fn diff(&mut self, tree: &mut Tree) {
if let Some(label) = &mut self.label { tree.diff_children(std::slice::from_mut(&mut self.label));
tree.diff_children(std::slice::from_mut(label));
}
} }
fn size(&self) -> Size<Length> { fn size(&self) -> Size<Length> {
Size { Size {
@ -202,20 +180,16 @@ where
renderer: &Renderer, renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node { ) -> layout::Node {
if let Some(label) = &mut self.label {
layout::next_to_each_other( layout::next_to_each_other(
&limits.width(self.width), &limits.width(self.width),
self.spacing, self.spacing,
|_| layout::Node::new(Size::new(self.size, self.size)), |_| layout::Node::new(Size::new(self.size, self.size)),
|limits| { |limits| {
label self.label
.as_widget_mut() .as_widget_mut()
.layout(&mut tree.children[0], renderer, limits) .layout(&mut tree.children[0], renderer, limits)
}, },
) )
} else {
layout::Node::new(Size::new(self.size, self.size))
}
} }
fn operate( fn operate(
@ -225,15 +199,13 @@ where
renderer: &Renderer, renderer: &Renderer,
operation: &mut dyn iced_core::widget::Operation<()>, operation: &mut dyn iced_core::widget::Operation<()>,
) { ) {
if let Some(label) = &mut self.label { self.label.as_widget_mut().operate(
label.as_widget_mut().operate(
&mut tree.children[0], &mut tree.children[0],
layout.children().nth(1).unwrap(), layout.children().nth(1).unwrap(),
renderer, renderer,
operation, operation,
); );
} }
}
fn update( fn update(
&mut self, &mut self,
@ -246,8 +218,7 @@ where
shell: &mut Shell<'_, Message>, shell: &mut Shell<'_, Message>,
viewport: &Rectangle, viewport: &Rectangle,
) { ) {
if let Some(label) = &mut self.label { self.label.as_widget_mut().update(
label.as_widget_mut().update(
&mut tree.children[0], &mut tree.children[0],
event, event,
layout.children().nth(1).unwrap(), layout.children().nth(1).unwrap(),
@ -257,14 +228,14 @@ where
shell, shell,
viewport, viewport,
); );
}
if !shell.is_event_captured() { if !shell.is_event_captured() {
match event { match event {
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerLifted { .. }) => { | Event::Touch(touch::Event::FingerPressed { .. }) => {
if cursor.is_over(layout.bounds()) { if cursor.is_over(layout.bounds()) {
shell.publish(self.on_click.clone()); shell.publish(self.on_click.clone());
shell.capture_event(); shell.capture_event();
return; return;
} }
@ -282,17 +253,13 @@ where
viewport: &Rectangle, viewport: &Rectangle,
renderer: &Renderer, renderer: &Renderer,
) -> mouse::Interaction { ) -> mouse::Interaction {
let interaction = if let Some(label) = &self.label { let interaction = self.label.as_widget().mouse_interaction(
label.as_widget().mouse_interaction(
&tree.children[0], &tree.children[0],
layout.children().nth(1).unwrap(), layout.children().nth(1).unwrap(),
cursor, cursor,
viewport, viewport,
renderer, renderer,
) );
} else {
mouse::Interaction::default()
};
if interaction == mouse::Interaction::default() { if interaction == mouse::Interaction::default() {
if cursor.is_over(layout.bounds()) { if cursor.is_over(layout.bounds()) {
@ -317,6 +284,8 @@ where
) { ) {
let is_mouse_over = cursor.is_over(layout.bounds()); let is_mouse_over = cursor.is_over(layout.bounds());
let mut children = layout.children();
let custom_style = if is_mouse_over { let custom_style = if is_mouse_over {
theme.style( 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; let dot_size = 6.0;
renderer.fill_quad( renderer.fill_quad(
renderer::Quad { renderer::Quad {
bounds: dot_bounds, bounds,
border: Border { border: Border {
radius: (size / 2.0).into(), radius: (size / 2.0).into(),
width: custom_style.border_width, width: custom_style.border_width,
@ -362,8 +326,8 @@ where
renderer.fill_quad( renderer.fill_quad(
renderer::Quad { renderer::Quad {
bounds: Rectangle { bounds: Rectangle {
x: dot_bounds.x + (size - dot_size) / 2.0, x: bounds.x + (size - dot_size) / 2.0,
y: dot_bounds.y + (size - dot_size) / 2.0, y: bounds.y + (size - dot_size) / 2.0,
width: dot_size, width: dot_size,
height: 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], &tree.children[0],
renderer, renderer,
theme, theme,
@ -396,7 +361,7 @@ where
viewport: &Rectangle, viewport: &Rectangle,
translation: Vector, translation: Vector,
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> { ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
self.label.as_mut()?.as_widget_mut().overlay( self.label.as_widget_mut().overlay(
&mut tree.children[0], &mut tree.children[0],
layout.children().nth(1).unwrap(), layout.children().nth(1).unwrap(),
renderer, renderer,
@ -412,15 +377,13 @@ where
renderer: &Renderer, renderer: &Renderer,
dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles,
) { ) {
if let Some(label) = &self.label { self.label.as_widget().drag_destinations(
label.as_widget().drag_destinations(
&state.children[0], &state.children[0],
layout.children().nth(1).unwrap(), layout.children().nth(1).unwrap(),
renderer, renderer,
dnd_rectangles, dnd_rectangles,
); );
} }
}
} }
impl<'a, Message, Renderer> From<Radio<'a, Message, Renderer>> impl<'a, Message, Renderer> From<Radio<'a, Message, Renderer>>

View file

@ -25,7 +25,7 @@ impl Default for ResponsiveMenuBar {
fn default() -> ResponsiveMenuBar { fn default() -> ResponsiveMenuBar {
ResponsiveMenuBar { ResponsiveMenuBar {
collapsed_item_width: { collapsed_item_width: {
#[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] #[cfg(all(feature = "winit", feature = "wayland"))]
if matches!( if matches!(
crate::app::cosmic::WINDOWING_SYSTEM.get(), crate::app::cosmic::WINDOWING_SYSTEM.get(),
Some(crate::app::cosmic::WindowingSystem::Wayland) Some(crate::app::cosmic::WindowingSystem::Wayland)
@ -34,7 +34,7 @@ impl Default for ResponsiveMenuBar {
} else { } else {
ItemWidth::Static(84) ItemWidth::Static(84)
} }
#[cfg(not(all(feature = "winit", feature = "wayland", target_os = "linux")))] #[cfg(not(all(feature = "winit", feature = "wayland")))]
{ {
ItemWidth::Static(84) ItemWidth::Static(84)
} }

View file

@ -213,18 +213,6 @@ where
state.buttons_offset = num - state.buttons_visible; 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 size
} }
} }

View file

@ -117,15 +117,10 @@ where
height += item_height; height += item_height;
} }
let size = limits.height(Length::Fixed(height)).resolve( limits.height(Length::Fixed(height)).resolve(
self.width, self.width,
self.height, self.height,
Size::new(width, height), Size::new(width, height),
); )
// Resize paragraph bounds so that text ellipsis can take effect.
self.resize_paragraphs(state, size.width);
size
} }
} }

View file

@ -3,6 +3,7 @@
use super::model::{Entity, Model, Selectable}; use super::model::{Entity, Model, Selectable};
use super::{InsertPosition, ReorderEvent}; use super::{InsertPosition, ReorderEvent};
use crate::iced_core::id::Internal;
use crate::theme::{SegmentedButton as Style, THEME}; use crate::theme::{SegmentedButton as Style, THEME};
use crate::widget::dnd_destination::DragId; use crate::widget::dnd_destination::DragId;
use crate::widget::menu::{ use crate::widget::menu::{
@ -21,7 +22,6 @@ use iced::{
Alignment, Background, Color, Event, Length, Padding, Rectangle, Size, Task, Vector, alignment, Alignment, Background, Color, Event, Length, Padding, Rectangle, Size, Task, Vector, alignment,
keyboard, mouse, touch, window, keyboard, mouse, touch, window,
}; };
use iced_core::id::Internal;
use iced_core::mouse::ScrollDelta; use iced_core::mouse::ScrollDelta;
use iced_core::text::{self, Ellipsize, LineHeight, Renderer as TextRenderer, Shaping, Wrapping}; use iced_core::text::{self, Ellipsize, LineHeight, Renderer as TextRenderer, Shaping, Wrapping};
use iced_core::widget::operation::Focusable; use iced_core::widget::operation::Focusable;
@ -156,8 +156,6 @@ where
pub(super) spacing: u16, pub(super) spacing: u16,
/// LineHeight of the font. /// LineHeight of the font.
pub(super) line_height: LineHeight, pub(super) line_height: LineHeight,
/// Ellipsize strategy for button text.
pub(super) ellipsize: Ellipsize,
/// Style to draw the widget in. /// Style to draw the widget in.
#[setters(into)] #[setters(into)]
pub(super) style: Style, pub(super) style: Style,
@ -218,14 +216,13 @@ where
maximum_button_width: u16::MAX, maximum_button_width: u16::MAX,
indent_spacing: 16, indent_spacing: 16,
font_active: crate::font::semibold(), font_active: crate::font::semibold(),
font_hovered: crate::font::default(), font_hovered: crate::font::semibold(),
font_inactive: crate::font::default(), font_inactive: crate::font::default(),
font_size: 14.0, font_size: 14.0,
height: Length::Shrink, height: Length::Shrink,
width: Length::Fill, width: Length::Fill,
spacing: 0, spacing: 0,
line_height: LineHeight::default(), line_height: LineHeight::default(),
ellipsize: Ellipsize::default(),
style: Style::default(), style: Style::default(),
context_menu: None, context_menu: None,
on_activate: None, on_activate: None,
@ -246,13 +243,12 @@ where
fn update_entity_paragraph(&mut self, state: &mut LocalState, key: Entity) { fn update_entity_paragraph(&mut self, state: &mut LocalState, key: Entity) {
if let Some(text) = self.model.text.get(key) { if let Some(text) = self.model.text.get(key) {
let font = if self.button_is_focused(state, key) let font = if self.button_is_focused(state, key) {
|| state.show_context == Some(key)
|| self.model.is_active(key)
{
self.font_active 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 self.font_hovered
} else if self.model.is_active(key) {
self.font_active
} else { } else {
self.font_inactive self.font_inactive
}; };
@ -262,11 +258,11 @@ where
font.hash(&mut hasher); font.hash(&mut hasher);
let text_hash = hasher.finish(); let text_hash = hasher.finish();
if let Some(prev_hash) = state.text_hashes.insert(key, text_hash) if let Some(prev_hash) = state.text_hashes.insert(key, text_hash) {
&& prev_hash == text_hash if prev_hash == text_hash {
{
return; return;
} }
}
if let Some(paragraph) = state.paragraphs.get_mut(key) { if let Some(paragraph) = state.paragraphs.get_mut(key) {
let text = Text { let text = Text {
@ -279,7 +275,7 @@ where
shaping: Shaping::Advanced, shaping: Shaping::Advanced,
wrapping: Wrapping::None, wrapping: Wrapping::None,
line_height: self.line_height, line_height: self.line_height,
ellipsize: self.ellipsize, ellipsize: Ellipsize::default(),
}; };
paragraph.update(text); paragraph.update(text);
} else { } else {
@ -293,7 +289,7 @@ where
shaping: Shaping::Advanced, shaping: Shaping::Advanced,
wrapping: Wrapping::None, wrapping: Wrapping::None,
line_height: self.line_height, line_height: self.line_height,
ellipsize: self.ellipsize, ellipsize: Ellipsize::default(),
}; };
state.paragraphs.insert(key, crate::Plain::new(text)); state.paragraphs.insert(key, crate::Plain::new(text));
} }
@ -306,7 +302,7 @@ where
{ {
self.context_menu = context_menu.map(|menus| { self.context_menu = context_menu.map(|menus| {
vec![menu::Tree::with_children( vec![menu::Tree::with_children(
crate::Element::from(crate::widget::Row::new()), crate::Element::from(crate::widget::row::<'static, Message>()),
menus, menus,
)] )]
}); });
@ -625,7 +621,7 @@ where
align_y: alignment::Vertical::Center, align_y: alignment::Vertical::Center,
shaping: Shaping::Advanced, shaping: Shaping::Advanced,
wrapping: Wrapping::default(), wrapping: Wrapping::default(),
ellipsize: self.ellipsize, ellipsize: Ellipsize::default(),
line_height: self.line_height, line_height: self.line_height,
}) })
}); });
@ -661,50 +657,6 @@ where
(width, f32::from(self.button_height)) (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( pub(super) fn max_button_dimensions(
&self, &self,
state: &mut LocalState, state: &mut LocalState,
@ -928,6 +880,7 @@ where
fn diff(&mut self, tree: &mut Tree) { fn diff(&mut self, tree: &mut Tree) {
let state = tree.state.downcast_mut::<LocalState>(); let state = tree.state.downcast_mut::<LocalState>();
for key in self.model.order.iter().copied() { for key in self.model.order.iter().copied() {
self.update_entity_paragraph(state, key); self.update_entity_paragraph(state, key);
} }
@ -1481,7 +1434,7 @@ where
} }
} }
} else { } 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() { for key in self.model.order.iter().copied() {
self.update_entity_paragraph(state, key); self.update_entity_paragraph(state, key);
} }
@ -1985,9 +1938,7 @@ where
// Align contents of the button to the requested `button_alignment`. // Align contents of the button to the requested `button_alignment`.
{ {
// Avoid shifting content outside the left edge when the measured content is let actual_width = state.internal_layout[nth].1.width;
// wider than the available button bounds (for example, non-ellipsized text).
let actual_width = state.internal_layout[nth].1.width.min(bounds.width);
let offset = match self.button_alignment { let offset = match self.button_alignment {
Alignment::Start => None, Alignment::Start => None,
@ -2043,10 +1994,10 @@ where
..image_bounds ..image_bounds
}, },
crate::widget::icon(match crate::widget::common::object_select().data() { 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) 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()) crate::widget::icon::from_path(path.clone())
} }
}), }),
@ -2139,7 +2090,7 @@ where
tree: &'b mut Tree, tree: &'b mut Tree,
layout: iced_core::Layout<'b>, layout: iced_core::Layout<'b>,
_renderer: &Renderer, _renderer: &Renderer,
_viewport: &iced_core::Rectangle, viewport: &iced_core::Rectangle,
translation: Vector, translation: Vector,
) -> Option<iced_core::overlay::Element<'b, Message, crate::Theme, Renderer>> { ) -> Option<iced_core::overlay::Element<'b, Message, crate::Theme, Renderer>> {
let state = tree.state.downcast_mut::<LocalState>(); let state = tree.state.downcast_mut::<LocalState>();

View file

@ -4,8 +4,8 @@
use std::borrow::Cow; use std::borrow::Cow;
use crate::{ use crate::{
Element, Theme, theme, Element, theme,
widget::{FlexRow, Row, column, container, flex_row, list, row, text}, widget::{FlexRow, Row, column, container, flex_row, row, text},
}; };
use derive_setters::Setters; use derive_setters::Setters;
use iced_core::{Length, text::Wrapping}; use iced_core::{Length, text::Wrapping};
@ -18,12 +18,12 @@ use taffy::AlignContent;
pub fn item<'a, Message: 'static>( pub fn item<'a, Message: 'static>(
title: impl Into<Cow<'a, str>> + 'a, title: impl Into<Cow<'a, str>> + 'a,
widget: impl Into<Element<'a, Message>> + 'a, widget: impl Into<Element<'a, Message>> + 'a,
) -> Row<'a, Message, Theme> { ) -> Row<'a, Message> {
#[inline(never)] #[inline(never)]
fn inner<'a, Message: 'static>( fn inner<'a, Message: 'static>(
title: Cow<'a, str>, title: Cow<'a, str>,
widget: Element<'a, Message>, widget: Element<'a, Message>,
) -> Row<'a, Message, Theme> { ) -> Row<'a, Message> {
item_row(vec![ item_row(vec![
text(title).wrapping(Wrapping::Word).into(), text(title).wrapping(Wrapping::Word).into(),
space::horizontal().into(), space::horizontal().into(),
@ -37,7 +37,7 @@ pub fn item<'a, Message: 'static>(
/// A settings item aligned in a row /// A settings item aligned in a row
#[must_use] #[must_use]
#[allow(clippy::module_name_repetitions)] #[allow(clippy::module_name_repetitions)]
pub fn item_row<Message>(children: Vec<Element<Message>>) -> Row<Message, Theme> { pub fn item_row<Message>(children: Vec<Element<Message>>) -> Row<Message> {
row::with_children(children) row::with_children(children)
.spacing(theme::spacing().space_xs) .spacing(theme::spacing().space_xs)
.align_y(iced::Alignment::Center) .align_y(iced::Alignment::Center)
@ -103,9 +103,9 @@ pub struct Item<'a, Message> {
icon: Option<Element<'a, Message>>, icon: Option<Element<'a, Message>>,
} }
impl<'a, Message: Clone + 'static> Item<'a, Message> { impl<'a, Message: 'static> Item<'a, Message> {
/// Assigns a control to the item. /// Assigns a control to the item.
pub fn control(self, widget: impl Into<Element<'a, Message>>) -> Row<'a, Message, Theme> { pub fn control(self, widget: impl Into<Element<'a, Message>>) -> Row<'a, Message> {
item_row(self.control_(widget.into())) 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())) flex_item_row(self.control_(widget.into()))
} }
fn label(self) -> Element<'a, Message> { #[inline(never)]
fn control_(self, widget: Element<'a, Message>) -> Vec<Element<'a, Message>> {
let mut contents = Vec::with_capacity(4);
if let Some(icon) = self.icon {
contents.push(icon);
}
if let Some(description) = self.description { if let Some(description) = self.description {
column::with_capacity(2) let column = column::with_capacity(2)
.spacing(2) .spacing(2)
.push(text::body(self.title).wrapping(Wrapping::Word)) .push(text::body(self.title).wrapping(Wrapping::Word))
.push(text::caption(description).wrapping(Wrapping::Word)) .push(text::caption(description).wrapping(Wrapping::Word))
.width(Length::Fill) .width(Length::Fill);
.into()
contents.push(column.into());
} else { } else {
text(self.title).width(Length::Fill).into() contents.push(text(self.title).width(Length::Fill).into());
}
} }
#[inline(never)]
fn control_(mut self, widget: Element<'a, Message>) -> Vec<Element<'a, Message>> {
let mut contents = Vec::with_capacity(3);
if let Some(icon) = self.icon.take() {
contents.push(icon);
}
contents.push(self.label());
contents.push(widget); contents.push(widget);
contents contents
} }
fn control_start(self, widget: impl Into<Element<'a, Message>>) -> Row<'a, Message, Theme> {
item_row(vec![widget.into(), self.label()])
}
pub fn toggler( pub fn toggler(
self, self,
is_checked: bool, is_checked: bool,
message: impl Fn(bool) -> Message + 'static, message: impl Fn(bool) -> Message + 'static,
) -> list::ListButton<'a, Message> { ) -> Row<'a, Message> {
let on_press = message(!is_checked);
list::button(
self.control( self.control(
crate::widget::toggler(is_checked) crate::widget::toggler(is_checked)
.width(Length::Shrink) .width(Length::Shrink)
.on_toggle(message), .on_toggle(message),
),
) )
.on_press(on_press)
}
pub fn toggler_maybe(
self,
is_checked: bool,
message: Option<impl Fn(bool) -> 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<impl Fn(bool) -> 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<V, F>(self, value: V, selected: Option<V>, 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)
} }
} }

View file

@ -8,10 +8,10 @@ pub use self::item::{flex_item, flex_item_row, item, item_row};
pub use self::section::{Section, section}; pub use self::section::{Section, section};
use crate::widget::{Column, column}; 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 /// A column with a predefined style for creating a settings panel
#[must_use] #[must_use]
pub fn view_column<Message: 'static>(children: Vec<Element<Message>>) -> Column<Message, Theme> { pub fn view_column<Message: 'static>(children: Vec<Element<Message>>) -> Column<Message> {
column::with_children(children).spacing(theme::spacing().space_m) column::with_children(children).spacing(theme::spacing().space_m)
} }

View file

@ -2,24 +2,22 @@
// SPDX-License-Identifier: MPL-2.0 // SPDX-License-Identifier: MPL-2.0
use crate::Element; use crate::Element;
use crate::widget::list_column::IntoListItem; use crate::widget::{ListColumn, column, text};
use crate::widget::{ListColumn, column, list_column, text};
use std::borrow::Cow; use std::borrow::Cow;
/// A section within a settings view column. /// 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<Cow<'a, str>>) -> 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()) 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. /// A section with a pre-defined list column.
pub fn with_column<Message: Clone + 'static>( pub fn with_column<Message: 'static>(children: ListColumn<'_, Message>) -> Section<'_, Message> {
children: ListColumn<'_, Message>,
) -> Section<'_, Message> {
Section { Section {
header: None, header: None,
children, children,
@ -32,9 +30,9 @@ pub struct Section<'a, Message> {
children: ListColumn<'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. /// Define an optional title for the section.
pub fn title(self, title: impl Into<Cow<'a, str>>) -> Self { pub fn title(mut self, title: impl Into<Cow<'a, str>>) -> Self {
self.header(text::heading(title.into())) 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. /// Add a child element to the section's list column.
#[allow(clippy::should_implement_trait)] #[allow(clippy::should_implement_trait)]
pub fn add(mut self, item: impl IntoListItem<'a, Message>) -> Self { pub fn add(mut self, item: impl Into<Element<'a, Message>>) -> Self {
self.children = self.children.add(item); self.children = self.children.add(item.into());
self self
} }
/// Add a child element to the section's list column, if `Some`. /// Add a child element to the section's list column, if `Some`.
pub fn add_maybe(self, item: Option<impl IntoListItem<'a, Message>>) -> Self { pub fn add_maybe(self, item: Option<impl Into<Element<'a, Message>>>) -> Self {
if let Some(item) = item { if let Some(item) = item {
self.add(item) self.add(item)
} else { } else {
@ -63,13 +61,13 @@ impl<'a, Message: Clone + 'static> Section<'a, Message> {
/// Extends the [`Section`] with the given children. /// Extends the [`Section`] with the given children.
pub fn extend( pub fn extend(
self, self,
children: impl IntoIterator<Item = impl IntoListItem<'a, Message>>, children: impl IntoIterator<Item = impl Into<Element<'a, Message>>>,
) -> Self { ) -> Self {
children.into_iter().fold(self, Self::add) children.into_iter().fold(self, Self::add)
} }
} }
impl<'a, Message: Clone + 'static> From<Section<'a, Message>> for Element<'a, Message> { impl<'a, Message: 'static> From<Section<'a, Message>> for Element<'a, Message> {
fn from(data: Section<'a, Message>) -> Self { fn from(data: Section<'a, Message>) -> Self {
column::with_capacity(2) column::with_capacity(2)
.spacing(8) .spacing(8)

View file

@ -65,7 +65,7 @@ where
let selected = val.model.is_active(entity); let selected = val.model.is_active(entity);
let context_menu = (val.item_context_builder)(item); let context_menu = (val.item_context_builder)(item);
widget::column::with_capacity(2) widget::column()
.spacing(val.item_spacing) .spacing(val.item_spacing)
.push( .push(
widget::divider::horizontal::default() widget::divider::horizontal::default()
@ -73,7 +73,7 @@ where
.padding(val.divider_padding), .padding(val.divider_padding),
) )
.push( .push(
widget::row::with_capacity(2) widget::row()
.spacing(space_xxxs) .spacing(space_xxxs)
.align_y(Alignment::Center) .align_y(Alignment::Center)
.push_maybe( .push_maybe(
@ -81,7 +81,7 @@ where
.map(|icon| icon.size(val.icon_size)), .map(|icon| icon.size(val.icon_size)),
) )
.push( .push(
widget::column::with_capacity(2) widget::column()
.push(widget::text::body(item.get_text(Category::default()))) .push(widget::text::body(item.get_text(Category::default())))
.push({ .push({
let mut elements = val let mut elements = val

View file

@ -99,7 +99,7 @@ where
}; };
// Build the category header // Build the category header
widget::row::with_capacity(2) widget::row()
.spacing(val.icon_spacing) .spacing(val.icon_spacing)
.push(widget::text::heading(category.to_string())) .push(widget::text::heading(category.to_string()))
.push_maybe(match sort_state { .push_maybe(match sort_state {
@ -152,7 +152,7 @@ where
categories categories
.iter() .iter()
.map(|category| { .map(|category| {
widget::row::with_capacity(2) widget::row()
.spacing(val.icon_spacing) .spacing(val.icon_spacing)
.push_maybe( .push_maybe(
item.get_icon(*category) item.get_icon(*category)

View file

@ -3,19 +3,16 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
//! Track the cursor of a text input. //! Track the cursor of a text input.
use iced_core::text::Affinity;
use super::value::Value; use super::value::Value;
/// The cursor of a text input. /// The cursor of a text input.
#[derive(Debug, Copy, Clone, PartialEq, Eq)] #[derive(Debug, Copy, Clone)]
pub struct Cursor { pub struct Cursor {
state: State, state: State,
affinity: Affinity,
} }
/// The state of a [`Cursor`]. /// The state of a [`Cursor`].
#[derive(Debug, Copy, Clone, PartialEq, Eq)] #[derive(Debug, Copy, Clone)]
pub enum State { pub enum State {
/// Cursor without a selection /// Cursor without a selection
Index(usize), Index(usize),
@ -34,7 +31,6 @@ impl Default for Cursor {
fn default() -> Self { fn default() -> Self {
Self { Self {
state: State::Index(0), state: State::Index(0),
affinity: Affinity::Before,
} }
} }
} }
@ -197,37 +193,4 @@ impl Cursor {
State::Selection { start, end } => start.max(end), 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),
}
}
} }

File diff suppressed because it is too large Load diff

View file

@ -132,42 +132,11 @@ impl Value {
graphemes: std::iter::repeat_n(String::from(""), self.graphemes.len()).collect(), 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<usize> {
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] #[inline]
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn to_string(&self) -> String {
f.write_str(&self.graphemes.concat()) self.graphemes.concat()
} }
} }

View file

@ -34,10 +34,10 @@ pub fn toaster<'a, Message: Clone + 'static>(
} = theme.cosmic().spacing; } = theme.cosmic().spacing;
let make_toast = move |(id, toast): (ToastId, &'a Toast<Message>)| { let make_toast = move |(id, toast): (ToastId, &'a Toast<Message>)| {
let row = row::with_capacity(2) let row = row()
.push(text(&toast.message)) .push(text(&toast.message))
.push( .push(
row::with_capacity(2) row()
.push_maybe(toast.action.as_ref().map(|action| { .push_maybe(toast.action.as_ref().map(|action| {
button::text(&action.description).on_press((action.message)(id)) button::text(&action.description).on_press((action.message)(id))
})) }))

View file

@ -2,18 +2,18 @@
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use crate::{Element, anim}; use crate::{Element, anim, iced_core::Border, iced_widget::toggler::Status};
use iced_core::{ use iced_core::{
Border, Clipboard, Event, Layout, Length, Pixels, Rectangle, Shell, Size, Widget, alignment, Clipboard, Event, Layout, Length, Pixels, Rectangle, Shell, Size, Widget, alignment, event,
event, layout, mouse, layout, mouse,
renderer::{self, Renderer}, renderer::{self, Renderer},
text, touch, text,
widget::{self, Tree, tree}, widget::{self, Tree, tree},
window, 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> { pub fn toggler<'a, Message>(is_checked: bool) -> Toggler<'a, Message> {
Toggler::new(is_checked) Toggler::new(is_checked)
@ -161,10 +161,7 @@ impl<'a, Message> Widget<Message, crate::Theme, crate::Renderer> for Toggler<'a,
} }
fn state(&self) -> tree::State { fn state(&self) -> tree::State {
tree::State::new(State { tree::State::new(State::default())
prev_toggled: self.is_toggled,
..State::default()
})
} }
fn id(&self) -> Option<Id> { fn id(&self) -> Option<Id> {
@ -203,7 +200,7 @@ impl<'a, Message> Widget<Message, crate::Theme, crate::Renderer> for Toggler<'a,
align_x: self.text_alignment, align_x: self.text_alignment,
align_y: alignment::Vertical::Top, align_y: alignment::Vertical::Top,
shaping: self.text_shaping, shaping: self.text_shaping,
wrapping: iced_core::text::Wrapping::default(), wrapping: crate::iced_core::text::Wrapping::default(),
ellipsize: self.ellipsize, ellipsize: self.ellipsize,
}, },
); );
@ -241,23 +238,13 @@ impl<'a, Message> Widget<Message, crate::Theme, crate::Renderer> for Toggler<'a,
return; return;
}; };
let state = tree.state.downcast_mut::<State>(); let state = tree.state.downcast_mut::<State>();
// 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 { match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
| Event::Touch(touch::Event::FingerPressed { .. }) => {
let mouse_over = cursor_position.is_over(layout.bounds()); let mouse_over = cursor_position.is_over(layout.bounds());
if mouse_over { if mouse_over {
shell.publish((on_toggle)(!self.is_toggled)); shell.publish((on_toggle)(!self.is_toggled));
state.anim.changed(self.duration); state.anim.changed(self.duration);
state.prev_toggled = !self.is_toggled;
shell.capture_event(); shell.capture_event();
} }
} }
@ -442,5 +429,4 @@ pub fn next_to_each_other(
pub struct State { pub struct State {
text: widget::text::State<<crate::Renderer as iced_core::text::Renderer>::Paragraph>, text: widget::text::State<<crate::Renderer as iced_core::text::Renderer>::Paragraph>,
anim: anim::State, anim: anim::State,
prev_toggled: bool,
} }

Some files were not shown because too many files have changed in this diff Show more