Compare commits

...
Sign in to create a new pull request.

130 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
Vukašin Vojinović
0bb006c5bb fix(header_bar): add vertical SSD padding
Prevents SSDs from having a gap after the rebase.
2026-03-17 17:28:23 +01:00
Vukašin Vojinović
adb6e30405
feat(header_bar): use custom widget for layout 2026-03-17 16:23:31 +01:00
Ashley Wulber
9602dfd2f1 chore: update iced 2026-03-16 16:37:18 -04:00
Ashley Wulber
12cc536cd5 chore: update iced
fix for tiny-skia rotation
2026-03-16 19:37:18 +01:00
Jonathan Wingrove
c52ef97650 fix(table): Use on_item_mb_double for double-click handler instead of on_item_mb_left 2026-03-15 00:30:16 +01:00
Ashley Wulber
01e5593741 chore: update iced 2026-03-12 11:05:32 -04:00
Dryadxon
1dc9aa37ed feat(flex_row): re-export JustifyItems 2026-03-11 17:52:24 +01:00
Dryadxon
ce9e8b5205 fix(flex_row): layout::resolve swap align_items with justify_items 2026-03-11 17:52:24 +01:00
Ashley Wulber
b4533e3a56 chore: update deps 2026-03-11 15:43:49 +01:00
Jeremy Soller
c66652df41
chore: udpate iced (#1162)
- [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-03-11 08:38:12 -06:00
Ashley Wulber
242fe6c4ac chore: update iced 2026-03-11 10:15:30 -04:00
Ashley Wulber
26f4086931
fix(iced): fix touch event handling 2026-03-10 17:33:00 +01:00
Ashley Wulber
4b92ee5f80 chore: update iced
includes fix for virtual offsets
2026-03-09 17:05:27 -04:00
Jeremy Soller
ff6454248f
i18n: translation update from Hosted Weblate (#1147)
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-03-09 09:50:33 -06:00
Hosted Weblate
5eec820615
i18n: translation updates from weblate
Co-authored-by: Aman Alam <aalam@users.noreply.hosted.weblate.org>
Co-authored-by: Ettore Atalan <atalanttore@googlemail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Vilius Paliokas <viliuspaliokas@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/de/
Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/lt/
Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/pa/
Translation: Pop OS/libcosmic
2026-03-08 09:09:56 +00:00
Ashley Wulber
03d0171bbe chore: update iced 2026-03-06 16:49:44 -05:00
Ashley Wulber
3d2c018cd1
fix(dnd_source): rely on current cursor position for hover state 2026-03-06 20:37:56 +01:00
Ashley Wulber
79f8337634
fix(iced): space key is now handled differently in iced-winit 2026-03-06 19:21:34 +01:00
Ashley Wulber
14a5d0c0ba
fix(iced): reversed scroll direction 2026-03-06 17:55:53 +01:00
Ashley Wulber
1970499459 fix: capture mouse motion and mouse interactions in overlay 2026-03-05 22:02:40 +01:00
Alex Marín
1810bedfa5
fix(navbar): fill height of panel instead of shrinking 2026-03-05 15:07:26 +01:00
Ashley Wulber
ad65416551 fix: resize border 2026-03-04 13:12:28 -05:00
Ashley Wulber
8795c506fa chore: update iced
should fix responsive widgets
2026-03-04 12:04:33 -05:00
Ashley Wulber
976e0e214f chore: update iced 2026-03-04 12:04:33 -05:00
Ashley Wulber
0bfda2e28c chore: update deps and test fixes 2026-03-04 12:04:33 -05:00
Ashley Wulber
5432fee112 chore: update iced 2026-03-04 12:04:33 -05:00
Ashley Wulber
925cc9a39f chore: update iced 2026-03-04 12:04:33 -05:00
Ashley Wulber
0e1a9d46eb chore: update iced & cleanup text input 2026-03-04 12:04:33 -05:00
Ashley Wulber
89d31e988d chore: update iced 2026-03-04 12:04:33 -05:00
Ashley Wulber
3d8596287c fix: missed event status after rebase 2026-03-04 12:04:33 -05:00
Ashley Wulber
0298487096 fix: overlay event handling and mouse interaction 2026-03-04 12:04:33 -05:00
Ashley Wulber
904133397b fix: toggler width fixes & cleanup 2026-03-04 12:04:32 -05:00
Ashley Wulber
bee2d591db chore: update iced 2026-03-04 12:04:32 -05:00
Ashley Wulber
442ce6ad0c fix: context-menu
when a popup is created and a focus event is received, we shouldn't close the popups, because it may be a focus event for a popup
2026-03-04 12:04:32 -05:00
Ashley Wulber
fb1a7d3640 fix: open-dialog example 2026-03-04 12:04:32 -05:00
Ashley Wulber
89ee66f251 fix: menu bar and flex row event handling 2026-03-04 12:04:32 -05:00
Ashley Wulber
7554540b78 fix: update for applet widgets and grid 2026-03-04 12:04:32 -05:00
Ashley Wulber
71e2c7c99e fix: responsive menu layout 2026-03-04 12:04:32 -05:00
Ashley Wulber
0d37dc69e3 fix: applet popup width 2026-03-04 12:04:32 -05:00
Ashley Wulber
e6fe1a6811 fix: ellipsize 2026-03-04 12:04:32 -05:00
Ashley Wulber
e8d53b14ea chore: various fixes and some cleanup 2026-03-04 12:04:32 -05:00
Ashley Wulber
e10459fb37 wip rebase updates 2026-03-04 12:04:32 -05:00
Michael Aaron Murphy
86dcf8af6c
feat(cosmic-icons): new icons for cosmic image viewer app 2026-03-03 23:32:00 +01:00
Michael Aaron Murphy
85c27a9960
fix(cosmic-theme): on reset of theme exports, do not remove VS code configs
Closes #1139
2026-03-03 21:18:45 +01:00
Hojjat Abdollahi
bd1d3d5a73
fix: ellipsize headerbar title instead of wrapping (#1140) 2026-03-02 12:01:19 -07:00
Michael Murphy
5b648ca03f
i18n: translation update from Hosted Weblate (#1126)
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-03-02 17:31:52 +01:00
Hosted Weblate
f2caa66f0f
i18n: translation updates from weblate
Co-authored-by: Aindriú Mac Giolla Eoin <aindriu80@gmail.com>
Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Arve Eriksson <031299870@telia.com>
Co-authored-by: Baurzhan Muftakhidinov <baurthefirst@gmail.com>
Co-authored-by: Benmak Kizuna <benmakworkshop@gmail.com>
Co-authored-by: David Carvalho <david.snt.carvalho@gmail.com>
Co-authored-by: Ettore Atalan <atalanttore@googlemail.com>
Co-authored-by: Fedorov Alexei <aleksejfedorov963@gmail.com>
Co-authored-by: Feike Donia <feikedonia@proton.me>
Co-authored-by: Geeson Wan <wang14240@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Jiri Grönroos <jiri.gronroos@iki.fi>
Co-authored-by: Julien Brouillard <julienbrouillard1@gmail.com>
Co-authored-by: Marko X <duffsd@gmail.com>
Co-authored-by: Tommi Nieminen <translator@legisign.org>
Co-authored-by: VandaL <vandalhj@gmail.com>
Co-authored-by: Zahid Rizky Fakhri <zahidrizkyfakhri@gmail.com>
Co-authored-by: jonnysemon <jonnysemon@users.noreply.hosted.weblate.org>
Co-authored-by: lorduskordus <lorduskordus@gmail.com>
Co-authored-by: therealmate <hellogaming91@gmail.com>
Co-authored-by: yakup <mtopac2018@gmail.com>
Co-authored-by: Димко <Term0@ukr.net>
Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/ar/
Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/cs/
Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/de/
Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/fi/
Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/fr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/ga/
Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/hu/
Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/id/
Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/kk/
Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/nl/
Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/pl/
Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/ru/
Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/sv/
Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/tr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/uk/
Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/zh_Hans/
Translation: Pop OS/libcosmic
2026-03-02 16:10:07 +00:00
Michael Aaron Murphy
a37be90e81 fix(single-instance): unminimize main window on dbus activate 2026-02-24 16:02:56 +01:00
Alex Marín
384e8f6e21
fix(segmented_button): clear bold button text on context menu close 2026-02-20 12:06:45 +01:00
Hojjat Abdollahi
b9bd773940
feat: ellipsize text (#1132) 2026-02-19 10:06:45 -07:00
Jeremy Soller
1f6086e5ea Update iced 2026-02-19 09:18:35 -07:00
mariinkys
c1c09624bd fix: right-clicking any sidebar item makes all sidebar items bold 2026-02-19 16:43:56 +01:00
155 changed files with 7586 additions and 3542 deletions

View file

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

View file

@ -7,19 +7,30 @@ on:
jobs:
pages:
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v3
with:
submodules: recursive
- name: Build documentation
run: cargo doc --verbose --features tokio,winit
- name: Deploy documentation
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./target/doc
force_orphan: true
- name: Checkout sources
uses: actions/checkout@v3
with:
submodules: recursive
- name: Install Rust nightly
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2025-07-31
- name: System dependencies
run: sudo apt-get update; sudo apt-get install -y libxkbcommon-dev libwayland-dev
- name: Build documentation
run: |
RUSTDOCFLAGS="--cfg docsrs" \
cargo +nightly-2025-07-31 doc --no-deps \
-p cosmic-client-toolkit \
-p cosmic-protocols \
-p libcosmic \
--verbose --features tokio,winit,wayland,desktop,single-instance,applet,xdg-portal,multi-window
- name: Deploy documentation
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./target/doc
force_orphan: true

View file

@ -8,13 +8,29 @@ rust-version = "1.90"
name = "cosmic"
[features]
default = ["dbus-config", "multi-window", "a11y"]
default = [
"winit",
"tokio",
"a11y",
"dbus-config",
"x11",
"iced-wayland",
"multi-window",
]
advanced-shaping = ["iced/advanced-shaping"]
# Accessibility support
a11y = ["iced/a11y", "iced_accessibility"]
# Enable about widget
about = []
# Builds support for animated images
animated-image = ["dep:async-fs", "image/gif", "tokio?/io-util", "tokio?/fs"]
animated-image = [
"dep:async-fs",
"image/gif",
"image/webp",
"image/png",
"tokio?/io-util",
"tokio?/fs",
]
# XXX autosize should not be used on winit windows unless dialogs
autosize = []
applet = [
@ -42,6 +58,7 @@ desktop = [
"process",
"dep:cosmic-settings-config",
"dep:freedesktop-desktop-entry",
"dep:image-extras",
"dep:mime",
"dep:shlex",
"tokio?/io-util",
@ -65,18 +82,24 @@ tokio = [
]
# Tokio async runtime
# Wayland window support
wayland = [
iced-wayland = [
"ashpd?/wayland",
"autosize",
"iced_runtime/wayland",
"iced/wayland",
"iced_winit/wayland",
"cctk",
"surface-message",
]
wayland = [
"iced-wayland",
"iced_runtime/cctk",
"iced_winit/cctk",
"iced_wgpu/cctk",
"iced/cctk",
"dep:cctk",
]
surface-message = []
# multi-window support
multi-window = ["iced/multi-window"]
multi-window = []
# Render with wgpu
wgpu = ["iced/wgpu", "iced_wgpu"]
# X11 window support via winit
@ -96,14 +119,15 @@ async-std = [
"zbus?/async-io",
"iced/async-std",
]
x11 = ["iced/x11", "iced_winit/x11"]
[dependencies]
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-std = { version = "1.13", optional = true }
auto_enums = "0.8.7"
cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "d0e95be", optional = true }
auto_enums = "0.8.8"
cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "160b086", optional = true }
jiff = "0.2"
cosmic-config = { path = "cosmic-config" }
cosmic-settings-config = { git = "https://github.com/pop-os/cosmic-settings-daemon", optional = true }
@ -115,17 +139,21 @@ i18n-embed = { version = "0.16.0", features = [
i18n-embed-fl = "0.10"
rust-embed = "8.11.0"
css-color = "0.2.8"
derive_setters = "0.1.8"
derive_setters = "0.1.9"
futures = "0.3"
image = { version = "0.25.9", default-features = false, features = [
image = { version = "0.25.10", default-features = false, features = [
"ico",
"jpeg",
"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"
mime = { version = "0.3.17", optional = true }
palette = "0.7.6"
raw-window-handle = "0.6"
rfd = { version = "0.16.0", default-features = false, features = [
"xdg-portal",
], optional = true }
@ -135,24 +163,25 @@ slotmap = "1.1.1"
smol = { version = "2.0.2", optional = true }
thiserror = "2.0.18"
taffy = { version = "0.9.2", features = ["grid"] }
tokio = { version = "1.49.0", optional = true }
tokio = { version = "1.50.0", optional = true }
tracing = "0.1.44"
unicode-segmentation = "1.12"
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"
# Enable DBus feature on Linux targets
[target.'cfg(target_os = "linux")'.dependencies]
cosmic-config = { path = "cosmic-config", features = ["dbus"] }
cosmic-settings-daemon = { git = "https://github.com/pop-os/dbus-settings-bindings" }
zbus = { version = "5.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-desktop-entry = { version = "0.8.1", 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.
phf = { version = "0.13.1", features = ["macros"] }
@ -225,4 +254,4 @@ exclude = ["iced"]
dirs = "6.0.0"
[dev-dependencies]
tempfile = "3.24.0"
tempfile = "3.27.0"

View file

@ -3,7 +3,9 @@ use std::env;
fn main() {
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();
}
}

View file

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

View file

@ -1,11 +1,11 @@
use std::ops::Deref;
use std::{any::TypeId, ops::Deref};
use crate::{CosmicConfigEntry, Update};
use cosmic_settings_daemon::{Changed, ConfigProxy, CosmicSettingsDaemonProxy};
use futures_util::SinkExt;
use iced_futures::{
Subscription,
futures::{self, Stream, StreamExt, future::pending},
futures::{self, StreamExt, future::pending},
stream,
};
@ -57,6 +57,20 @@ impl Watcher {
}
}
#[derive(Clone)]
struct Wrapper(
TypeId,
CosmicSettingsDaemonProxy<'static>,
&'static str,
bool,
);
impl std::hash::Hash for Wrapper {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.0.hash(state);
}
}
#[allow(clippy::too_many_lines)]
pub fn watcher_subscription<T: CosmicConfigEntry + Send + Sync + Default + 'static + Clone>(
settings_daemon: CosmicSettingsDaemonProxy<'static>,
@ -64,166 +78,185 @@ pub fn watcher_subscription<T: CosmicConfigEntry + Send + Sync + Default + 'stat
is_state: bool,
) -> iced_futures::Subscription<Update<T>> {
let id = std::any::TypeId::of::<T>();
Subscription::run_with_id(
(id, config_id),
watcher_stream(settings_daemon, config_id, is_state),
)
}
Subscription::run_with(
Wrapper(id, settings_daemon, config_id, is_state),
|&Wrapper(_, ref settings_daemon, ref config_id, ref is_state)| {
let is_state = *is_state;
let config_id = *config_id;
let settings_daemon = settings_daemon.clone();
enum Change {
Changes(Changed),
OwnerChanged(bool),
}
stream::channel(
5,
move |mut tx: futures::channel::mpsc::Sender<Update<T>>| async move {
let version = T::VERSION;
fn watcher_stream<T: CosmicConfigEntry + Send + Sync + Default + 'static + Clone>(
settings_daemon: CosmicSettingsDaemonProxy<'static>,
config_id: &'static str,
is_state: bool,
) -> impl Stream<Item = Update<T>> {
enum Change {
Changes(Changed),
OwnerChanged(bool),
}
stream::channel(5, move |mut tx| async move {
let version = T::VERSION;
let Ok(cosmic_config) = (if is_state {
crate::Config::new_state(config_id, version)
} else {
crate::Config::new(config_id, version)
}) else {
pending::<()>().await;
unreachable!();
};
let Ok(cosmic_config) = (if is_state {
crate::Config::new_state(config_id, version)
} else {
crate::Config::new(config_id, version)
}) else {
pending::<()>().await;
unreachable!();
};
let mut attempts = 0;
let mut attempts = 0;
loop {
let watcher = if is_state {
Watcher::new_state(&settings_daemon, config_id, version).await
} else {
Watcher::new_config(&settings_daemon, config_id, version).await
};
let Ok(watcher) = watcher else {
tracing::error!("Failed to create watcher for {config_id}");
loop {
let watcher = if is_state {
Watcher::new_state(&settings_daemon, config_id, version).await
} else {
Watcher::new_config(&settings_daemon, config_id, version).await
};
let Ok(watcher) = watcher else {
tracing::error!("Failed to create watcher for {config_id}");
#[cfg(feature = "tokio")]
::tokio::time::sleep(::tokio::time::Duration::from_secs(
2_u64.pow(attempts),
))
.await;
#[cfg(feature = "async-std")]
async_std::task::sleep(std::time::Duration::from_secs(
2_u64.pow(attempts),
))
.await;
#[cfg(not(any(feature = "tokio", feature = "async-std")))]
{
pending::<()>().await;
unreachable!();
}
attempts += 1;
// The settings daemon has exited
continue;
};
let Ok(changes) = watcher.receive_changed().await else {
tracing::error!("Failed to listen for changes for {config_id}");
#[cfg(feature = "tokio")]
::tokio::time::sleep(::tokio::time::Duration::from_secs(2_u64.pow(attempts))).await;
#[cfg(feature = "async-std")]
async_std::task::sleep(std::time::Duration::from_secs(2_u64.pow(attempts))).await;
#[cfg(not(any(feature = "tokio", feature = "async-std")))]
{
pending::<()>().await;
unreachable!();
}
attempts += 1;
// The settings daemon has exited
continue;
};
let Ok(changes) = watcher.receive_changed().await else {
tracing::error!("Failed to listen for changes for {config_id}");
#[cfg(feature = "tokio")]
::tokio::time::sleep(::tokio::time::Duration::from_secs(
2_u64.pow(attempts),
))
.await;
#[cfg(feature = "async-std")]
async_std::task::sleep(std::time::Duration::from_secs(
2_u64.pow(attempts),
))
.await;
#[cfg(not(any(feature = "tokio", feature = "async-std")))]
{
pending::<()>().await;
unreachable!();
}
attempts += 1;
// The settings daemon has exited
continue;
};
#[cfg(feature = "tokio")]
::tokio::time::sleep(::tokio::time::Duration::from_secs(2_u64.pow(attempts))).await;
#[cfg(feature = "async-std")]
async_std::task::sleep(std::time::Duration::from_secs(2_u64.pow(attempts))).await;
#[cfg(not(any(feature = "tokio", feature = "async-std")))]
{
pending::<()>().await;
unreachable!();
}
attempts += 1;
// The settings daemon has exited
continue;
};
let mut changes = changes.map(Change::Changes).fuse();
let mut changes = changes.map(Change::Changes).fuse();
let Ok(owner_changed) = watcher.inner().receive_owner_changed().await
else {
tracing::error!("Failed to listen for owner changes for {config_id}");
#[cfg(feature = "tokio")]
::tokio::time::sleep(::tokio::time::Duration::from_secs(
2_u64.pow(attempts),
))
.await;
#[cfg(feature = "async-std")]
async_std::task::sleep(std::time::Duration::from_secs(
2_u64.pow(attempts),
))
.await;
#[cfg(not(any(feature = "tokio", feature = "async-std")))]
{
pending::<()>().await;
unreachable!();
}
attempts += 1;
// The settings daemon has exited
continue;
};
let mut owner_changed = owner_changed
.map(|c| Change::OwnerChanged(c.is_some()))
.fuse();
let Ok(owner_changed) = watcher.inner().receive_owner_changed().await else {
tracing::error!("Failed to listen for owner changes for {config_id}");
#[cfg(feature = "tokio")]
::tokio::time::sleep(::tokio::time::Duration::from_secs(2_u64.pow(attempts))).await;
#[cfg(feature = "async-std")]
async_std::task::sleep(std::time::Duration::from_secs(2_u64.pow(attempts))).await;
#[cfg(not(any(feature = "tokio", feature = "async-std")))]
{
pending::<()>().await;
unreachable!();
}
attempts += 1;
// The settings daemon has exited
continue;
};
let mut owner_changed = owner_changed
.map(|c| Change::OwnerChanged(c.is_some()))
.fuse();
// update now, just in case we missed changes while setting up stream
let mut config = match T::get_entry(&cosmic_config) {
Ok(config) => config,
Err((errors, default)) => {
for why in &errors {
if why.is_err() {
if let crate::Error::GetKey(_, err) = &why {
if err.kind() == std::io::ErrorKind::NotFound {
// No system default config installed; don't error
continue;
}
}
tracing::error!("error getting config: {config_id} {why}");
}
}
default
}
};
// update now, just in case we missed changes while setting up stream
let mut config = match T::get_entry(&cosmic_config) {
Ok(config) => config,
Err((errors, default)) => {
for why in &errors {
if why.is_err() {
if let crate::Error::GetKey(_, err) = &why {
if err.kind() == std::io::ErrorKind::NotFound {
// No system default config installed; don't error
continue;
if let Err(err) = tx
.send(Update {
errors: Vec::new(),
keys: Vec::new(),
config: config.clone(),
})
.await
{
tracing::error!("Failed to send config: {err}");
}
loop {
let change: Changed = futures::select! {
c = changes.next() => {
let Some(Change::Changes(c)) = c else {
break;
};
c
}
c = owner_changed.next() => {
let Some(Change::OwnerChanged(cont)) = c else {
break;
};
if cont {
continue;
} else {
// The settings daemon has exited
break;
}
},
};
// Reset the attempts counter if we received a change
attempts = 0;
let Ok(args) = change.args() else {
// The settings daemon has exited
break;
};
let (errors, keys) = config.update_keys(&cosmic_config, &[args.key]);
if !keys.is_empty() {
if let Err(err) = tx
.send(Update {
errors,
keys,
config: config.clone(),
})
.await
{
tracing::error!("Failed to send config update: {err}");
}
}
tracing::error!("error getting config: {config_id} {why}");
}
}
default
}
};
if let Err(err) = tx
.send(Update {
errors: Vec::new(),
keys: Vec::new(),
config: config.clone(),
})
.await
{
tracing::error!("Failed to send config: {err}");
}
loop {
let change: Changed = futures::select! {
c = changes.next() => {
let Some(Change::Changes(c)) = c else {
break;
};
c
}
c = owner_changed.next() => {
let Some(Change::OwnerChanged(cont)) = c else {
break;
};
if cont {
continue;
} else {
// The settings daemon has exited
break;
}
},
};
// Reset the attempts counter if we received a change
attempts = 0;
let Ok(args) = change.args() else {
// The settings daemon has exited
break;
};
let (errors, keys) = config.update_keys(&cosmic_config, &[args.key]);
if !keys.is_empty() {
if let Err(err) = tx
.send(Update {
errors,
keys,
config: config.clone(),
})
.await
{
tracing::error!("Failed to send config update: {err}");
}
}
}
}
})
},
)
},
)
}

View file

@ -25,7 +25,24 @@ pub fn config_subscription<
config_id: Cow<'static, str>,
config_version: u64,
) -> iced_futures::Subscription<crate::Update<T>> {
iced_futures::Subscription::run_with_id(id, watcher_stream(config_id, config_version, false))
iced_futures::Subscription::run_with(
(id, config_id, config_version, false),
// FIXME there are type issues related to the 'static lifetime of the Cow if this is extracted to a named function...
|(_, config_id, config_version, is_state)| {
let config_id = config_id.clone();
let config_version = *config_version;
let is_state = *is_state;
stream::channel(100, move |mut output| async move {
let config_id = config_id.clone();
let mut state = ConfigState::Init(config_id, config_version, is_state);
loop {
state = start_listening::<T>(state, &mut output).await;
}
})
},
)
}
#[cold]
@ -37,25 +54,23 @@ pub fn config_state_subscription<
config_id: Cow<'static, str>,
config_version: u64,
) -> iced_futures::Subscription<crate::Update<T>> {
iced_futures::Subscription::run_with_id(id, watcher_stream(config_id, config_version, true))
}
fn watcher_stream<T: 'static + Send + Sync + PartialEq + Clone + CosmicConfigEntry>(
config_id: Cow<'static, str>,
config_version: u64,
is_state: bool,
) -> impl Stream<Item = crate::Update<T>> {
stream::channel(100, move |mut output| {
let config_id = config_id.clone();
async move {
iced_futures::Subscription::run_with(
(id, config_id, config_version, true),
|(_, config_id, config_version, is_state)| {
let config_id = config_id.clone();
let mut state = ConfigState::Init(config_id, config_version, is_state);
let config_version = *config_version;
let is_state = *is_state;
loop {
state = start_listening::<T>(state, &mut output).await;
}
}
})
stream::channel(100, move |mut output| async move {
let config_id = config_id.clone();
let mut state = ConfigState::Init(config_id, config_version, is_state);
loop {
state = start_listening::<T>(state, &mut output).await;
}
})
},
)
}
async fn start_listening<T: 'static + Send + Sync + PartialEq + Clone + CosmicConfigEntry>(

@ -1 +1 @@
Subproject commit 70b07582e24ec2114672256b9657ca80670bca8a
Subproject commit 5252095787cc96e2aed64604158f94e450703455

View file

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

View file

@ -986,19 +986,19 @@ impl ThemeBuilder {
let success = if let Some(success) = success {
success.into_color()
} else {
palette.as_ref().accent_green
palette.as_ref().bright_green
};
let warning = if let Some(warning) = warning {
warning.into_color()
} else {
palette.as_ref().accent_yellow
palette.as_ref().bright_orange
};
let destructive = if let Some(destructive) = destructive {
destructive.into_color()
} 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()));

View file

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

View file

@ -1,8 +1,11 @@
use crate::Theme;
use configparser::ini::Ini;
use palette::{Mix, Srgba, WithAlpha, blend::Compose, rgb::Rgba};
use std::{
fs::{self, File},
io::Write,
path::PathBuf,
vec,
};
use super::{OutputError, qt_settings_ini_style};
@ -15,7 +18,117 @@ impl Theme {
/// Increment this value when changes to qt{5,6}ct.conf are needed.
/// If the config's version is outdated, we update several sections.
/// Otherwise, only the light/dark mode is updated.
const COSMIC_QT_VERSION: u64 = 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.
#[cold]
@ -39,7 +152,7 @@ impl Theme {
.map_err(OutputError::Ini)?
.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" };
ini.set(
@ -91,11 +204,48 @@ impl Theme {
Ok(())
}
/// Reset the applied qt56ct config by removing COSMIC-specific entries from the config file.
#[cold]
pub fn reset_qt56ct() -> Result<(), OutputError> {
let qt5ct_res = Self::reset_ct("qt5ct");
let qt6ct_res = Self::reset_ct("qt6ct");
qt5ct_res?;
qt6ct_res?;
Ok(())
}
#[must_use]
#[cold]
fn reset_ct(ct: &str) -> Result<(), OutputError> {
let path = Self::get_conf_path(ct)?;
let file_content = fs::read_to_string(&path).map_err(OutputError::Io)?;
let mut ini = Ini::new_cs();
ini.read(file_content).map_err(OutputError::Ini)?;
let old_version = ini
.getuint("Appearance", "cosmic_qt_version")
.map_err(OutputError::Ini)?
.unwrap_or_default();
if old_version == 0 {
return Ok(());
}
ini.remove_key("Appearance", "cosmic_qt_version");
ini.remove_key("Appearance", "color_scheme_path");
ini.remove_key("Appearance", "icon_theme");
ini.pretty_write(path, &qt_settings_ini_style())
.map_err(OutputError::Io)?;
Ok(())
}
/// Returns the file paths of the form `~/.config/ct/ct.conf`:
/// e.g. `~/.config/qt6ct/qt6ct.conf`.
///
/// The file and its parent directory are created if they don't exist.
#[cold]
fn get_conf_path(ct: &str) -> Result<PathBuf, OutputError> {
assert!(ct == "qt5ct" || ct == "qt6ct");
let Some(mut config_dir) = dirs::config_dir() else {
return Err(OutputError::MissingConfigDir);
};
@ -111,4 +261,155 @@ impl Theme {
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.
///
/// 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]
#[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
let disabled_color_effects = IniColorEffects {
color: self.button.disabled,
@ -41,7 +42,7 @@ impl Theme {
let bg = self.background.base;
// the background container
let view_colors = IniColors {
let window_colors = IniColors {
background_alternate: bg.mix(self.accent.base, 0.05),
background_normal: bg,
decoration_focus: self.accent_text_color(),
@ -56,16 +57,17 @@ impl Theme {
foreground_visited: self.accent_text_color(),
};
// 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_normal: self.background.component.base,
..view_colors
..window_colors
};
// selected text and items
let selection_colors = {
let selected = self.background.component.selected;
let selected_text = self.background.component.selected_text;
// selection colors are swapped to fix menu bar contrast
let selected = self.background.component.selected_text;
let selected_text = self.background.component.selected;
IniColors {
background_alternate: selected.mix(bg, 0.5),
background_normal: selected,
@ -92,8 +94,11 @@ impl Theme {
let complementary_colors = {
let dark = if self.is_dark {
self.clone()
} else if cfg!(test) {
// For reproducible results in tests, use the default dark theme
Theme::dark_default()
} else {
Theme::light_config()
Theme::dark_config()
.ok()
.as_ref()
.and_then(|conf| Theme::get_entry(conf).ok())
@ -116,10 +121,10 @@ impl Theme {
};
// headers in cosmic don't have a background
let header_colors = &view_colors;
let header_colors_inactive = &view_colors;
let header_colors = &window_colors;
let header_colors_inactive = &window_colors;
// 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 {
"CosmicDark"
@ -198,7 +203,7 @@ widgetStyle=qt6ct-style
format_ini_colors(&tooltip_colors, bg),
format_ini_colors(&view_colors, bg),
format_ini_colors(&window_colors, bg),
format_ini_wm_colors(&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.
#[cold]
pub fn write_qt(&self) -> Result<(), OutputError> {
let colors = self.as_qt();
let file_path = Self::get_qt_colors_path(self.is_dark)?;
let kcolorscheme = self.as_kcolorscheme();
let file_path = Self::get_kcolorscheme_path(self.is_dark)?;
let tmp_file_path = file_path.with_extension("colors.new");
// Write to tmp_file_path first, then move it to file_path
let mut tmp_file = File::create(&tmp_file_path).map_err(OutputError::Io)?;
let res = tmp_file
.write_all(colors.as_bytes())
.write_all(kcolorscheme.as_bytes())
.and_then(|_| tmp_file.flush())
.and_then(|_| std::fs::rename(&tmp_file_path, file_path));
if let Err(e) = res {
@ -245,7 +250,7 @@ widgetStyle=qt6ct-style
let kdeglobals_file = config_dir.join("kdeglobals");
let mut kdeglobals_ini = Self::read_ini(&kdeglobals_file)?;
let src_file = Self::get_qt_colors_path(is_dark)?;
let src_file = Self::get_kcolorscheme_path(is_dark)?;
let src_ini = Self::read_ini(&src_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 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)?;
for (section, key_value) in src_ini.get_map_ref() {
@ -303,8 +308,8 @@ widgetStyle=qt6ct-style
Ok(())
}
/// Gets a path like `~/.config/color-schemes/CosmicDark.colors`
pub fn get_qt_colors_path(is_dark: bool) -> Result<PathBuf, OutputError> {
/// Gets a path like `~/.local/share/color-schemes/CosmicDark.colors`
fn get_kcolorscheme_path(is_dark: bool) -> Result<PathBuf, OutputError> {
let Some(mut data_dir) = dirs::data_dir() else {
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)]
mod tests {
use almost::equal;
use palette::{OklabHue, Srgba};
use super::{is_valid_srgb, oklch_to_srgba_nearest_chroma};
@ -173,57 +172,57 @@ mod tests {
fn test_conversion_boundaries() {
let c1 = palette::Oklcha::new(0.0, 0.288, OklabHue::from_degrees(0.0), 1.0);
let srgb = oklch_to_srgba_nearest_chroma(c1);
equal(srgb.red, 0.0);
equal(srgb.blue, 0.0);
equal(srgb.green, 0.0);
almost::zero(srgb.red);
almost::zero(srgb.blue);
almost::zero(srgb.green);
let c1 = palette::Oklcha::new(1.0, 0.288, OklabHue::from_degrees(0.0), 1.0);
let srgb = oklch_to_srgba_nearest_chroma(c1);
equal(srgb.red, 1.0);
equal(srgb.blue, 1.0);
equal(srgb.green, 1.0);
almost::equal(srgb.red, 1.0);
almost::equal(srgb.blue, 1.0);
almost::equal(srgb.green, 1.0);
}
#[test]
fn test_conversion_colors() {
let c1 = palette::Oklcha::new(0.4608, 0.11111, OklabHue::new(57.31), 1.0);
let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::<u8, u8>();
assert!(srgb.red == 133);
assert!(srgb.green == 69);
assert!(srgb.blue == 0);
assert_eq!(srgb.red, 133);
assert_eq!(srgb.green, 69);
assert_eq!(srgb.blue, 0);
let c1 = palette::Oklcha::new(0.30, 0.08, OklabHue::new(35.0), 1.0);
let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::<u8, u8>();
assert!(srgb.red == 78);
assert!(srgb.green == 27);
assert!(srgb.blue == 15);
assert_eq!(srgb.red, 78);
assert_eq!(srgb.green, 27);
assert_eq!(srgb.blue, 15);
let c1 = palette::Oklcha::new(0.757, 0.146, OklabHue::new(301.2), 1.0);
let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::<u8, u8>();
assert!(srgb.red == 192);
assert!(srgb.green == 153);
assert!(srgb.blue == 253);
assert_eq!(srgb.red, 192);
assert_eq!(srgb.green, 153);
assert_eq!(srgb.blue, 253);
}
#[test]
fn test_conversion_fallback_colors() {
let c1 = palette::Oklcha::new(0.70, 0.284, OklabHue::new(35.0), 1.0);
let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::<u8, u8>();
assert!(srgb.red == 255);
assert!(srgb.green == 103);
assert!(srgb.blue == 65);
assert_eq!(srgb.red, 255);
assert_eq!(srgb.green, 102);
assert_eq!(srgb.blue, 65);
let c1 = palette::Oklcha::new(0.757, 0.239, OklabHue::new(301.2), 1.0);
let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::<u8, u8>();
assert!(srgb.red == 193);
assert!(srgb.green == 152);
assert!(srgb.blue == 255);
assert_eq!(srgb.red, 193);
assert_eq!(srgb.green, 152);
assert_eq!(srgb.blue, 255);
let c1 = palette::Oklcha::new(0.163, 0.333, OklabHue::new(141.0), 1.0);
let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::<u8, u8>();
assert!(srgb.red == 1);
assert!(srgb.green == 19);
assert!(srgb.blue == 0);
assert_eq!(srgb.red, 1);
assert_eq!(srgb.green, 19);
assert_eq!(srgb.blue, 0);
}
}

View file

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

View file

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

View file

@ -1,8 +1,8 @@
use cosmic::app::{Core, Task};
use cosmic::iced::core::window;
use cosmic::iced::window::Id;
use cosmic::iced::{Length, Rectangle};
use cosmic::iced_runtime::core::window;
use cosmic::surface::action::{app_popup, destroy_popup};
use cosmic::widget::{dropdown::popup_dropdown, list_column, settings, toggler};
use cosmic::Element;
@ -13,6 +13,7 @@ pub struct Window {
core: Core,
popup: Option<Id>,
example_row: bool,
toggle: bool,
selected: Option<usize>,
}
@ -22,6 +23,7 @@ impl Default for Window {
core: Core::default(),
popup: None,
example_row: false,
toggle: false,
selected: None,
}
}
@ -33,6 +35,7 @@ pub enum Message {
ToggleExampleRow(bool),
Selected(usize),
Surface(cosmic::surface::Action),
Toggle(bool),
}
impl cosmic::Application for Window {
@ -71,7 +74,6 @@ impl cosmic::Application for Window {
Message::ToggleExampleRow(toggled) => {
self.example_row = toggled;
}
Message::Surface(a) => {
return cosmic::task::message(cosmic::Action::Cosmic(
cosmic::app::Action::Surface(a),
@ -80,6 +82,9 @@ impl cosmic::Application for Window {
Message::Selected(i) => {
self.selected = Some(i);
}
Message::Toggle(v) => {
self.toggle = v;
}
};
Task::none()
}
@ -123,9 +128,8 @@ impl cosmic::Application for Window {
"Example row",
cosmic::widget::container(
toggler(state.example_row)
.on_toggle(|value| Message::ToggleExampleRow(value)),
)
.height(Length::Fixed(50.)),
.on_toggle(Message::ToggleExampleRow),
),
))
.add(popup_dropdown(
&["1", "asdf", "hello", "test"],
@ -155,7 +159,7 @@ impl cosmic::Application for Window {
"oops".into()
}
fn style(&self) -> Option<cosmic::iced_runtime::Appearance> {
fn style(&self) -> Option<cosmic::iced::theme::Style> {
Some(cosmic::applet::style())
}
}

View file

@ -8,9 +8,7 @@ default = ["wayland"]
wayland = ["libcosmic/wayland"]
[dependencies]
tracing = "0.1.44"
tracing-subscriber = "0.3.22"
tracing-log = "0.2.0"
env_logger = "0.11"
[dependencies.libcosmic]
path = "../../"
@ -20,7 +18,8 @@ features = [
"tokio",
"xdg-portal",
"a11y",
"wgpu",
"single-instance",
"surface-message",
"multi-window",
"wgpu",
]

View file

@ -54,8 +54,9 @@ impl widget::menu::Action for Action {
/// Runs application with these settings
#[rustfmt::skip]
fn main() -> Result<(), Box<dyn std::error::Error>> {
// tracing_subscriber::fmt::init();
// let _ = tracing_log::LogTracer::init();
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init();
let input = vec![
(Page::Page1, "🖖 Hello from libcosmic.".into()),
@ -66,9 +67,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let settings = Settings::default()
.size(Size::new(1024., 768.));
cosmic::app::run::<App>(settings, input)?;
cosmic::app::run::<App>(settings, input).unwrap();
Ok(())
}
@ -83,6 +82,7 @@ pub enum Message {
Hi,
Hi2,
Hi3,
Tick,
}
/// The [`App`] stores application-specific state.
@ -93,6 +93,7 @@ pub struct App {
input_2: String,
hidden: bool,
keybinds: HashMap<KeyBind, Action>,
progress: f32,
}
/// Implement [`cosmic::Application`] to integrate with COSMIC.
@ -134,6 +135,7 @@ impl cosmic::Application for App {
input_2: String::new(),
hidden: true,
keybinds: HashMap::new(),
progress: 0.0,
};
let command = app.update_title();
@ -179,10 +181,17 @@ impl cosmic::Application for App {
Message::Hi3 => {
dbg!("hi 3");
}
Message::Tick => {
self.progress = (self.progress + 0.01) % 1.0;
}
}
Task::none()
}
fn subscription(&self) -> iced::Subscription<Self::Message> {
iced::time::every(std::time::Duration::from_millis(64)).map(|_| Message::Tick)
}
/// Creates a view after each update.
fn view(&self) -> Element<'_, Self::Message> {
let page_content = self
@ -191,7 +200,7 @@ impl cosmic::Application for App {
.map_or("No page selected", String::as_str);
let centered = widget::container(
widget::column()
widget::column::with_capacity(14)
.push(widget::text::body(page_content))
.push(
widget::text_input::text_input("", &self.input_1)
@ -213,6 +222,47 @@ impl cosmic::Application for App {
.on_input(Message::Input2)
.on_clear(Message::Ignore),
)
.push(widget::progress_bar::circular::Circular::new().size(50.0))
.push(widget::progress_bar::circular::Circular::new().size(20.0))
.push(
widget::progress_bar::linear::Linear::new()
.girth(10.0)
.width(Length::Fill),
)
.push(
widget::progress_bar::circular::Circular::new()
.bar_height(10.0)
.size(50.0)
.progress(self.progress),
)
.push(
widget::progress_bar::linear::Linear::new()
.girth(10.0)
.progress(self.progress)
.width(Length::Fill),
)
.push(
widget::progress_bar::circular::Circular::new()
.size(50.0)
.progress(0.0),
)
.push(
widget::progress_bar::linear::Linear::new()
.girth(10.0)
.progress(0.0)
.width(Length::Fill),
)
.push(
widget::progress_bar::circular::Circular::new()
.size(50.0)
.progress(1.0),
)
.push(
widget::progress_bar::linear::Linear::new()
.girth(10.0)
.progress(1.0)
.width(Length::Fill),
)
.spacing(cosmic::theme::spacing().space_s)
.width(Length::Fill)
.height(Length::Shrink)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -80,7 +80,7 @@ impl cosmic::Application for App {
/// Creates a view after each update.
fn view(&self) -> Element<'_, Self::Message> {
let mut content = cosmic::widget::column().spacing(12);
let mut content = cosmic::widget::column::with_capacity(self.images.len()).spacing(12);
for (id, image) in self.images.iter().enumerate() {
content = content.push(

View file

@ -7,10 +7,10 @@ use std::collections::HashMap;
use std::{env, process};
use cosmic::app::{Core, Settings, Task};
use cosmic::iced::alignment::{Horizontal, Vertical};
use cosmic::iced::keyboard::Key;
use cosmic::iced::window;
use cosmic::iced_core::alignment::{Horizontal, Vertical};
use cosmic::iced_core::keyboard::Key;
use cosmic::iced_core::{Length, Size};
use cosmic::iced::{Length, Size};
use cosmic::widget::menu::action::MenuAction;
use cosmic::widget::menu::key_bind::KeyBind;
use cosmic::widget::menu::key_bind::Modifier;

View file

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

View file

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

View file

@ -6,7 +6,7 @@
use apply::Apply;
use cosmic::app::{Core, Settings, Task};
use cosmic::dialog::file_chooser::{self, FileFilter};
use cosmic::iced_core::Length;
use cosmic::iced::Length;
use cosmic::widget::button;
use cosmic::{executor, iced, ApplicationExt, Element};
use std::sync::Arc;
@ -207,7 +207,7 @@ impl cosmic::Application for App {
);
content.push(
iced::widget::vertical_space()
iced::widget::space::vertical()
.height(Length::Fixed(12.0))
.into(),
);

View file

@ -64,7 +64,7 @@ impl cosmic::Application for App {
/// Creates a view after each update.
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 cosmic::app::{Core, Settings, Task};
use cosmic::iced_core::Size;
use cosmic::iced::Size;
use cosmic::prelude::*;
use cosmic::widget::table;
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 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))
.width(iced::Length::Fill)

View file

@ -27,3 +27,10 @@ thursday = الخميس
friday = الجمعة
saturday = السبت
sunday = الأحد
mon = ن
tue = ث
wed = ر
thu = خ
fri = ج
sat = س
sun = ح

View file

@ -8,7 +8,7 @@ designers = Designéři
artists = Grafici
translators = Překladatelé
documenters = Tvůrci dokumentace
sunday = Ne
sunday = Neděle
january = Leden { $year }
february = Únor { $year }
march = Březen { $year }
@ -21,9 +21,16 @@ september = Září { $year }
october = Říjen { $year }
november = Listopad { $year }
december = Prosinec { $year }
monday = Po
tuesday = Út
wednesday = St
thursday = Čt
friday = Pá
saturday = So
monday = Pondělí
tuesday = Úterý
wednesday = Středa
thursday = Čtvrtek
friday = Pátek
saturday = Sobota
mon = Po
tue = Út
wed = St
thu = Čt
fri = Pá
sat = So
sun = Ne

View file

@ -3,11 +3,11 @@ close = Schließen
# About
license = Lizenz
links = Links
developers = Entwickler*innen
designers = Designer*innen
artists = Künstler*innen
translators = Übersetzer*innen
documenters = Dokumentierer*innen
developers = Entwickler(innen)
designers = Designer(innen)
artists = Künstler(innen)
translators = Übersetzer(innen)
documenters = Dokumentierer(innen)
# Calendar
january = Januar { $year }
february = Februar { $year }
@ -21,10 +21,17 @@ september = September { $year }
october = Oktober { $year }
november = November { $year }
december = Dezember { $year }
monday = Mo
tuesday = Di
wednesday = Mi
thursday = Do
friday = Fr
saturday = Sa
sunday = So
monday = Montag
tuesday = Dienstag
wednesday = Mittwoch
thursday = Donnerstag
friday = Freitag
saturday = Samstag
sunday = Sonntag
wed = Mi
thu = Do
fri = Fr
sat = Sa
sun = So
tue = Di
mon = Mo

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

View file

@ -0,0 +1,34 @@
monday = Maanantai
mon = ma
tuesday = Tiistai
tue = ti
wednesday = Keskiviikko
wed = ke
thursday = Torstai
thu = to
friday = Perjantai
fri = pe
saturday = Lauantai
sat = la
sunday = Sunnuntai
sun = su
close = Sulje
license = Lisenssi
links = Linkit
developers = Kehittäjät
designers = Suunnittelijat
artists = Artistit
translators = Kääntäjät
documenters = Dokumentoijat
january = Tammikuu { $year }
february = Helmikuu { $year }
march = Maaliskuu { $year }
april = Huhtikuu { $year }
may = Toukokuu { $year }
june = Kesäkuu { $year }
july = Heinäkuu { $year }
august = Elokuu { $year }
september = Syyskuu { $year }
october = Lokakuu { $year }
november = Marraskuu { $year }
december = Joulukuu { $year }

View file

@ -10,18 +10,25 @@ february = Février { $year }
april = Avril { $year }
march = Mars { $year }
november = Novembre { $year }
friday = Ven
tuesday = Mar
friday = Vendredi
tuesday = Mardi
may = Mai { $year }
wednesday = Mer
monday = Lun
wednesday = Mercredi
monday = Lundi
december = Décembre { $year }
sunday = Dim
sunday = Dimanche
june = Juin { $year }
saturday = Sam
saturday = Samedi
august = Août { $year }
july = Juillet { $year }
thursday = Jeu
thursday = Jeudi
september = Septembre { $year }
october = Octobre { $year }
designers = Designers
mon = Lun
tue = Mar
wed = Mer
thu = Jeu
fri = Ven
sat = Sam
sun = Dim

View file

@ -18,10 +18,17 @@ september = Meán Fómhair { $year }
october = Deireadh Fómhair { $year }
november = Samhain { $year }
december = Nollaig { $year }
monday = Lua
tuesday = Mái
wednesday = Céa
thursday = Déa
friday = Aoi
saturday = Sat
sunday = Dom
monday = Dé Luain
tuesday = Dé Máirt
wednesday = Dé Céadaoin
thursday = Déardaoin
friday = Dé hAoine
saturday = Dé Sathairn
sunday = Dé Domhnaigh
mon = Lua
tue = Mái
wed = Céa
thu = Déa
fri = Aoi
sat = Sat
sun = Dom

View file

@ -20,10 +20,17 @@ september = { $year } szeptember
october = { $year } október
november = { $year } november
december = { $year } december
monday = H
tuesday = K
wednesday = Sze
thursday = Cs
friday = P
saturday = Szo
sunday = V
monday = Hétfő
tuesday = Kedd
wednesday = Szerda
thursday = Csütörtök
friday = Péntek
saturday = Szombat
sunday = Vasárnap
mon = H
tue = K
wed = Sze
thu = Cs
fri = P
sat = Szo
sun = V

View file

@ -18,10 +18,17 @@ september = September { $year }
october = Oktober { $year }
november = November { $year }
december = Desember { $year }
monday = Sen
tuesday = Sel
wednesday = Rab
sunday = Min
saturday = Sab
friday = Jum
thursday = Kam
monday = Senin
tuesday = Selasa
wednesday = Rabu
sunday = Minggu
saturday = Sabtu
friday = Jum'at
thursday = Kamis
mon = Sen
tue = Sel
wed = Rab
thu = Kam
fri = Jum
sat = Sab
sun = Min

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

@ -18,10 +18,17 @@ september = Қыркүйек { $year }
october = Қазан { $year }
november = Қараша { $year }
december = Желтоқсан { $year }
monday = Дс
tuesday = Сс
wednesday = Ср
thursday = Бс
friday = Жм
saturday = Сб
sunday = Жс
monday = Дүйсенбі
tuesday = Сейсенбі
wednesday = Сәрсенбі
thursday = Бейсенбі
friday = Жұма
saturday = Сенбі
sunday = Жексенбі
mon = Дс
tue = Сс
wed = Ср
thu = Бс
fri = Жм
sat = Сн
sun = Жк

View file

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

View file

@ -2,26 +2,33 @@ february = Vasaris { $year }
close = Uždaryti
documenters = Dokumentuotojai
november = Lapkritis { $year }
friday = Penk
tuesday = Antr
friday = Penktadienis
tuesday = Antradienis
may = Gegužė { $year }
wednesday = Treč
wednesday = Trečiadienis
april = Balandis { $year }
monday = Pirm
monday = Pirmadienis
translators = Vertėjai
artists = Menininkai
license = Licencija
december = Gruodis { $year }
sunday = Sekm
sunday = Sekmadienis
links = Nuorodos
march = Kovas { $year }
june = Birželis { $year }
saturday = Šešt
saturday = Šeštadienis
august = Rugpjūtis { $year }
developers = Kūrėjai
july = Liepa { $year }
thursday = Ketv
thursday = Ketvirtadienis
september = Rugsėjis { $year }
designers = Dizaineriai
october = Spalis { $year }
january = Sausis { $year }
mon = Pirm
tue = Antr
wed = Treč
thu = Ketv
fri = Penkt
sat = Šešt
sun = Sekm

View file

@ -18,4 +18,10 @@ wednesday = Woe
thursday = Do
friday = Vrij
saturday = Za
sunday = Zon
sunday = Zo
links = Links
developers = Ontwikkeling
designers = Ontwerp
translators = Vertaling
documenters = Documentatie
artists = Vormgeving

View file

@ -0,0 +1,34 @@
close = ਬੰਦ ਕਰੋ
license = ਲਸੰਸ
links = ਲਿੰਕ
developers = ਡਿਵੈਲਪਰ
designers = ਡਿਜ਼ਾਇਨਰ
artists = ਕਲਾਕਾਰ
translators = ਅਨੁਵਾਦਕ
documenters = ਦਸਤਾਵੇਜ਼ ਤਿਆਰ ਕਰਤਾ
january = ਜਨਵਰੀ { $year }
february = ਫਰਵਰੀ { $year }
march = ਮਾਰਚ { $year }
april = ਅਪਰੈਲ { $year }
may = ਮਈ { $year }
june = ਜੂਨ { $year }
july = ਜੁਲਾਈ { $year }
august = ਅਗਸਤ { $year }
september = ਸਤੰਬਰ { $year }
october = ਅਕਤੂਬਰ { $year }
november = ਨਵੰਬਰ { $year }
december = ਦਸੰਬਰ { $year }
monday = ਸੋਮਵਾਰ
mon = ਸੋਮ
tuesday = ਮੰਗਲਵਾਰ
tue = ਮੰਗਲ
wednesday = ਬੁੱਧਵਾਰ
wed = ਬੁੱਧ
thursday = ਵੀਰਵਾਰ
thu = ਵੀਰ
friday = ਸ਼ੁੱਕਰਵਾਰ
fri = ਸ਼ੁੱਕਰ
saturday = ਸ਼ਨਿੱਚਰਵਾਰ
sat = ਸ਼ਨਿੱਚਰ
sunday = ਐਤਵਾਰ
sun = ਐਤ

View file

@ -20,10 +20,17 @@ september = Wrzesień { $year }
october = Październik { $year }
november = Listopad { $year }
december = Grudzień { $year }
monday = Pon
tuesday = Wto
wednesday = Śro
thursday = Czw
friday = Pią
saturday = Sob
sunday = Nie
monday = Poniedziałek
tuesday = Wtorek
wednesday = Środa
thursday = Czwartek
friday = Piątek
saturday = Sobota
sunday = Niedziela
mon = Pon
tue = Wto
wed = Śro
thu = Czw
fri = Pia
sat = Sob
sun = Nie

View file

@ -20,10 +20,17 @@ september = Setembro de { $year }
october = Outubro de { $year }
november = Novembro de { $year }
december = Dezembro de { $year }
monday = Seg
tuesday = Ter
wednesday = Qua
thursday = Qui
friday = Sex
saturday = Sáb
sunday = Dom
monday = Segunda-feira
tuesday = Terça-feira
wednesday = Quarta-feira
thursday = Quinta-feira
friday = Sexta-feira
saturday = Sábado
sunday = Domingo
mon = Seg
tue = Ter
wed = Qua
thu = Qui
fri = Sex
sat = Sáb
sun = Dom

View file

@ -18,10 +18,17 @@ september = Сентябрь { $year }
october = Октябрь { $year }
november = Ноябрь { $year }
december = Декабрь { $year }
monday = Пн
tuesday = Вт
wednesday = Ср
thursday = Чт
friday = Пт
saturday = Сб
sunday = Вс
monday = Понедельник
tuesday = Вторник
wednesday = Среда
thursday = Четверг
friday = Пятница
saturday = Суббота
sunday = Воскресенье
mon = Пн
tue = Вт
wed = Ср
thu = Чт
fri = Пт
sat = Сб
sun = Вс

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

View file

@ -18,10 +18,17 @@ september = September { $year }
october = Oktober { $year }
november = November { $year }
december = December { $year }
monday = Mån
tuesday = Tis
wednesday = Ons
thursday = Tor
friday = Fre
saturday = Lör
sunday = Sön
monday = Måndag
tuesday = Tisdag
wednesday = Onsdag
thursday = Torsdag
friday = Fredag
saturday = Lördag
sunday = Söndag
sun = Sön
mon = Mån
tue = Tis
wed = Ons
thu = Tor
fri = Fre
sat = Lör

View file

@ -1,6 +1,5 @@
# Context Drawer
close = Kapat
# About
license = Lisans
links = Bağlantılar
@ -9,3 +8,29 @@ designers = Tasarımcılar
artists = Sanatçılar
translators = Çevirmenler
documenters = Belgelendiriciler
january = Ocak { $year }
february = Şubat { $year }
march = Mart { $year }
april = Nisan { $year }
may = Mayıs { $year }
june = Haziran { $year }
july = Temmuz { $year }
august = Ağustos { $year }
september = Eylül { $year }
october = Ekim { $year }
november = Kasım { $year }
december = Aralık { $year }
monday = Pazartesi
mon = Pzt
tuesday = Salı
tue = Sal
wednesday = Çarşamba
wed = Çar
thursday = Perşembe
thu = Per
friday = Cuma
fri = Cum
saturday = Cumartesi
sat = Cmt
sunday = Pazar
sun = Paz

View file

@ -10,20 +10,27 @@ translators = Перекладачі
documenters = Документатори
february = Лютий { $year }
november = Листопад { $year }
friday = Пт
tuesday = Вт
friday = П'ятниця
tuesday = Вівторок
may = Травень { $year }
wednesday = Ср
wednesday = Середа
april = Квітень { $year }
monday = Пн
monday = Понеділок
december = Грудень { $year }
sunday = Нд
sunday = Неділя
march = Березень { $year }
june = Червень { $year }
saturday = Сб
saturday = Субота
august = Серпень { $year }
july = Липень { $year }
thursday = Чт
thursday = Четвер
september = Вересень { $year }
october = Жовтень { $year }
january = Січень { $year }
mon = Пн
tue = Вт
wed = Ср
thu = Чт
fri = Пт
sat = Cб
sun = Нд

View file

@ -16,12 +16,19 @@ september = { $year }年9月
october = { $year }年10月
november = { $year }年11月
december = { $year }年12月
monday =
tuesday =
wednesday =
thursday =
friday =
saturday =
sunday =
monday = 星期
tuesday = 星期
wednesday = 星期
thursday = 星期
friday = 星期
saturday = 星期
sunday = 星期
artists = 艺术家
documenters = 文档作者
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 e2a2441789a7e302f099c0e8e9493ef81b58e265
Subproject commit 78caabba7ef91cd1030da6f70b41d266704ffece

51
src/anim.rs Normal file
View file

@ -0,0 +1,51 @@
use std::time::{Duration, Instant};
/// A simple linear interpolation calculation function.
/// p = `percent_complete` in decimal form
#[must_use]
pub fn lerp(start: f32, end: f32, p: f32) -> f32 {
(1.0 - p) * start + p * end
}
/// A fast smooth interpolation calculation function.
/// p = `percent_complete` in decimal form
#[must_use]
pub fn slerp(start: f32, end: f32, p: f32) -> f32 {
let t = smootherstep(p);
(1.0 - t) * start + t * end
}
/// utility function which maps a value [0, 1] -> [0, 1] using the smootherstep function
pub fn smootherstep(t: f32) -> f32 {
(6.0 * t.powi(5) - 15.0 * t.powi(4) + 10.0 * t.powi(3)).clamp(0.0, 1.0)
}
#[derive(Default, Debug)]
pub struct State {
pub last_change: Option<Instant>,
}
impl State {
pub fn changed(&mut self, dur: Duration) {
let t = self.t(dur, false);
let diff = dur.mul_f32(t.abs());
let now = Instant::now();
self.last_change = Some(now.checked_sub(diff).unwrap_or(now));
}
pub fn anim_done(&mut self, dur: Duration) {
if self
.last_change
.is_some_and(|t| Instant::now().duration_since(t) > dur)
{
self.last_change = None;
}
}
pub fn t(&self, dur: Duration, forward: bool) -> f32 {
let res = self.last_change.map_or(1., |t| {
Instant::now().duration_since(t).as_millis() as f32 / dur.as_millis() as f32
});
if forward { res } else { 1. - res }
}
}

View file

@ -5,11 +5,9 @@ use crate::surface;
use crate::theme::Theme;
use crate::widget::nav_bar;
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 cosmic_theme::ThemeMode;
#[cfg(not(any(feature = "multi-window", feature = "wayland")))]
use iced::Application as IcedApplication;
/// A message managed internally by COSMIC.
#[derive(Clone, Debug)]
@ -71,10 +69,10 @@ pub enum Action {
/// Updates the tracked window geometry.
WindowResize(iced::window::Id, f32, f32),
/// Tracks updates to window state.
#[cfg(feature = "wayland")]
#[cfg(all(feature = "wayland", target_os = "linux"))]
WindowState(iced::window::Id, WindowState),
/// Capabilities the window manager supports
#[cfg(feature = "wayland")]
#[cfg(all(feature = "wayland", target_os = "linux"))]
WmCapabilities(iced::window::Id, WindowManagerCapabilities),
#[cfg(feature = "xdg-portal")]
DesktopSettings(crate::theme::portal::Desktop),

View file

@ -8,16 +8,16 @@ use std::sync::Arc;
use super::{Action, Application, ApplicationExt, Subscription};
use crate::theme::{THEME, Theme, ThemeType};
use crate::{Core, Element, keyboard_nav};
#[cfg(feature = "wayland")]
#[cfg(all(feature = "wayland", target_os = "linux"))]
use cctk::sctk::reexports::csd_frame::{WindowManagerCapabilities, WindowState};
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;
#[cfg(feature = "wayland")]
#[cfg(all(feature = "wayland", target_os = "linux"))]
use iced::event::wayland;
use iced::{Task, window};
use iced::{Task, theme, window};
use iced_futures::event::listen_with;
#[cfg(feature = "wayland")]
#[cfg(all(feature = "wayland", target_os = "linux"))]
use iced_winit::SurfaceIdWrapper;
use palette::color_difference::EuclideanDistance;
@ -49,8 +49,8 @@ pub fn windowing_system() -> Option<WindowingSystem> {
WINDOWING_SYSTEM.get().copied()
}
fn init_windowing_system<M>(handle: raw_window_handle::WindowHandle) -> crate::Action<M> {
let raw: &raw_window_handle::RawWindowHandle = handle.as_ref();
fn init_windowing_system<M>(handle: window::raw_window_handle::WindowHandle) -> crate::Action<M> {
let raw = handle.as_ref();
let system = match raw {
window::raw_window_handle::RawWindowHandle::UiKit(_) => WindowingSystem::UiKit,
window::raw_window_handle::RawWindowHandle::AppKit(_) => WindowingSystem::AppKit,
@ -83,7 +83,7 @@ fn init_windowing_system<M>(handle: raw_window_handle::WindowHandle) -> crate::A
#[derive(Default)]
pub struct Cosmic<App: Application> {
pub app: App,
#[cfg(feature = "wayland")]
#[cfg(all(feature = "wayland", target_os = "linux"))]
pub surface_views: HashMap<
window::Id,
(
@ -138,7 +138,7 @@ where
) -> iced::Task<crate::Action<T::Message>> {
#[cfg(feature = "surface-message")]
match _surface_message {
#[cfg(feature = "wayland")]
#[cfg(all(feature = "wayland", target_os = "linux"))]
crate::surface::Action::AppSubsurface(settings, view) => {
let Some(settings) = std::sync::Arc::try_unwrap(settings)
.ok()
@ -168,7 +168,7 @@ where
iced_winit::commands::subsurface::get_subsurface(settings(&mut self.app))
}
}
#[cfg(feature = "wayland")]
#[cfg(all(feature = "wayland", target_os = "linux"))]
crate::surface::Action::Subsurface(settings, view) => {
let Some(settings) = std::sync::Arc::try_unwrap(settings)
.ok()
@ -196,7 +196,7 @@ where
iced_winit::commands::subsurface::get_subsurface(settings())
}
}
#[cfg(feature = "wayland")]
#[cfg(all(feature = "wayland", target_os = "linux"))]
crate::surface::Action::AppPopup(settings, view) => {
let Some(settings) = std::sync::Arc::try_unwrap(settings)
.ok()
@ -225,15 +225,26 @@ where
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) => {
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) => {
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::ResponsiveMenuBar {
menu_bar,
@ -244,7 +255,7 @@ where
core.menu_bars.insert(menu_bar, (limits, size));
iced::Task::none()
}
#[cfg(feature = "wayland")]
#[cfg(all(feature = "wayland", target_os = "linux"))]
crate::surface::Action::Popup(settings, view) => {
let Some(settings) = std::sync::Arc::try_unwrap(settings)
.ok()
@ -271,7 +282,7 @@ where
iced_winit::commands::popup::get_popup(settings())
}
}
#[cfg(feature = "wayland")]
#[cfg(all(feature = "wayland", target_os = "linux"))]
crate::surface::Action::AppWindow(id, settings, view) => {
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>>()
@ -310,7 +321,7 @@ where
.discard()
}
}
#[cfg(feature = "wayland")]
#[cfg(all(feature = "wayland", target_os = "linux"))]
crate::surface::Action::Window(id, settings, view) => {
let Some(settings) = std::sync::Arc::try_unwrap(settings).ok().and_then(|s| {
s.downcast::<Box<dyn Fn() -> iced::window::Settings + Send + Sync>>()
@ -397,15 +408,16 @@ where
f64::from(self.app.core().scale_factor())
}
pub fn style(&self, theme: &Theme) -> iced_runtime::Appearance {
pub fn style(&self, theme: &Theme) -> theme::Style {
if let Some(style) = self.app.style() {
style
} else if self.app.core().window.is_maximized {
let theme = THEME.lock().unwrap();
crate::style::iced::application::appearance(theme.borrow())
crate::style::iced::application::style(theme.borrow())
} else {
let theme = THEME.lock().unwrap();
iced_runtime::Appearance {
theme::Style {
background_color: iced_core::Color::TRANSPARENT,
icon_color: theme.cosmic().on_bg_color().into(),
text_color: theme.cosmic().on_bg_color().into(),
@ -429,7 +441,7 @@ where
}
iced::Event::Window(window::Event::Focused) => return Some(Action::Focus(id)),
iced::Event::Window(window::Event::Unfocused) => return Some(Action::Unfocus(id)),
#[cfg(feature = "wayland")]
#[cfg(all(feature = "wayland", target_os = "linux"))]
iced::Event::PlatformSpecific(iced::event::PlatformSpecific::Wayland(event)) => {
match event {
wayland::Event::Popup(wayland::PopupEvent::Done, _, id)
@ -442,7 +454,7 @@ where
) => {
return Some(Action::SuggestedBounds(b));
}
#[cfg(feature = "wayland")]
#[cfg(all(feature = "wayland", target_os = "linux"))]
wayland::Event::Window(iced::event::wayland::WindowEvent::WindowState(
s,
)) => {
@ -559,7 +571,7 @@ where
#[cfg(feature = "multi-window")]
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) {
return v(&self.app);
}
@ -610,7 +622,7 @@ impl<T: Application> Cosmic<T> {
fn cosmic_update(&mut self, message: Action) -> iced::Task<crate::Action<T::Message>> {
match message {
Action::WindowMaximized(id, maximized) => {
#[cfg(not(feature = "wayland"))]
#[cfg(not(all(feature = "wayland", target_os = "linux")))]
if self
.app
.core()
@ -635,12 +647,12 @@ impl<T: Application> Cosmic<T> {
self.app.on_window_resize(id, width, height);
//TODO: more efficient test of maximized (winit has no event for maximize if set by the OS)
return iced::window::get_maximized(id).map(move |maximized| {
return iced::window::is_maximized(id).map(move |maximized| {
crate::Action::Cosmic(Action::WindowMaximized(id, maximized))
});
}
#[cfg(feature = "wayland")]
#[cfg(all(feature = "wayland", target_os = "linux"))]
Action::WindowState(id, state) => {
if self
.app
@ -692,7 +704,7 @@ impl<T: Application> Cosmic<T> {
}
}
#[cfg(feature = "wayland")]
#[cfg(all(feature = "wayland", target_os = "linux"))]
Action::WmCapabilities(id, capabilities) => {
if self
.app
@ -711,10 +723,10 @@ impl<T: Application> Cosmic<T> {
Action::KeyboardNav(message) => match message {
keyboard_nav::Action::FocusNext => {
return iced::widget::focus_next().map(crate::Action::Cosmic);
return iced::widget::operation::focus_next().map(crate::Action::Cosmic);
}
keyboard_nav::Action::FocusPrevious => {
return iced::widget::focus_previous().map(crate::Action::Cosmic);
return iced::widget::operation::focus_previous().map(crate::Action::Cosmic);
}
keyboard_nav::Action::Escape => return self.app.on_escape(),
keyboard_nav::Action::Search => return self.app.on_search(),
@ -799,7 +811,7 @@ impl<T: Application> Cosmic<T> {
new_theme.theme_type.prefer_dark(prefer_dark);
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() {
use iced_runtime::platform_specific::wayland::CornerRadius;
use iced_winit::platform_specific::commands::corner_radius::corner_radius;
@ -945,7 +957,7 @@ impl<T: Application> Cosmic<T> {
// Only apply update if the theme is set to load a system theme
if let ThemeType::System { .. } = cosmic_theme.theme_type {
cosmic_theme.set_theme(new_theme.theme_type);
#[cfg(feature = "wayland")]
#[cfg(all(feature = "wayland", target_os = "linux"))]
if self.app.core().sync_window_border_radii_to_theme() {
use iced_runtime::platform_specific::wayland::CornerRadius;
use iced_winit::platform_specific::commands::corner_radius::corner_radius;
@ -1034,15 +1046,28 @@ impl<T: Application> Cosmic<T> {
}
return Task::batch(cmds);
}
Action::Activate(_token) =>
{
#[cfg(feature = "wayland")]
Action::Activate(_token) => {
if let Some(id) = self.app.core().main_window_id() {
return iced_winit::platform_specific::commands::activation::activate(
id,
#[allow(clippy::used_underscore_binding)]
_token,
);
// Unminimize window before requesting to activate it.
let mut task = iced_runtime::window::minimize(id, false);
#[cfg(all(feature = "wayland", target_os = "linux"))]
{
task = task.chain(
iced_winit::platform_specific::commands::activation::activate(
id,
#[allow(clippy::used_underscore_binding)]
_token,
),
)
}
#[cfg(not(all(feature = "wayland", target_os = "linux")))]
{
task = task.chain(iced_runtime::window::gain_focus(id));
}
return task;
}
}
@ -1054,7 +1079,7 @@ impl<T: Application> Cosmic<T> {
*v == 0
}) {
self.opened_surfaces.remove(&id);
#[cfg(feature = "wayland")]
#[cfg(all(feature = "wayland", target_os = "linux"))]
self.surface_views.remove(&id);
self.tracked_windows.remove(&id);
}
@ -1176,7 +1201,8 @@ impl<T: Application> Cosmic<T> {
#[cfg(all(
feature = "wayland",
feature = "multi-window",
feature = "surface-message"
feature = "surface-message",
target_os = "linux"
))]
if let Some((
parent,
@ -1221,7 +1247,7 @@ impl<T: Application> Cosmic<T> {
core.applet.suggested_bounds = b;
}
Action::Opened(id) => {
#[cfg(feature = "wayland")]
#[cfg(all(feature = "wayland", target_os = "linux"))]
if self.app.core().sync_window_border_radii_to_theme() {
use iced_runtime::platform_specific::wayland::CornerRadius;
use iced_winit::platform_specific::commands::corner_radius::corner_radius;
@ -1270,14 +1296,14 @@ impl<App: Application> Cosmic<App> {
pub fn new(app: App) -> Self {
Self {
app,
#[cfg(feature = "wayland")]
#[cfg(all(feature = "wayland", target_os = "linux"))]
surface_views: HashMap::new(),
tracked_windows: HashSet::new(),
opened_surfaces: HashMap::new(),
}
}
#[cfg(feature = "wayland")]
#[cfg(all(feature = "wayland", target_os = "linux"))]
/// Create a subsurface
pub fn get_subsurface(
&mut self,
@ -1300,7 +1326,7 @@ impl<App: Application> Cosmic<App> {
get_subsurface(settings)
}
#[cfg(feature = "wayland")]
#[cfg(all(feature = "wayland", target_os = "linux"))]
/// Create a subsurface
pub fn get_popup(
&mut self,
@ -1322,7 +1348,7 @@ impl<App: Application> Cosmic<App> {
get_popup(settings)
}
#[cfg(feature = "wayland")]
#[cfg(all(feature = "wayland", target_os = "linux"))]
/// Create a window surface
pub fn get_window(
&mut self,

View file

@ -11,9 +11,8 @@ pub use action::Action;
use cosmic_config::CosmicConfigEntry;
pub mod context_drawer;
pub use context_drawer::{ContextDrawer, context_drawer};
use iced::application::BootFn;
pub mod cosmic;
#[cfg(all(feature = "winit", feature = "multi-window"))]
pub(crate) mod multi_window;
pub mod settings;
pub type Task<M> = iced::Task<crate::Action<M>>;
@ -21,12 +20,13 @@ pub type Task<M> = iced::Task<crate::Action<M>>;
pub use crate::Core;
use crate::prelude::*;
use crate::theme::THEME;
use crate::widget::{container, horizontal_space, id_container, menu, nav_bar, popover};
use crate::widget::{container, id_container, menu, nav_bar, popover, space};
use apply::Apply;
use iced::window;
use iced::{Length, Subscription};
use iced::{theme, window};
pub use settings::Settings;
use std::borrow::Cow;
use std::{cell::RefCell, rc::Rc};
#[cold]
pub(crate) fn iced_settings<App: Application>(
@ -82,7 +82,7 @@ pub(crate) fn iced_settings<App: Application>(
window_settings.min_size = Some(min_size);
}
let max_size = settings.size_limits.max();
if max_size != iced::Size::INFINITY {
if max_size != iced::Size::INFINITE {
window_settings.max_size = Some(max_size);
}
@ -90,51 +90,99 @@ pub(crate) fn iced_settings<App: Application>(
(iced, (core, flags), window_settings)
}
pub(crate) struct BootDataInner<A: crate::app::Application> {
pub flags: A::Flags,
pub core: Core,
pub settings: window::Settings,
}
pub(crate) struct BootData<A: crate::app::Application>(pub Rc<RefCell<Option<BootDataInner<A>>>>);
impl<A: crate::app::Application> BootFn<cosmic::Cosmic<A>, crate::Action<A::Message>>
for BootData<A>
{
fn boot(&self) -> (cosmic::Cosmic<A>, iced::Task<crate::Action<A::Message>>) {
let mut data = self.0.borrow_mut();
let mut data = data.take().unwrap();
let mut tasks = Vec::new();
#[cfg(feature = "multi-window")]
if data.core.main_window_id().is_some() {
let window_task = iced_runtime::task::oneshot(|channel| {
iced_runtime::Action::Window(iced_runtime::window::Action::Open(
window::Id::RESERVED,
data.settings,
channel,
))
});
data.core.set_main_window_id(Some(window::Id::RESERVED));
tasks.push(window_task.discard());
}
let (a, t) = cosmic::Cosmic::<A>::init((data.core, data.flags));
tasks.push(t);
(a, Task::batch(tasks))
}
}
/// Launch a COSMIC application with the given [`Settings`].
///
/// # Errors
///
/// Returns error on application failure.
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")))]
if let Some(threshold) = settings.default_mmap_threshold {
crate::malloc::limit_mmap_threshold(threshold);
}
let default_font = settings.default_font;
let (settings, mut flags, window_settings) = iced_settings::<App>(settings, flags);
let (settings, (mut core, flags), window_settings) = iced_settings::<App>(settings, flags);
#[cfg(not(feature = "multi-window"))]
{
flags.0.main_window = Some(iced::window::Id::RESERVED);
core.main_window = Some(iced::window::Id::RESERVED);
iced::application(
cosmic::Cosmic::title,
BootData(Rc::new(RefCell::new(Some(BootDataInner::<App> {
flags,
core,
settings: window_settings.clone(),
})))),
cosmic::Cosmic::update,
cosmic::Cosmic::view,
)
.subscription(cosmic::Cosmic::subscription)
.title(cosmic::Cosmic::title)
.style(cosmic::Cosmic::style)
.theme(cosmic::Cosmic::theme)
.window_size((500.0, 800.0))
.settings(settings)
.window(window_settings)
.run_with(move || cosmic::Cosmic::<App>::init(flags))
.run()
}
#[cfg(feature = "multi-window")]
{
let mut app = multi_window::multi_window::<_, _, _, _, App::Executor>(
cosmic::Cosmic::title,
let no_main_window = core.main_window.is_none();
if no_main_window {
// app = app.window(window_settings);
core.main_window = Some(iced_core::window::Id::RESERVED);
}
let app = iced::daemon(
BootData(Rc::new(RefCell::new(Some(BootDataInner::<App> {
flags,
core,
settings: window_settings,
})))),
cosmic::Cosmic::update,
cosmic::Cosmic::view,
);
if flags.0.main_window.is_none() {
app = app.window(window_settings);
flags.0.main_window = Some(iced_core::window::Id::RESERVED);
}
app.subscription(cosmic::Cosmic::subscription)
.title(cosmic::Cosmic::title)
.style(cosmic::Cosmic::style)
.theme(cosmic::Cosmic::theme)
.settings(settings)
.run_with(move || cosmic::Cosmic::<App>::init(flags))
.run()
}
}
@ -149,6 +197,9 @@ where
App::Flags: CosmicFlags,
App::Message: Clone + std::fmt::Debug + Send + 'static,
{
#[cfg(feature = "desktop")]
image_extras::register();
use std::collections::HashMap;
let activation_token = std::env::var("XDG_ACTIVATION_TOKEN").ok();
@ -204,13 +255,17 @@ where
tracing::info!("Another instance is running");
Ok(())
} else {
let (settings, mut flags, window_settings) = iced_settings::<App>(settings, flags);
flags.0.single_instance = true;
let (settings, (mut core, flags), window_settings) = iced_settings::<App>(settings, flags);
core.single_instance = true;
#[cfg(not(feature = "multi-window"))]
{
iced::application(
cosmic::Cosmic::title,
BootData(Rc::new(RefCell::new(Some(BootDataInner::<App> {
flags,
core,
settings: window_settings.clone(),
})))),
cosmic::Cosmic::update,
cosmic::Cosmic::view,
)
@ -220,24 +275,31 @@ where
.window_size((500.0, 800.0))
.settings(settings)
.window(window_settings)
.run_with(move || cosmic::Cosmic::<App>::init(flags))
.run()
}
#[cfg(feature = "multi-window")]
{
let mut app = multi_window::multi_window::<_, _, _, _, App::Executor>(
cosmic::Cosmic::title,
let no_main_window = core.main_window.is_none();
if no_main_window {
// app = app.window(window_settings);
core.main_window = Some(iced_core::window::Id::RESERVED);
}
let mut app = iced::daemon(
BootData(Rc::new(RefCell::new(Some(BootDataInner::<App> {
flags,
core,
settings: window_settings,
})))),
cosmic::Cosmic::update,
cosmic::Cosmic::view,
);
if flags.0.main_window.is_none() {
app = app.window(window_settings);
flags.0.main_window = Some(iced_core::window::Id::RESERVED);
}
app.subscription(cosmic::Cosmic::subscription)
.style(cosmic::Cosmic::style)
.title(cosmic::Cosmic::title)
.theme(cosmic::Cosmic::theme)
.settings(settings)
.run_with(move || cosmic::Cosmic::<App>::init(flags))
.run()
}
}
}
@ -329,9 +391,8 @@ where
.on_context(|id| crate::Action::Cosmic(Action::NavBarContext(id)))
.context_menu(self.nav_context_menu(self.core().nav_bar_context()))
.into_container()
// XXX both must be shrink to avoid flex layout from ignoring it
.width(iced::Length::Shrink)
.height(iced::Length::Shrink);
.height(iced::Length::Fill);
if !self.core().is_condensed() {
nav = nav.max_width(280);
@ -428,7 +489,7 @@ where
}
/// Overrides the default style for applications
fn style(&self) -> Option<iced_runtime::Appearance> {
fn style(&self) -> Option<theme::Style> {
None
}
@ -664,16 +725,17 @@ impl<App: Application> ApplicationExt for App {
[0, 0, 0, 0]
})
.into(),
)
);
} else {
//TODO: this element is added to workaround state issues
widgets.push(horizontal_space().width(Length::Shrink).into());
widgets.push(space::horizontal().width(Length::Shrink).into());
}
}
}
widgets
});
let content_col = crate::widget::column::with_capacity(2)
.push(content_row)
.push_maybe(self.footer().map(|footer| {
@ -686,7 +748,6 @@ impl<App: Application> ApplicationExt for App {
}));
let content: Element<_> = if content_container {
content_col
.apply(container)
.width(iced::Length::Fill)
.height(iced::Length::Fill)
.apply(|w| id_container(w, iced_core::id::Id::new("COSMIC_content_container")))
@ -716,8 +777,7 @@ impl<App: Application> ApplicationExt for App {
.title(&core.window.header_title)
.on_drag(crate::Action::Cosmic(Action::Drag))
.on_right_click(crate::Action::Cosmic(Action::ShowWindowMenu))
.on_double_click(crate::Action::Cosmic(Action::Maximize))
.is_condensed(is_condensed);
.on_double_click(crate::Action::Cosmic(Action::Maximize));
if self.nav_model().is_some() {
let toggle = crate::widget::nav_bar_toggle()

View file

@ -1,244 +0,0 @@
// Copyright 2024 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
//! Create and run daemons that run in the background.
//! Copied from iced 0.13, but adds optional initial window
use iced::application;
use iced::window;
use iced::{
self, Program,
program::{self, with_style, with_subscription, with_theme, with_title},
runtime::{Appearance, DefaultStyle},
};
use iced::{Element, Result, Settings, Subscription, Task};
use std::marker::PhantomData;
pub(crate) struct Instance<State, Message, Theme, Renderer, Update, View, Executor> {
update: Update,
view: View,
_state: PhantomData<State>,
_message: PhantomData<Message>,
_theme: PhantomData<Theme>,
_renderer: PhantomData<Renderer>,
_executor: PhantomData<Executor>,
}
/// Creates an iced [`MultiWindow`] given its title, update, and view logic.
pub fn multi_window<State, Message, Theme, Renderer, Executor>(
title: impl Title<State>,
update: impl application::Update<State, Message>,
view: impl for<'a> self::View<'a, State, Message, Theme, Renderer>,
) -> MultiWindow<impl Program<State = State, Message = Message, Theme = Theme>>
where
State: 'static,
Message: Send + std::fmt::Debug + 'static,
Theme: Default + DefaultStyle,
Renderer: program::Renderer,
Executor: iced::Executor,
{
use std::marker::PhantomData;
impl<State, Message, Theme, Renderer, Update, View, Executor> Program
for Instance<State, Message, Theme, Renderer, Update, View, Executor>
where
Message: Send + std::fmt::Debug + 'static,
Theme: Default + DefaultStyle,
Renderer: program::Renderer,
Update: application::Update<State, Message>,
View: for<'a> self::View<'a, State, Message, Theme, Renderer>,
Executor: iced::Executor,
{
type State = State;
type Message = Message;
type Theme = Theme;
type Renderer = Renderer;
type Executor = Executor;
fn update(&self, state: &mut Self::State, message: Self::Message) -> Task<Self::Message> {
self.update.update(state, message).into()
}
fn view<'a>(
&self,
state: &'a Self::State,
window: window::Id,
) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> {
self.view.view(state, window).into()
}
}
MultiWindow {
raw: Instance {
update,
view,
_state: PhantomData,
_message: PhantomData,
_theme: PhantomData,
_renderer: PhantomData,
_executor: PhantomData::<Executor>,
},
settings: Settings::default(),
window: None,
}
.title(title)
}
/// The underlying definition and configuration of an iced daemon.
///
/// You can use this API to create and run iced applications
/// step by step—without coupling your logic to a trait
/// or a specific type.
///
/// You can create a [`MultiWindow`] with the [`daemon`] helper.
#[derive(Debug)]
pub struct MultiWindow<P: Program> {
raw: P,
settings: Settings,
window: Option<window::Settings>,
}
impl<P: Program> MultiWindow<P> {
#[cfg(any(feature = "winit", feature = "wayland"))]
/// Runs the [`MultiWindow`].
///
/// The state of the [`MultiWindow`] must implement [`Default`].
/// If your state does not implement [`Default`], use [`run_with`]
/// instead.
///
/// [`run_with`]: Self::run_with
pub fn run(self) -> Result
where
Self: 'static,
P::State: Default,
{
self.raw.run(self.settings, self.window)
}
#[cfg(any(feature = "winit", feature = "wayland"))]
/// Runs the [`MultiWindow`] with a closure that creates the initial state.
pub fn run_with<I>(self, initialize: I) -> Result
where
Self: 'static,
I: FnOnce() -> (P::State, Task<P::Message>) + 'static,
{
self.raw.run_with(self.settings, self.window, initialize)
}
/// Sets the [`Settings`] that will be used to run the [`MultiWindow`].
pub fn settings(self, settings: Settings) -> Self {
Self { settings, ..self }
}
/// Sets the [`Title`] of the [`MultiWindow`].
pub(crate) fn title(
self,
title: impl Title<P::State>,
) -> MultiWindow<impl Program<State = P::State, Message = P::Message, Theme = P::Theme>> {
MultiWindow {
raw: with_title(self.raw, move |state, window| title.title(state, window)),
settings: self.settings,
window: self.window,
}
}
/// Sets the subscription logic of the [`MultiWindow`].
pub fn subscription(
self,
f: impl Fn(&P::State) -> Subscription<P::Message>,
) -> MultiWindow<impl Program<State = P::State, Message = P::Message, Theme = P::Theme>> {
MultiWindow {
raw: with_subscription(self.raw, f),
settings: self.settings,
window: self.window,
}
}
/// Sets the theme logic of the [`MultiWindow`].
pub fn theme(
self,
f: impl Fn(&P::State, window::Id) -> P::Theme,
) -> MultiWindow<impl Program<State = P::State, Message = P::Message, Theme = P::Theme>> {
MultiWindow {
raw: with_theme(self.raw, f),
settings: self.settings,
window: self.window,
}
}
/// Sets the style logic of the [`MultiWindow`].
pub fn style(
self,
f: impl Fn(&P::State, &P::Theme) -> Appearance,
) -> MultiWindow<impl Program<State = P::State, Message = P::Message, Theme = P::Theme>> {
MultiWindow {
raw: with_style(self.raw, f),
settings: self.settings,
window: self.window,
}
}
/// Sets the window settings of the [`MultiWindow`].
pub fn window(self, window: window::Settings) -> Self {
Self {
raw: self.raw,
settings: self.settings,
window: Some(window),
}
}
}
/// The title logic of some [`MultiWindow`].
///
/// This trait is implemented both for `&static str` and
/// any closure `Fn(&State, window::Id) -> String`.
///
/// This trait allows the [`daemon`] builder to take any of them.
pub trait Title<State> {
/// Produces the title of the [`MultiWindow`].
fn title(&self, state: &State, window: window::Id) -> String;
}
impl<State> Title<State> for &'static str {
fn title(&self, _state: &State, _window: window::Id) -> String {
(*self).to_string()
}
}
impl<T, State> Title<State> for T
where
T: Fn(&State, window::Id) -> String,
{
fn title(&self, state: &State, window: window::Id) -> String {
self(state, window)
}
}
/// The view logic of some [`MultiWindow`].
///
/// This trait allows the [`daemon`] builder to take any closure that
/// returns any `Into<Element<'_, Message>>`.
pub trait View<'a, State, Message, Theme, Renderer> {
/// Produces the widget of the [`MultiWindow`].
fn view(
&self,
state: &'a State,
window: window::Id,
) -> impl Into<Element<'a, Message, Theme, Renderer>>;
}
impl<'a, T, State, Message, Theme, Renderer, Widget> View<'a, State, Message, Theme, Renderer> for T
where
T: Fn(&'a State, window::Id) -> Widget,
State: 'static,
Widget: Into<Element<'a, Message, Theme, Renderer>>,
{
fn view(
&self,
state: &'a State,
window: window::Id,
) -> impl Into<Element<'a, Message, Theme, Renderer>> {
self(state, window)
}
}

View file

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

View file

@ -217,7 +217,7 @@ where
}
fn layout(
&self,
&mut self,
tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
@ -233,25 +233,26 @@ where
self.padding,
self.spacing,
self.align,
&self.children,
&mut self.children,
&mut tree.children,
)
}
fn operate(
&self,
&mut self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn Operation,
) {
operation.container(None, layout.bounds(), &mut |operation| {
operation.container(None, layout.bounds());
operation.traverse(&mut |operation| {
self.children
.iter()
.iter_mut()
.zip(&mut tree.children)
.zip(layout.children())
.for_each(|((child, state), c_layout)| {
child.as_widget().operate(
child.as_widget_mut().operate(
state,
c_layout.with_virtual_offset(layout.virtual_offset()),
renderer,
@ -261,17 +262,17 @@ where
});
}
fn on_event(
fn update(
&mut self,
tree: &mut Tree,
event: Event,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) -> event::Status {
) {
let my_state = tree.state.downcast_mut::<State>();
if let Some(hovered) = my_state.hovered {
@ -285,7 +286,7 @@ where
e,
mouse::Event::CursorLeft | mouse::Event::ButtonReleased { .. }
) {
return self.children[hovered].as_widget_mut().on_event(
return self.children[hovered].as_widget_mut().update(
&mut tree.children[hovered],
event,
child_layout.with_virtual_offset(layout.virtual_offset()),
@ -302,7 +303,7 @@ where
iced::core::touch::Event::FingerLifted { .. }
| iced::core::touch::Event::FingerLost { .. }
) {
return self.children[hovered].as_widget_mut().on_event(
return self.children[hovered].as_widget_mut().update(
&mut tree.children[hovered],
event,
child_layout.with_virtual_offset(layout.virtual_offset()),
@ -319,49 +320,49 @@ where
}
}
self.children
for (((i, child), state), c_layout) in self
.children
.iter_mut()
.enumerate()
.zip(&mut tree.children)
.zip(layout.children())
.map(|(((i, child), state), c_layout)| {
let mut cursor_virtual = cursor;
if matches!(
event,
Event::Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorEntered)
| Event::Touch(
iced_core::touch::Event::FingerMoved { .. }
| iced_core::touch::Event::FingerPressed { .. }
)
) && cursor.is_over(c_layout.bounds())
{
my_state.hovered = Some(i);
return child.as_widget_mut().on_event(
state,
event.clone(),
c_layout.with_virtual_offset(layout.virtual_offset()),
cursor_virtual,
renderer,
clipboard,
shell,
viewport,
);
} else if my_state.hovered.is_some_and(|h| i != h) {
cursor_virtual = mouse::Cursor::Unavailable;
}
child.as_widget_mut().on_event(
{
let mut cursor_virtual = cursor;
if matches!(
event,
Event::Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorEntered)
| Event::Touch(
iced_core::touch::Event::FingerMoved { .. }
| iced_core::touch::Event::FingerPressed { .. }
)
) && cursor.is_over(c_layout.bounds())
{
my_state.hovered = Some(i);
return child.as_widget_mut().update(
state,
event.clone(),
&event,
c_layout.with_virtual_offset(layout.virtual_offset()),
cursor_virtual,
renderer,
clipboard,
shell,
viewport,
)
})
.fold(event::Status::Ignored, event::Status::merge)
);
} else if my_state.hovered.is_some_and(|h| i != h) {
cursor_virtual = mouse::Cursor::Unavailable;
}
child.as_widget_mut().update(
state,
&event,
c_layout.with_virtual_offset(layout.virtual_offset()),
cursor_virtual,
renderer,
clipboard,
shell,
viewport,
);
}
}
fn mouse_interaction(
@ -436,11 +437,19 @@ where
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'_>,
layout: Layout<'b>,
renderer: &Renderer,
viewport: &Rectangle,
translation: Vector,
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
overlay::from_children(&mut self.children, tree, layout, renderer, translation)
overlay::from_children(
&mut self.children,
tree,
layout,
renderer,
viewport,
translation,
)
}
#[cfg(feature = "a11y")]

View file

@ -1,34 +1,36 @@
#[cfg(feature = "applet-token")]
pub mod token;
use crate::app::cosmic;
use crate::app::{BootData, BootDataInner, cosmic};
use crate::{
Application, Element, Renderer,
app::iced_settings,
cctk::sctk,
iced::{
self, Color, Length, Limits, Rectangle,
alignment::{Alignment, Horizontal, Vertical},
widget::Container,
window,
},
iced_widget,
theme::{self, Button, THEME, system_dark, system_light},
widget::{
self,
autosize::{self, Autosize, autosize},
column::Column,
horizontal_space, layer_container,
layer_container,
row::Row,
vertical_space,
space::horizontal,
space::vertical,
},
};
pub use cosmic_panel_config;
use cosmic_panel_config::{CosmicPanelBackground, PanelAnchor, PanelSize};
use iced::{
self, Color, Length, Limits, Rectangle,
alignment::{Alignment, Horizontal, Vertical},
widget::Container,
window,
};
use iced_core::{Padding, Shadow};
use iced_runtime::platform_specific::wayland::popup::{SctkPopupSettings, SctkPositioner};
use iced_widget::Text;
use iced_widget::runtime::platform_specific::wayland::popup::{SctkPopupSettings, SctkPositioner};
use sctk::reexports::protocols::xdg::shell::client::xdg_positioner::{Anchor, Gravity};
use std::cell::RefCell;
use std::{borrow::Cow, num::NonZeroU32, rc::Rc, sync::LazyLock, time::Duration};
use tracing::info;
@ -40,7 +42,7 @@ static AUTOSIZE_ID: LazyLock<iced::id::Id> =
static AUTOSIZE_MAIN_ID: LazyLock<iced::id::Id> =
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_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)]
pub struct Context {
@ -224,7 +226,7 @@ impl Context {
let symbolic = icon.symbolic;
let icon = widget::icon(icon)
.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()),
}))
} else {
@ -386,10 +388,10 @@ impl Context {
},
shadow: Shadow::default(),
icon_color: Some(cosmic.background.on.into()),
snap: true,
}
}),
)
.width(Length::Shrink)
.height(Length::Shrink)
.align_x(horizontal_align)
.align_y(vertical_align),
@ -571,26 +573,33 @@ pub fn run<App: Application>(flags: App::Flags) -> iced::Result {
// TODO make multi-window not mandatory
let mut app = super::app::multi_window::multi_window::<_, _, _, _, App::Executor>(
cosmic::Cosmic::title,
let no_main_window = core.main_window.is_none();
if no_main_window {
// TODO still apply window settings?
// window_settings = window_settings.clone();
core.main_window = Some(iced_core::window::Id::RESERVED);
}
let mut app = iced::daemon(
BootData(Rc::new(RefCell::new(Some(BootDataInner::<App> {
flags,
core,
settings: window_settings,
})))),
cosmic::Cosmic::update,
cosmic::Cosmic::view,
);
if core.main_window.is_none() {
app = app.window(window_settings.clone());
core.main_window = Some(iced_core::window::Id::RESERVED);
}
app.subscription(cosmic::Cosmic::subscription)
.style(cosmic::Cosmic::style)
.theme(cosmic::Cosmic::theme)
.settings(iced_settings)
.run_with(move || cosmic::Cosmic::<App>::init((core, flags)))
.run()
}
#[must_use]
pub fn style() -> iced_runtime::Appearance {
pub fn style() -> iced::theme::Style {
let theme = crate::theme::THEME.lock().unwrap();
iced_runtime::Appearance {
iced::theme::Style {
background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0),
text_color: theme.cosmic().on_bg_color().into(),
icon_color: theme.cosmic().on_bg_color().into(),

View file

@ -208,7 +208,7 @@ where
}
fn layout(
&self,
&mut self,
tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
@ -222,25 +222,26 @@ where
self.padding,
self.spacing,
self.align,
&self.children,
&mut self.children,
&mut tree.children,
)
}
fn operate(
&self,
&mut self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn Operation,
) {
operation.container(None, layout.bounds(), &mut |operation| {
operation.container(None, layout.bounds());
operation.traverse(&mut |operation| {
self.children
.iter()
.iter_mut()
.zip(&mut tree.children)
.zip(layout.children())
.for_each(|((child, state), c_layout)| {
child.as_widget().operate(
child.as_widget_mut().operate(
state,
c_layout.with_virtual_offset(layout.virtual_offset()),
renderer,
@ -250,17 +251,17 @@ where
});
}
fn on_event(
fn update(
&mut self,
tree: &mut Tree,
event: Event,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) -> event::Status {
) {
let my_state = tree.state.downcast_mut::<State>();
if let Some(hovered) = my_state.hovered {
@ -274,7 +275,7 @@ where
e,
mouse::Event::CursorLeft | mouse::Event::ButtonReleased { .. }
) {
return self.children[hovered].as_widget_mut().on_event(
return self.children[hovered].as_widget_mut().update(
&mut tree.children[hovered],
event,
child_layout.with_virtual_offset(layout.virtual_offset()),
@ -291,7 +292,7 @@ where
iced::core::touch::Event::FingerLifted { .. }
| iced::core::touch::Event::FingerLost { .. }
) {
return self.children[hovered].as_widget_mut().on_event(
return self.children[hovered].as_widget_mut().update(
&mut tree.children[hovered],
event,
child_layout.with_virtual_offset(layout.virtual_offset()),
@ -308,50 +309,50 @@ where
}
}
self.children
for (((i, child), state), c_layout) in self
.children
.iter_mut()
.enumerate()
.zip(&mut tree.children)
.zip(layout.children())
.map(|(((i, child), state), c_layout)| {
let mut cursor_virtual = cursor;
{
let mut cursor_virtual = cursor;
if matches!(
event,
Event::Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorEntered)
| Event::Touch(
iced_core::touch::Event::FingerMoved { .. }
| iced_core::touch::Event::FingerPressed { .. }
)
) && cursor.is_over(c_layout.bounds())
{
my_state.hovered = Some(i);
return child.as_widget_mut().on_event(
state,
event.clone(),
c_layout.with_virtual_offset(layout.virtual_offset()),
cursor_virtual,
renderer,
clipboard,
shell,
viewport,
);
} else if my_state.hovered.is_some_and(|h| i != h) {
cursor_virtual = mouse::Cursor::Unavailable;
}
child.as_widget_mut().on_event(
if matches!(
event,
Event::Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorEntered)
| Event::Touch(
iced_core::touch::Event::FingerMoved { .. }
| iced_core::touch::Event::FingerPressed { .. }
)
) && cursor.is_over(c_layout.bounds())
{
my_state.hovered = Some(i);
return child.as_widget_mut().update(
state,
event.clone(),
&event,
c_layout.with_virtual_offset(layout.virtual_offset()),
cursor_virtual,
renderer,
clipboard,
shell,
viewport,
)
})
.fold(event::Status::Ignored, event::Status::merge)
);
} else if my_state.hovered.is_some_and(|h| i != h) {
cursor_virtual = mouse::Cursor::Unavailable;
}
child.as_widget_mut().update(
state,
&event,
c_layout.with_virtual_offset(layout.virtual_offset()),
cursor_virtual,
renderer,
clipboard,
shell,
viewport,
);
}
}
fn mouse_interaction(
@ -426,11 +427,19 @@ where
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'_>,
layout: Layout<'b>,
renderer: &Renderer,
viewport: &Rectangle,
translation: Vector,
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
overlay::from_children(&mut self.children, tree, layout, renderer, translation)
overlay::from_children(
&mut self.children,
tree,
layout,
renderer,
viewport,
translation,
)
}
#[cfg(feature = "a11y")]

View file

@ -1,11 +1,11 @@
use crate::iced;
use crate::iced_futures::futures;
use cctk::sctk::reexports::calloop;
use futures::{
SinkExt, StreamExt,
channel::mpsc::{UnboundedReceiver, unbounded},
};
use iced::Subscription;
use iced_futures::futures;
use iced_futures::stream;
use std::{fmt::Debug, hash::Hash, thread::JoinHandle};
@ -14,16 +14,15 @@ use super::wayland_handler::wayland_handler;
pub fn activation_token_subscription<I: 'static + Hash + Copy + Send + Sync + Debug>(
id: I,
) -> iced::Subscription<TokenUpdate> {
Subscription::run_with_id(
id,
Subscription::run_with(id, |_| {
stream::channel(50, move |mut output| async move {
let mut state = State::Ready;
loop {
state = start_listening(state, &mut output).await;
}
}),
)
})
})
}
pub enum State {

View file

@ -39,7 +39,7 @@ pub fn set_theme<M: Send + 'static>(theme: crate::Theme) -> iced::Task<crate::Ac
/// Sets the window mode to windowed.
pub fn set_windowed<M>(id: window::Id) -> iced::Task<crate::Action<M>> {
iced_runtime::window::change_mode(id, window::Mode::Windowed)
iced_runtime::window::set_mode(id, window::Mode::Windowed)
}
/// Toggles the windows' maximize state.

View file

@ -99,7 +99,7 @@ pub struct Core {
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,
}
@ -159,7 +159,7 @@ impl Default for Core {
main_window: None,
exit_on_main_window_closed: true,
menu_bars: HashMap::new(),
#[cfg(feature = "wayland")]
#[cfg(all(feature = "wayland", target_os = "linux"))]
sync_window_border_radii_to_theme: true,
}
}
@ -493,12 +493,12 @@ impl Core {
}
// TODO should we emit tasks setting the corner radius or unsetting it if this is changed?
#[cfg(feature = "wayland")]
#[cfg(all(feature = "wayland", target_os = "linux"))]
pub fn set_sync_window_border_radii_to_theme(&mut self, sync: bool) {
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 {
self.sync_window_border_radii_to_theme
}

View file

@ -16,75 +16,80 @@ use {
#[cold]
pub fn subscription<App: ApplicationExt>() -> Subscription<crate::Action<App::Message>> {
use iced_futures::futures::StreamExt;
iced_futures::Subscription::run_with_id(
TypeId::of::<DbusActivation>(),
iced::stream::channel(10, move |mut output| async move {
let mut single_instance: DbusActivation = DbusActivation::new();
let mut rx = single_instance.rx();
if let Ok(builder) = zbus::connection::Builder::session() {
let path: String = format!("/{}", App::APP_ID.replace('.', "/"));
if let Ok(conn) = builder.build().await {
// XXX Setup done this way seems to be more reliable.
//
// the docs for serve_at seem to imply it will replace the
// existing interface at the requested path, but it doesn't
// seem to work that way all the time. The docs for
// object_server().at() imply it won't replace the existing
// interface.
//
// request_name is used either way, with the builder or
// with the connection, but it must be done after the
// object server is setup.
if conn.object_server().at(path, single_instance).await != Ok(true) {
tracing::error!("Failed to serve dbus");
std::process::exit(1);
}
if conn.request_name(App::APP_ID).await.is_err() {
tracing::error!("Failed to serve dbus");
std::process::exit(1);
}
iced_futures::Subscription::run_with(TypeId::of::<DbusActivation>(), |_| {
iced::stream::channel(
10,
move |mut output: Sender<crate::Action<App::Message>>| async move {
let mut single_instance: DbusActivation = DbusActivation::new();
let mut rx = single_instance.rx();
if let Ok(builder) = zbus::connection::Builder::session() {
let path: String = format!("/{}", App::APP_ID.replace('.', "/"));
if let Ok(conn) = builder.build().await {
// XXX Setup done this way seems to be more reliable.
//
// the docs for serve_at seem to imply it will replace the
// existing interface at the requested path, but it doesn't
// seem to work that way all the time. The docs for
// object_server().at() imply it won't replace the existing
// interface.
//
// request_name is used either way, with the builder or
// with the connection, but it must be done after the
// object server is setup.
if conn.object_server().at(path, single_instance).await != Ok(true) {
tracing::error!("Failed to serve dbus");
std::process::exit(1);
}
if conn.request_name(App::APP_ID).await.is_err() {
tracing::error!("Failed to serve dbus");
std::process::exit(1);
}
output
.send(crate::Action::Cosmic(crate::app::Action::DbusConnection(
conn.clone(),
)))
.await;
output
.send(crate::Action::Cosmic(crate::app::Action::DbusConnection(
conn.clone(),
)))
.await;
#[cfg(feature = "smol")]
let handle = {
std::thread::spawn(move || {
let conn_clone = _conn.clone();
#[cfg(feature = "smol")]
let handle = {
std::thread::spawn(move || {
let conn_clone = _conn.clone();
zbus::block_on(async move {
loop {
conn_clone.executor().tick().await;
}
zbus::block_on(async move {
loop {
conn_clone.executor().tick().await;
}
})
})
})
};
while let Some(mut msg) = rx.next().await {
if let Some(token) = msg.activation_token.take() {
if let Err(err) = output
.send(crate::Action::Cosmic(crate::app::Action::Activate(token)))
.await
};
while let Some(mut msg) = rx.next().await {
if let Some(token) = msg.activation_token.take() {
if let Err(err) = output
.send(crate::Action::Cosmic(crate::app::Action::Activate(
token,
)))
.await
{
tracing::error!(?err, "Failed to send message");
}
}
if let Err(err) = output.send(crate::Action::DbusActivation(msg)).await
{
tracing::error!(?err, "Failed to send message");
}
}
if let Err(err) = output.send(crate::Action::DbusActivation(msg)).await {
tracing::error!(?err, "Failed to send message");
}
}
} else {
tracing::warn!("Failed to connect to dbus for single instance");
}
} else {
tracing::warn!("Failed to connect to dbus for single instance");
}
loop {
iced::futures::pending!();
}
}),
)
loop {
iced::futures::pending!();
}
},
)
})
}
#[derive(Debug, Clone)]

View file

@ -416,7 +416,6 @@ fn match_exec_basename(
};
let basename_lower = basename.to_ascii_lowercase();
if normalized
.iter()
.any(|candidate| candidate == &basename_lower)
@ -440,8 +439,7 @@ fn fallback_entry(context: &DesktopLookupContext<'_>) -> fde::DesktopEntry {
let name = context
.title
.as_ref()
.map(|title| title.to_string())
.unwrap_or_else(|| context.app_id.to_string());
.map_or_else(|| context.app_id.to_string(), |title| title.to_string());
entry.add_desktop_entry("Name".to_string(), name);
entry
}
@ -458,7 +456,9 @@ fn proton_or_wine_fallback(
) -> Option<fde::DesktopEntry> {
let app_id = context.app_id.as_ref();
let is_proton_game = app_id == "steam_app_default";
let is_wine_entry = app_id.ends_with(".exe");
let is_wine_entry = std::path::Path::new(app_id)
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("exe"));
if !is_proton_game && !is_wine_entry {
return None;
@ -487,10 +487,6 @@ fn proton_or_wine_fallback(
#[cfg(not(windows))]
fn candidate_desktop_ids(context: &DesktopLookupContext<'_>) -> Vec<String> {
const SUFFIXES: &[&str] = &[".desktop", ".Desktop", ".DESKTOP"];
let mut ordered = Vec::new();
let mut seen = HashSet::new();
fn push_candidate(seen: &mut HashSet<String>, ordered: &mut Vec<String>, candidate: &str) {
let trimmed = candidate.trim();
if trimmed.is_empty() {
@ -531,11 +527,11 @@ fn candidate_desktop_ids(context: &DesktopLookupContext<'_>) -> Vec<String> {
}
}
if trimmed.contains('.') {
if let Some(last) = trimmed.rsplit('.').next() {
if last.len() >= 2 {
push_candidate(seen, ordered, last);
}
if trimmed.contains('.')
&& let Some(last) = trimmed.rsplit('.').next()
{
if last.len() >= 2 {
push_candidate(seen, ordered, last);
}
}
@ -546,13 +542,20 @@ fn candidate_desktop_ids(context: &DesktopLookupContext<'_>) -> Vec<String> {
push_candidate(seen, ordered, &trimmed.replace('_', "-"));
}
for token in trimmed.split(|c: char| matches!(c, '.' | '-' | '_' | '@' | ' ')) {
for token in
trimmed.split(|c: char| matches!(c, '.' | '-' | '_' | '@') || c.is_whitespace())
{
if token.len() >= 2 && token != trimmed {
push_candidate(seen, ordered, token);
}
}
}
const SUFFIXES: &[&str] = &[".desktop", ".Desktop", ".DESKTOP"];
let mut ordered = Vec::new();
let mut seen = HashSet::new();
add_variants(
&mut seen,
&mut ordered,
@ -786,7 +789,7 @@ pub async fn spawn_desktop_exec<S, I, K, V>(
})
.unwrap_or_else(|| String::from("cosmic-term"));
term_exec = format!("{term} -- {}", exec.as_ref());
term_exec = format!("{term} -e {}", exec.as_ref());
&term_exec
} else {
exec.as_ref()
@ -915,12 +918,20 @@ mod tests {
let candidates = candidate_desktop_ids(&ctx);
assert_eq!(candidates.first().unwrap(), "com.example.App.desktop");
assert!(candidates.contains(&"com.example.App".to_string()));
assert!(candidates.contains(&"com-example-App".to_string()));
assert!(candidates.contains(&"com_example_App".to_string()));
assert!(candidates.contains(&"Example App".to_string()));
assert!(candidates.contains(&"Example".to_string()));
assert!(candidates.contains(&"App".to_string()));
for test in [
"com.example.App",
"com-example-App",
"com_example_App",
"Example App",
"Example",
"App",
] {
assert!(
candidates
.iter()
.any(|c| c.to_ascii_lowercase() == test.to_ascii_lowercase()),
);
}
}
#[test]
@ -985,7 +996,7 @@ Icon=vmware-workstation\n\
let resolved = resolve_desktop_entry(&mut cache, &ctx, &DesktopResolveOptions::default());
assert_eq!(resolved.id(), "vmware-workstation.desktop");
assert_eq!(resolved.id(), "vmware-workstation");
}
#[test]

View file

@ -26,4 +26,8 @@ impl iced::Executor for Executor {
let _guard = self.0.enter();
f()
}
fn block_on<T>(&self, future: impl Future<Output = T>) -> T {
self.0.block_on(future)
}
}

View file

@ -30,4 +30,8 @@ impl iced::Executor for Executor {
let _guard = self.0.enter();
f()
}
fn block_on<T>(&self, future: impl Future<Output = T>) -> T {
self.0.block_on(future)
}
}

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 {
/// Combines color with background to create appearance of transparency.
#[must_use]

View file

@ -3,6 +3,7 @@
#![allow(clippy::module_name_repetitions)]
#![cfg_attr(target_os = "redox", feature(lazy_cell))]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
/// Recommended default imports.
pub mod prelude {
@ -18,6 +19,8 @@ pub use apply::{Also, Apply};
pub mod action;
pub use action::Action;
pub mod anim;
#[cfg(feature = "winit")]
pub mod app;
#[cfg(feature = "winit")]
@ -64,29 +67,6 @@ pub mod font;
#[doc(inline)]
pub use iced;
#[doc(inline)]
pub use iced_core;
#[doc(inline)]
pub use iced_futures;
#[doc(inline)]
pub use iced_renderer;
#[doc(inline)]
pub use iced_runtime;
#[doc(inline)]
pub use iced_widget;
#[doc(inline)]
#[cfg(feature = "winit")]
pub use iced_winit;
#[doc(inline)]
#[cfg(feature = "wgpu")]
pub use iced_wgpu;
pub mod icon_theme;
pub mod keyboard_nav;
@ -98,7 +78,8 @@ pub(crate) mod malloc;
#[cfg(all(feature = "process", not(windows)))]
pub mod process;
#[cfg(feature = "wayland")]
#[doc(inline)]
#[cfg(all(feature = "wayland", target_os = "linux"))]
pub use cctk;
pub mod surface;

View file

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

View file

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

View file

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

View file

@ -13,9 +13,8 @@ pub enum Desktop {
#[cold]
pub fn desktop_settings() -> iced_futures::Subscription<Desktop> {
iced_futures::Subscription::run_with_id(
std::any::TypeId::of::<Desktop>(),
stream::channel(10, |mut tx| {
iced_futures::Subscription::run(|| {
stream::channel(10, |mut tx: futures::channel::mpsc::Sender<Desktop>| {
async move {
let mut attempts = 0;
loop {
@ -99,6 +98,6 @@ pub fn desktop_settings() -> iced_futures::Subscription<Desktop> {
}
}
}
}),
)
})
})
}

View file

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

View file

@ -7,6 +7,7 @@ use crate::theme::{CosmicComponent, TRANSPARENT_COMPONENT, Theme};
use cosmic_theme::composite::over;
use iced::{
overlay::menu,
theme::Base,
widget::{
button as iced_button, checkbox as iced_checkbox, combo_box, container as iced_container,
pane_grid, pick_list, progress_bar, radio, rule, scrollable,
@ -15,7 +16,7 @@ use iced::{
},
};
use iced_core::{Background, Border, Color, Shadow, Vector};
use iced_widget::{pane_grid::Highlight, text_editor, text_input};
use iced_widget::{pane_grid::Highlight, scrollable::AutoScroll, text_editor, text_input};
use palette::WithAlpha;
use std::rc::Rc;
@ -36,13 +37,13 @@ pub mod application {
}
}
pub fn appearance(theme: &Theme) -> Appearance {
pub fn style(theme: &Theme) -> iced::theme::Style {
let cosmic = theme.cosmic();
Appearance {
icon_color: cosmic.bg_color().into(),
iced::theme::Style {
background_color: cosmic.bg_color().into(),
text_color: cosmic.on_bg_color().into(),
icon_color: cosmic.on_bg_color().into(),
}
}
}
@ -422,6 +423,7 @@ impl<'a> Container<'a> {
..Default::default()
},
shadow: Shadow::default(),
snap: true,
}
}
@ -436,6 +438,7 @@ impl<'a> Container<'a> {
..Default::default()
},
shadow: Shadow::default(),
snap: true,
}
}
@ -450,6 +453,7 @@ impl<'a> Container<'a> {
..Default::default()
},
shadow: Shadow::default(),
snap: true,
}
}
}
@ -493,6 +497,7 @@ impl iced_container::Catalog for Theme {
..Default::default()
},
shadow: Shadow::default(),
snap: true,
},
Container::List => {
@ -506,6 +511,7 @@ impl iced_container::Catalog for Theme {
..Default::default()
},
shadow: Shadow::default(),
snap: true,
}
}
@ -552,6 +558,7 @@ impl iced_container::Catalog for Theme {
.into(),
..Default::default()
},
snap: true,
shadow: Shadow::default(),
}
}
@ -582,6 +589,7 @@ impl iced_container::Catalog for Theme {
radius: cosmic.corner_radii.radius_s.into(),
},
shadow: Shadow::default(),
snap: true,
},
Container::Tooltip => iced_container::Style {
@ -593,6 +601,7 @@ impl iced_container::Catalog for Theme {
..Default::default()
},
shadow: Shadow::default(),
snap: true,
},
Container::Card => {
@ -610,6 +619,7 @@ impl iced_container::Catalog for Theme {
..Default::default()
},
shadow: Shadow::default(),
snap: true,
},
cosmic_theme::Layer::Primary => iced_container::Style {
icon_color: Some(Color::from(cosmic.primary.component.on)),
@ -622,6 +632,7 @@ impl iced_container::Catalog for Theme {
..Default::default()
},
shadow: Shadow::default(),
snap: true,
},
cosmic_theme::Layer::Secondary => iced_container::Style {
icon_color: Some(Color::from(cosmic.secondary.component.on)),
@ -634,6 +645,7 @@ impl iced_container::Catalog for Theme {
..Default::default()
},
shadow: Shadow::default(),
snap: true,
},
}
}
@ -652,6 +664,7 @@ impl iced_container::Catalog for Theme {
offset: Vector::new(0.0, 4.0),
blur_radius: 16.0,
},
snap: true,
},
}
}
@ -791,6 +804,7 @@ impl menu::Catalog for Theme {
},
selected_text_color: cosmic.accent_text_color().into(),
selected_background: Background::Color(cosmic.background.component.hover.into()),
shadow: Default::default(),
}
}
}
@ -830,7 +844,7 @@ impl pick_list::Catalog for Theme {
background: Background::Color(cosmic.background.base.into()),
..appearance
},
pick_list::Status::Opened => appearance,
pick_list::Status::Opened { is_hovered: _ } => appearance,
}
}
}
@ -920,6 +934,8 @@ impl toggler::Catalog for Theme {
background_border_color: Color::TRANSPARENT,
foreground_border_width: 0.0,
foreground_border_color: Color::TRANSPARENT,
text_color: None,
padding_ratio: 0.0,
};
match status {
toggler::Status::Active { is_toggled } => active,
@ -942,9 +958,9 @@ impl toggler::Catalog for Theme {
..active
}
}
toggler::Status::Disabled => {
active.background.a /= 2.;
active.foreground.a /= 2.;
toggler::Status::Disabled { is_toggled } => {
active.background = active.background.scale_alpha(0.5);
active.foreground = active.foreground.scale_alpha(0.5);
active
}
}
@ -1086,21 +1102,21 @@ impl rule::Catalog for Theme {
match class {
Rule::Default => rule::Style {
color: self.current_container().divider.into(),
width: 1,
radius: 0.0.into(),
fill_mode: rule::FillMode::Full,
snap: true,
},
Rule::LightDivider => rule::Style {
color: self.current_container().divider.into(),
width: 1,
radius: 0.0.into(),
fill_mode: rule::FillMode::Padded(8),
snap: true,
},
Rule::HeavyDivider => rule::Style {
color: self.current_container().divider.into(),
width: 4,
radius: 2.0.into(),
fill_mode: rule::FillMode::Full,
snap: true,
},
Rule::Custom(f) => f(self),
}
@ -1126,7 +1142,10 @@ impl scrollable::Catalog for Theme {
fn style(&self, class: &Self::Class<'_>, status: scrollable::Status) -> scrollable::Style {
match status {
scrollable::Status::Active => {
scrollable::Status::Active {
is_horizontal_scrollbar_disabled,
is_vertical_scrollbar_disabled,
} => {
let cosmic = self.cosmic();
let neutral_5 = cosmic.palette.neutral_5.with_alpha(0.7);
let neutral_6 = cosmic.palette.neutral_6.with_alpha(0.7);
@ -1139,7 +1158,7 @@ impl scrollable::Catalog for Theme {
},
background: None,
scroller: scrollable::Scroller {
color: if cosmic.is_dark {
background: if cosmic.is_dark {
neutral_6.into()
} else {
neutral_5.into()
@ -1157,7 +1176,7 @@ impl scrollable::Catalog for Theme {
},
background: None,
scroller: scrollable::Scroller {
color: if cosmic.is_dark {
background: if cosmic.is_dark {
neutral_6.into()
} else {
neutral_5.into()
@ -1169,6 +1188,13 @@ impl scrollable::Catalog for Theme {
},
},
gap: None,
// TODO: what is auto scroll?
auto_scroll: AutoScroll {
background: Color::TRANSPARENT.into(),
border: Border::default(),
shadow: Shadow::default(),
icon: Color::TRANSPARENT.into(),
},
};
let small_widget_container = self.current_container().small_widget.with_alpha(0.7);
@ -1200,7 +1226,7 @@ impl scrollable::Catalog for Theme {
},
background: None,
scroller: scrollable::Scroller {
color: if cosmic.is_dark {
background: if cosmic.is_dark {
neutral_6.into()
} else {
neutral_5.into()
@ -1218,7 +1244,7 @@ impl scrollable::Catalog for Theme {
},
background: None,
scroller: scrollable::Scroller {
color: if cosmic.is_dark {
background: if cosmic.is_dark {
neutral_6.into()
} else {
neutral_5.into()
@ -1230,6 +1256,13 @@ impl scrollable::Catalog for Theme {
},
},
gap: None,
// TODO: what is auto scroll?
auto_scroll: AutoScroll {
background: Color::TRANSPARENT.into(),
border: Border::default(),
shadow: Shadow::default(),
icon: Color::TRANSPARENT.into(),
},
};
if matches!(class, Scrollable::Permanent) {
@ -1400,7 +1433,7 @@ impl text_input::Catalog for Theme {
},
}
}
text_input::Status::Focused => {
text_input::Status::Focused { is_hovered } => {
let bg = self.current_container().small_widget.with_alpha(0.25);
match class {
@ -1477,7 +1510,8 @@ impl iced_widget::text_editor::Catalog for Theme {
let selection = cosmic.accent.base.into();
let value = cosmic.palette.neutral_9.into();
let placeholder = cosmic.palette.neutral_9.with_alpha(0.7).into();
let icon = cosmic.background.on.into();
let icon: Color = cosmic.background.on.into();
// TODO do we need to add icon color back?
match status {
iced_widget::text_editor::Status::Active
@ -1489,23 +1523,23 @@ impl iced_widget::text_editor::Catalog for Theme {
width: f32::from(cosmic.space_xxxs()),
color: iced::Color::from(cosmic.bg_divider()),
},
icon,
placeholder,
value,
selection,
},
iced_widget::text_editor::Status::Focused => iced_widget::text_editor::Style {
background: iced::Color::from(cosmic.bg_color()).into(),
border: Border {
radius: cosmic.corner_radii.radius_0.into(),
width: f32::from(cosmic.space_xxxs()),
color: iced::Color::from(cosmic.accent.base),
},
icon,
placeholder,
value,
selection,
},
iced_widget::text_editor::Status::Focused { is_hovered } => {
iced_widget::text_editor::Style {
background: iced::Color::from(cosmic.bg_color()).into(),
border: Border {
radius: cosmic.corner_radii.radius_0.into(),
width: f32::from(cosmic.space_xxxs()),
color: iced::Color::from(cosmic.accent.base),
},
placeholder,
value,
selection,
}
}
}
}
}
@ -1522,6 +1556,21 @@ impl iced_widget::markdown::Catalog for Theme {
}
}
impl iced_widget::table::Catalog for Theme {
type Class<'a> = iced_widget::table::StyleFn<'a, Self>;
fn default<'a>() -> Self::Class<'a> {
Box::new(|theme| iced_widget::table::Style {
separator_x: theme.current_container().divider.into(),
separator_y: theme.current_container().divider.into(),
})
}
fn style(&self, class: &Self::Class<'_>) -> iced_widget::table::Style {
class(self)
}
}
#[cfg(feature = "qr_code")]
impl iced_widget::qr_code::Catalog for Theme {
type Class<'a> = iced_widget::qr_code::StyleFn<'a, Self>;
@ -1539,3 +1588,50 @@ impl iced_widget::qr_code::Catalog for Theme {
}
impl combo_box::Catalog for Theme {}
impl Base for Theme {
fn default(preference: iced::theme::Mode) -> Self {
match preference {
iced::theme::Mode::Light => Theme::light(),
iced::theme::Mode::Dark | iced::theme::Mode::None => Theme::dark(),
}
}
fn mode(&self) -> iced::theme::Mode {
if self.theme_type.is_dark() {
iced::theme::Mode::Dark
} else {
iced::theme::Mode::Light
}
}
fn base(&self) -> iced::theme::Style {
iced::theme::Style {
background_color: self.cosmic().bg_color().into(),
text_color: self.cosmic().on_bg_color().into(),
icon_color: self.cosmic().on_bg_color().into(),
}
}
fn palette(&self) -> Option<iced::theme::Palette> {
Some(iced::theme::Palette {
primary: self.cosmic().accent.base.into(),
success: self.cosmic().success.base.into(),
warning: self.cosmic().warning.base.into(),
danger: self.cosmic().destructive.base.into(),
background: iced::Color::from(self.cosmic().bg_color()),
text: iced::Color::from(self.cosmic().on_bg_color()),
})
}
fn name(&self) -> &str {
match &self.theme_type {
crate::theme::ThemeType::Dark => "Cosmic Dark Theme",
crate::theme::ThemeType::Light => "Cosmic Light Theme",
crate::theme::ThemeType::HighContrastDark => "Cosmic High Contrast Dark Theme",
crate::theme::ThemeType::HighContrastLight => "Cosmic High Contrast Light Theme",
crate::theme::ThemeType::Custom(theme) => "Custom Cosmic Theme",
crate::theme::ThemeType::System { prefer_dark, theme } => &theme.name,
}
}
}

View file

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

View file

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

View file

@ -2,7 +2,7 @@
use iced::Size;
use iced::widget::Container;
use iced_core::event::{self, Event};
use iced_core::event::Event;
use iced_core::layout;
use iced_core::mouse;
use iced_core::overlay;
@ -172,7 +172,7 @@ where
}
fn layout(
&self,
&mut self,
tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
@ -186,7 +186,7 @@ where
}
fn operate(
&self,
&mut self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
@ -195,18 +195,18 @@ where
self.container.operate(tree, layout, renderer, operation);
}
fn on_event(
fn update(
&mut self,
tree: &mut Tree,
event: Event,
event: &Event,
layout: Layout<'_>,
cursor_position: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) -> event::Status {
self.container.on_event(
) {
self.container.update(
tree,
event,
layout,
@ -254,11 +254,13 @@ where
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'_>,
layout: Layout<'b>,
renderer: &Renderer,
viewport: &Rectangle,
translation: Vector,
) -> Option<overlay::Element<'b, Message, crate::Theme, Renderer>> {
self.container.overlay(tree, layout, renderer, translation)
self.container
.overlay(tree, layout, renderer, viewport, translation)
}
#[cfg(feature = "a11y")]

View file

@ -5,7 +5,7 @@ use iced_core::layout;
use iced_core::mouse;
use iced_core::overlay;
use iced_core::renderer;
use iced_core::widget::{Id, Tree};
use iced_core::widget::{Id, Operation, Tree};
use iced_core::{Clipboard, Element, Layout, Length, Rectangle, Shell, Vector, Widget};
pub use iced_widget::container::{Catalog, Style};
@ -107,7 +107,7 @@ where
}
fn diff(&mut self, tree: &mut Tree) {
tree.children[0].diff(&mut self.content);
tree.diff_children(std::slice::from_mut(&mut self.content));
}
fn size(&self) -> iced_core::Size<Length> {
@ -115,7 +115,7 @@ where
}
fn layout(
&self,
&mut self,
tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
@ -131,21 +131,22 @@ where
}
let node = self
.content
.as_widget()
.as_widget_mut()
.layout(&mut tree.children[0], renderer, &my_limits);
let size = node.size();
layout::Node::with_children(size, vec![node])
}
fn operate(
&self,
&mut self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn iced_core::widget::Operation<()>,
operation: &mut dyn Operation,
) {
operation.container(Some(&self.id), layout.bounds(), &mut |operation| {
self.content.as_widget().operate(
operation.container(Some(&self.id), layout.bounds());
operation.traverse(&mut |operation| {
self.content.as_widget_mut().operate(
&mut tree.children[0],
layout
.children()
@ -158,18 +159,18 @@ where
});
}
fn on_event(
fn update(
&mut self,
tree: &mut Tree,
event: Event,
event: &Event,
layout: Layout<'_>,
cursor_position: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) -> event::Status {
#[cfg(feature = "wayland")]
) {
#[cfg(all(feature = "wayland", target_os = "linux"))]
if matches!(
event,
Event::PlatformSpecific(event::PlatformSpecific::Wayland(
@ -179,9 +180,9 @@ where
let bounds = layout.bounds().size();
clipboard.request_logical_window_size(bounds.width.max(1.), bounds.height.max(1.));
}
self.content.as_widget_mut().on_event(
self.content.as_widget_mut().update(
&mut tree.children[0],
event.clone(),
event,
layout
.children()
.next()
@ -192,7 +193,7 @@ where
clipboard,
shell,
viewport,
)
);
}
fn mouse_interaction(
@ -238,8 +239,9 @@ where
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'_>,
layout: Layout<'b>,
renderer: &Renderer,
viewport: &Rectangle,
translation: Vector,
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
self.content.as_widget_mut().overlay(
@ -250,6 +252,7 @@ where
.unwrap()
.with_virtual_offset(layout.virtual_offset()),
renderer,
viewport,
translation,
)
}

View file

@ -3,10 +3,7 @@
use super::{Builder, ButtonClass};
use crate::Element;
use crate::widget::{
icon::{self, Handle},
tooltip,
};
use crate::widget::{icon::Handle, tooltip};
use apply::Apply;
use iced_core::{Alignment, Length, Padding, font::Weight, text::LineHeight, widget::Id};
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> {
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);
content.push(

View file

@ -318,7 +318,7 @@ impl<'a, Message: 'a + Clone> Widget<Message, crate::Theme, crate::Renderer>
}
fn layout(
&self,
&mut self,
tree: &mut Tree,
renderer: &crate::Renderer,
limits: &layout::Limits,
@ -331,21 +331,22 @@ impl<'a, Message: 'a + Clone> Widget<Message, crate::Theme, crate::Renderer>
self.padding,
|renderer, limits| {
self.content
.as_widget()
.as_widget_mut()
.layout(&mut tree.children[0], renderer, limits)
},
)
}
fn operate(
&self,
&mut self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &crate::Renderer,
operation: &mut dyn Operation<()>,
) {
operation.container(None, layout.bounds(), &mut |operation| {
self.content.as_widget().operate(
operation.container(None, layout.bounds());
operation.traverse(&mut |operation| {
self.content.as_widget_mut().operate(
&mut tree.children[0],
layout
.children()
@ -357,20 +358,20 @@ impl<'a, Message: 'a + Clone> Widget<Message, crate::Theme, crate::Renderer>
);
});
let state = tree.state.downcast_mut::<State>();
operation.focusable(state, Some(&self.id));
operation.focusable(Some(&self.id), layout.bounds(), state);
}
fn on_event(
fn update(
&mut self,
tree: &mut Tree,
event: Event,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &crate::Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) -> event::Status {
) {
if let Variant::Image {
on_remove: Some(on_remove),
..
@ -383,7 +384,8 @@ impl<'a, Message: 'a + Clone> Widget<Message, crate::Theme, crate::Renderer>
if let Some(position) = cursor.position() {
if removal_bounds(layout.bounds(), 4.0).contains(position) {
shell.publish(on_remove.clone());
return event::Status::Captured;
shell.capture_event();
return;
}
}
}
@ -391,10 +393,9 @@ impl<'a, Message: 'a + Clone> Widget<Message, crate::Theme, crate::Renderer>
_ => (),
}
}
if self.content.as_widget_mut().on_event(
self.content.as_widget_mut().update(
&mut tree.children[0],
event.clone(),
event,
layout
.children()
.next()
@ -405,9 +406,9 @@ impl<'a, Message: 'a + Clone> Widget<Message, crate::Theme, crate::Renderer>
clipboard,
shell,
viewport,
) == event::Status::Captured
{
return event::Status::Captured;
);
if shell.is_event_captured() {
return;
}
update(
@ -541,6 +542,7 @@ impl<'a, Message: 'a + Clone> Widget<Message, crate::Theme, crate::Renderer>
..Default::default()
},
shadow: Shadow::default(),
snap: true,
},
selection_background,
);
@ -554,7 +556,7 @@ impl<'a, Message: 'a + Clone> Widget<Message, crate::Theme, crate::Renderer>
y: bounds.y + (bounds.height - 18.0 - styling.border_width),
};
if bounds.intersects(viewport) {
iced_core::svg::Renderer::draw_svg(renderer, svg_handle, bounds);
iced_core::svg::Renderer::draw_svg(renderer, svg_handle, bounds, bounds);
}
}
@ -570,6 +572,7 @@ impl<'a, Message: 'a + Clone> Widget<Message, crate::Theme, crate::Renderer>
radius: c_rad.radius_m.into(),
..Default::default()
},
snap: true,
},
selection_background,
);
@ -583,6 +586,12 @@ impl<'a, Message: 'a + Clone> Widget<Message, crate::Theme, crate::Renderer>
x: bounds.x + 4.0,
y: bounds.y + 4.0,
},
Rectangle {
width: 16.0,
height: 16.0,
x: bounds.x + 4.0,
y: bounds.y + 4.0,
},
);
}
}
@ -609,8 +618,9 @@ impl<'a, Message: 'a + Clone> Widget<Message, crate::Theme, crate::Renderer>
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'_>,
layout: Layout<'b>,
renderer: &crate::Renderer,
viewport: &Rectangle,
mut translation: Vector,
) -> Option<overlay::Element<'b, Message, crate::Theme, crate::Renderer>> {
let position = layout.bounds().position();
@ -624,6 +634,7 @@ impl<'a, Message: 'a + Clone> Widget<Message, crate::Theme, crate::Renderer>
.unwrap()
.with_virtual_offset(layout.virtual_offset()),
renderer,
viewport,
translation,
)
}
@ -638,7 +649,7 @@ impl<'a, Message: 'a + Clone> Widget<Message, crate::Theme, crate::Renderer>
) -> iced_accessibility::A11yTree {
use iced_accessibility::{
A11yNode, A11yTree,
accesskit::{Action, DefaultActionVerb, NodeBuilder, NodeId, Rect, Role},
accesskit::{Action, Node, NodeId, Rect, Role},
};
// TODO why is state None sometimes?
if matches!(state.state, iced_core::widget::tree::State::None) {
@ -658,12 +669,12 @@ impl<'a, Message: 'a + Clone> Widget<Message, crate::Theme, crate::Renderer>
let bounds = Rect::new(x as f64, y as f64, (x + width) as f64, (y + height) as f64);
let is_hovered = state.state.downcast_ref::<State>().is_hovered;
let mut node = NodeBuilder::new(Role::Button);
let mut node = Node::new(Role::Button);
node.add_action(Action::Focus);
node.add_action(Action::Default);
node.add_action(Action::Click);
node.set_bounds(bounds);
if let Some(name) = self.name.as_ref() {
node.set_name(name.clone());
node.set_label(name.clone());
}
match self.description.as_ref() {
Some(iced_accessibility::Description::Id(id)) => {
@ -682,10 +693,10 @@ impl<'a, Message: 'a + Clone> Widget<Message, crate::Theme, crate::Renderer>
if self.on_press.is_none() {
node.set_disabled();
}
if is_hovered {
node.set_hovered();
}
node.set_default_action_verb(DefaultActionVerb::Click);
// TODO hover
// if is_hovered {
// node.set_hovered();
// }
if let Some(child_tree) = child_tree.map(|child_tree| {
self.content.as_widget().a11y_nodes(
@ -761,14 +772,14 @@ impl State {
#[allow(clippy::needless_pass_by_value, clippy::too_many_arguments)]
pub fn update<'a, Message: Clone>(
_id: Id,
event: Event,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
shell: &mut Shell<'_, Message>,
on_press: Option<&dyn Fn(Vector, Rectangle) -> Message>,
on_press_down: Option<&dyn Fn(Vector, Rectangle) -> Message>,
state: impl FnOnce() -> &'a mut State,
) -> event::Status {
) {
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
@ -787,7 +798,8 @@ pub fn update<'a, Message: Clone>(
shell.publish(msg);
}
return event::Status::Captured;
shell.capture_event();
return;
}
}
}
@ -806,7 +818,8 @@ pub fn update<'a, Message: Clone>(
shell.publish(msg);
}
return event::Status::Captured;
shell.capture_event();
return;
}
} else if on_press_down.is_some() {
let state = state();
@ -816,7 +829,7 @@ pub fn update<'a, Message: Clone>(
#[cfg(feature = "a11y")]
Event::A11y(event_id, iced_accessibility::accesskit::ActionRequest { action, .. }) => {
let state = state();
if let Some(on_press) = matches!(action, iced_accessibility::accesskit::Action::Default)
if let Some(on_press) = matches!(action, iced_accessibility::accesskit::Action::Click)
.then_some(on_press)
.flatten()
{
@ -825,17 +838,19 @@ pub fn update<'a, Message: Clone>(
shell.publish(msg);
}
return event::Status::Captured;
shell.capture_event();
return;
}
Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => {
if let Some(on_press) = on_press {
let state = state();
if state.is_focused && key == keyboard::Key::Named(keyboard::key::Named::Enter) {
if state.is_focused && *key == keyboard::Key::Named(keyboard::key::Named::Enter) {
state.is_pressed = true;
let msg = (on_press)(layout.virtual_offset(), layout.bounds());
shell.publish(msg);
return event::Status::Captured;
shell.capture_event();
return;
}
}
}
@ -846,8 +861,6 @@ pub fn update<'a, Message: Clone>(
}
_ => {}
}
event::Status::Ignored
}
#[allow(clippy::too_many_arguments)]
@ -879,6 +892,7 @@ pub fn draw<Renderer: iced_core::Renderer, Theme>(
radius: styling.border_radius,
},
shadow: Shadow::default(),
snap: true,
},
Color::TRANSPARENT,
);
@ -900,6 +914,7 @@ pub fn draw<Renderer: iced_core::Renderer, Theme>(
..Default::default()
},
shadow: Shadow::default(),
snap: true,
},
Background::Color([0.0, 0.0, 0.0, 0.5].into()),
);
@ -915,6 +930,7 @@ pub fn draw<Renderer: iced_core::Renderer, Theme>(
..Default::default()
},
shadow: Shadow::default(),
snap: true,
},
background,
);
@ -930,6 +946,7 @@ pub fn draw<Renderer: iced_core::Renderer, Theme>(
..Default::default()
},
shadow: Shadow::default(),
snap: true,
},
overlay,
);
@ -953,6 +970,7 @@ pub fn draw<Renderer: iced_core::Renderer, Theme>(
radius: styling.border_radius,
},
shadow: Shadow::default(),
snap: true,
},
Color::TRANSPARENT,
);

View file

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

586
src/widget/cards.rs Normal file
View file

@ -0,0 +1,586 @@
//! An expandable stack of cards
use std::time::Duration;
use crate::{
anim,
widget::{
button,
card::style::Style,
column,
icon::{self, Handle},
row, text,
},
};
use float_cmp::approx_eq;
use iced::widget;
use iced_core::{
Border, Element, Event, Length, Shadow, Size, Vector, Widget, border::Radius, id::Id,
layout::Node, renderer::Quad, widget::Tree,
};
use iced_core::{widget::tree, window};
const ICON_SIZE: u16 = 16;
const TOP_SPACING: u16 = 4;
const VERTICAL_SPACING: f32 = 8.0;
const PADDING: u16 = 16;
const BG_CARD_VISIBLE_HEIGHT: f32 = 4.0;
const BG_CARD_BORDER_RADIUS: f32 = 8.0;
const BG_CARD_MARGIN_STEP: f32 = 8.0;
/// get an expandable stack of cards
#[allow(clippy::too_many_arguments)]
pub fn cards<'a, Message, F, G>(
id: widget::Id,
card_inner_elements: Vec<Element<'a, Message, crate::Theme, crate::Renderer>>,
on_clear_all: Message,
on_show_more: Option<F>,
on_activate: Option<G>,
show_more_label: &'a str,
show_less_label: &'a str,
clear_all_label: &'a str,
show_less_icon: Option<Handle>,
expanded: bool,
) -> Cards<'a, Message, crate::Renderer>
where
Message: 'static + Clone,
F: 'a + Fn(bool) -> Message,
G: 'a + Fn(usize) -> Message,
{
Cards::new(
id,
card_inner_elements,
on_clear_all,
on_show_more,
on_activate,
show_more_label,
show_less_label,
clear_all_label,
show_less_icon,
expanded,
)
}
impl<'a, Message, Renderer> Cards<'a, Message, Renderer>
where
Renderer: iced_core::text::Renderer,
{
fn fully_expanded(&self, t: f32) -> bool {
self.expanded && self.elements.len() > 1 && self.can_show_more && approx_eq!(f32, t, 1.0)
}
fn fully_unexpanded(&self, t: f32) -> bool {
self.elements.len() == 1
|| (!self.expanded && (!self.can_show_more || approx_eq!(f32, t, 0.0)))
}
}
/// An expandable stack of cards.
#[allow(missing_debug_implementations)]
pub struct Cards<'a, Message, Renderer = crate::Renderer>
where
Renderer: iced_core::text::Renderer,
{
id: Id,
show_less_button: Element<'a, Message, crate::Theme, Renderer>,
clear_all_button: Element<'a, Message, crate::Theme, Renderer>,
elements: Vec<Element<'a, Message, crate::Theme, Renderer>>,
expanded: bool,
can_show_more: bool,
width: Length,
anim_multiplier: f32,
duration: Duration,
}
impl<'a, Message> Cards<'a, Message, crate::Renderer>
where
Message: Clone + 'static,
{
/// Get an expandable stack of cards
#[allow(clippy::too_many_arguments)]
pub fn new<F, G>(
id: widget::Id,
card_inner_elements: Vec<Element<'a, Message, crate::Theme, crate::Renderer>>,
on_clear_all: Message,
on_show_more: Option<F>,
on_activate: Option<G>,
show_more_label: &'a str,
show_less_label: &'a str,
clear_all_label: &'a str,
show_less_icon: Option<Handle>,
expanded: bool,
) -> Self
where
F: 'a + Fn(bool) -> Message,
G: 'a + Fn(usize) -> Message,
{
let can_show_more = card_inner_elements.len() > 1 && on_show_more.is_some();
Self {
can_show_more,
id: Id::unique(),
show_less_button: {
let mut show_less_children = Vec::with_capacity(3);
if let Some(source) = show_less_icon {
show_less_children.push(icon::icon(source).size(ICON_SIZE).into());
}
show_less_children.push(text::body(show_less_label).width(Length::Shrink).into());
show_less_children.push(
icon::from_name("pan-up-symbolic")
.size(ICON_SIZE)
.icon()
.into(),
);
let button_content = row::with_children(show_less_children)
.align_y(iced_core::Alignment::Center)
.spacing(TOP_SPACING)
.width(Length::Shrink);
Element::from(
button::custom(button_content)
.class(crate::theme::Button::Text)
.width(Length::Shrink)
.on_press_maybe(on_show_more.as_ref().map(|f| f(false)))
.padding([PADDING / 2, PADDING]),
)
},
clear_all_button: Element::from(
button::custom(text(clear_all_label))
.class(crate::theme::Button::Text)
.width(Length::Shrink)
.on_press(on_clear_all)
.padding([PADDING / 2, PADDING]),
),
elements: card_inner_elements
.into_iter()
.enumerate()
.map(|(i, w)| {
let custom_content = if i == 0 && !expanded && can_show_more {
column::with_capacity(2)
.push(w)
.push(text::caption(show_more_label))
.spacing(VERTICAL_SPACING)
.align_x(iced_core::Alignment::Center)
.into()
} else {
w
};
let b = crate::iced::widget::button(custom_content)
.class(crate::theme::iced::Button::Card)
.padding(PADDING);
if i == 0 && !expanded && can_show_more {
b.on_press_maybe(on_show_more.as_ref().map(|f| f(true)))
} else {
b.on_press_maybe(on_activate.as_ref().map(|f| f(i)))
}
.into()
})
// we will set the width of the container to shrink, then when laying out the top bar
// we will set the fill limit to the max of the shrink top bar width and the max shrink width of the
// cards
.collect(),
width: Length::Shrink,
anim_multiplier: 1.0,
expanded,
duration: Duration::from_millis(200),
}
}
/// Set the width of the cards stack
#[must_use]
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}
#[must_use]
/// The default animation time is 100ms, to speed up the toggle
/// animation use a value less than 1.0, and to slow down the
/// animation use a value greater than 1.0.
pub fn anim_multiplier(mut self, multiplier: f32) -> Self {
self.anim_multiplier = multiplier;
self
}
pub fn duration(mut self, dur: Duration) -> Self {
self.duration = dur;
self
}
pub fn id(mut self, id: Id) -> Self {
self.id = id;
self
}
}
impl<'a, Message, Renderer> Widget<Message, crate::Theme, Renderer> for Cards<'a, Message, Renderer>
where
Message: 'a + Clone,
Renderer: 'a + iced_core::Renderer + iced_core::text::Renderer,
{
fn children(&self) -> Vec<Tree> {
[&self.show_less_button, &self.clear_all_button]
.iter()
.map(|w| Tree::new(w.as_widget()))
.chain(self.elements.iter().map(|w| Tree::new(w.as_widget())))
.collect()
}
fn diff(&mut self, tree: &mut Tree) {
let mut children: Vec<_> = vec![
self.show_less_button.as_widget_mut(),
self.clear_all_button.as_widget_mut(),
]
.into_iter()
.chain(
self.elements
.iter_mut()
.map(iced_core::Element::as_widget_mut),
)
.collect();
tree.diff_children(children.as_mut_slice());
}
#[allow(clippy::too_many_lines)]
fn layout(
&mut self,
tree: &mut Tree,
renderer: &Renderer,
limits: &iced_core::layout::Limits,
) -> iced_core::layout::Node {
let my_state = tree.state.downcast_ref::<State>();
let mut children = Vec::with_capacity(1 + self.elements.len());
let mut size = Size::new(0.0, 0.0);
let tree_children = &mut tree.children;
let count = self.elements.len();
if self.elements.is_empty() {
return Node::with_children(Size::new(1., 1.), children);
}
let s = anim::smootherstep(my_state.anim.t(self.duration, self.expanded));
let fully_expanded: bool = self.fully_expanded(s);
let fully_unexpanded: bool = self.fully_unexpanded(s);
let show_less = &mut self.show_less_button;
let clear_all = &mut self.clear_all_button;
let show_less_node = if self.can_show_more {
show_less
.as_widget_mut()
.layout(&mut tree_children[0], renderer, limits)
} else {
Node::new(Size::default())
};
let clear_all_node =
clear_all
.as_widget_mut()
.layout(&mut tree_children[1], renderer, limits);
size.width += show_less_node.size().width + clear_all_node.size().width;
let custom_limits = limits.min_width(size.width);
for (c, t) in self.elements.iter_mut().zip(tree_children[2..].iter_mut()) {
let card_node = c.as_widget_mut().layout(t, renderer, &custom_limits);
size.width = size.width.max(card_node.size().width);
}
if fully_expanded {
let show_less = &mut self.show_less_button;
let clear_all = &mut self.clear_all_button;
let show_less_node = if self.can_show_more {
show_less
.as_widget_mut()
.layout(&mut tree_children[0], renderer, limits)
} else {
Node::new(Size::default())
};
let clear_all_node = if self.can_show_more {
let mut n =
clear_all
.as_widget_mut()
.layout(&mut tree_children[1], renderer, limits);
let clear_all_node_size = n.size();
n = clear_all_node
.translate(Vector::new(size.width - clear_all_node_size.width, 0.0));
size.height += show_less_node.size().height.max(n.size().height) + VERTICAL_SPACING;
n
} else {
Node::new(Size::default())
};
children.push(show_less_node);
children.push(clear_all_node);
}
let custom_limits = limits
.min_width(size.width)
.max_width(size.width)
.width(Length::Fixed(size.width));
for (i, (c, t)) in self
.elements
.iter_mut()
.zip(tree_children[2..].iter_mut())
.enumerate()
{
let progress = s * size.height;
let card_node = c
.as_widget_mut()
.layout(t, renderer, &custom_limits)
.translate(Vector::new(0.0, progress));
size.height = size.height.max(progress + card_node.size().height);
children.push(card_node);
if fully_unexpanded {
let width = children.last().unwrap().bounds().width;
// push the background card nodes
for i in 1..self.elements.len().min(3) {
// height must be 16px for 8px padding
// but we only want 4px visible
let margin = f32::from(u8::try_from(i).unwrap()) * BG_CARD_MARGIN_STEP;
let node =
Node::new(Size::new(width - 2.0 * margin, BG_CARD_BORDER_RADIUS * 2.0))
.translate(Vector::new(
margin,
size.height - BG_CARD_BORDER_RADIUS * 2.0 + BG_CARD_VISIBLE_HEIGHT,
));
size.height += BG_CARD_VISIBLE_HEIGHT;
children.push(node);
}
break;
}
if i + 1 < count {
size.height += VERTICAL_SPACING;
}
}
Node::with_children(size, children)
}
fn draw(
&self,
state: &iced_core::widget::Tree,
renderer: &mut Renderer,
theme: &crate::Theme,
style: &iced_core::renderer::Style,
layout: iced_core::Layout<'_>,
cursor: iced_core::mouse::Cursor,
viewport: &iced_core::Rectangle,
) {
let my_state = state.state.downcast_ref::<State>();
// there are 4 cases for drawing
// 1. empty entries list
// Nothing to draw
// 2. un-expanded
// go through the layout, draw the card, the inner card, and the bg cards
// 3. expanding / unexpanding
// go through the layout. draw each card and its inner card
// 4. expanded =>
// go through the layout. draw the top bar, and do all of 3
// cards may be hovered
// any buttons may have a hover state as well
if self.elements.is_empty() {
return;
}
let t = my_state.anim.t(self.duration, self.expanded);
let fully_unexpanded = self.fully_unexpanded(t);
let fully_expanded = self.fully_expanded(t);
let mut layout = layout.children();
let mut tree_children = state.children.iter();
if fully_expanded {
let show_less = &self.show_less_button;
let clear_all = &self.clear_all_button;
let show_less_layout = layout.next().unwrap();
let clear_all_layout = layout.next().unwrap();
show_less.as_widget().draw(
tree_children.next().unwrap(),
renderer,
theme,
style,
show_less_layout,
cursor,
viewport,
);
clear_all.as_widget().draw(
tree_children.next().unwrap(),
renderer,
theme,
style,
clear_all_layout,
cursor,
viewport,
);
} else {
_ = tree_children.next();
_ = tree_children.next();
}
// Draw first to appear behind
if fully_unexpanded {
let card_layout = layout.next().unwrap();
let appearance = Style::default();
let bg_layout = layout.collect::<Vec<_>>();
for (i, layout) in (0..2).zip(bg_layout.into_iter()).rev() {
renderer.fill_quad(
Quad {
bounds: layout.bounds(),
border: Border {
radius: Radius::from([
0.0,
0.0,
BG_CARD_BORDER_RADIUS,
BG_CARD_BORDER_RADIUS,
]),
..Default::default()
},
shadow: Shadow::default(),
snap: true,
},
if i == 0 {
appearance.card_1
} else {
appearance.card_2
},
);
}
self.elements[0].as_widget().draw(
tree_children.next().unwrap(),
renderer,
theme,
style,
card_layout,
cursor,
viewport,
);
} else {
let layout = layout.collect::<Vec<_>>();
// draw in reverse order so later cards appear behind earlier cards
for ((inner, layout), c_state) in self
.elements
.iter()
.rev()
.zip(layout.into_iter().rev())
.zip(tree_children.rev())
{
inner
.as_widget()
.draw(c_state, renderer, theme, style, layout, cursor, viewport);
}
}
}
fn update(
&mut self,
state: &mut Tree,
event: &iced_core::Event,
layout: iced_core::Layout<'_>,
cursor: iced_core::mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn iced_core::Clipboard,
shell: &mut iced_core::Shell<'_, Message>,
viewport: &iced_core::Rectangle,
) {
if self.elements.is_empty() {
return;
}
if let Event::Window(window::Event::RedrawRequested(_)) = event {
let state = state.state.downcast_mut::<State>();
state.anim.anim_done(self.duration);
if state.anim.last_change.is_some() {
shell.request_redraw();
shell.invalidate_layout();
}
}
let my_state = state.state.downcast_ref::<State>();
let mut layout = layout.children();
let mut tree_children = state.children.iter_mut();
let t = my_state.anim.t(self.duration, self.expanded);
let fully_expanded = self.fully_expanded(t);
let fully_unexpanded = self.fully_unexpanded(t);
let show_less_state = tree_children.next();
let clear_all_state = tree_children.next();
if fully_expanded {
let c_layout = layout.next().unwrap();
let state = show_less_state.unwrap();
self.show_less_button.as_widget_mut().update(
state, event, c_layout, cursor, renderer, clipboard, shell, viewport,
);
if shell.is_event_captured() {
return;
}
let c_layout = layout.next().unwrap();
let state = clear_all_state.unwrap();
self.clear_all_button.as_widget_mut().update(
state, &event, c_layout, cursor, renderer, clipboard, shell, viewport,
);
}
if shell.is_event_captured() {
return;
}
for ((inner, layout), c_state) in self.elements.iter_mut().zip(layout).zip(tree_children) {
inner.as_widget_mut().update(
c_state, &event, layout, cursor, renderer, clipboard, shell, viewport,
);
if shell.is_event_captured() || fully_unexpanded {
break;
}
}
}
fn size(&self) -> Size<Length> {
Size::new(self.width, Length::Shrink)
}
fn tag(&self) -> tree::Tag {
tree::Tag::of::<State>()
}
fn state(&self) -> tree::State {
tree::State::new(State::default())
}
fn id(&self) -> Option<Id> {
Some(self.id.clone())
}
fn set_id(&mut self, id: Id) {
self.id = id;
}
}
impl<'a, Message> From<Cards<'a, Message>> for Element<'a, Message, crate::Theme, crate::Renderer>
where
Message: Clone + 'a,
{
fn from(cards: Cards<'a, Message>) -> Self {
Self::new(cards)
}
}
#[derive(Debug, Default)]
pub struct State {
anim: anim::State,
}

View file

@ -4,7 +4,6 @@
//! Widgets for selecting colors with a color picker.
use std::borrow::Cow;
use std::iter;
use std::rc::Rc;
use std::sync::LazyLock;
use std::sync::atomic::{AtomicBool, Ordering};
@ -26,7 +25,10 @@ use iced_core::{
};
use iced_widget::slider::HandleShape;
use iced_widget::{Row, canvas, column, horizontal_space, row, scrollable, vertical_space};
use iced_widget::{
Row, canvas, column, row, scrollable,
space::{horizontal, vertical},
};
use palette::{FromColor, RgbHue};
use super::divider::horizontal;
@ -90,8 +92,6 @@ pub struct ColorPickerModel {
#[setters(skip)]
active_color: palette::Hsv,
#[setters(skip)]
save_next: Option<Color>,
#[setters(skip)]
input_color: String,
#[setters(skip)]
applied_color: Option<Color>,
@ -125,7 +125,6 @@ impl ColorPickerModel {
.insert(move |b| b.text(rgb.clone()))
.build(),
active_color: hsv,
save_next: None,
input_color: color_to_string(hsv, true),
applied_color: initial,
fallback_color,
@ -156,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> {
match update {
ColorPickerUpdate::ActiveColor(c) => {
self.must_clear_cache.store(true, Ordering::SeqCst);
self.input_color = color_to_string(c, self.is_hex());
if let Some(to_save) = self.save_next.take() {
self.recent_colors.insert(0, to_save);
self.recent_colors.truncate(MAX_RECENT);
}
self.active_color = c;
self.copied_at = None;
}
ColorPickerUpdate::AppliedColor => {
ColorPickerUpdate::AppliedColor | 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.update_recent_colors(applied_color);
}
self.applied_color = Some(Color::from(srgb));
self.active = false;
@ -212,21 +215,12 @@ impl ColorPickerModel {
palette::Hsv::from_color(palette::Srgb::new(c.red, c.green, c.blue));
}
}
ColorPickerUpdate::ActionFinished => {
let srgb = palette::Srgb::from_color(self.active_color);
if let Some(applied_color) = self.applied_color.take() {
self.recent_colors.push(applied_color);
}
self.applied_color = Some(Color::from(srgb));
self.active = false;
self.save_next = Some(Color::from(srgb));
}
ColorPickerUpdate::ToggleColorPicker => {
self.must_clear_cache.store(true, Ordering::SeqCst);
self.active = !self.active;
self.copied_at = None;
}
};
}
Task::none()
}
@ -334,7 +328,7 @@ where
.width(self.width),
// canvas with gradient for the current color
// still needs the canvas and the handle to be drawn on it
container(vertical_space().height(self.height))
container(vertical().height(self.height))
.width(self.width)
.height(self.height),
slider(
@ -392,7 +386,8 @@ where
text_input("", self.input_color)
.on_input(move |s| on_update(ColorPickerUpdate::Input(s)))
.on_paste(move |s| on_update(ColorPickerUpdate::Input(s)))
.on_submit(move |_| on_update(ColorPickerUpdate::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(
color_button(
None,
@ -548,13 +543,13 @@ where
}
fn layout(
&self,
&mut self,
tree: &mut Tree,
renderer: &crate::Renderer,
limits: &layout::Limits,
) -> layout::Node {
self.inner
.as_widget()
.as_widget_mut()
.layout(&mut tree.children[0], renderer, limits)
}
@ -657,6 +652,7 @@ where
radius: (1.0 + handle_radius).into(),
},
shadow: Shadow::default(),
snap: true,
},
Color::TRANSPARENT,
);
@ -674,6 +670,7 @@ where
radius: handle_radius.into(),
},
shadow: Shadow::default(),
snap: true,
},
Color::TRANSPARENT,
);
@ -684,26 +681,31 @@ where
fn overlay<'b>(
&'b mut self,
state: &'b mut Tree,
layout: Layout<'_>,
layout: Layout<'b>,
renderer: &crate::Renderer,
viewport: &Rectangle,
translation: Vector,
) -> Option<iced_core::overlay::Element<'b, Message, crate::Theme, crate::Renderer>> {
self.inner
.as_widget_mut()
.overlay(&mut state.children[0], layout, renderer, translation)
self.inner.as_widget_mut().overlay(
&mut state.children[0],
layout,
renderer,
viewport,
translation,
)
}
fn on_event(
fn update(
&mut self,
tree: &mut Tree,
event: Event,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &crate::Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) -> event::Status {
) {
// if the pointer is performing a drag, intercept pointer motion and button events
// else check if event is handled by child elements
// if the event is not handled by a child element, check if it is over the canvas when pressing a button
@ -732,24 +734,26 @@ where
shell.publish((self.on_update)(ColorPickerUpdate::ActionFinished));
state.dragging = false;
}
_ => return event::Status::Ignored,
_ => return,
};
return event::Status::Captured;
shell.capture_event();
return;
}
let column_tree = &mut tree.children[0];
if self.inner.as_widget_mut().on_event(
self.inner.as_widget_mut().update(
column_tree,
event.clone(),
&event,
column_layout,
cursor,
renderer,
clipboard,
shell,
viewport,
) == event::Status::Captured
{
return event::Status::Captured;
);
if shell.is_event_captured() {
shell.capture_event();
return;
}
match event {
@ -764,12 +768,10 @@ where
state.dragging = true;
let hsv: palette::Hsv = palette::Hsv::new(self.active_color.hue, s, v);
shell.publish((self.on_update)(ColorPickerUpdate::ActiveColor(hsv)));
event::Status::Captured
} else {
event::Status::Ignored
shell.capture_event();
}
}
_ => event::Status::Ignored,
_ => {}
}
}
@ -812,12 +814,12 @@ pub fn color_button<'a, Message: Clone + 'static>(
let spacing = THEME.lock().unwrap().cosmic().spacing;
button::custom(if color.is_some() {
Element::from(vertical_space().height(Length::Fixed(f32::from(spacing.space_s))))
Element::from(vertical().height(Length::Fixed(f32::from(spacing.space_s))))
} else {
Element::from(column![
vertical_space().height(Length::FillPortion(6)),
vertical().height(Length::FillPortion(6)),
row![
horizontal_space().width(Length::FillPortion(6)),
horizontal().width(Length::FillPortion(6)),
Icon::from(
icon::from_name("list-add-symbolic")
.prefer_svg(true)
@ -827,11 +829,11 @@ pub fn color_button<'a, Message: Clone + 'static>(
.width(icon_portion)
.height(Length::Fill)
.content_fit(iced_core::ContentFit::Contain),
horizontal_space().width(Length::FillPortion(6)),
horizontal().width(Length::FillPortion(6)),
]
.height(icon_portion)
.width(Length::Fill),
vertical_space().height(Length::FillPortion(6)),
vertical().height(Length::FillPortion(6)),
])
})
.width(Length::Fixed(f32::from(spacing.space_s)))

View file

@ -7,8 +7,8 @@ use iced::advanced::layout::{self, Layout};
use iced::advanced::widget::{self, Operation};
use iced::advanced::{Clipboard, Shell};
use iced::advanced::{overlay, renderer};
use iced::{Event, Point, Rectangle, Size, event, mouse};
use iced_core::Renderer;
use iced::{Event, Point, Size, mouse};
use iced_core::{Renderer, touch};
pub(super) struct Overlay<'a, 'b, Message> {
pub(crate) position: Point,
@ -29,7 +29,7 @@ where
let node = self
.content
.as_widget()
.as_widget_mut()
.layout(self.tree, renderer, &limits);
let node_size = node.size();
@ -47,16 +47,16 @@ where
})
}
fn on_event(
fn update(
&mut self,
event: Event,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &crate::Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) -> event::Status {
self.content.as_widget_mut().on_event(
) {
self.content.as_widget_mut().update(
self.tree,
event,
layout,
@ -65,7 +65,20 @@ where
clipboard,
shell,
&layout.bounds(),
)
);
match event {
Event::Mouse(e) if !matches!(e, mouse::Event::CursorLeft) => {
if cursor.is_over(layout.bounds()) {
shell.capture_event();
}
}
Event::Touch(e) if !matches!(e, touch::Event::FingerLost { .. }) => {
if cursor.is_over(layout.bounds()) {
shell.capture_event();
}
}
_ => {}
}
}
fn draw(
@ -86,7 +99,7 @@ where
cursor,
&layout.bounds(),
);
})
});
}
fn operate(
@ -104,21 +117,35 @@ where
&self,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &crate::Renderer,
) -> mouse::Interaction {
self.content
// TODO how to handle viewport here?
let viewport = &layout.bounds();
let interaction = self
.content
.as_widget()
.mouse_interaction(self.tree, layout, cursor, viewport, renderer)
.mouse_interaction(self.tree, layout, cursor, viewport, renderer);
if let mouse::Interaction::None = interaction
&& cursor.is_over(layout.bounds())
{
return mouse::Interaction::Idle;
}
interaction
}
fn overlay<'c>(
&'c mut self,
layout: Layout<'_>,
layout: Layout<'c>,
renderer: &crate::Renderer,
) -> Option<overlay::Element<'c, Message, crate::Theme, crate::Renderer>> {
self.content
.as_widget_mut()
.overlay(self.tree, layout, renderer, iced::Vector::default())
let viewport = &layout.bounds();
self.content.as_widget_mut().overlay(
self.tree,
layout,
renderer,
viewport,
iced::Vector::default(),
)
}
}

View file

@ -7,7 +7,7 @@ use crate::{Apply, Element, Renderer, Theme, fl};
use std::borrow::Cow;
use iced_core::Alignment;
use iced_core::event::{self, Event};
use iced_core::event::Event;
use iced_core::widget::{Operation, Tree};
use iced_core::{
Clipboard, Layout, Length, Rectangle, Shell, Vector, Widget, layout, mouse,
@ -65,7 +65,7 @@ impl<'a, Message: Clone + 'static> ContextDrawer<'a, Message> {
} else {
let title = title
.map(|title| text::title4(title).width(Length::Fill).apply(Element::from))
.unwrap_or_else(|| widget::horizontal_space().apply(Element::from));
.unwrap_or_else(|| widget::space::horizontal().apply(Element::from));
(title, None)
};
@ -196,40 +196,40 @@ impl<Message: Clone> Widget<Message, crate::Theme, Renderer> for ContextDrawer<'
}
fn layout(
&self,
&mut self,
tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
self.content
.as_widget()
.as_widget_mut()
.layout(&mut tree.children[0], renderer, limits)
}
fn operate(
&self,
&mut self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn Operation<()>,
) {
self.content
.as_widget()
.as_widget_mut()
.operate(&mut tree.children[0], layout, renderer, operation);
}
fn on_event(
fn update(
&mut self,
tree: &mut Tree,
event: Event,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) -> event::Status {
self.content.as_widget_mut().on_event(
) {
self.content.as_widget_mut().update(
&mut tree.children[0],
event,
layout,
@ -238,7 +238,7 @@ impl<Message: Clone> Widget<Message, crate::Theme, Renderer> for ContextDrawer<'
clipboard,
shell,
viewport,
)
);
}
fn mouse_interaction(
@ -282,8 +282,9 @@ impl<Message: Clone> Widget<Message, crate::Theme, Renderer> for ContextDrawer<'
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'_>,
layout: Layout<'b>,
_renderer: &Renderer,
_viewport: &Rectangle,
translation: Vector,
) -> Option<iced_overlay::Element<'b, Message, crate::Theme, Renderer>> {
let bounds = layout.bounds();

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.
#[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::widget::menu::{
self, CloseCondition, Direction, ItemHeight, ItemWidth, MenuBarState, PathHighlight,
@ -13,7 +18,7 @@ use derive_setters::Setters;
use iced::touch::Finger;
use iced::{Event, Vector, keyboard, window};
use iced_core::widget::{Tree, Widget, tree};
use iced_core::{Length, Point, Size, event, mouse, touch};
use iced_core::{Length, Point, Size, mouse, touch};
use std::collections::HashSet;
use std::sync::Arc;
@ -27,7 +32,7 @@ pub fn context_menu<'a, Message: 'static + Clone>(
content: content.into(),
context_menu: context_menu.map(|menus| {
vec![menu::Tree::with_children(
crate::Element::from(crate::widget::row::<'static, Message>()),
crate::Element::from(crate::widget::Row::new()),
menus,
)]
}),
@ -59,7 +64,12 @@ pub struct ContextMenu<'a, 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)]
fn create_popup(
&mut self,
@ -85,6 +95,7 @@ impl<Message: Clone + 'static> ContextMenu<'_, Message> {
// close existing popups
state.menu_states.clear();
state.active_root.clear();
shell.publish(self.on_surface_action.as_ref().unwrap()(destroy_popup(id)));
state.view_cursor = view_cursor;
(
@ -249,7 +260,7 @@ impl<Message: 'static + Clone> Widget<Message, crate::Theme, crate::Renderer>
}
fn diff(&mut self, tree: &mut Tree) {
tree.children[0].diff(self.content.as_widget_mut());
tree.diff_children(std::slice::from_mut(&mut self.content));
let state = tree.state.downcast_mut::<LocalState>();
state.menu_bar_state.inner.with_data_mut(|inner| {
menu_roots_diff(self.context_menu.as_mut().unwrap(), &mut inner.tree);
@ -270,13 +281,13 @@ impl<Message: 'static + Clone> Widget<Message, crate::Theme, crate::Renderer>
}
fn layout(
&self,
&mut self,
tree: &mut Tree,
renderer: &crate::Renderer,
limits: &iced_core::layout::Limits,
) -> iced_core::layout::Node {
self.content
.as_widget()
.as_widget_mut()
.layout(&mut tree.children[0], renderer, limits)
}
@ -302,29 +313,29 @@ impl<Message: 'static + Clone> Widget<Message, crate::Theme, crate::Renderer>
}
fn operate(
&self,
&mut self,
tree: &mut Tree,
layout: iced_core::Layout<'_>,
renderer: &crate::Renderer,
operation: &mut dyn iced_core::widget::Operation<()>,
) {
self.content
.as_widget()
.as_widget_mut()
.operate(&mut tree.children[0], layout, renderer, operation);
}
#[allow(clippy::too_many_lines)]
fn on_event(
fn update(
&mut self,
tree: &mut Tree,
event: iced::Event,
event: &iced::Event,
layout: iced_core::Layout<'_>,
cursor: iced_core::mouse::Cursor,
renderer: &crate::Renderer,
clipboard: &mut dyn iced_core::Clipboard,
shell: &mut iced_core::Shell<'_, Message>,
viewport: &iced::Rectangle,
) -> iced_core::event::Status {
) {
let state = tree.state.downcast_mut::<LocalState>();
let bounds = layout.bounds();
@ -336,13 +347,12 @@ impl<Message: 'static + Clone> Widget<Message, crate::Theme, crate::Renderer>
.with_data(|d| !d.open && !d.active_root.is_empty());
let open = state.menu_bar_state.inner.with_data_mut(|state| {
if reset {
if let Some(popup_id) = state.popup_id.get(&self.window_id).copied() {
if let Some(handler) = self.on_surface_action.as_ref() {
shell.publish((handler)(crate::surface::Action::DestroyPopup(popup_id)));
state.reset();
}
}
if reset
&& let Some(popup_id) = state.popup_id.get(&self.window_id).copied()
&& let Some(handler) = self.on_surface_action.as_ref()
{
shell.publish((handler)(crate::surface::Action::DestroyPopup(popup_id)));
state.reset();
}
state.open
});
@ -356,7 +366,6 @@ impl<Message: 'static + Clone> Widget<Message, crate::Theme, crate::Renderer>
mouse::Button::Right | mouse::Button::Left,
))
| Event::Touch(touch::Event::FingerPressed { .. })
| Event::Window(window::Event::Focused)
if open )
{
state.menu_bar_state.inner.with_data_mut(|state| {
@ -365,16 +374,20 @@ impl<Message: 'static + Clone> Widget<Message, crate::Theme, crate::Renderer>
state.active_root.clear();
state.open = false;
#[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))]
if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) {
if let Some(id) = state.popup_id.remove(&self.window_id) {
{
let surface_action = self.on_surface_action.as_ref().unwrap();
shell
.publish(surface_action(crate::surface::action::destroy_popup(id)));
}
state.view_cursor = cursor;
#[cfg(all(
feature = "wayland",
target_os = "linux",
feature = "winit",
feature = "surface-message"
))]
if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland))
&& let Some(id) = state.popup_id.remove(&self.window_id)
{
{
let surface_action = self.on_surface_action.as_ref().unwrap();
shell.publish(surface_action(crate::surface::action::destroy_popup(id)));
}
state.view_cursor = cursor;
}
});
}
@ -384,11 +397,11 @@ impl<Message: 'static + Clone> Widget<Message, crate::Theme, crate::Renderer>
match event {
Event::Touch(touch::Event::FingerPressed { id, .. }) => {
state.fingers_pressed.insert(id);
state.fingers_pressed.insert(*id);
}
Event::Touch(touch::Event::FingerLifted { id, .. }) => {
state.fingers_pressed.remove(&id);
state.fingers_pressed.remove(id);
}
_ => (),
@ -397,7 +410,7 @@ impl<Message: 'static + Clone> Widget<Message, crate::Theme, crate::Renderer>
// Present a context menu on a right click event.
if !was_open
&& self.context_menu.is_some()
&& (right_button_released(&event) || (touch_lifted(&event) && fingers_pressed == 2))
&& (right_button_released(event) || (touch_lifted(event) && fingers_pressed == 2))
{
state.context_cursor = cursor.position().unwrap_or_default();
let state = tree.state.downcast_mut::<LocalState>();
@ -405,15 +418,21 @@ impl<Message: 'static + Clone> Widget<Message, crate::Theme, crate::Renderer>
state.open = true;
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)) {
self.create_popup(layout, cursor, renderer, shell, viewport, state);
}
return event::Status::Captured;
} else if !was_open && right_button_released(&event)
|| (touch_lifted(&event))
|| left_button_released(&event)
shell.capture_event();
return;
} else if !was_open && right_button_released(event)
|| (touch_lifted(event))
|| left_button_released(event)
{
state.menu_bar_state.inner.with_data_mut(|state| {
was_open = true;
@ -423,24 +442,24 @@ impl<Message: 'static + Clone> Widget<Message, crate::Theme, crate::Renderer>
#[cfg(all(
feature = "wayland",
target_os = "linux",
feature = "winit",
feature = "surface-message"
))]
if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) {
if let Some(id) = state.popup_id.remove(&self.window_id) {
{
let surface_action = self.on_surface_action.as_ref().unwrap();
shell.publish(surface_action(
crate::surface::action::destroy_popup(id),
));
}
state.view_cursor = cursor;
if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland))
&& let Some(id) = state.popup_id.remove(&self.window_id)
{
{
let surface_action = self.on_surface_action.as_ref().unwrap();
shell
.publish(surface_action(crate::surface::action::destroy_popup(id)));
}
state.view_cursor = cursor;
}
});
}
}
self.content.as_widget_mut().on_event(
self.content.as_widget_mut().update(
&mut tree.children[0],
event,
layout,
@ -449,7 +468,7 @@ impl<Message: 'static + Clone> Widget<Message, crate::Theme, crate::Renderer>
clipboard,
shell,
viewport,
)
);
}
fn overlay<'b>(
@ -457,9 +476,15 @@ impl<Message: 'static + Clone> Widget<Message, crate::Theme, crate::Renderer>
tree: &'b mut Tree,
layout: iced_core::Layout<'_>,
_renderer: &crate::Renderer,
_viewport: &iced::Rectangle,
translation: Vector,
) -> 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))
&& self.window_id != window::Id::NONE
&& self.on_surface_action.is_some()

View file

@ -123,7 +123,7 @@ impl<'a, Message: Clone + 'static> From<Dialog<'a, Message>> for Element<'a, Mes
if let Some(body) = dialog.body {
if should_space {
content_col = content_col
.push(widget::vertical_space().height(Length::Fixed(space_xxs.into())));
.push(widget::space::vertical().height(Length::Fixed(space_xxs.into())));
}
content_col = content_col.push(
widget::container(widget::scrollable(widget::text::body(body))).max_height(300.),
@ -133,7 +133,7 @@ impl<'a, Message: Clone + 'static> From<Dialog<'a, Message>> for Element<'a, Mes
for control in dialog.controls {
if should_space {
content_col = content_col
.push(widget::vertical_space().height(Length::Fixed(space_s.into())));
.push(widget::space::vertical().height(Length::Fixed(space_s.into())));
}
content_col = content_col.push(control);
should_space = true;
@ -149,7 +149,7 @@ impl<'a, Message: Clone + 'static> From<Dialog<'a, Message>> for Element<'a, Mes
if let Some(button) = dialog.tertiary_action {
button_row = button_row.push(button);
}
button_row = button_row.push(widget::horizontal_space());
button_row = button_row.push(widget::space::horizontal());
if let Some(button) = dialog.secondary_action {
button_row = button_row.push(button);
}

View file

@ -7,23 +7,24 @@ use iced::Vector;
use crate::{
Element,
iced::{
Event, Length, Rectangle,
clipboard::{
dnd::{self, DndAction, DndDestinationRectangle, DndEvent, OfferEvent},
mime::AllowedMimeTypes,
},
event,
id::Internal,
mouse, overlay,
},
iced_core::{
self, Clipboard, Shell, layout,
widget::{Tree, tree},
},
widget::{Id, Widget},
};
use iced::{
Event, Length, Rectangle,
clipboard::{
dnd::{self, DndAction, DndDestinationRectangle, DndEvent, OfferEvent},
mime::AllowedMimeTypes,
},
event,
id::Internal,
mouse, overlay,
};
use iced_core::{
self, Clipboard, Shell, layout,
widget::{Tree, tree},
};
pub fn dnd_destination<'a, Message: 'static>(
child: impl Into<Element<'a, Message>>,
mimes: Vec<Cow<'static, str>>,
@ -291,7 +292,7 @@ impl<Message: 'static> Widget<Message, crate::Theme, crate::Renderer>
}
fn diff(&mut self, tree: &mut Tree) {
tree.children[0].diff(self.container.as_widget_mut());
tree.diff_children(std::slice::from_mut(&mut self.container));
}
fn state(&self) -> iced_core::widget::tree::State {
@ -303,43 +304,43 @@ impl<Message: 'static> Widget<Message, crate::Theme, crate::Renderer>
}
fn layout(
&self,
&mut self,
tree: &mut Tree,
renderer: &crate::Renderer,
limits: &layout::Limits,
) -> layout::Node {
self.container
.as_widget()
.as_widget_mut()
.layout(&mut tree.children[0], renderer, limits)
}
fn operate(
&self,
&mut self,
tree: &mut Tree,
layout: layout::Layout<'_>,
renderer: &crate::Renderer,
operation: &mut dyn iced_core::widget::Operation<()>,
) {
self.container
.as_widget()
.as_widget_mut()
.operate(&mut tree.children[0], layout, renderer, operation);
}
#[allow(clippy::too_many_lines)]
fn on_event(
fn update(
&mut self,
tree: &mut Tree,
event: Event,
event: &Event,
layout: layout::Layout<'_>,
cursor: mouse::Cursor,
renderer: &crate::Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) -> event::Status {
let s = self.container.as_widget_mut().on_event(
) {
self.container.as_widget_mut().update(
&mut tree.children[0],
event.clone(),
event,
layout,
cursor,
renderer,
@ -347,8 +348,8 @@ impl<Message: 'static> Widget<Message, crate::Theme, crate::Renderer>
shell,
viewport,
);
if matches!(s, event::Status::Captured) {
return event::Status::Captured;
if shell.is_event_captured() {
return;
}
let state = tree.state.downcast_mut::<State<()>>();
@ -367,23 +368,23 @@ impl<Message: 'static> Widget<Message, crate::Theme, crate::Renderer>
OfferEvent::Enter {
x, y, mime_types, ..
},
)) if id == Some(my_id) => {
)) if *id == Some(my_id) => {
if !self.mime_matches(&mime_types) {
log::trace!(
target: DND_DEST_LOG_TARGET,
"offer enter id={my_id:?} ignored (mimes={mime_types:?} not in {:?})",
self.mime_types
);
return event::Status::Ignored;
return;
}
log::trace!(
target: DND_DEST_LOG_TARGET,
"offer enter id={my_id:?} coords=({x},{y}) mimes={mime_types:?}"
);
if let Some(msg) = state.on_enter(
x,
y,
mime_types,
*x,
*y,
mime_types.clone(),
self.on_enter.as_ref().map(std::convert::AsRef::as_ref),
(),
) {
@ -391,13 +392,13 @@ impl<Message: 'static> Widget<Message, crate::Theme, crate::Renderer>
}
if self.forward_drag_as_cursor {
#[allow(clippy::cast_possible_truncation)]
let drag_cursor = mouse::Cursor::Available((x as f32, y as f32).into());
let drag_cursor = mouse::Cursor::Available((*x as f32, *y as f32).into());
let event = Event::Mouse(mouse::Event::CursorMoved {
position: drag_cursor.position().unwrap(),
});
self.container.as_widget_mut().on_event(
self.container.as_widget_mut().update(
&mut tree.children[0],
event,
&event,
layout,
drag_cursor,
renderer,
@ -406,7 +407,8 @@ impl<Message: 'static> Widget<Message, crate::Theme, crate::Renderer>
viewport,
);
}
return event::Status::Captured;
shell.capture_event();
return;
}
Event::Dnd(DndEvent::Offer(_, OfferEvent::Leave)) => {
log::trace!(
@ -423,9 +425,9 @@ impl<Message: 'static> Widget<Message, crate::Theme, crate::Renderer>
if self.forward_drag_as_cursor {
let drag_cursor = mouse::Cursor::Unavailable;
let event = Event::Mouse(mouse::Event::CursorLeft);
self.container.as_widget_mut().on_event(
self.container.as_widget_mut().update(
&mut tree.children[0],
event,
&event,
layout,
drag_cursor,
renderer,
@ -434,16 +436,16 @@ impl<Message: 'static> Widget<Message, crate::Theme, crate::Renderer>
viewport,
);
}
return event::Status::Ignored;
return;
}
Event::Dnd(DndEvent::Offer(id, OfferEvent::Motion { x, y })) if id == Some(my_id) => {
Event::Dnd(DndEvent::Offer(id, OfferEvent::Motion { x, y })) if *id == Some(my_id) => {
log::trace!(
target: DND_DEST_LOG_TARGET,
"offer motion id={my_id:?} coords=({x},{y})"
);
if let Some(msg) = state.on_motion(
x,
y,
*x,
*y,
self.on_motion.as_ref().map(std::convert::AsRef::as_ref),
self.on_enter.as_ref().map(std::convert::AsRef::as_ref),
(),
@ -453,13 +455,13 @@ impl<Message: 'static> Widget<Message, crate::Theme, crate::Renderer>
if self.forward_drag_as_cursor {
#[allow(clippy::cast_possible_truncation)]
let drag_cursor = mouse::Cursor::Available((x as f32, y as f32).into());
let drag_cursor = mouse::Cursor::Available((*x as f32, *y as f32).into());
let event = Event::Mouse(mouse::Event::CursorMoved {
position: drag_cursor.position().unwrap(),
});
self.container.as_widget_mut().on_event(
self.container.as_widget_mut().update(
&mut tree.children[0],
event,
&event,
layout,
drag_cursor,
renderer,
@ -468,7 +470,8 @@ impl<Message: 'static> Widget<Message, crate::Theme, crate::Renderer>
viewport,
);
}
return event::Status::Captured;
shell.capture_event();
return;
}
Event::Dnd(DndEvent::Offer(_, OfferEvent::LeaveDestination)) => {
log::trace!(
@ -481,9 +484,9 @@ impl<Message: 'static> Widget<Message, crate::Theme, crate::Renderer>
{
shell.publish(msg);
}
return event::Status::Ignored;
return;
}
Event::Dnd(DndEvent::Offer(id, OfferEvent::Drop)) if id == Some(my_id) => {
Event::Dnd(DndEvent::Offer(id, OfferEvent::Drop)) if *id == Some(my_id) => {
log::trace!(
target: DND_DEST_LOG_TARGET,
"offer drop id={my_id:?}"
@ -493,27 +496,29 @@ impl<Message: 'static> Widget<Message, crate::Theme, crate::Renderer>
{
shell.publish(msg);
}
return event::Status::Captured;
shell.capture_event();
return;
}
Event::Dnd(DndEvent::Offer(id, OfferEvent::SelectedAction(action)))
if id == Some(my_id) =>
if *id == Some(my_id) =>
{
log::trace!(
target: DND_DEST_LOG_TARGET,
"offer selected-action id={my_id:?} action={action:?}"
);
if let Some(msg) = state.on_action_selected(
action,
*action,
self.on_action_selected
.as_ref()
.map(std::convert::AsRef::as_ref),
) {
shell.publish(msg);
}
return event::Status::Captured;
shell.capture_event();
return;
}
Event::Dnd(DndEvent::Offer(id, OfferEvent::Data { data, mime_type }))
if id == Some(my_id) =>
if *id == Some(my_id) =>
{
log::trace!(
target: DND_DEST_LOG_TARGET,
@ -527,25 +532,33 @@ impl<Message: 'static> Widget<Message, crate::Theme, crate::Renderer>
&& let Ok(s) = String::from_utf8(data[..data.len() - 1].to_vec())
{
shell.publish(f(s));
return event::Status::Captured;
shell.capture_event();
return;
}
if let (Some(msg), ret) = state.on_data_received(
mime_type,
data,
mime_type.clone(),
data.clone(),
self.on_data_received
.as_ref()
.map(std::convert::AsRef::as_ref),
self.on_finish.as_ref().map(std::convert::AsRef::as_ref),
) {
shell.publish(msg);
return ret;
if ret == event::Status::Captured {
log::trace!(
target: DND_DEST_LOG_TARGET,
"offer data id={my_id:?} captured"
);
shell.capture_event();
}
return;
}
return event::Status::Captured;
shell.capture_event();
return;
}
_ => {}
}
event::Status::Ignored
}
fn mouse_interaction(
@ -589,13 +602,18 @@ impl<Message: 'static> Widget<Message, crate::Theme, crate::Renderer>
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: layout::Layout<'_>,
layout: layout::Layout<'b>,
renderer: &crate::Renderer,
viewport: &Rectangle,
translation: Vector,
) -> Option<overlay::Element<'b, Message, crate::Theme, crate::Renderer>> {
self.container
.as_widget_mut()
.overlay(&mut tree.children[0], layout, renderer, translation)
self.container.as_widget_mut().overlay(
&mut tree.children[0],
layout,
renderer,
viewport,
translation,
)
}
fn drag_destinations(

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