Compare commits

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

97 commits

Author SHA1 Message Date
24da1dc0e4 merge: pop-os/iced a l'etat pinne par libcosmic 08a09e3df (epoch-1.9.0)
Some checks failed
Audit / vulnerabilities (push) Has been cancelled
Check / wasm (push) Has been cancelled
Check / widget (push) Has been cancelled
Document / all (push) Has been cancelled
Format / all (push) Has been cancelled
Lint / all (push) Has been cancelled
Test / all (macOS-latest, 1.88) (push) Has been cancelled
Test / all (macOS-latest, beta) (push) Has been cancelled
Test / all (macOS-latest, stable) (push) Has been cancelled
Test / all (ubuntu-latest, 1.88) (push) Has been cancelled
Test / all (ubuntu-latest, beta) (push) Has been cancelled
Test / all (ubuntu-latest, stable) (push) Has been cancelled
Test / all (windows-latest, 1.88) (push) Has been cancelled
Test / all (windows-latest, beta) (push) Has been cancelled
Test / all (windows-latest, stable) (push) Has been cancelled
# Conflicts:
#	Cargo.toml
2026-09-24 20:45:11 +02:00
Hojjat
0968a10fc1 fix: arrow keys in text editor on a layer_surface 2026-09-22 01:10:34 -04:00
Hojjat
6f1b3a7170 fix(wayland): share a grabbing popup's modifiers with its toplevel 2026-09-21 17:43:21 -06:00
Hojjat
5716b9a691 fix: build user interfaces in window id order 2026-09-21 17:43:21 -06:00
Ashley Wulber
5e2b580791 chore: update accesskit 2026-09-21 12:13:00 -04:00
Vukašin Vojinović
021cb93fa3 improv(scrollable): match padding impl to upstream 2026-09-18 20:57:23 +02:00
Blake Gardner
ffc3ac317e fix(test): build iced_test against this fork's runtime and renderer style
`iced_test` has not compiled here since it arrived from upstream: it
matches `runtime::Action` exhaustively without the fork's `Dnd` and
`PlatformSpecific` variants, and builds `renderer::Style` without the
fork's `icon_color` and `scale_factor` fields. Nothing else in the
workspace depends on it, so the errors only show up in
`cargo test --workspace` and for anyone adding the crate to an
application.

