Compare commits

..

78 commits

Author SHA1 Message Date
Vukašin Vojinović
95756b1a57 improv(circular): prevent caps from touching 2026-04-18 16:08:34 -04:00
Vukašin Vojinović
c423ad1bfc improv(about): use ListButton 2026-04-17 13:52:08 +02:00
Vukašin Vojinović
8d7bcab258 fix(list_column): add back divider_padding
Also matches previous behavior of both paddings being applied to subsequent items, rather than globally.
2026-04-17 13:17:23 +02:00
Hojjat
c162a1f24a fix(animated-image): update frames and fix compilation errors 2026-04-16 19:58:39 +02:00
Vukašin Vojinović
3f9e93067b fix(item builder): remove unnecessary lifetime bound for radio 2026-04-16 18:26:17 +02:00
Vukašin Vojinović
917af9fda2 feat(radio): internal method for radio without label
Also adds the related settings item builder.
2026-04-16 17:19:36 +02:00
Vukašin Vojinović
9b465a8b5c feat(list_column): button list items 2026-04-16 17:19:36 +02:00
Vukašin Vojinović
9cac422c24 fix(toggler): animate external changes 2026-04-16 17:19:36 +02:00
Hojjat
0fc4638af3 fix: register image_extras in run_single_instance too 2026-04-15 23:59:57 +02:00
Hojjat
3d8d8915be chore: enable ico and xpm image support for desktop feature 2026-04-15 13:10:25 +02:00
Ian Douglas Scott
46d9f0c344 widget/icon: Bundle icons on macOS, not just Windows 2026-04-14 21:46:05 +02:00
Jeremy Soller
0d69cd9183
i18n: translation update from Hosted Weblate (#1177)
Translations update from [Hosted Weblate](https://hosted.weblate.org)
for [Pop
OS/libcosmic](https://hosted.weblate.org/projects/pop-os/libcosmic/).



Current translation status:

![Weblate translation
status](https://hosted.weblate.org/widget/pop-os/libcosmic/horizontal-auto.svg)
2026-04-14 09:52:02 -06:00
Hojjat
52116d2f36 chore: update iced 2026-04-13 22:26:33 +02:00
Hosted Weblate
0e72508dcc
i18n: translation updates from weblate
Co-authored-by: Amadɣas <massiin@proton.me>
Co-authored-by: Asier Saratsua Garmendia <asier.sarasua@gmail.com>
Co-authored-by: ButterflyOfFire <boffire@users.noreply.hosted.weblate.org>
Co-authored-by: Ettore Atalan <atalanttore@googlemail.com>
Co-authored-by: Geeson Wan <wang14240@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: 麋麓 BigELK176 <BigELK176@gmail.com>
Co-authored-by: 김유빈 <k.sein1016@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/de/
Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/kab/
Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/ko/
Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/zh_Hant/
Translation: Pop OS/libcosmic
2026-04-12 18:50:19 +02:00
Jeremy Soller
1d7113a244
chore: update iced (#1240)
- [x] I have disclosed use of any AI generated code in my commit
messages.
- If you are using an LLM, and do not fully understand the changes it is
making to the code base, do not create a PR.
- In our experience, AI generated code often results in overly complex
code that lacks enough context for a proper fix or feature inclusion.
This results in considerably longer code reviews. Due to this, AI
authored or partially authored PRs may be closed without comment.
- [x] I understand these changes in full and will be able to respond to
review comments.
- [x] My change is accurately described in the commit message.
- [x] My contribution is tested and working as described.
- [x] I have read the [Developer Certificate of
Origin](https://developercertificate.org/) and certify my contribution
under its conditions.
2026-04-11 06:27:56 -06:00
Hojjat
e287a789c1 chore: update iced 2026-04-10 20:53:43 -06:00
Hojjat
6caccaba33 fix: icon color when window is maximized 2026-04-09 12:54:32 -04:00
Ashley Wulber
a44cff8011 fix(text_input): always clip input text with the text bounds
this issue seems unique to tiny-skia
2026-04-08 17:05:40 +02:00
Ashley Wulber
47ab72be50
fix!(progress_bar): remove unused generic Message type 2026-04-08 07:38:18 +02:00
Adam Cosner
c7093beca3 fix(ci): cargo now running properly 2026-04-08 07:34:13 +02:00
Adam Cosner
77b37f2246 fix(ci) removed the smithay and wayland protocol docs builds 2026-04-08 07:04:54 +02:00
Adam Cosner
6df3f76a33 ci: Added a few more enabled dependency docs 2026-04-08 07:04:54 +02:00
Adam Cosner
12d2233c6b fix(ci): Added an inline doc to cctk reexport 2026-04-08 07:04:54 +02:00
Adam Cosner
e5955b568d ci: Updated pages.yml workflow
Use nightly channel to enable docs generating feature badges, plus enabled more features in the docs build, and building the cctk docs also
2026-04-08 07:04:54 +02:00
Adam Cosner
5d1dfc4c54
refactor!: remove cosmic::iced_* re-exports 2026-04-08 03:12:10 +02:00
Ashley Wulber
d9121d6f0d refactor: better helpers for the progress_bar 2026-04-07 21:47:46 +02:00
Ashley Wulber
b963fbfea9
feat(widget): progress bars 2026-04-07 17:02:58 +02:00
Hojjat
724351727a feat: select until char and double click select delimiter
adds a feature to select from the start of the sentence until the last
occurrence of a character. This can be used to select until the
extension in cosmic-files save dialog or rename pop up.

Also, it adds a feature to select until the last occurrence of a
character on double-click.
2026-04-07 13:35:26 +02:00
Hojjat
1f87cbc883 fix: do not allow cursor or keyboard activity when popup is open
traps Tab from escaping, and won't allow elements in the background to
react to hover
2026-04-07 13:32:21 +02:00
Ashley Wulber
9aa87cd66b fix(segmented_button): active font for context menu & prioritize active font over hover 2026-04-06 18:57:27 -04:00
Hojjat
ab3eedd0f2 chore: update iced
This pulls in the fix in cosmic-text to fallback to the default
SansSerif if there are missing glyphs in basic shaping.

Also removes advanced-shaping from the default features list.
2026-04-06 15:15:30 -04:00
KENZ
8e3672a7dd fix: focus detecting in IME logic 2026-04-06 15:59:18 +02:00
Hojjat
1d01054993 chore: update iced
pulls in fixes for cycling focus
2026-04-03 19:23:40 -04:00
Vukašin Vojinović
fdf3369cea chore: re-export iced row and column
This removes the custom row and column implementations and uses the iced ones directly.
2026-04-03 20:39:31 +02:00
Vukašin Vojinović
a9e0671075 fix(segmented_button): hover text style 2026-04-03 20:36:23 +02:00
Ashley Wulber
34219d1fd4 chore: wgpu cctk feature for wayland 2026-04-03 20:15:31 +02:00
Ashley Wulber
cdd825b953 fix: update iced
softbuffer released version doesn't support transparency yet
2026-04-03 16:17:44 +02:00
Ashley Wulber
b0f4e931f2 fix: font issues
some fonts are not falling back when a glyph is missing for a selected font and weight
2026-04-03 16:17:44 +02:00
Hendrik Hamerlinck
97a805e5a1 feat(applets): add destroy tooltip popup action
This commit adds a new surface action to explicitly destroy the tooltip
popup on `TOOLTIP_WINDOW_ID`, allowing proper cleanup when minimizing
applets.
2026-04-03 08:26:29 -04:00
Hojjat
24464908f6 fix: buttons are focusable again 2026-04-03 02:28:00 +02:00
GroobleDierne
7a02c9a296 fix(color palette): avoid duplicates 2026-04-02 16:21:50 -04:00
Hojjat
61e5d882ae fix(ci): only document libcosmic, no dependency 2026-04-02 15:47:43 -04:00
Hojjat
12be83a8ef chore: update iced 2026-04-01 22:14:07 -04:00
KENZ
f6eb314606
feat(text_input): minimal IME support for COSMIC specific text widgets 2026-04-02 00:35:57 +02:00
TobyDig
0ba668eb52
fix(desktop): use -e argument for spawning desktop entries with a terminal 2026-04-01 23:32:36 +02:00
Hojjat
aef328238f fix(editable): the UX is closer to design now
This fixes the unresponsive trailing icon and changes the behavior to be
closer to the UI/UX design.
2026-04-01 23:29:26 +02:00
Hojjat
22661fd764 chore: udpate iced 2026-04-01 23:26:42 +02:00
Hojjat
e1738d2ea7 fix(text_input): keyboard shortcuts when keyboard is a different language
Matches what Iced does
2026-04-01 23:26:42 +02:00
Hojjat
2299fba69b fix(text_input): RTL text cursor and highlight fixes 2026-04-01 23:26:42 +02:00
Adil Hanney
c33455e9ad test: use default dark theme, not real system theme 2026-04-01 23:23:37 +02:00
Adil Hanney
9a72fe6c2d fix: complementary should be dark not light 2026-04-01 23:23:37 +02:00
Adil Hanney
39e8300d90 test: snapshots of kcolorscheme and qpalette
AI disclosure: I asked GitHub Copilot (Claude Haiku 4.5) "What's the best way to add tests for my recently merged qt theming contributions?" It suggested the insta crate for golden testing the output strings as well as some unit tests. I implemented it myself.
2026-04-01 23:23:37 +02:00
Adil Hanney
f734ccbbde test: fix expected color value 2026-04-01 23:23:37 +02:00
Adil Hanney
e86304cf3f ref: use assert_eq not assert
This way, the test log can show the expected and actual result if it fails.

thread 'steps::tests::test_conversion_fallback_colors' (61338) panicked at cosmic-theme/src/steps.rs:213:9:
assertion `left == right` failed
  left: 102
 right: 103
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
2026-04-01 23:23:37 +02:00
Adil Hanney
672f9047a2 test: use almost::zero instead of almost::equal as per documentation
"Do not use this to compare a value with a constant zero. Instead, for this you should use almost::zero."
2026-04-01 23:23:37 +02:00
Adil Hanney
8b52592f2d ci: test cosmic-theme 2026-04-01 23:23:37 +02:00
Ashley Wulber
d631f9d6d7
chore: update iced 2026-04-01 23:21:27 +02:00
Ashley Wulber
4541c6a275 fix: example deps 2026-03-31 21:34:26 +02:00
Ashley Wulber
1433b89e40 chore: update iced 2026-03-31 21:34:26 +02:00
Adil Hanney
f06d15ae35
feat(cosmic-theme): produce QPalette ini for more compatibility 2026-03-31 17:02:52 +02:00
Ashley Wulber
413e63f62a chore: update features and feature gates 2026-03-30 22:25:27 -04:00
Ítalo Dell Areti
380b341bdc feat(text_input): add select_range method and Task function 2026-03-28 00:09:34 -04:00
Ashley Wulber
254c13cfc4 fix: ellipsize text in menu items 2026-03-27 21:22:54 +01:00
Ashley Wulber
e63f3196e2 fix: MenuActive path highlight 2026-03-27 21:21:59 +01:00
Hojjat
a38a6f5d73 fix(ci): install dependencies 2026-03-27 01:22:25 +01:00
Ashley Wulber
763f0da64c
fix(iced): RTL text fix 2026-03-26 22:19:39 +01:00
Ashley Wulber
adb3e341fc fix(theme): bright colors for success, warn, destructive 2026-03-25 19:04:30 +01:00
Ashley Wulber
8e439c842c chore: update iced 2026-03-24 01:25:57 +01:00
Frederic Laing
d7fd880ac6 fix(toggler): add touch input support 2026-03-23 10:22:04 -04:00
Hojjat
141261b9bf chore: update iced 2026-03-23 10:21:15 -04:00
Hojjat
c804d3851d fix: don't ever draw glyphs outside of the bounds 2026-03-23 10:21:15 -04:00
Hojjat
dc3ebaa38e feat(segmented_button): add ellipsize support 2026-03-23 10:21:15 -04:00
Hojjat
7a56762422 fix: restore width and height fill for app content 2026-03-20 22:23:16 +01:00
Ashley Wulber
36cba695d2 chore: update iced 2026-03-20 16:04:48 +01:00
Hojjat
3da55e8074 fix(flex_row): calculate height based on nodes 2026-03-18 15:54:33 +01:00
Vukašin Vojinović
54bcb9ec12
chore: update dependencies and examples 2026-03-18 15:54:07 +01:00
Ashley Wulber
6c6d16d34a
fix(iced): scaling issue in the cosmic-greeter lock screen 2026-03-18 15:53:09 +01:00
Ashley Wulber
c7ac9cfd31 fix: if not in bounds, return default mouse interaction 2026-03-17 20:51:22 +01:00
101 changed files with 3590 additions and 1156 deletions

View file

@ -33,16 +33,17 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
features: test_args:
- "" # for cosmic-comp, don't remove! - --no-default-features --features "" # for cosmic-comp, don't remove!
- 'winit_debug' - --no-default-features --features "winit_debug"
- 'winit_tokio' - --no-default-features --features "winit_tokio"
- winit - --no-default-features --features "winit"
- winit_wgpu - --no-default-features --features "winit_wgpu"
- wayland - --no-default-features --features "wayland"
- applet - --no-default-features --features "applet"
- desktop,smol - --no-default-features --features "desktop,smol"
- desktop,tokio - --no-default-features --features "desktop,tokio"
- -p cosmic-theme
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- name: Checkout sources - name: Checkout sources
@ -66,7 +67,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 --no-default-features --features "${{ matrix.features }}" -- --test-threads=1 run: cargo test ${{ matrix.test_args }} -- --test-threads=1
env: env:
RUST_BACKTRACE: full RUST_BACKTRACE: full
@ -103,7 +104,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: Test example - name: Check example
run: cargo check -p "${{ matrix.examples }}" run: cargo check -p "${{ matrix.examples }}"
env: env:
RUST_BACKTRACE: full RUST_BACKTRACE: full

View file

@ -7,7 +7,6 @@ on:
jobs: jobs:
pages: pages:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@ -15,8 +14,20 @@ 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: cargo doc --verbose --features tokio,winit run: |
RUSTDOCFLAGS="--cfg docsrs" \
cargo +nightly-2025-07-31 doc --no-deps \
-p cosmic-client-toolkit \
-p cosmic-protocols \
-p libcosmic \
--verbose --features tokio,winit,wayland,desktop,single-instance,applet,xdg-portal,multi-window
- name: Deploy documentation - name: Deploy documentation
uses: peaceiris/actions-gh-pages@v3 uses: peaceiris/actions-gh-pages@v3
with: with:

View file

@ -14,9 +14,10 @@ default = [
"a11y", "a11y",
"dbus-config", "dbus-config",
"x11", "x11",
"wayland", "iced-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
@ -57,6 +58,7 @@ 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",
@ -80,15 +82,21 @@ tokio = [
] ]
# Tokio async runtime # Tokio async runtime
# Wayland window support # Wayland window support
wayland = [ iced-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 = []
@ -115,10 +123,10 @@ x11 = ["iced/x11", "iced_winit/x11"]
[dependencies] [dependencies]
apply = "0.3.0" apply = "0.3.0"
ashpd = { version = "0.12.1", default-features = false, optional = true } ashpd = { version = "0.12.3", 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.7" auto_enums = "0.8.8"
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" }
@ -131,17 +139,21 @@ 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.8" derive_setters = "0.1.9"
futures = "0.3" futures = "0.3"
image = { version = "0.25.9", default-features = false, features = [ image = { version = "0.25.10", default-features = false, features = [
"ico",
"jpeg", "jpeg",
"png", "png",
] } ] }
libc = { version = "0.2.180", optional = true } image-extras = { version = "0.1.0", default-features = false, features = [
"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 }
@ -151,25 +163,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.49.0", optional = true } tokio = { version = "1.50.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.13.2", default-features = false, optional = true } zbus = { version = "5.14.0", 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.13.2", default-features = false } zbus = { version = "5.14.0", default-features = false }
[target.'cfg(unix)'.dependencies] [target.'cfg(all(unix, not(target_os = "macos")))'.dependencies]
freedesktop-icons = { package = "cosmic-freedesktop-icons", git = "https://github.com/pop-os/freedesktop-icons" } freedesktop-icons = { package = "cosmic-freedesktop-icons", 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(not(unix))'.dependencies] [target.'cfg(any(not(unix), target_os = "macos"))'.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"] }
@ -242,4 +254,4 @@ exclude = ["iced"]
dirs = "6.0.0" dirs = "6.0.0"
[dev-dependencies] [dev-dependencies]
tempfile = "3.24.0" tempfile = "3.27.0"

View file

@ -3,7 +3,9 @@ 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.13.2", default-features = false, optional = true } zbus = { version = "5.14.0", 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.3", optional = true } calloop = { version = "0.14.4", 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.49", optional = true, features = ["time"] } tokio = { version = "1.50", 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.0" known-folders = "1.4.2"

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.1", features = ["serde"] } csscolorparser = { version = "0.8.3", 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,3 +30,10 @@ 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().accent_green palette.as_ref().bright_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().accent_yellow palette.as_ref().bright_orange
}; };
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().accent_red palette.as_ref().bright_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,8 +46,10 @@ 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(())
} }
@ -56,8 +58,10 @@ 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,8 +1,11 @@
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};
@ -15,7 +18,117 @@ 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 = 1; const COSMIC_QT_VERSION: u64 = 2;
/// Produces a QPalette ini file for qt5ct and qt6ct.
///
/// Example file: https://github.com/trialuser02/qt6ct/blob/master/colors/airy.conf
#[must_use]
#[cold]
pub fn as_qpalette(&self) -> String {
let lightest = if self.is_dark {
self.background.on
} else {
self.background.base
};
let darkest = if self.is_dark {
self.background.base
} else {
self.background.on
};
let active = QPaletteGroup {
window_text: self.background.on,
button: self.button.base,
light: self.button.base.mix(lightest, 0.1),
midlight: self.button.base.mix(lightest, 0.05),
dark: self.button.base.mix(darkest, 0.1),
mid: self.button.base.mix(darkest, 0.05),
text: self.background.component.on,
bright_text: lightest,
button_text: self.button.on,
base: self.background.component.base,
window: self.background.base,
shadow: darkest,
// selection colors are swapped to fix menu bar contrast
highlight: self.background.component.selected_text,
highlighted_text: self.background.component.selected,
link: self.link_button.on,
link_visited: self.link_button.on.mix(self.secondary.component.base, 0.2),
alternate_base: self.background.base.mix(self.accent.base, 0.05),
no_role: self.background.component.disabled,
tool_tip_base: self.background.component.base,
tool_tip_text: self.background.component.on,
placeholder_text: self.background.component.on.with_alpha(0.5),
};
let inactive = QPaletteGroup {
window_text: active.window_text.with_alpha(0.8),
text: active.text.with_alpha(0.8),
highlighted_text: active.highlighted_text.with_alpha(0.8),
tool_tip_text: active.tool_tip_text.with_alpha(0.8),
..active
};
let disabled = QPaletteGroup {
button: self.button.disabled,
text: self.background.component.on_disabled,
button_text: self.button.on_disabled,
base: self.background.component.disabled,
highlighted_text: active.highlighted_text.with_alpha(0.5),
link: self.link_button.on_disabled,
link_visited: self
.link_button
.on_disabled
.mix(self.secondary.component.disabled, 0.2),
alternate_base: self.background.base.mix(self.accent.disabled, 0.05),
tool_tip_base: self.background.component.disabled,
tool_tip_text: self.background.component.on_disabled,
placeholder_text: self.background.component.on_disabled.with_alpha(0.5),
..inactive
};
format!(
r#"# GENERATED BY COSMIC
[ColorScheme]
active_colors={}
disabled_colors={}
inactive_colors={}
"#,
active.as_list(),
disabled.as_list(),
inactive.as_list(),
)
}
/// Writes the QPalette ini files to:
/// - `~/.config/qt6ct/colors/`
/// - `~/.config/qt5ct/colors/`
#[cold]
pub fn write_qt56ct(&self) -> Result<(), OutputError> {
let qpalette = self.as_qpalette();
let qt5ct_res = self.write_ct("qt5ct", &qpalette);
let qt6ct_res = self.write_ct("qt6ct", &qpalette);
qt5ct_res?;
qt6ct_res?;
Ok(())
}
#[must_use]
#[cold]
fn write_ct(&self, ct: &str, qpalette: &str) -> Result<(), OutputError> {
let file_path = Self::get_qpalette_path(ct, self.is_dark)?;
let tmp_file_path = file_path.with_extension("conf.new");
let mut tmp_file = File::create(&tmp_file_path).map_err(OutputError::Io)?;
let res = tmp_file
.write_all(qpalette.as_bytes())
.and_then(|_| tmp_file.flush())
.and_then(|_| std::fs::rename(&tmp_file_path, file_path));
if let Err(e) = res {
_ = std::fs::remove_file(&tmp_file_path);
return Err(OutputError::Io(e));
}
Ok(())
}
/// 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]
@ -39,7 +152,7 @@ impl Theme {
.map_err(OutputError::Ini)? .map_err(OutputError::Ini)?
.unwrap_or_default(); .unwrap_or_default();
let color_scheme_path = Self::get_qt_colors_path(is_dark)?; let color_scheme_path = Self::get_qpalette_path(ct, 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(
@ -91,11 +204,48 @@ impl Theme {
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);
}; };
@ -111,4 +261,155 @@ impl Theme {
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,10 +14,11 @@ 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://web.archive.org/web/20250402234329/https://docs.kde.org/stable5/en/plasma-workspace/kcontrol/colors/ /// - https://api.kde.org/kcolorscheme.html
/// - 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_qt(&self) -> String { pub fn as_kcolorscheme(&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,
@ -41,7 +42,7 @@ impl Theme {
let bg = self.background.base; let bg = self.background.base;
// the background container // the background container
let view_colors = IniColors { let window_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(),
@ -56,16 +57,17 @@ 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 window_colors = IniColors { let view_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,
..view_colors ..window_colors
}; };
// selected text and items // selected text and items
let selection_colors = { let selection_colors = {
let selected = self.background.component.selected; // selection colors are swapped to fix menu bar contrast
let selected_text = self.background.component.selected_text; let selected = 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,
@ -92,8 +94,11 @@ 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::light_config() Theme::dark_config()
.ok() .ok()
.as_ref() .as_ref()
.and_then(|conf| Theme::get_entry(conf).ok()) .and_then(|conf| Theme::get_entry(conf).ok())
@ -116,10 +121,10 @@ impl Theme {
}; };
// headers in cosmic don't have a background // headers in cosmic don't have a background
let header_colors = &view_colors; let header_colors = &window_colors;
let header_colors_inactive = &view_colors; let header_colors_inactive = &window_colors;
// tool tips, "What's This" tips, and similar elements // tool tips, "What's This" tips, and similar elements
let tooltip_colors = &window_colors; let tooltip_colors = &view_colors;
let general_color_scheme = if self.is_dark { let general_color_scheme = if self.is_dark {
"CosmicDark" "CosmicDark"
@ -198,7 +203,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(&view_colors, self.is_dark), format_ini_wm_colors(&window_colors, self.is_dark),
) )
} }
@ -212,14 +217,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 colors = self.as_qt(); let kcolorscheme = self.as_kcolorscheme();
let file_path = Self::get_qt_colors_path(self.is_dark)?; let file_path = Self::get_kcolorscheme_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(colors.as_bytes()) .write_all(kcolorscheme.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 {
@ -245,7 +250,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_qt_colors_path(is_dark)?; let src_file = Self::get_kcolorscheme_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)
@ -288,7 +293,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_qt_colors_path(is_dark)?; let src_file = Self::get_kcolorscheme_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() {
@ -303,8 +308,8 @@ widgetStyle=qt6ct-style
Ok(()) Ok(())
} }
/// Gets a path like `~/.config/color-schemes/CosmicDark.colors` /// Gets a path like `~/.local/share/color-schemes/CosmicDark.colors`
pub fn get_qt_colors_path(is_dark: bool) -> Result<PathBuf, OutputError> { fn get_kcolorscheme_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);
}; };
@ -520,3 +525,44 @@ 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

@ -0,0 +1,10 @@
---
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

@ -0,0 +1,10 @@
---
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

@ -0,0 +1,157 @@
---
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

@ -0,0 +1,157 @@
---
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,7 +145,6 @@ 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};
@ -173,57 +172,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);
equal(srgb.red, 0.0); almost::zero(srgb.red);
equal(srgb.blue, 0.0); almost::zero(srgb.blue);
equal(srgb.green, 0.0); almost::zero(srgb.green);
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);
equal(srgb.red, 1.0); almost::equal(srgb.red, 1.0);
equal(srgb.blue, 1.0); almost::equal(srgb.blue, 1.0);
equal(srgb.green, 1.0); almost::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!(srgb.red == 133); assert_eq!(srgb.red, 133);
assert!(srgb.green == 69); assert_eq!(srgb.green, 69);
assert!(srgb.blue == 0); assert_eq!(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!(srgb.red == 78); assert_eq!(srgb.red, 78);
assert!(srgb.green == 27); assert_eq!(srgb.green, 27);
assert!(srgb.blue == 15); assert_eq!(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!(srgb.red == 192); assert_eq!(srgb.red, 192);
assert!(srgb.green == 153); assert_eq!(srgb.green, 153);
assert!(srgb.blue == 253); assert_eq!(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!(srgb.red == 255); assert_eq!(srgb.red, 255);
assert!(srgb.green == 103); assert_eq!(srgb.green, 102);
assert!(srgb.blue == 65); assert_eq!(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!(srgb.red == 193); assert_eq!(srgb.red, 193);
assert!(srgb.green == 152); assert_eq!(srgb.green, 152);
assert!(srgb.blue == 255); assert_eq!(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!(srgb.red == 1); assert_eq!(srgb.red, 1);
assert!(srgb.green == 19); assert_eq!(srgb.green, 19);
assert!(srgb.blue == 0); assert_eq!(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() widget::column::with_capacity(1)
.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]
git = "https://github.com/pop-os/libcosmic" path = "../../"
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_core::theme::Style> { fn style(&self) -> Option<cosmic::iced::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]
git = "https://github.com/pop-os/libcosmic" path = "../../"
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,6 +82,7 @@ pub enum Message {
Hi, Hi,
Hi2, Hi2,
Hi3, Hi3,
Tick,
} }
/// The [`App`] stores application-specific state. /// The [`App`] stores application-specific state.
@ -92,6 +93,7 @@ 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.
@ -133,6 +135,7 @@ 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();
@ -178,10 +181,17 @@ 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
@ -190,7 +200,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() widget::column::with_capacity(14)
.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)
@ -212,6 +222,47 @@ 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,8 +85,6 @@ 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),
@ -95,9 +93,7 @@ impl cosmic::Application for App {
Weekday::Sunday, Weekday::Sunday,
); );
content = content.push(calendar); let centered = cosmic::widget::container(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_core::Size; use cosmic::iced::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,13 +28,14 @@ impl State {
column!( column!(
list_column().add(settings::item( list_column().add(settings::item(
"Bluetooth", "Bluetooth",
toggler(None, self.enabled, Message::Enable) toggler(self.enabled).on_toggle(Message::Enable)
)), )),
text("Now visible as \"TODO\", just kidding") text("Now visible as \"TODO\", just kidding")
) )
.spacing(8) .spacing(8)
.into(), .into(),
settings::view_section("Devices") settings::section()
.title("Devices")
.add(settings::item("No devices found", text(""))) .add(settings::item("No devices found", text("")))
.into(), .into(),
]) ])

View file

@ -258,12 +258,13 @@ 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::view_section("Debug") settings::section()
.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(None, window.debug, Message::Debug), toggler(window.debug).on_toggle(Message::Debug),
)) ))
.add(settings::item( .add(settings::item(
"Scaling Factor", "Scaling Factor",
@ -276,10 +277,11 @@ impl State {
.into(), .into(),
])) ]))
.into(), .into(),
settings::view_section("Controls") settings::section()
.title("Controls")
.add(settings::item( .add(settings::item(
"Toggler", "Toggler",
toggler(None, self.toggler_value, Message::TogglerToggled), toggler(self.toggler_value).on_toggle(Message::TogglerToggled),
)) ))
.add(settings::item( .add(settings::item(
"Pick List (TODO)", "Pick List (TODO)",
@ -299,14 +301,12 @@ 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)
.width(Length::Fixed(250.0)) .length(Length::Fixed(250.0))
.height(Length::Fixed(4.0)), .girth(Length::Fixed(4.0)),
)) ))
.add(settings::item_row(vec![checkbox( .add(settings::item_row(vec![checkbox(self.checkbox_value)
"Checkbox", .label("Checkbox")
self.checkbox_value, .on_toggle(Message::CheckboxToggled)
Message::CheckboxToggled,
)
.into()])) .into()]))
.add(settings::item( .add(settings::item(
format!( format!(
@ -354,8 +354,7 @@ impl State {
.width(Length::Shrink) .width(Length::Shrink)
.on_activate(Message::MultiSelection) .on_activate(Message::MultiSelection)
.apply(container) .apply(container)
.center_x() .center_x(Length::Fill)
.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![
@ -424,13 +423,12 @@ impl State {
]) ])
.padding(0) .padding(0)
.into(), .into(),
Some(DemoView::TabC) => { Some(DemoView::TabC) => settings::view_column(vec![settings::section()
settings::view_column(vec![settings::view_section("Tab C") .title("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,7 +147,8 @@ 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::view_section("Super Key Action") settings::section()
.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(
@ -155,38 +156,34 @@ impl State {
horizontal_space(Length::Fill), horizontal_space(Length::Fill),
)) ))
.into(), .into(),
settings::view_section("Hot Corner") settings::section()
.title("Hot Corner")
.add(settings::item( .add(settings::item(
"Enable top-left hot corner for Workspaces", "Enable top-left hot corner for Workspaces",
toggler(None, self.top_left_hot_corner, Message::TopLeftHotCorner), toggler(self.top_left_hot_corner).on_toggle(Message::TopLeftHotCorner),
)) ))
.into(), .into(),
settings::view_section("Top Panel") settings::section()
.title("Top Panel")
.add(settings::item( .add(settings::item(
"Show Workspaces Button", "Show Workspaces Button",
toggler( toggler(self.show_workspaces_button).on_toggle(Message::ShowWorkspacesButton),
None,
self.show_workspaces_button,
Message::ShowWorkspacesButton,
),
)) ))
.add(settings::item( .add(settings::item(
"Show Applications Button", "Show Applications Button",
toggler( toggler(self.show_applications_button)
None, .on_toggle(Message::ShowApplicationsButton),
self.show_applications_button,
Message::ShowApplicationsButton,
),
)) ))
.into(), .into(),
settings::view_section("Window Controls") settings::section()
.title("Window Controls")
.add(settings::item( .add(settings::item(
"Show Minimize Button", "Show Minimize Button",
toggler(None, self.show_minimize_button, Message::ShowMinimizeButton), toggler(self.show_minimize_button).on_toggle(Message::ShowMinimizeButton),
)) ))
.add(settings::item( .add(settings::item(
"Show Maximize Button", "Show Maximize Button",
toggler(None, self.show_maximize_button, Message::ShowMaximizeButton), toggler(self.show_maximize_button).on_toggle(Message::ShowMaximizeButton),
)) ))
.into(), .into(),
]) ])
@ -245,12 +242,12 @@ impl State {
list_column() list_column()
.add(settings::item( .add(settings::item(
"Same background on all displays", "Same background on all displays",
toggler(None, self.same_background, Message::SameBackground), toggler(self.same_background).on_toggle(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(None, self.slideshow, Message::Slideshow), toggler(self.slideshow).on_toggle(Message::Slideshow),
)) ))
.into(), .into(),
column(image_column).spacing(16).into(), column(image_column).spacing(16).into(),
@ -261,7 +258,8 @@ 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::view_section("Workspace Behavior") settings::section()
.title("Workspace Behavior")
.add(settings::item( .add(settings::item(
"Dynamic workspaces", "Dynamic workspaces",
horizontal_space(Length::Fill), horizontal_space(Length::Fill),
@ -271,7 +269,8 @@ impl State {
horizontal_space(Length::Fill), horizontal_space(Length::Fill),
)) ))
.into(), .into(),
settings::view_section("Multi-monitor Behavior") settings::section()
.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,14 +69,16 @@ impl State {
list_column() list_column()
.add(settings::item("Device name", text("TODO"))) .add(settings::item("Device name", text("TODO")))
.into(), .into(),
settings::view_section("Hardware") settings::section()
.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::view_section("Operating System") settings::section()
.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",
@ -85,7 +87,8 @@ 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::view_section("Related settings") settings::section()
.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().spacing(12); let mut content = cosmic::widget::column::with_capacity(self.images.len()).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_core::alignment::{Horizontal, Vertical}; use cosmic::iced::{Length, Size};
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_core::Size; use cosmic::iced::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_core::Length; use cosmic::iced::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().into() widget::Row::new().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_core::Size; use cosmic::iced::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,7 +99,9 @@ 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().push(editable).push(inline); let column = cosmic::widget::column::with_capacity(2)
.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 }

0
i18n/eu/libcosmic.ftl Normal file
View file

View file

@ -0,0 +1,33 @@
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,26 +2,33 @@ 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

@ -0,0 +1,34 @@
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 7491547d7078c8bad54cf350b1276c7f32e50df5 Subproject commit 78caabba7ef91cd1030da6f70b41d266704ffece

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(feature = "wayland")] #[cfg(all(feature = "wayland", target_os = "linux"))]
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(feature = "wayland")] #[cfg(all(feature = "wayland", target_os = "linux"))]
WindowState(iced::window::Id, WindowState), WindowState(iced::window::Id, WindowState),
/// Capabilities the window manager supports /// Capabilities the window manager supports
#[cfg(feature = "wayland")] #[cfg(all(feature = "wayland", target_os = "linux"))]
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(feature = "wayland")] #[cfg(all(feature = "wayland", target_os = "linux"))]
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")))] #[cfg(not(any(feature = "multi-window", feature = "wayland", target_os = "linux")))]
use iced::Application as IcedApplication; use iced::Application as IcedApplication;
#[cfg(feature = "wayland")] #[cfg(all(feature = "wayland", target_os = "linux"))]
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(feature = "wayland")] #[cfg(all(feature = "wayland", target_os = "linux"))]
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: raw_window_handle::WindowHandle) -> crate::Action<M> { fn init_windowing_system<M>(handle: window::raw_window_handle::WindowHandle) -> crate::Action<M> {
let raw: &raw_window_handle::RawWindowHandle = handle.as_ref(); let raw = 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: raw_window_handle::WindowHandle) -> crate::A
#[derive(Default)] #[derive(Default)]
pub struct Cosmic<App: Application> { pub struct Cosmic<App: Application> {
pub app: App, pub app: App,
#[cfg(feature = "wayland")] #[cfg(all(feature = "wayland", target_os = "linux"))]
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(feature = "wayland")] #[cfg(all(feature = "wayland", target_os = "linux"))]
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(feature = "wayland")] #[cfg(all(feature = "wayland", target_os = "linux"))]
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(feature = "wayland")] #[cfg(all(feature = "wayland", target_os = "linux"))]
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,15 +225,26 @@ where
iced_winit::commands::popup::get_popup(settings(&mut self.app)) iced_winit::commands::popup::get_popup(settings(&mut self.app))
} }
} }
#[cfg(feature = "wayland")] #[cfg(all(feature = "wayland", target_os = "linux"))]
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(feature = "wayland")] #[cfg(all(feature = "wayland", target_os = "linux"))]
crate::surface::Action::DestroyTooltipPopup => {
#[cfg(feature = "applet")]
{
iced_winit::commands::popup::destroy_popup(*crate::applet::TOOLTIP_WINDOW_ID)
}
#[cfg(not(feature = "applet"))]
{
Task::none()
}
}
#[cfg(all(feature = "wayland", target_os = "linux"))]
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(feature = "wayland")] #[cfg(all(feature = "wayland", target_os = "linux"))]
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,
@ -244,7 +255,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(feature = "wayland")] #[cfg(all(feature = "wayland", target_os = "linux"))]
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()
@ -271,7 +282,7 @@ where
iced_winit::commands::popup::get_popup(settings()) iced_winit::commands::popup::get_popup(settings())
} }
} }
#[cfg(feature = "wayland")] #[cfg(all(feature = "wayland", target_os = "linux"))]
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>>()
@ -310,7 +321,7 @@ where
.discard() .discard()
} }
} }
#[cfg(feature = "wayland")] #[cfg(all(feature = "wayland", target_os = "linux"))]
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>>()
@ -430,7 +441,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(feature = "wayland")] #[cfg(all(feature = "wayland", target_os = "linux"))]
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)
@ -443,7 +454,7 @@ where
) => { ) => {
return Some(Action::SuggestedBounds(b)); return Some(Action::SuggestedBounds(b));
} }
#[cfg(feature = "wayland")] #[cfg(all(feature = "wayland", target_os = "linux"))]
wayland::Event::Window(iced::event::wayland::WindowEvent::WindowState( wayland::Event::Window(iced::event::wayland::WindowEvent::WindowState(
s, s,
)) => { )) => {
@ -560,7 +571,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(feature = "wayland")] #[cfg(all(feature = "wayland", target_os = "linux"))]
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);
} }
@ -611,7 +622,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(feature = "wayland"))] #[cfg(not(all(feature = "wayland", target_os = "linux")))]
if self if self
.app .app
.core() .core()
@ -641,7 +652,7 @@ impl<T: Application> Cosmic<T> {
}); });
} }
#[cfg(feature = "wayland")] #[cfg(all(feature = "wayland", target_os = "linux"))]
Action::WindowState(id, state) => { Action::WindowState(id, state) => {
if self if self
.app .app
@ -693,7 +704,7 @@ impl<T: Application> Cosmic<T> {
} }
} }
#[cfg(feature = "wayland")] #[cfg(all(feature = "wayland", target_os = "linux"))]
Action::WmCapabilities(id, capabilities) => { Action::WmCapabilities(id, capabilities) => {
if self if self
.app .app
@ -800,7 +811,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(feature = "wayland")] #[cfg(all(feature = "wayland", target_os = "linux"))]
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;
@ -946,7 +957,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(feature = "wayland")] #[cfg(all(feature = "wayland", target_os = "linux"))]
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;
@ -1040,7 +1051,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(feature = "wayland")] #[cfg(all(feature = "wayland", target_os = "linux"))]
{ {
task = task.chain( task = task.chain(
iced_winit::platform_specific::commands::activation::activate( iced_winit::platform_specific::commands::activation::activate(
@ -1051,7 +1062,7 @@ impl<T: Application> Cosmic<T> {
) )
} }
#[cfg(not(feature = "wayland"))] #[cfg(not(all(feature = "wayland", target_os = "linux")))]
{ {
task = task.chain(iced_runtime::window::gain_focus(id)); task = task.chain(iced_runtime::window::gain_focus(id));
} }
@ -1068,7 +1079,7 @@ impl<T: Application> Cosmic<T> {
*v == 0 *v == 0
}) { }) {
self.opened_surfaces.remove(&id); self.opened_surfaces.remove(&id);
#[cfg(feature = "wayland")] #[cfg(all(feature = "wayland", target_os = "linux"))]
self.surface_views.remove(&id); self.surface_views.remove(&id);
self.tracked_windows.remove(&id); self.tracked_windows.remove(&id);
} }
@ -1190,7 +1201,8 @@ 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,
@ -1235,7 +1247,7 @@ impl<T: Application> Cosmic<T> {
core.applet.suggested_bounds = b; core.applet.suggested_bounds = b;
} }
Action::Opened(id) => { Action::Opened(id) => {
#[cfg(feature = "wayland")] #[cfg(all(feature = "wayland", target_os = "linux"))]
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;
@ -1284,14 +1296,14 @@ impl<App: Application> Cosmic<App> {
pub fn new(app: App) -> Self { pub fn new(app: App) -> Self {
Self { Self {
app, app,
#[cfg(feature = "wayland")] #[cfg(all(feature = "wayland", target_os = "linux"))]
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(feature = "wayland")] #[cfg(all(feature = "wayland", target_os = "linux"))]
/// Create a subsurface /// Create a subsurface
pub fn get_subsurface( pub fn get_subsurface(
&mut self, &mut self,
@ -1314,7 +1326,7 @@ impl<App: Application> Cosmic<App> {
get_subsurface(settings) get_subsurface(settings)
} }
#[cfg(feature = "wayland")] #[cfg(all(feature = "wayland", target_os = "linux"))]
/// Create a subsurface /// Create a subsurface
pub fn get_popup( pub fn get_popup(
&mut self, &mut self,
@ -1336,7 +1348,7 @@ impl<App: Application> Cosmic<App> {
get_popup(settings) get_popup(settings)
} }
#[cfg(feature = "wayland")] #[cfg(all(feature = "wayland", target_os = "linux"))]
/// Create a window surface /// Create a window surface
pub fn get_window( pub fn get_window(
&mut self, &mut self,

View file

@ -128,6 +128,9 @@ 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);
@ -194,6 +197,9 @@ 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();
@ -742,6 +748,8 @@ 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(feature = "wayland")] #[cfg(all(feature = "wayland", target_os = "linux"))]
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(feature = "wayland")] #[cfg(all(feature = "wayland", target_os = "linux"))]
autosize: false, autosize: false,
no_main_window: false, no_main_window: false,
client_decorations: true, client_decorations: true,

View file

@ -6,13 +6,6 @@ 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,
@ -24,8 +17,15 @@ 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"));
static TOOLTIP_WINDOW_ID: LazyLock<window::Id> = LazyLock::new(window::Id::unique); pub(crate) 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| crate::iced_widget::svg::Style { theme::Svg::Custom(Rc::new(|theme| 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(feature = "wayland")] #[cfg(all(feature = "wayland", target_os = "linux"))]
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(feature = "wayland")] #[cfg(all(feature = "wayland", target_os = "linux"))]
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(feature = "wayland")] #[cfg(all(feature = "wayland", target_os = "linux"))]
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(feature = "wayland")] #[cfg(all(feature = "wayland", target_os = "linux"))]
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} -- {}", exec.as_ref()); term_exec = format!("{term} -e {}", exec.as_ref());
&term_exec &term_exec
} else { } else {
exec.as_ref() exec.as_ref()

View file

@ -19,72 +19,6 @@ 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,6 +3,7 @@
#![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 {
@ -66,29 +67,6 @@ 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;
@ -100,7 +78,8 @@ pub(crate) mod malloc;
#[cfg(all(feature = "process", not(windows)))] #[cfg(all(feature = "process", not(windows)))]
pub mod process; pub mod process;
#[cfg(feature = "wayland")] #[doc(inline)]
#[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(feature = "wayland")] #[cfg(all(feature = "wayland", target_os = "linux"))]
#[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(feature = "wayland")] #[cfg(all(feature = "wayland", target_os = "linux"))]
#[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(feature = "wayland")] #[cfg(all(feature = "wayland", target_os = "linux"))]
#[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", feature = "winit"))] #[cfg(all(feature = "wayland", target_os = "linux", 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", feature = "winit"))] #[cfg(all(feature = "wayland", target_os = "linux", 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", feature = "winit"))] #[cfg(all(feature = "wayland", target_os = "linux", 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", feature = "winit"))] #[cfg(all(feature = "wayland", target_os = "linux", 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", feature = "winit"))] #[cfg(all(feature = "wayland", target_os = "linux", 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", feature = "winit"))] #[cfg(all(feature = "wayland", target_os = "linux", 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,6 +36,8 @@ 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(
@ -85,6 +87,7 @@ 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.bg_color().into(), icon_color: cosmic.on_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, ListItem([f32; 4]),
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 => { Button::ListItem(radii) => {
corner_radii = &[0.0; 4]; corner_radii = radii;
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);
} }
appearance(self, focused, selected, false, style, move |component| { let mut s = 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,7 +209,15 @@ 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 {
@ -237,7 +245,7 @@ impl Catalog for crate::Theme {
return hovered(focused, self); return hovered(focused, self);
} }
appearance( let mut s = appearance(
self, self,
focused || matches!(style, Button::Image), focused || matches!(style, Button::Image),
selected, selected,
@ -256,7 +264,15 @@ 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.bg_color().into(), icon_color: cosmic.on_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", feature = "winit"))] #[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))]
pub mod tooltip; pub mod tooltip;
#[cfg(all(feature = "wayland", feature = "winit"))] #[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))]
pub use tooltip::Tooltip; pub use tooltip::Tooltip;

View file

@ -1,8 +1,9 @@
use crate::{ use crate::{
Apply, Element, fl, Apply, Element, fl,
iced::{Alignment, Length}, iced::{Alignment, Length},
widget::{self, space}, widget::{self, list},
}; };
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)]
@ -47,32 +48,40 @@ 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.to_string(), format!("mailto:{email}"))) .map(|(name, email)| (name.into(), format!("mailto:{email}")))
.collect() .collect()
} }
macro_rules! set_contributors { impl<'a> About {
($field:ident, $doc:expr) => { /// Artists who contributed to the application.
#[doc = $doc] pub fn artists(mut self, contributors: impl Into<Vec<(&'a str, &'a str)>>) -> Self {
pub fn $field(mut self, contributors: impl Into<Vec<(&'a str, &'a str)>>) -> Self { self.artists = add_contributors(contributors.into());
self.$field = add_contributors(contributors.into());
self self
} }
};
/// Designers who contributed to the application.
pub fn designers(mut self, contributors: impl Into<Vec<(&'a str, &'a str)>>) -> Self {
self.designers = add_contributors(contributors.into());
self
} }
impl<'a> About { /// Developers who contributed to the application.
set_contributors!(artists, "Artists who contributed to the application."); pub fn developers(mut self, contributors: impl Into<Vec<(&'a str, &'a str)>>) -> Self {
set_contributors!(designers, "Designers who contributed to the application."); self.developers = add_contributors(contributors.into());
set_contributors!(developers, "Developers who contributed to the application."); self
set_contributors!( }
documenters,
"Documenters who contributed to the application." /// Documenters who contributed to the application.
); pub fn documenters(mut self, contributors: impl Into<Vec<(&'a str, &'a str)>>) -> Self {
set_contributors!( self.documenters = add_contributors(contributors.into());
translators, self
"Translators who contributed to the application." }
);
/// 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>>(
@ -96,19 +105,23 @@ pub fn about<'a, Message: Clone + 'static>(
space_xxs, space_m, .. space_xxs, space_m, ..
} = crate::theme::spacing(); } = crate::theme::spacing();
let section_button = |name: &'a str, url: &'a str| -> Element<'a, Message> { let svg_accent = Rc::new(|theme: &crate::Theme| widget::svg::Style {
widget::row() color: Some(theme.cosmic().accent_text_color().into()),
.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(crate::widget::icon::from_name("link-symbolic").icon()), (!url.is_empty()).then_some(
widget::icon::from_name("link-symbolic")
.icon()
.class(crate::theme::Svg::Custom(svg_accent.clone())),
),
) )
.align_y(Alignment::Center) .align_y(Alignment::Center)
.apply(widget::button::custom) .apply(list::button)
.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| {
@ -158,7 +171,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() widget::column::with_capacity(10)
.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(feature = "wayland")] #[cfg(all(feature = "wayland", target_os = "linux"))]
if matches!( if matches!(
event, event,
Event::PlatformSpecific(event::PlatformSpecific::Wayland( Event::PlatformSpecific(event::PlatformSpecific::Wayland(

View file

@ -3,10 +3,7 @@
use super::{Builder, ButtonClass}; use super::{Builder, ButtonClass};
use crate::Element; use crate::Element;
use crate::widget::{ use crate::widget::{icon::Handle, tooltip};
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;
@ -133,7 +130,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(mut builder: Button<'a, Message>) -> Element<'a, Message> { fn from(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,6 +357,8 @@ 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().push(date).push(day).into(), column([date.into(), day.into()]).into(),
crate::widget::space::horizontal() crate::widget::space::horizontal()
.width(Length::Fill) .width(Length::Fill)
.into(), .into(),

View file

@ -1,13 +1,8 @@
//! 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,
@ -18,6 +13,10 @@ 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,7 +4,6 @@
//! 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};
@ -93,8 +92,6 @@ 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>,
@ -128,7 +125,6 @@ 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,
@ -159,22 +155,26 @@ 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::AppliedColor | ColorPickerUpdate::ActionFinished => {
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.recent_colors.push(applied_color); self.update_recent_colors(applied_color);
} }
self.applied_color = Some(Color::from(srgb)); self.applied_color = Some(Color::from(srgb));
self.active = false; self.active = false;
@ -215,21 +215,12 @@ 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()
} }
@ -395,7 +386,8 @@ 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::AppliedColor)) .on_submit(move |_| on_update(ColorPickerUpdate::ActionFinished))
// .on_unfocus(on_update(ColorPickerUpdate::ActionFinished)) Somehow this is called even when the field wasn't previously focused
.leading_icon( .leading_icon(
color_button( color_button(
None, None,

View file

@ -3,7 +3,12 @@
//! 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(feature = "wayland", feature = "winit", feature = "surface-message"))] #[cfg(all(
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,
@ -27,7 +32,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::<'static, Message>()), crate::Element::from(crate::widget::Row::new()),
menus, menus,
)] )]
}), }),
@ -59,7 +64,12 @@ pub struct ContextMenu<'a, Message> {
} }
impl<Message: Clone + 'static> ContextMenu<'_, Message> { impl<Message: Clone + 'static> ContextMenu<'_, Message> {
#[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] #[cfg(all(
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,
@ -364,7 +374,12 @@ 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(feature = "wayland", feature = "winit", feature = "surface-message"))] #[cfg(all(
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)
{ {
@ -403,7 +418,12 @@ 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(feature = "wayland", feature = "winit", feature = "surface-message"))] #[cfg(all(
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);
} }
@ -422,6 +442,7 @@ 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"
))] ))]
@ -458,7 +479,12 @@ 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(feature = "wayland", feature = "winit", feature = "surface-message"))] #[cfg(all(
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,7 +7,10 @@ use iced::Vector;
use crate::{ use crate::{
Element, Element,
iced::{ widget::{Id, Widget},
};
use iced::{
Event, Length, Rectangle, Event, Length, Rectangle,
clipboard::{ clipboard::{
dnd::{self, DndAction, DndDestinationRectangle, DndEvent, OfferEvent}, dnd::{self, DndAction, DndDestinationRectangle, DndEvent, OfferEvent},
@ -16,12 +19,10 @@ use crate::{
event, event,
id::Internal, id::Internal,
mouse, overlay, mouse, overlay,
}, };
iced_core::{ use 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,
iced::{ widget::{Id, Widget, container},
};
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,
}, };
iced_core::{ use 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"))] #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))]
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"))] #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))]
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"))] #[cfg(all(feature = "winit", feature = "wayland", 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,
action_map: None, action_map: None,
} }
} }
#[cfg(all(feature = "winit", feature = "wayland"))] #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))]
/// 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"))] #[cfg(all(feature = "winit", feature = "wayland", 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,
@ -268,7 +268,7 @@ where
layout, layout,
cursor, cursor,
shell, shell,
#[cfg(all(feature = "winit", feature = "wayland"))] #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))]
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"))] #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))]
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"))] #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))]
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"))] #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))]
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"))] #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))]
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"))] #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))]
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"))] #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))]
/// 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,9 +162,14 @@ 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: flex_layout.content_size.height, height: actual_height.max(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, Shell, Size, Vector, Widget, Clipboard, ContentFit, Element, Event, Layout, Length, Rectangle, Rotation, Shell, Size,
event, layout, renderer, window, Widget, event, layout, renderer, window,
}; };
use iced_widget::image::{self, Handle}; use iced_widget::image::{self, FilterMethod, 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.data() { .map(|f| match &f.handle {
iced_core::image::Handle::Path(..) => 0, Handle::Path(..) => 0,
iced_core::image::Handle::Bytes(_, b) => b.len(), Handle::Bytes(_, b) => b.len(),
iced_core::image::Handle::Rgba { pixels, .. } => pixels.len(), Handle::Rgba { pixels, .. } => pixels.len(),
}) })
.sum::<usize>() .sum::<usize>()
.try_into() .try_into()
@ -324,7 +324,11 @@ 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],
) )
} }
@ -371,37 +375,18 @@ where
) { ) {
let state = tree.state.downcast_ref::<State>(); let state = tree.state.downcast_ref::<State>();
// Pulled from iced_native::widget::<Image as Widget>::draw iced_widget::image::draw(
// renderer,
// TODO: export iced_native::widget::image::draw as standalone function layout,
{ &state.current.frame.handle,
let Size { width, height } = renderer.dimensions(&state.current.frame.handle); None,
let image_size = Size::new(width as f32, height as f32); iced_core::border::Radius::default(),
self.content_fit,
let bounds = layout.bounds(); FilterMethod::default(),
let adjusted_fit = self.content_fit.fit(image_size, bounds.size()); Rotation::default(),
1.0,
let render = |renderer: &mut Renderer| { 1.0,
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,7 +197,16 @@ 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 (center_node, center_x) = if let Some(center) = &mut self.center { let vcenter = |node: layout::Node, x: f32| -> layout::Node {
let dy = ((height - node.size().height) / 2.0).max(0.0);
node.translate(Vector::new(x, dy))
};
let mut child_nodes = Vec::with_capacity(3);
child_nodes.push(vcenter(start_node, 0.0));
child_nodes.push(vcenter(end_node, width - end_width));
if let Some(center) = &mut self.center {
let slot_start = start_width + gap; let slot_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;
@ -217,21 +226,8 @@ 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)
};
let vcenter = |node: layout::Node, x: f32| -> layout::Node { child_nodes.push(vcenter(node, center_x))
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)
@ -247,10 +243,13 @@ 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,
) { ) {
for ((e, s), l) in self.elems().zip(&tree.children).zip(layout.children()) { self.elems()
.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(
@ -264,14 +263,13 @@ 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,
) { ) {
for ((e, s), l) in self self.elems_mut()
.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(
@ -300,13 +298,12 @@ 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<()>,
) { ) {
for ((e, s), l) in self self.elems_mut()
.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>(
@ -317,27 +314,13 @@ 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>> {
let mut layouts = layout.children(); self.elems_mut()
let mut try_overlay = |elem: &'b mut Element<'a, Message>, .zip(&mut state.children)
state: &'b mut tree::Tree| .zip(layout.children())
-> Option< .find_map(|((e, s), l)| {
iced_core::overlay::Element<'b, Message, crate::Theme, crate::Renderer>, e.as_widget_mut()
> { .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(
@ -347,10 +330,13 @@ 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,
) { ) {
for ((e, s), l) in self.elems().zip(&state.children).zip(layout.children()) { self.elems()
.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")]
@ -398,8 +384,7 @@ 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),
// Center content depending on window border self.maximized, // window border handling
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],
@ -436,7 +421,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(crate::theme::Container::HeaderBar { .class(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,
@ -468,7 +453,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(crate::theme::Button::HeaderBar) .class(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(unix)] #[cfg(all(unix, not(target_os = "macos")))]
pub fn get(icon_name: &str) -> Option<super::Data> { pub fn get(icon_name: &str) -> Option<super::Data> {
None None
} }
#[cfg(not(unix))] #[cfg(any(not(unix), target_os = "macos"))]
/// 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(not(unix))] #[cfg(any(not(unix), target_os = "macos"))]
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(not(windows))] #[cfg(all(unix, not(target_os = "macos")))]
#[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(windows)] #[cfg(any(not(unix), target_os = "macos"))]
#[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

View file

@ -1,128 +0,0 @@
// 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

@ -0,0 +1,213 @@
// 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 column; pub mod list_column;
pub use self::column::{ListColumn, list_column}; pub use self::list_column::{ListButton, ListColumn, button, list_column};

View file

@ -12,6 +12,7 @@ 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"
))] ))]
@ -195,7 +196,12 @@ 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(feature = "multi-window", feature = "wayland", feature = "winit"))] #[cfg(all(
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>>,
@ -230,7 +236,12 @@ 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(feature = "multi-window", feature = "wayland", feature = "winit"))] #[cfg(all(
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,
} }
@ -324,7 +335,12 @@ where
self self
} }
#[cfg(all(feature = "multi-window", feature = "wayland", feature = "winit"))] #[cfg(all(
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,
@ -359,6 +375,7 @@ 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"
))] ))]
@ -629,6 +646,7 @@ 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"
))] ))]
@ -652,6 +670,7 @@ 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"
))] ))]
@ -666,6 +685,7 @@ 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"
))] ))]
@ -748,6 +768,7 @@ 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,6 +7,7 @@ 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"
))] ))]
@ -680,6 +681,7 @@ 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"
))] ))]
@ -765,7 +767,13 @@ 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 => self.depth == state.active_root.len() - 1, PathHighlight::MenuActive => {
!indices.is_empty()
&& i < indices.len()
&& menu_roots.len() > indices[i]
&& (i < indices.len() - 1
|| !menu_roots[indices[i]].children.is_empty())
}
}); });
// react only to the last menu // react only to the last menu
@ -960,7 +968,8 @@ 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
@ -1220,6 +1229,7 @@ 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"
))] ))]
@ -1517,7 +1527,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", feature = "winit", feature = "surface-message"))] #[cfg(all(feature = "multi-window", feature = "wayland",target_os = "linux", feature = "winit", feature = "surface-message"))]
if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) && remove { if 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,9 +252,18 @@ 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).into(), widget::text(l)
.ellipsize(iced_core::text::Ellipsize::Middle(
iced_core::text::EllipsizeHeightLimit::Lines(1),
))
.into(),
widget::space::horizontal().into(), widget::space::horizontal().into(),
widget::text(key).class(key_class).into(), widget::text(key)
.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 {
@ -275,9 +284,18 @@ 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).into(), widget::text(l)
.ellipsize(iced_core::text::Ellipsize::Middle(
iced_core::text::EllipsizeHeightLimit::Lines(1),
))
.into(),
widget::space::horizontal().into(), widget::space::horizontal().into(),
widget::text(key).class(key_class).into(), widget::text(key)
.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 {
@ -312,9 +330,19 @@ 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).align_x(iced::Alignment::Start).into(), widget::text(label)
.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).class(key_class).into(), widget::text(key)
.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 {
@ -335,7 +363,11 @@ 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()).into(), widget::text(l.clone())
.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() //! let content = widget::column::with_capacity(3)
//! .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,6 +53,9 @@ 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};
@ -75,10 +78,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::{ProgressBar, progress_bar}; pub use iced::widget::{Responsive, responsive};
#[doc(inline)] #[doc(inline)]
pub use iced::widget::{Responsive, responsive}; pub use iced::widget::{Row, row};
#[doc(inline)] #[doc(inline)]
pub use iced::widget::{Slider, VerticalSlider, slider, vertical_slider}; pub use iced::widget::{Slider, VerticalSlider, slider, vertical_slider};
@ -135,34 +138,6 @@ 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};
@ -279,6 +254,13 @@ 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};
@ -287,35 +269,6 @@ 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;
@ -355,7 +308,7 @@ pub use toggler::{Toggler, toggler};
#[doc(inline)] #[doc(inline)]
pub use tooltip::{Tooltip, tooltip}; pub use tooltip::{Tooltip, tooltip};
#[cfg(all(feature = "wayland", feature = "winit"))] #[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))]
pub mod wayland; pub mod wayland;
pub mod tooltip { pub mod tooltip {

View file

@ -138,6 +138,10 @@ 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);
@ -172,11 +176,17 @@ 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_position, cursor,
renderer, renderer,
clipboard, clipboard,
shell, shell,
@ -214,13 +224,19 @@ 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_position, cursor,
viewport, viewport,
); );
} }

View file

@ -0,0 +1,462 @@
//! 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

@ -0,0 +1,306 @@
//! 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

@ -0,0 +1,23 @@
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

@ -0,0 +1,105 @@
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; use crate::{Theme, 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: Element<'a, Message, Theme, Renderer>, label: Option<Element<'a, Message, Theme, Renderer>>,
width: Length, width: Length,
size: f32, size: f32,
spacing: f32, spacing: f32,
@ -106,9 +106,6 @@ 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:
@ -126,10 +123,29 @@ where
Radio { Radio {
is_selected: Some(value) == selected, is_selected: Some(value) == selected,
on_click: f(value), on_click: f(value),
label: label.into(), label: Some(label.into()),
width: Length::Shrink, width: Length::Shrink,
size: Self::DEFAULT_SIZE, size: Self::DEFAULT_SIZE,
spacing: Self::DEFAULT_SPACING, spacing: theme::spacing().space_xs as f32,
}
}
/// Creates a new [`Radio`] button without a label.
///
/// This is intended for internal use with the settings item builder,
/// where the label comes from the settings item title instead.
pub(crate) fn new_no_label<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,
} }
} }
@ -161,11 +177,17 @@ where
Renderer: iced_core::Renderer, Renderer: iced_core::Renderer,
{ {
fn children(&self) -> Vec<Tree> { fn children(&self) -> Vec<Tree> {
vec![Tree::new(&self.label)] if let Some(label) = &self.label {
vec![Tree::new(label)]
} else {
vec![]
}
} }
fn diff(&mut self, tree: &mut Tree) { fn diff(&mut self, tree: &mut Tree) {
tree.diff_children(std::slice::from_mut(&mut self.label)); if let Some(label) = &mut self.label {
tree.diff_children(std::slice::from_mut(label));
}
} }
fn size(&self) -> Size<Length> { fn size(&self) -> Size<Length> {
Size { Size {
@ -180,16 +202,20 @@ 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| {
self.label 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(
@ -199,13 +225,15 @@ where
renderer: &Renderer, renderer: &Renderer,
operation: &mut dyn iced_core::widget::Operation<()>, operation: &mut dyn iced_core::widget::Operation<()>,
) { ) {
self.label.as_widget_mut().operate( if let Some(label) = &mut self.label {
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,
@ -218,7 +246,8 @@ where
shell: &mut Shell<'_, Message>, shell: &mut Shell<'_, Message>,
viewport: &Rectangle, viewport: &Rectangle,
) { ) {
self.label.as_widget_mut().update( if let Some(label) = &mut self.label {
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(),
@ -228,14 +257,14 @@ where
shell, shell,
viewport, viewport,
); );
}
if !shell.is_event_captured() { if !shell.is_event_captured() {
match event { match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => { | Event::Touch(touch::Event::FingerLifted { .. }) => {
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;
} }
@ -253,13 +282,17 @@ where
viewport: &Rectangle, viewport: &Rectangle,
renderer: &Renderer, renderer: &Renderer,
) -> mouse::Interaction { ) -> mouse::Interaction {
let interaction = self.label.as_widget().mouse_interaction( let interaction = if let Some(label) = &self.label {
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()) {
@ -284,8 +317,6 @@ 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(
&(), &(),
@ -302,16 +333,21 @@ where
) )
}; };
{ let (dot_bounds, label_layout) = if self.label.is_some() {
let layout = children.next().unwrap(); let mut children = layout.children();
let bounds = layout.bounds(); let dot_bounds = children.next().unwrap().bounds();
(dot_bounds, children.next())
} else {
(layout.bounds(), None)
};
let size = bounds.width; {
let size = dot_bounds.width;
let dot_size = 6.0; let dot_size = 6.0;
renderer.fill_quad( renderer.fill_quad(
renderer::Quad { renderer::Quad {
bounds, bounds: dot_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,
@ -326,8 +362,8 @@ where
renderer.fill_quad( renderer.fill_quad(
renderer::Quad { renderer::Quad {
bounds: Rectangle { bounds: Rectangle {
x: bounds.x + (size - dot_size) / 2.0, x: dot_bounds.x + (size - dot_size) / 2.0,
y: bounds.y + (size - dot_size) / 2.0, y: dot_bounds.y + (size - dot_size) / 2.0,
width: dot_size, width: dot_size,
height: dot_size, height: dot_size,
}, },
@ -339,9 +375,8 @@ where
} }
} }
{ if let (Some(label), Some(label_layout)) = (&self.label, label_layout) {
let label_layout = children.next().unwrap(); label.as_widget().draw(
self.label.as_widget().draw(
&tree.children[0], &tree.children[0],
renderer, renderer,
theme, theme,
@ -361,7 +396,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_widget_mut().overlay( self.label.as_mut()?.as_widget_mut().overlay(
&mut tree.children[0], &mut tree.children[0],
layout.children().nth(1).unwrap(), layout.children().nth(1).unwrap(),
renderer, renderer,
@ -377,7 +412,8 @@ where
renderer: &Renderer, renderer: &Renderer,
dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles,
) { ) {
self.label.as_widget().drag_destinations( if let Some(label) = &self.label {
label.as_widget().drag_destinations(
&state.children[0], &state.children[0],
layout.children().nth(1).unwrap(), layout.children().nth(1).unwrap(),
renderer, renderer,
@ -385,6 +421,7 @@ where
); );
} }
} }
}
impl<'a, Message, Renderer> From<Radio<'a, Message, Renderer>> impl<'a, Message, Renderer> From<Radio<'a, Message, Renderer>>
for Element<'a, Message, Theme, Renderer> for Element<'a, Message, Theme, 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"))] #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))]
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")))] #[cfg(not(all(feature = "winit", feature = "wayland", target_os = "linux")))]
{ {
ItemWidth::Static(84) ItemWidth::Static(84)
} }

View file

@ -213,6 +213,18 @@ 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,10 +117,15 @@ where
height += item_height; height += item_height;
} }
limits.height(Length::Fixed(height)).resolve( let size = 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,7 +3,6 @@
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::{
@ -22,6 +21,7 @@ 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,6 +156,8 @@ 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,
@ -216,13 +218,14 @@ 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::semibold(), font_hovered: crate::font::default(),
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,
@ -243,12 +246,13 @@ 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 state.show_context == Some(key) || self.button_is_hovered(state, key) { } else if 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
}; };
@ -258,11 +262,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)
if prev_hash == text_hash { && 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 {
@ -275,7 +279,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: Ellipsize::default(), ellipsize: self.ellipsize,
}; };
paragraph.update(text); paragraph.update(text);
} else { } else {
@ -289,7 +293,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: Ellipsize::default(), ellipsize: self.ellipsize,
}; };
state.paragraphs.insert(key, crate::Plain::new(text)); state.paragraphs.insert(key, crate::Plain::new(text));
} }
@ -302,7 +306,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::<'static, Message>()), crate::Element::from(crate::widget::Row::new()),
menus, menus,
)] )]
}); });
@ -621,7 +625,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: Ellipsize::default(), ellipsize: self.ellipsize,
line_height: self.line_height, line_height: self.line_height,
}) })
}); });
@ -657,6 +661,50 @@ 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,
@ -880,7 +928,6 @@ 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);
} }
@ -1434,7 +1481,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);
} }
@ -1938,7 +1985,9 @@ where
// Align contents of the button to the requested `button_alignment`. // Align contents of the button to the requested `button_alignment`.
{ {
let actual_width = state.internal_layout[nth].1.width; // Avoid shifting content outside the left edge when the measured content is
// wider than the available button bounds (for example, non-ellipsized text).
let actual_width = state.internal_layout[nth].1.width.min(bounds.width);
let offset = match self.button_alignment { let offset = match self.button_alignment {
Alignment::Start => None, Alignment::Start => None,
@ -1994,10 +2043,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() {
crate::iced_core::svg::Data::Bytes(bytes) => { 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)
} }
crate::iced_core::svg::Data::Path(path) => { iced_core::svg::Data::Path(path) => {
crate::widget::icon::from_path(path.clone()) crate::widget::icon::from_path(path.clone())
} }
}), }),
@ -2090,7 +2139,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, Element, Theme, theme,
widget::{FlexRow, Row, column, container, flex_row, row, text}, widget::{FlexRow, Row, column, container, flex_row, list, 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> { ) -> Row<'a, Message, Theme> {
#[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> { ) -> Row<'a, Message, Theme> {
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> { pub fn item_row<Message>(children: Vec<Element<Message>>) -> Row<Message, Theme> {
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: 'static> Item<'a, Message> { impl<'a, Message: Clone + '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> { pub fn control(self, widget: impl Into<Element<'a, Message>>) -> Row<'a, Message, Theme> {
item_row(self.control_(widget.into())) item_row(self.control_(widget.into()))
} }
@ -114,39 +114,109 @@ impl<'a, Message: 'static> Item<'a, Message> {
flex_item_row(self.control_(widget.into())) flex_item_row(self.control_(widget.into()))
} }
#[inline(never)] fn label(self) -> Element<'a, Message> {
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 {
let column = column::with_capacity(2) 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 {
contents.push(text(self.title).width(Length::Fill).into()); 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,
) -> Row<'a, Message> { ) -> list::ListButton<'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}; use crate::{Element, Theme, 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> { pub fn view_column<Message: 'static>(children: Vec<Element<Message>>) -> Column<Message, Theme> {
column::with_children(children).spacing(theme::spacing().space_m) column::with_children(children).spacing(theme::spacing().space_m)
} }

View file

@ -2,22 +2,24 @@
// SPDX-License-Identifier: MPL-2.0 // SPDX-License-Identifier: MPL-2.0
use crate::Element; use crate::Element;
use crate::widget::{ListColumn, column, text}; use crate::widget::list_column::IntoListItem;
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.
#[deprecated(note = "use `settings::section().title()` instead")] pub fn section<'a, Message: Clone + 'static>() -> Section<'a, Message> {
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: 'static>(children: ListColumn<'_, Message>) -> Section<'_, Message> { pub fn with_column<Message: Clone + 'static>(
children: ListColumn<'_, Message>,
) -> Section<'_, Message> {
Section { Section {
header: None, header: None,
children, children,
@ -30,9 +32,9 @@ pub struct Section<'a, Message> {
children: ListColumn<'a, Message>, children: ListColumn<'a, Message>,
} }
impl<'a, Message: 'static> Section<'a, Message> { impl<'a, Message: Clone + 'static> Section<'a, Message> {
/// Define an optional title for the section. /// Define an optional title for the section.
pub fn title(mut self, title: impl Into<Cow<'a, str>>) -> Self { pub fn title(self, title: impl Into<Cow<'a, str>>) -> Self {
self.header(text::heading(title.into())) self.header(text::heading(title.into()))
} }
@ -44,13 +46,13 @@ impl<'a, Message: '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 Into<Element<'a, Message>>) -> Self { pub fn add(mut self, item: impl IntoListItem<'a, Message>) -> Self {
self.children = self.children.add(item.into()); self.children = self.children.add(item);
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 Into<Element<'a, Message>>>) -> Self { pub fn add_maybe(self, item: Option<impl IntoListItem<'a, Message>>) -> Self {
if let Some(item) = item { if let Some(item) = item {
self.add(item) self.add(item)
} else { } else {
@ -61,13 +63,13 @@ impl<'a, Message: '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 Into<Element<'a, Message>>>, children: impl IntoIterator<Item = impl IntoListItem<'a, Message>>,
) -> Self { ) -> Self {
children.into_iter().fold(self, Self::add) children.into_iter().fold(self, Self::add)
} }
} }
impl<'a, Message: 'static> From<Section<'a, Message>> for Element<'a, Message> { impl<'a, Message: Clone + '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() widget::column::with_capacity(2)
.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() widget::row::with_capacity(2)
.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() widget::column::with_capacity(2)
.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() widget::row::with_capacity(2)
.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() widget::row::with_capacity(2)
.spacing(val.icon_spacing) .spacing(val.icon_spacing)
.push_maybe( .push_maybe(
item.get_icon(*category) item.get_icon(*category)

View file

@ -3,16 +3,19 @@
// 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)] #[derive(Debug, Copy, Clone, PartialEq, Eq)]
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)] #[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum State { pub enum State {
/// Cursor without a selection /// Cursor without a selection
Index(usize), Index(usize),
@ -31,6 +34,7 @@ impl Default for Cursor {
fn default() -> Self { fn default() -> Self {
Self { Self {
state: State::Index(0), state: State::Index(0),
affinity: Affinity::Before,
} }
} }
} }
@ -193,4 +197,37 @@ 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,11 +132,42 @@ 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()
} }
impl ToString for Value { /// 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 {
#[inline] #[inline]
fn to_string(&self) -> String { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.graphemes.concat() f.write_str(&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() let row = row::with_capacity(2)
.push(text(&toast.message)) .push(text(&toast.message))
.push( .push(
row() row::with_capacity(2)
.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, iced_core::Border, iced_widget::toggler::Status}; use crate::{Element, anim};
use iced_core::{ use iced_core::{
Clipboard, Event, Layout, Length, Pixels, Rectangle, Shell, Size, Widget, alignment, event, Border, Clipboard, Event, Layout, Length, Pixels, Rectangle, Shell, Size, Widget, alignment,
layout, mouse, event, layout, mouse,
renderer::{self, Renderer}, renderer::{self, Renderer},
text, text, touch,
widget::{self, Tree, tree}, widget::{self, Tree, tree},
window, window,
}; };
use iced_widget::Id; use iced_widget::{Id, toggler::Status};
pub use crate::iced_widget::toggler::{Catalog, Style}; pub use 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,7 +161,10 @@ 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::default()) tree::State::new(State {
prev_toggled: self.is_toggled,
..State::default()
})
} }
fn id(&self) -> Option<Id> { fn id(&self) -> Option<Id> {
@ -200,7 +203,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: crate::iced_core::text::Wrapping::default(), wrapping: iced_core::text::Wrapping::default(),
ellipsize: self.ellipsize, ellipsize: self.ellipsize,
}, },
); );
@ -238,13 +241,23 @@ 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();
} }
} }
@ -429,4 +442,5 @@ 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