The emulator ignores the two actions: drag and drop and platform
surfaces need a windowing system, which it has none of. The style
fields follow what the winit runtime passes: the icon color is the
text color, and the scale factor is the one the emulator or simulator
already renders its screenshot at.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 23:29:51 -04:00
Hojjat
85d4e4d5c2 chore: update winit to support moving windows with touchscreen 2026-09-16 15:08:25 -04:00
Hojjat
890a53fef1 fix: add tablet support for toplevel windows 2026-09-16 06:57:02 -06:00
Hojjat
5fc4372d87 fix: do not report unfocused if focus moved to surface's own popup 2026-09-15 19:05:32 -04:00
Ian Douglas Scott
87e1787fbb cosmic-session-lock-layer-v1 protocol 2026-09-11 13:06:42 -06:00
Hojjat
9ef36c9604 feat(text): add selected_text_color to the text style 2026-09-11 19:34:51 +02:00
Ashley Wulber
2e12eba5cd improv: PreOperation for tracking children in selections 2026-09-11 11:45:52 -04:00
Ashley Wulber
880f2c7565 improv: operation and select for directional navigation 2026-09-11 11:45:52 -04:00
Hojjat
d0f221c48b feat(text): expose has_text and clipboard_has_text for context menus 2026-09-11 11:34:02 -04:00
d12bb8e056 merge: upstream/master (pre-epoch-1.8.0)
Some checks failed
Audit / vulnerabilities (push) Has been cancelled
Check / wasm (push) Has been cancelled
Check / widget (push) Has been cancelled
Document / all (push) Has been cancelled
Format / all (push) Has been cancelled
Lint / all (push) Has been cancelled
Test / all (macOS-latest, 1.88) (push) Has been cancelled
Test / all (macOS-latest, beta) (push) Has been cancelled
Test / all (macOS-latest, stable) (push) Has been cancelled
Test / all (ubuntu-latest, 1.88) (push) Has been cancelled
Test / all (ubuntu-latest, beta) (push) Has been cancelled
Test / all (ubuntu-latest, stable) (push) Has been cancelled
Test / all (windows-latest, 1.88) (push) Has been cancelled
Test / all (windows-latest, beta) (push) Has been cancelled
Test / all (windows-latest, stable) (push) Has been cancelled
2026-09-10 17:52:30 +02:00
Hojjat
ffe1f1dbe3 fix(wayland): deliver popup Done to the parent window's widgets too 2026-09-09 16:17:27 -04:00
Hojjat
511180679b feat: Lancsoz downsampler when target size is relatively too small 2026-09-04 12:54:04 -04:00
Ashley Wulber
cc5ca1ce29 fix: handling of grab chains and deletion of pending popups 2026-09-03 18:52:20 +02:00
Ashley Wulber
39d3c99d13 improv: allow multiple popup chains
multiple grabs are still not allowed, but this seems to be working well with a tooltip, nested popup chains, and hopefully a context drawer popup
2026-08-28 17:20:22 +02:00
Hojjat
aca94cf2af feat(wayland): add ability to hide the cursor over a surface
also now the surfaces say what the cursor over them should look like,
previously one cursor shape was selected globally for the seat
2026-08-27 22:05:27 -04:00
Hojjat
65e9faa43c feat(wayland): let a layer surface change its input region 2026-08-27 21:07:50 -04:00
e891f34b32 Merge remote-tracking branch 'upstream/master' into yoda-wayland-only
Some checks failed
Audit / vulnerabilities (push) Has been cancelled
Check / wasm (push) Has been cancelled
Check / widget (push) Has been cancelled
Document / all (push) Has been cancelled
Format / all (push) Has been cancelled
Lint / all (push) Has been cancelled
Test / all (macOS-latest, 1.88) (push) Has been cancelled
Test / all (macOS-latest, beta) (push) Has been cancelled
Test / all (macOS-latest, stable) (push) Has been cancelled
Test / all (ubuntu-latest, 1.88) (push) Has been cancelled
Test / all (ubuntu-latest, beta) (push) Has been cancelled
Test / all (ubuntu-latest, stable) (push) Has been cancelled
Test / all (windows-latest, 1.88) (push) Has been cancelled
Test / all (windows-latest, beta) (push) Has been cancelled
Test / all (windows-latest, stable) (push) Has been cancelled
2026-08-27 07:48:56 +02:00
Frederic Laing
87c0358985 fix(scrollable): allow scrolling when children capture touch presses 2026-08-20 21:34:13 +02:00
c34af04431 fix: create_surface still gated on the removed cctk feature
Some checks failed
Audit / vulnerabilities (push) Has been cancelled
Check / wasm (push) Has been cancelled
Check / widget (push) Has been cancelled
Document / all (push) Has been cancelled
Format / all (push) Has been cancelled
Lint / all (push) Has been cancelled
Test / all (macOS-latest, 1.88) (push) Has been cancelled
Test / all (macOS-latest, beta) (push) Has been cancelled
Test / all (macOS-latest, stable) (push) Has been cancelled
Test / all (ubuntu-latest, 1.88) (push) Has been cancelled
Test / all (ubuntu-latest, beta) (push) Has been cancelled
Test / all (ubuntu-latest, stable) (push) Has been cancelled
Test / all (windows-latest, 1.88) (push) Has been cancelled
Test / all (windows-latest, beta) (push) Has been cancelled
Test / all (windows-latest, stable) (push) Has been cancelled
Upstream renamed the iced `cctk` features to `wayland` and moved the
positive arm of create_surface() to the wayland_platform cfg alias, but
left the negative arm on `feature = "cctk"`. That feature no longer
exists, so its cfg was always true and both arms compiled at once,
giving `expected (), found Option<Box<dyn HasWindowHandle>>`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 21:53:10 +02:00
d24bcd0272 Merge remote-tracking branch 'upstream/master' into yoda-wayland-only
# Conflicts:
#	Cargo.lock
#	Cargo.toml
#	wgpu/Cargo.toml
#	wgpu/src/window/compositor.rs
2026-08-19 21:44:35 +02:00
Hojjat
7918b2826e fix: request redraw for Text widget 2026-08-09 05:35:16 +02:00
Hojjat
f5aa5a6f8c fix: don't request redraw for pointer 2026-08-05 15:31:43 -04:00
Hojjat
b87e660eed perf: do not redraw on mouse move events 2026-08-05 15:31:43 -04:00
Ashley Wulber
86b236f2e1 fix: remove any old blur surface or corners on layer shell done event 2026-07-29 14:21:04 -04:00
Ashley Wulber
f86d983896 fix: remove any old blur surface or corners on surface creation 2026-07-29 14:21:04 -04:00
Aadil
65b5dbc671
feat(slider): add handle bounds to avoid handle shifting on hover (#357) 2026-07-24 09:25:32 +02:00
sanua356
5a6cd771d1
fix: compiler error for e2k CPU architecture 2026-07-23 20:15:43 +02:00
Inuxiuz
dc0e80edd8 feat: add overflow option to Pin widget to allow content beyond boundaries 2026-07-23 19:49:24 +02:00
Hojjat
ba4f09b7bf fix: tear down dnd surface when the dnd is ended programatically 2026-07-23 19:48:49 +02:00
RipleyTom
3a82d48548 fix: ignore resize border on fullscreen/maximized 2026-07-23 13:44:53 -04:00
Frederic Laing
300fa97ee2 fix: consume XDG activation token on window creation 2026-07-18 07:43:12 -06:00
Ashley Wulber
a7c71f8dd3 improv: feature gate for xdg_portal 2026-07-15 16:46:43 -04:00
Ashley Wulber
b3b0ab1cd5 fix: feature gates 2026-07-15 16:46:43 -04:00
Ilia Malanin
f6daee14ac fix: avoid panic on sub-pixel rounded borders 2026-07-14 00:11:26 +02:00
Hojjat
c195232875 fix: avoid 1×1 popup geometry that trips corner-radius validation 2026-07-14 00:10:47 +02:00
Hojjat
775abd45d1 Revert "fix: validate corner radius against window geometry"
This reverts commit 5da1188557.
2026-07-14 00:10:47 +02:00
Hojjat
5da1188557 fix: validate corner radius against window geometry 2026-07-10 02:23:59 +02:00
Ian Douglas Scott
5671e49c9b wgpu: Apply Nvidia driver workaround regardless of cctk feature 2026-07-09 13:25:14 -07:00
Ilia Malanin
371dd94a2a fix: intersect nested clip bounds with parent layer 2026-07-08 10:59:02 -04:00
Ashley Wulber
b5b2b2fcc3 fix: popup cleanup 2026-07-06 18:44:30 -04:00
1a57634add yoda: cargo fix sweep post-rebase (unused imports/vars)
Some checks failed
Check / widget (push) Has been cancelled
Audit / vulnerabilities (push) Has been cancelled
Check / wasm (push) Has been cancelled
Document / all (push) Has been cancelled
Format / all (push) Has been cancelled
Lint / all (push) Has been cancelled
Test / all (macOS-latest, 1.88) (push) Has been cancelled
Test / all (macOS-latest, beta) (push) Has been cancelled
Test / all (macOS-latest, stable) (push) Has been cancelled
Test / all (ubuntu-latest, 1.88) (push) Has been cancelled
Test / all (ubuntu-latest, beta) (push) Has been cancelled
Test / all (ubuntu-latest, stable) (push) Has been cancelled
Test / all (windows-latest, 1.88) (push) Has been cancelled
Test / all (windows-latest, beta) (push) Has been cancelled
Test / all (windows-latest, stable) (push) Has been cancelled
Leyoda 2026 – GPLv3
2026-07-02 19:10:42 +02:00
362b4abb15 chore: use local winit and softbuffer 2026-07-02 18:44:56 +02:00
cf7828fb70 chore: use local glyphon and cosmic-text 2026-07-02 18:44:56 +02:00
3982e04aa2 chore: use local cctk and clipboard crates 2026-07-02 18:44:11 +02:00
4e2e58e03d chore: gate accessibility-only winit state 2026-07-02 18:43:40 +02:00
Votre Nom
b90b1413a6 yoda: point window_clipboard at public Forgejo fork
Replace pop-os/window_clipboard sctk-0.20 (no wayland feature) with
forge.aditua.com/leyoda/window_clipboard branch yoda-x11-optional. This
removes the need for cosmic-files' [patch] redirect to a local path and
makes libcosmic build standalone again.

Leyoda 2026 – GPLv3
2026-07-02 18:42:57 +02:00
Votre Nom
ce5ea665e2 Fix high-signal widget and Wayland warnings
Avoid recursive operation traversal, execute visible list updates instead of leaving a lazy iterator unused, remove unreachable cfg-gated surface creation code, and handle subsurface buffer attachment failures.
2026-07-02 18:42:57 +02:00
f1f42f2bff yoda: softbuffer + window_clipboard default-features=false
Two one-line workspace-dep fixes that drop the last remaining X11 crates
for Wayland-only builds:

- softbuffer: its default = [kms, x11, x11-dlopen, wayland, wayland-dlopen]
  pulled tiny-xlib + as-raw-xcb-connection unconditionally even though
  iced_tiny_skia only needed softbuffer/wayland. Disabling the default
  lets iced_tiny_skia's own feature propagation ask for just wayland +
  wayland-dlopen.

- window_clipboard: upstream pulled clipboard_x11 + clipboard_wayland
  unconditionally on unix. Our local fork (branch yoda-x11-optional)
  gates clipboard_x11 behind an opt-in feature, so default-features=false
  + features=["wayland"] drops ~500 x11 symbols (clipboard_x11, x11rb,
  x11rb-protocol).

Combined with previous yoda-v4 cuts, cosmic-yoterm goes from
1526 -> 43 x11 symbols (-97%), and 55.4 MB -> 53.3 MB (-2.1 MB).
2026-07-02 18:42:42 +02:00
702b6359a5 yoda: gate iced_wgpu x11 backend behind an opt-in feature
iced_wgpu's unix block unconditionally depended on as-raw-xcb-connection,
tiny-xlib and x11rb for a DRI3 device-id fallback in
window/compositor.rs that only fires when wayland can't determine the
GPU. On a Wayland-only build this code path never runs, so the deps
are pure bloat.

Changes:
- Cargo.toml: move the 3 x11 crates to optional + new feature `x11`
  (not in default — wayland-only is the yoda target; users who need
  X11 opt in).
- window.rs: cfg-gate `mod x11;` on feature = "x11"
- window/compositor.rs: cfg-gate `use super::x11::get_x11_device_ids`
  + the or_else fallback on feature = "x11" (wrapped in a let-binding
  so the non-x11 branch compiles cleanly).

Result: iced_wgpu no longer pulls any x11 crate when built without
the feature.
2026-07-02 18:42:13 +02:00
a50a29b390 yoda: drop x11 default on winit workspace dep
Was pulling winit's full x11 backend (winit-x11 + x11-dl) unconditionally
despite iced_winit's own x11 feature being disabled. Fixed by adding
default-features=false on the workspace-level winit dep — iced_winit/wayland
+ wayland-dlopen propagate to winit/wayland + wayland-dlopen as needed.
2026-07-02 18:42:13 +02:00
Ashley Wulber
c781ff6199 chore: cargo fmt 2026-07-01 10:37:52 -04:00
Ashley Wulber
633ed14411 feat: popup reposition 2026-07-01 10:37:52 -04:00
Ashley Wulber
da9d15b2e8 chore: update cosmic-protocols 2026-07-01 10:37:52 -04:00
Ashley Wulber
8e94441efa fix: apply initial corner radius after adding surfaces to the state
otherwise the corners won't be set.
2026-07-01 10:37:52 -04:00
Ashley Wulber
46781102fe fix: set blur and corners on creation 2026-07-01 10:37:52 -04:00
Ashley Wulber
5cc9f8d621 fix: single blur surface per wl surface 2026-07-01 10:37:52 -04:00
Ashley Wulber
6be49cb530 blur command helper 2026-07-01 10:37:52 -04:00
Ashley Wulber
b8b0804140 fix(overlap): fix check for disabling 2026-07-01 10:37:52 -04:00
Ashley Wulber
6521f7fa20 fix: improved layer surface popup cleanup 2026-07-01 10:37:52 -04:00
Ashley Wulber
f6fa506f6c chore(layer shell): add padding action 2026-07-01 10:37:52 -04:00
Ashley Wulber
e0c7d353aa better handle pending corners 2026-07-01 10:37:52 -04:00
Ashley Wulber
a63d57261f blur enabled event 2026-07-01 10:37:52 -04:00
Ashley Wulber
1bcba7e30c wip: corner radius v2 2026-07-01 10:37:52 -04:00
Hojjat
9f41055861 Revert "feat: add direct SCTK action sender for wayland popups"
This reverts commit b007caba44.
2026-06-30 12:26:07 -04:00
Hojjat
7afd9ee0f8 fix: add alt clipboard bindings for text_editor 2026-06-21 06:49:01 -04:00
Jeremy Soller
62b90179a2 Fix List widget not updating children because iterator not consumed 2026-06-10 12:04:26 -06:00
Ilia Malanin
c3c8fcf52c refactor: wrap repeated run_action call in a macro 2026-06-09 10:14:25 -04:00
Ilia Malanin
f725961c87 fix: reuse wgpu compositor across windows 2026-06-09 10:14:25 -04:00
Ashley Wulber
dcf4cf0194 improv: ignore duplicate surfaces and log a warning 2026-06-06 02:22:09 +02:00
Hojjat
5013f63956 improv: reuse loaded fonts by cosmic-text for svg renderer 2026-05-30 02:56:54 +02:00
Ashley Wulber
ebb3b125bf Revert "fix(text): don't reload cache every redraw"
This reverts commit 590e093086.
2026-05-29 16:43:44 -04:00
Gavin John
590e093086 fix(text): don't reload cache every redraw
I have >1000 fonts installed. COSMIC has always been somewhat laggy for me, but I had assumed it was just like that for everyone, or was the fault of my T480s. Eventually, when trying to diagnose some hitching, I ended up discovering that cosmic-launcher was consuming on average ~5% total CPU over time, which led me to discover that it was fetching all the fonts from disk and software rendering the fonts, every redraw, causing the high cpu usage *and* the stutters (as a result of hammering disk I/O). Oops.

The fix is simple: just persist the cache across redraws.
2026-05-29 19:56:54 +02:00
Ashley Wulber
9c748f1656 Revert "fix: avoid deadlock in context menu rendering"
This reverts commit 0094b347d4.
2026-05-27 17:25:49 -04:00
Hojjat
dfcd989947 fix: present suboptimal frames 2026-05-27 17:22:00 -04:00
Hojjat
0094b347d4 fix: avoid deadlock in context menu rendering 2026-05-27 17:22:00 -04:00
Ashley Wulber
a7d71d9ace chore: modifiers event on keyboard enter and leave 2026-05-27 20:01:47 +02:00
Ashley Wulber
6105d8bdef reexport geometry from iced_graphics 2026-05-22 15:42:09 -04:00
Michael Aaron Murphy
98fb6a0fba
docs: update cosmic-protocols with docs cfg fix 2026-05-20 17:00:28 +02:00
Hojjat
b007caba44 feat: add direct SCTK action sender for wayland popups 2026-05-20 16:04:35 +02:00
Hojjat
a7ca336eb4 feat: HasSelectableText impl to TextEditor 2026-05-20 16:04:35 +02:00
Hojjat
2e1e4bd6e0 feat: add HasSelectableText trait to Text widget 2026-05-20 16:04:35 +02:00
Hojjat
9ae155b8e2 fix: crash while resizing on x11 2026-05-20 16:00:15 +02:00
Ericky Dos Santos
346faf2e79 Make shader_f16 optional because it does not work in webgl 2026-05-20 07:26:41 -04:00
Hojjat
251254ad51 feat: add selectable() to Text 2026-05-16 08:28:37 -04:00
Ashley Wulber
347d91f7ea improv(wgpu): adapter selection 2026-05-10 22:41:11 -04:00
KENZ
1444d5c43e fix(ime): Don't panic if some IME sends DeleteSurrounding events 2026-05-08 00:23:00 +02:00
KENZ
f752bba18f Revert "Fix the candidate window position of fcitx5 which does't work well wi…"
This reverts commit a9bcb00529.
2026-05-08 00:22:22 +02:00
Lcstyle
d7595fab95 fix: bootstrap FrameStatus on session lock surface configure
The session lock configure handler was missing receive_frame() and
request_redraw() calls that the layer surface configure handler has.
Without these, after S3 resume or DPMS wake, the FrameStatus entry
for lock surfaces could be missing, causing the rendering gate to
never reach Ready state — resulting in permanently grey lock screens.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-07 12:40:09 -04:00
Ashley Wulber
e000c71ff2 fix: send redraw request after events
creation events must be handled before processing redraws
2026-05-07 17:39:02 +02:00
edwloef
3928e0d385 wgpu: request devices with SHADER_F16 feature 2026-04-27 15:03:07 +02:00
Hojjat
90d2c418a7 chore: update to wgpu 28 and cosmic-text 0.19 2026-04-27 14:35:24 +02:00
122 changed files with 4579 additions and 2379 deletions

2312
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -118,13 +118,17 @@ winit = [
"iced_accessibility?/accesskit_winit",
"iced_program/winit",
]
wayland = ["iced_renderer/wayland", "iced_winit/wayland"]
cctk = ["iced_winit/cctk", "iced_widget/cctk", "iced_core/cctk", "wayland"]
# Enables the sctk shell.
wayland = [
"iced_renderer/wayland",
"iced_winit/wayland",
"iced_runtime/wayland",
"iced_widget/wayland",
"iced_core/wayland",
]
[dependencies]
iced_devtools.workspace = true
iced_devtools.optional = true
iced_debug.workspace = true
iced_program.workspace = true
@ -134,10 +138,9 @@ iced_renderer.workspace = true
iced_runtime.workspace = true
iced_widget.workspace = true
iced_winit.workspace = true
iced_devtools.optional = true
iced_winit.optional = true
iced_tester.workspace = true
iced_tester.optional = true
iced_winit.optional = true
iced_highlighter.workspace = true
iced_highlighter.optional = true
iced_accessibility.workspace = true
@ -190,6 +193,7 @@ members = [
"winit",
"examples/*",
"accessibility",
"build_helpers",
]
exclude = ["examples/integration"]
@ -202,7 +206,7 @@ repository = "https://github.com/iced-rs/iced"
homepage = "https://iced.rs"
categories = ["gui"]
keywords = ["gui", "ui", "graphics", "interface", "widgets"]
rust-version = "1.89"
rust-version = "1.92"
[workspace.dependencies]
iced = { version = "0.14.0", path = "." }
@ -238,10 +242,9 @@ async-std = "1.0"
bitflags = "2.5"
bytemuck = { version = "1.0", features = ["derive"] }
bytes = "1.6"
cosmic-text = { git = "https://github.com/pop-os/cosmic-text.git" }
# cosmic-text = "0.10"
cosmic-text = { path = "../../cosmic-text" }
cryoglyph = { package = "cryoglyph", path = "../../glyphon" }
dark-light = "1.0"
cryoglyph = { package = "cryoglyph", git = "https://github.com/pop-os/glyphon.git", tag = "cosmic-0.14" }
resvg = "0.45"
web-sys = "0.3.69"
guillotiere = "0.6"
@ -275,8 +278,11 @@ tiny-skia = { version = "0.11", default-features = false, features = [
"std",
"simd",
] }
cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "160b086" }
softbuffer = { git = "https://github.com/pop-os/softbuffer", tag = "cosmic-4.0" }
cctk = { path = "../../cosmic-protocols/client-toolkit", package = "cosmic-client-toolkit" }
# Yoda: default-features=false drops softbuffer's x11 / x11-dlopen (pulled
# tiny-xlib + as-raw-xcb-connection). The wayland + wayland-dlopen features
# are re-activated via iced_tiny_skia's own `wayland` feature propagation.
softbuffer = { path = "../../softbuffer", default-features = false }
syntect = "5.2"
tokio = "1.0"
tracing = "0.1"
@ -288,7 +294,7 @@ url = "2.5"
wasm-bindgen-futures = "0.4"
wasmtimer = "0.4.2"
web-time = "1.1"
wgpu = { version = "27.0", default-features = false, features = [
wgpu = { version = "28.0", default-features = false, features = [
"std",
"wgsl",
] }
@ -302,13 +308,40 @@ wayland-client = { version = "0.31.5" }
winapi = "0.3"
# window_clipboard = "0.4.1"
window_clipboard = { git = "https://github.com/pop-os/window_clipboard.git", tag = "sctk-0.20" }
dnd = { git = "https://github.com/pop-os/window_clipboard.git", tag = "sctk-0.20" }
mime = { git = "https://github.com/pop-os/window_clipboard.git", tag = "sctk-0.20" }
winit = { git = "https://github.com/pop-os/winit.git", tag = "cosmic-0.14" }
winit-core = { git = "https://github.com/pop-os/winit.git", tag = "cosmic-0.14" }
# Yoda: window_clipboard pinned to our public fork that has x11 gated behind
# an opt-in feature. default-features=false + features=["wayland"] drops
# clipboard_x11 + x11rb from the build. Branch yoda-x11-optional on
# https://forge.aditua.com/leyoda/window_clipboard.
window_clipboard = { path = "../../window_clipboard", default-features = false, features = ["wayland"] }
dnd = { path = "../../window_clipboard/dnd" }
mime = { path = "../../window_clipboard/mime" }
# Yoda: default-features = false drops winit's x11 default, which otherwise
# pulls winit-x11, x11-dl, tiny-xlib, as-raw-xcb-connection. Our Wayland-only
# fork doesn't need any of that — the wayland + wayland-dlopen features are
# re-enabled via iced_winit's own feature propagation from libcosmic-yoda.
winit = { path = "../../winit/winit", default-features = false }
winit-core = { path = "../../winit/winit-core" }
cursor-icon = "1.1.0"
[patch.'https://github.com/pop-os/softbuffer']
softbuffer = { path = "../../softbuffer" }
[patch.'https://github.com/pop-os/smithay-clipboard']
smithay-clipboard = { path = "../../smithay-clipboard" }
[patch.'https://github.com/pop-os/winit.git']
dpi = { path = "../../winit/dpi" }
winit = { path = "../../winit/winit" }
winit-android = { path = "../../winit/winit-android" }
winit-appkit = { path = "../../winit/winit-appkit" }
winit-common = { path = "../../winit/winit-common" }
winit-core = { path = "../../winit/winit-core" }
winit-orbital = { path = "../../winit/winit-orbital" }
winit-uikit = { path = "../../winit/winit-uikit" }
winit-wayland = { path = "../../winit/winit-wayland" }
winit-web = { path = "../../winit/winit-web" }
winit-win32 = { path = "../../winit/winit-win32" }
winit-x11 = { path = "../../winit/winit-x11" }
[workspace.lints.rust]
rust_2018_idioms = { level = "deny", priority = -1 }
@ -334,3 +367,6 @@ useless_conversion = "deny"
[workspace.lints.rustdoc]
broken_intra_doc_links = "forbid"
[build-dependencies]
build_helpers = { path = "./build_helpers" }

View file

@ -10,10 +10,10 @@ async-io = ["accesskit_winit?/async-io"]
tokio = ["accesskit_winit?/tokio"]
[dependencies]
accesskit = { git = "https://github.com/wash2/accesskit", tag = "cosmic-0.14" }
accesskit_windows = { git = "https://github.com/wash2/accesskit", tag = "cosmic-0.14", optional = true }
accesskit_macos = { git = "https://github.com/wash2/accesskit", tag = "cosmic-0.14", optional = true }
accesskit_winit = { git = "https://github.com/wash2/accesskit", tag = "cosmic-0.14", optional = true, default-features = false, features = [
accesskit = { git = "https://github.com/wash2/accesskit", rev = "6c20249" }
accesskit_windows = { git = "https://github.com/wash2/accesskit", rev = "6c20249", optional = true }
accesskit_macos = { git = "https://github.com/wash2/accesskit", rev = "6c20249", optional = true }
accesskit_winit = { git = "https://github.com/wash2/accesskit", rev = "6c20249", optional = true, default-features = false, features = [
"rwh_06",
] }
# accesskit = { path = "../../../accesskit/common" }

3
build.rs Normal file
View file

@ -0,0 +1,3 @@
fn main() {
build_helpers::cfg_aliases_setup();
}

17
build_helpers/Cargo.toml Normal file
View file

@ -0,0 +1,17 @@
[package]
name = "build_helpers"
version.workspace = true
authors.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
homepage.workspace = true
categories.workspace = true
keywords.workspace = true
rust-version.workspace = true
[dependencies]
cfg_aliases = { version = "0.2.1" }
[lints]
workspace = true

24
build_helpers/src/lib.rs Normal file
View file

@ -0,0 +1,24 @@
use cfg_aliases::cfg_aliases;
pub fn cfg_aliases_setup() {
// The script doesn't depend on our code
println!("cargo:rerun-if-changed=build.rs");
// Setup cfg aliases
cfg_aliases! {
// Systems.
android_platform: { target_os = "android" },
wasm_platform: { all(target_family = "wasm", not(target_os = "emscripten")) },
macos_platform: { target_os = "macos" },
ios_platform: { target_os = "ios" },
windows_platform: { target_os = "windows" },
apple: { any(target_os = "ios", target_os = "macos") },
free_unix: { all(unix, not(apple), not(android_platform), not(target_os = "emscripten")) },
redox: { target_os = "redox" },
// Native displays.
wayland_platform: { all(feature = "wayland", free_unix, not(wasm), not(redox)) },
xdg_portal: { all(feature = "xdg-portal", free_unix, not(wasm), not(redox)) },
orbital_platform: { redox },
}
}

View file

@ -19,6 +19,7 @@ crisp = []
basic-shaping = []
advanced-shaping = []
a11y = ["iced_accessibility"]
wayland = ["cctk"]
[dependencies]
palette = "0.7"
@ -31,6 +32,7 @@ num-traits.workspace = true
rustc-hash.workspace = true
smol_str.workspace = true
thiserror.workspace = true
unicode-segmentation.workspace = true
web-time.workspace = true
serde.workspace = true
@ -42,8 +44,6 @@ window_clipboard.workspace = true
dnd.workspace = true
mime.workspace = true
cctk.workspace = true
cctk.optional = true
# /TODO(POP)
[dependencies.iced_accessibility]
version = "0.1.0"
@ -55,3 +55,10 @@ raw-window-handle.workspace = true
[dev-dependencies]
approx = "0.5"
[target.'cfg(all(unix, not(any(target_os = "redox", target_family = "wasm", target_os = "android", target_vendor = "apple"))))'.dependencies]
cctk.workspace = true
cctk.optional = true
[build-dependencies]
build_helpers = { path = "../build_helpers" }

3
core/build.rs Normal file
View file

@ -0,0 +1,3 @@
fn main() {
build_helpers::cfg_aliases_setup();
}

View file

@ -1,4 +1,4 @@
use crate::event::{self, Event};
use crate::event::Event;
use crate::id::Id;
use crate::layout;
use crate::mouse;

View file

@ -7,7 +7,8 @@ use crate::keyboard;
use crate::mouse;
use crate::touch;
use crate::window;
#[cfg(all(feature = "cctk", target_os = "linux"))]
#[cfg(wayland_platform)]
/// A platform specific event for wayland
pub mod wayland;
/// A user interface event.
@ -49,7 +50,7 @@ pub enum Event {
/// A platform specific event
#[derive(Debug, Clone, PartialEq)]
pub enum PlatformSpecific {
#[cfg(all(feature = "cctk", target_os = "linux"))]
#[cfg(wayland_platform)]
/// A Wayland specific event
Wayland(wayland::Event),
}

View file

@ -46,4 +46,6 @@ pub enum Event {
Subsurface(SubsurfaceEvent),
/// Keyboard inhibit shortcuts
ShortcutsInhibited(bool),
/// Blur Support Enabled
BlurEnabled,
}

View file

@ -3,7 +3,7 @@ use crate::mouse;
use crate::overlay;
use crate::renderer;
use crate::widget;
use crate::{Clipboard, Event, Layout, Overlay, Point, Rectangle, Shell, Size};
use crate::{Clipboard, Event, Layout, Overlay, Shell, Size};
/// An [`Overlay`] container that displays multiple overlay [`overlay::Element`]
/// children.

View file

@ -78,6 +78,8 @@ pub enum Action {
SelectLine,
/// Select the entire buffer.
SelectAll,
/// Clear the selection
ClearSelection,
/// Perform an [`Edit`].
Edit(Edit),
/// Click the [`Editor`] at the given [`Point`].

View file

@ -9,7 +9,7 @@ pub use scrollable::Scrollable;
pub use text_input::TextInput;
use crate::widget::Id;
use crate::{Rectangle, Vector};
use crate::{Rectangle, Vector, window};
use std::any::Any;
use std::fmt;
@ -75,6 +75,12 @@ pub trait Operation<T = ()>: Send {
fn finish(&self) -> Outcome<T> {
Outcome::None
}
/// Track the active window id being processed if relevant to the operation
fn set_window_id(&mut self, _id: window::Id) {}
/// Used to mark the beginning of operations for a widget and its children
fn pre_operation(&mut self, _id: Option<&Id>) {}
}
impl<T, O> Operation<O> for Box<T>
@ -140,6 +146,14 @@ where
fn finish(&self) -> Outcome<O> {
self.as_ref().finish()
}
fn set_window_id(&mut self, id: window::Id) {
self.as_mut().set_window_id(id);
}
fn pre_operation(&mut self, id: Option<&Id>) {
self.as_mut().pre_operation(id);
}
}
/// The result of an [`Operation`].
@ -243,6 +257,14 @@ where
fn finish(&self) -> Outcome<O> {
Outcome::None
}
fn pre_operation(&mut self, id: Option<&Id>) {
self.operation.pre_operation(id);
}
fn set_window_id(&mut self, id: window::Id) {
self.operation.set_window_id(id);
}
}
BlackBox { operation }
@ -341,6 +363,10 @@ where
) {
self.operation.custom(id, bounds, state);
}
fn pre_operation(&mut self, id: Option<&Id>) {
self.operation.pre_operation(id);
}
}
self.operation.traverse(&mut |operation| {
@ -410,6 +436,14 @@ where
})),
}
}
fn set_window_id(&mut self, id: window::Id) {
self.operation.set_window_id(id);
}
fn pre_operation(&mut self, id: Option<&Id>) {
self.operation.pre_operation(id);
}
}
Map {
@ -515,6 +549,14 @@ where
}
}
}
fn set_window_id(&mut self, id: window::Id) {
self.operation.set_window_id(id);
}
fn pre_operation(&mut self, id: Option<&Id>) {
self.operation.pre_operation(id);
}
}
Chain {
@ -566,6 +608,14 @@ pub fn scoped<T: 'static>(
outcome => outcome,
}
}
fn pre_operation(&mut self, id: Option<&Id>) {
self.operation.pre_operation(id);
}
fn set_window_id(&mut self, id: window::Id) {
self.operation.set_window_id(id);
}
}
ScopedOperation {

View file

@ -4,7 +4,7 @@ use super::Operation;
use crate::{
Rectangle,
id::Id,
widget::operation::{Outcome, focusable::Count},
widget::operation::Outcome,
};
/// Produces an [`Operation`] that searches for the Id

View file

@ -22,16 +22,18 @@
//! ```
use crate::alignment;
use crate::layout;
use crate::mouse;
use crate::mouse::{self, click};
use crate::renderer;
use crate::text;
use crate::text::paragraph::{self, Paragraph};
use crate::text::{self, Fragment};
use crate::widget::tree::{self, Tree};
use crate::{
Color, Element, Layout, Length, Pixels, Rectangle, Size, Theme, Widget,
Clipboard, Color, Element, Event, Layout, Length, Pixels, Point, Rectangle,
Shell, Size, Theme, Widget, keyboard, touch,
};
use std::borrow::Cow;
use unicode_segmentation::UnicodeSegmentation;
pub use text::{Alignment, Ellipsize, LineHeight, Shaping, Wrapping};
/// A bunch of text.
@ -65,6 +67,7 @@ where
fragment: text::Fragment<'a>,
format: Format<Renderer::Font>,
class: Theme::Class<'a>,
selectable: bool,
}
impl<'a, Theme, Renderer> Text<'a, Theme, Renderer>
@ -79,6 +82,7 @@ where
fragment: fragment.into_fragment(),
format: Format::default(),
class: Theme::default(),
selectable: false,
}
}
@ -189,7 +193,17 @@ where
{
let color = color.map(Into::into);
self.style(move |_theme| Style { color })
self.style(move |_theme| Style {
color,
..Style::default()
})
}
/// Makes the [`Text`] selectable. When enabled, the user can click and
/// drag to select text, and copy it with Ctrl+C / Cmd+C.
pub fn selectable(mut self) -> Self {
self.selectable = true;
self
}
/// Sets the style class of the [`Text`].
@ -202,7 +216,116 @@ where
}
/// The internal state of a [`Text`] widget.
pub type State<P> = paragraph::Plain<P>;
pub struct State<P: Paragraph> {
/// The cached paragraph layout.
pub paragraph: paragraph::Plain<P>,
/// Lazily allocated when text is selectable and first interacted with.
selection: Option<Box<SelectionState>>,
focused: bool,
keyboard_focused: bool,
context_menu_position: Option<Point>,
clipboard_has_text: bool,
}
impl<P: Paragraph> Default for State<P> {
fn default() -> Self {
Self {
paragraph: paragraph::Plain::default(),
selection: None,
focused: false,
keyboard_focused: false,
context_menu_position: None,
clipboard_has_text: false,
}
}
}
impl<P: Paragraph> std::fmt::Debug for State<P> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("State")
.field("selection", &self.selection)
.field("focused", &self.focused)
.finish_non_exhaustive()
}
}
impl<P: Paragraph> State<P> {
/// Returns `true` if the widget currently has focus.
pub fn is_focused(&self) -> bool {
self.focused
}
/// Returns `true` if focus was gained via keyboard navigation (Tab).
pub fn is_keyboard_focused(&self) -> bool {
self.keyboard_focused
}
/// Clears focus, selection, and all interaction state.
pub fn clear_focus(&mut self) {
self.focused = false;
self.keyboard_focused = false;
self.context_menu_position = None;
if let Some(sel) = &mut self.selection {
sel.clear();
}
}
/// Returns the context menu position, if a context menu should be shown.
pub fn context_menu_position(&self) -> Option<Point> {
self.context_menu_position
}
/// Sets or clears the context menu position.
pub fn set_context_menu_position(&mut self, pos: Option<Point>) {
self.context_menu_position = pos;
}
}
impl<P: Paragraph> std::ops::Deref for State<P> {
type Target = paragraph::Plain<P>;
fn deref(&self) -> &paragraph::Plain<P> {
&self.paragraph
}
}
impl<P: Paragraph> std::ops::DerefMut for State<P> {
fn deref_mut(&mut self) -> &mut paragraph::Plain<P> {
&mut self.paragraph
}
}
#[derive(Debug, Clone, Default)]
struct SelectionState {
anchor: usize,
end: usize,
dragging: bool,
modifiers: keyboard::Modifiers,
last_click: Option<click::Click>,
}
impl SelectionState {
fn clear(&mut self) {
self.anchor = 0;
self.end = 0;
self.dragging = false;
}
}
impl<P: Paragraph> crate::widget::operation::Focusable for State<P> {
fn is_focused(&self) -> bool {
self.focused
}
fn focus(&mut self) {
self.focused = true;
self.keyboard_focused = true;
}
fn unfocus(&mut self) {
self.clear_focus();
}
}
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Text<'_, Theme, Renderer>
@ -215,7 +338,7 @@ where
}
fn state(&self) -> tree::State {
tree::State::new(paragraph::Plain::<Renderer::Paragraph>::default())
tree::State::new(State::<Renderer::Paragraph>::default())
}
fn size(&self) -> Size<Length> {
@ -231,8 +354,10 @@ where
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
layout(
tree.state.downcast_mut::<State<Renderer::Paragraph>>(),
&mut state.paragraph,
renderer,
limits,
&self.fragment,
@ -252,25 +377,372 @@ where
) {
let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>();
let style = theme.style(&self.class);
draw(
renderer,
defaults,
layout.bounds(),
state.raw(),
style,
viewport,
let bounds = layout.bounds();
let paragraph = state.paragraph.raw();
let anchor = bounds.anchor(
paragraph.min_bounds(),
paragraph.align_x(),
paragraph.align_y(),
);
let rects: Vec<Rectangle> = state
.selection
.as_ref()
.filter(|sel| sel.anchor != sel.end)
.map(|sel| {
let content: &str = self.fragment.as_ref();
let lo_byte =
grapheme_to_byte(content, sel.anchor.min(sel.end));
let hi_byte =
grapheme_to_byte(content, sel.anchor.max(sel.end));
paragraph
.highlight(
0,
(lo_byte, text::Affinity::After),
(hi_byte, text::Affinity::Before),
)
.into_iter()
.map(|r| Rectangle {
x: anchor.x + r.x,
y: anchor.y + r.y,
width: r.width,
height: r.height,
})
.collect()
})
.unwrap_or_default();
let fill_selection = |renderer: &mut Renderer| {
for r in &rects {
renderer.fill_quad(
renderer::Quad {
bounds: *r,
..renderer::Quad::default()
},
style.selected_fill,
);
}
};
if style.selected_text_color.is_none() {
fill_selection(renderer);
}
draw(renderer, defaults, bounds, paragraph, style, viewport);
if let Some(color) = style.selected_text_color {
fill_selection(renderer);
for r in &rects {
renderer.with_layer(*r, |renderer| {
renderer
.fill_paragraph(paragraph, anchor, color, *viewport);
});
}
}
}
fn update(
&mut self,
tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
_renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
_viewport: &Rectangle,
) {
if !self.selectable {
return;
}
let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
let bounds = layout.bounds();
let content: &str = self.fragment.as_ref();
let grapheme_count = content.graphemes(true).count();
let paragraph = state.paragraph.raw();
// Any click outside this widget clears selection and focus.
if matches!(
event,
Event::Mouse(mouse::Event::ButtonPressed(_))
| Event::Touch(touch::Event::FingerPressed { .. })
) {
if cursor.position_over(bounds).is_none() {
let was_visible = state.focused
|| state.keyboard_focused
|| state
.selection
.as_ref()
.is_some_and(|sel| sel.anchor != sel.end);
state.focused = false;
state.keyboard_focused = false;
if let Some(sel) = &mut state.selection {
sel.clear();
}
if was_visible {
shell.request_redraw();
}
}
}
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
if state.context_menu_position.take().is_some() {
shell.capture_event();
return;
}
if let Some(pos) = cursor.position_over(bounds) {
let sel = state.selection.get_or_insert_with(|| {
Box::new(SelectionState::default())
});
let anchor = bounds.anchor(
paragraph.min_bounds(),
paragraph.align_x(),
paragraph.align_y(),
);
let relative =
Point::new(pos.x - anchor.x, pos.y - anchor.y);
let grapheme_pos =
hit_to_grapheme(paragraph, relative, content);
let new_click = click::Click::new(
pos,
mouse::Button::Left,
sel.last_click.take(),
);
match new_click.kind() {
click::Kind::Single => {
if sel.modifiers.shift() {
sel.end = grapheme_pos;
} else {
sel.anchor = grapheme_pos;
sel.end = grapheme_pos;
}
sel.dragging = true;
}
click::Kind::Double => {
sel.anchor =
previous_start_of_word(content, grapheme_pos);
sel.end = next_end_of_word(content, grapheme_pos);
sel.dragging = true;
}
click::Kind::Triple => {
sel.anchor = 0;
sel.end = grapheme_count;
sel.dragging = true;
}
}
sel.last_click = Some(new_click);
state.focused = true;
state.keyboard_focused = false;
shell.capture_event();
shell.request_redraw();
}
}
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) => {
if let Some(pos) = cursor.position_over(bounds) {
state.context_menu_position = Some(pos);
state.clipboard_has_text = clipboard_has_text(clipboard);
state.focused = true;
state.keyboard_focused = false;
shell.capture_event();
shell.request_redraw();
}
}
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
| Event::Touch(touch::Event::FingerLifted { .. }) => {
if let Some(sel) = &mut state.selection {
sel.dragging = false;
}
}
Event::Mouse(mouse::Event::CursorMoved { position })
| Event::Touch(touch::Event::FingerMoved { position, .. }) => {
if let Some(sel) = &mut state.selection {
if sel.dragging {
let anchor = bounds.anchor(
paragraph.min_bounds(),
paragraph.align_x(),
paragraph.align_y(),
);
let relative = Point::new(
position.x - anchor.x,
position.y - anchor.y,
);
let end = hit_to_grapheme(paragraph, relative, content);
// Only the drag reaching a new grapheme changes what
// is drawn; plain motion inside one does not.
if end != sel.end {
sel.end = end;
shell.request_redraw();
}
shell.capture_event();
}
}
}
Event::Keyboard(keyboard::Event::KeyPressed {
key,
modifiers,
physical_key,
text: _,
..
}) => {
if !state.focused {
return;
}
let sel = state
.selection
.get_or_insert_with(|| Box::new(SelectionState::default()));
if modifiers.command() {
match key.to_latin(*physical_key) {
Some('c') => {
let left = sel.anchor.min(sel.end);
let right = sel.anchor.max(sel.end);
if left != right {
let selected: String = content
.graphemes(true)
.skip(left)
.take(right - left)
.collect();
clipboard.write(
crate::clipboard::Kind::Standard,
selected,
);
}
shell.capture_event();
return;
}
Some('a') => {
sel.anchor = 0;
sel.end = grapheme_count;
shell.capture_event();
shell.request_redraw();
return;
}
_ => {}
}
}
match key {
keyboard::Key::Named(keyboard::key::Named::ArrowLeft) => {
let by_word = is_jump_modifier(*modifiers);
if modifiers.shift() {
sel.end = if by_word {
previous_start_of_word(content, sel.end)
} else {
sel.end.saturating_sub(1)
};
} else {
let left = sel.anchor.min(sel.end);
let pos = if by_word {
previous_start_of_word(content, left)
} else {
left.saturating_sub(1)
};
sel.anchor = pos;
sel.end = pos;
}
shell.capture_event();
shell.request_redraw();
}
keyboard::Key::Named(keyboard::key::Named::ArrowRight) => {
let by_word = is_jump_modifier(*modifiers);
if modifiers.shift() {
sel.end = if by_word {
next_end_of_word(content, sel.end)
} else {
(sel.end + 1).min(grapheme_count)
};
} else {
let right = sel.anchor.max(sel.end);
let pos = if by_word {
next_end_of_word(content, right)
} else {
(right + 1).min(grapheme_count)
};
sel.anchor = pos;
sel.end = pos;
}
shell.capture_event();
shell.request_redraw();
}
keyboard::Key::Named(keyboard::key::Named::Home) => {
if modifiers.shift() {
sel.end = 0;
} else {
sel.anchor = 0;
sel.end = 0;
}
shell.capture_event();
shell.request_redraw();
}
keyboard::Key::Named(keyboard::key::Named::End) => {
if modifiers.shift() {
sel.end = grapheme_count;
} else {
sel.anchor = grapheme_count;
sel.end = grapheme_count;
}
shell.capture_event();
shell.request_redraw();
}
_ => {}
}
}
Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
if let Some(sel) = &mut state.selection {
sel.modifiers = *modifiers;
}
}
_ => {}
}
}
fn mouse_interaction(
&self,
_tree: &Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
if self.selectable && cursor.is_over(layout.bounds()) {
mouse::Interaction::Text
} else {
mouse::Interaction::None
}
}
fn operate(
&mut self,
_tree: &mut Tree,
tree: &mut Tree,
layout: Layout<'_>,
_renderer: &Renderer,
operation: &mut dyn super::Operation,
) {
operation.text(None, layout.bounds(), &self.fragment);
if self.selectable {
let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
operation.focusable(None, layout.bounds(), state);
}
}
#[cfg(feature = "a11y")]
@ -471,12 +943,28 @@ where
}
/// The appearance of some text.
#[derive(Debug, Clone, Copy, PartialEq, Default)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Style {
/// The [`Color`] of the text.
///
/// The default, `None`, means using the inherited color.
pub color: Option<Color>,
/// The fill [`Color`] of the selection highlight.
pub selected_fill: Color,
/// The [`Color`] of selected text.
///
/// The default, `None`, keeps the regular text color.
pub selected_text_color: Option<Color>,
}
impl Default for Style {
fn default() -> Self {
Self {
color: None,
selected_fill: DEFAULT_SELECTION_COLOR,
selected_text_color: None,
}
}
}
/// The theme catalog of a [`Text`].
@ -510,13 +998,14 @@ impl Catalog for Theme {
/// The default text styling; color is inherited.
pub fn default(_theme: &Theme) -> Style {
Style { color: None }
Style::default()
}
/// Text with the default base color.
pub fn base(theme: &Theme) -> Style {
Style {
color: Some(theme.palette().text),
..Style::default()
}
}
@ -524,6 +1013,7 @@ pub fn base(theme: &Theme) -> Style {
pub fn primary(theme: &Theme) -> Style {
Style {
color: Some(theme.palette().primary),
..Style::default()
}
}
@ -531,6 +1021,7 @@ pub fn primary(theme: &Theme) -> Style {
pub fn secondary(theme: &Theme) -> Style {
Style {
color: Some(theme.extended_palette().secondary.base.color),
..Style::default()
}
}
@ -538,6 +1029,7 @@ pub fn secondary(theme: &Theme) -> Style {
pub fn success(theme: &Theme) -> Style {
Style {
color: Some(theme.palette().success),
..Style::default()
}
}
@ -545,6 +1037,7 @@ pub fn success(theme: &Theme) -> Style {
pub fn warning(theme: &Theme) -> Style {
Style {
color: Some(theme.palette().warning),
..Style::default()
}
}
@ -552,5 +1045,206 @@ pub fn warning(theme: &Theme) -> Style {
pub fn danger(theme: &Theme) -> Style {
Style {
color: Some(theme.palette().danger),
..Style::default()
}
}
use crate::widget::tree::Tree as WidgetTree;
/// Returns `true` if the clipboard currently holds non-empty text.
pub fn clipboard_has_text(clipboard: &dyn Clipboard) -> bool {
clipboard
.read(crate::clipboard::Kind::Standard)
.is_some_and(|s| !s.is_empty())
}
/// Implement this on a **widget** to enable context menu support for
/// text selection (Copy, Select All, and optionally Cut / Paste) in libcosmic
pub trait HasSelectableText {
/// Returns the currently selected text, if any.
fn selected_text(&self, tree: &WidgetTree) -> Option<String>;
/// Selects all text.
fn select_all(&self, tree: &mut WidgetTree);
/// Returns `true` if the widget is editable (shows Cut / Paste).
fn is_editable(&self) -> bool {
false
}
/// Returns `true` if the widget has any text (enables Select All).
fn has_text(&self, _tree: &WidgetTree) -> bool {
true
}
/// Returns whether the clipboard held text when the context menu was
/// requested (enables Paste). Widgets cache this on right-click because
/// overlays have no clipboard access.
fn clipboard_has_text(&self, _tree: &WidgetTree) -> bool {
true
}
/// Returns `true` if the widget is currently focused.
fn is_focused(&self, tree: &WidgetTree) -> bool;
/// Returns the position where the context menu should appear
fn context_menu_position(&self, tree: &WidgetTree) -> Option<Point>;
/// Sets or clears the context menu position.
fn set_context_menu_position(
&self,
tree: &mut WidgetTree,
pos: Option<Point>,
);
/// Copies the selection to the clipboard.
fn copy_to_clipboard(
&self,
tree: &WidgetTree,
clipboard: &mut dyn Clipboard,
) {
if let Some(text) = self.selected_text(tree) {
clipboard.write(crate::clipboard::Kind::Standard, text);
}
}
/// Deletes the selected text and returns the new full content.
/// Only called when [`is_editable`](Self::is_editable) is `true`.
fn delete_selection(&self, _tree: &mut WidgetTree) -> Option<String> {
None
}
/// Inserts `text` at the cursor (replacing any selection) and returns
/// the new full content.
/// Only called when [`is_editable`](Self::is_editable) is `true`.
fn paste_text(
&self,
_tree: &mut WidgetTree,
_text: &str,
) -> Option<String> {
None
}
}
impl<Theme: Catalog, Renderer: text::Renderer> HasSelectableText
for Text<'_, Theme, Renderer>
{
fn selected_text(&self, tree: &WidgetTree) -> Option<String> {
let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>();
let sel = state.selection.as_ref()?;
let left = sel.anchor.min(sel.end);
let right = sel.anchor.max(sel.end);
if left == right {
return None;
}
let content = state.paragraph.content();
let lo = grapheme_to_byte(content, left);
let hi = grapheme_to_byte(content, right);
content.get(lo..hi).map(|s| s.to_owned())
}
fn select_all(&self, tree: &mut WidgetTree) {
let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
let count = state.paragraph.content().graphemes(true).count();
let sel = state
.selection
.get_or_insert_with(|| Box::new(SelectionState::default()));
sel.anchor = 0;
sel.end = count;
}
fn has_text(&self, tree: &WidgetTree) -> bool {
let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>();
!state.paragraph.content().is_empty()
}
fn clipboard_has_text(&self, tree: &WidgetTree) -> bool {
let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>();
state.clipboard_has_text
}
fn is_focused(&self, tree: &WidgetTree) -> bool {
let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>();
state.is_focused()
}
fn context_menu_position(&self, tree: &WidgetTree) -> Option<Point> {
let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>();
state.context_menu_position()
}
fn set_context_menu_position(
&self,
tree: &mut WidgetTree,
pos: Option<Point>,
) {
let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
state.set_context_menu_position(pos);
}
}
const DEFAULT_SELECTION_COLOR: Color = Color {
r: 0.0,
g: 0.47,
b: 0.84,
a: 0.3,
};
fn grapheme_to_byte(content: &str, grapheme_index: usize) -> usize {
content
.graphemes(true)
.take(grapheme_index)
.map(|g| g.len())
.sum()
}
fn hit_to_grapheme<P: Paragraph>(
paragraph: &P,
point: Point,
content: &str,
) -> usize {
match paragraph.hit_test(point) {
Some(hit) => {
let byte_offset = hit.cursor().min(content.len());
content[..byte_offset].graphemes(true).count()
}
None => content.graphemes(true).count(),
}
}
fn previous_start_of_word(content: &str, grapheme_index: usize) -> usize {
let graphemes: Vec<&str> = content.graphemes(true).collect();
let clamped = grapheme_index.min(graphemes.len());
let before: String = graphemes[..clamped].concat();
UnicodeSegmentation::split_word_bound_indices(&*before)
.filter(|(_, word)| !word.trim_start().is_empty())
.next_back()
.map_or(0, |(i, prev_word)| {
clamped
- prev_word.graphemes(true).count()
- before[i + prev_word.len()..].graphemes(true).count()
})
}
fn next_end_of_word(content: &str, grapheme_index: usize) -> usize {
let graphemes: Vec<&str> = content.graphemes(true).collect();
let clamped = grapheme_index.min(graphemes.len());
let after: String = graphemes[clamped..].concat();
UnicodeSegmentation::split_word_bound_indices(&*after)
.find(|(_, word)| !word.trim_start().is_empty())
.map_or(graphemes.len(), |(i, next_word)| {
clamped
+ next_word.graphemes(true).count()
+ after[..i].graphemes(true).count()
})
}
fn is_jump_modifier(modifiers: keyboard::Modifiers) -> bool {
if cfg!(target_os = "macos") {
modifiers.alt()
} else {
modifiers.control()
}
}

View file

@ -1,5 +1,5 @@
use iced::widget::{button, column, text, Column};
use iced::Center;
use iced::widget::{Column, button, column, text};
pub fn main() -> iced::Result {
iced::run(Counter::update, Counter::view)
@ -42,7 +42,7 @@ impl Counter {
#[cfg(test)]
mod tests {
use super::*;
use iced_test::{Error, simulator};
use iced_test::{simulator, Error};
#[test]
fn it_counts() -> Result<(), Error> {

View file

@ -140,7 +140,7 @@ impl Pipeline {
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::MipmapFilterMode::Linear,
..Default::default()
});
@ -223,7 +223,7 @@ impl Pipeline {
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("cubes pipeline layout"),
bind_group_layouts: &[&uniform_bind_group_layout],
push_constant_ranges: &[],
immediate_size: 0,
});
let shader =
@ -280,7 +280,7 @@ impl Pipeline {
compilation_options:
wgpu::PipelineCompilationOptions::default(),
}),
multiview: None,
multiview_mask: None,
cache: None,
});
@ -388,6 +388,7 @@ impl Pipeline {
),
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
pass.set_viewport(
@ -478,7 +479,7 @@ impl DepthPipeline {
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("cubes.depth_pipeline.layout"),
bind_group_layouts: &[&bind_group_layout],
push_constant_ranges: &[],
immediate_size: 0,
});
let shader =
@ -520,7 +521,7 @@ impl DepthPipeline {
compilation_options:
wgpu::PipelineCompilationOptions::default(),
}),
multiview: None,
multiview_mask: None,
cache: None,
});
@ -586,6 +587,7 @@ impl DepthPipeline {
),
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
pass.set_viewport(

View file

@ -43,6 +43,7 @@ impl Scene {
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
})
}
@ -64,8 +65,8 @@ fn build_pipeline(
let pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: None,
push_constant_ranges: &[],
bind_group_layouts: &[],
immediate_size: 0,
});
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
@ -101,7 +102,7 @@ fn build_pipeline(
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview: None,
multiview_mask: None,
cache: None,
})
}

View file

@ -3,20 +3,20 @@ use std::{
sync::atomic::{AtomicU64, Ordering},
};
use iced::{Element, id::Id};
use iced::{
Event, Length, Rectangle,
clipboard::{
dnd::{self, DndAction, DndDestinationRectangle, DndEvent, OfferEvent},
mime::AllowedMimeTypes,
},
event,
id::Internal,
mouse, overlay,
mouse, overlay, Event, Length, Rectangle,
};
use iced::{id::Id, Element};
use iced_core::{
self, Clipboard, Layout, Shell, Widget, layout,
widget::{Tree, tree},
self, layout,
widget::{tree, Tree},
Clipboard, Layout, Shell, Widget,
};
pub fn dnd_destination<'a, Message: 'static>(

View file

@ -1,16 +1,16 @@
use std::any::Any;
use iced::Element;
use iced::id::Id;
use iced::widget::container;
use iced::Element;
use iced::{
Event, Length, Point, Rectangle,
clipboard::dnd::{DndAction, DndEvent, SourceEvent},
event, mouse, overlay,
event, mouse, overlay, Event, Length, Point, Rectangle,
};
use iced_core::{
Clipboard, Shell, layout, renderer,
widget::{Tree, tree},
layout, renderer,
widget::{tree, Tree},
Clipboard, Shell,
};
use iced_core::{Layout, Widget};
@ -37,11 +37,11 @@ pub struct DndSource<'a, Message, AppMessage, D> {
}
impl<
'a,
Message: 'static,
AppMessage: 'static,
D: iced::clipboard::mime::AsMimeTypes + std::marker::Send + 'static,
> DndSource<'a, Message, AppMessage, D>
'a,
Message: 'static,
AppMessage: 'static,
D: iced::clipboard::mime::AsMimeTypes + std::marker::Send + 'static,
> DndSource<'a, Message, AppMessage, D>
{
pub fn new(child: impl Into<Element<'a, Message>>) -> Self {
Self {
@ -119,11 +119,11 @@ impl<
}
impl<
'a,
Message: 'static,
AppMessage: 'static,
D: iced::clipboard::mime::AsMimeTypes + std::marker::Send + 'static,
> Widget<Message, iced::Theme, iced::Renderer>
'a,
Message: 'static,
AppMessage: 'static,
D: iced::clipboard::mime::AsMimeTypes + std::marker::Send + 'static,
> Widget<Message, iced::Theme, iced::Renderer>
for DndSource<'a, Message, AppMessage, D>
{
fn children(&self) -> Vec<Tree> {
@ -357,11 +357,11 @@ impl<
}
impl<
'a,
Message: 'static,
AppMessage: 'static,
D: iced::clipboard::mime::AsMimeTypes + std::marker::Send + 'static,
> From<DndSource<'a, Message, AppMessage, D>> for Element<'a, Message>
'a,
Message: 'static,
AppMessage: 'static,
D: iced::clipboard::mime::AsMimeTypes + std::marker::Send + 'static,
> From<DndSource<'a, Message, AppMessage, D>> for Element<'a, Message>
{
fn from(e: DndSource<'a, Message, AppMessage, D>) -> Element<'a, Message> {
Element::new(e)

View file

@ -7,7 +7,6 @@ use cctk::sctk::reexports::{
use iced::platform_specific::shell::commands::subsurface::get_subsurface;
use iced::{
Element, Length, Subscription, Task,
event::wayland::Event as WaylandEvent,
platform_specific::{
runtime::wayland::subsurface::SctkSubsurfaceSettings,
@ -15,6 +14,7 @@ use iced::{
},
widget::{button, column, text, text_input},
window::{self, Id, Settings},
Element, Length, Subscription, Task,
};
use std::sync::{Arc, Mutex};

View file

@ -477,6 +477,7 @@ fn delete_icon() -> Text<'static> {
fn subtle(theme: &Theme) -> text::Style {
text::Style {
color: Some(theme.extended_palette().background.strongest.color),
..text::Style::default()
}
}

View file

@ -1,9 +1,9 @@
//! Load and operate on images.
#[cfg(feature = "image")]
use crate::core::Bytes;
#[cfg(feature = "image")]
use crate::core::Size;
use crate::core::Color;
use crate::core::Radians;
use crate::core::Rectangle;
use crate::core::image;
use crate::core::svg;
@ -180,3 +180,105 @@ fn to_error(error: ::image::ImageError) -> image::Error {
error => image::Error::Invalid(Arc::new(error)),
}
}
/// resample a raster image to this target
#[cfg(feature = "image")]
pub fn downsample_target(
native: Size<u32>,
bounds: Size<f32>,
) -> Option<Size<u32>> {
if !(bounds.width >= 1.0 && bounds.height >= 1.0) {
return None;
}
// Round up to 4px so an animated resize does not make a copy per pixel
// TODO: maybe find a better approach
let quantize = |length: f32| (length.ceil() as u32).div_ceil(4) * 4;
let target = Size::new(
quantize(bounds.width).min(native.width),
quantize(bounds.height).min(native.height),
);
let minified = native.width as f32 >= target.width as f32 * 1.25
|| native.height as f32 >= target.height as f32 * 1.25;
// If the target is big then Lanczos will be too expensive, and sampler is good enough
let small = u64::from(target.width) * u64::from(target.height) <= 1 << 18;
(minified && small).then_some(target)
}
/// Resamples premultiplied pixels down to `target`.
#[cfg(feature = "image")]
pub fn downsample_premultiplied(
pixels: &[u8],
size: Size<u32>,
target: Size<u32>,
) -> Vec<u8> {
let image =
::image::RgbaImage::from_raw(size.width, size.height, pixels.to_vec())
.expect("pixels hold width * height RGBA pixels");
resize(&image, target).into_raw()
}
#[cfg(feature = "image")]
fn resize(image: &::image::RgbaImage, target: Size<u32>) -> ::image::RgbaImage {
use ::image::imageops::{self, FilterType};
use std::borrow::Cow;
// if image is too big compared to target, box average it to double the target
// and then do Lanczos. The final result is almost the same, but lanczos is too
// expensive on large images.
let image = if image.width() >= target.width * 4
&& image.height() >= target.height * 4
{
Cow::Owned(imageops::thumbnail(
image,
target.width * 2,
target.height * 2,
))
} else {
Cow::Borrowed(image)
};
imageops::resize(&*image, target.width, target.height, FilterType::Lanczos3)
}
/// Resamples RGBA pixels down to `target`.
///
/// Premultiplies the image, to avoid fringing the edges of an icon when interpolating
/// transparent pixels.
#[cfg(feature = "image")]
pub fn downsample(image: &Buffer, target: Size<u32>) -> ::image::RgbaImage {
let mut image = ::image::RgbaImage::from_raw(
image.width(),
image.height(),
image.as_raw().to_vec(),
)
.expect("buffer holds width * height RGBA pixels");
for pixel in image.pixels_mut() {
let alpha = u32::from(pixel[3]);
for channel in &mut pixel.0[..3] {
*channel = ((u32::from(*channel) * alpha + 127) / 255) as u8;
}
}
let mut image = resize(&image, target);
for pixel in image.pixels_mut() {
let alpha = u32::from(pixel[3]);
if alpha > 0 {
for channel in &mut pixel.0[..3] {
*channel = ((u32::from(*channel) * 255 + alpha / 2) / alpha)
.min(255) as u8;
}
}
}
image
}

View file

@ -83,13 +83,20 @@ impl<T: Layer> Stack<T> {
/// Pushes a new clipping region in the [`Stack`]; creating a new layer in the
/// process.
pub fn push_clip(&mut self, bounds: Rectangle) {
let bounds = bounds * self.transformation();
let bounds = bounds
.intersection(&self.layers[self.current].bounds())
.unwrap_or(Rectangle {
width: 0.0,
height: 0.0,
..bounds
});
self.previous.push(self.current);
self.current = self.active_count;
self.active_count += 1;
let bounds = bounds * self.transformation();
if self.current == self.layers.len() {
self.layers.push(T::with_bounds(bounds));
} else {

View file

@ -161,6 +161,14 @@ impl FontSystem {
&mut self.raw
}
/// Returns a reference to the underlying [`fontdb::Database`].
///
/// Unlike going through [`raw`](Self::raw) and `db_mut`, this does not
/// clear `cosmic-text`'s internal font-match cache.
pub fn db(&self) -> &cosmic_text::fontdb::Database {
self.raw.db()
}
/// Loads a font from its bytes.
pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) {
if let Cow::Borrowed(bytes) = bytes {

View file

@ -336,6 +336,10 @@ impl editor::Editor for Editor {
}
}
Action::ClearSelection => {
editor.set_selection(cosmic_text::Selection::None);
}
// Editing events
Action::Edit(edit) => {
let topmost_line_before_edit = editor

View file

@ -17,7 +17,7 @@ workspace = true
debug = []
selector = ["dep:iced_selector"]
a11y = ["iced_accessibility", "iced_core/a11y"]
cctk = ["iced_core/cctk", "dep:cctk"]
wayland = ["iced_core/wayland", "dep:cctk"]
[dependencies]
bytes.workspace = true
@ -33,9 +33,14 @@ sipper.optional = true
iced_selector.workspace = true
iced_selector.optional = true
iced_futures.features = ["thread-pool"]
cctk.workspace = true
cctk.optional = true
iced_accessibility.workspace = true
iced_accessibility.optional = true
window_clipboard.workspace = true
dnd.workspace = true
[target.'cfg(all(unix, not(any(target_os = "redox", target_family = "wasm", target_os = "android", target_vendor = "apple"))))'.dependencies]
cctk.workspace = true
cctk.optional = true
[build-dependencies]
build_helpers = { path = "../build_helpers" }

3
runtime/build.rs Normal file
View file

@ -0,0 +1,3 @@
fn main() {
build_helpers::cfg_aliases_setup();
}

View file

@ -1,12 +1,10 @@
//! Access the clipboard.
use std::any::Any;
use dnd::{DndDestinationRectangle, DndSurface};
use iced_core::clipboard::DndSource;
use window_clipboard::mime::{AllowedMimeTypes, AsMimeTypes};
use window_clipboard::mime::AllowedMimeTypes;
use crate::{oneshot, task, Action, Task};
use crate::{Action, Task, oneshot, task};
/// An action to be performed on the system.
pub enum DndAction {

View file

@ -28,7 +28,6 @@ pub use task::Task;
pub use user_interface::UserInterface;
pub use window::Window;
use crate::core::Color;
use crate::futures::futures::channel::oneshot;
use std::borrow::Cow;

View file

@ -2,23 +2,23 @@
use std::fmt;
#[cfg(all(feature = "cctk", target_os = "linux"))]
#[cfg(wayland_platform)]
/// Platform specific actions defined for wayland
pub mod wayland;
/// Platform specific actions defined for wayland
pub enum Action {
/// Wayland Specific Actions
#[cfg(all(feature = "cctk", target_os = "linux"))]
#[cfg(wayland_platform)]
Wayland(wayland::Action),
}
impl fmt::Debug for Action {
fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
#[cfg(all(feature = "cctk", target_os = "linux"))]
#[cfg(wayland_platform)]
Action::Wayland(action) => action.fmt(_f),
#[cfg(not(all(feature = "cctk", target_os = "linux")))]
#[cfg(not(wayland_platform))]
_ => Ok(()),
}
}

View file

@ -36,7 +36,7 @@ impl fmt::Debug for Action {
f,
"Action::ActivationAction::Activate {{ window: {:?}, token: {:?} }}",
window, token,
)
),
}
}
}

View file

@ -6,8 +6,8 @@ use cctk::sctk::{
};
use iced_core::layout::Limits;
use iced_core::window::Id;
use iced_core::Rectangle;
use iced_core::window::Id;
/// output for layer surface
#[derive(Debug, Clone)]
@ -150,6 +150,18 @@ pub enum Action {
/// layer of the layer surface
layer: Layer,
},
/// padding of the surface geometry
Padding {
/// id of the layer surface
id: Id,
/// padding of the surface geometry
padding: IcedMargin,
},
ShowOnLock {
/// id of the layer surface
id: Id,
value: bool,
}
}
impl fmt::Debug for Action {
@ -162,35 +174,56 @@ impl fmt::Debug for Action {
),
Action::Size { id, width, height } => write!(
f,
"Action::LayerSurfaceAction::Size {{ id: {:#?}, width: {:?}, height: {:?} }}", id, width, height
"Action::LayerSurfaceAction::Size {{ id: {:#?}, width: {:?}, height: {:?} }}",
id, width, height
),
Action::Destroy(id) => write!(
f,
"Action::LayerSurfaceAction::Destroy {{ id: {:#?} }}", id
"Action::LayerSurfaceAction::Destroy {{ id: {:#?} }}",
id
),
Action::Anchor { id, anchor } => write!(
f,
"Action::LayerSurfaceAction::Anchor {{ id: {:#?}, anchor: {:?} }}", id, anchor
"Action::LayerSurfaceAction::Anchor {{ id: {:#?}, anchor: {:?} }}",
id, anchor
),
Action::ExclusiveZone { id, exclusive_zone } => write!(
f,
"Action::LayerSurfaceAction::ExclusiveZone {{ id: {:#?}, exclusive_zone: {exclusive_zone} }}", id
"Action::LayerSurfaceAction::ExclusiveZone {{ id: {:#?}, exclusive_zone: {exclusive_zone} }}",
id
),
Action::Margin { id, margin } => write!(
f,
"Action::LayerSurfaceAction::Margin {{ id: {:#?}, margin: {:?} }}", id, margin
"Action::LayerSurfaceAction::Margin {{ id: {:#?}, margin: {:?} }}",
id, margin
),
Action::KeyboardInteractivity { id, keyboard_interactivity } => write!(
Action::KeyboardInteractivity {
id,
keyboard_interactivity,
} => write!(
f,
"Action::LayerSurfaceAction::KeyboardInteractivity {{ id: {:#?}, keyboard_interactivity: {:?} }}", id, keyboard_interactivity
"Action::LayerSurfaceAction::KeyboardInteractivity {{ id: {:#?}, keyboard_interactivity: {:?} }}",
id, keyboard_interactivity
),
Action::InputZone { id, zone } => write!(
f,
"Action::LayerSurfaceAction::InputZone {{ id: {:#?}, zone: {:?} }}", id, zone
"Action::LayerSurfaceAction::InputZone {{ id: {:#?}, zone: {:?} }}",
id, zone
),
Action::Layer { id, layer } => write!(
f,
"Action::LayerSurfaceAction::Layer {{ id: {:#?}, layer: {:?} }}", id, layer
"Action::LayerSurfaceAction::Layer {{ id: {:#?}, layer: {:?} }}",
id, layer
),
Action::Padding { id, padding } => write!(
f,
"Action::LayerSurfaceAction::Padding {{ id: {:#?}, padding: {:?} }}",
id, padding
),
Action::ShowOnLock { id, value } => write!(
f,
"Action::LayerSurfaceAction::ShowOnLock {{ id: {:#?}, value: {:?} }}",
id, value
),
}
}

View file

@ -124,26 +124,34 @@ pub enum Action {
/// height
height: u32,
},
/// set the size of the popup
Reposition {
/// id of the popup
id: Id,
/// positioner of the popup
positioner: SctkPositioner,
},
}
impl fmt::Debug for Action {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Action::Popup { popup, .. } => write!(
f,
"Action::PopupAction::Popup {{ popup: {:?} }}",
popup
),
Action::Destroy { id } => write!(
f,
"Action::PopupAction::Destroy {{ id: {:?} }}",
id
),
Action::Popup { popup, .. } => {
write!(f, "Action::PopupAction::Popup {{ popup: {:?} }}", popup)
}
Action::Destroy { id } => {
write!(f, "Action::PopupAction::Destroy {{ id: {:?} }}", id)
}
Action::Size { id, width, height } => write!(
f,
"Action::PopupAction::Size {{ id: {:?}, width: {:?}, height: {:?} }}",
id, width, height
),
Action::Reposition { id, positioner } => write!(
f,
"Action::Reposition {{ id: {:?}, positioner: {:?} }}",
id, positioner
),
}
}
}

View file

@ -1,6 +1,6 @@
//! Find and query widgets in your applications.
pub use iced_selector::{
Bounded, Candidate, Selector, Target, Text, id, is_focused,
Bounded, Candidate, Selector, Target, Text, focus, id, is_focused,
};
use crate::Task;

View file

@ -222,7 +222,7 @@ pub fn frames() -> Subscription<(Id, Instant)> {
_ => None,
})
}
#[cfg(all(feature = "cctk", target_os = "linux"))]
#[cfg(wayland_platform)]
/// Subscribes to the frames of the window of the running application.
///
/// The resulting [`Subscription`] will produce items at a rate equal to the

View file

@ -1,3 +1,5 @@
use iced_core::window;
use crate::Selector;
use crate::core::widget::operation::{
Focusable, Outcome, Scrollable, TextInput,
@ -100,6 +102,10 @@ where
fn finish(&self) -> Self::Output {
self.outputs.clone()
}
fn set_window_id(&mut self, id: window::Id) {
self.selector.set_window_id(id);
}
}
pub trait Strategy {
@ -110,6 +116,8 @@ pub trait Strategy {
fn is_done(&self) -> bool;
fn finish(&self) -> Self::Output;
fn set_window_id(&mut self, _id: window::Id) {}
}
#[derive(Debug)]
@ -273,4 +281,12 @@ where
fn finish(&self) -> Outcome<S::Output> {
Outcome::Some(self.strategy.finish())
}
fn set_window_id(&mut self, id: window::Id) {
self.strategy.set_window_id(id);
}
fn pre_operation(&mut self, id: Option<&Id>) {
self.strategy.feed(Candidate::PreOperation { id });
}
}

View file

@ -1,5 +1,6 @@
//! Select data from the widget tree.
use iced_core as core;
use iced_core::window;
mod find;
mod target;
@ -48,6 +49,8 @@ pub trait Selector {
{
FindAll::new(find::All::new(self))
}
fn set_window_id(&mut self, _id: window::Id) {}
}
impl Selector for &str {
@ -171,3 +174,42 @@ pub fn is_focused() -> impl Selector<Output = Target> {
IsFocused
}
/// Returns a [`Selector`] that matches widgets that are focusable.
pub fn focus() -> impl Selector<Output = (bool, Target, window::Id)> {
struct Focus {
cur_window_id: window::Id,
}
impl Selector for Focus {
type Output = (bool, Target, window::Id);
fn select(&mut self, candidate: Candidate<'_>) -> Option<Self::Output> {
if let Candidate::Focusable { state, .. } = candidate {
Some((
state.is_focused(),
Target::from(candidate),
self.cur_window_id,
))
} else if matches!(candidate, Candidate::Scrollable { .. }) {
Some((false, Target::from(candidate), self.cur_window_id))
} else if matches!(candidate, Candidate::PreOperation { .. }) {
Some((false, Target::from(candidate), self.cur_window_id))
} else {
None
}
}
fn description(&self) -> String {
"is focusable and focused".to_owned()
}
fn set_window_id(&mut self, id: iced_core::window::Id) {
self.cur_window_id = id;
}
}
Focus {
cur_window_id: window::Id::NONE,
}
}

View file

@ -42,6 +42,9 @@ pub enum Target {
bounds: Rectangle,
visible_bounds: Option<Rectangle>,
},
PreOperation {
id: Option<Id>,
},
}
impl Target {
@ -54,6 +57,7 @@ impl Target {
| Target::TextInput { bounds, .. }
| Target::Text { bounds, .. }
| Target::Custom { bounds, .. } => *bounds,
Target::PreOperation { .. } => Rectangle::default(),
}
}
@ -66,6 +70,7 @@ impl Target {
| Target::TextInput { visible_bounds, .. }
| Target::Text { visible_bounds, .. }
| Target::Custom { visible_bounds, .. } => *visible_bounds,
Target::PreOperation { .. } => None,
}
}
}
@ -138,6 +143,9 @@ impl From<Candidate<'_>> for Target {
bounds,
visible_bounds,
},
Candidate::PreOperation { id } => {
Self::PreOperation { id: id.cloned() }
}
}
}
}
@ -195,6 +203,8 @@ pub enum Candidate<'a> {
visible_bounds: Option<Rectangle>,
state: &'a dyn Any,
},
/// Used to mark the beginning of operations for a widget and its children
PreOperation { id: Option<&'a Id> },
}
impl<'a> Candidate<'a> {
@ -206,7 +216,8 @@ impl<'a> Candidate<'a> {
| Candidate::Scrollable { id, .. }
| Candidate::TextInput { id, .. }
| Candidate::Text { id, .. }
| Candidate::Custom { id, .. } => *id,
| Candidate::Custom { id, .. }
| Candidate::PreOperation { id } => *id,
}
}
@ -219,6 +230,7 @@ impl<'a> Candidate<'a> {
| Candidate::TextInput { bounds, .. }
| Candidate::Text { bounds, .. }
| Candidate::Custom { bounds, .. } => *bounds,
Candidate::PreOperation { .. } => Rectangle::default(),
}
}
@ -231,6 +243,7 @@ impl<'a> Candidate<'a> {
| Candidate::TextInput { visible_bounds, .. }
| Candidate::Text { visible_bounds, .. }
| Candidate::Custom { visible_bounds, .. } => *visible_bounds,
Candidate::PreOperation { .. } => None,
}
}
}

View file

@ -606,7 +606,7 @@ pub mod font {
pub mod event {
//! Handle events of a user interface.
pub use crate::core::event::PlatformSpecific;
#[cfg(all(feature = "cctk", target_os = "linux"))]
#[cfg(wayland_platform)]
pub use crate::core::event::wayland;
pub use crate::core::event::{Event, Status};
pub use iced_futures::event::{listen, listen_raw, listen_with};
@ -683,6 +683,8 @@ pub use event::Event;
pub use executor::Executor;
pub use font::Font;
pub use renderer::Renderer;
#[cfg(feature = "canvas")]
pub use renderer::graphics::geometry;
pub use task::Task;
pub use window::Window;

View file

@ -273,6 +273,11 @@ impl<P: Program + 'static> Emulator<P> {
runtime::Action::Reload => {
// TODO
}
runtime::Action::Dnd(_)
| runtime::Action::PlatformSpecific(_) => {
// Drag and drop and platform surfaces need a
// windowing system, which an emulator has none of.
}
},
}
}
@ -474,7 +479,9 @@ impl<P: Program + 'static> Emulator<P> {
&mut self.renderer,
theme,
&renderer::Style {
icon_color: style.text_color,
text_color: style.text_color,
scale_factor: f64::from(scale_factor),
},
mouse::Cursor::Unavailable,
);

View file

@ -209,17 +209,19 @@ where
&mut self.messages,
);
let scale_factor = 2.0_f32;
self.raw.draw(
&mut self.renderer,
theme,
&core::renderer::Style {
icon_color: base.text_color,
text_color: base.text_color,
scale_factor: f64::from(scale_factor),
},
self.cursor,
);
let scale_factor = 2.0;
let physical_size = Size::new(
(self.size.width * scale_factor).round() as u32,
(self.size.height * scale_factor).round() as u32,

View file

@ -775,6 +775,7 @@ impl<P: Program + 'static> Tester<P> {
}
_ => None,
},
..text::Style::default()
})
.into()
},
@ -945,6 +946,7 @@ where
text(label).size(14).style(|theme: &core::Theme| {
text::Style {
color: Some(theme.extended_palette().background.weak.text),
..text::Style::default()
}
}),
space::horizontal(),

View file

@ -263,6 +263,10 @@ impl Engine {
} else {
// Draw corners that have too small border radii as having no border radius,
// but mask them with the rounded rectangle with the correct border radius.
if path_bounds.width < 1.0 || path_bounds.height < 1.0 {
return;
}
let mut temp_pixmap = tiny_skia::Pixmap::new(
path_bounds.width as u32,
path_bounds.height as u32,

View file

@ -51,7 +51,21 @@ impl Pipeline {
) {
let mut cache = self.cache.borrow_mut();
let Ok(mut image) = cache.allocate(handle) else {
let target = {
let Ok(image) = cache.allocate(handle) else {
return;
};
graphics::image::downsample_target(
Size::new(image.width(), image.height()),
bounds.size(),
)
};
let Ok(mut image) = (match target {
Some(target) => cache.allocate_resampled(handle, target),
None => cache.allocate(handle),
}) else {
return;
};
@ -110,6 +124,8 @@ impl Pipeline {
struct Cache {
entries: FxHashMap<raster::Id, Option<Entry>>,
hits: FxHashSet<raster::Id>,
resampled: FxHashMap<(raster::Id, u32, u32), Entry>,
resampled_hits: FxHashSet<(raster::Id, u32, u32)>,
}
impl Cache {
@ -166,9 +182,59 @@ impl Cache {
Ok(ret)
}
/// Like [`Self::allocate`], resampled to `target`. Call after
/// [`Self::allocate`] has decoded the image.
pub fn allocate_resampled(
&mut self,
handle: &raster::Handle,
target: Size<u32>,
) -> Result<tiny_skia::PixmapRef<'_>, raster::Error> {
let key = (handle.id(), target.width, target.height);
if !self.resampled.contains_key(&key) {
let native = self
.entries
.get(&handle.id())
.and_then(Option::as_ref)
.ok_or(raster::Error::Empty)?;
// Stored pixels are already premultiplied, so resample them as is.
let pixels = graphics::image::downsample_premultiplied(
bytemuck::cast_slice(&native.pixels),
Size::new(native.width, native.height),
target,
);
let _ = self.resampled.insert(
key,
Entry {
width: target.width,
height: target.height,
pixels: pixels
.chunks_exact(4)
.map(|p| u32::from_ne_bytes([p[0], p[1], p[2], p[3]]))
.collect(),
},
);
}
let _ = self.resampled_hits.insert(key);
let entry = &self.resampled[&key];
Ok(tiny_skia::PixmapRef::from_bytes(
bytemuck::cast_slice(&entry.pixels),
entry.width,
entry.height,
)
.expect("Build pixmap from image bytes"))
}
fn trim(&mut self) {
self.entries.retain(|key, _| self.hits.contains(key));
self.resampled
.retain(|key, _| self.resampled_hits.contains(key));
self.hits.clear();
self.resampled_hits.clear();
}
}

View file

@ -1,5 +1,6 @@
use crate::core::svg::{Data, Handle};
use crate::core::{Color, Rectangle, Size};
use crate::graphics::text;
use resvg::usvg;
use rustc_hash::{FxHashMap, FxHashSet};
@ -74,6 +75,7 @@ struct Cache {
rasters: FxHashMap<RasterKey, tiny_skia::Pixmap>,
raster_hits: FxHashSet<RasterKey>,
fontdb: Option<Arc<usvg::fontdb::Database>>,
fontdb_version: Option<text::Version>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
@ -87,12 +89,15 @@ impl Cache {
fn load(&mut self, handle: &Handle) -> Option<&usvg::Tree> {
let id = handle.id();
// TODO: Reuse `cosmic-text` font database
if self.fontdb.is_none() {
let mut fontdb = usvg::fontdb::Database::new();
fontdb.load_system_fonts();
// Reuse the font database already loaded by the `cosmic-text` font
// system. We clone it once and refresh only when a font is
// loaded (i.e. when the font system's version changes).
let font_system = text::font_system().read().expect("Read font system");
let version = font_system.version();
self.fontdb = Some(Arc::new(fontdb));
if self.fontdb_version != Some(version) {
self.fontdb = Some(Arc::new(font_system.db().clone()));
self.fontdb_version = Some(version);
}
let options = usvg::Options {

View file

@ -25,6 +25,12 @@ svg = ["iced_graphics/svg", "resvg/text"]
web-colors = ["iced_graphics/web-colors"]
webgl = ["wgpu/webgl"]
strict-assertions = []
# Yoda: opt-in X11 backend. Pulls tiny-xlib/as-raw-xcb-connection/x11rb
# (which otherwise were unconditional direct deps on unix). Wayland-only
# builds just leave this off — window/x11.rs and the DRI3 device-id
# fallback in window/compositor.rs are cfg-gated on it.
x11 = ["dep:as-raw-xcb-connection", "dep:tiny-xlib", "dep:x11rb"]
wayland = ["cctk"]
[dependencies]
iced_debug.workspace = true
@ -57,11 +63,14 @@ wayland-protocols.workspace = true
wayland-backend = { version = "0.3.3", features = ["client_system"] }
wayland-client = { version = "0.31.2" }
wayland-sys = { version = "0.31.1", features = ["dlopen"] }
as-raw-xcb-connection = "1.0.1"
tiny-xlib = "0.2.3"
x11rb = { version = "0.13.1", features = [
as-raw-xcb-connection = { version = "1.0.1", optional = true }
tiny-xlib = { version = "0.2.3", optional = true }
x11rb = { version = "0.13.1", optional = true, features = [
"allow-unsafe-code",
"dl-libxcb",
"dri3",
"randr",
] }
[build-dependencies]
build_helpers = { path = "../build_helpers" }

3
wgpu/build.rs Normal file
View file

@ -0,0 +1,3 @@
fn main() {
build_helpers::cfg_aliases_setup();
}

View file

@ -63,7 +63,6 @@ impl<T: bytemuck::Pod> Buffer<T> {
/// Returns the size of the written bytes.
pub fn write(
&mut self,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
belt: &mut wgpu::util::StagingBelt,
offset: usize,
@ -79,7 +78,6 @@ impl<T: bytemuck::Pod> Buffer<T> {
&self.raw,
(offset + bytes_written) as u64,
MAX_WRITE_SIZE_U64,
device,
)
.copy_from_slice(
&bytes[bytes_written..bytes_written + MAX_WRITE_SIZE],
@ -100,7 +98,6 @@ impl<T: bytemuck::Pod> Buffer<T> {
&self.raw,
(offset + bytes_written) as u64,
bytes_left,
device,
)
.copy_from_slice(&bytes[bytes_written..]);

View file

@ -99,7 +99,7 @@ pub fn convert(
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("iced_wgpu.offscreen.blit.pipeline_layout"),
bind_group_layouts: &[&constant_layout, &texture_layout],
push_constant_ranges: &[],
immediate_size: 0,
});
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
@ -149,7 +149,7 @@ pub fn convert(
},
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview: None,
multiview_mask: None,
cache: None,
});
@ -194,6 +194,7 @@ pub fn convert(
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
pass.set_pipeline(&pipeline);

View file

@ -132,7 +132,7 @@ impl Atlas {
match &entry {
Entry::Contiguous(allocation) => {
self.upload_allocation(
pixels, width, 0, allocation, device, encoder, belt,
pixels, width, 0, allocation, encoder, belt,
);
}
Entry::Fragmented { fragments, .. } => {
@ -145,7 +145,6 @@ impl Atlas {
width,
offset,
&fragment.allocation,
device,
encoder,
belt,
);
@ -312,7 +311,6 @@ impl Atlas {
image_width: u32,
offset: usize,
allocation: &Allocation,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
belt: &mut wgpu::util::StagingBelt,
) {
@ -334,7 +332,6 @@ impl Atlas {
let buffer_slice = belt.allocate(
wgpu::BufferSize::new(total_bytes as u64).unwrap(),
wgpu::BufferSize::new(8 * 4).unwrap(),
device,
);
const PIXEL: usize = 4;

View file

@ -5,6 +5,8 @@ use crate::image::atlas::{self, Atlas};
#[cfg(all(feature = "image", not(target_arch = "wasm32")))]
use worker::Worker;
#[cfg(feature = "image")]
use rustc_hash::{FxHashMap, FxHashSet};
#[cfg(feature = "image")]
use std::collections::HashMap;
@ -38,7 +40,13 @@ impl Cache {
raster: Raster {
cache: crate::image::raster::Cache::default(),
pending: HashMap::new(),
belt: wgpu::util::StagingBelt::new(2 * 1024 * 1024),
resampled: FxHashMap::default(),
resampled_hits: FxHashSet::default(),
should_trim: false,
belt: wgpu::util::StagingBelt::new(
device.clone(),
2 * 1024 * 1024,
),
},
#[cfg(feature = "svg")]
vector: crate::image::vector::Cache::default(),
@ -206,7 +214,9 @@ impl Cache {
encoder: &mut wgpu::CommandEncoder,
belt: &mut wgpu::util::StagingBelt,
handle: &core::image::Handle,
bounds: Size<f32>,
) -> Option<(&atlas::Entry, &Arc<wgpu::BindGroup>)> {
use crate::graphics::image::{downsample, downsample_target, load};
use crate::image::raster::Memory;
self.receive();
@ -220,6 +230,34 @@ impl Cache {
None,
)?;
if let Some(target) = downsample_target(memory.dimensions(), bounds) {
let key = (handle.id(), target.width, target.height);
if !self.raster.resampled.contains_key(&key) {
let image = memory.host().or_else(|| load(handle).ok())?;
let image = downsample(&image, target);
let entry = self.atlas.upload(
device,
encoder,
belt,
target.width,
target.height,
&image,
)?;
let _ = self.raster.resampled.insert(key, entry);
self.raster.should_trim = true;
}
let _ = self.raster.resampled_hits.insert(key);
return Some((
self.raster.resampled.get(&key)?,
self.atlas.bind_group(),
));
}
if let Memory::Device {
entry, bind_group, ..
} = memory
@ -300,6 +338,23 @@ impl Cache {
#[cfg(not(target_arch = "wasm32"))]
self.worker.drop(_bind_group);
});
if self.raster.should_trim {
let hits = &self.raster.resampled_hits;
let atlas = &mut self.atlas;
self.raster.resampled.retain(|key, entry| {
let retain = hits.contains(key);
if !retain {
atlas.remove(entry);
}
retain
});
self.raster.resampled_hits.clear();
self.raster.should_trim = false;
}
}
#[cfg(feature = "svg")]
@ -373,6 +428,9 @@ impl Drop for Cache {
struct Raster {
cache: crate::image::raster::Cache,
pending: HashMap<core::image::Id, Vec<Callback>>,
resampled: FxHashMap<(core::image::Id, u32, u32), atlas::Entry>,
resampled_hits: FxHashSet<(core::image::Id, u32, u32)>,
should_trim: bool,
belt: wgpu::util::StagingBelt,
}
@ -451,7 +509,10 @@ mod worker {
backend,
texture_layout,
shell: shell.clone(),
belt: wgpu::util::StagingBelt::new(4 * 1024 * 1024),
belt: wgpu::util::StagingBelt::new(
device.clone(),
4 * 1024 * 1024,
),
jobs: jobs_receiver,
output: work_sender,
quit: quit_receiver,

View file

@ -45,7 +45,7 @@ impl Pipeline {
address_mode_w: wgpu::AddressMode::ClampToEdge,
min_filter: wgpu::FilterMode::Nearest,
mag_filter: wgpu::FilterMode::Nearest,
mipmap_filter: wgpu::FilterMode::Nearest,
mipmap_filter: wgpu::MipmapFilterMode::Nearest,
..Default::default()
});
@ -55,7 +55,7 @@ impl Pipeline {
address_mode_w: wgpu::AddressMode::ClampToEdge,
min_filter: wgpu::FilterMode::Linear,
mag_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::MipmapFilterMode::Linear,
..Default::default()
});
@ -106,8 +106,8 @@ impl Pipeline {
let layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("iced_wgpu::image pipeline layout"),
push_constant_ranges: &[],
bind_group_layouts: &[&constant_layout, &texture_layout],
immediate_size: 0,
});
let shader =
@ -191,7 +191,7 @@ impl Pipeline {
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview: None,
multiview_mask: None,
cache: None,
});
@ -267,7 +267,13 @@ impl State {
clip_bounds,
} => {
if let Some((atlas_entry, bind_group)) = cache
.upload_raster(device, encoder, belt, &image.handle)
.upload_raster(
device,
encoder,
belt,
&image.handle,
bounds.size() * scale,
)
{
match atlas.as_mut() {
None => {
@ -528,7 +534,6 @@ impl Layer {
&self.uniforms,
0,
(bytes.len() as u64).try_into().expect("Sized uniforms"),
device,
)
.copy_from_slice(bytes);
@ -539,11 +544,11 @@ impl Layer {
let mut offset = 0;
if !nearest.is_empty() {
offset += self.instances.write(device, encoder, belt, 0, nearest);
offset += self.instances.write(encoder, belt, 0, nearest);
}
if !linear.is_empty() {
let _ = self.instances.write(device, encoder, belt, offset, linear);
let _ = self.instances.write(encoder, belt, offset, linear);
}
}

View file

@ -126,6 +126,7 @@ impl Renderer {
// It would be great if the `StagingBelt` API exposed methods
// for introspection to detect when a resize may be worth it.
staging_belt: wgpu::util::StagingBelt::new(
engine.device.clone(),
buffer::MAX_WRITE_SIZE as u64,
),
@ -454,6 +455,7 @@ impl Renderer {
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
},
));
@ -528,6 +530,7 @@ impl Renderer {
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
},
));
}
@ -621,6 +624,7 @@ impl Renderer {
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
},
));
}

View file

@ -209,7 +209,7 @@ impl Layer {
transformation: Transformation,
scale: f32,
) {
self.update(device, encoder, belt, transformation, scale);
self.update(encoder, belt, transformation, scale);
if !quads.solids.is_empty() {
self.solid.prepare(device, encoder, belt, &quads.solids);
@ -223,7 +223,6 @@ impl Layer {
pub fn update(
&mut self,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
belt: &mut wgpu::util::StagingBelt,
transformation: Transformation,
@ -237,7 +236,6 @@ impl Layer {
&self.constants_buffer,
0,
(bytes.len() as u64).try_into().expect("Sized uniforms"),
device,
)
.copy_from_slice(bytes);
}

View file

@ -51,7 +51,7 @@ impl Layer {
instances: &[Gradient],
) {
let _ = self.instances.resize(device, instances.len());
let _ = self.instances.write(device, encoder, belt, 0, instances);
let _ = self.instances.write(encoder, belt, 0, instances);
self.instance_count = instances.len();
}
@ -75,8 +75,8 @@ impl Pipeline {
let layout = device.create_pipeline_layout(
&wgpu::PipelineLayoutDescriptor {
label: Some("iced_wgpu.quad.gradient.pipeline"),
push_constant_ranges: &[],
bind_group_layouts: &[constants_layout],
immediate_size: 0,
},
);
@ -155,7 +155,7 @@ impl Pipeline {
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview: None,
multiview_mask: None,
cache: None,
},
);

View file

@ -45,7 +45,7 @@ impl Layer {
instances: &[Solid],
) {
let _ = self.instances.resize(device, instances.len());
let _ = self.instances.write(device, encoder, belt, 0, instances);
let _ = self.instances.write(encoder, belt, 0, instances);
self.instance_count = instances.len();
}
@ -65,8 +65,8 @@ impl Pipeline {
let layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("iced_wgpu.quad.solid.pipeline"),
push_constant_ranges: &[],
bind_group_layouts: &[constants_layout],
immediate_size: 0,
});
let shader =
@ -139,7 +139,7 @@ impl Pipeline {
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview: None,
multiview_mask: None,
cache: None,
});

View file

@ -619,7 +619,7 @@ fn prepare(
)?;
Some(cryoglyph::TextArea {
buffer,
text: buffer.layout_runs(),
left: position.x,
top: position.y,
scale: transformation.scale_factor()

View file

@ -313,6 +313,7 @@ fn render<'a>(
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
})
};
@ -425,18 +426,13 @@ impl Layer {
let indices = mesh.indices();
index_offset += self.index_buffer.write(
device,
encoder,
belt,
index_offset,
indices,
);
index_offset +=
self.index_buffer
.write(encoder, belt, index_offset, indices);
match mesh {
Mesh::Solid { buffers, .. } => {
solid_vertex_offset += self.solid.vertices.write(
device,
encoder,
belt,
solid_vertex_offset,
@ -444,16 +440,14 @@ impl Layer {
);
solid_uniform_offset += self.solid.uniforms.write(
device,
encoder,
belt,
solid_uniform_offset,
&[uniforms],
);
)
}
Mesh::Gradient { buffers, .. } => {
gradient_vertex_offset += self.gradient.vertices.write(
device,
encoder,
belt,
gradient_vertex_offset,
@ -461,7 +455,6 @@ impl Layer {
);
gradient_uniform_offset += self.gradient.uniforms.write(
device,
encoder,
belt,
gradient_uniform_offset,
@ -731,7 +724,7 @@ mod solid {
&wgpu::PipelineLayoutDescriptor {
label: Some("iced_wgpu.triangle.solid.pipeline_layout"),
bind_group_layouts: &[&constants_layout],
push_constant_ranges: &[],
immediate_size: 0,
},
);
@ -784,7 +777,7 @@ mod solid {
primitive: triangle::primitive_state(),
depth_stencil: None,
multisample: triangle::multisample_state(antialiasing),
multiview: None,
multiview_mask: None,
cache: None,
},
);
@ -886,7 +879,7 @@ mod gradient {
&wgpu::PipelineLayoutDescriptor {
label: Some("iced_wgpu.triangle.gradient.pipeline_layout"),
bind_group_layouts: &[&constants_layout],
push_constant_ranges: &[],
immediate_size: 0,
},
);
@ -949,7 +942,7 @@ mod gradient {
primitive: triangle::primitive_state(),
depth_stencil: None,
multisample: triangle::multisample_state(antialiasing),
multiview: None,
multiview_mask: None,
cache: None,
},
);

View file

@ -69,8 +69,8 @@ impl Pipeline {
let layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("iced_wgpu::triangle::msaa pipeline layout"),
push_constant_ranges: &[],
bind_group_layouts: &[&constant_layout, &texture_layout],
immediate_size: 0,
});
let shader =
@ -116,7 +116,7 @@ impl Pipeline {
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview: None,
multiview_mask: None,
cache: None,
});
@ -177,6 +177,7 @@ impl Pipeline {
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
})
}
}
@ -320,7 +321,6 @@ impl State {
0,
NonZeroU64::new(std::mem::size_of::<Ratio>() as u64)
.expect("non-empty ratio"),
device,
)
.copy_from_slice(bytemuck::bytes_of(&ratio));
@ -351,6 +351,7 @@ impl State {
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
render_pass.set_pipeline(&pipeline.raw);

View file

@ -1,13 +1,8 @@
//! Display rendering results on windows.
pub mod compositor;
#[cfg(all(
unix,
feature = "cctk",
not(target_os = "macos"),
not(target_os = "redox")
))]
#[cfg(wayland_platform)]
mod wayland;
#[cfg(all(unix, not(target_os = "macos"), not(target_os = "redox")))]
#[cfg(all(unix, feature = "x11", not(target_os = "macos"), not(target_os = "redox")))]
mod x11;
pub use compositor::Compositor;

View file

@ -9,16 +9,10 @@ use crate::graphics::{self, Shell, Viewport};
use crate::settings::{self, Settings};
use crate::{Engine, Renderer};
#[cfg(all(
unix,
feature = "cctk",
not(target_os = "macos"),
not(target_os = "redox")
))]
#[cfg(wayland_platform)]
use super::wayland::get_wayland_device_ids;
#[cfg(all(unix, not(target_os = "macos"), not(target_os = "redox")))]
#[cfg(all(unix, feature = "x11", not(target_os = "macos"), not(target_os = "redox")))]
use super::x11::get_x11_device_ids;
use std::future::Future;
/// A window graphics backend for iced powered by `wgpu`.
pub struct Compositor {
@ -65,15 +59,12 @@ impl Compositor {
compatible_window: Option<W>,
shell: Shell,
) -> Result<Self, Error> {
#[cfg(all(
unix,
feature = "cctk",
not(target_os = "macos"),
not(target_os = "redox")
))]
#[cfg(wayland_platform)]
let ids = compatible_window.as_ref().and_then(|window| {
get_wayland_device_ids(window)
.or_else(|| get_x11_device_ids(window))
let w = get_wayland_device_ids(window);
#[cfg(feature = "x11")]
let w = w.or_else(|| get_x11_device_ids(window));
w
});
// HACK:
@ -82,12 +73,7 @@ impl Compositor {
// 3. and the user didn't set an adapter name,
// 4. and the user didn't request the high power pref
// => don't load the nvidia icd, as it might power on the gpu in hybrid setups causing severe delays
#[cfg(all(
unix,
feature = "cctk",
not(target_os = "macos"),
not(target_os = "redox")
))]
#[cfg(wayland_platform)]
if !matches!(ids, Some((0x10de, _)))
&& std::env::var_os("__NV_PRIME_RENDER_OFFLOAD")
.is_none_or(|var| var == "0")
@ -126,6 +112,7 @@ impl Compositor {
// if log::max_level() >= log::LevelFilter::Info {
// let available_adapters: Vec<_> = instance
// .enumerate_adapters(settings.backends)
// .await
// .iter()
// .map(wgpu::Adapter::get_info)
// .collect();
@ -146,15 +133,11 @@ impl Compositor {
let mut adapter = None;
#[cfg_attr(not(unix), allow(dead_code))]
if std::env::var_os("WGPU_ADAPTER_NAME").is_none() {
#[cfg(all(
unix,
feature = "cctk",
not(target_os = "macos"),
not(target_os = "redox")
))]
#[cfg(wayland_platform)]
if let Some((vendor_id, device_id)) = ids {
adapter = instance
.enumerate_adapters(settings.backends)
.await
.into_iter()
.filter(|adapter| {
let info = adapter.get_info();
@ -172,6 +155,7 @@ impl Compositor {
} else if let Ok(name) = std::env::var("WGPU_ADAPTER_NAME") {
adapter = instance
.enumerate_adapters(settings.backends)
.await
.into_iter()
.filter(|adapter| {
let info = adapter.get_info();
@ -184,6 +168,8 @@ impl Compositor {
true
}
});
} else {
adapter = instance.request_adapter(&adapter_options).await.ok();
}
let adapter = match adapter {
@ -286,6 +272,14 @@ impl Compositor {
..limits
});
// Request SHADER_F16 only if the adapter supports it (e.g., not available in WebGL2)
let required_features =
if adapter.features().contains(wgpu::Features::SHADER_F16) {
wgpu::Features::SHADER_F16
} else {
wgpu::Features::empty()
};
let mut errors = Vec::new();
for required_limits in limits {
@ -294,7 +288,7 @@ impl Compositor {
label: Some(
"iced_wgpu::window::compositor device descriptor",
),
required_features: wgpu::Features::empty(),
required_features,
required_limits: required_limits.clone(),
memory_hints: wgpu::MemoryHints::MemoryUsage,
trace: wgpu::Trace::Off,
@ -352,6 +346,14 @@ pub fn present(
) -> Result<(), compositor::SurfaceError> {
match surface.get_current_texture() {
Ok(frame) => {
if frame.texture.width() != viewport.physical_width()
|| frame.texture.height() != viewport.physical_height()
{
return Err(compositor::SurfaceError::Outdated);
}
let suboptimal = frame.suboptimal;
let view = &frame
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
@ -367,6 +369,10 @@ pub fn present(
on_pre_present();
frame.present();
if suboptimal {
return Err(compositor::SurfaceError::Outdated);
}
Ok(())
}
Err(error) => match error {

View file

@ -1,3 +1,5 @@
#![allow(unexpected_cfgs)]
use std::{
fs,
io::{BufRead, BufReader},
@ -144,7 +146,9 @@ pub fn get_x11_device_ids<W: Window>(window: &W) -> Option<(u16, u16)> {
{
let stat =
stat(Path::new("/dev/dri").join(device)).ok()?;
let dev = stat.st_rdev;
// Hack (u64 as u64) for E2K CPU architecture
#[allow(clippy::unnecessary_cast)]
let dev = stat.st_rdev as u64;
return super::ids_from_dev(dev);
}
}
@ -165,7 +169,9 @@ pub fn get_x11_device_ids<W: Window>(window: &W) -> Option<(u16, u16)> {
let dri3 = conn.dri3_open(root, x11rb::NONE).ok()?.reply().ok()?;
let device_fd = dri3.device_fd;
let stat = fstat(device_fd).ok()?;
let dev = stat.st_rdev;
// Hack (u64 as u64) for E2K CPU architecture
#[allow(clippy::unnecessary_cast)]
let dev = stat.st_rdev as u64;
super::ids_from_dev(dev)
}
}

View file

@ -29,15 +29,13 @@ highlighter = ["dep:iced_highlighter"]
advanced = []
crisp = []
a11y = ["iced_accessibility"]
cctk = ["iced_runtime/cctk", "dep:cctk"]
wayland = ["iced_runtime/wayland", "dep:cctk"]
[dependencies]
iced_renderer.workspace = true
iced_runtime.workspace = true
iced_accessibility.workspace = true
iced_accessibility.optional = true
cctk.workspace = true
cctk.optional = true
num-traits.workspace = true
log.workspace = true
rustc-hash.workspace = true
@ -57,3 +55,10 @@ pulldown-cmark.optional = true
iced_highlighter.workspace = true
iced_highlighter.optional = true
[target.'cfg(all(unix, not(any(target_os = "redox", target_family = "wasm", target_os = "android", target_vendor = "apple"))))'.dependencies]
cctk.workspace = true
cctk.optional = true
[build-dependencies]
build_helpers = { path = "../build_helpers" }

3
widget/build.rs Normal file
View file

@ -0,0 +1,3 @@
fn main() {
build_helpers::cfg_aliases_setup();
}

View file

@ -267,6 +267,7 @@ struct State {
is_hovered: bool,
is_pressed: bool,
is_focused: bool,
needs_redraw: bool,
}
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
@ -368,6 +369,12 @@ where
viewport,
);
let state = tree.state.downcast_mut::<State>();
if state.needs_redraw {
state.needs_redraw = false;
shell.request_redraw();
}
if shell.is_event_captured() {
return;
}
@ -777,11 +784,11 @@ impl Default for Style {
///
/// impl Catalog for MyTheme {
/// type Class<'a> = ButtonClass;
///
///
/// fn default<'a>() -> Self::Class<'a> {
/// ButtonClass::default()
/// }
///
///
///
/// fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
/// let mut style = Style::default();
@ -1000,10 +1007,12 @@ impl operation::Focusable for State {
}
fn focus(&mut self) {
self.needs_redraw |= !self.is_focused;
self.is_focused = true;
}
fn unfocus(&mut self) {
self.needs_redraw |= self.is_focused;
self.is_focused = false;
}
}

View file

@ -505,6 +505,7 @@ where
state.raw(),
crate::text::Style {
color: style.text_color,
..crate::text::Style::default()
},
viewport,
);

View file

@ -35,7 +35,7 @@ use crate::core::{
color,
};
use iced_runtime::{Action, Task, task};
use iced_runtime::{Task, task};
/// A widget that aligns its contents inside of its boundaries.
///
@ -601,7 +601,7 @@ pub fn visible_bounds(id: Id) -> Task<Option<Rectangle>> {
operate: &mut dyn FnMut(&mut dyn Operation<Option<Rectangle>>),
) {
self.depth += 1;
self.traverse(operate);
operate(self);
self.depth -= 1;
}

View file

@ -1,7 +1,6 @@
//! Keyed columns distribute content vertically while keeping continuity.
//! Distribute content vertically.
use crate::core::event;
use crate::core::layout;
use crate::core::mouse;
use crate::core::overlay;
@ -293,9 +292,12 @@ where
.zip(&mut tree.children)
.zip(layout.children())
.for_each(|((child, state), c_layout)| {
child
.as_widget_mut()
.operate(state, c_layout.with_virtual_offset(layout.virtual_offset()), renderer, operation);
child.as_widget_mut().operate(
state,
c_layout.with_virtual_offset(layout.virtual_offset()),
renderer,
operation,
);
});
});
}
@ -318,7 +320,13 @@ where
.zip(layout.children())
{
child.as_widget_mut().update(
tree, event, c_layout.with_virtual_offset(layout.virtual_offset()), cursor, renderer, clipboard, shell,
tree,
event,
c_layout.with_virtual_offset(layout.virtual_offset()),
cursor,
renderer,
clipboard,
shell,
viewport,
);
}
@ -337,9 +345,13 @@ where
.zip(&tree.children)
.zip(layout.children())
.map(|((child, tree), c_layout)| {
child
.as_widget()
.mouse_interaction(tree, c_layout.with_virtual_offset(layout.virtual_offset()), cursor, viewport, renderer)
child.as_widget().mouse_interaction(
tree,
c_layout.with_virtual_offset(layout.virtual_offset()),
cursor,
viewport,
renderer,
)
})
.max()
.unwrap_or_default()

View file

@ -9,6 +9,8 @@ pub use iced_renderer::graphics;
pub use core::widget::Id;
pub(crate) const DRAG_DEADBAND_DISTANCE: f32 = 10.0;
mod action;
mod mouse_area;
mod pin;

View file

@ -1,5 +1,5 @@
#![allow(missing_docs)]
use crate::core::event::{self, Event};
use crate::core::event::Event;
use crate::core::layout;
use crate::core::mouse;
use crate::core::overlay;
@ -363,24 +363,25 @@ where
let state = tree.state.downcast_mut::<State>();
let offset = layout.position() - Point::ORIGIN;
self.visible_elements
for (element, (index, layout, tree)) in self
.visible_elements
.iter_mut()
.zip(&mut state.visible_layouts)
.map(|(element, (index, layout, tree))| {
element.as_widget_mut().update(
tree,
event,
Layout::with_offset(
offset + Vector::new(0.0, self.spacing * *index as f32),
layout,
),
cursor,
renderer,
clipboard,
shell,
viewport,
)
});
{
element.as_widget_mut().update(
tree,
event,
Layout::with_offset(
offset + Vector::new(0.0, self.spacing * *index as f32),
layout,
),
cursor,
renderer,
clipboard,
shell,
viewport,
);
}
if let Event::Window(window::Event::RedrawRequested(_)) = event {
match &mut state.task {

View file

@ -2,7 +2,6 @@
use iced_renderer::core::mouse::Click;
use crate::core::event;
use crate::core::layout;
use crate::core::mouse;
use crate::core::overlay;

View file

@ -92,7 +92,8 @@ use crate::core::{
};
use log::trace;
const DRAG_DEADBAND_DISTANCE: f32 = 10.0;
use crate::DRAG_DEADBAND_DISTANCE;
const THICKNESS_RATIO: f32 = 25.0;
/// A collection of panes distributed using either vertical or horizontal splits

View file

@ -6,7 +6,7 @@ use crate::core::renderer;
use crate::core::widget::{self, Tree};
use crate::core::{
self, Clipboard, Element, Event, Layout, Point, Rectangle, Shell, Size,
Vector, event,
Vector,
};
use crate::pane_grid::{Draggable, TitleBar};

View file

@ -60,6 +60,7 @@ where
width: Length,
height: Length,
position: Point,
overflow: bool,
}
impl<'a, Message, Theme, Renderer> Pin<'a, Message, Theme, Renderer>
@ -75,6 +76,7 @@ where
width: Length::Fill,
height: Length::Fill,
position: Point::ORIGIN,
overflow: false,
}
}
@ -107,6 +109,12 @@ where
self.position.y = y.into().0;
self
}
/// Sets whether the [`Pin`] can overflow its boundaries.
pub fn overflow(mut self, overflow: bool) -> Self {
self.overflow = overflow;
self
}
}
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
@ -145,8 +153,12 @@ where
) -> layout::Node {
let limits = limits.width(self.width).height(self.height);
let available =
limits.max() - Size::new(self.position.x, self.position.y);
let available =
if self.overflow {
limits.max()
} else {
limits.max() - Size::new(self.position.x, self.position.y)
};
let node = self
.content

View file

@ -459,6 +459,7 @@ where
state.raw(),
crate::text::Style {
color: style.text_color,
..crate::text::Style::default()
},
viewport,
);

View file

@ -27,7 +27,6 @@ use iced_runtime::core::widget::Id;
#[cfg(feature = "a11y")]
use std::borrow::Cow;
use crate::core::event;
use crate::core::keyboard;
use crate::core::layout;
use crate::core::mouse;
@ -248,64 +247,19 @@ where
self
}
/// Sets whether the user should be allowed to auto-scroll the [`Scrollable`]
/// with the middle mouse button.
/// Adds padding at the ends of the [`Scrollbar`]s of the [`Scrollable`].
///
/// By default, it is disabled.
pub fn auto_scroll(mut self, auto_scroll: bool) -> Self {
self.auto_scroll = auto_scroll;
self
}
/// Sets the scrollbar width of the [`Scrollbar`].
pub fn scrollbar_width(mut self, width: impl Into<Pixels>) -> Self {
let width = width.into().0.max(0.0);
/// The `padding` provided will be used as space at the top and bottom of a
/// vertical [`Scrollbar`], and at the left and right ends of a horizontal
/// [`Scrollbar`], when they are visible.
///
/// Unlike [`Self::spacing`], the padding does not affect the layout of the
/// [`Scrollable`].
pub fn padding(mut self, new_padding: impl Into<Pixels>) -> Self {
let padding = new_padding.into().0;
match &mut self.direction {
Direction::Horizontal(scrollbar)
| Direction::Vertical(scrollbar) => {
scrollbar.width = width;
}
Direction::Both {
horizontal,
vertical,
} => {
horizontal.width = width;
vertical.width = width;
}
}
self
}
/// Sets the scroller width of the [`Scrollbar`].
pub fn scroller_width(mut self, width: impl Into<Pixels>) -> Self {
let width = width.into().0.max(0.0);
match &mut self.direction {
Direction::Horizontal(scrollbar)
| Direction::Vertical(scrollbar) => {
scrollbar.scroller_width = width;
}
Direction::Both {
horizontal,
vertical,
} => {
horizontal.scroller_width = width;
vertical.scroller_width = width;
}
}
self
}
/// Sets the padding at the start and end of the [`Scrollbar`].
pub fn scrollbar_padding(mut self, padding: impl Into<Pixels>) -> Self {
let padding = padding.into().0.max(0.0);
match &mut self.direction {
Direction::Horizontal(scrollbar)
| Direction::Vertical(scrollbar) => {
Direction::Horizontal(scrollbar) | Direction::Vertical(scrollbar) => {
scrollbar.padding = padding;
}
Direction::Both {
@ -320,6 +274,15 @@ where
self
}
/// Sets whether the user should be allowed to auto-scroll the [`Scrollable`]
/// with the middle mouse button.
///
/// By default, it is disabled.
pub fn auto_scroll(mut self, auto_scroll: bool) -> Self {
self.auto_scroll = auto_scroll;
self
}
/// Sets the style of this [`Scrollable`].
#[must_use]
pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
@ -504,9 +467,15 @@ impl Scrollbar {
self
}
/// Sets the padding at the start and end of the [`Scrollbar`].
/// Sets the padding of the [`Scrollbar`].
///
/// The padding is added at the top and bottom of the scrollbar (or at the
/// left and right ends for a horizontal [`Scrollbar`]) when it is visible.
///
/// Unlike [`Self::margin`] and [`Self::spacing`], the padding does not
/// affect the layout of the [`Scrollable`].
pub fn padding(mut self, padding: impl Into<Pixels>) -> Self {
self.padding = padding.into().0.max(0.0);
self.padding = padding.into().0;
self
}
}
@ -676,13 +645,7 @@ where
let translation =
state.translation(self.direction, bounds, content_bounds);
operation.scrollable(
Some(&self.id),
bounds,
content_bounds,
translation,
state,
);
operation.pre_operation(Some(&self.id));
operation.traverse(&mut |operation| {
self.content.as_widget_mut().operate(
@ -696,6 +659,14 @@ where
operation,
);
});
// XXX must be done after traversing to perform DFS
operation.scrollable(
Some(&self.id),
bounds,
content_bounds,
translation,
state,
);
}
fn update(
@ -933,7 +904,7 @@ where
let had_input_method = shell.input_method().is_enabled();
let mut c_event = match event.clone() {
let c_event = match event.clone() {
Event::Dnd(dnd::DndEvent::Offer(
id,
dnd::OfferEvent::Enter {
@ -961,6 +932,16 @@ where
y: y + translation.y as f64,
},
)),
Event::Touch(touch::Event::FingerLifted {
id,
position,
}) if matches!(
state.interaction,
Interaction::TouchScrolling(_)
) =>
{
Event::Touch(touch::Event::FingerLost { id, position })
}
e => e,
};
self.content.as_widget_mut().update(
@ -988,6 +969,14 @@ where
}
};
if matches!(
event,
Event::Mouse(mouse::Event::CursorMoved { .. })
| Event::Touch(touch::Event::FingerPressed { .. })
) {
state.suppress_touch_hover = false;
}
if matches!(
event,
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
@ -996,10 +985,18 @@ where
| touch::Event::FingerLost { .. }
)
) {
if matches!(state.interaction, Interaction::TouchScrolling(_)) {
state.suppress_touch_hover = true;
}
state.interaction = Interaction::None;
state.touch_press_start = None;
return;
}
if let Event::Touch(touch::Event::FingerPressed { .. }) = event {
state.touch_press_start = cursor_over_scrollable;
}
if shell.is_event_captured() {
return;
}
@ -1093,14 +1090,31 @@ where
Interaction::TouchScrolling(position);
}
touch::Event::FingerMoved { .. } => {
let Interaction::TouchScrolling(
scroll_box_touched_at,
) = state.interaction
let Some(cursor_position) = cursor.position()
else {
return;
};
let Some(cursor_position) = cursor.position()
if !matches!(
state.interaction,
Interaction::TouchScrolling(_)
) {
let Some(start) = state.touch_press_start
else {
return;
};
if start.distance(cursor_position)
< crate::DRAG_DEADBAND_DISTANCE
{
return;
}
state.interaction =
Interaction::TouchScrolling(start);
}
let Interaction::TouchScrolling(
scroll_box_touched_at,
) = state.interaction
else {
return;
};
@ -1322,6 +1336,14 @@ where
state.translation(self.direction, bounds, content_bounds);
let cursor = match cursor_over_scrollable {
_ if state.suppress_touch_hover
|| matches!(
state.interaction,
Interaction::TouchScrolling(_)
) =>
{
mouse::Cursor::Unavailable
}
Some(cursor_position)
if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) =>
{
@ -1497,6 +1519,14 @@ where
state.translation(self.direction, bounds, content_bounds);
let cursor = match cursor_over_scrollable {
_ if state.suppress_touch_hover
|| matches!(
state.interaction,
Interaction::TouchScrolling(_)
) =>
{
cursor.levitate() + translation
}
Some(cursor_position)
if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) =>
{
@ -2022,6 +2052,8 @@ struct State {
last_notified: Option<Viewport>,
last_scrolled: Option<Instant>,
is_scrollbar_visible: bool,
touch_press_start: Option<Point>,
suppress_touch_hover: bool,
}
#[derive(Debug, Clone, Copy)]
@ -2047,6 +2079,8 @@ impl Default for State {
last_notified: None,
last_scrolled: None,
is_scrollbar_visible: true,
touch_press_start: None,
suppress_touch_hover: false,
}
}
}
@ -2349,13 +2383,16 @@ impl Scrollbars {
let total_scrollbar_width =
width.max(scroller_width) + 2.0 * margin;
// The padding is purely visual: it shrinks the top and bottom of the
// scrollbar without affecting the layout
let scrollbar_height = (bounds.height - x_scrollbar_height - 2.0 * padding).max(0.0);
// Total bounds of the scrollbar + margin + scroller width
let total_scrollbar_bounds = Rectangle {
x: bounds.x + bounds.width - total_scrollbar_width,
y: bounds.y + padding,
width: total_scrollbar_width,
height: (bounds.height - x_scrollbar_height - 2.0 * padding)
.max(0.0),
height: scrollbar_height,
};
// Bounds of just the scrollbar
@ -2365,8 +2402,7 @@ impl Scrollbars {
- width / 2.0,
y: bounds.y + padding,
width,
height: (bounds.height - x_scrollbar_height - 2.0 * padding)
.max(0.0),
height: scrollbar_height,
};
let ratio = bounds.height / content_bounds.height;
@ -2423,12 +2459,15 @@ impl Scrollbars {
let total_scrollbar_height =
width.max(scroller_width) + 2.0 * margin;
// The padding is purely visual: it shrinks the left and right ends of
// the scrollbar without affecting the layout
let scrollbar_width = (bounds.width - scrollbar_y_width - 2.0 * padding).max(0.0);
// Total bounds of the scrollbar + margin + scroller width
let total_scrollbar_bounds = Rectangle {
x: bounds.x + padding,
y: bounds.y + bounds.height - total_scrollbar_height,
width: (bounds.width - scrollbar_y_width - 2.0 * padding)
.max(0.0),
width: scrollbar_width,
height: total_scrollbar_height,
};
@ -2438,8 +2477,7 @@ impl Scrollbars {
y: bounds.y + bounds.height
- total_scrollbar_height / 2.0
- width / 2.0,
width: (bounds.width - scrollbar_y_width - 2.0 * padding)
.max(0.0),
width: scrollbar_width,
height: width,
};

View file

@ -110,6 +110,8 @@ where
height: f32,
class: Theme::Class<'a>,
status: Option<Status>,
handle_width: f32,
handle_height: f32,
}
impl<'a, T, Message, Theme> Slider<'a, T, Message, Theme>
@ -119,7 +121,13 @@ where
Theme: Catalog,
{
/// The default height of a [`Slider`].
pub const DEFAULT_HEIGHT: f32 = 16.0;
pub const DEFAULT_HEIGHT: f32 = 26.0;
/// The default max height of a [`Handle`].
pub const DEFAULT_MAX_HANDLE_HEIGHT: f32 = 26.0;
/// The default max width of a [`Handle`].
pub const DEFAULT_MAX_HANDLE_WIDTH: f32 = 26.0;
/// Creates a new [`Slider`].
///
@ -165,6 +173,8 @@ where
height: Self::DEFAULT_HEIGHT,
class: Theme::default(),
status: None,
handle_width: Self::DEFAULT_MAX_HANDLE_WIDTH,
handle_height: Self::DEFAULT_MAX_HANDLE_HEIGHT,
}
}
@ -207,6 +217,24 @@ where
self
}
/// Sets the max width of the [`Handle`].
///
/// Theme can be used to change the size and shape of handle
/// but width of handle defined in theme will not exceed this width
pub fn handle_width(mut self, width: impl Into<Pixels>) -> Self {
self.handle_width = width.into().0;
self
}
/// Sets the max height of the [`Handle`].
///
/// Theme can be used to change the size and shape of handle
/// but height of handle defined in theme will not exceed this height
pub fn handle_height(mut self, height: impl Into<Pixels>) -> Self {
self.handle_height = height.into().0;
self
}
/// Sets the step size of the [`Slider`].
pub fn step(mut self, step: impl Into<T>) -> Self {
self.step = step.into();
@ -323,8 +351,15 @@ where
let mut update = || {
let current_value = self.value;
self.handle_width = self.handle_width.min(layout.bounds().width);
self.handle_height = self.handle_height.min(layout.bounds().height);
let locate = |cursor_position: Point| -> Option<T> {
let bounds = layout.bounds();
let bounds = Rectangle {
x: layout.bounds().x + self.handle_width / 2.0,
width: layout.bounds().width - self.handle_width,
..layout.bounds()
};
if cursor_position.x <= bounds.x {
Some(*self.range.start())
@ -509,54 +544,30 @@ where
_cursor: mouse::Cursor,
_viewport: &Rectangle,
) {
let bounds = layout.bounds();
let bounds = Rectangle {
x: layout.bounds().x + self.handle_width / 2.0,
width: layout.bounds().width - self.handle_width,
..layout.bounds()
};
let style =
theme.style(&self.class, self.status.unwrap_or(Status::Active));
let border_width = style
.handle
.border_width
.min(bounds.height / 2.0)
.min(bounds.width / 2.0);
let (handle_width, handle_height, handle_border_radius) =
let (mut handle_width, mut handle_height, handle_border_radius) =
match style.handle.shape {
HandleShape::Circle { radius } => {
let radius = (radius)
.max(2.0 * border_width)
.min(bounds.height / 2.0)
.min(bounds.width / 2.0 + 2.0 * border_width);
(radius * 2.0, radius * 2.0, Radius::from(radius))
}
HandleShape::Rectangle {
height,
width,
border_radius,
} => {
let width = (f32::from(width)).max(2.0 * border_width);
let height = (f32::from(height)).max(2.0 * border_width);
let mut border_radius: [f32; 4] = border_radius.into();
for r in &mut border_radius {
*r = (*r)
.min(height / 2.0)
.min(width / 2.0)
.max(*r * (width + border_width * 2.0) / width);
}
(
width,
height,
Radius {
top_left: border_radius[0],
top_right: border_radius[1],
bottom_right: border_radius[2],
bottom_left: border_radius[3],
},
)
}
} => (f32::from(width), f32::from(height), border_radius),
};
handle_width = handle_width.min(self.handle_width);
handle_height = handle_height.min(self.handle_height);
let value = self.value.into() as f32;
let (range_start, range_end) = {
let (start, end) = self.range.clone().into_inner();
@ -567,8 +578,7 @@ where
let offset = if range_start >= range_end {
0.0
} else {
(bounds.width - handle_width) * (value - range_start)
/ (range_end - range_start)
bounds.width * (value - range_start) / (range_end - range_start)
};
let rail_y = bounds.y + bounds.height / 2.0;
@ -590,7 +600,8 @@ where
x: bounds.x + offset,
y: rail_y + 6.0,
width: BREAKPOINT_WIDTH,
height: 8.0,
height: 8.0_f32
.min(bounds.y + bounds.height - (rail_y + 6.0)),
},
border: Border {
radius: 0.0.into(),
@ -607,9 +618,9 @@ where
renderer::Quad {
bounds: Rectangle {
x: bounds.x,
y: rail_y - style.rail.width / 2.0,
width: offset + handle_width / 2.0,
height: style.rail.width,
y: rail_y - (style.rail.width).min(bounds.height) / 2.0,
width: offset,
height: (style.rail.width).min(bounds.height),
},
border: style.rail.border,
..renderer::Quad::default()
@ -622,10 +633,10 @@ where
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle {
x: bounds.x + offset + handle_width / 2.0,
y: rail_y - style.rail.width / 2.0,
width: bounds.width - offset - handle_width / 2.0,
height: style.rail.width,
x: bounds.x + offset,
y: rail_y - (style.rail.width).min(bounds.height) / 2.0,
width: bounds.width - offset,
height: (style.rail.width).min(bounds.height),
},
border: style.rail.border,
..renderer::Quad::default()
@ -637,7 +648,7 @@ where
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle {
x: bounds.x + offset,
x: bounds.x + offset - handle_width / 2.0,
y: rail_y - (handle_height / 2.0),
width: handle_width,
height: handle_height,

View file

@ -1,6 +1,5 @@
//! Display content on top of other content.
use crate::core::event;
use crate::core::layout;
use crate::core::mouse;
use crate::core::overlay;

View file

@ -179,7 +179,10 @@ where
{
let color = color.map(Into::into);
self.style(move |_theme| Style { color })
self.style(move |_theme| Style {
color,
..Style::default()
})
}
/// Sets the default style class of the [`Rich`] text.

View file

@ -544,6 +544,9 @@ pub struct State<Highlighter: text::Highlighter> {
highlighter: RefCell<Highlighter>,
highlighter_settings: Highlighter::Settings,
highlighter_format_address: usize,
context_menu_position: Option<Point>,
clipboard_has_text: bool,
pending_edit: Option<Action>,
}
#[derive(Debug, Clone)]
@ -579,6 +582,14 @@ impl<Highlighter: text::Highlighter> State<Highlighter> {
pub fn is_focused(&self) -> bool {
self.focus.is_some()
}
/// Clears focus, selection, and all interaction state.
pub fn clear_focus(&mut self) {
self.focus = None;
self.drag_click = None;
self.context_menu_position = None;
self.pending_edit = Some(Action::ClearSelection);
}
}
impl<Highlighter: text::Highlighter> operation::Focusable
@ -593,7 +604,7 @@ impl<Highlighter: text::Highlighter> operation::Focusable
}
fn unfocus(&mut self) {
self.focus = None;
self.clear_focus();
}
}
@ -621,6 +632,9 @@ where
)),
highlighter_settings: self.highlighter_settings.clone(),
highlighter_format_address: self.highlighter_format as usize,
context_menu_position: None,
clipboard_has_text: false,
pending_edit: None,
})
}
@ -699,6 +713,60 @@ where
shell: &mut Shell<'_, Message>,
_viewport: &Rectangle,
) {
// Handle context menu and focus-loss on clicks outside.
{
let state = tree.state.downcast_mut::<State<Highlighter>>();
let text_bounds = layout.bounds();
if matches!(
event,
Event::Mouse(mouse::Event::ButtonPressed(_))
| Event::Touch(
crate::core::touch::Event::FingerPressed { .. }
)
) && cursor.position_over(text_bounds).is_none()
{
state.clear_focus();
}
match event {
Event::Mouse(mouse::Event::ButtonPressed(
mouse::Button::Right,
)) => {
if let Some(pos) = cursor.position_over(text_bounds) {
if state.focus.is_none() {
state.focus = Some(Focus::now());
}
state.context_menu_position = Some(pos);
state.clipboard_has_text =
widget::text::clipboard_has_text(clipboard);
shell.capture_event();
return;
}
}
Event::Mouse(mouse::Event::ButtonPressed(
mouse::Button::Left,
)) => {
if state.context_menu_position.take().is_some() {
shell.capture_event();
return;
}
}
_ => {}
}
}
// Process pending edits from context menu
// publishes via on_edit so the app calls content.perform().
{
let state = tree.state.downcast_mut::<State<Highlighter>>();
if let Some(action) = state.pending_edit.take() {
if let Some(on_edit) = self.on_edit.as_ref() {
shell.publish((on_edit)(action));
}
}
}
let Some(on_edit) = self.on_edit.as_ref() else {
return;
};
@ -1223,6 +1291,23 @@ impl<Message> Binding<Message> {
return Some(binding);
}
let alt_clipboard = match key.as_ref() {
keyboard::Key::Named(key::Named::Insert) if modifiers.shift() => {
Some(Self::Paste)
}
keyboard::Key::Named(key::Named::Insert) if modifiers.command() => {
Some(Self::Copy)
}
keyboard::Key::Named(key::Named::Delete) if modifiers.shift() => {
Some(Self::Cut)
}
_ => None,
};
if let Some(binding) = alt_clipboard {
return Some(binding);
}
#[cfg(target_os = "macos")]
let modified_key =
convert_macos_shortcut(&key, modifiers).unwrap_or(modified_key);
@ -1545,3 +1630,73 @@ pub(crate) fn convert_macos_shortcut(
Some(keyboard::Key::Named(key))
}
use crate::core::widget::text::HasSelectableText;
impl<Highlighter, Message, Theme, Renderer> HasSelectableText
for TextEditor<'_, Highlighter, Message, Theme, Renderer>
where
Highlighter: text::Highlighter,
Theme: Catalog,
Renderer: text::Renderer,
{
fn selected_text(&self, _tree: &widget::Tree) -> Option<String> {
self.content.selection()
}
fn select_all(&self, tree: &mut widget::Tree) {
let state = tree.state.downcast_mut::<State<Highlighter>>();
state.pending_edit = Some(Action::SelectAll);
}
fn is_editable(&self) -> bool {
self.on_edit.is_some()
}
fn has_text(&self, _tree: &widget::Tree) -> bool {
!self.content.is_empty()
}
fn clipboard_has_text(&self, tree: &widget::Tree) -> bool {
tree.state
.downcast_ref::<State<Highlighter>>()
.clipboard_has_text
}
fn is_focused(&self, tree: &widget::Tree) -> bool {
tree.state.downcast_ref::<State<Highlighter>>().is_focused()
}
fn context_menu_position(&self, tree: &widget::Tree) -> Option<Point> {
tree.state
.downcast_ref::<State<Highlighter>>()
.context_menu_position
}
fn set_context_menu_position(
&self,
tree: &mut widget::Tree,
pos: Option<Point>,
) {
tree.state
.downcast_mut::<State<Highlighter>>()
.context_menu_position = pos;
}
fn delete_selection(&self, tree: &mut widget::Tree) -> Option<String> {
let state = tree.state.downcast_mut::<State<Highlighter>>();
state.pending_edit = Some(Action::Edit(Edit::Delete));
Some(String::new())
}
fn paste_text(
&self,
tree: &mut widget::Tree,
text: &str,
) -> Option<String> {
let state = tree.state.downcast_mut::<State<Highlighter>>();
state.pending_edit =
Some(Action::Edit(Edit::Paste(Arc::new(text.to_owned()))));
Some(String::new())
}
}

View file

@ -1,6 +1,5 @@
use crate::container;
use crate::core::event;
use crate::core::layout;
use crate::core::mouse;
use crate::core::overlay;

View file

@ -37,7 +37,6 @@ use std::borrow::Cow;
use iced_runtime::core::border::Radius;
use crate::core::alignment;
use crate::core::border;
use crate::core::layout;
use crate::core::mouse;
use crate::core::renderer;
@ -483,6 +482,7 @@ where
state.raw(),
crate::text::Style {
color: style.text_color,
..crate::text::Style::default()
},
viewport,
);
@ -509,7 +509,7 @@ where
let space = style.handle_margin;
let toggler_background_bounds = Rectangle {
let _toggler_background_bounds = Rectangle {
x: bounds.x,
y: bounds.y,
width: bounds.width,
@ -529,7 +529,7 @@ where
style.background,
);
let padding = (style.padding_ratio * bounds.height).round();
let _padding = (style.padding_ratio * bounds.height).round();
let toggler_foreground_bounds = Rectangle {
x: bounds.x
+ if self.is_toggled {

View file

@ -14,7 +14,18 @@ keywords.workspace = true
workspace = true
[features]
wayland = ["winit/wayland", "wayland-csd-adwaita"]
wayland = [
"winit/wayland",
"wayland-csd-adwaita",
"wayland-protocols",
"raw-window-handle",
"iced_runtime/wayland",
"wayland-backend",
"xkbcommon",
"xkbcommon-dl",
"xkeysym",
"dep:cctk",
]
default = ["x11", "wayland-dlopen"]
debug = ["iced_debug/enable"]
sysinfo = ["dep:sysinfo"]
@ -26,17 +37,6 @@ program = []
wayland-dlopen = ["winit/wayland-dlopen"]
wayland-csd-adwaita = ["winit/wayland-csd-adwaita"]
a11y = ["iced_accessibility", "iced_runtime/a11y"]
cctk = [
"wayland",
"wayland-protocols",
"raw-window-handle",
"iced_runtime/cctk",
"wayland-backend",
"xkbcommon",
"xkbcommon-dl",
"xkeysym",
"dep:cctk",
]
# Prevents multiple separate process instances.
single-instance = []
@ -64,7 +64,8 @@ iced_debug.workspace = true
iced_program.workspace = true
[target.'cfg(target_os = "linux")'.dependencies]
[target.'cfg(all(unix, not(any(target_os = "redox", target_family = "wasm", target_os = "android", target_vendor = "apple"))))'.dependencies]
mundy.workspace = true
mundy.optional = true
@ -89,3 +90,6 @@ winapi.workspace = true
web-sys.workspace = true
web-sys.features = ["Document", "Window", "HtmlCanvasElement"]
wasm-bindgen-futures.workspace = true
[build-dependencies]
build_helpers = { path = "../build_helpers" }

3
winit/build.rs Normal file
View file

@ -0,0 +1,3 @@
fn main() {
build_helpers::cfg_aliases_setup();
}

View file

@ -49,7 +49,10 @@ pub fn event_func(
position,
..
} => {
if !window.is_decorated() {
let resizable = !window.is_decorated()
&& !window.is_maximized()
&& window.fullscreen().is_none();
if resizable {
let location = cursor_resize_direction(
window.surface_size(),
*position,

View file

@ -92,7 +92,7 @@ impl Clipboard {
})
.unwrap_or(State::Unavailable);
#[cfg(target_os = "linux")]
#[cfg(wayland_platform)]
if let State::Connected { clipboard, .. } = &state {
clipboard.init_dnd(Box::new(sender));
}

View file

@ -23,6 +23,33 @@ use winit::event::Force;
use winit::icon::IconProvider;
use winit::keyboard::SmolStr;
/// Returns the activation token provided by the compositor, if any.
///
/// The token is single-use, so it is only ever handed out to the first window.
#[cfg(target_os = "linux")]
fn take_activation_token() -> Option<winit::window::ActivationToken> {
use std::sync::Mutex;
use std::sync::OnceLock;
use winit::platform::startup_notify::reset_activation_token_env;
static TOKEN: OnceLock<Mutex<Option<winit::window::ActivationToken>>> =
OnceLock::new();
TOKEN
.get_or_init(|| {
let token = std::env::var("XDG_ACTIVATION_TOKEN")
.ok()
.map(winit::window::ActivationToken::from_raw);
reset_activation_token_env();
Mutex::new(token)
})
.lock()
.ok()
.and_then(|mut token| token.take())
}
/// Converts some [`window::Settings`] into some `WindowAttributes` from `winit`.
pub fn window_attributes(
settings: window::Settings,
@ -182,12 +209,19 @@ pub fn window_attributes(
{
use winit::platform::wayland::WindowAttributesWayland;
attributes = attributes.with_platform_attributes(Box::new(
WindowAttributesWayland::default().with_name(
let mut wayland_attributes = WindowAttributesWayland::default()
.with_name(
&settings.platform_specific.application_id,
&settings.platform_specific.application_id,
),
));
);
if let Some(token) = take_activation_token() {
wayland_attributes =
wayland_attributes.with_activation_token(token);
}
attributes = attributes
.with_platform_attributes(Box::new(wayland_attributes));
}
}
@ -235,6 +269,13 @@ pub fn window_event(
scale_factor,
)))
}
winit::event::PointerSource::TabletTool { .. } => {
let position = position.to_logical::<f64>(scale_factor);
Some(Event::Mouse(mouse::Event::CursorMoved {
position: Point::new(position.x as f32, position.y as f32),
}))
}
_ => None,
},
// TODO how to handle this for Touch? Is it different than a press? Need to double check winit impl
@ -272,6 +313,24 @@ pub fn window_event(
}
}))
}
WindowEvent::PointerButton {
button: ButtonSource::TabletTool { button, .. },
state,
..
} => {
let button: Option<winit::event::MouseButton> = button.into();
button.map(mouse_button).map(|button| {
Event::Mouse(match state {
winit::event::ElementState::Pressed => {
mouse::Event::ButtonPressed(button)
}
winit::event::ElementState::Released => {
mouse::Event::ButtonReleased(button)
}
})
})
}
WindowEvent::PointerButton {
button: ButtonSource::Touch { finger_id, force },
state,
@ -387,19 +446,19 @@ pub fn window_event(
self::modifiers(new_modifiers.state()),
)))
}
WindowEvent::Ime(event) => Some(Event::InputMethod(match event {
Ime::Enabled => input_method::Event::Opened,
Ime::Preedit(content, size) => input_method::Event::Preedit(
WindowEvent::Ime(event) => match event {
Ime::Enabled => Some(input_method::Event::Opened),
Ime::Preedit(content, size) => Some(input_method::Event::Preedit(
content,
size.map(|(start, end)| start..end),
),
Ime::Commit(content) => input_method::Event::Commit(content),
Ime::Disabled => input_method::Event::Closed,
Ime::DeleteSurrounding {
before_bytes,
after_bytes,
} => todo!(),
})),
)),
Ime::Commit(content) => Some(input_method::Event::Commit(content)),
Ime::Disabled => Some(input_method::Event::Closed),
// Upstream iced depends on the winit version which doesn't support
// this kind of ime event.
Ime::DeleteSurrounding { .. } => None,
}
.map(Event::InputMethod),
WindowEvent::Focused(focused) => Some(Event::Window(if focused {
window::Event::Focused
} else {
@ -424,7 +483,7 @@ pub fn window_event(
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
Some(Event::Window(window::Event::Rescaled(scale_factor as f32)))
}
#[cfg(all(feature = "cctk", target_os = "linux"))]
#[cfg(wayland_platform)]
WindowEvent::SuggestedBounds(bounds) => {
let size = bounds.map(|bounds| {
let size = bounds.to_logical(scale_factor);
@ -437,7 +496,7 @@ pub fn window_event(
),
)))
}
#[cfg(all(feature = "cctk", target_os = "linux"))]
#[cfg(wayland_platform)]
WindowEvent::WindowStateChanged => {
use cctk::sctk::reexports::csd_frame::WindowState;
use winit::platform::wayland::WindowExtWayland;
@ -1491,7 +1550,7 @@ pub fn winit_key_code(
})
}
#[cfg(all(feature = "cctk", target_os = "linux"))]
#[cfg(wayland_platform)]
fn winit_native_key_code(
keycode: keyboard::key::NativeCode,
) -> winit::keyboard::NativeKeyCode {
@ -1515,7 +1574,7 @@ fn winit_native_key_code(
}
/// Reconstruct the raw keycode
#[cfg(all(feature = "cctk", target_os = "linux"))]
#[cfg(wayland_platform)]
pub fn physical_to_scancode(physical: keyboard::key::Physical) -> Option<u32> {
let Some(physical_key) = (match physical {
keyboard::key::Physical::Code(code) => {

View file

@ -40,12 +40,6 @@ pub mod clipboard;
pub mod conversion;
pub mod platform_specific;
#[cfg(feature = "program")]
pub mod program;
#[cfg(feature = "system")]
pub mod system;
mod error;
mod proxy;
mod window;
@ -53,7 +47,6 @@ mod window;
pub use clipboard::Clipboard;
pub use error::Error;
pub use proxy::Proxy;
use winit::dpi::LogicalSize;
use winit::dpi::PhysicalPosition;
use winit::dpi::PhysicalSize;
@ -76,6 +69,8 @@ use crate::runtime::user_interface::{self, UserInterface};
use crate::runtime::{Action, Task};
use program::Program;
#[cfg(feature = "program")]
pub use program::Program;
use window::WindowManager;
use rustc_hash::FxHashMap;
@ -98,10 +93,10 @@ where
let event_loop = EventLoop::new().expect("Create event loop");
#[cfg(all(feature = "cctk", target_os = "linux"))]
#[cfg(wayland_platform)]
let is_wayland =
winit::platform::wayland::EventLoopExtWayland::is_wayland(&event_loop);
#[cfg(not(all(feature = "cctk", target_os = "linux")))]
#[cfg(not(wayland_platform))]
let is_wayland = false;
// TODO this is new..
@ -192,11 +187,15 @@ where
receiver: mpsc::UnboundedReceiver<Control>,
error: Option<Error>,
system_theme: Option<oneshot::Sender<theme::Mode>>,
control_sender: mpsc::UnboundedSender<Control>,
#[cfg(feature = "a11y")]
adapters: std::collections::HashMap<window::Id, (u64, iced_accessibility::accesskit_winit::Adapter)>,
control_sender: mpsc::UnboundedSender<Control>,
#[cfg(feature = "a11y")]
adapters: std::collections::HashMap<
window::Id,
(u64, iced_accessibility::accesskit_winit::Adapter),
>,
#[cfg(target_arch = "wasm32")]
is_booted: std::rc::Rc<std::cell::RefCell<bool>>,
#[cfg(target_arch = "wasm32")]
@ -215,6 +214,7 @@ where
id: settings.id,
sender: event_sender,
receiver: control_receiver,
#[cfg(feature = "a11y")]
control_sender: control_sender.clone(),
error: None,
system_theme: Some(system_theme_sender),
@ -263,7 +263,7 @@ where
| winit::event::WindowEvent::Moved(_)
);
#[cfg(all(feature = "cctk", target_os = "linux"))]
#[cfg(wayland_platform)]
{
if matches!(event, WindowEvent::RedrawRequested) {
for id in
@ -501,9 +501,15 @@ where
}
};
}
let window: Arc<dyn winit::window::Window + 'static> = Arc::from(window);
let window: Arc<
dyn winit::window::Window + 'static,
> = Arc::from(window);
#[cfg(feature = "a11y")]
self.init_adapter(event_loop, id, window.clone());
self.init_adapter(
event_loop,
id,
window.clone(),
);
self.process_event(
event_loop,
@ -572,11 +578,10 @@ where
.expect("Send event");
}
Control::Winit(id, e) => {
#[cfg(all(feature = "cctk", target_os = "linux"))]
#[cfg(wayland_platform)]
{
if matches!(e, WindowEvent::RedrawRequested)
{
{
for id in crate::subsurface_widget::subsurface_ids(id) {
_ = self.sender
.unbounded_send(Event::Winit(
@ -597,7 +602,7 @@ where
}
}
}
self.sender
.start_send(Event::Winit(id, e))
.expect("Send event");
@ -610,7 +615,7 @@ where
#[cfg(feature = "a11y")]
Control::InitAdapter(id, window) => {
self.init_adapter(event_loop, id, window);
self.process_event(
event_loop,
Some(Event::A11yAdapter(id)),
@ -618,8 +623,8 @@ where
}
#[cfg(feature = "a11y")]
Control::Cleanup(id) => {
_ = self.adapters.remove(&id);
},
_ = self.adapters.remove(&id);
}
},
_ => {
break;
@ -633,34 +638,35 @@ where
}
}
#[cfg(feature = "a11y")]
fn init_adapter(&mut self, event_loop: &(dyn winit::event_loop::ActiveEventLoop + 'static), id: core::window::Id, window: Arc<dyn winit::window::Window + 'static>) {
#[cfg(feature = "a11y")]
fn init_adapter(
&mut self,
event_loop: &(dyn winit::event_loop::ActiveEventLoop + 'static),
id: core::window::Id,
window: Arc<dyn winit::window::Window + 'static>,
) {
use crate::a11y::*;
use iced_accessibility::accesskit::{
ActivationHandler, Node, NodeId, Role,
Tree, TreeUpdate,
ActivationHandler, Node, NodeId, Role, Tree, TreeUpdate,
};
use iced_accessibility::accesskit_winit::Adapter;
let node_id =
iced_runtime::core::id::window_node_id();
let activation_handler =
WinitActivationHandler {
proxy: self.control_sender.clone(),
title: String::new(),
};
let node_id = iced_runtime::core::id::window_node_id();
let activation_handler = WinitActivationHandler {
proxy: self.control_sender.clone(),
title: String::new(),
};
let action_handler = WinitActionHandler {
id,
proxy: self.control_sender.clone(),
};
let deactivation_handler =
WinitDeactivationHandler {
proxy: self.control_sender.clone(),
};
let deactivation_handler = WinitDeactivationHandler {
proxy: self.control_sender.clone(),
};
_ = self.adapters.insert(
id,
(
@ -674,8 +680,6 @@ where
),
),
);
}
}
@ -770,7 +774,7 @@ async fn run_instance<P>(
let mut platform_specific_handler =
crate::platform_specific::PlatformSpecific::default();
#[cfg(all(feature = "cctk", target_os = "linux"))]
#[cfg(wayland_platform)]
if is_wayland {
platform_specific_handler = platform_specific_handler.with_wayland(
control_sender.clone(),
@ -838,6 +842,30 @@ async fn run_instance<P>(
log::info!("System theme: {system_theme:?}");
macro_rules! run_action {
($action:expr) => {
run_action(
$action,
&program,
&mut runtime,
&mut compositor,
&mut events,
&mut messages,
&mut clipboard,
&mut control_sender,
&mut user_interfaces,
&mut window_manager,
&mut ui_caches,
&mut is_window_opening,
&mut system_theme,
&mut platform_specific_handler,
&mut dnd_surface,
&mut dnd_surface_id,
is_daemon,
)
};
}
'next_event: loop {
// Empty the queue if possible
let event = if let Ok(event) = event_receiver.try_recv() {
@ -857,35 +885,36 @@ async fn run_instance<P>(
exit_on_close_request,
make_visible,
on_open,
resize_border
resize_border,
} => {
#[cfg(all(feature = "cctk", target_os = "linux"))]
#[cfg(wayland_platform)]
platform_specific_handler.send_wayland(
platform_specific::Action::TrackWindow(window.clone(), id),
);
match create_compositor::<P>(
window.clone(),
CreateCompositor {
proxy: &proxy,
runtime: &mut runtime,
display_handle: &display_handle,
graphics_settings: &graphics_settings,
default_fonts: &default_fonts,
},
)
.await
{
Ok(comp) => {
compositor = Some(comp);
}
Err(error) => {
control_sender
.start_send(Control::Crash(
Error::GraphicsCreationFailed(error),
))
.expect("Send control message");
continue;
if compositor.is_none() {
match create_compositor::<P>(
window.clone(),
CreateCompositor {
proxy: &proxy,
runtime: &mut runtime,
display_handle: &display_handle,
graphics_settings: &graphics_settings,
default_fonts: &default_fonts,
},
)
.await
{
Ok(comp) => {
compositor = Some(comp);
}
Err(error) => {
control_sender
.start_send(Control::Crash(
Error::GraphicsCreationFailed(error),
))
.expect("Send control message");
continue;
}
}
}
@ -913,7 +942,7 @@ async fn run_instance<P>(
compositor,
exit_on_close_request,
system_theme,
resize_border
resize_border,
);
window.raw.set_theme(conversion::window_theme(
@ -999,22 +1028,7 @@ async fn run_instance<P>(
}
}
Event::UserEvent(action) => {
let exited = run_action(
action,
&program,
&mut runtime,
&mut compositor,
&mut events,
&mut messages,
&mut clipboard,
&mut control_sender,
&mut user_interfaces,
&mut window_manager,
&mut ui_caches,
&mut is_window_opening,
&mut system_theme,
&mut platform_specific_handler,
);
let exited = run_action!(action);
if exited {
runtime.track(None.into_iter());
}
@ -1025,7 +1039,7 @@ async fn run_instance<P>(
continue;
};
let Some((id, mut window)) =
let Some((id, mut window)) =
window_manager.get_mut_alias(window_id)
else {
continue;
@ -1040,12 +1054,12 @@ async fn run_instance<P>(
))
.expect("Send redraw event");
continue;
}
}
// XX must force update to corner radius before the surface is committed.
#[cfg(all(feature = "cctk", target_os = "linux"))]
#[cfg(wayland_platform)]
if (window.surface_version != window.state.surface_version()
|| window.logical_size() != window.state.logical_size()
) && !crate::subsurface_widget::is_subsurface(window_id)
|| window.logical_size() != window.state.logical_size())
&& !crate::subsurface_widget::is_subsurface(window_id)
{
platform_specific_handler.send_wayland(
platform_specific::Action::ResizeWindow(id),
@ -1156,22 +1170,7 @@ async fn run_instance<P>(
continue;
}
_ = run_action(
action,
&program,
&mut runtime,
&mut compositor,
&mut events,
&mut messages,
&mut clipboard,
&mut control_sender,
&mut user_interfaces,
&mut window_manager,
&mut ui_caches,
&mut is_window_opening,
&mut system_theme,
&mut platform_specific_handler,
);
_ = run_action!(action);
}
for (window_id, window) in window_manager.iter_mut() {
@ -1326,13 +1325,22 @@ async fn run_instance<P>(
continue;
};
// Initiates a drag resize window state when found.
if let Some(func) =
window.drag_resize_window_func.as_mut()
{
if let Some(func) = window.drag_resize_window_func.as_mut() {
if func(window.raw.as_ref(), &event) {
continue;
}
}
// to avoid race with the reported modifier state from the popup of
// this window, don't empty the modifiers, on focus loss
#[cfg(wayland_platform)]
if let winit::event::WindowEvent::ModifiersChanged(modifiers) =
&event
&& modifiers.state().is_empty()
&& platform_specific_handler.has_popup(id)
{
continue;
}
match event {
winit::event::WindowEvent::SurfaceResized(_) => {
window.raw.request_redraw();
@ -1354,22 +1362,9 @@ async fn run_instance<P>(
if matches!(event, winit::event::WindowEvent::CloseRequested)
&& window.exit_on_close_request
{
_ = run_action(
Action::Window(runtime::window::Action::Close(id)),
&program,
&mut runtime,
&mut compositor,
&mut events,
&mut messages,
&mut clipboard,
&mut control_sender,
&mut user_interfaces,
&mut window_manager,
&mut ui_caches,
&mut is_window_opening,
&mut system_theme,
&mut platform_specific_handler,
);
_ = run_action!(Action::Window(
runtime::window::Action::Close(id)
));
} else {
window.state.update(&program, window.raw.as_ref(), &event);
@ -1411,8 +1406,7 @@ async fn run_instance<P>(
true
}
});
let no_window_events = window_events.is_empty();
#[cfg(all(feature = "cctk", target_os = "linux"))]
#[cfg(wayland_platform)]
window_events.push(core::Event::PlatformSpecific(
core::event::PlatformSpecific::Wayland(
core::event::wayland::Event::RequestResize,
@ -1429,8 +1423,7 @@ async fn run_instance<P>(
&mut clipboard,
&mut messages,
);
let mut needs_redraw =
!no_window_events || !messages.is_empty();
let mut needs_redraw = !messages.is_empty();
if let Some(requested_size) =
clipboard.requested_logical_size.lock().unwrap().take()
{
@ -1486,8 +1479,17 @@ async fn run_instance<P>(
mouse_interaction,
..
} => {
let mouse_changed =
window.mouse_interaction != mouse_interaction;
window.update_mouse(mouse_interaction);
if mouse_changed {
window.request_redraw(
core::window::RedrawRequest::NextFrame,
);
}
#[cfg(not(feature = "unconditional-rendering"))]
window.request_redraw(_redraw_request);
}
@ -1547,22 +1549,7 @@ async fn run_instance<P>(
));
for action in actions {
let exited = run_action(
action,
&program,
&mut runtime,
&mut compositor,
&mut events,
&mut messages,
&mut clipboard,
&mut control_sender,
&mut user_interfaces,
&mut window_manager,
&mut ui_caches,
&mut is_window_opening,
&mut system_theme,
&mut platform_specific_handler,
);
let exited = run_action!(action);
if exited {
runtime.track(None.into_iter());
}
@ -1772,7 +1759,6 @@ async fn run_instance<P>(
};
// search windows for widget with operation
if result.is_none() {
log::warn!(
"start_dnd: widget {:?} not found; drag will fail",
@ -2106,6 +2092,11 @@ fn run_action<'a, P, C>(
is_window_opening: &mut bool,
system_theme: &mut theme::Mode,
platform_specific: &mut crate::platform_specific::PlatformSpecific,
dnd_surface: &mut Option<
Arc<Box<dyn HasWindowHandle + Send + Sync + 'static>>,
>,
dnd_surface_id: &mut Option<window::Id>,
is_daemon: bool,
) -> bool
where
P: Program,
@ -2148,7 +2139,7 @@ where
on_open: channel,
})
.expect("Send control action");
*is_window_opening = true;
}
window::Action::Close(id) => {
@ -2159,7 +2150,7 @@ where
_ = control_sender.start_send(Control::Cleanup(id)).ok();
}
let proxy = clipboard.proxy();
#[cfg(all(feature = "cctk", target_os = "linux"))]
#[cfg(wayland_platform)]
platform_specific
.send_wayland(platform_specific::Action::RemoveWindow(id));
if let Some(window) = window_manager.remove(id) {
@ -2190,7 +2181,7 @@ where
));
}
if window_manager.is_empty() {
if window_manager.is_empty() && !is_daemon {
*compositor = None;
}
}
@ -2543,6 +2534,8 @@ where
while let Some(mut operation) = current_operation.take() {
for (id, ui) in interfaces.iter_mut() {
if let Some(window) = window_manager.get_mut(*id) {
operation.as_mut().set_window_id(*id);
ui.operate(&window.renderer, operation.as_mut());
}
}
@ -2620,6 +2613,10 @@ where
}
iced_runtime::dnd::DndAction::EndDnd => {
clipboard.end_dnd();
*dnd_surface = None;
if let Some(id) = dnd_surface_id.take() {
platform_specific.retain_subsurfaces(|other| other != id);
}
}
iced_runtime::dnd::DndAction::PeekDnd(m, channel) => {
let data = clipboard.peek_dnd(m);
@ -2657,10 +2654,10 @@ where
.and_then(|window| theme::Base::palette(window.state.theme()))
});
cached_user_interfaces
.drain()
.filter_map(|(id, cache)| {
let window = window_manager.get_mut(id)?;
window_manager
.iter_mut()
.filter_map(|(id, window)| {
let cache = cached_user_interfaces.remove(&id)?;
Some((
id,
@ -2733,7 +2730,5 @@ fn system_information(
graphics_backend: graphics.backend,
}
}
#[cfg(feature = "program")]
pub use program::Program;
pub use platform_specific::*;

View file

@ -1,24 +1,20 @@
//! Wayland specific shell
//!
use std::{borrow::Cow, collections::HashMap, sync::Arc};
use std::collections::HashMap;
#[cfg(all(feature = "cctk", target_os = "linux"))]
use cctk::sctk::reexports::client::Connection;
use iced_graphics::{Compositor, compositor};
use iced_graphics::compositor;
use iced_runtime::{
core::{Vector, window},
platform_specific, user_interface,
user_interface,
};
use winit::raw_window_handle::HasWindowHandle;
#[cfg(all(feature = "cctk", target_os = "linux"))]
#[cfg(wayland_platform)]
pub mod wayland;
#[cfg(all(feature = "cctk", target_os = "linux"))]
#[cfg(wayland_platform)]
pub use wayland::*;
#[cfg(all(feature = "cctk", target_os = "linux"))]
use wayland_backend::client::Backend;
use crate::{CreateCompositor, Program, WindowManager};
@ -35,7 +31,7 @@ pub type UserInterfaces<'a, P> = HashMap<
#[derive(Debug)]
pub enum Event {
#[cfg(all(feature = "cctk", target_os = "linux"))]
#[cfg(wayland_platform)]
Wayland(sctk_event::SctkEvent),
}
@ -61,7 +57,7 @@ impl SurfaceIdWrapper {
#[derive(Debug, Default)]
pub struct PlatformSpecific {
#[cfg(all(feature = "cctk", target_os = "linux"))]
#[cfg(wayland_platform)]
wayland: WaylandSpecific,
}
@ -71,25 +67,30 @@ impl PlatformSpecific {
action: iced_runtime::platform_specific::Action,
) {
match action {
#[cfg(all(feature = "cctk", target_os = "linux"))]
#[cfg(wayland_platform)]
iced_runtime::platform_specific::Action::Wayland(a) => {
self.send_wayland(wayland::Action::Action(a));
}
}
}
#[cfg(wayland_platform)]
pub(crate) fn has_popup(&self, toplevel: window::Id) -> bool {
self.wayland.has_popup(toplevel)
}
pub(crate) fn retain_subsurfaces<F: Fn(window::Id) -> bool>(
&mut self,
keep: F,
) {
#[cfg(all(feature = "cctk", target_os = "linux"))]
#[cfg(wayland_platform)]
{
self.wayland.retain_subsurfaces(keep);
}
}
pub(crate) fn clear_subsurface_list(&mut self) {
#[cfg(all(feature = "cctk", target_os = "linux"))]
#[cfg(wayland_platform)]
{
self.wayland.clear_subsurface_list();
}
@ -100,7 +101,7 @@ impl PlatformSpecific {
id: window::Id,
window: &dyn HasWindowHandle,
) {
#[cfg(all(feature = "cctk", target_os = "linux"))]
#[cfg(wayland_platform)]
{
use cctk::sctk::reexports::client::{
Proxy, protocol::wl_surface::WlSurface,
@ -151,11 +152,14 @@ impl PlatformSpecific {
pub(crate) fn create_surface(
&mut self,
) -> Option<Box<dyn HasWindowHandle + Send + Sync + 'static>> {
#[cfg(all(feature = "cctk", target_os = "linux"))]
#[cfg(wayland_platform)]
{
return self.wayland.create_surface();
self.wayland.create_surface()
}
#[cfg(not(wayland_platform))]
{
None
}
None
}
pub(crate) fn update_surface_shm(
@ -167,7 +171,7 @@ impl PlatformSpecific {
data: &[u8],
offset: Vector,
) {
#[cfg(all(feature = "cctk", target_os = "linux"))]
#[cfg(wayland_platform)]
{
return self.wayland.update_surface_shm(
surface, width, height, scale, data, offset,
@ -195,7 +199,7 @@ pub(crate) async fn handle_event<'a, 'b, P>(
P: Program,
{
match e {
#[cfg(all(feature = "cctk", target_os = "linux"))]
#[cfg(wayland_platform)]
Event::Wayland(e) => {
platform_specific
.wayland

View file

@ -1,8 +1,8 @@
use crate::core::window::Id as SurfaceId;
use iced_runtime::{
self,
self, Action, Task,
platform_specific::{self, wayland},
task, Action, Task,
task,
};
pub fn request_token(

View file

@ -0,0 +1,17 @@
use iced_futures::core::window;
use iced_runtime::{
self, Action, Task,
platform_specific::{self, wayland},
task,
};
pub fn blur(
id: window::Id,
blur: Option<Vec<iced_runtime::core::Rectangle>>,
) -> Task<()> {
task::oneshot(|_| {
Action::PlatformSpecific(platform_specific::Action::Wayland(
wayland::Action::BlurSurface(id, blur),
))
})
}

View file

@ -1,11 +1,11 @@
use iced_futures::core::window;
use iced_runtime::{
self,
self, Action, Task,
platform_specific::{
self,
wayland::{self, CornerRadius},
},
task, Action, Task,
task,
};
pub fn corner_radius(

View file

@ -1,7 +1,7 @@
use iced_runtime::{
self,
self, Action, Task,
platform_specific::{self, wayland},
task, Action, Task,
task,
};
pub fn inhibit_shortcuts(inhibit: bool) -> Task<()> {

View file

@ -1,8 +1,9 @@
//! Interact with the window of your application.
use crate::core::Rectangle;
use crate::core::window::Id as SurfaceId;
use iced_runtime::{
self,
self, Action, Task,
platform_specific::{
self,
wayland::{
@ -10,7 +11,7 @@ use iced_runtime::{
layer_surface::{IcedMargin, SctkLayerSurfaceSettings},
},
},
task, Action, Task,
task,
};
pub use cctk::sctk::shell::wlr_layer::{Anchor, KeyboardInteractivity, Layer};
@ -114,3 +115,37 @@ pub fn set_layer<Message>(id: SurfaceId, layer: Layer) -> Task<Message> {
)),
))
}
pub fn set_padding<Message>(
id: SurfaceId,
padding: IcedMargin,
) -> Task<Message> {
task::effect(Action::PlatformSpecific(
platform_specific::Action::Wayland(wayland::Action::LayerSurface(
wayland::layer_surface::Action::Padding { id, padding },
)),
))
}
/// <https://wayland.app/protocols/wayland#wl_surface:request:set_input_region>
pub fn set_input_zone<Message>(
id: SurfaceId,
zone: Option<Vec<Rectangle>>,
) -> Task<Message> {
task::effect(Action::PlatformSpecific(
platform_specific::Action::Wayland(wayland::Action::LayerSurface(
wayland::layer_surface::Action::InputZone { id, zone },
)),
))
}
pub fn set_show_on_lock<Message>(
id: SurfaceId,
value: bool,
) -> Task<Message> {
task::effect(Action::PlatformSpecific(
platform_specific::Action::Wayland(wayland::Action::LayerSurface(
wayland::layer_surface::Action::ShowOnLock { id, value },
)),
))
}

View file

@ -1,6 +1,7 @@
//! Interact with the wayland objects of your application.
pub mod activation;
pub mod blur;
pub mod corner_radius;
pub mod keyboard_shortcuts_inhibit;
pub mod layer_surface;

View file

@ -1,7 +1,8 @@
use iced_futures::core::window::Id;
use iced_runtime::{
Action, Task,
platform_specific::{self, wayland},
task, Action, Task,
task,
};
/// Request subscription for overlap notification events on the surface

View file

@ -1,12 +1,15 @@
//! Interact with the popups of your application.
use crate::core::window::Id as SurfaceId;
use iced_runtime::{
self,
self, Action, Task,
platform_specific::{
self,
wayland::{self, popup::SctkPopupSettings},
wayland::{
self,
popup::{SctkPopupSettings, SctkPositioner},
},
},
task, Action, Task,
task,
};
/// <https://wayland.app/protocols/wlr-layer-shell-unstable-v1#zwlr_layer_surface_v1:request:get_popup>
@ -32,6 +35,18 @@ pub fn set_size<Message>(
))
}
/// <https://wayland.app/protocols/xdg-shell#xdg_popup:request:reposition>
pub fn reposition<Message>(
id: SurfaceId,
positioner: SctkPositioner,
) -> Task<Message> {
task::effect(Action::PlatformSpecific(
platform_specific::Action::Wayland(wayland::Action::Popup(
wayland::popup::Action::Reposition { id, positioner },
)),
))
}
/// <https://wayland.app/protocols/xdg-shell#xdg_popup:request:destroy>
pub fn destroy_popup<Message>(id: SurfaceId) -> Task<Message> {
task::effect(Action::PlatformSpecific(

View file

@ -1,9 +1,9 @@
use crate::core::window::Id as SurfaceId;
use cctk::sctk::reexports::client::protocol::wl_output::WlOutput;
use iced_runtime::{
self,
self, Action, Task,
platform_specific::{self, wayland},
task, Action, Task,
task,
};
pub fn lock<Message>() -> Task<Message> {

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