summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock334
-rw-r--r--Cargo.toml4
-rw-r--r--core/src/animation.rs12
-rw-r--r--core/src/element.rs9
-rw-r--r--core/src/event.rs4
-rw-r--r--core/src/input_method.rs221
-rw-r--r--core/src/length.rs6
-rw-r--r--core/src/lib.rs68
-rw-r--r--core/src/mouse/cursor.rs39
-rw-r--r--core/src/overlay.rs2
-rw-r--r--core/src/overlay/element.rs4
-rw-r--r--core/src/overlay/group.rs11
-rw-r--r--core/src/padding.rs6
-rw-r--r--core/src/pixels.rs14
-rw-r--r--core/src/shell.rs76
-rw-r--r--core/src/text.rs27
-rw-r--r--core/src/text/paragraph.rs6
-rw-r--r--core/src/widget.rs2
-rw-r--r--core/src/widget/operation/focusable.rs27
-rw-r--r--core/src/window/redraw_request.rs12
-rw-r--r--examples/bezier_tool/src/main.rs2
-rw-r--r--examples/changelog/src/main.rs26
-rw-r--r--examples/download_progress/src/download.rs23
-rw-r--r--examples/download_progress/src/main.rs54
-rw-r--r--examples/gallery/Cargo.toml3
-rw-r--r--examples/gallery/src/civitai.rs119
-rw-r--r--examples/gallery/src/main.rs190
-rw-r--r--examples/game_of_life/src/main.rs18
-rw-r--r--examples/loading_spinners/src/circular.rs4
-rw-r--r--examples/loading_spinners/src/linear.rs4
-rw-r--r--examples/markdown/Cargo.toml13
-rw-r--r--examples/markdown/build.rs5
-rw-r--r--examples/markdown/fonts/markdown-icons.toml4
-rw-r--r--examples/markdown/fonts/markdown-icons.ttfbin0 -> 5856 bytes
-rw-r--r--examples/markdown/src/icon.rs15
-rw-r--r--examples/markdown/src/main.rs291
-rw-r--r--examples/multi_window/src/main.rs8
-rw-r--r--examples/multitouch/src/main.rs4
-rw-r--r--examples/scrollable/src/main.rs12
-rw-r--r--examples/sierpinski_triangle/src/main.rs2
-rw-r--r--examples/system_information/src/main.rs18
-rw-r--r--examples/toast/src/main.rs6
-rw-r--r--examples/todos/src/main.rs9
-rw-r--r--examples/tour/src/main.rs14
-rw-r--r--examples/websocket/src/echo.rs96
-rw-r--r--futures/src/backend/native/tokio.rs11
-rw-r--r--futures/src/subscription.rs3
-rw-r--r--graphics/src/text/editor.rs31
-rw-r--r--runtime/Cargo.toml3
-rw-r--r--runtime/src/overlay/nested.rs6
-rw-r--r--runtime/src/task.rs20
-rw-r--r--runtime/src/user_interface.rs46
-rw-r--r--src/lib.rs10
-rw-r--r--wgpu/src/lib.rs19
-rw-r--r--widget/src/action.rs14
-rw-r--r--widget/src/button.rs4
-rw-r--r--widget/src/canvas.rs17
-rw-r--r--widget/src/canvas/program.rs4
-rw-r--r--widget/src/checkbox.rs2
-rw-r--r--widget/src/column.rs10
-rw-r--r--widget/src/combo_box.rs24
-rw-r--r--widget/src/container.rs42
-rw-r--r--widget/src/helpers.rs48
-rw-r--r--widget/src/image/viewer.rs11
-rw-r--r--widget/src/keyed/column.rs10
-rw-r--r--widget/src/lazy.rs4
-rw-r--r--widget/src/lazy/component.rs34
-rw-r--r--widget/src/lazy/responsive.rs4
-rw-r--r--widget/src/lib.rs2
-rw-r--r--widget/src/markdown.rs617
-rw-r--r--widget/src/mouse_area.rs8
-rw-r--r--widget/src/overlay/menu.rs4
-rw-r--r--widget/src/pane_grid.rs15
-rw-r--r--widget/src/pane_grid/content.rs4
-rw-r--r--widget/src/pane_grid/title_bar.rs8
-rw-r--r--widget/src/pick_list.rs8
-rw-r--r--widget/src/pin.rs2
-rw-r--r--widget/src/pop.rs26
-rw-r--r--widget/src/radio.rs2
-rw-r--r--widget/src/row.rs12
-rw-r--r--widget/src/scrollable.rs29
-rw-r--r--widget/src/shader.rs18
-rw-r--r--widget/src/shader/program.rs2
-rw-r--r--widget/src/slider.rs2
-rw-r--r--widget/src/stack.rs31
-rw-r--r--widget/src/text/rich.rs59
-rw-r--r--widget/src/text_editor.rs132
-rw-r--r--widget/src/text_input.rs151
-rw-r--r--widget/src/themer.rs4
-rw-r--r--widget/src/toggler.rs2
-rw-r--r--widget/src/tooltip.rs2
-rw-r--r--widget/src/vertical_slider.rs6
-rw-r--r--winit/src/conversion.rs24
-rw-r--r--winit/src/program.rs44
-rw-r--r--winit/src/program/window_manager.rs246
-rw-r--r--winit/src/system.rs8
96 files changed, 2723 insertions, 946 deletions
diff --git a/Cargo.lock b/Cargo.lock
index f38c1efa..072f8e61 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -43,7 +43,7 @@ dependencies = [
"getrandom 0.2.15",
"once_cell",
"version_check",
- "zerocopy",
+ "zerocopy 0.7.35",
]
[[package]]
@@ -198,7 +198,7 @@ dependencies = [
"enumflags2",
"futures-channel",
"futures-util",
- "rand",
+ "rand 0.8.5",
"serde",
"serde_repr",
"url",
@@ -665,10 +665,16 @@ dependencies = [
]
[[package]]
+name = "blurhash"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e79769241dcd44edf79a732545e8b5cec84c247ac060f5252cd51885d093a8fc"
+
+[[package]]
name = "built"
-version = "0.7.5"
+version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c360505aed52b7ec96a3636c3f039d99103c37d1d9b4f7a8c743d3ea9ffcd03b"
+checksum = "73848a43c5d63a1251d17adf6c2bf78aa94830e60a335a95eeea45d6ba9e1e4d"
[[package]]
name = "bumpalo"
@@ -716,15 +722,15 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]]
name = "bytes"
-version = "1.9.0"
+version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
+checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9"
[[package]]
name = "bytesize"
-version = "1.3.0"
+version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a3e368af43e418a04d52505cf3dbc23dda4e3407ae2fa99fd0e4f308ce546acc"
+checksum = "2d2c12f985c78475a6b8d629afd0c360260ef34cfef52efccdcfd31972f81c2e"
[[package]]
name = "cairo-sys-rs"
@@ -770,9 +776,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]]
name = "cc"
-version = "1.2.11"
+version = "1.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e4730490333d58093109dc02c23174c3f4d490998c3fed3cc8e82d57afedb9cf"
+checksum = "0c3d1b2e905a3a7b00a6141adb0e4c0bb941d11caf55349d863942a1cc44e3c9"
dependencies = [
"jobserver",
"libc",
@@ -877,18 +883,18 @@ dependencies = [
[[package]]
name = "clap"
-version = "4.5.27"
+version = "4.5.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "769b0145982b4b48713e01ec42d61614425f27b7058bda7180a3a41f30104796"
+checksum = "8acebd8ad879283633b343856142139f2da2317c96b05b4dd6181c61e2480184"
dependencies = [
"clap_builder",
]
[[package]]
name = "clap_builder"
-version = "4.5.27"
+version = "4.5.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7"
+checksum = "f6ba32cbda51c7e1dfd49acc1457ba1a7dec5b64fe360e828acb13ca8dc9c2f9"
dependencies = [
"anstyle",
"clap_lex",
@@ -1245,7 +1251,7 @@ dependencies = [
"glam",
"iced",
"image",
- "rand",
+ "rand 0.8.5",
]
[[package]]
@@ -1271,9 +1277,9 @@ dependencies = [
[[package]]
name = "data-encoding"
-version = "2.7.0"
+version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0e60eed09d8c01d3cee5b7d30acb059b76614c918fa0f992e0dd6eeb10daad6f"
+checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010"
[[package]]
name = "data-url"
@@ -1467,9 +1473,9 @@ dependencies = [
[[package]]
name = "equivalent"
-version = "1.0.1"
+version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
@@ -1863,11 +1869,13 @@ dependencies = [
name = "gallery"
version = "0.1.0"
dependencies = [
+ "blurhash",
"bytes",
"iced",
"image",
"reqwest",
"serde",
+ "sipper",
"tokio",
]
@@ -1877,7 +1885,7 @@ version = "0.1.0"
dependencies = [
"iced",
"itertools 0.12.1",
- "rustc-hash 2.1.0",
+ "rustc-hash 2.1.1",
"tokio",
"tracing-subscriber",
]
@@ -2073,7 +2081,7 @@ dependencies = [
"cosmic-text",
"etagere",
"lru",
- "rustc-hash 2.1.0",
+ "rustc-hash 2.1.1",
"wgpu",
]
@@ -2423,7 +2431,7 @@ dependencies = [
"http 1.2.0",
"hyper 1.6.0",
"hyper-util",
- "rustls 0.23.22",
+ "rustls 0.23.23",
"rustls-pki-types",
"tokio",
"tokio-rustls 0.26.1",
@@ -2517,7 +2525,7 @@ dependencies = [
"log",
"num-traits",
"palette",
- "rustc-hash 2.1.0",
+ "rustc-hash 2.1.1",
"smol_str",
"thiserror 1.0.69",
"web-time",
@@ -2531,7 +2539,7 @@ dependencies = [
"futures",
"iced_core",
"log",
- "rustc-hash 2.1.0",
+ "rustc-hash 2.1.1",
"smol",
"tokio",
"wasm-bindgen-futures",
@@ -2553,7 +2561,7 @@ dependencies = [
"log",
"lyon_path",
"raw-window-handle 0.6.2",
- "rustc-hash 2.1.0",
+ "rustc-hash 2.1.1",
"thiserror 1.0.69",
"unicode-segmentation",
]
@@ -2585,6 +2593,7 @@ dependencies = [
"iced_core",
"iced_futures",
"raw-window-handle 0.6.2",
+ "sipper",
"thiserror 1.0.69",
]
@@ -2609,7 +2618,7 @@ dependencies = [
"kurbo 0.10.4",
"log",
"resvg",
- "rustc-hash 2.1.0",
+ "rustc-hash 2.1.1",
"softbuffer",
"tiny-skia",
]
@@ -2628,7 +2637,7 @@ dependencies = [
"log",
"lyon",
"resvg",
- "rustc-hash 2.1.0",
+ "rustc-hash 2.1.1",
"thiserror 1.0.69",
"wgpu",
]
@@ -2645,7 +2654,7 @@ dependencies = [
"ouroboros",
"pulldown-cmark",
"qrcode",
- "rustc-hash 2.1.0",
+ "rustc-hash 2.1.1",
"thiserror 1.0.69",
"unicode-segmentation",
"url",
@@ -2659,7 +2668,7 @@ dependencies = [
"iced_graphics",
"iced_runtime",
"log",
- "rustc-hash 2.1.0",
+ "rustc-hash 2.1.1",
"sysinfo",
"thiserror 1.0.69",
"tracing",
@@ -3299,7 +3308,10 @@ name = "markdown"
version = "0.1.0"
dependencies = [
"iced",
+ "image",
"open",
+ "reqwest",
+ "tokio",
]
[[package]]
@@ -3384,9 +3396,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
-version = "0.8.3"
+version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924"
+checksum = "b3b1c9bd4fe1f0f8b387f6eb9eb3b4a1aa26185e5750efb9140301703f62cd1b"
dependencies = [
"adler2",
"simd-adler32",
@@ -3912,9 +3924,9 @@ dependencies = [
[[package]]
name = "once_cell"
-version = "1.20.2"
+version = "1.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
+checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e"
[[package]]
name = "onig"
@@ -3957,9 +3969,9 @@ dependencies = [
[[package]]
name = "openssl"
-version = "0.10.69"
+version = "0.10.70"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f5e534d133a060a3c19daec1eb3e98ec6f4685978834f2dbadfe2ec215bab64e"
+checksum = "61cfb4e166a8bb8c9b55c500bc2308550148ece889be90f609377e58140f42c6"
dependencies = [
"bitflags 2.8.0",
"cfg-if",
@@ -3989,9 +4001,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
[[package]]
name = "openssl-sys"
-version = "0.9.104"
+version = "0.9.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741"
+checksum = "8b22d5b84be05a8d6947c7cb71f7c849aa0f112acd4bf51c2a7c1c988ac0a9dc"
dependencies = [
"cc",
"libc",
@@ -4170,7 +4182,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
dependencies = [
"phf_shared",
- "rand",
+ "rand 0.8.5",
]
[[package]]
@@ -4210,18 +4222,18 @@ checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315"
[[package]]
name = "pin-project"
-version = "1.1.8"
+version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e2ec53ad785f4d35dac0adea7f7dc6f1bb277ad84a680c7afefeae05d1f5916"
+checksum = "dfe2e71e1471fe07709406bf725f710b02927c9c54b2b5b2ec0e8087d97c327d"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
-version = "1.1.8"
+version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d56a66c0c55993aa927429d0f8a0abfd74f084e4d9c192cffed01e418d83eefb"
+checksum = "f6e859e6e5bd50440ab63c47e3ebabc90f26251f7c73c3d3e837b74a1cc3fa67"
dependencies = [
"proc-macro2",
"quote",
@@ -4317,7 +4329,7 @@ version = "0.1.0"
dependencies = [
"getrandom 0.2.15",
"iced",
- "rand",
+ "rand 0.8.5",
"reqwest",
"serde",
"serde_json",
@@ -4366,7 +4378,7 @@ version = "0.2.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
dependencies = [
- "zerocopy",
+ "zerocopy 0.7.35",
]
[[package]]
@@ -4513,8 +4525,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
- "rand_chacha",
- "rand_core",
+ "rand_chacha 0.3.1",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
+dependencies = [
+ "rand_chacha 0.9.0",
+ "rand_core 0.9.0",
+ "zerocopy 0.8.18",
]
[[package]]
@@ -4524,7 +4547,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
- "rand_core",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.9.0",
]
[[package]]
@@ -4537,6 +4570,16 @@ dependencies = [
]
[[package]]
+name = "rand_core"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b08f3c9802962f7e1b25113931d94f43ed9725bebc59db9d0c3e9a23b67e15ff"
+dependencies = [
+ "getrandom 0.3.1",
+ "zerocopy 0.8.18",
+]
+
+[[package]]
name = "range-alloc"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4574,8 +4617,8 @@ dependencies = [
"once_cell",
"paste",
"profiling",
- "rand",
- "rand_chacha",
+ "rand 0.8.5",
+ "rand_chacha 0.3.1",
"simd_helpers",
"system-deps",
"thiserror 1.0.69",
@@ -4800,15 +4843,14 @@ dependencies = [
[[package]]
name = "ring"
-version = "0.17.8"
+version = "0.17.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
+checksum = "e75ec5e92c4d8aede845126adc388046234541629e76029599ed35a003c7ed24"
dependencies = [
"cc",
"cfg-if",
"getrandom 0.2.15",
"libc",
- "spin",
"untrusted",
"windows-sys 0.52.0",
]
@@ -4833,9 +4875,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustc-hash"
-version = "2.1.0"
+version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497"
+checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]]
name = "rustix"
@@ -4880,9 +4922,9 @@ dependencies = [
[[package]]
name = "rustls"
-version = "0.23.22"
+version = "0.23.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9fb9263ab4eb695e42321db096e3b8fbd715a59b154d5c88d82db2175b681ba7"
+checksum = "47796c98c480fce5406ef69d1c76378375492c3b0a0de587be0c1d9feb12f395"
dependencies = [
"once_cell",
"rustls-pki-types",
@@ -5141,7 +5183,7 @@ name = "sierpinski_triangle"
version = "0.1.0"
dependencies = [
"iced",
- "rand",
+ "rand 0.8.5",
]
[[package]]
@@ -5184,6 +5226,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
[[package]]
+name = "sipper"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bccb4192828b3d9a08e0b5a73f17795080dfb278b50190216e3ae2132cf4f95"
+dependencies = [
+ "futures",
+ "pin-project-lite",
+]
+
+[[package]]
name = "skrifa"
version = "0.22.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5220,9 +5272,9 @@ dependencies = [
[[package]]
name = "smallvec"
-version = "1.13.2"
+version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
+checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
[[package]]
name = "smithay-client-toolkit"
@@ -5343,7 +5395,7 @@ name = "solar_system"
version = "0.1.0"
dependencies = [
"iced",
- "rand",
+ "rand 0.8.5",
"tracing-subscriber",
]
@@ -5501,17 +5553,16 @@ dependencies = [
[[package]]
name = "sysinfo"
-version = "0.30.13"
+version = "0.33.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0a5b4ddaee55fb2bea2bf0e5000747e5f5c0de765e5a5ff87f4cd106439f4bb3"
+checksum = "4fc858248ea01b66f19d8e8a6d55f41deaf91e9d495246fd01368d99935c6c01"
dependencies = [
- "cfg-if",
"core-foundation-sys",
"libc",
+ "memchr",
"ntapi",
- "once_cell",
"rayon",
- "windows 0.52.0",
+ "windows 0.57.0",
]
[[package]]
@@ -5590,7 +5641,7 @@ name = "the_matrix"
version = "0.1.0"
dependencies = [
"iced",
- "rand",
+ "rand 0.8.5",
"tracing-subscriber",
]
@@ -5826,7 +5877,7 @@ version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37"
dependencies = [
- "rustls 0.23.22",
+ "rustls 0.23.23",
"tokio",
]
@@ -5857,9 +5908,9 @@ dependencies = [
[[package]]
name = "toml"
-version = "0.8.19"
+version = "0.8.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e"
+checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148"
dependencies = [
"serde",
"serde_spanned",
@@ -5878,15 +5929,15 @@ dependencies = [
[[package]]
name = "toml_edit"
-version = "0.22.23"
+version = "0.22.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02a8b472d1a3d7c18e2d61a489aee3453fd9031c33e4f55bd533f4a7adca1bee"
+checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
- "winnow 0.7.0",
+ "winnow",
]
[[package]]
@@ -6027,7 +6078,7 @@ dependencies = [
"http 1.2.0",
"httparse",
"log",
- "rand",
+ "rand 0.8.5",
"rustls 0.22.4",
"rustls-pki-types",
"sha1",
@@ -6197,12 +6248,13 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "uuid"
-version = "1.12.1"
+version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b3758f5e68192bb96cc8f9b7e2c2cfdabb435499a28499a42f8f984092adad4b"
+checksum = "ced87ca4be083373936a67f8de945faa23b6b42384bd5b64434850802c6dccd0"
dependencies = [
- "getrandom 0.2.15",
- "rand",
+ "getrandom 0.3.1",
+ "js-sys",
+ "rand 0.9.0",
"serde",
"wasm-bindgen",
]
@@ -6763,11 +6815,11 @@ dependencies = [
[[package]]
name = "windows"
-version = "0.52.0"
+version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be"
+checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143"
dependencies = [
- "windows-core 0.52.0",
+ "windows-core 0.57.0",
"windows-targets 0.52.6",
]
@@ -6792,19 +6844,42 @@ dependencies = [
[[package]]
name = "windows-core"
+version = "0.57.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d"
+dependencies = [
+ "windows-implement 0.57.0",
+ "windows-interface 0.57.0",
+ "windows-result 0.1.2",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-core"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
dependencies = [
- "windows-implement",
- "windows-interface",
- "windows-result",
+ "windows-implement 0.58.0",
+ "windows-interface 0.58.0",
+ "windows-result 0.2.0",
"windows-strings",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-implement"
+version = "0.57.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "windows-implement"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
@@ -6816,6 +6891,17 @@ dependencies = [
[[package]]
name = "windows-interface"
+version = "0.57.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "windows-interface"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
@@ -6831,13 +6917,22 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0"
dependencies = [
- "windows-result",
+ "windows-result 0.2.0",
"windows-strings",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-result"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-result"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
@@ -6851,7 +6946,7 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
dependencies = [
- "windows-result",
+ "windows-result 0.2.0",
"windows-targets 0.52.6",
]
@@ -7122,18 +7217,9 @@ dependencies = [
[[package]]
name = "winnow"
-version = "0.6.26"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e90edd2ac1aa278a5c4599b1d89cf03074b610800f866d4026dc199d7929a28"
-dependencies = [
- "memchr",
-]
-
-[[package]]
-name = "winnow"
-version = "0.7.0"
+version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7e49d2d35d3fad69b39b94139037ecfb4f359f08958b9c11e7315ce770462419"
+checksum = "59690dea168f2198d1a3b0cac23b8063efcd11012f10ae4698f284808c8ef603"
dependencies = [
"memchr",
]
@@ -7295,9 +7381,9 @@ dependencies = [
[[package]]
name = "zbus"
-version = "5.3.1"
+version = "5.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2494e4b3f44d8363eef79a8a75fc0649efb710eef65a66b5e688a5eb4afe678a"
+checksum = "59c333f648ea1b647bc95dc1d34807c8e25ed7a6feff3394034dc4776054b236"
dependencies = [
"async-broadcast",
"async-executor",
@@ -7312,7 +7398,7 @@ dependencies = [
"enumflags2",
"event-listener 5.4.0",
"futures-core",
- "futures-util",
+ "futures-lite 2.6.0",
"hex",
"nix",
"ordered-stream",
@@ -7322,7 +7408,7 @@ dependencies = [
"tracing",
"uds_windows",
"windows-sys 0.59.0",
- "winnow 0.6.26",
+ "winnow",
"xdg-home",
"zbus_macros",
"zbus_names",
@@ -7331,9 +7417,9 @@ dependencies = [
[[package]]
name = "zbus_macros"
-version = "5.3.1"
+version = "5.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "445efc01929302aee95e2b25bbb62a301ea8a6369466e4278e58e7d1dfb23631"
+checksum = "f325ad10eb0d0a3eb060203494c3b7ec3162a01a59db75d2deee100339709fc0"
dependencies = [
"proc-macro-crate",
"proc-macro2",
@@ -7346,13 +7432,13 @@ dependencies = [
[[package]]
name = "zbus_names"
-version = "4.1.1"
+version = "4.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "519629a3f80976d89c575895b05677cbc45eaf9f70d62a364d819ba646409cc8"
+checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97"
dependencies = [
"serde",
"static_assertions",
- "winnow 0.6.26",
+ "winnow",
"zvariant",
]
@@ -7369,7 +7455,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
dependencies = [
"byteorder",
- "zerocopy-derive",
+ "zerocopy-derive 0.7.35",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.8.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79386d31a42a4996e3336b0919ddb90f81112af416270cff95b5f5af22b839c2"
+dependencies = [
+ "zerocopy-derive 0.8.18",
]
[[package]]
@@ -7384,6 +7479,17 @@ dependencies = [
]
[[package]]
+name = "zerocopy-derive"
+version = "0.8.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76331675d372f91bf8d17e13afbd5fe639200b73d01f0fc748bb059f9cca2db7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
name = "zerofrom"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -7458,25 +7564,25 @@ dependencies = [
[[package]]
name = "zvariant"
-version = "5.2.0"
+version = "5.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "55e6b9b5f1361de2d5e7d9fd1ee5f6f7fcb6060618a1f82f3472f58f2b8d4be9"
+checksum = "b2df9ee044893fcffbdc25de30546edef3e32341466811ca18421e3cd6c5a3ac"
dependencies = [
"endi",
"enumflags2",
"serde",
"static_assertions",
"url",
- "winnow 0.6.26",
+ "winnow",
"zvariant_derive",
"zvariant_utils",
]
[[package]]
name = "zvariant_derive"
-version = "5.2.0"
+version = "5.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "573a8dd76961957108b10f7a45bac6ab1ea3e9b7fe01aff88325dc57bb8f5c8b"
+checksum = "74170caa85b8b84cc4935f2d56a57c7a15ea6185ccdd7eadb57e6edd90f94b2f"
dependencies = [
"proc-macro-crate",
"proc-macro2",
@@ -7487,14 +7593,14 @@ dependencies = [
[[package]]
name = "zvariant_utils"
-version = "3.1.0"
+version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ddd46446ea2a1f353bfda53e35f17633afa79f4fe290a611c94645c69fe96a50"
+checksum = "e16edfee43e5d7b553b77872d99bc36afdda75c223ca7ad5e3fbecd82ca5fc34"
dependencies = [
"proc-macro2",
"quote",
"serde",
"static_assertions",
"syn",
- "winnow 0.6.26",
+ "winnow",
]
diff --git a/Cargo.toml b/Cargo.toml
index 4d6531e1..364f1b5c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -172,11 +172,12 @@ raw-window-handle = "0.6"
resvg = "0.42"
rustc-hash = "2.0"
sha2 = "0.10"
+sipper = "0.1"
smol = "1.0"
smol_str = "0.2"
softbuffer = "0.4"
syntect = "5.1"
-sysinfo = "0.30"
+sysinfo = "0.33"
thiserror = "1.0"
tiny-skia = "0.11"
tokio = "1.0"
@@ -200,6 +201,7 @@ unused_results = "deny"
[workspace.lints.clippy]
type-complexity = "allow"
+map-entry = "allow"
semicolon_if_nothing_returned = "deny"
trivially-copy-pass-by-ref = "deny"
default_trait_access = "deny"
diff --git a/core/src/animation.rs b/core/src/animation.rs
index 258fd084..14cbb5c3 100644
--- a/core/src/animation.rs
+++ b/core/src/animation.rs
@@ -13,6 +13,7 @@ where
T: Clone + Copy + PartialEq + Float,
{
raw: lilt::Animated<T, Instant>,
+ duration: Duration, // TODO: Expose duration getter in `lilt`
}
impl<T> Animation<T>
@@ -23,6 +24,7 @@ where
pub fn new(state: T) -> Self {
Self {
raw: lilt::Animated::new(state),
+ duration: Duration::from_millis(100),
}
}
@@ -58,6 +60,7 @@ where
/// Sets the duration of the [`Animation`] to the given value.
pub fn duration(mut self, duration: Duration) -> Self {
self.raw = self.raw.duration(duration.as_secs_f32() * 1_000.0);
+ self.duration = duration;
self
}
@@ -133,4 +136,13 @@ impl Animation<bool> {
{
self.raw.animate_bool(start, end, at)
}
+
+ /// Returns the remaining [`Duration`] of the [`Animation`].
+ pub fn remaining(&self, at: Instant) -> Duration {
+ Duration::from_secs_f32(self.interpolate(
+ self.duration.as_secs_f32(),
+ 0.0,
+ at,
+ ))
+ }
}
diff --git a/core/src/element.rs b/core/src/element.rs
index 82ba753b..b7d51aeb 100644
--- a/core/src/element.rs
+++ b/core/src/element.rs
@@ -93,6 +93,7 @@ impl<'a, Message, Theme, Renderer> Element<'a, Message, Theme, Renderer> {
///
/// ```no_run
/// # mod iced {
+ /// # pub use iced_core::Function;
/// # pub type Element<'a, Message> = iced_core::Element<'a, Message, iced_core::Theme, ()>;
/// #
/// # pub mod widget {
@@ -119,7 +120,7 @@ impl<'a, Message, Theme, Renderer> Element<'a, Message, Theme, Renderer> {
/// use counter::Counter;
///
/// use iced::widget::row;
- /// use iced::Element;
+ /// use iced::{Element, Function};
///
/// struct ManyCounters {
/// counters: Vec<Counter>,
@@ -142,7 +143,7 @@ impl<'a, Message, Theme, Renderer> Element<'a, Message, Theme, Renderer> {
/// // Here we turn our `Element<counter::Message>` into
/// // an `Element<Message>` by combining the `index` and the
/// // message of the `element`.
- /// counter.map(move |message| Message::Counter(index, message))
+ /// counter.map(Message::Counter.with(index))
/// }),
/// )
/// .into()
@@ -311,7 +312,7 @@ where
fn update(
&mut self,
tree: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
@@ -447,7 +448,7 @@ where
fn update(
&mut self,
state: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
diff --git a/core/src/event.rs b/core/src/event.rs
index b6cf321e..7f0ab914 100644
--- a/core/src/event.rs
+++ b/core/src/event.rs
@@ -1,4 +1,5 @@
//! Handle events of a user interface.
+use crate::input_method;
use crate::keyboard;
use crate::mouse;
use crate::touch;
@@ -23,6 +24,9 @@ pub enum Event {
/// A touch event
Touch(touch::Event),
+
+ /// An input method event
+ InputMethod(input_method::Event),
}
/// The status of an [`Event`] after being processed.
diff --git a/core/src/input_method.rs b/core/src/input_method.rs
new file mode 100644
index 00000000..cd8d459d
--- /dev/null
+++ b/core/src/input_method.rs
@@ -0,0 +1,221 @@
+//! Listen to input method events.
+use crate::{Pixels, Point};
+
+use std::ops::Range;
+
+/// The input method strategy of a widget.
+#[derive(Debug, Clone, PartialEq)]
+pub enum InputMethod<T = String> {
+ /// Input method is disabled.
+ Disabled,
+ /// Input method is enabled.
+ Enabled {
+ /// The position at which the input method dialog should be placed.
+ position: Point,
+ /// The [`Purpose`] of the input method.
+ purpose: Purpose,
+ /// The preedit to overlay on top of the input method dialog, if needed.
+ ///
+ /// Ideally, your widget will show pre-edits on-the-spot; but, since that can
+ /// be tricky, you can instead provide the current pre-edit here and the
+ /// runtime will display it as an overlay (i.e. "Over-the-spot IME").
+ preedit: Option<Preedit<T>>,
+ },
+}
+
+/// The pre-edit of an [`InputMethod`].
+#[derive(Debug, Clone, PartialEq, Default)]
+pub struct Preedit<T = String> {
+ /// The current content.
+ pub content: T,
+ /// The selected range of the content.
+ pub selection: Option<Range<usize>>,
+ /// The text size of the content.
+ pub text_size: Option<Pixels>,
+}
+
+impl<T> Preedit<T> {
+ /// Creates a new empty [`Preedit`].
+ pub fn new() -> Self
+ where
+ T: Default,
+ {
+ Self::default()
+ }
+
+ /// Turns a [`Preedit`] into its owned version.
+ pub fn to_owned(&self) -> Preedit
+ where
+ T: AsRef<str>,
+ {
+ Preedit {
+ content: self.content.as_ref().to_owned(),
+ selection: self.selection.clone(),
+ text_size: self.text_size,
+ }
+ }
+}
+
+impl Preedit {
+ /// Borrows the contents of a [`Preedit`].
+ pub fn as_ref(&self) -> Preedit<&str> {
+ Preedit {
+ content: &self.content,
+ selection: self.selection.clone(),
+ text_size: self.text_size,
+ }
+ }
+}
+
+/// The purpose of an [`InputMethod`].
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
+pub enum Purpose {
+ /// No special hints for the IME (default).
+ #[default]
+ Normal,
+ /// The IME is used for secure input (e.g. passwords).
+ Secure,
+ /// The IME is used to input into a terminal.
+ ///
+ /// For example, that could alter OSK on Wayland to show extra buttons.
+ Terminal,
+}
+
+impl InputMethod {
+ /// Merges two [`InputMethod`] strategies, prioritizing the first one when both open:
+ /// ```
+ /// # use iced_core::input_method::{InputMethod, Purpose, Preedit};
+ /// # use iced_core::Point;
+ ///
+ /// let open = InputMethod::Enabled {
+ /// position: Point::ORIGIN,
+ /// purpose: Purpose::Normal,
+ /// preedit: Some(Preedit { content: "1".to_owned(), selection: None, text_size: None }),
+ /// };
+ ///
+ /// let open_2 = InputMethod::Enabled {
+ /// position: Point::ORIGIN,
+ /// purpose: Purpose::Secure,
+ /// preedit: Some(Preedit { content: "2".to_owned(), selection: None, text_size: None }),
+ /// };
+ ///
+ /// let mut ime = InputMethod::Disabled;
+ ///
+ /// ime.merge(&open);
+ /// assert_eq!(ime, open);
+ ///
+ /// ime.merge(&open_2);
+ /// assert_eq!(ime, open);
+ /// ```
+ pub fn merge<T: AsRef<str>>(&mut self, other: &InputMethod<T>) {
+ if let InputMethod::Enabled { .. } = self {
+ return;
+ }
+
+ *self = other.to_owned();
+ }
+
+ /// Returns true if the [`InputMethod`] is open.
+ pub fn is_enabled(&self) -> bool {
+ matches!(self, Self::Enabled { .. })
+ }
+}
+
+impl<T> InputMethod<T> {
+ /// Turns an [`InputMethod`] into its owned version.
+ pub fn to_owned(&self) -> InputMethod
+ where
+ T: AsRef<str>,
+ {
+ match self {
+ Self::Disabled => InputMethod::Disabled,
+ Self::Enabled {
+ position,
+ purpose,
+ preedit,
+ } => InputMethod::Enabled {
+ position: *position,
+ purpose: *purpose,
+ preedit: preedit.as_ref().map(Preedit::to_owned),
+ },
+ }
+ }
+}
+
+/// Describes [input method](https://en.wikipedia.org/wiki/Input_method) events.
+///
+/// This is also called a "composition event".
+///
+/// Most keypresses using a latin-like keyboard layout simply generate a
+/// [`keyboard::Event::KeyPressed`](crate::keyboard::Event::KeyPressed).
+/// However, one couldn't possibly have a key for every single
+/// unicode character that the user might want to type. The solution operating systems employ is
+/// to allow the user to type these using _a sequence of keypresses_ instead.
+///
+/// A prominent example of this is accents—many keyboard layouts allow you to first click the
+/// "accent key", and then the character you want to apply the accent to. In this case, some
+/// platforms will generate the following event sequence:
+///
+/// ```ignore
+/// // Press "`" key
+/// Ime::Preedit("`", Some((0, 0)))
+/// // Press "E" key
+/// Ime::Preedit("", None) // Synthetic event generated to clear preedit.
+/// Ime::Commit("é")
+/// ```
+///
+/// Additionally, certain input devices are configured to display a candidate box that allow the
+/// user to select the desired character interactively. (To properly position this box, you must use
+/// [`Shell::request_input_method`](crate::Shell::request_input_method).)
+///
+/// An example of a keyboard layout which uses candidate boxes is pinyin. On a latin keyboard the
+/// following event sequence could be obtained:
+///
+/// ```ignore
+/// // Press "A" key
+/// Ime::Preedit("a", Some((1, 1)))
+/// // Press "B" key
+/// Ime::Preedit("a b", Some((3, 3)))
+/// // Press left arrow key
+/// Ime::Preedit("a b", Some((1, 1)))
+/// // Press space key
+/// Ime::Preedit("啊b", Some((3, 3)))
+/// // Press space key
+/// Ime::Preedit("", None) // Synthetic event generated to clear preedit.
+/// Ime::Commit("啊不")
+/// ```
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub enum Event {
+ /// Notifies when the IME was opened.
+ ///
+ /// After getting this event you could receive [`Preedit`][Self::Preedit] and
+ /// [`Commit`][Self::Commit] events. You should also start performing IME related requests
+ /// like [`Shell::request_input_method`].
+ ///
+ /// [`Shell::request_input_method`]: crate::Shell::request_input_method
+ Opened,
+
+ /// Notifies when a new composing text should be set at the cursor position.
+ ///
+ /// The value represents a pair of the preedit string and the cursor begin position and end
+ /// position. When it's `None`, the cursor should be hidden. When `String` is an empty string
+ /// this indicates that preedit was cleared.
+ ///
+ /// The cursor range is byte-wise indexed.
+ Preedit(String, Option<Range<usize>>),
+
+ /// Notifies when text should be inserted into the editor widget.
+ ///
+ /// Right before this event, an empty [`Self::Preedit`] event will be issued.
+ Commit(String),
+
+ /// Notifies when the IME was disabled.
+ ///
+ /// After receiving this event you won't get any more [`Preedit`][Self::Preedit] or
+ /// [`Commit`][Self::Commit] events until the next [`Opened`][Self::Opened] event. You should
+ /// also stop issuing IME related requests like [`Shell::request_input_method`] and clear
+ /// pending preedit text.
+ ///
+ /// [`Shell::request_input_method`]: crate::Shell::request_input_method
+ Closed,
+}
diff --git a/core/src/length.rs b/core/src/length.rs
index 5f24169f..363833c4 100644
--- a/core/src/length.rs
+++ b/core/src/length.rs
@@ -77,8 +77,8 @@ impl From<f32> for Length {
}
}
-impl From<u16> for Length {
- fn from(units: u16) -> Self {
- Length::Fixed(f32::from(units))
+impl From<u32> for Length {
+ fn from(units: u32) -> Self {
+ Length::Fixed(units as f32)
}
}
diff --git a/core/src/lib.rs b/core/src/lib.rs
index 16b3aa0f..03cc0632 100644
--- a/core/src/lib.rs
+++ b/core/src/lib.rs
@@ -17,6 +17,7 @@ pub mod event;
pub mod font;
pub mod gradient;
pub mod image;
+pub mod input_method;
pub mod keyboard;
pub mod layout;
pub mod mouse;
@@ -61,6 +62,7 @@ pub use event::Event;
pub use font::Font;
pub use gradient::Gradient;
pub use image::Image;
+pub use input_method::InputMethod;
pub use layout::Layout;
pub use length::Length;
pub use overlay::Overlay;
@@ -82,3 +84,69 @@ pub use vector::Vector;
pub use widget::Widget;
pub use smol_str::SmolStr;
+
+/// A function that can _never_ be called.
+///
+/// This is useful to turn generic types into anything
+/// you want by coercing them into a type with no possible
+/// values.
+pub fn never<T>(never: std::convert::Infallible) -> T {
+ match never {}
+}
+
+/// A trait extension for binary functions (`Fn(A, B) -> O`).
+///
+/// It enables you to use a bunch of nifty functional programming paradigms
+/// that work well with iced.
+pub trait Function<A, B, O> {
+ /// Applies the given first argument to a binary function and returns
+ /// a new function that takes the other argument.
+ ///
+ /// This lets you partially "apply" a function—equivalent to currying,
+ /// but it only works with binary functions. If you want to apply an
+ /// arbitrary number of arguments, create a little struct for them.
+ ///
+ /// # When is this useful?
+ /// Sometimes you will want to identify the source or target
+ /// of some message in your user interface. This can be achieved through
+ /// normal means by defining a closure and moving the identifier
+ /// inside:
+ ///
+ /// ```rust
+ /// # let element: Option<()> = Some(());
+ /// # enum Message { ButtonPressed(u32, ()) }
+ /// let id = 123;
+ ///
+ /// # let _ = {
+ /// element.map(move |result| Message::ButtonPressed(id, result))
+ /// # };
+ /// ```
+ ///
+ /// That's quite a mouthful. [`with`](Self::with) lets you write:
+ ///
+ /// ```rust
+ /// # use iced_core::Function;
+ /// # let element: Option<()> = Some(());
+ /// # enum Message { ButtonPressed(u32, ()) }
+ /// let id = 123;
+ ///
+ /// # let _ = {
+ /// element.map(Message::ButtonPressed.with(id))
+ /// # };
+ /// ```
+ ///
+ /// Effectively creating the same closure that partially applies
+ /// the `id` to the message—but much more concise!
+ fn with(self, prefix: A) -> impl Fn(B) -> O;
+}
+
+impl<F, A, B, O> Function<A, B, O> for F
+where
+ F: Fn(A, B) -> O,
+ Self: Sized,
+ A: Copy,
+{
+ fn with(self, prefix: A) -> impl Fn(B) -> O {
+ move |result| self(prefix, result)
+ }
+}
diff --git a/core/src/mouse/cursor.rs b/core/src/mouse/cursor.rs
index 616cd315..9388a578 100644
--- a/core/src/mouse/cursor.rs
+++ b/core/src/mouse/cursor.rs
@@ -1,13 +1,14 @@
use crate::{Point, Rectangle, Transformation, Vector};
-use std::ops::Mul;
-
/// The mouse cursor state.
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum Cursor {
/// The cursor has a defined position.
Available(Point),
+ /// The cursor has a defined position, but it's levitating over a layer above.
+ Levitating(Point),
+
/// The cursor is currently unavailable (i.e. out of bounds or busy).
#[default]
Unavailable,
@@ -18,7 +19,7 @@ impl Cursor {
pub fn position(self) -> Option<Point> {
match self {
Cursor::Available(position) => Some(position),
- Cursor::Unavailable => None,
+ Cursor::Levitating(_) | Cursor::Unavailable => None,
}
}
@@ -51,17 +52,41 @@ impl Cursor {
pub fn is_over(self, bounds: Rectangle) -> bool {
self.position_over(bounds).is_some()
}
+
+ /// Returns true if the [`Cursor`] is levitating over a layer above.
+ pub fn is_levitating(self) -> bool {
+ matches!(self, Self::Levitating(_))
+ }
+
+ /// Makes the [`Cursor`] levitate over a layer above.
+ pub fn levitate(self) -> Self {
+ match self {
+ Self::Available(position) => Self::Levitating(position),
+ _ => self,
+ }
+ }
+
+ /// Brings the [`Cursor`] back to the current layer.
+ pub fn land(self) -> Self {
+ match self {
+ Cursor::Levitating(position) => Cursor::Available(position),
+ _ => self,
+ }
+ }
}
-impl Mul<Transformation> for Cursor {
+impl std::ops::Mul<Transformation> for Cursor {
type Output = Self;
fn mul(self, transformation: Transformation) -> Self {
match self {
- Cursor::Unavailable => Cursor::Unavailable,
- Cursor::Available(point) => {
- Cursor::Available(point * transformation)
+ Self::Available(position) => {
+ Self::Available(position * transformation)
+ }
+ Self::Levitating(position) => {
+ Self::Levitating(position * transformation)
}
+ Self::Unavailable => Self::Unavailable,
}
}
}
diff --git a/core/src/overlay.rs b/core/src/overlay.rs
index 383663af..94239152 100644
--- a/core/src/overlay.rs
+++ b/core/src/overlay.rs
@@ -58,7 +58,7 @@ where
/// By default, it does nothing.
fn update(
&mut self,
- _event: Event,
+ _event: &Event,
_layout: Layout<'_>,
_cursor: mouse::Cursor,
_renderer: &Renderer,
diff --git a/core/src/overlay/element.rs b/core/src/overlay/element.rs
index 7a179663..de6e73fd 100644
--- a/core/src/overlay/element.rs
+++ b/core/src/overlay/element.rs
@@ -51,7 +51,7 @@ where
/// Processes a runtime [`Event`].
pub fn update(
&mut self,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
@@ -150,7 +150,7 @@ where
fn update(
&mut self,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
diff --git a/core/src/overlay/group.rs b/core/src/overlay/group.rs
index e07744e3..970c1b0e 100644
--- a/core/src/overlay/group.rs
+++ b/core/src/overlay/group.rs
@@ -74,7 +74,7 @@ where
fn update(
&mut self,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
@@ -82,14 +82,7 @@ where
shell: &mut Shell<'_, Message>,
) {
for (child, layout) in self.children.iter_mut().zip(layout.children()) {
- child.update(
- event.clone(),
- layout,
- cursor,
- renderer,
- clipboard,
- shell,
- );
+ child.update(event, layout, cursor, renderer, clipboard, shell);
}
}
diff --git a/core/src/padding.rs b/core/src/padding.rs
index e26cdd9b..9ec02e6d 100644
--- a/core/src/padding.rs
+++ b/core/src/padding.rs
@@ -202,3 +202,9 @@ impl From<Padding> for Size {
Self::new(padding.horizontal(), padding.vertical())
}
}
+
+impl From<Pixels> for Padding {
+ fn from(pixels: Pixels) -> Self {
+ Self::from(pixels.0)
+ }
+}
diff --git a/core/src/pixels.rs b/core/src/pixels.rs
index a1ea0f15..c87e2b31 100644
--- a/core/src/pixels.rs
+++ b/core/src/pixels.rs
@@ -20,9 +20,9 @@ impl From<f32> for Pixels {
}
}
-impl From<u16> for Pixels {
- fn from(amount: u16) -> Self {
- Self(f32::from(amount))
+impl From<u32> for Pixels {
+ fn from(amount: u32) -> Self {
+ Self(amount as f32)
}
}
@@ -79,3 +79,11 @@ impl std::ops::Div<f32> for Pixels {
Pixels(self.0 / rhs)
}
}
+
+impl std::ops::Div<u32> for Pixels {
+ type Output = Pixels;
+
+ fn div(self, rhs: u32) -> Self {
+ Pixels(self.0 / rhs as f32)
+ }
+}
diff --git a/core/src/shell.rs b/core/src/shell.rs
index 12ebbaa8..56250e2e 100644
--- a/core/src/shell.rs
+++ b/core/src/shell.rs
@@ -1,6 +1,6 @@
use crate::event;
-use crate::time::Instant;
use crate::window;
+use crate::InputMethod;
/// A connection to the state of a shell.
///
@@ -12,7 +12,8 @@ use crate::window;
pub struct Shell<'a, Message> {
messages: &'a mut Vec<Message>,
event_status: event::Status,
- redraw_request: Option<window::RedrawRequest>,
+ redraw_request: window::RedrawRequest,
+ input_method: InputMethod,
is_layout_invalid: bool,
are_widgets_invalid: bool,
}
@@ -23,9 +24,10 @@ impl<'a, Message> Shell<'a, Message> {
Self {
messages,
event_status: event::Status::Ignored,
- redraw_request: None,
+ redraw_request: window::RedrawRequest::Wait,
is_layout_invalid: false,
are_widgets_invalid: false,
+ input_method: InputMethod::Disabled,
}
}
@@ -59,27 +61,55 @@ impl<'a, Message> Shell<'a, Message> {
/// Requests a new frame to be drawn as soon as possible.
pub fn request_redraw(&mut self) {
- self.redraw_request = Some(window::RedrawRequest::NextFrame);
- }
-
- /// Requests a new frame to be drawn at the given [`Instant`].
- pub fn request_redraw_at(&mut self, at: Instant) {
- match self.redraw_request {
- None => {
- self.redraw_request = Some(window::RedrawRequest::At(at));
- }
- Some(window::RedrawRequest::At(current)) if at < current => {
- self.redraw_request = Some(window::RedrawRequest::At(at));
- }
- _ => {}
- }
+ self.redraw_request = window::RedrawRequest::NextFrame;
+ }
+
+ /// Requests a new frame to be drawn at the given [`window::RedrawRequest`].
+ pub fn request_redraw_at(
+ &mut self,
+ redraw_request: impl Into<window::RedrawRequest>,
+ ) {
+ self.redraw_request = self.redraw_request.min(redraw_request.into());
}
/// Returns the request a redraw should happen, if any.
- pub fn redraw_request(&self) -> Option<window::RedrawRequest> {
+ pub fn redraw_request(&self) -> window::RedrawRequest {
self.redraw_request
}
+ /// Replaces the redraw request of the [`Shell`]; without conflict resolution.
+ ///
+ /// This is useful if you want to overwrite the redraw request to a previous value.
+ /// Since it's a fairly advanced use case and should rarely be used, it is a static
+ /// method.
+ pub fn replace_redraw_request(
+ shell: &mut Self,
+ redraw_request: window::RedrawRequest,
+ ) {
+ shell.redraw_request = redraw_request;
+ }
+
+ /// Requests the current [`InputMethod`] strategy.
+ ///
+ /// __Important__: This request will only be honored by the
+ /// [`Shell`] only during a [`window::Event::RedrawRequested`].
+ pub fn request_input_method<T: AsRef<str>>(
+ &mut self,
+ ime: &InputMethod<T>,
+ ) {
+ self.input_method.merge(ime);
+ }
+
+ /// Returns the current [`InputMethod`] strategy.
+ pub fn input_method(&self) -> &InputMethod {
+ &self.input_method
+ }
+
+ /// Returns the current [`InputMethod`] strategy.
+ pub fn input_method_mut(&mut self) -> &mut InputMethod {
+ &mut self.input_method
+ }
+
/// Returns whether the current layout is invalid or not.
pub fn is_layout_invalid(&self) -> bool {
self.is_layout_invalid
@@ -122,20 +152,14 @@ impl<'a, Message> Shell<'a, Message> {
pub fn merge<B>(&mut self, other: Shell<'_, B>, f: impl Fn(B) -> Message) {
self.messages.extend(other.messages.drain(..).map(f));
- if let Some(new) = other.redraw_request {
- self.redraw_request = Some(
- self.redraw_request
- .map(|current| if current < new { current } else { new })
- .unwrap_or(new),
- );
- }
-
self.is_layout_invalid =
self.is_layout_invalid || other.is_layout_invalid;
self.are_widgets_invalid =
self.are_widgets_invalid || other.are_widgets_invalid;
+ self.redraw_request = self.redraw_request.min(other.redraw_request);
self.event_status = self.event_status.merge(other.event_status);
+ self.input_method.merge(&other.input_method);
}
}
diff --git a/core/src/text.rs b/core/src/text.rs
index c144fd24..a7e1f281 100644
--- a/core/src/text.rs
+++ b/core/src/text.rs
@@ -284,15 +284,7 @@ impl<'a, Link, Font> Span<'a, Link, Font> {
pub fn new(fragment: impl IntoFragment<'a>) -> Self {
Self {
text: fragment.into_fragment(),
- size: None,
- line_height: None,
- font: None,
- color: None,
- highlight: None,
- link: None,
- padding: Padding::ZERO,
- underline: false,
- strikethrough: false,
+ ..Self::default()
}
}
@@ -440,6 +432,23 @@ impl<'a, Link, Font> Span<'a, Link, Font> {
}
}
+impl<Link, Font> Default for Span<'_, Link, Font> {
+ fn default() -> Self {
+ Self {
+ text: Cow::default(),
+ size: None,
+ line_height: None,
+ font: None,
+ color: None,
+ link: None,
+ highlight: None,
+ padding: Padding::default(),
+ underline: false,
+ strikethrough: false,
+ }
+ }
+}
+
impl<'a, Link, Font> From<&'a str> for Span<'a, Link, Font> {
fn from(value: &'a str) -> Self {
Span::new(value)
diff --git a/core/src/text/paragraph.rs b/core/src/text/paragraph.rs
index 924276c3..700c2c75 100644
--- a/core/src/text/paragraph.rs
+++ b/core/src/text/paragraph.rs
@@ -129,6 +129,12 @@ impl<P: Paragraph> Plain<P> {
self.raw.min_width()
}
+ /// Returns the minimum height that can fit the contents of the
+ /// [`Paragraph`].
+ pub fn min_height(&self) -> f32 {
+ self.raw.min_height()
+ }
+
/// Returns the cached [`Paragraph`].
pub fn raw(&self) -> &P {
&self.raw
diff --git a/core/src/widget.rs b/core/src/widget.rs
index 2a40f823..3c9c50ab 100644
--- a/core/src/widget.rs
+++ b/core/src/widget.rs
@@ -114,7 +114,7 @@ where
fn update(
&mut self,
_state: &mut Tree,
- _event: Event,
+ _event: &Event,
_layout: Layout<'_>,
_cursor: mouse::Cursor,
_renderer: &Renderer,
diff --git a/core/src/widget/operation/focusable.rs b/core/src/widget/operation/focusable.rs
index 8f66e575..a1327bc1 100644
--- a/core/src/widget/operation/focusable.rs
+++ b/core/src/widget/operation/focusable.rs
@@ -61,6 +61,33 @@ pub fn focus<T>(target: Id) -> impl Operation<T> {
Focus { target }
}
+/// Produces an [`Operation`] that unfocuses the focused widget.
+pub fn unfocus<T>() -> impl Operation<T> {
+ struct Unfocus;
+
+ impl<T> Operation<T> for Unfocus {
+ fn focusable(
+ &mut self,
+ _id: Option<&Id>,
+ _bounds: Rectangle,
+ state: &mut dyn Focusable,
+ ) {
+ state.unfocus();
+ }
+
+ fn container(
+ &mut self,
+ _id: Option<&Id>,
+ _bounds: Rectangle,
+ operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
+ ) {
+ operate_on_children(self);
+ }
+ }
+
+ Unfocus
+}
+
/// Produces an [`Operation`] that generates a [`Count`] and chains it with the
/// provided function to build a new [`Operation`].
pub fn count() -> impl Operation<Count> {
diff --git a/core/src/window/redraw_request.rs b/core/src/window/redraw_request.rs
index b0c000d6..0ae4face 100644
--- a/core/src/window/redraw_request.rs
+++ b/core/src/window/redraw_request.rs
@@ -8,6 +8,15 @@ pub enum RedrawRequest {
/// Redraw at the given time.
At(Instant),
+
+ /// No redraw is needed.
+ Wait,
+}
+
+impl From<Instant> for RedrawRequest {
+ fn from(time: Instant) -> Self {
+ Self::At(time)
+ }
}
#[cfg(test)]
@@ -34,5 +43,8 @@ mod tests {
assert!(RedrawRequest::At(now) <= RedrawRequest::At(now));
assert!(RedrawRequest::At(now) <= RedrawRequest::At(later));
assert!(RedrawRequest::At(later) >= RedrawRequest::At(now));
+
+ assert!(RedrawRequest::Wait > RedrawRequest::NextFrame);
+ assert!(RedrawRequest::Wait > RedrawRequest::At(later));
}
}
diff --git a/examples/bezier_tool/src/main.rs b/examples/bezier_tool/src/main.rs
index 4d438bd9..95ad299d 100644
--- a/examples/bezier_tool/src/main.rs
+++ b/examples/bezier_tool/src/main.rs
@@ -93,7 +93,7 @@ mod bezier {
fn update(
&self,
state: &mut Self::State,
- event: Event,
+ event: &Event,
bounds: Rectangle,
cursor: mouse::Cursor,
) -> Option<canvas::Action<Curve>> {
diff --git a/examples/changelog/src/main.rs b/examples/changelog/src/main.rs
index f889e757..a1d0d799 100644
--- a/examples/changelog/src/main.rs
+++ b/examples/changelog/src/main.rs
@@ -267,25 +267,21 @@ impl Generator {
} => {
let details = {
let title = rich_text![
- span(&pull_request.title).size(24).link(
- Message::OpenPullRequest(pull_request.id)
- ),
+ span(&pull_request.title)
+ .size(24)
+ .link(pull_request.id),
span(format!(" by {}", pull_request.author))
.font(Font {
style: font::Style::Italic,
..Font::default()
}),
]
+ .on_link_click(Message::OpenPullRequest)
.font(Font::MONOSPACE);
- let description = markdown::view(
- description,
- markdown::Settings::default(),
- markdown::Style::from_palette(
- self.theme().palette(),
- ),
- )
- .map(Message::UrlClicked);
+ let description =
+ markdown(description, self.theme())
+ .map(Message::UrlClicked);
let labels =
row(pull_request.labels.iter().map(|label| {
@@ -348,11 +344,11 @@ impl Generator {
} else {
container(
scrollable(
- markdown::view(
+ markdown(
preview,
- markdown::Settings::with_text_size(12),
- markdown::Style::from_palette(
- self.theme().palette(),
+ markdown::Settings::with_text_size(
+ 12,
+ self.theme(),
),
)
.map(Message::UrlClicked),
diff --git a/examples/download_progress/src/download.rs b/examples/download_progress/src/download.rs
index d63fb906..5b81f7a2 100644
--- a/examples/download_progress/src/download.rs
+++ b/examples/download_progress/src/download.rs
@@ -1,16 +1,14 @@
-use iced::futures::{SinkExt, Stream, StreamExt};
-use iced::stream::try_channel;
+use iced::futures::StreamExt;
+use iced::task::{sipper, Straw};
use std::sync::Arc;
-pub fn download(
- url: impl AsRef<str>,
-) -> impl Stream<Item = Result<Progress, Error>> {
- try_channel(1, move |mut output| async move {
+pub fn download(url: impl AsRef<str>) -> impl Straw<(), Progress, Error> {
+ sipper(move |mut progress| async move {
let response = reqwest::get(url.as_ref()).await?;
let total = response.content_length().ok_or(Error::NoContentLength)?;
- let _ = output.send(Progress::Downloading { percent: 0.0 }).await;
+ let _ = progress.send(Progress { percent: 0.0 }).await;
let mut byte_stream = response.bytes_stream();
let mut downloaded = 0;
@@ -19,23 +17,20 @@ pub fn download(
let bytes = next_bytes?;
downloaded += bytes.len();
- let _ = output
- .send(Progress::Downloading {
+ let _ = progress
+ .send(Progress {
percent: 100.0 * downloaded as f32 / total as f32,
})
.await;
}
- let _ = output.send(Progress::Finished).await;
-
Ok(())
})
}
#[derive(Debug, Clone)]
-pub enum Progress {
- Downloading { percent: f32 },
- Finished,
+pub struct Progress {
+ pub percent: f32,
}
#[derive(Debug, Clone)]
diff --git a/examples/download_progress/src/main.rs b/examples/download_progress/src/main.rs
index f4b07203..8082eccd 100644
--- a/examples/download_progress/src/main.rs
+++ b/examples/download_progress/src/main.rs
@@ -4,7 +4,7 @@ use download::download;
use iced::task;
use iced::widget::{button, center, column, progress_bar, text, Column};
-use iced::{Center, Element, Right, Task};
+use iced::{Center, Element, Function, Right, Task};
pub fn main() -> iced::Result {
iced::application(
@@ -25,7 +25,7 @@ struct Example {
pub enum Message {
Add,
Download(usize),
- DownloadProgressed(usize, Result<download::Progress, download::Error>),
+ DownloadUpdated(usize, Update),
}
impl Example {
@@ -52,15 +52,13 @@ impl Example {
let task = download.start();
- task.map(move |progress| {
- Message::DownloadProgressed(index, progress)
- })
+ task.map(Message::DownloadUpdated.with(index))
}
- Message::DownloadProgressed(id, progress) => {
+ Message::DownloadUpdated(id, update) => {
if let Some(download) =
self.downloads.iter_mut().find(|download| download.id == id)
{
- download.progress(progress);
+ download.update(update);
}
Task::none()
@@ -95,6 +93,12 @@ struct Download {
state: State,
}
+#[derive(Debug, Clone)]
+pub enum Update {
+ Downloading(download::Progress),
+ Finished(Result<(), download::Error>),
+}
+
#[derive(Debug)]
enum State {
Idle,
@@ -111,18 +115,20 @@ impl Download {
}
}
- pub fn start(
- &mut self,
- ) -> Task<Result<download::Progress, download::Error>> {
+ pub fn start(&mut self) -> Task<Update> {
match self.state {
State::Idle { .. }
| State::Finished { .. }
| State::Errored { .. } => {
- let (task, handle) = Task::stream(download(
- "https://huggingface.co/\
+ let (task, handle) = Task::sip(
+ download(
+ "https://huggingface.co/\
mattshumer/Reflection-Llama-3.1-70B/\
resolve/main/model-00001-of-00162.safetensors",
- ))
+ ),
+ Update::Downloading,
+ Update::Finished,
+ )
.abortable();
self.state = State::Downloading {
@@ -136,20 +142,18 @@ impl Download {
}
}
- pub fn progress(
- &mut self,
- new_progress: Result<download::Progress, download::Error>,
- ) {
+ pub fn update(&mut self, update: Update) {
if let State::Downloading { progress, .. } = &mut self.state {
- match new_progress {
- Ok(download::Progress::Downloading { percent }) => {
- *progress = percent;
- }
- Ok(download::Progress::Finished) => {
- self.state = State::Finished;
+ match update {
+ Update::Downloading(new_progress) => {
+ *progress = new_progress.percent;
}
- Err(_error) => {
- self.state = State::Errored;
+ Update::Finished(result) => {
+ self.state = if result.is_ok() {
+ State::Finished
+ } else {
+ State::Errored
+ };
}
}
}
diff --git a/examples/gallery/Cargo.toml b/examples/gallery/Cargo.toml
index 573389b1..6e8aba06 100644
--- a/examples/gallery/Cargo.toml
+++ b/examples/gallery/Cargo.toml
@@ -17,7 +17,10 @@ serde.features = ["derive"]
bytes.workspace = true
image.workspace = true
+sipper.workspace = true
tokio.workspace = true
+blurhash = "0.2.3"
+
[lints]
workspace = true
diff --git a/examples/gallery/src/civitai.rs b/examples/gallery/src/civitai.rs
index 986b6bf2..04589030 100644
--- a/examples/gallery/src/civitai.rs
+++ b/examples/gallery/src/civitai.rs
@@ -1,5 +1,6 @@
use bytes::Bytes;
use serde::Deserialize;
+use sipper::{sipper, Straw};
use tokio::task;
use std::fmt;
@@ -10,6 +11,7 @@ use std::sync::Arc;
pub struct Image {
pub id: Id,
url: String,
+ hash: String,
}
impl Image {
@@ -40,45 +42,76 @@ impl Image {
Ok(response.items)
}
- pub async fn download(self, size: Size) -> Result<Rgba, Error> {
- let client = reqwest::Client::new();
-
- let bytes = client
- .get(match size {
- Size::Original => self.url,
- Size::Thumbnail => self
- .url
- .split("/")
- .map(|part| {
- if part.starts_with("width=") {
- "width=640"
- } else {
- part
- }
- })
- .collect::<Vec<_>>()
- .join("/"),
+ pub async fn blurhash(
+ self,
+ width: u32,
+ height: u32,
+ ) -> Result<Blurhash, Error> {
+ task::spawn_blocking(move || {
+ let pixels = blurhash::decode(&self.hash, width, height, 1.0)?;
+
+ Ok::<_, Error>(Blurhash {
+ rgba: Rgba {
+ width,
+ height,
+ pixels: Bytes::from(pixels),
+ },
})
- .send()
- .await?
- .error_for_status()?
- .bytes()
- .await?;
-
- let image = task::spawn_blocking(move || {
- Ok::<_, Error>(
- image::ImageReader::new(io::Cursor::new(bytes))
- .with_guessed_format()?
- .decode()?
- .to_rgba8(),
- )
})
- .await??;
+ .await?
+ }
- Ok(Rgba {
- width: image.width(),
- height: image.height(),
- pixels: Bytes::from(image.into_raw()),
+ pub fn download(self, size: Size) -> impl Straw<Rgba, Blurhash, Error> {
+ sipper(move |mut sender| async move {
+ let client = reqwest::Client::new();
+
+ if let Size::Thumbnail { width, height } = size {
+ let image = self.clone();
+
+ drop(task::spawn(async move {
+ if let Ok(blurhash) = image.blurhash(width, height).await {
+ sender.send(blurhash).await;
+ }
+ }));
+ }
+
+ let bytes = client
+ .get(match size {
+ Size::Original => self.url,
+ Size::Thumbnail { width, .. } => self
+ .url
+ .split("/")
+ .map(|part| {
+ if part.starts_with("width=") {
+ format!("width={}", width * 2) // High DPI
+ } else {
+ part.to_owned()
+ }
+ })
+ .collect::<Vec<_>>()
+ .join("/"),
+ })
+ .send()
+ .await?
+ .error_for_status()?
+ .bytes()
+ .await?;
+
+ let image = task::spawn_blocking(move || {
+ Ok::<_, Error>(
+ image::ImageReader::new(io::Cursor::new(bytes))
+ .with_guessed_format()?
+ .decode()?
+ .to_rgba8(),
+ )
+ })
+ .await??;
+
+ Ok(Rgba {
+ width: image.width(),
+ height: image.height(),
+ pixels: Bytes::from(image.into_raw()),
+ })
})
}
}
@@ -88,6 +121,11 @@ impl Image {
)]
pub struct Id(u32);
+#[derive(Debug, Clone)]
+pub struct Blurhash {
+ pub rgba: Rgba,
+}
+
#[derive(Clone)]
pub struct Rgba {
pub width: u32,
@@ -107,7 +145,7 @@ impl fmt::Debug for Rgba {
#[derive(Debug, Clone, Copy)]
pub enum Size {
Original,
- Thumbnail,
+ Thumbnail { width: u32, height: u32 },
}
#[derive(Debug, Clone)]
@@ -117,6 +155,7 @@ pub enum Error {
IOFailed(Arc<io::Error>),
JoinFailed(Arc<task::JoinError>),
ImageDecodingFailed(Arc<image::ImageError>),
+ BlurhashDecodingFailed(Arc<blurhash::Error>),
}
impl From<reqwest::Error> for Error {
@@ -142,3 +181,9 @@ impl From<image::ImageError> for Error {
Self::ImageDecodingFailed(Arc::new(error))
}
}
+
+impl From<blurhash::Error> for Error {
+ fn from(error: blurhash::Error) -> Self {
+ Self::BlurhashDecodingFailed(Arc::new(error))
+ }
+}
diff --git a/examples/gallery/src/main.rs b/examples/gallery/src/main.rs
index 290fa6a0..abafaf2d 100644
--- a/examples/gallery/src/main.rs
+++ b/examples/gallery/src/main.rs
@@ -7,14 +7,15 @@ mod civitai;
use crate::civitai::{Error, Id, Image, Rgba, Size};
use iced::animation;
-use iced::time::Instant;
+use iced::time::{milliseconds, Instant};
use iced::widget::{
button, center_x, container, horizontal_space, image, mouse_area, opaque,
pop, row, scrollable, stack,
};
use iced::window;
use iced::{
- color, Animation, ContentFit, Element, Fill, Subscription, Task, Theme,
+ color, Animation, ContentFit, Element, Fill, Function, Subscription, Task,
+ Theme,
};
use std::collections::HashMap;
@@ -28,7 +29,7 @@ fn main() -> iced::Result {
struct Gallery {
images: Vec<Image>,
- thumbnails: HashMap<Id, Thumbnail>,
+ previews: HashMap<Id, Preview>,
viewer: Viewer,
now: Instant,
}
@@ -40,6 +41,7 @@ enum Message {
ImageDownloaded(Result<Rgba, Error>),
ThumbnailDownloaded(Id, Result<Rgba, Error>),
ThumbnailHovered(Id, bool),
+ BlurhashDecoded(Id, civitai::Blurhash),
Open(Id),
Close,
Animate(Instant),
@@ -50,7 +52,7 @@ impl Gallery {
(
Self {
images: Vec::new(),
- thumbnails: HashMap::new(),
+ previews: HashMap::new(),
viewer: Viewer::new(),
now: Instant::now(),
},
@@ -64,9 +66,9 @@ impl Gallery {
pub fn subscription(&self) -> Subscription<Message> {
let is_animating = self
- .thumbnails
+ .previews
.values()
- .any(|thumbnail| thumbnail.is_animating(self.now))
+ .any(|preview| preview.is_animating(self.now))
|| self.viewer.is_animating(self.now);
if is_animating {
@@ -93,9 +95,14 @@ impl Gallery {
return Task::none();
};
- Task::perform(image.download(Size::Thumbnail), move |result| {
- Message::ThumbnailDownloaded(id, result)
- })
+ Task::sip(
+ image.download(Size::Thumbnail {
+ width: Preview::WIDTH,
+ height: Preview::HEIGHT,
+ }),
+ Message::BlurhashDecoded.with(id),
+ Message::ThumbnailDownloaded.with(id),
+ )
}
Message::ImageDownloaded(Ok(rgba)) => {
self.viewer.show(rgba);
@@ -103,14 +110,29 @@ impl Gallery {
Task::none()
}
Message::ThumbnailDownloaded(id, Ok(rgba)) => {
- let thumbnail = Thumbnail::new(rgba);
- let _ = self.thumbnails.insert(id, thumbnail);
+ let thumbnail = if let Some(preview) = self.previews.remove(&id)
+ {
+ preview.load(rgba)
+ } else {
+ Preview::ready(rgba)
+ };
+
+ let _ = self.previews.insert(id, thumbnail);
Task::none()
}
Message::ThumbnailHovered(id, is_hovered) => {
- if let Some(thumbnail) = self.thumbnails.get_mut(&id) {
- thumbnail.zoom.go_mut(is_hovered);
+ if let Some(preview) = self.previews.get_mut(&id) {
+ preview.toggle_zoom(is_hovered);
+ }
+
+ Task::none()
+ }
+ Message::BlurhashDecoded(id, blurhash) => {
+ if !self.previews.contains_key(&id) {
+ let _ = self
+ .previews
+ .insert(id, Preview::loading(blurhash.rgba));
}
Task::none()
@@ -157,7 +179,7 @@ impl Gallery {
row((0..=Image::LIMIT).map(|_| placeholder()))
} else {
row(self.images.iter().map(|image| {
- card(image, self.thumbnails.get(&image.id), self.now)
+ card(image, self.previews.get(&image.id), self.now)
}))
}
.spacing(10)
@@ -174,33 +196,52 @@ impl Gallery {
fn card<'a>(
metadata: &'a Image,
- thumbnail: Option<&'a Thumbnail>,
+ preview: Option<&'a Preview>,
now: Instant,
) -> Element<'a, Message> {
- let image: Element<'_, _> = if let Some(thumbnail) = thumbnail {
- image(&thumbnail.handle)
- .width(Fill)
- .height(Fill)
- .content_fit(ContentFit::Cover)
- .opacity(thumbnail.fade_in.interpolate(0.0, 1.0, now))
- .scale(thumbnail.zoom.interpolate(1.0, 1.1, now))
- .into()
+ let image = if let Some(preview) = preview {
+ let thumbnail: Element<'_, _> =
+ if let Preview::Ready { thumbnail, .. } = &preview {
+ image(&thumbnail.handle)
+ .width(Fill)
+ .height(Fill)
+ .content_fit(ContentFit::Cover)
+ .opacity(thumbnail.fade_in.interpolate(0.0, 1.0, now))
+ .scale(thumbnail.zoom.interpolate(1.0, 1.1, now))
+ .into()
+ } else {
+ horizontal_space().into()
+ };
+
+ if let Some(blurhash) = preview.blurhash(now) {
+ let blurhash = image(&blurhash.handle)
+ .width(Fill)
+ .height(Fill)
+ .content_fit(ContentFit::Cover)
+ .opacity(blurhash.fade_in.interpolate(0.0, 1.0, now));
+
+ stack![blurhash, thumbnail].into()
+ } else {
+ thumbnail
+ }
} else {
horizontal_space().into()
};
let card = mouse_area(
container(image)
- .width(Thumbnail::WIDTH)
- .height(Thumbnail::HEIGHT)
+ .width(Preview::WIDTH)
+ .height(Preview::HEIGHT)
.style(container::dark),
)
.on_enter(Message::ThumbnailHovered(metadata.id, true))
.on_exit(Message::ThumbnailHovered(metadata.id, false));
- if thumbnail.is_some() {
+ if let Some(preview) = preview {
+ let is_thumbnail = matches!(preview, Preview::Ready { .. });
+
button(card)
- .on_press(Message::Open(metadata.id))
+ .on_press_maybe(is_thumbnail.then_some(Message::Open(metadata.id)))
.padding(0)
.style(button::text)
.into()
@@ -213,23 +254,102 @@ fn card<'a>(
fn placeholder<'a>() -> Element<'a, Message> {
container(horizontal_space())
- .width(Thumbnail::WIDTH)
- .height(Thumbnail::HEIGHT)
+ .width(Preview::WIDTH)
+ .height(Preview::HEIGHT)
.style(container::dark)
.into()
}
+enum Preview {
+ Loading {
+ blurhash: Blurhash,
+ },
+ Ready {
+ blurhash: Option<Blurhash>,
+ thumbnail: Thumbnail,
+ },
+}
+
+struct Blurhash {
+ handle: image::Handle,
+ fade_in: Animation<bool>,
+}
+
struct Thumbnail {
handle: image::Handle,
fade_in: Animation<bool>,
zoom: Animation<bool>,
}
-impl Thumbnail {
- const WIDTH: u16 = 320;
- const HEIGHT: u16 = 410;
+impl Preview {
+ const WIDTH: u32 = 320;
+ const HEIGHT: u32 = 410;
+
+ fn loading(rgba: Rgba) -> Self {
+ Self::Loading {
+ blurhash: Blurhash {
+ fade_in: Animation::new(false)
+ .duration(milliseconds(700))
+ .easing(animation::Easing::EaseIn)
+ .go(true),
+ handle: image::Handle::from_rgba(
+ rgba.width,
+ rgba.height,
+ rgba.pixels,
+ ),
+ },
+ }
+ }
- fn new(rgba: Rgba) -> Self {
+ fn ready(rgba: Rgba) -> Self {
+ Self::Ready {
+ blurhash: None,
+ thumbnail: Thumbnail::new(rgba),
+ }
+ }
+
+ fn load(self, rgba: Rgba) -> Self {
+ let Self::Loading { blurhash } = self else {
+ return self;
+ };
+
+ Self::Ready {
+ blurhash: Some(blurhash),
+ thumbnail: Thumbnail::new(rgba),
+ }
+ }
+
+ fn toggle_zoom(&mut self, enabled: bool) {
+ if let Self::Ready { thumbnail, .. } = self {
+ thumbnail.zoom.go_mut(enabled);
+ }
+ }
+
+ fn is_animating(&self, now: Instant) -> bool {
+ match &self {
+ Self::Loading { blurhash } => blurhash.fade_in.is_animating(now),
+ Self::Ready { thumbnail, .. } => {
+ thumbnail.fade_in.is_animating(now)
+ || thumbnail.zoom.is_animating(now)
+ }
+ }
+ }
+
+ fn blurhash(&self, now: Instant) -> Option<&Blurhash> {
+ match self {
+ Self::Loading { blurhash, .. } => Some(blurhash),
+ Self::Ready {
+ blurhash: Some(blurhash),
+ thumbnail,
+ ..
+ } if thumbnail.fade_in.is_animating(now) => Some(blurhash),
+ Self::Ready { .. } => None,
+ }
+ }
+}
+
+impl Thumbnail {
+ pub fn new(rgba: Rgba) -> Self {
Self {
handle: image::Handle::from_rgba(
rgba.width,
@@ -242,10 +362,6 @@ impl Thumbnail {
.easing(animation::Easing::EaseInOut),
}
}
-
- fn is_animating(&self, now: Instant) -> bool {
- self.fade_in.is_animating(now) || self.zoom.is_animating(now)
- }
}
struct Viewer {
diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs
index 1008e477..9516f832 100644
--- a/examples/game_of_life/src/main.rs
+++ b/examples/game_of_life/src/main.rs
@@ -9,7 +9,7 @@ use iced::time::{self, milliseconds};
use iced::widget::{
button, checkbox, column, container, pick_list, row, slider, text,
};
-use iced::{Center, Element, Fill, Subscription, Task, Theme};
+use iced::{Center, Element, Fill, Function, Subscription, Task, Theme};
pub fn main() -> iced::Result {
tracing_subscriber::fmt::init();
@@ -37,7 +37,7 @@ struct GameOfLife {
#[derive(Debug, Clone)]
enum Message {
- Grid(grid::Message, usize),
+ Grid(usize, grid::Message),
Tick,
TogglePlayback,
ToggleGrid(bool),
@@ -61,7 +61,7 @@ impl GameOfLife {
fn update(&mut self, message: Message) -> Task<Message> {
match message {
- Message::Grid(message, version) => {
+ Message::Grid(version, message) => {
if version == self.version {
self.grid.update(message);
}
@@ -78,9 +78,7 @@ impl GameOfLife {
let version = self.version;
- return Task::perform(task, move |message| {
- Message::Grid(message, version)
- });
+ return Task::perform(task, Message::Grid.with(version));
}
}
Message::TogglePlayback => {
@@ -129,9 +127,7 @@ impl GameOfLife {
);
let content = column![
- self.grid
- .view()
- .map(move |message| Message::Grid(message, version)),
+ self.grid.view().map(Message::Grid.with(version)),
controls,
]
.height(Fill);
@@ -380,7 +376,7 @@ mod grid {
fn update(
&self,
interaction: &mut Interaction,
- event: Event,
+ event: &Event,
bounds: Rectangle,
cursor: mouse::Cursor,
) -> Option<canvas::Action<Message>> {
@@ -471,7 +467,7 @@ mod grid {
_ => action.and_capture(),
})
}
- mouse::Event::WheelScrolled { delta } => match delta {
+ mouse::Event::WheelScrolled { delta } => match *delta {
mouse::ScrollDelta::Lines { y, .. }
| mouse::ScrollDelta::Pixels { y, .. } => {
if y < 0.0 && self.scaling > Self::MIN_SCALING
diff --git a/examples/loading_spinners/src/circular.rs b/examples/loading_spinners/src/circular.rs
index 33232fac..24293138 100644
--- a/examples/loading_spinners/src/circular.rs
+++ b/examples/loading_spinners/src/circular.rs
@@ -264,7 +264,7 @@ where
fn update(
&mut self,
tree: &mut Tree,
- event: Event,
+ event: &Event,
_layout: Layout<'_>,
_cursor: mouse::Cursor,
_renderer: &Renderer,
@@ -278,7 +278,7 @@ where
state.animation = state.animation.timed_transition(
self.cycle_duration,
self.rotation_duration,
- now,
+ *now,
);
state.cache.clear();
diff --git a/examples/loading_spinners/src/linear.rs b/examples/loading_spinners/src/linear.rs
index a10b64f0..a6713c7a 100644
--- a/examples/loading_spinners/src/linear.rs
+++ b/examples/loading_spinners/src/linear.rs
@@ -178,7 +178,7 @@ where
fn update(
&mut self,
tree: &mut Tree,
- event: Event,
+ event: &Event,
_layout: Layout<'_>,
_cursor: mouse::Cursor,
_renderer: &Renderer,
@@ -189,7 +189,7 @@ where
let state = tree.state.downcast_mut::<State>();
if let Event::Window(window::Event::RedrawRequested(now)) = event {
- *state = state.timed_transition(self.cycle_duration, now);
+ *state = state.timed_transition(self.cycle_duration, *now);
shell.request_redraw();
}
diff --git a/examples/markdown/Cargo.toml b/examples/markdown/Cargo.toml
index fa6ced74..7af1741b 100644
--- a/examples/markdown/Cargo.toml
+++ b/examples/markdown/Cargo.toml
@@ -7,6 +7,17 @@ publish = false
[dependencies]
iced.workspace = true
-iced.features = ["markdown", "highlighter", "tokio", "debug"]
+iced.features = ["markdown", "highlighter", "image", "tokio", "debug"]
+
+reqwest.version = "0.12"
+reqwest.features = ["json"]
+
+image.workspace = true
+tokio.workspace = true
open = "5.3"
+
+# Disabled to keep amount of build dependencies low
+# This can be re-enabled on demand
+# [build-dependencies]
+# iced_fontello = "0.13"
diff --git a/examples/markdown/build.rs b/examples/markdown/build.rs
new file mode 100644
index 00000000..ecbb7666
--- /dev/null
+++ b/examples/markdown/build.rs
@@ -0,0 +1,5 @@
+pub fn main() {
+ // println!("cargo::rerun-if-changed=fonts/markdown-icons.toml");
+ // iced_fontello::build("fonts/markdown-icons.toml")
+ // .expect("Build icons font");
+}
diff --git a/examples/markdown/fonts/markdown-icons.toml b/examples/markdown/fonts/markdown-icons.toml
new file mode 100644
index 00000000..60c91d17
--- /dev/null
+++ b/examples/markdown/fonts/markdown-icons.toml
@@ -0,0 +1,4 @@
+module = "icon"
+
+[glyphs]
+copy = "fontawesome-docs"
diff --git a/examples/markdown/fonts/markdown-icons.ttf b/examples/markdown/fonts/markdown-icons.ttf
new file mode 100644
index 00000000..013f03a5
--- /dev/null
+++ b/examples/markdown/fonts/markdown-icons.ttf
Binary files differ
diff --git a/examples/markdown/src/icon.rs b/examples/markdown/src/icon.rs
new file mode 100644
index 00000000..cfe32541
--- /dev/null
+++ b/examples/markdown/src/icon.rs
@@ -0,0 +1,15 @@
+// Generated automatically by iced_fontello at build time.
+// Do not edit manually. Source: ../fonts/markdown-icons.toml
+// dcd2f0c969d603e2ee9237a4b70fa86b1a6e84d86f4689046d8fdd10440b06b9
+use iced::widget::{text, Text};
+use iced::Font;
+
+pub const FONT: &[u8] = include_bytes!("../fonts/markdown-icons.ttf");
+
+pub fn copy<'a>() -> Text<'a> {
+ icon("\u{F0C5}")
+}
+
+fn icon(codepoint: &str) -> Text<'_> {
+ text(codepoint).font(Font::with_name("markdown-icons"))
+}
diff --git a/examples/markdown/src/main.rs b/examples/markdown/src/main.rs
index ba93ee18..512d4b44 100644
--- a/examples/markdown/src/main.rs
+++ b/examples/markdown/src/main.rs
@@ -1,50 +1,79 @@
+mod icon;
+
+use iced::animation;
+use iced::clipboard;
use iced::highlighter;
-use iced::time::{self, milliseconds};
+use iced::task;
+use iced::time::{self, milliseconds, Instant};
use iced::widget::{
- self, hover, markdown, right, row, scrollable, text_editor, toggler,
+ self, button, center_x, container, horizontal_space, hover, image,
+ markdown, pop, right, row, scrollable, text_editor, toggler,
};
-use iced::{Element, Fill, Font, Subscription, Task, Theme};
+use iced::window;
+use iced::{Animation, Element, Fill, Font, Subscription, Task, Theme};
+
+use std::collections::HashMap;
+use std::io;
+use std::sync::Arc;
pub fn main() -> iced::Result {
iced::application("Markdown - Iced", Markdown::update, Markdown::view)
+ .font(icon::FONT)
.subscription(Markdown::subscription)
.theme(Markdown::theme)
.run_with(Markdown::new)
}
struct Markdown {
- content: text_editor::Content,
+ content: markdown::Content,
+ raw: text_editor::Content,
+ images: HashMap<markdown::Url, Image>,
mode: Mode,
theme: Theme,
+ now: Instant,
}
enum Mode {
- Preview(Vec<markdown::Item>),
- Stream {
- pending: String,
- parsed: markdown::Content,
+ Preview,
+ Stream { pending: String },
+}
+
+enum Image {
+ Loading {
+ _download: task::Handle,
+ },
+ Ready {
+ handle: image::Handle,
+ fade_in: Animation<bool>,
},
+ #[allow(dead_code)]
+ Errored(Error),
}
#[derive(Debug, Clone)]
enum Message {
Edit(text_editor::Action),
+ Copy(String),
LinkClicked(markdown::Url),
+ ImageShown(markdown::Url),
+ ImageDownloaded(markdown::Url, Result<image::Handle, Error>),
ToggleStream(bool),
NextToken,
+ Animate(Instant),
}
impl Markdown {
fn new() -> (Self, Task<Message>) {
const INITIAL_CONTENT: &str = include_str!("../overview.md");
- let theme = Theme::TokyoNight;
-
(
Self {
- content: text_editor::Content::with_text(INITIAL_CONTENT),
- mode: Mode::Preview(markdown::parse(INITIAL_CONTENT).collect()),
- theme,
+ content: markdown::Content::parse(INITIAL_CONTENT),
+ raw: text_editor::Content::with_text(INITIAL_CONTENT),
+ images: HashMap::new(),
+ mode: Mode::Preview,
+ theme: Theme::TokyoNight,
+ now: Instant::now(),
},
widget::focus_next(),
)
@@ -55,26 +84,73 @@ impl Markdown {
Message::Edit(action) => {
let is_edit = action.is_edit();
- self.content.perform(action);
+ self.raw.perform(action);
if is_edit {
- self.mode = Mode::Preview(
- markdown::parse(&self.content.text()).collect(),
- );
+ self.content = markdown::Content::parse(&self.raw.text());
+ self.mode = Mode::Preview;
+
+ let images = self.content.images();
+ self.images.retain(|url, _image| images.contains(url));
}
Task::none()
}
+ Message::Copy(content) => clipboard::write(content),
Message::LinkClicked(link) => {
let _ = open::that_in_background(link.to_string());
Task::none()
}
+ Message::ImageShown(url) => {
+ if self.images.contains_key(&url) {
+ return Task::none();
+ }
+
+ let (download_image, handle) = Task::future({
+ let url = url.clone();
+
+ async move {
+ // Wait half a second for further editions before attempting download
+ tokio::time::sleep(milliseconds(500)).await;
+ download_image(url).await
+ }
+ })
+ .abortable();
+
+ let _ = self.images.insert(
+ url.clone(),
+ Image::Loading {
+ _download: handle.abort_on_drop(),
+ },
+ );
+
+ download_image.map(move |result| {
+ Message::ImageDownloaded(url.clone(), result)
+ })
+ }
+ Message::ImageDownloaded(url, result) => {
+ let _ = self.images.insert(
+ url,
+ result
+ .map(|handle| Image::Ready {
+ handle,
+ fade_in: Animation::new(false)
+ .quick()
+ .easing(animation::Easing::EaseInOut)
+ .go(true),
+ })
+ .unwrap_or_else(Image::Errored),
+ );
+
+ Task::none()
+ }
Message::ToggleStream(enable_stream) => {
if enable_stream {
+ self.content = markdown::Content::new();
+
self.mode = Mode::Stream {
- pending: self.content.text(),
- parsed: markdown::Content::new(),
+ pending: self.raw.text(),
};
scrollable::snap_to(
@@ -82,24 +158,22 @@ impl Markdown {
scrollable::RelativeOffset::END,
)
} else {
- self.mode = Mode::Preview(
- markdown::parse(&self.content.text()).collect(),
- );
+ self.mode = Mode::Preview;
Task::none()
}
}
Message::NextToken => {
match &mut self.mode {
- Mode::Preview(_) => {}
- Mode::Stream { pending, parsed } => {
+ Mode::Preview => {}
+ Mode::Stream { pending } => {
if pending.is_empty() {
- self.mode = Mode::Preview(parsed.items().to_vec());
+ self.mode = Mode::Preview;
} else {
let mut tokens = pending.split(' ');
if let Some(token) = tokens.next() {
- parsed.push_str(&format!("{token} "));
+ self.content.push_str(&format!("{token} "));
}
*pending = tokens.collect::<Vec<_>>().join(" ");
@@ -109,11 +183,16 @@ impl Markdown {
Task::none()
}
+ Message::Animate(now) => {
+ self.now = now;
+
+ Task::none()
+ }
}
}
fn view(&self) -> Element<Message> {
- let editor = text_editor(&self.content)
+ let editor = text_editor(&self.raw)
.placeholder("Type your Markdown here...")
.on_action(Message::Edit)
.height(Fill)
@@ -121,17 +200,14 @@ impl Markdown {
.font(Font::MONOSPACE)
.highlight("markdown", highlighter::Theme::Base16Ocean);
- let items = match &self.mode {
- Mode::Preview(items) => items.as_slice(),
- Mode::Stream { parsed, .. } => parsed.items(),
- };
-
- let preview = markdown(
- items,
- markdown::Settings::default(),
- markdown::Style::from_palette(self.theme.palette()),
- )
- .map(Message::LinkClicked);
+ let preview = markdown::view_with(
+ self.content.items(),
+ &self.theme,
+ &CustomViewer {
+ images: &self.images,
+ now: self.now,
+ },
+ );
row![
editor,
@@ -159,11 +235,146 @@ impl Markdown {
}
fn subscription(&self) -> Subscription<Message> {
- match self.mode {
- Mode::Preview(_) => Subscription::none(),
+ let listen_stream = match self.mode {
+ Mode::Preview => Subscription::none(),
Mode::Stream { .. } => {
time::every(milliseconds(10)).map(|_| Message::NextToken)
}
+ };
+
+ let animate = {
+ let is_animating = self.images.values().any(|image| match image {
+ Image::Ready { fade_in, .. } => fade_in.is_animating(self.now),
+ _ => false,
+ });
+
+ if is_animating {
+ window::frames().map(Message::Animate)
+ } else {
+ Subscription::none()
+ }
+ };
+
+ Subscription::batch([listen_stream, animate])
+ }
+}
+
+struct CustomViewer<'a> {
+ images: &'a HashMap<markdown::Url, Image>,
+ now: Instant,
+}
+
+impl<'a> markdown::Viewer<'a, Message> for CustomViewer<'a> {
+ fn on_link_click(url: markdown::Url) -> Message {
+ Message::LinkClicked(url)
+ }
+
+ fn image(
+ &self,
+ _settings: markdown::Settings,
+ url: &'a markdown::Url,
+ _title: &'a str,
+ _alt: &markdown::Text,
+ ) -> Element<'a, Message> {
+ if let Some(Image::Ready { handle, fade_in }) = self.images.get(url) {
+ center_x(
+ image(handle)
+ .opacity(fade_in.interpolate(0.0, 1.0, self.now))
+ .scale(fade_in.interpolate(1.2, 1.0, self.now)),
+ )
+ .into()
+ } else {
+ pop(horizontal_space())
+ .key(url.as_str())
+ .on_show(|_size| Message::ImageShown(url.clone()))
+ .into()
}
}
+
+ fn code_block(
+ &self,
+ settings: markdown::Settings,
+ _language: Option<&'a str>,
+ code: &'a str,
+ lines: &'a [markdown::Text],
+ ) -> Element<'a, Message> {
+ let code_block =
+ markdown::code_block(settings, lines, Message::LinkClicked);
+
+ let copy = button(icon::copy().size(12))
+ .padding(2)
+ .on_press_with(|| Message::Copy(code.to_owned()))
+ .style(button::text);
+
+ hover(
+ code_block,
+ right(container(copy).style(container::dark))
+ .padding(settings.spacing / 2),
+ )
+ }
+}
+
+async fn download_image(url: markdown::Url) -> Result<image::Handle, Error> {
+ use std::io;
+ use tokio::task;
+
+ println!("Trying to download image: {url}");
+
+ let client = reqwest::Client::new();
+
+ let bytes = client
+ .get(url)
+ .send()
+ .await?
+ .error_for_status()?
+ .bytes()
+ .await?;
+
+ let image = task::spawn_blocking(move || {
+ Ok::<_, Error>(
+ ::image::ImageReader::new(io::Cursor::new(bytes))
+ .with_guessed_format()?
+ .decode()?
+ .to_rgba8(),
+ )
+ })
+ .await??;
+
+ Ok(image::Handle::from_rgba(
+ image.width(),
+ image.height(),
+ image.into_raw(),
+ ))
+}
+
+#[derive(Debug, Clone)]
+pub enum Error {
+ RequestFailed(Arc<reqwest::Error>),
+ IOFailed(Arc<io::Error>),
+ JoinFailed(Arc<tokio::task::JoinError>),
+ ImageDecodingFailed(Arc<::image::ImageError>),
+}
+
+impl From<reqwest::Error> for Error {
+ fn from(error: reqwest::Error) -> Self {
+ Self::RequestFailed(Arc::new(error))
+ }
+}
+
+impl From<io::Error> for Error {
+ fn from(error: io::Error) -> Self {
+ Self::IOFailed(Arc::new(error))
+ }
+}
+
+impl From<tokio::task::JoinError> for Error {
+ fn from(error: tokio::task::JoinError) -> Self {
+ Self::JoinFailed(Arc::new(error))
+ }
+}
+
+impl From<::image::ImageError> for Error {
+ fn from(error: ::image::ImageError) -> Self {
+ Self::ImageDecodingFailed(Arc::new(error))
+ }
}
diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs
index f9021c8d..8cec9d4c 100644
--- a/examples/multi_window/src/main.rs
+++ b/examples/multi_window/src/main.rs
@@ -3,7 +3,9 @@ use iced::widget::{
text_input,
};
use iced::window;
-use iced::{Center, Element, Fill, Subscription, Task, Theme, Vector};
+use iced::{
+ Center, Element, Fill, Function, Subscription, Task, Theme, Vector,
+};
use std::collections::BTreeMap;
@@ -169,7 +171,7 @@ impl Window {
let scale_input = column![
text("Window scale factor:"),
text_input("Window Scale", &self.scale_input)
- .on_input(move |msg| { Message::ScaleInputChanged(id, msg) })
+ .on_input(Message::ScaleInputChanged.with(id))
.on_submit(Message::ScaleChanged(
id,
self.scale_input.to_string()
@@ -179,7 +181,7 @@ impl Window {
let title_input = column![
text("Window title:"),
text_input("Window Title", &self.title)
- .on_input(move |msg| { Message::TitleChanged(id, msg) })
+ .on_input(Message::TitleChanged.with(id))
.id(format!("input-{id}"))
];
diff --git a/examples/multitouch/src/main.rs b/examples/multitouch/src/main.rs
index 5f4a5c90..bda3b8f7 100644
--- a/examples/multitouch/src/main.rs
+++ b/examples/multitouch/src/main.rs
@@ -55,11 +55,11 @@ impl canvas::Program<Message> for Multitouch {
fn update(
&self,
_state: &mut Self::State,
- event: Event,
+ event: &Event,
_bounds: Rectangle,
_cursor: mouse::Cursor,
) -> Option<canvas::Action<Message>> {
- let message = match event {
+ let message = match event.clone() {
Event::Touch(
touch::Event::FingerPressed { id, position }
| touch::Event::FingerMoved { id, position },
diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs
index 6359fb5a..fec4e1b4 100644
--- a/examples/scrollable/src/main.rs
+++ b/examples/scrollable/src/main.rs
@@ -21,9 +21,9 @@ pub fn main() -> iced::Result {
struct ScrollableDemo {
scrollable_direction: Direction,
- scrollbar_width: u16,
- scrollbar_margin: u16,
- scroller_width: u16,
+ scrollbar_width: u32,
+ scrollbar_margin: u32,
+ scroller_width: u32,
current_scroll_offset: scrollable::RelativeOffset,
anchor: scrollable::Anchor,
}
@@ -39,9 +39,9 @@ enum Direction {
enum Message {
SwitchDirection(Direction),
AlignmentChanged(scrollable::Anchor),
- ScrollbarWidthChanged(u16),
- ScrollbarMarginChanged(u16),
- ScrollerWidthChanged(u16),
+ ScrollbarWidthChanged(u32),
+ ScrollbarMarginChanged(u32),
+ ScrollerWidthChanged(u32),
ScrollToBeginning,
ScrollToEnd,
Scrolled(scrollable::Viewport),
diff --git a/examples/sierpinski_triangle/src/main.rs b/examples/sierpinski_triangle/src/main.rs
index d4d483f5..a4a89455 100644
--- a/examples/sierpinski_triangle/src/main.rs
+++ b/examples/sierpinski_triangle/src/main.rs
@@ -76,7 +76,7 @@ impl canvas::Program<Message> for SierpinskiGraph {
fn update(
&self,
_state: &mut Self::State,
- event: Event,
+ event: &Event,
bounds: Rectangle,
cursor: mouse::Cursor,
) -> Option<canvas::Action<Message>> {
diff --git a/examples/system_information/src/main.rs b/examples/system_information/src/main.rs
index afa657d8..56f934b7 100644
--- a/examples/system_information/src/main.rs
+++ b/examples/system_information/src/main.rs
@@ -102,22 +102,22 @@ impl Example {
);
let memory_readable =
- ByteSize::b(information.memory_total).to_string();
+ ByteSize::b(information.memory_total).to_string_as(true);
let memory_total = text!(
"Memory (total): {} bytes ({memory_readable})",
information.memory_total,
);
- let memory_text = if let Some(memory_used) =
- information.memory_used
- {
- let memory_readable = ByteSize::b(memory_used).to_string();
+ let memory_text =
+ if let Some(memory_used) = information.memory_used {
+ let memory_readable =
+ ByteSize::b(memory_used).to_string_as(true);
- format!("{memory_used} bytes ({memory_readable})")
- } else {
- String::from("None")
- };
+ format!("{memory_used} bytes ({memory_readable})")
+ } else {
+ String::from("None")
+ };
let memory_used = text!("Memory (used): {memory_text}");
diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs
index 2ae9bfe2..dc314df8 100644
--- a/examples/toast/src/main.rs
+++ b/examples/toast/src/main.rs
@@ -361,7 +361,7 @@ mod toast {
fn update(
&mut self,
state: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
@@ -491,7 +491,7 @@ mod toast {
fn update(
&mut self,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
@@ -530,7 +530,7 @@ mod toast {
child.as_widget_mut().update(
state,
- event.clone(),
+ event,
layout,
cursor,
renderer,
diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs
index 74c4c464..033cb122 100644
--- a/examples/todos/src/main.rs
+++ b/examples/todos/src/main.rs
@@ -4,7 +4,9 @@ use iced::widget::{
scrollable, text, text_input, Text,
};
use iced::window;
-use iced::{Center, Element, Fill, Font, Subscription, Task as Command};
+use iced::{
+ Center, Element, Fill, Font, Function, Subscription, Task as Command,
+};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
@@ -215,9 +217,8 @@ impl Todos {
.map(|(i, task)| {
(
task.id,
- task.view(i).map(move |message| {
- Message::TaskMessage(i, message)
- }),
+ task.view(i)
+ .map(Message::TaskMessage.with(i)),
)
}),
)
diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs
index 32720c47..2ca1df44 100644
--- a/examples/tour/src/main.rs
+++ b/examples/tour/src/main.rs
@@ -24,12 +24,12 @@ pub struct Tour {
screen: Screen,
slider: u8,
layout: Layout,
- spacing: u16,
- text_size: u16,
+ spacing: u32,
+ text_size: u32,
text_color: Color,
language: Option<Language>,
toggler: bool,
- image_width: u16,
+ image_width: u32,
image_filter_method: image::FilterMethod,
input_value: String,
input_is_secure: bool,
@@ -43,11 +43,11 @@ pub enum Message {
NextPressed,
SliderChanged(u8),
LayoutChanged(Layout),
- SpacingChanged(u16),
- TextSizeChanged(u16),
+ SpacingChanged(u32),
+ TextSizeChanged(u32),
TextColorChanged(Color),
LanguageSelected(Language),
- ImageWidthChanged(u16),
+ ImageWidthChanged(u32),
ImageUseNearestToggled(bool),
InputChanged(String),
ToggleSecureInput(bool),
@@ -537,7 +537,7 @@ impl Screen {
}
fn ferris<'a>(
- width: u16,
+ width: u32,
filter_method: image::FilterMethod,
) -> Container<'a, Message> {
center_x(
diff --git a/examples/websocket/src/echo.rs b/examples/websocket/src/echo.rs
index 14652936..149a260c 100644
--- a/examples/websocket/src/echo.rs
+++ b/examples/websocket/src/echo.rs
@@ -1,73 +1,59 @@
pub mod server;
use iced::futures;
-use iced::stream;
+use iced::task::{sipper, Never, Sipper};
use iced::widget::text;
use futures::channel::mpsc;
use futures::sink::SinkExt;
-use futures::stream::{Stream, StreamExt};
+use futures::stream::StreamExt;
use async_tungstenite::tungstenite;
use std::fmt;
-pub fn connect() -> impl Stream<Item = Event> {
- stream::channel(100, |mut output| async move {
- let mut state = State::Disconnected;
-
+pub fn connect() -> impl Sipper<Never, Event> {
+ sipper(|mut output| async move {
loop {
- match &mut state {
- State::Disconnected => {
- const ECHO_SERVER: &str = "ws://127.0.0.1:3030";
+ const ECHO_SERVER: &str = "ws://127.0.0.1:3030";
- match async_tungstenite::tokio::connect_async(ECHO_SERVER)
- .await
- {
- Ok((websocket, _)) => {
- let (sender, receiver) = mpsc::channel(100);
+ let (mut websocket, mut input) =
+ match async_tungstenite::tokio::connect_async(ECHO_SERVER).await
+ {
+ Ok((websocket, _)) => {
+ let (sender, receiver) = mpsc::channel(100);
- let _ = output
- .send(Event::Connected(Connection(sender)))
- .await;
+ output.send(Event::Connected(Connection(sender))).await;
- state = State::Connected(websocket, receiver);
- }
- Err(_) => {
- tokio::time::sleep(
- tokio::time::Duration::from_secs(1),
- )
+ (websocket.fuse(), receiver)
+ }
+ Err(_) => {
+ tokio::time::sleep(tokio::time::Duration::from_secs(1))
.await;
- let _ = output.send(Event::Disconnected).await;
- }
+ output.send(Event::Disconnected).await;
+ continue;
}
- }
- State::Connected(websocket, input) => {
- let mut fused_websocket = websocket.by_ref().fuse();
-
- futures::select! {
- received = fused_websocket.select_next_some() => {
- match received {
- Ok(tungstenite::Message::Text(message)) => {
- let _ = output.send(Event::MessageReceived(Message::User(message))).await;
- }
- Err(_) => {
- let _ = output.send(Event::Disconnected).await;
-
- state = State::Disconnected;
- }
- Ok(_) => continue,
+ };
+
+ loop {
+ futures::select! {
+ received = websocket.select_next_some() => {
+ match received {
+ Ok(tungstenite::Message::Text(message)) => {
+ output.send(Event::MessageReceived(Message::User(message))).await;
+ }
+ Err(_) => {
+ output.send(Event::Disconnected).await;
+ break;
}
+ Ok(_) => {},
}
+ }
+ message = input.select_next_some() => {
+ let result = websocket.send(tungstenite::Message::Text(message.to_string())).await;
- message = input.select_next_some() => {
- let result = websocket.send(tungstenite::Message::Text(message.to_string())).await;
-
- if result.is_err() {
- let _ = output.send(Event::Disconnected).await;
-
- state = State::Disconnected;
- }
+ if result.is_err() {
+ output.send(Event::Disconnected).await;
}
}
}
@@ -76,18 +62,6 @@ pub fn connect() -> impl Stream<Item = Event> {
})
}
-#[derive(Debug)]
-#[allow(clippy::large_enum_variant)]
-enum State {
- Disconnected,
- Connected(
- async_tungstenite::WebSocketStream<
- async_tungstenite::tokio::ConnectStream,
- >,
- mpsc::Receiver<Message>,
- ),
-}
-
#[derive(Debug, Clone)]
pub enum Event {
Connected(Connection),
diff --git a/futures/src/backend/native/tokio.rs b/futures/src/backend/native/tokio.rs
index e0be83a6..c38ef566 100644
--- a/futures/src/backend/native/tokio.rs
+++ b/futures/src/backend/native/tokio.rs
@@ -23,11 +23,10 @@ impl crate::Executor for Executor {
pub mod time {
//! Listen and react to time.
use crate::core::time::{Duration, Instant};
- use crate::stream;
use crate::subscription::Subscription;
use crate::MaybeSend;
- use futures::SinkExt;
+ use futures::stream;
use std::future::Future;
/// Returns a [`Subscription`] that produces messages at a set interval.
@@ -66,12 +65,12 @@ pub mod time {
let f = *f;
let interval = *interval;
- stream::channel(1, move |mut output| async move {
- loop {
- let _ = output.send(f().await).await;
-
+ stream::unfold(0, move |i| async move {
+ if i > 0 {
tokio::time::sleep(interval).await;
}
+
+ Some((f().await, i + 1))
})
})
}
diff --git a/futures/src/subscription.rs b/futures/src/subscription.rs
index 82cba9a1..3577d19f 100644
--- a/futures/src/subscription.rs
+++ b/futures/src/subscription.rs
@@ -210,7 +210,8 @@ impl<T> Subscription<T> {
/// Returns a [`Subscription`] that will create and asynchronously run the
/// given [`Stream`].
///
- /// The `id` will be used to uniquely identify the [`Subscription`].
+ /// Both the `data` and the function pointer will be used to uniquely identify
+ /// the [`Subscription`].
pub fn run_with<D, S>(data: D, builder: fn(&D) -> S) -> Self
where
D: Hash + 'static,
diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs
index c73d189c..765de07e 100644
--- a/graphics/src/text/editor.rs
+++ b/graphics/src/text/editor.rs
@@ -11,7 +11,7 @@ use cosmic_text::Edit as _;
use std::borrow::Cow;
use std::fmt;
-use std::sync::{self, Arc};
+use std::sync::{self, Arc, RwLock};
/// A multi-line text editor.
#[derive(Debug, PartialEq)]
@@ -19,6 +19,7 @@ pub struct Editor(Option<Arc<Internal>>);
struct Internal {
editor: cosmic_text::Editor<'static>,
+ cursor: RwLock<Option<Cursor>>,
font: Font,
bounds: Size,
topmost_line_changed: Option<usize>,
@@ -114,10 +115,14 @@ impl editor::Editor for Editor {
fn cursor(&self) -> editor::Cursor {
let internal = self.internal();
+ if let Ok(Some(cursor)) = internal.cursor.read().as_deref() {
+ return cursor.clone();
+ }
+
let cursor = internal.editor.cursor();
let buffer = buffer_from_editor(&internal.editor);
- match internal.editor.selection_bounds() {
+ let cursor = match internal.editor.selection_bounds() {
Some((start, end)) => {
let line_height = buffer.metrics().line_height;
let selected_lines = end.line - start.line + 1;
@@ -237,7 +242,12 @@ impl editor::Editor for Editor {
- buffer.scroll().vertical,
))
}
- }
+ };
+
+ *internal.cursor.write().expect("Write to cursor cache") =
+ Some(cursor.clone());
+
+ cursor
}
fn cursor_position(&self) -> (usize, usize) {
@@ -259,6 +269,13 @@ impl editor::Editor for Editor {
let editor = &mut internal.editor;
+ // Clear cursor cache
+ let _ = internal
+ .cursor
+ .write()
+ .expect("Write to cursor cache")
+ .take();
+
match action {
// Motion events
Action::Move(motion) => {
@@ -527,6 +544,13 @@ impl editor::Editor for Editor {
internal.editor.shape_as_needed(font_system.raw(), false);
+ // Clear cursor cache
+ let _ = internal
+ .cursor
+ .write()
+ .expect("Write to cursor cache")
+ .take();
+
self.0 = Some(Arc::new(internal));
}
@@ -635,6 +659,7 @@ impl Default for Internal {
line_height: 1.0,
},
)),
+ cursor: RwLock::new(None),
font: Font::default(),
bounds: Size::ZERO,
topmost_line_changed: None,
diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml
index 703c3ed9..fc212ef8 100644
--- a/runtime/Cargo.toml
+++ b/runtime/Cargo.toml
@@ -23,5 +23,6 @@ iced_core.workspace = true
iced_futures.workspace = true
iced_futures.features = ["thread-pool"]
-thiserror.workspace = true
raw-window-handle.workspace = true
+sipper.workspace = true
+thiserror.workspace = true
diff --git a/runtime/src/overlay/nested.rs b/runtime/src/overlay/nested.rs
index 342ad70c..38054d7b 100644
--- a/runtime/src/overlay/nested.rs
+++ b/runtime/src/overlay/nested.rs
@@ -160,7 +160,7 @@ where
/// Processes a runtime [`Event`].
pub fn update(
&mut self,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
@@ -170,7 +170,7 @@ where
fn recurse<Message, Theme, Renderer>(
element: &mut overlay::Element<'_, Message, Theme, Renderer>,
layout: Layout<'_>,
- event: Event,
+ event: &Event,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
@@ -188,7 +188,7 @@ where
recurse(
&mut nested,
nested_layout,
- event.clone(),
+ event,
cursor,
renderer,
clipboard,
diff --git a/runtime/src/task.rs b/runtime/src/task.rs
index 22cfb63e..022483f7 100644
--- a/runtime/src/task.rs
+++ b/runtime/src/task.rs
@@ -3,7 +3,6 @@ use crate::core::widget;
use crate::futures::futures::channel::mpsc;
use crate::futures::futures::channel::oneshot;
use crate::futures::futures::future::{self, FutureExt};
-use crate::futures::futures::never::Never;
use crate::futures::futures::stream::{self, Stream, StreamExt};
use crate::futures::{boxed_stream, BoxStream, MaybeSend};
use crate::Action;
@@ -11,6 +10,9 @@ use crate::Action;
use std::future::Future;
use std::sync::Arc;
+#[doc(no_inline)]
+pub use sipper::{sipper, stream, Never, Sender, Sipper, Straw};
+
/// A set of concurrent actions to be performed by the iced runtime.
///
/// A [`Task`] _may_ produce a bunch of values of type `T`.
@@ -57,6 +59,22 @@ impl<T> Task<T> {
Self::stream(stream.map(f))
}
+ /// Creates a [`Task`] that runs the given [`Sipper`] to completion, mapping
+ /// progress with the first closure and the output with the second one.
+ pub fn sip<S>(
+ sipper: S,
+ on_progress: impl FnMut(S::Progress) -> T + MaybeSend + 'static,
+ on_output: impl FnOnce(<S as Future>::Output) -> T + MaybeSend + 'static,
+ ) -> Self
+ where
+ S: sipper::Core + MaybeSend + 'static,
+ T: MaybeSend + 'static,
+ {
+ Self::stream(stream(sipper::sipper(move |sender| async move {
+ on_output(sipper.with(on_progress).run(sender).await)
+ })))
+ }
+
/// Combines the given tasks and produces a single [`Task`] that will run all of them
/// in parallel.
pub fn batch(tasks: impl IntoIterator<Item = Self>) -> Self
diff --git a/runtime/src/user_interface.rs b/runtime/src/user_interface.rs
index b2826f71..9b396c69 100644
--- a/runtime/src/user_interface.rs
+++ b/runtime/src/user_interface.rs
@@ -5,7 +5,9 @@ use crate::core::mouse;
use crate::core::renderer;
use crate::core::widget;
use crate::core::window;
-use crate::core::{Clipboard, Element, Layout, Rectangle, Shell, Size, Vector};
+use crate::core::{
+ Clipboard, Element, InputMethod, Layout, Rectangle, Shell, Size, Vector,
+};
use crate::overlay;
/// A set of interactive graphical elements with a specific [`Layout`].
@@ -186,7 +188,8 @@ where
use std::mem::ManuallyDrop;
let mut outdated = false;
- let mut redraw_request = None;
+ let mut redraw_request = window::RedrawRequest::Wait;
+ let mut input_method = InputMethod::Disabled;
let mut manual_overlay = ManuallyDrop::new(
self.root
@@ -207,7 +210,7 @@ where
let mut layout = overlay.layout(renderer, bounds);
let mut event_statuses = Vec::new();
- for event in events.iter().cloned() {
+ for event in events {
let mut shell = Shell::new(messages);
overlay.update(
@@ -220,16 +223,8 @@ where
);
event_statuses.push(shell.event_status());
-
- match (redraw_request, shell.redraw_request()) {
- (None, Some(at)) => {
- redraw_request = Some(at);
- }
- (Some(current), Some(new)) if new < current => {
- redraw_request = Some(new);
- }
- _ => {}
- }
+ redraw_request = redraw_request.min(shell.redraw_request());
+ input_method.merge(shell.input_method());
if shell.is_layout_invalid() {
let _ = ManuallyDrop::into_inner(manual_overlay);
@@ -299,7 +294,6 @@ where
let event_statuses = events
.iter()
- .cloned()
.zip(overlay_statuses)
.map(|(event, overlay_status)| {
if matches!(overlay_status, event::Status::Captured) {
@@ -323,15 +317,8 @@ where
self.overlay = None;
}
- match (redraw_request, shell.redraw_request()) {
- (None, Some(at)) => {
- redraw_request = Some(at);
- }
- (Some(current), Some(new)) if new < current => {
- redraw_request = Some(new);
- }
- _ => {}
- }
+ redraw_request = redraw_request.min(shell.redraw_request());
+ input_method.merge(shell.input_method());
shell.revalidate_layout(|| {
self.base = self.root.as_widget().layout(
@@ -355,7 +342,10 @@ where
if outdated {
State::Outdated
} else {
- State::Updated { redraw_request }
+ State::Updated {
+ redraw_request,
+ input_method,
+ }
},
event_statuses,
)
@@ -636,7 +626,7 @@ impl Default for Cache {
}
/// The resulting state after updating a [`UserInterface`].
-#[derive(Debug, Clone, Copy)]
+#[derive(Debug, Clone)]
pub enum State {
/// The [`UserInterface`] is outdated and needs to be rebuilt.
Outdated,
@@ -644,7 +634,9 @@ pub enum State {
/// The [`UserInterface`] is up-to-date and can be reused without
/// rebuilding.
Updated {
- /// The [`window::RedrawRequest`] when a redraw should be performed.
- redraw_request: Option<window::RedrawRequest>,
+ /// The [`window::RedrawRequest`] describing when a redraw should be performed.
+ redraw_request: window::RedrawRequest,
+ /// The current [`InputMethod`] strategy of the user interface.
+ input_method: InputMethod,
},
}
diff --git a/src/lib.rs b/src/lib.rs
index eec844bc..441826d6 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -505,9 +505,9 @@ pub use crate::core::gradient;
pub use crate::core::padding;
pub use crate::core::theme;
pub use crate::core::{
- Alignment, Animation, Background, Border, Color, ContentFit, Degrees,
- Gradient, Length, Padding, Pixels, Point, Radians, Rectangle, Rotation,
- Settings, Shadow, Size, Theme, Transformation, Vector,
+ never, Alignment, Animation, Background, Border, Color, ContentFit,
+ Degrees, Function, Gradient, Length, Padding, Pixels, Point, Radians,
+ Rectangle, Rotation, Settings, Shadow, Size, Theme, Transformation, Vector,
};
pub use crate::runtime::exit;
pub use iced_futures::Subscription;
@@ -519,7 +519,9 @@ pub use Length::{Fill, FillPortion, Shrink};
pub mod task {
//! Create runtime tasks.
- pub use crate::runtime::task::{Handle, Task};
+ pub use crate::runtime::task::{
+ sipper, stream, Handle, Never, Sipper, Straw, Task,
+ };
}
pub mod clipboard {
diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs
index 2283cf71..b1998da7 100644
--- a/wgpu/src/lib.rs
+++ b/wgpu/src/lib.rs
@@ -280,13 +280,16 @@ impl Renderer {
let scale = Transformation::scale(scale_factor);
for layer in self.layers.iter() {
- let Some(scissor_rect) = physical_bounds
- .intersection(&(layer.bounds * scale_factor))
- .and_then(Rectangle::snap)
+ let Some(physical_bounds) =
+ physical_bounds.intersection(&(layer.bounds * scale_factor))
else {
continue;
};
+ let Some(scissor_rect) = physical_bounds.snap() else {
+ continue;
+ };
+
if !layer.quads.is_empty() {
engine.quad_pipeline.render(
quad_layer,
@@ -557,6 +560,16 @@ impl core::svg::Renderer for Renderer {
impl graphics::mesh::Renderer for Renderer {
fn draw_mesh(&mut self, mesh: graphics::Mesh) {
+ debug_assert!(
+ !mesh.indices().is_empty(),
+ "Mesh must not have empty indices"
+ );
+
+ debug_assert!(
+ mesh.indices().len() % 3 == 0,
+ "Mesh indices length must be a multiple of 3"
+ );
+
let (layer, transformation) = self.layers.current_mut();
layer.draw_mesh(mesh, transformation);
}
diff --git a/widget/src/action.rs b/widget/src/action.rs
index 1dd3a787..cc31e76a 100644
--- a/widget/src/action.rs
+++ b/widget/src/action.rs
@@ -6,7 +6,7 @@ use crate::core::window;
#[derive(Debug, Clone)]
pub struct Action<Message> {
message_to_publish: Option<Message>,
- redraw_request: Option<window::RedrawRequest>,
+ redraw_request: window::RedrawRequest,
event_status: event::Status,
}
@@ -14,7 +14,7 @@ impl<Message> Action<Message> {
fn new() -> Self {
Self {
message_to_publish: None,
- redraw_request: None,
+ redraw_request: window::RedrawRequest::Wait,
event_status: event::Status::Ignored,
}
}
@@ -46,7 +46,7 @@ impl<Message> Action<Message> {
/// soon as possible; without publishing any `Message`.
pub fn request_redraw() -> Self {
Self {
- redraw_request: Some(window::RedrawRequest::NextFrame),
+ redraw_request: window::RedrawRequest::NextFrame,
..Self::new()
}
}
@@ -58,7 +58,7 @@ impl<Message> Action<Message> {
/// blinking caret on a text input.
pub fn request_redraw_at(at: Instant) -> Self {
Self {
- redraw_request: Some(window::RedrawRequest::At(at)),
+ redraw_request: window::RedrawRequest::At(at),
..Self::new()
}
}
@@ -75,11 +75,7 @@ impl<Message> Action<Message> {
/// widget implementations.
pub fn into_inner(
self,
- ) -> (
- Option<Message>,
- Option<window::RedrawRequest>,
- event::Status,
- ) {
+ ) -> (Option<Message>, window::RedrawRequest, event::Status) {
(
self.message_to_publish,
self.redraw_request,
diff --git a/widget/src/button.rs b/widget/src/button.rs
index 11839d5e..0e24328f 100644
--- a/widget/src/button.rs
+++ b/widget/src/button.rs
@@ -275,7 +275,7 @@ where
fn update(
&mut self,
tree: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
@@ -285,7 +285,7 @@ where
) {
self.content.as_widget_mut().update(
&mut tree.children[0],
- event.clone(),
+ event,
layout.children().next().unwrap(),
cursor,
renderer,
diff --git a/widget/src/canvas.rs b/widget/src/canvas.rs
index 23cc3f2b..046abddf 100644
--- a/widget/src/canvas.rs
+++ b/widget/src/canvas.rs
@@ -218,7 +218,7 @@ where
fn update(
&mut self,
tree: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
@@ -238,27 +238,18 @@ where
{
let (message, redraw_request, event_status) = action.into_inner();
+ shell.request_redraw_at(redraw_request);
+
if let Some(message) = message {
shell.publish(message);
}
- if let Some(redraw_request) = redraw_request {
- match redraw_request {
- window::RedrawRequest::NextFrame => {
- shell.request_redraw();
- }
- window::RedrawRequest::At(at) => {
- shell.request_redraw_at(at);
- }
- }
- }
-
if event_status == event::Status::Captured {
shell.capture_event();
}
}
- if shell.redraw_request() != Some(window::RedrawRequest::NextFrame) {
+ if shell.redraw_request() != window::RedrawRequest::NextFrame {
let mouse_interaction = self
.mouse_interaction(tree, layout, cursor, viewport, renderer);
diff --git a/widget/src/canvas/program.rs b/widget/src/canvas/program.rs
index c68b2830..43446b64 100644
--- a/widget/src/canvas/program.rs
+++ b/widget/src/canvas/program.rs
@@ -32,7 +32,7 @@ where
fn update(
&self,
_state: &mut Self::State,
- _event: Event,
+ _event: &Event,
_bounds: Rectangle,
_cursor: mouse::Cursor,
) -> Option<Action<Message>> {
@@ -82,7 +82,7 @@ where
fn update(
&self,
state: &mut Self::State,
- event: Event,
+ event: &Event,
bounds: Rectangle,
cursor: mouse::Cursor,
) -> Option<Action<Message>> {
diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs
index 663bfad1..6ed3e080 100644
--- a/widget/src/checkbox.rs
+++ b/widget/src/checkbox.rs
@@ -305,7 +305,7 @@ where
fn update(
&mut self,
_tree: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
_renderer: &Renderer,
diff --git a/widget/src/column.rs b/widget/src/column.rs
index c729cbdb..7200690b 100644
--- a/widget/src/column.rs
+++ b/widget/src/column.rs
@@ -260,7 +260,7 @@ where
fn update(
&mut self,
tree: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
@@ -275,13 +275,7 @@ where
.zip(layout.children())
{
child.as_widget_mut().update(
- state,
- event.clone(),
- layout,
- cursor,
- renderer,
- clipboard,
- shell,
+ state, event, layout, cursor, renderer, clipboard, shell,
viewport,
);
}
diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs
index 500d2bec..f71e4a6e 100644
--- a/widget/src/combo_box.rs
+++ b/widget/src/combo_box.rs
@@ -63,7 +63,6 @@ use crate::core::renderer;
use crate::core::text;
use crate::core::time::Instant;
use crate::core::widget::{self, Widget};
-use crate::core::window;
use crate::core::{
Clipboard, Element, Event, Length, Padding, Rectangle, Shell, Size, Theme,
Vector,
@@ -513,7 +512,7 @@ where
fn update(
&mut self,
tree: &mut widget::Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
@@ -541,7 +540,7 @@ where
// Provide it to the widget
self.text_input.update(
&mut tree.children[0],
- event.clone(),
+ event,
layout,
cursor,
renderer,
@@ -554,16 +553,8 @@ where
shell.capture_event();
}
- if let Some(redraw_request) = local_shell.redraw_request() {
- match redraw_request {
- window::RedrawRequest::NextFrame => {
- shell.request_redraw();
- }
- window::RedrawRequest::At(at) => {
- shell.request_redraw_at(at);
- }
- }
- }
+ shell.request_redraw_at(local_shell.redraw_request());
+ shell.request_input_method(local_shell.input_method());
// Then finally react to them here
for message in local_messages {
@@ -742,18 +733,21 @@ where
published_message_to_shell = true;
// Unfocus the input
+ let mut local_messages = Vec::new();
+ let mut local_shell = Shell::new(&mut local_messages);
self.text_input.update(
&mut tree.children[0],
- Event::Mouse(mouse::Event::ButtonPressed(
+ &Event::Mouse(mouse::Event::ButtonPressed(
mouse::Button::Left,
)),
layout,
mouse::Cursor::Unavailable,
renderer,
clipboard,
- &mut Shell::new(&mut vec![]),
+ &mut local_shell,
viewport,
);
+ shell.request_input_method(local_shell.input_method());
}
});
diff --git a/widget/src/container.rs b/widget/src/container.rs
index 852481f1..86c1c7a8 100644
--- a/widget/src/container.rs
+++ b/widget/src/container.rs
@@ -26,6 +26,7 @@ use crate::core::layout;
use crate::core::mouse;
use crate::core::overlay;
use crate::core::renderer;
+use crate::core::theme;
use crate::core::widget::tree::{self, Tree};
use crate::core::widget::{self, Operation};
use crate::core::{
@@ -300,7 +301,7 @@ where
fn update(
&mut self,
tree: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
@@ -714,9 +715,44 @@ pub fn bordered_box(theme: &Theme) -> Style {
/// A [`Container`] with a dark background and white text.
pub fn dark(_theme: &Theme) -> Style {
+ style(theme::palette::Pair {
+ color: color!(0x111111),
+ text: Color::WHITE,
+ })
+}
+
+/// A [`Container`] with a primary background color.
+pub fn primary(theme: &Theme) -> Style {
+ let palette = theme.extended_palette();
+
+ style(palette.primary.base)
+}
+
+/// A [`Container`] with a secondary background color.
+pub fn secondary(theme: &Theme) -> Style {
+ let palette = theme.extended_palette();
+
+ style(palette.secondary.base)
+}
+
+/// A [`Container`] with a success background color.
+pub fn success(theme: &Theme) -> Style {
+ let palette = theme.extended_palette();
+
+ style(palette.success.base)
+}
+
+/// A [`Container`] with a danger background color.
+pub fn danger(theme: &Theme) -> Style {
+ let palette = theme.extended_palette();
+
+ style(palette.danger.base)
+}
+
+fn style(pair: theme::palette::Pair) -> Style {
Style {
- background: Some(color!(0x111111).into()),
- text_color: Some(Color::WHITE),
+ background: Some(pair.color.into()),
+ text_color: Some(pair.text),
border: border::rounded(2),
..Style::default()
}
diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs
index 17cf94cc..42d0f499 100644
--- a/widget/src/helpers.rs
+++ b/widget/src/helpers.rs
@@ -167,7 +167,7 @@ macro_rules! text {
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
/// use iced::font;
/// use iced::widget::{rich_text, span};
-/// use iced::{color, Font};
+/// use iced::{color, never, Font};
///
/// #[derive(Debug, Clone)]
/// enum Message {
@@ -177,9 +177,10 @@ macro_rules! text {
/// fn view(state: &State) -> Element<'_, Message> {
/// rich_text![
/// span("I am red!").color(color!(0xff0000)),
-/// " ",
+/// span(" "),
/// span("And I am bold!").font(Font { weight: font::Weight::Bold, ..Font::default() }),
/// ]
+/// .on_link_click(never)
/// .size(20)
/// .into()
/// }
@@ -187,7 +188,7 @@ macro_rules! text {
#[macro_export]
macro_rules! rich_text {
() => (
- $crate::Column::new()
+ $crate::text::Rich::new()
);
($($x:expr),+ $(,)?) => (
$crate::text::Rich::from_iter([$($crate::text::Span::from($x)),+])
@@ -633,7 +634,7 @@ where
fn update(
&mut self,
state: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
@@ -836,7 +837,7 @@ where
fn update(
&mut self,
tree: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
@@ -871,26 +872,28 @@ where
shell.request_redraw();
}
+ let is_visible =
+ is_hovered || self.is_top_focused || self.is_top_overlay_active;
+
if matches!(
event,
Event::Mouse(
mouse::Event::CursorMoved { .. }
| mouse::Event::ButtonReleased(_)
)
- ) || is_hovered
- || self.is_top_focused
- || self.is_top_overlay_active
+ ) || is_visible
{
+ let redraw_request = shell.redraw_request();
+
self.top.as_widget_mut().update(
- top_tree,
- event.clone(),
- top_layout,
- cursor,
- renderer,
- clipboard,
- shell,
- viewport,
+ top_tree, event, top_layout, cursor, renderer, clipboard,
+ shell, viewport,
);
+
+ // Ignore redraw requests of invisible content
+ if !is_visible {
+ Shell::replace_redraw_request(shell, redraw_request);
+ }
};
if shell.is_event_captured() {
@@ -899,7 +902,7 @@ where
self.base.as_widget_mut().update(
base_tree,
- event.clone(),
+ event,
base_layout,
cursor,
renderer,
@@ -1136,10 +1139,11 @@ where
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
/// use iced::font;
/// use iced::widget::{rich_text, span};
-/// use iced::{color, Font};
+/// use iced::{color, never, Font};
///
/// #[derive(Debug, Clone)]
/// enum Message {
+/// LinkClicked(&'static str),
/// // ...
/// }
///
@@ -1149,13 +1153,14 @@ where
/// span(" "),
/// span("And I am bold!").font(Font { weight: font::Weight::Bold, ..Font::default() }),
/// ])
+/// .on_link_click(never)
/// .size(20)
/// .into()
/// }
/// ```
-pub fn rich_text<'a, Link, Theme, Renderer>(
+pub fn rich_text<'a, Link, Message, Theme, Renderer>(
spans: impl AsRef<[text::Span<'a, Link, Renderer::Font>]> + 'a,
-) -> text::Rich<'a, Link, Theme, Renderer>
+) -> text::Rich<'a, Link, Message, Theme, Renderer>
where
Link: Clone + 'static,
Theme: text::Catalog + 'a,
@@ -1179,7 +1184,7 @@ where
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
/// use iced::font;
/// use iced::widget::{rich_text, span};
-/// use iced::{color, Font};
+/// use iced::{color, never, Font};
///
/// #[derive(Debug, Clone)]
/// enum Message {
@@ -1192,6 +1197,7 @@ where
/// " ",
/// span("And I am bold!").font(Font { weight: font::Weight::Bold, ..Font::default() }),
/// ]
+/// .on_link_click(never)
/// .size(20)
/// .into()
/// }
diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs
index 20a7955f..811241a9 100644
--- a/widget/src/image/viewer.rs
+++ b/widget/src/image/viewer.rs
@@ -151,7 +151,7 @@ where
fn update(
&mut self,
tree: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
@@ -167,7 +167,7 @@ where
return;
};
- match delta {
+ match *delta {
mouse::ScrollDelta::Lines { y, .. }
| mouse::ScrollDelta::Pixels { y, .. } => {
let state = tree.state.downcast_mut::<State>();
@@ -215,6 +215,7 @@ where
}
}
+ shell.request_redraw();
shell.capture_event();
}
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
@@ -226,6 +227,8 @@ where
state.cursor_grabbed_at = Some(cursor_position);
state.starting_offset = state.current_offset;
+
+ shell.request_redraw();
shell.capture_event();
}
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
@@ -233,6 +236,7 @@ where
if state.cursor_grabbed_at.is_some() {
state.cursor_grabbed_at = None;
+ shell.request_redraw();
shell.capture_event();
}
}
@@ -256,7 +260,7 @@ where
.max(0.0)
.round();
- let delta = position - origin;
+ let delta = *position - origin;
let x = if bounds.width < scaled_size.width {
(state.starting_offset.x - delta.x)
@@ -273,6 +277,7 @@ where
};
state.current_offset = Vector::new(x, y);
+ shell.request_redraw();
shell.capture_event();
}
}
diff --git a/widget/src/keyed/column.rs b/widget/src/keyed/column.rs
index ab0b0bde..313b728a 100644
--- a/widget/src/keyed/column.rs
+++ b/widget/src/keyed/column.rs
@@ -300,7 +300,7 @@ where
fn update(
&mut self,
tree: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
@@ -315,13 +315,7 @@ where
.zip(layout.children())
{
child.as_widget_mut().update(
- state,
- event.clone(),
- layout,
- cursor,
- renderer,
- clipboard,
- shell,
+ state, event, layout, cursor, renderer, clipboard, shell,
viewport,
);
}
diff --git a/widget/src/lazy.rs b/widget/src/lazy.rs
index c6710e30..6df026de 100644
--- a/widget/src/lazy.rs
+++ b/widget/src/lazy.rs
@@ -198,7 +198,7 @@ where
fn update(
&mut self,
tree: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
@@ -386,7 +386,7 @@ where
fn update(
&mut self,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
diff --git a/widget/src/lazy/component.rs b/widget/src/lazy/component.rs
index 15b8b62e..c215de7a 100644
--- a/widget/src/lazy/component.rs
+++ b/widget/src/lazy/component.rs
@@ -6,7 +6,6 @@ use crate::core::overlay;
use crate::core::renderer;
use crate::core::widget;
use crate::core::widget::tree::{self, Tree};
-use crate::core::window;
use crate::core::{
self, Clipboard, Element, Length, Point, Rectangle, Shell, Size, Vector,
Widget,
@@ -267,7 +266,10 @@ where
state: tree::State::new(S::default()),
children: vec![Tree::empty()],
})));
+
*self.tree.borrow_mut() = state.clone();
+ self.diff_self();
+
tree::State::new(state)
}
@@ -314,7 +316,7 @@ where
fn update(
&mut self,
tree: &mut Tree,
- event: core::Event,
+ event: &core::Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
@@ -344,17 +346,8 @@ where
}
local_shell.revalidate_layout(|| shell.invalidate_layout());
-
- if let Some(redraw_request) = local_shell.redraw_request() {
- match redraw_request {
- window::RedrawRequest::NextFrame => {
- shell.request_redraw();
- }
- window::RedrawRequest::At(at) => {
- shell.request_redraw_at(at);
- }
- }
- }
+ shell.request_redraw_at(local_shell.redraw_request());
+ shell.request_input_method(local_shell.input_method());
if !local_messages.is_empty() {
let mut heads = self.state.take().unwrap().into_heads();
@@ -603,7 +596,7 @@ where
fn update(
&mut self,
- event: core::Event,
+ event: &core::Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
@@ -629,17 +622,8 @@ where
}
local_shell.revalidate_layout(|| shell.invalidate_layout());
-
- if let Some(redraw_request) = local_shell.redraw_request() {
- match redraw_request {
- window::RedrawRequest::NextFrame => {
- shell.request_redraw();
- }
- window::RedrawRequest::At(at) => {
- shell.request_redraw_at(at);
- }
- }
- }
+ shell.request_redraw_at(local_shell.redraw_request());
+ shell.request_input_method(local_shell.input_method());
if !local_messages.is_empty() {
let mut inner =
diff --git a/widget/src/lazy/responsive.rs b/widget/src/lazy/responsive.rs
index 8129336e..e7c937af 100644
--- a/widget/src/lazy/responsive.rs
+++ b/widget/src/lazy/responsive.rs
@@ -188,7 +188,7 @@ where
fn update(
&mut self,
tree: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
@@ -416,7 +416,7 @@ where
fn update(
&mut self,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
diff --git a/widget/src/lib.rs b/widget/src/lib.rs
index b8cfa98f..31dcc205 100644
--- a/widget/src/lib.rs
+++ b/widget/src/lib.rs
@@ -12,7 +12,6 @@ mod action;
mod column;
mod mouse_area;
mod pin;
-mod row;
mod space;
mod stack;
mod themer;
@@ -28,6 +27,7 @@ pub mod pick_list;
pub mod pop;
pub mod progress_bar;
pub mod radio;
+pub mod row;
pub mod rule;
pub mod scrollable;
pub mod slider;
diff --git a/widget/src/markdown.rs b/widget/src/markdown.rs
index 858ee281..b69c663e 100644
--- a/widget/src/markdown.rs
+++ b/widget/src/markdown.rs
@@ -29,13 +29,9 @@
//! }
//!
//! fn view(&self) -> Element<'_, Message> {
-//! markdown::view(
-//! &self.markdown,
-//! markdown::Settings::default(),
-//! markdown::Style::from_palette(Theme::TokyoNightStorm.palette()),
-//! )
-//! .map(Message::LinkClicked)
-//! .into()
+//! markdown::view(&self.markdown, Theme::TokyoNight)
+//! .map(Message::LinkClicked)
+//! .into()
//! }
//!
//! fn update(state: &mut State, message: Message) {
@@ -59,6 +55,7 @@ use crate::{column, container, rich_text, row, scrollable, span, text};
use std::borrow::BorrowMut;
use std::cell::{Cell, RefCell};
use std::collections::{HashMap, HashSet};
+use std::mem;
use std::ops::Range;
use std::rc::Rc;
use std::sync::Arc;
@@ -144,6 +141,7 @@ impl Content {
let mut state = State {
leftover: String::new(),
references: self.state.references.clone(),
+ images: HashSet::new(),
highlighter: None,
};
@@ -153,6 +151,7 @@ impl Content {
self.items[*index] = item;
}
+ self.state.images.extend(state.images.drain());
drop(state);
}
@@ -167,6 +166,11 @@ impl Content {
pub fn items(&self) -> &[Item] {
&self.items
}
+
+ /// Returns the URLs of the Markdown images present in the [`Content`].
+ pub fn images(&self) -> &HashSet<Url> {
+ &self.state.images
+ }
}
/// A Markdown item.
@@ -179,7 +183,14 @@ pub enum Item {
/// A code block.
///
/// You can enable the `highlighter` feature for syntax highlighting.
- CodeBlock(Vec<Text>),
+ CodeBlock {
+ /// The language of the code block, if any.
+ language: Option<String>,
+ /// The raw code of the code block.
+ code: String,
+ /// The styled lines of text in the code block.
+ lines: Vec<Text>,
+ },
/// A list.
List {
/// The first number of the list, if it is ordered.
@@ -187,6 +198,15 @@ pub enum Item {
/// The items of the list.
items: Vec<Vec<Item>>,
},
+ /// An image.
+ Image {
+ /// The destination URL of the image.
+ url: Url,
+ /// The title of the image.
+ title: String,
+ /// The alternative text of the image.
+ alt: Text,
+ },
}
/// A bunch of parsed Markdown text.
@@ -319,13 +339,9 @@ impl Span {
/// }
///
/// fn view(&self) -> Element<'_, Message> {
-/// markdown::view(
-/// &self.markdown,
-/// markdown::Settings::default(),
-/// markdown::Style::from_palette(Theme::TokyoNightStorm.palette()),
-/// )
-/// .map(Message::LinkClicked)
-/// .into()
+/// markdown::view(&self.markdown, Theme::TokyoNight)
+/// .map(Message::LinkClicked)
+/// .into()
/// }
///
/// fn update(state: &mut State, message: Message) {
@@ -346,6 +362,7 @@ pub fn parse(markdown: &str) -> impl Iterator<Item = Item> + '_ {
struct State {
leftover: String,
references: HashMap<String, String>,
+ images: HashSet<Url>,
#[cfg(feature = "highlighter")]
highlighter: Option<Highlighter>,
}
@@ -367,7 +384,7 @@ impl Highlighter {
parser: iced_highlighter::Stream::new(
&iced_highlighter::Settings {
theme: iced_highlighter::Theme::Base16Ocean,
- token: language.to_string(),
+ token: language.to_owned(),
},
),
language: language.to_owned(),
@@ -436,6 +453,10 @@ fn parse_with<'a>(
mut state: impl BorrowMut<State> + 'a,
markdown: &'a str,
) -> impl Iterator<Item = (Item, &'a str, HashSet<String>)> + 'a {
+ enum Scope {
+ List(List),
+ }
+
struct List {
start: Option<u64>,
items: Vec<Vec<Item>>,
@@ -444,14 +465,17 @@ fn parse_with<'a>(
let broken_links = Rc::new(RefCell::new(HashSet::new()));
let mut spans = Vec::new();
- let mut code = Vec::new();
+ let mut code = String::new();
+ let mut code_language = None;
+ let mut code_lines = Vec::new();
let mut strong = false;
let mut emphasis = false;
let mut strikethrough = false;
let mut metadata = false;
let mut table = false;
let mut link = None;
- let mut lists = Vec::new();
+ let mut image = None;
+ let mut stack = Vec::new();
#[cfg(feature = "highlighter")]
let mut highlighter = None;
@@ -476,7 +500,7 @@ fn parse_with<'a>(
))
} else {
let _ = RefCell::borrow_mut(&broken_links)
- .insert(broken_link.reference.to_string());
+ .insert(broken_link.reference.into_string());
None
}
@@ -492,10 +516,18 @@ fn parse_with<'a>(
}
let produce = move |state: &mut State,
- lists: &mut Vec<List>,
+ stack: &mut Vec<Scope>,
item,
source: Range<usize>| {
- if lists.is_empty() {
+ if let Some(scope) = stack.last_mut() {
+ match scope {
+ Scope::List(list) => {
+ list.items.last_mut().expect("item context").push(item);
+ }
+ }
+
+ None
+ } else {
state.leftover = markdown[source.start..].to_owned();
Some((
@@ -503,16 +535,6 @@ fn parse_with<'a>(
&markdown[source.start..source.end],
broken_links.take(),
))
- } else {
- lists
- .last_mut()
- .expect("list context")
- .items
- .last_mut()
- .expect("item context")
- .push(item);
-
- None
}
};
@@ -549,35 +571,42 @@ fn parse_with<'a>(
None
}
+ pulldown_cmark::Tag::Image {
+ dest_url, title, ..
+ } if !metadata && !table => {
+ image = Url::parse(&dest_url)
+ .ok()
+ .map(|url| (url, title.into_string()));
+ None
+ }
pulldown_cmark::Tag::List(first_item) if !metadata && !table => {
let prev = if spans.is_empty() {
None
} else {
produce(
state.borrow_mut(),
- &mut lists,
+ &mut stack,
Item::Paragraph(Text::new(spans.drain(..).collect())),
source,
)
};
- lists.push(List {
+ stack.push(Scope::List(List {
start: first_item,
items: Vec::new(),
- });
+ }));
prev
}
pulldown_cmark::Tag::Item => {
- lists
- .last_mut()
- .expect("list context")
- .items
- .push(Vec::new());
+ if let Some(Scope::List(list)) = stack.last_mut() {
+ list.items.push(Vec::new());
+ }
+
None
}
pulldown_cmark::Tag::CodeBlock(
- pulldown_cmark::CodeBlockKind::Fenced(_language),
+ pulldown_cmark::CodeBlockKind::Fenced(language),
) if !metadata && !table => {
#[cfg(feature = "highlighter")]
{
@@ -587,9 +616,9 @@ fn parse_with<'a>(
.highlighter
.take()
.filter(|highlighter| {
- highlighter.language == _language.as_ref()
+ highlighter.language == language.as_ref()
})
- .unwrap_or_else(|| Highlighter::new(&_language));
+ .unwrap_or_else(|| Highlighter::new(&language));
highlighter.prepare();
@@ -597,12 +626,15 @@ fn parse_with<'a>(
});
}
+ code_language =
+ (!language.is_empty()).then(|| language.into_string());
+
let prev = if spans.is_empty() {
None
} else {
produce(
state.borrow_mut(),
- &mut lists,
+ &mut stack,
Item::Paragraph(Text::new(spans.drain(..).collect())),
source,
)
@@ -624,7 +656,7 @@ fn parse_with<'a>(
pulldown_cmark::TagEnd::Heading(level) if !metadata && !table => {
produce(
state.borrow_mut(),
- &mut lists,
+ &mut stack,
Item::Heading(level, Text::new(spans.drain(..).collect())),
source,
)
@@ -646,12 +678,16 @@ fn parse_with<'a>(
None
}
pulldown_cmark::TagEnd::Paragraph if !metadata && !table => {
- produce(
- state.borrow_mut(),
- &mut lists,
- Item::Paragraph(Text::new(spans.drain(..).collect())),
- source,
- )
+ if spans.is_empty() {
+ None
+ } else {
+ produce(
+ state.borrow_mut(),
+ &mut stack,
+ Item::Paragraph(Text::new(spans.drain(..).collect())),
+ source,
+ )
+ }
}
pulldown_cmark::TagEnd::Item if !metadata && !table => {
if spans.is_empty() {
@@ -659,18 +695,20 @@ fn parse_with<'a>(
} else {
produce(
state.borrow_mut(),
- &mut lists,
+ &mut stack,
Item::Paragraph(Text::new(spans.drain(..).collect())),
source,
)
}
}
pulldown_cmark::TagEnd::List(_) if !metadata && !table => {
- let list = lists.pop().expect("list context");
+ let scope = stack.pop()?;
+
+ let Scope::List(list) = scope;
produce(
state.borrow_mut(),
- &mut lists,
+ &mut stack,
Item::List {
start: list.start,
items: list.items,
@@ -678,6 +716,20 @@ fn parse_with<'a>(
source,
)
}
+ pulldown_cmark::TagEnd::Image if !metadata && !table => {
+ let (url, title) = image.take()?;
+ let alt = Text::new(spans.drain(..).collect());
+
+ let state = state.borrow_mut();
+ let _ = state.images.insert(url.clone());
+
+ produce(
+ state,
+ &mut stack,
+ Item::Image { url, title, alt },
+ source,
+ )
+ }
pulldown_cmark::TagEnd::CodeBlock if !metadata && !table => {
#[cfg(feature = "highlighter")]
{
@@ -686,8 +738,12 @@ fn parse_with<'a>(
produce(
state.borrow_mut(),
- &mut lists,
- Item::CodeBlock(code.drain(..).collect()),
+ &mut stack,
+ Item::CodeBlock {
+ language: code_language.take(),
+ code: mem::take(&mut code),
+ lines: code_lines.drain(..).collect(),
+ },
source,
)
}
@@ -704,8 +760,10 @@ fn parse_with<'a>(
pulldown_cmark::Event::Text(text) if !metadata && !table => {
#[cfg(feature = "highlighter")]
if let Some(highlighter) = &mut highlighter {
+ code.push_str(&text);
+
for line in text.lines() {
- code.push(Text::new(
+ code_lines.push(Text::new(
highlighter.highlight_line(line).to_vec(),
));
}
@@ -786,15 +844,25 @@ pub struct Settings {
pub code_size: Pixels,
/// The spacing to be used between elements.
pub spacing: Pixels,
+ /// The styling of the Markdown.
+ pub style: Style,
}
impl Settings {
+ /// Creates new [`Settings`] with default text size and the given [`Style`].
+ pub fn with_style(style: impl Into<Style>) -> Self {
+ Self::with_text_size(16, style)
+ }
+
/// Creates new [`Settings`] with the given base text size in [`Pixels`].
///
/// Heading levels will be adjusted automatically. Specifically,
/// the first level will be twice the base size, and then every level
/// after that will be 25% smaller.
- pub fn with_text_size(text_size: impl Into<Pixels>) -> Self {
+ pub fn with_text_size(
+ text_size: impl Into<Pixels>,
+ style: impl Into<Style>,
+ ) -> Self {
let text_size = text_size.into();
Self {
@@ -807,13 +875,20 @@ impl Settings {
h6_size: text_size,
code_size: text_size * 0.75,
spacing: text_size * 0.875,
+ style: style.into(),
}
}
}
-impl Default for Settings {
- fn default() -> Self {
- Self::with_text_size(16)
+impl From<&Theme> for Settings {
+ fn from(theme: &Theme) -> Self {
+ Self::with_style(Style::from(theme))
+ }
+}
+
+impl From<Theme> for Settings {
+ fn from(theme: Theme) -> Self {
+ Self::with_style(Style::from(theme))
}
}
@@ -845,6 +920,24 @@ impl Style {
}
}
+impl From<theme::Palette> for Style {
+ fn from(palette: theme::Palette) -> Self {
+ Self::from_palette(palette)
+ }
+}
+
+impl From<&Theme> for Style {
+ fn from(theme: &Theme) -> Self {
+ Self::from_palette(theme.palette())
+ }
+}
+
+impl From<Theme> for Style {
+ fn from(theme: Theme) -> Self {
+ Self::from_palette(theme.palette())
+ }
+}
+
/// Display a bunch of Markdown items.
///
/// You can obtain the items with [`parse`].
@@ -873,13 +966,9 @@ impl Style {
/// }
///
/// fn view(&self) -> Element<'_, Message> {
-/// markdown::view(
-/// &self.markdown,
-/// markdown::Settings::default(),
-/// markdown::Style::from_palette(Theme::TokyoNightStorm.palette()),
-/// )
-/// .map(Message::LinkClicked)
-/// .into()
+/// markdown::view(&self.markdown, Theme::TokyoNight)
+/// .map(Message::LinkClicked)
+/// .into()
/// }
///
/// fn update(state: &mut State, message: Message) {
@@ -891,109 +980,345 @@ impl Style {
/// }
/// }
/// ```
-pub fn view<'a, 'b, Theme, Renderer>(
- items: impl IntoIterator<Item = &'b Item>,
- settings: Settings,
- style: Style,
+pub fn view<'a, Theme, Renderer>(
+ items: impl IntoIterator<Item = &'a Item>,
+ settings: impl Into<Settings>,
) -> Element<'a, Url, Theme, Renderer>
where
Theme: Catalog + 'a,
Renderer: core::text::Renderer<Font = Font> + 'a,
{
+ view_with(items, settings, &DefaultViewer)
+}
+
+/// Runs [`view`] but with a custom [`Viewer`] to turn an [`Item`] into
+/// an [`Element`].
+///
+/// This is useful if you want to customize the look of certain Markdown
+/// elements.
+pub fn view_with<'a, Message, Theme, Renderer>(
+ items: impl IntoIterator<Item = &'a Item>,
+ settings: impl Into<Settings>,
+ viewer: &impl Viewer<'a, Message, Theme, Renderer>,
+) -> Element<'a, Message, Theme, Renderer>
+where
+ Message: 'a,
+ Theme: Catalog + 'a,
+ Renderer: core::text::Renderer<Font = Font> + 'a,
+{
+ let settings = settings.into();
+
+ let blocks = items
+ .into_iter()
+ .enumerate()
+ .map(|(i, item_)| item(viewer, settings, item_, i));
+
+ Element::new(column(blocks).spacing(settings.spacing))
+}
+
+/// Displays an [`Item`] using the given [`Viewer`].
+pub fn item<'a, Message, Theme, Renderer>(
+ viewer: &impl Viewer<'a, Message, Theme, Renderer>,
+ settings: Settings,
+ item: &'a Item,
+ index: usize,
+) -> Element<'a, Message, Theme, Renderer>
+where
+ Message: 'a,
+ Theme: Catalog + 'a,
+ Renderer: core::text::Renderer<Font = Font> + 'a,
+{
+ match item {
+ Item::Image { url, title, alt } => {
+ viewer.image(settings, url, title, alt)
+ }
+ Item::Heading(level, text) => {
+ viewer.heading(settings, level, text, index)
+ }
+ Item::Paragraph(text) => viewer.paragraph(settings, text),
+ Item::CodeBlock {
+ language,
+ code,
+ lines,
+ } => viewer.code_block(settings, language.as_deref(), code, lines),
+ Item::List { start: None, items } => {
+ viewer.unordered_list(settings, items)
+ }
+ Item::List {
+ start: Some(start),
+ items,
+ } => viewer.ordered_list(settings, *start, items),
+ }
+}
+
+/// Displays a heading using the default look.
+pub fn heading<'a, Message, Theme, Renderer>(
+ settings: Settings,
+ level: &'a HeadingLevel,
+ text: &'a Text,
+ index: usize,
+ on_link_click: impl Fn(Url) -> Message + 'a,
+) -> Element<'a, Message, Theme, Renderer>
+where
+ Message: 'a,
+ Theme: Catalog + 'a,
+ Renderer: core::text::Renderer<Font = Font> + 'a,
+{
let Settings {
- text_size,
h1_size,
h2_size,
h3_size,
h4_size,
h5_size,
h6_size,
- code_size,
- spacing,
+ text_size,
+ ..
} = settings;
- let blocks = items.into_iter().enumerate().map(|(i, item)| match item {
- Item::Heading(level, heading) => {
- container(rich_text(heading.spans(style)).size(match level {
+ container(
+ rich_text(text.spans(settings.style))
+ .on_link_click(on_link_click)
+ .size(match level {
pulldown_cmark::HeadingLevel::H1 => h1_size,
pulldown_cmark::HeadingLevel::H2 => h2_size,
pulldown_cmark::HeadingLevel::H3 => h3_size,
pulldown_cmark::HeadingLevel::H4 => h4_size,
pulldown_cmark::HeadingLevel::H5 => h5_size,
pulldown_cmark::HeadingLevel::H6 => h6_size,
- }))
- .padding(padding::top(if i > 0 {
- text_size / 2.0
- } else {
- Pixels::ZERO
- }))
- .into()
- }
- Item::Paragraph(paragraph) => {
- rich_text(paragraph.spans(style)).size(text_size).into()
- }
- Item::List { start: None, items } => {
- column(items.iter().map(|items| {
- row![
- text("•").size(text_size),
- view(
- items,
- Settings {
- spacing: settings.spacing * 0.6,
- ..settings
- },
- style
- )
- ]
- .spacing(spacing)
- .into()
- }))
- .spacing(spacing * 0.75)
- .into()
- }
- Item::List {
- start: Some(start),
- items,
- } => column(items.iter().enumerate().map(|(i, items)| {
- row![
- text!("{}.", i as u64 + *start).size(text_size),
- view(
- items,
- Settings {
- spacing: settings.spacing * 0.6,
- ..settings
- },
- style
- )
- ]
- .spacing(spacing)
- .into()
- }))
- .spacing(spacing * 0.75)
- .into(),
- Item::CodeBlock(lines) => container(
- scrollable(
- container(column(lines.iter().map(|line| {
- rich_text(line.spans(style))
- .font(Font::MONOSPACE)
- .size(code_size)
- .into()
- })))
- .padding(spacing.0 / 2.0),
+ }),
+ )
+ .padding(padding::top(if index > 0 {
+ text_size / 2.0
+ } else {
+ Pixels::ZERO
+ }))
+ .into()
+}
+
+/// Displays a paragraph using the default look.
+pub fn paragraph<'a, Message, Theme, Renderer>(
+ settings: Settings,
+ text: &'a Text,
+ on_link_click: impl Fn(Url) -> Message + 'a,
+) -> Element<'a, Message, Theme, Renderer>
+where
+ Message: 'a,
+ Theme: Catalog + 'a,
+ Renderer: core::text::Renderer<Font = Font> + 'a,
+{
+ rich_text(text.spans(settings.style))
+ .size(settings.text_size)
+ .on_link_click(on_link_click)
+ .into()
+}
+
+/// Displays an unordered list using the default look and
+/// calling the [`Viewer`] for each bullet point item.
+pub fn unordered_list<'a, Message, Theme, Renderer>(
+ viewer: &impl Viewer<'a, Message, Theme, Renderer>,
+ settings: Settings,
+ items: &'a [Vec<Item>],
+) -> Element<'a, Message, Theme, Renderer>
+where
+ Message: 'a,
+ Theme: Catalog + 'a,
+ Renderer: core::text::Renderer<Font = Font> + 'a,
+{
+ column(items.iter().map(|items| {
+ row![
+ text("•").size(settings.text_size),
+ view_with(
+ items,
+ Settings {
+ spacing: settings.spacing * 0.6,
+ ..settings
+ },
+ viewer,
+ )
+ ]
+ .spacing(settings.spacing)
+ .into()
+ }))
+ .spacing(settings.spacing * 0.75)
+ .padding([0.0, settings.spacing.0])
+ .into()
+}
+
+/// Displays an ordered list using the default look and
+/// calling the [`Viewer`] for each numbered item.
+pub fn ordered_list<'a, Message, Theme, Renderer>(
+ viewer: &impl Viewer<'a, Message, Theme, Renderer>,
+ settings: Settings,
+ start: u64,
+ items: &'a [Vec<Item>],
+) -> Element<'a, Message, Theme, Renderer>
+where
+ Message: 'a,
+ Theme: Catalog + 'a,
+ Renderer: core::text::Renderer<Font = Font> + 'a,
+{
+ column(items.iter().enumerate().map(|(i, items)| {
+ row![
+ text!("{}.", i as u64 + start).size(settings.text_size),
+ view_with(
+ items,
+ Settings {
+ spacing: settings.spacing * 0.6,
+ ..settings
+ },
+ viewer,
)
- .direction(scrollable::Direction::Horizontal(
- scrollable::Scrollbar::default()
- .width(spacing.0 / 2.0)
- .scroller_width(spacing.0 / 2.0),
- )),
+ ]
+ .spacing(settings.spacing)
+ .into()
+ }))
+ .spacing(settings.spacing * 0.75)
+ .padding([0.0, settings.spacing.0])
+ .into()
+}
+
+/// Displays a code block using the default look.
+pub fn code_block<'a, Message, Theme, Renderer>(
+ settings: Settings,
+ lines: &'a [Text],
+ on_link_click: impl Fn(Url) -> Message + Clone + 'a,
+) -> Element<'a, Message, Theme, Renderer>
+where
+ Message: 'a,
+ Theme: Catalog + 'a,
+ Renderer: core::text::Renderer<Font = Font> + 'a,
+{
+ container(
+ scrollable(
+ container(column(lines.iter().map(|line| {
+ rich_text(line.spans(settings.style))
+ .on_link_click(on_link_click.clone())
+ .font(Font::MONOSPACE)
+ .size(settings.code_size)
+ .into()
+ })))
+ .padding(settings.code_size),
)
- .width(Length::Fill)
- .padding(spacing.0 / 2.0)
+ .direction(scrollable::Direction::Horizontal(
+ scrollable::Scrollbar::default()
+ .width(settings.code_size / 2)
+ .scroller_width(settings.code_size / 2),
+ )),
+ )
+ .width(Length::Fill)
+ .padding(settings.code_size / 4)
+ .class(Theme::code_block())
+ .into()
+}
+
+/// A view strategy to display a Markdown [`Item`].j
+pub trait Viewer<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
+where
+ Self: Sized + 'a,
+ Message: 'a,
+ Theme: Catalog + 'a,
+ Renderer: core::text::Renderer<Font = Font> + 'a,
+{
+ /// Produces a message when a link is clicked with the given [`Url`].
+ fn on_link_click(url: Url) -> Message;
+
+ /// Displays an image.
+ ///
+ /// By default, it will show a container with the image title.
+ fn image(
+ &self,
+ settings: Settings,
+ url: &'a Url,
+ title: &'a str,
+ alt: &Text,
+ ) -> Element<'a, Message, Theme, Renderer> {
+ let _url = url;
+ let _title = title;
+
+ container(
+ rich_text(alt.spans(settings.style))
+ .on_link_click(Self::on_link_click),
+ )
+ .padding(settings.spacing.0)
.class(Theme::code_block())
- .into(),
- });
+ .into()
+ }
+
+ /// Displays a heading.
+ ///
+ /// By default, it calls [`heading`].
+ fn heading(
+ &self,
+ settings: Settings,
+ level: &'a HeadingLevel,
+ text: &'a Text,
+ index: usize,
+ ) -> Element<'a, Message, Theme, Renderer> {
+ heading(settings, level, text, index, Self::on_link_click)
+ }
- Element::new(column(blocks).spacing(spacing))
+ /// Displays a paragraph.
+ ///
+ /// By default, it calls [`paragraph`].
+ fn paragraph(
+ &self,
+ settings: Settings,
+ text: &'a Text,
+ ) -> Element<'a, Message, Theme, Renderer> {
+ paragraph(settings, text, Self::on_link_click)
+ }
+
+ /// Displays a code block.
+ ///
+ /// By default, it calls [`code_block`].
+ fn code_block(
+ &self,
+ settings: Settings,
+ language: Option<&'a str>,
+ code: &'a str,
+ lines: &'a [Text],
+ ) -> Element<'a, Message, Theme, Renderer> {
+ let _language = language;
+ let _code = code;
+
+ code_block(settings, lines, Self::on_link_click)
+ }
+
+ /// Displays an unordered list.
+ ///
+ /// By default, it calls [`unordered_list`].
+ fn unordered_list(
+ &self,
+ settings: Settings,
+ items: &'a [Vec<Item>],
+ ) -> Element<'a, Message, Theme, Renderer> {
+ unordered_list(self, settings, items)
+ }
+
+ /// Displays an ordered list.
+ ///
+ /// By default, it calls [`ordered_list`].
+ fn ordered_list(
+ &self,
+ settings: Settings,
+ start: u64,
+ items: &'a [Vec<Item>],
+ ) -> Element<'a, Message, Theme, Renderer> {
+ ordered_list(self, settings, start, items)
+ }
+}
+
+#[derive(Debug, Clone, Copy)]
+struct DefaultViewer;
+
+impl<'a, Theme, Renderer> Viewer<'a, Url, Theme, Renderer> for DefaultViewer
+where
+ Theme: Catalog + 'a,
+ Renderer: core::text::Renderer<Font = Font> + 'a,
+{
+ fn on_link_click(url: Url) -> Url {
+ url
+ }
}
/// The theme catalog of Markdown items.
diff --git a/widget/src/mouse_area.rs b/widget/src/mouse_area.rs
index 9ba3cff5..c1c3ba0f 100644
--- a/widget/src/mouse_area.rs
+++ b/widget/src/mouse_area.rs
@@ -218,7 +218,7 @@ where
fn update(
&mut self,
tree: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
@@ -228,7 +228,7 @@ where
) {
self.content.as_widget_mut().update(
&mut tree.children[0],
- event.clone(),
+ event,
layout,
cursor,
renderer,
@@ -326,7 +326,7 @@ where
fn update<Message: Clone, Theme, Renderer>(
widget: &mut MouseArea<'_, Message, Theme, Renderer>,
tree: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
shell: &mut Shell<'_, Message>,
@@ -425,7 +425,7 @@ fn update<Message: Clone, Theme, Renderer>(
}
Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
if let Some(on_scroll) = widget.on_scroll.as_ref() {
- shell.publish(on_scroll(delta));
+ shell.publish(on_scroll(*delta));
shell.capture_event();
}
}
diff --git a/widget/src/overlay/menu.rs b/widget/src/overlay/menu.rs
index 611476ce..9d0539ff 100644
--- a/widget/src/overlay/menu.rs
+++ b/widget/src/overlay/menu.rs
@@ -263,7 +263,7 @@ where
fn update(
&mut self,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
@@ -388,7 +388,7 @@ where
fn update(
&mut self,
tree: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs
index 5c3b343c..3ae1dfc7 100644
--- a/widget/src/pane_grid.rs
+++ b/widget/src/pane_grid.rs
@@ -474,7 +474,7 @@ where
fn update(
&mut self,
tree: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
@@ -509,15 +509,8 @@ where
let is_picked = picked_pane == Some(pane);
content.update(
- tree,
- event.clone(),
- layout,
- cursor,
- renderer,
- clipboard,
- shell,
- viewport,
- is_picked,
+ tree, event, layout, cursor, renderer, clipboard, shell,
+ viewport, is_picked,
);
}
@@ -687,7 +680,7 @@ where
_ => {}
}
- if shell.redraw_request() != Some(window::RedrawRequest::NextFrame) {
+ if shell.redraw_request() != window::RedrawRequest::NextFrame {
let interaction = self
.grid_interaction(action, layout, cursor)
.or_else(|| {
diff --git a/widget/src/pane_grid/content.rs b/widget/src/pane_grid/content.rs
index be5e5066..4d63dd18 100644
--- a/widget/src/pane_grid/content.rs
+++ b/widget/src/pane_grid/content.rs
@@ -242,7 +242,7 @@ where
pub(crate) fn update(
&mut self,
tree: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
@@ -256,7 +256,7 @@ where
title_bar.update(
&mut tree.children[1],
- event.clone(),
+ event,
children.next().unwrap(),
cursor,
renderer,
diff --git a/widget/src/pane_grid/title_bar.rs b/widget/src/pane_grid/title_bar.rs
index 4bd2c2f6..611c3d67 100644
--- a/widget/src/pane_grid/title_bar.rs
+++ b/widget/src/pane_grid/title_bar.rs
@@ -430,7 +430,7 @@ where
pub(crate) fn update(
&mut self,
tree: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
@@ -456,7 +456,7 @@ where
compact.as_widget_mut().update(
&mut tree.children[2],
- event.clone(),
+ event,
compact_layout,
cursor,
renderer,
@@ -469,7 +469,7 @@ where
controls.full.as_widget_mut().update(
&mut tree.children[1],
- event.clone(),
+ event,
controls_layout,
cursor,
renderer,
@@ -481,7 +481,7 @@ where
} else {
controls.full.as_widget_mut().update(
&mut tree.children[1],
- event.clone(),
+ event,
controls_layout,
cursor,
renderer,
diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs
index 6708e7cd..b751fcc3 100644
--- a/widget/src/pick_list.rs
+++ b/widget/src/pick_list.rs
@@ -430,7 +430,7 @@ where
fn update(
&mut self,
tree: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
_renderer: &Renderer,
@@ -489,13 +489,13 @@ where
let options = self.options.borrow();
let selected = self.selected.as_ref().map(Borrow::borrow);
- let next_option = if y < 0.0 {
+ let next_option = if *y < 0.0 {
if let Some(selected) = selected {
find_next(selected, options.iter())
} else {
options.first()
}
- } else if y > 0.0 {
+ } else if *y > 0.0 {
if let Some(selected) = selected {
find_next(selected, options.iter().rev())
} else {
@@ -513,7 +513,7 @@ where
}
}
Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
- state.keyboard_modifiers = modifiers;
+ state.keyboard_modifiers = *modifiers;
}
_ => {}
};
diff --git a/widget/src/pin.rs b/widget/src/pin.rs
index 7c1aca61..afa29398 100644
--- a/widget/src/pin.rs
+++ b/widget/src/pin.rs
@@ -177,7 +177,7 @@ where
fn update(
&mut self,
tree: &mut widget::Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
diff --git a/widget/src/pop.rs b/widget/src/pop.rs
index 146cfb2b..950371ea 100644
--- a/widget/src/pop.rs
+++ b/widget/src/pop.rs
@@ -3,6 +3,7 @@ use crate::core::layout;
use crate::core::mouse;
use crate::core::overlay;
use crate::core::renderer;
+use crate::core::text;
use crate::core::widget;
use crate::core::widget::tree::{self, Tree};
use crate::core::window;
@@ -17,6 +18,7 @@ use crate::core::{
#[allow(missing_debug_implementations)]
pub struct Pop<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> {
content: Element<'a, Message, Theme, Renderer>,
+ key: Option<text::Fragment<'a>>,
on_show: Option<Box<dyn Fn(Size) -> Message + 'a>>,
on_resize: Option<Box<dyn Fn(Size) -> Message + 'a>>,
on_hide: Option<Message>,
@@ -34,6 +36,7 @@ where
) -> Self {
Self {
content: content.into(),
+ key: None,
on_show: None,
on_resize: None,
on_hide: None,
@@ -66,6 +69,14 @@ where
self
}
+ /// Sets the key of the [`Pop`] widget, for continuity.
+ ///
+ /// If the key changes, the [`Pop`] widget will trigger again.
+ pub fn key(mut self, key: impl text::IntoFragment<'a>) -> Self {
+ self.key = Some(key.into_fragment());
+ self
+ }
+
/// Sets the distance in [`Pixels`] to use in anticipation of the
/// content popping into view.
///
@@ -77,10 +88,11 @@ where
}
}
-#[derive(Debug, Clone, Copy, Default)]
+#[derive(Debug, Clone, Default)]
struct State {
has_popped_in: bool,
last_size: Option<Size>,
+ last_key: Option<String>,
}
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
@@ -108,7 +120,7 @@ where
fn update(
&mut self,
tree: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
@@ -118,8 +130,16 @@ where
) {
if let Event::Window(window::Event::RedrawRequested(_)) = &event {
let state = tree.state.downcast_mut::<State>();
- let bounds = layout.bounds();
+ if state.has_popped_in
+ && state.last_key.as_deref() != self.key.as_deref()
+ {
+ state.has_popped_in = false;
+ state.last_key =
+ self.key.as_ref().cloned().map(text::Fragment::into_owned);
+ }
+
+ let bounds = layout.bounds();
let top_left_distance = viewport.distance(bounds.position());
let bottom_right_distance = viewport
diff --git a/widget/src/radio.rs b/widget/src/radio.rs
index 15c983df..0df4d715 100644
--- a/widget/src/radio.rs
+++ b/widget/src/radio.rs
@@ -326,7 +326,7 @@ where
fn update(
&mut self,
_state: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
_renderer: &Renderer,
diff --git a/widget/src/row.rs b/widget/src/row.rs
index 3b605f07..5ffeab49 100644
--- a/widget/src/row.rs
+++ b/widget/src/row.rs
@@ -256,7 +256,7 @@ where
fn update(
&mut self,
tree: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
@@ -271,13 +271,7 @@ where
.zip(layout.children())
{
child.as_widget_mut().update(
- state,
- event.clone(),
- layout,
- cursor,
- renderer,
- clipboard,
- shell,
+ state, event, layout, cursor, renderer, clipboard, shell,
viewport,
);
}
@@ -495,7 +489,7 @@ where
fn update(
&mut self,
tree: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs
index 312aee29..0cf75c04 100644
--- a/widget/src/scrollable.rs
+++ b/widget/src/scrollable.rs
@@ -33,8 +33,9 @@ use crate::core::widget::operation::{self, Operation};
use crate::core::widget::tree::{self, Tree};
use crate::core::window;
use crate::core::{
- self, Background, Clipboard, Color, Element, Event, Layout, Length,
- Padding, Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget,
+ self, Background, Clipboard, Color, Element, Event, InputMethod, Layout,
+ Length, Padding, Pixels, Point, Rectangle, Shell, Size, Theme, Vector,
+ Widget,
};
use crate::runtime::task::{self, Task};
use crate::runtime::Action;
@@ -516,7 +517,7 @@ where
fn update(
&mut self,
tree: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
@@ -563,7 +564,8 @@ where
Event::Mouse(mouse::Event::CursorMoved { .. })
| Event::Touch(touch::Event::FingerMoved { .. }) => {
if let Some(scrollbar) = scrollbars.y {
- let Some(cursor_position) = cursor.position()
+ let Some(cursor_position) =
+ cursor.land().position()
else {
return;
};
@@ -635,7 +637,8 @@ where
match event {
Event::Mouse(mouse::Event::CursorMoved { .. })
| Event::Touch(touch::Event::FingerMoved { .. }) => {
- let Some(cursor_position) = cursor.position() else {
+ let Some(cursor_position) = cursor.land().position()
+ else {
return;
};
@@ -726,12 +729,14 @@ where
_ => mouse::Cursor::Unavailable,
};
+ let had_input_method = shell.input_method().is_enabled();
+
let translation =
state.translation(self.direction, bounds, content_bounds);
self.content.as_widget_mut().update(
&mut tree.children[0],
- event.clone(),
+ event,
content,
cursor,
renderer,
@@ -743,6 +748,14 @@ where
..bounds
},
);
+
+ if !had_input_method {
+ if let InputMethod::Enabled { position, .. } =
+ shell.input_method_mut()
+ {
+ *position = *position - translation;
+ }
+ }
};
if matches!(
@@ -768,7 +781,7 @@ where
modifiers,
)) = event
{
- state.keyboard_modifiers = modifiers;
+ state.keyboard_modifiers = *modifiers;
return;
}
@@ -779,7 +792,7 @@ where
return;
}
- let delta = match delta {
+ let delta = match *delta {
mouse::ScrollDelta::Lines { x, y } => {
let is_shift_pressed =
state.keyboard_modifiers.shift();
diff --git a/widget/src/shader.rs b/widget/src/shader.rs
index 8ec57482..06254a1c 100644
--- a/widget/src/shader.rs
+++ b/widget/src/shader.rs
@@ -9,7 +9,6 @@ use crate::core::mouse;
use crate::core::renderer;
use crate::core::widget::tree::{self, Tree};
use crate::core::widget::{self, Widget};
-use crate::core::window;
use crate::core::{Clipboard, Element, Event, Length, Rectangle, Shell, Size};
use crate::renderer::wgpu::primitive;
@@ -89,7 +88,7 @@ where
fn update(
&mut self,
tree: &mut Tree,
- event: crate::core::Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
_renderer: &Renderer,
@@ -105,21 +104,12 @@ where
{
let (message, redraw_request, event_status) = action.into_inner();
+ shell.request_redraw_at(redraw_request);
+
if let Some(message) = message {
shell.publish(message);
}
- if let Some(redraw_request) = redraw_request {
- match redraw_request {
- window::RedrawRequest::NextFrame => {
- shell.request_redraw();
- }
- window::RedrawRequest::At(at) => {
- shell.request_redraw_at(at);
- }
- }
- }
-
if event_status == event::Status::Captured {
shell.capture_event();
}
@@ -184,7 +174,7 @@ where
fn update(
&self,
state: &mut Self::State,
- event: Event,
+ event: &Event,
bounds: Rectangle,
cursor: mouse::Cursor,
) -> Option<Action<Message>> {
diff --git a/widget/src/shader/program.rs b/widget/src/shader/program.rs
index 0fc110af..81ecc9b1 100644
--- a/widget/src/shader/program.rs
+++ b/widget/src/shader/program.rs
@@ -26,7 +26,7 @@ pub trait Program<Message> {
fn update(
&self,
_state: &mut Self::State,
- _event: shader::Event,
+ _event: &shader::Event,
_bounds: Rectangle,
_cursor: mouse::Cursor,
) -> Option<Action<Message>> {
diff --git a/widget/src/slider.rs b/widget/src/slider.rs
index 52500854..1908abc9 100644
--- a/widget/src/slider.rs
+++ b/widget/src/slider.rs
@@ -245,7 +245,7 @@ where
fn update(
&mut self,
tree: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
_renderer: &Renderer,
diff --git a/widget/src/stack.rs b/widget/src/stack.rs
index 12ed941d..df9f6162 100644
--- a/widget/src/stack.rs
+++ b/widget/src/stack.rs
@@ -207,7 +207,7 @@ where
fn update(
&mut self,
tree: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
mut cursor: mouse::Cursor,
renderer: &Renderer,
@@ -216,43 +216,34 @@ where
viewport: &Rectangle,
) {
let is_over = cursor.is_over(layout.bounds());
- let is_mouse_movement =
- matches!(event, Event::Mouse(mouse::Event::CursorMoved { .. }));
+ let end = self.children.len() - 1;
- for ((child, state), layout) in self
+ for (i, ((child, state), layout)) in self
.children
.iter_mut()
.rev()
.zip(tree.children.iter_mut().rev())
.zip(layout.children().rev())
+ .enumerate()
{
child.as_widget_mut().update(
- state,
- event.clone(),
- layout,
- cursor,
- renderer,
- clipboard,
- shell,
+ state, event, layout, cursor, renderer, clipboard, shell,
viewport,
);
- if is_over
- && !is_mouse_movement
- && cursor != mouse::Cursor::Unavailable
- {
+ if shell.is_event_captured() {
+ return;
+ }
+
+ if i < end && is_over && !cursor.is_levitating() {
let interaction = child.as_widget().mouse_interaction(
state, layout, cursor, viewport, renderer,
);
if interaction != mouse::Interaction::None {
- cursor = mouse::Cursor::Unavailable;
+ cursor = cursor.levitate();
}
}
-
- if shell.is_event_captured() {
- return;
- }
}
}
diff --git a/widget/src/text/rich.rs b/widget/src/text/rich.rs
index 0b499ec6..4d4a2861 100644
--- a/widget/src/text/rich.rs
+++ b/widget/src/text/rich.rs
@@ -14,8 +14,13 @@ use crate::core::{
/// A bunch of [`Rich`] text.
#[allow(missing_debug_implementations)]
-pub struct Rich<'a, Link, Theme = crate::Theme, Renderer = crate::Renderer>
-where
+pub struct Rich<
+ 'a,
+ Link,
+ Message,
+ Theme = crate::Theme,
+ Renderer = crate::Renderer,
+> where
Link: Clone + 'static,
Theme: Catalog,
Renderer: core::text::Renderer,
@@ -31,9 +36,11 @@ where
wrapping: Wrapping,
class: Theme::Class<'a>,
hovered_link: Option<usize>,
+ on_link_click: Option<Box<dyn Fn(Link) -> Message + 'a>>,
}
-impl<'a, Link, Theme, Renderer> Rich<'a, Link, Theme, Renderer>
+impl<'a, Link, Message, Theme, Renderer>
+ Rich<'a, Link, Message, Theme, Renderer>
where
Link: Clone + 'static,
Theme: Catalog,
@@ -54,6 +61,7 @@ where
wrapping: Wrapping::default(),
class: Theme::default(),
hovered_link: None,
+ on_link_click: None,
}
}
@@ -127,6 +135,16 @@ where
self
}
+ /// Sets the message that will be produced when a link of the [`Rich`] text
+ /// is clicked.
+ pub fn on_link_click(
+ mut self,
+ on_link_clicked: impl Fn(Link) -> Message + 'a,
+ ) -> Self {
+ self.on_link_click = Some(Box::new(on_link_clicked));
+ self
+ }
+
/// Sets the default style of the [`Rich`] text.
#[must_use]
pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
@@ -164,7 +182,8 @@ where
}
}
-impl<'a, Link, Theme, Renderer> Default for Rich<'a, Link, Theme, Renderer>
+impl<'a, Link, Message, Theme, Renderer> Default
+ for Rich<'a, Link, Message, Theme, Renderer>
where
Link: Clone + 'a,
Theme: Catalog,
@@ -182,8 +201,8 @@ struct State<Link, P: Paragraph> {
paragraph: P,
}
-impl<Link, Theme, Renderer> Widget<Link, Theme, Renderer>
- for Rich<'_, Link, Theme, Renderer>
+impl<Link, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
+ for Rich<'_, Link, Message, Theme, Renderer>
where
Link: Clone + 'static,
Theme: Catalog,
@@ -252,7 +271,8 @@ where
let style = theme.style(&self.class);
for (index, span) in self.spans.as_ref().as_ref().iter().enumerate() {
- let is_hovered_link = Some(index) == self.hovered_link;
+ let is_hovered_link = self.on_link_click.is_some()
+ && Some(index) == self.hovered_link;
if span.highlight.is_some()
|| span.underline
@@ -358,14 +378,18 @@ where
fn update(
&mut self,
tree: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
- shell: &mut Shell<'_, Link>,
+ shell: &mut Shell<'_, Message>,
_viewport: &Rectangle,
) {
+ let Some(on_link_clicked) = &self.on_link_click else {
+ return;
+ };
+
let was_hovered = self.hovered_link.is_some();
if let Some(position) = cursor.position_in(layout.bounds()) {
@@ -414,7 +438,7 @@ where
.get(span)
.and_then(|span| span.link.clone())
{
- shell.publish(link);
+ shell.publish(on_link_clicked(link));
}
}
_ => {}
@@ -509,8 +533,9 @@ where
})
}
-impl<'a, Link, Theme, Renderer> FromIterator<Span<'a, Link, Renderer::Font>>
- for Rich<'a, Link, Theme, Renderer>
+impl<'a, Link, Message, Theme, Renderer>
+ FromIterator<Span<'a, Link, Renderer::Font>>
+ for Rich<'a, Link, Message, Theme, Renderer>
where
Link: Clone + 'a,
Theme: Catalog,
@@ -524,16 +549,18 @@ where
}
}
-impl<'a, Link, Theme, Renderer> From<Rich<'a, Link, Theme, Renderer>>
- for Element<'a, Link, Theme, Renderer>
+impl<'a, Link, Message, Theme, Renderer>
+ From<Rich<'a, Link, Message, Theme, Renderer>>
+ for Element<'a, Message, Theme, Renderer>
where
+ Message: 'a,
Link: Clone + 'a,
Theme: Catalog + 'a,
Renderer: core::text::Renderer + 'a,
{
fn from(
- text: Rich<'a, Link, Theme, Renderer>,
- ) -> Element<'a, Link, Theme, Renderer> {
+ text: Rich<'a, Link, Message, Theme, Renderer>,
+ ) -> Element<'a, Message, Theme, Renderer> {
Element::new(text)
}
}
diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs
index f1ec589b..7e40a56a 100644
--- a/widget/src/text_editor.rs
+++ b/widget/src/text_editor.rs
@@ -33,6 +33,7 @@
//! ```
use crate::core::alignment;
use crate::core::clipboard::{self, Clipboard};
+use crate::core::input_method;
use crate::core::keyboard;
use crate::core::keyboard::key;
use crate::core::layout::{self, Layout};
@@ -46,14 +47,15 @@ use crate::core::widget::operation;
use crate::core::widget::{self, Widget};
use crate::core::window;
use crate::core::{
- Background, Border, Color, Element, Event, Length, Padding, Pixels, Point,
- Rectangle, Shell, Size, SmolStr, Theme, Vector,
+ Background, Border, Color, Element, Event, InputMethod, Length, Padding,
+ Pixels, Point, Rectangle, Shell, Size, SmolStr, Theme, Vector,
};
use std::borrow::Cow;
use std::cell::RefCell;
use std::fmt;
use std::ops::DerefMut;
+use std::ops::Range;
use std::sync::Arc;
pub use text::editor::{Action, Edit, Line, LineEnding, Motion};
@@ -322,6 +324,47 @@ where
self.class = class.into();
self
}
+
+ fn input_method<'b>(
+ &self,
+ state: &'b State<Highlighter>,
+ renderer: &Renderer,
+ layout: Layout<'_>,
+ ) -> InputMethod<&'b str> {
+ let Some(Focus {
+ is_window_focused: true,
+ ..
+ }) = &state.focus
+ else {
+ return InputMethod::Disabled;
+ };
+
+ let bounds = layout.bounds();
+ let internal = self.content.0.borrow_mut();
+
+ let text_bounds = bounds.shrink(self.padding);
+ let translation = text_bounds.position() - Point::ORIGIN;
+
+ let cursor = match internal.editor.cursor() {
+ Cursor::Caret(position) => position,
+ Cursor::Selection(ranges) => {
+ ranges.first().cloned().unwrap_or_default().position()
+ }
+ };
+
+ let line_height = self.line_height.to_absolute(
+ self.text_size.unwrap_or_else(|| renderer.default_size()),
+ );
+
+ let position =
+ cursor + translation + Vector::new(0.0, f32::from(line_height));
+
+ InputMethod::Enabled {
+ position,
+ purpose: input_method::Purpose::Normal,
+ preedit: state.preedit.as_ref().map(input_method::Preedit::as_ref),
+ }
+ }
}
/// The content of a [`TextEditor`].
@@ -450,6 +493,7 @@ where
#[derive(Debug)]
pub struct State<Highlighter: text::Highlighter> {
focus: Option<Focus>,
+ preedit: Option<input_method::Preedit>,
last_click: Option<mouse::Click>,
drag_click: Option<mouse::click::Kind>,
partial_scroll: f32,
@@ -458,7 +502,7 @@ pub struct State<Highlighter: text::Highlighter> {
highlighter_format_address: usize,
}
-#[derive(Debug, Clone, Copy)]
+#[derive(Debug, Clone)]
struct Focus {
updated_at: Instant,
now: Instant,
@@ -524,6 +568,7 @@ where
fn state(&self) -> widget::tree::State {
widget::tree::State::new(State {
focus: None,
+ preedit: None,
last_click: None,
drag_click: None,
partial_scroll: 0.0,
@@ -602,10 +647,10 @@ where
fn update(
&mut self,
tree: &mut widget::Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
- _renderer: &Renderer,
+ renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
_viewport: &Rectangle,
@@ -637,17 +682,18 @@ where
Event::Window(window::Event::RedrawRequested(now)) => {
if let Some(focus) = &mut state.focus {
if focus.is_window_focused {
- focus.now = now;
+ focus.now = *now;
let millis_until_redraw =
Focus::CURSOR_BLINK_INTERVAL_MILLIS
- - (now - focus.updated_at).as_millis()
+ - (focus.now - focus.updated_at).as_millis()
% Focus::CURSOR_BLINK_INTERVAL_MILLIS;
shell.request_redraw_at(
- now + Duration::from_millis(
- millis_until_redraw as u64,
- ),
+ focus.now
+ + Duration::from_millis(
+ millis_until_redraw as u64,
+ ),
);
}
}
@@ -701,6 +747,28 @@ where
}));
shell.capture_event();
}
+ Update::InputMethod(update) => match update {
+ Ime::Toggle(is_open) => {
+ state.preedit =
+ is_open.then(input_method::Preedit::new);
+
+ shell.request_redraw();
+ }
+ Ime::Preedit { content, selection } => {
+ state.preedit = Some(input_method::Preedit {
+ content,
+ selection,
+ text_size: self.text_size,
+ });
+
+ shell.request_redraw();
+ }
+ Ime::Commit(text) => {
+ shell.publish(on_edit(Action::Edit(Edit::Paste(
+ Arc::new(text),
+ ))));
+ }
+ },
Update::Binding(binding) => {
fn apply_binding<
H: text::Highlighter,
@@ -827,6 +895,10 @@ where
if is_redraw {
self.last_status = Some(status);
+
+ shell.request_input_method(
+ &self.input_method(state, renderer, layout),
+ );
} else if self
.last_status
.is_some_and(|last_status| status != last_status)
@@ -1129,12 +1201,22 @@ enum Update<Message> {
Drag(Point),
Release,
Scroll(f32),
+ InputMethod(Ime),
Binding(Binding<Message>),
}
+enum Ime {
+ Toggle(bool),
+ Preedit {
+ content: String,
+ selection: Option<Range<usize>>,
+ },
+ Commit(String),
+}
+
impl<Message> Update<Message> {
fn from_event<H: Highlighter>(
- event: Event,
+ event: &Event,
state: &State<H>,
bounds: Rectangle,
padding: Padding,
@@ -1191,6 +1273,28 @@ impl<Message> Update<Message> {
}
_ => None,
},
+ Event::InputMethod(event) => match event {
+ input_method::Event::Opened | input_method::Event::Closed => {
+ Some(Update::InputMethod(Ime::Toggle(matches!(
+ event,
+ input_method::Event::Opened
+ ))))
+ }
+ input_method::Event::Preedit(content, selection)
+ if state.focus.is_some() =>
+ {
+ Some(Update::InputMethod(Ime::Preedit {
+ content: content.clone(),
+ selection: selection.clone(),
+ }))
+ }
+ input_method::Event::Commit(content)
+ if state.focus.is_some() =>
+ {
+ Some(Update::InputMethod(Ime::Commit(content.clone())))
+ }
+ _ => None,
+ },
Event::Keyboard(keyboard::Event::KeyPressed {
key,
modifiers,
@@ -1206,9 +1310,9 @@ impl<Message> Update<Message> {
};
let key_press = KeyPress {
- key,
- modifiers,
- text,
+ key: key.clone(),
+ modifiers: *modifiers,
+ text: text.clone(),
status,
};
diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs
index 57ebe46a..ae3dfe4c 100644
--- a/widget/src/text_input.rs
+++ b/widget/src/text_input.rs
@@ -42,6 +42,7 @@ use editor::Editor;
use crate::core::alignment;
use crate::core::clipboard::{self, Clipboard};
+use crate::core::input_method;
use crate::core::keyboard;
use crate::core::keyboard::key;
use crate::core::layout;
@@ -56,8 +57,8 @@ use crate::core::widget::operation::{self, Operation};
use crate::core::widget::tree::{self, Tree};
use crate::core::window;
use crate::core::{
- Background, Border, Color, Element, Event, Layout, Length, Padding, Pixels,
- Point, Rectangle, Shell, Size, Theme, Vector, Widget,
+ Background, Border, Color, Element, Event, InputMethod, Layout, Length,
+ Padding, Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget,
};
use crate::runtime::task::{self, Task};
use crate::runtime::Action;
@@ -391,6 +392,54 @@ where
}
}
+ fn input_method<'b>(
+ &self,
+ state: &'b State<Renderer::Paragraph>,
+ layout: Layout<'_>,
+ value: &Value,
+ ) -> InputMethod<&'b str> {
+ let Some(Focus {
+ is_window_focused: true,
+ ..
+ }) = &state.is_focused
+ else {
+ return InputMethod::Disabled;
+ };
+
+ let secure_value = self.is_secure.then(|| value.secure());
+ let value = secure_value.as_ref().unwrap_or(value);
+
+ let text_bounds = layout.children().next().unwrap().bounds();
+
+ let caret_index = match state.cursor.state(value) {
+ cursor::State::Index(position) => position,
+ cursor::State::Selection { start, end } => start.min(end),
+ };
+
+ let text = state.value.raw();
+ let (cursor_x, scroll_offset) =
+ measure_cursor_and_scroll_offset(text, text_bounds, caret_index);
+
+ let alignment_offset = alignment_offset(
+ text_bounds.width,
+ text.min_width(),
+ self.alignment,
+ );
+
+ let x = (text_bounds.x + cursor_x).floor() - scroll_offset
+ + alignment_offset;
+
+ InputMethod::Enabled {
+ position: Point::new(x, text_bounds.y + text_bounds.height),
+ purpose: if self.is_secure {
+ input_method::Purpose::Secure
+ } else {
+ input_method::Purpose::Normal
+ },
+ preedit: state.preedit.as_ref().map(input_method::Preedit::as_ref),
+ }
+ }
+
/// Draws the [`TextInput`] with the given [`Renderer`], overriding its
/// [`Value`] if provided.
///
@@ -529,7 +578,13 @@ where
};
let draw = |renderer: &mut Renderer, viewport| {
- let paragraph = if text.is_empty() {
+ let paragraph = if text.is_empty()
+ && state
+ .preedit
+ .as_ref()
+ .map(|preedit| preedit.content.is_empty())
+ .unwrap_or(true)
+ {
state.placeholder.raw()
} else {
state.value.raw()
@@ -639,7 +694,7 @@ where
fn update(
&mut self,
tree: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
@@ -1197,6 +1252,52 @@ where
state.keyboard_modifiers = *modifiers;
}
+ Event::InputMethod(event) => match event {
+ input_method::Event::Opened | input_method::Event::Closed => {
+ let state = state::<Renderer>(tree);
+
+ state.preedit =
+ matches!(event, input_method::Event::Opened)
+ .then(input_method::Preedit::new);
+
+ shell.request_redraw();
+ }
+ input_method::Event::Preedit(content, selection) => {
+ let state = state::<Renderer>(tree);
+
+ if state.is_focused.is_some() {
+ state.preedit = Some(input_method::Preedit {
+ content: content.to_owned(),
+ selection: selection.clone(),
+ text_size: self.size,
+ });
+
+ shell.request_redraw();
+ }
+ }
+ input_method::Event::Commit(text) => {
+ let state = state::<Renderer>(tree);
+
+ if let Some(focus) = &mut state.is_focused {
+ let Some(on_input) = &self.on_input else {
+ return;
+ };
+
+ let mut editor =
+ Editor::new(&mut self.value, &mut state.cursor);
+ editor.paste(Value::new(text));
+
+ focus.updated_at = Instant::now();
+ state.is_pasting = None;
+
+ let message = (on_input)(editor.contents());
+ shell.publish(message);
+ shell.capture_event();
+
+ update_cache(state, &self.value);
+ }
+ }
+ },
Event::Window(window::Event::Unfocused) => {
let state = state::<Renderer>(tree);
@@ -1218,23 +1319,30 @@ where
let state = state::<Renderer>(tree);
if let Some(focus) = &mut state.is_focused {
- if focus.is_window_focused
- && matches!(
+ if focus.is_window_focused {
+ if matches!(
state.cursor.state(&self.value),
cursor::State::Index(_)
- )
- {
- focus.now = *now;
-
- let millis_until_redraw = CURSOR_BLINK_INTERVAL_MILLIS
- - (*now - focus.updated_at).as_millis()
- % CURSOR_BLINK_INTERVAL_MILLIS;
-
- shell.request_redraw_at(
- *now + Duration::from_millis(
- millis_until_redraw as u64,
- ),
- );
+ ) {
+ focus.now = *now;
+
+ let millis_until_redraw =
+ CURSOR_BLINK_INTERVAL_MILLIS
+ - (*now - focus.updated_at).as_millis()
+ % CURSOR_BLINK_INTERVAL_MILLIS;
+
+ shell.request_redraw_at(
+ *now + Duration::from_millis(
+ millis_until_redraw as u64,
+ ),
+ );
+ }
+
+ shell.request_input_method(&self.input_method(
+ state,
+ layout,
+ &self.value,
+ ));
}
}
}
@@ -1419,6 +1527,7 @@ pub struct State<P: text::Paragraph> {
is_focused: Option<Focus>,
is_dragging: bool,
is_pasting: Option<Value>,
+ preedit: Option<input_method::Preedit>,
last_click: Option<mouse::Click>,
cursor: Cursor,
keyboard_modifiers: keyboard::Modifiers,
@@ -1431,7 +1540,7 @@ fn state<Renderer: text::Renderer>(
tree.state.downcast_mut::<State<Renderer::Paragraph>>()
}
-#[derive(Debug, Clone, Copy)]
+#[derive(Debug, Clone)]
struct Focus {
updated_at: Instant,
now: Instant,
@@ -1614,7 +1723,7 @@ fn replace_paragraph<Renderer>(
bounds: Size::new(f32::INFINITY, text_bounds.height),
size: text_size,
horizontal_alignment: alignment::Horizontal::Left,
- vertical_alignment: alignment::Vertical::Top,
+ vertical_alignment: alignment::Vertical::Center,
shaping: text::Shaping::Advanced,
wrapping: text::Wrapping::default(),
});
diff --git a/widget/src/themer.rs b/widget/src/themer.rs
index 769cc4ca..4e583882 100644
--- a/widget/src/themer.rs
+++ b/widget/src/themer.rs
@@ -113,7 +113,7 @@ where
fn update(
&mut self,
tree: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
@@ -220,7 +220,7 @@ where
fn update(
&mut self,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs
index 56c2be1f..b711432e 100644
--- a/widget/src/toggler.rs
+++ b/widget/src/toggler.rs
@@ -309,7 +309,7 @@ where
fn update(
&mut self,
_state: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
_renderer: &Renderer,
diff --git a/widget/src/tooltip.rs b/widget/src/tooltip.rs
index a0ffe392..5bebeeac 100644
--- a/widget/src/tooltip.rs
+++ b/widget/src/tooltip.rs
@@ -192,7 +192,7 @@ where
fn update(
&mut self,
tree: &mut widget::Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
diff --git a/widget/src/vertical_slider.rs b/widget/src/vertical_slider.rs
index 2ed9419a..6f878fde 100644
--- a/widget/src/vertical_slider.rs
+++ b/widget/src/vertical_slider.rs
@@ -249,7 +249,7 @@ where
fn update(
&mut self,
tree: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
_renderer: &Renderer,
@@ -379,7 +379,7 @@ where
if state.keyboard_modifiers.control() =>
{
if cursor.is_over(layout.bounds()) {
- let delta = match delta {
+ let delta = match *delta {
mouse::ScrollDelta::Lines { x: _, y } => y,
mouse::ScrollDelta::Pixels { x: _, y } => y,
};
@@ -411,7 +411,7 @@ where
}
}
Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
- state.keyboard_modifiers = modifiers;
+ state.keyboard_modifiers = *modifiers;
}
_ => {}
}
diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs
index 462be65b..ab84afff 100644
--- a/winit/src/conversion.rs
+++ b/winit/src/conversion.rs
@@ -2,6 +2,7 @@
//!
//! [`winit`]: https://github.com/rust-windowing/winit
//! [`iced_runtime`]: https://github.com/iced-rs/iced/tree/0.13/runtime
+use crate::core::input_method;
use crate::core::keyboard;
use crate::core::mouse;
use crate::core::touch;
@@ -140,6 +141,7 @@ pub fn window_event(
scale_factor: f64,
modifiers: winit::keyboard::ModifiersState,
) -> Option<Event> {
+ use winit::event::Ime;
use winit::event::WindowEvent;
match event {
@@ -283,6 +285,15 @@ 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(
+ content,
+ size.map(|(start, end)| (start..end)),
+ ),
+ Ime::Commit(content) => input_method::Event::Commit(content),
+ Ime::Disabled => input_method::Event::Closed,
+ })),
WindowEvent::Focused(focused) => Some(Event::Window(if focused {
window::Event::Focused
} else {
@@ -1160,7 +1171,7 @@ pub fn resize_direction(
}
}
-/// Converts some [`window::Icon`] into it's `winit` counterpart.
+/// Converts some [`window::Icon`] into its `winit` counterpart.
///
/// Returns `None` if there is an error during the conversion.
pub fn icon(icon: window::Icon) -> Option<winit::window::Icon> {
@@ -1169,6 +1180,17 @@ pub fn icon(icon: window::Icon) -> Option<winit::window::Icon> {
winit::window::Icon::from_rgba(pixels, size.width, size.height).ok()
}
+/// Convertions some [`input_method::Purpose`] to its `winit` counterpart.
+pub fn ime_purpose(
+ purpose: input_method::Purpose,
+) -> winit::window::ImePurpose {
+ match purpose {
+ input_method::Purpose::Normal => winit::window::ImePurpose::Normal,
+ input_method::Purpose::Secure => winit::window::ImePurpose::Password,
+ input_method::Purpose::Terminal => winit::window::ImePurpose::Terminal,
+ }
+}
+
// See: https://en.wikipedia.org/wiki/Private_Use_Areas
fn is_private_use(c: char) -> bool {
('\u{E000}'..='\u{F8FF}').contains(&c)
diff --git a/winit/src/program.rs b/winit/src/program.rs
index d8436212..9a64fa51 100644
--- a/winit/src/program.rs
+++ b/winit/src/program.rs
@@ -363,7 +363,7 @@ where
(
ControlFlow::WaitUntil(current),
ControlFlow::WaitUntil(new),
- ) if new < current => {}
+ ) if current < new => {}
(
ControlFlow::WaitUntil(target),
ControlFlow::Wait,
@@ -873,20 +873,16 @@ async fn run_instance<P, C>(
});
if let user_interface::State::Updated {
- redraw_request: Some(redraw_request),
+ redraw_request,
+ input_method,
} = ui_state
{
- match redraw_request {
- window::RedrawRequest::NextFrame => {
- window.raw.request_redraw();
- window.redraw_at = None;
- }
- window::RedrawRequest::At(at) => {
- window.redraw_at = Some(at);
- }
- }
+ window.request_redraw(redraw_request);
+ window.request_input_method(input_method);
}
+ window.draw_preedit();
+
debug.render_started();
match compositor.present(
&mut window.renderer,
@@ -1029,27 +1025,23 @@ async fn run_instance<P, C>(
);
#[cfg(feature = "unconditional-rendering")]
- window.raw.request_redraw();
+ window.request_redraw(
+ window::RedrawRequest::NextFrame,
+ );
match ui_state {
- #[cfg(not(
- feature = "unconditional-rendering"
- ))]
user_interface::State::Updated {
- redraw_request: Some(redraw_request),
- } => match redraw_request {
- window::RedrawRequest::NextFrame => {
- window.raw.request_redraw();
- window.redraw_at = None;
- }
- window::RedrawRequest::At(at) => {
- window.redraw_at = Some(at);
- }
- },
+ redraw_request: _redraw_request,
+ ..
+ } => {
+ #[cfg(not(
+ feature = "unconditional-rendering"
+ ))]
+ window.request_redraw(_redraw_request);
+ }
user_interface::State::Outdated => {
uis_stale = true;
}
- user_interface::State::Updated { .. } => {}
}
for (event, status) in window_events
diff --git a/winit/src/program/window_manager.rs b/winit/src/program/window_manager.rs
index a3c991df..139d787a 100644
--- a/winit/src/program/window_manager.rs
+++ b/winit/src/program/window_manager.rs
@@ -1,14 +1,23 @@
+use crate::conversion;
+use crate::core::alignment;
+use crate::core::input_method;
use crate::core::mouse;
+use crate::core::renderer;
+use crate::core::text;
use crate::core::theme;
use crate::core::time::Instant;
-use crate::core::window::Id;
-use crate::core::{Point, Size};
+use crate::core::window::{Id, RedrawRequest};
+use crate::core::{
+ Color, InputMethod, Padding, Point, Rectangle, Size, Text, Vector,
+};
use crate::graphics::Compositor;
use crate::program::{Program, State};
+use winit::dpi::{LogicalPosition, LogicalSize};
+use winit::monitor::MonitorHandle;
+
use std::collections::BTreeMap;
use std::sync::Arc;
-use winit::monitor::MonitorHandle;
#[allow(missing_debug_implementations)]
pub struct WindowManager<P, C>
@@ -65,6 +74,8 @@ where
renderer,
mouse_interaction: mouse::Interaction::None,
redraw_at: None,
+ preedit: None,
+ ime_state: None,
},
);
@@ -155,6 +166,8 @@ where
pub surface: C::Surface,
pub renderer: P::Renderer,
pub redraw_at: Option<Instant>,
+ preedit: Option<Preedit<P::Renderer>>,
+ ime_state: Option<(Point, input_method::Purpose)>,
}
impl<P, C> Window<P, C>
@@ -179,4 +192,231 @@ where
Size::new(size.width, size.height)
}
+
+ pub fn request_redraw(&mut self, redraw_request: RedrawRequest) {
+ match redraw_request {
+ RedrawRequest::NextFrame => {
+ self.raw.request_redraw();
+ self.redraw_at = None;
+ }
+ RedrawRequest::At(at) => {
+ self.redraw_at = Some(at);
+ }
+ RedrawRequest::Wait => {}
+ }
+ }
+
+ pub fn request_input_method(&mut self, input_method: InputMethod) {
+ match input_method {
+ InputMethod::Disabled => {
+ self.disable_ime();
+ }
+ InputMethod::Enabled {
+ position,
+ purpose,
+ preedit,
+ } => {
+ self.enable_ime(position, purpose);
+
+ if let Some(preedit) = preedit {
+ if preedit.content.is_empty() {
+ self.preedit = None;
+ } else {
+ let mut overlay =
+ self.preedit.take().unwrap_or_else(Preedit::new);
+
+ overlay.update(
+ position,
+ &preedit,
+ self.state.background_color(),
+ &self.renderer,
+ );
+
+ self.preedit = Some(overlay);
+ }
+ } else {
+ self.preedit = None;
+ }
+ }
+ }
+ }
+
+ pub fn draw_preedit(&mut self) {
+ if let Some(preedit) = &self.preedit {
+ preedit.draw(
+ &mut self.renderer,
+ self.state.text_color(),
+ self.state.background_color(),
+ &Rectangle::new(
+ Point::ORIGIN,
+ self.state.viewport().logical_size(),
+ ),
+ );
+ }
+ }
+
+ fn enable_ime(&mut self, position: Point, purpose: input_method::Purpose) {
+ if self.ime_state.is_none() {
+ self.raw.set_ime_allowed(true);
+ }
+
+ if self.ime_state != Some((position, purpose)) {
+ self.raw.set_ime_cursor_area(
+ LogicalPosition::new(position.x, position.y),
+ LogicalSize::new(10, 10), // TODO?
+ );
+ self.raw.set_ime_purpose(conversion::ime_purpose(purpose));
+
+ self.ime_state = Some((position, purpose));
+ }
+ }
+
+ fn disable_ime(&mut self) {
+ if self.ime_state.is_some() {
+ self.raw.set_ime_allowed(false);
+ self.ime_state = None;
+ }
+
+ self.preedit = None;
+ }
+}
+
+struct Preedit<Renderer>
+where
+ Renderer: text::Renderer,
+{
+ position: Point,
+ content: Renderer::Paragraph,
+ spans: Vec<text::Span<'static, (), Renderer::Font>>,
+}
+
+impl<Renderer> Preedit<Renderer>
+where
+ Renderer: text::Renderer,
+{
+ fn new() -> Self {
+ Self {
+ position: Point::ORIGIN,
+ spans: Vec::new(),
+ content: Renderer::Paragraph::default(),
+ }
+ }
+
+ fn update(
+ &mut self,
+ position: Point,
+ preedit: &input_method::Preedit,
+ background: Color,
+ renderer: &Renderer,
+ ) {
+ self.position = position;
+
+ let spans = match &preedit.selection {
+ Some(selection) => {
+ vec![
+ text::Span::new(&preedit.content[..selection.start]),
+ text::Span::new(if selection.start == selection.end {
+ "\u{200A}"
+ } else {
+ &preedit.content[selection.start..selection.end]
+ })
+ .color(background),
+ text::Span::new(&preedit.content[selection.end..]),
+ ]
+ }
+ _ => vec![text::Span::new(&preedit.content)],
+ };
+
+ if spans != self.spans.as_slice() {
+ use text::Paragraph as _;
+
+ self.content = Renderer::Paragraph::with_spans(Text {
+ content: &spans,
+ bounds: Size::INFINITY,
+ size: preedit
+ .text_size
+ .unwrap_or_else(|| renderer.default_size()),
+ line_height: text::LineHeight::default(),
+ font: renderer.default_font(),
+ horizontal_alignment: alignment::Horizontal::Left,
+ vertical_alignment: alignment::Vertical::Top,
+ shaping: text::Shaping::Advanced,
+ wrapping: text::Wrapping::None,
+ });
+
+ self.spans.clear();
+ self.spans
+ .extend(spans.into_iter().map(text::Span::to_static));
+ }
+ }
+
+ fn draw(
+ &self,
+ renderer: &mut Renderer,
+ color: Color,
+ background: Color,
+ viewport: &Rectangle,
+ ) {
+ use text::Paragraph as _;
+
+ if self.content.min_width() < 1.0 {
+ return;
+ }
+
+ let mut bounds = Rectangle::new(
+ self.position - Vector::new(0.0, self.content.min_height()),
+ self.content.min_bounds(),
+ );
+
+ bounds.x = bounds
+ .x
+ .max(viewport.x)
+ .min(viewport.x + viewport.width - bounds.width);
+
+ bounds.y = bounds
+ .y
+ .max(viewport.y)
+ .min(viewport.y + viewport.height - bounds.height);
+
+ renderer.with_layer(bounds, |renderer| {
+ renderer.fill_quad(
+ renderer::Quad {
+ bounds,
+ ..Default::default()
+ },
+ background,
+ );
+
+ renderer.fill_paragraph(
+ &self.content,
+ bounds.position(),
+ color,
+ bounds,
+ );
+
+ const UNDERLINE: f32 = 2.0;
+
+ renderer.fill_quad(
+ renderer::Quad {
+ bounds: bounds.shrink(Padding {
+ top: bounds.height - UNDERLINE,
+ ..Default::default()
+ }),
+ ..Default::default()
+ },
+ color,
+ );
+
+ for span_bounds in self.content.span_bounds(1) {
+ renderer.fill_quad(
+ renderer::Quad {
+ bounds: span_bounds
+ + (bounds.position() - Point::ORIGIN),
+ ..Default::default()
+ },
+ color,
+ );
+ }
+ });
+ }
}
diff --git a/winit/src/system.rs b/winit/src/system.rs
index 361135be..0b476773 100644
--- a/winit/src/system.rs
+++ b/winit/src/system.rs
@@ -17,7 +17,11 @@ pub(crate) fn information(
let mut system = System::new_all();
system.refresh_all();
- let cpu = system.global_cpu_info();
+ let cpu_brand = system
+ .cpus()
+ .first()
+ .map(|cpu| cpu.brand().to_string())
+ .unwrap_or_default();
let memory_used = sysinfo::get_current_pid()
.and_then(|pid| system.process(pid).ok_or("Process not found"))
@@ -29,7 +33,7 @@ pub(crate) fn information(
system_kernel: System::kernel_version(),
system_version: System::long_os_version(),
system_short_version: System::os_version(),
- cpu_brand: cpu.brand().into(),
+ cpu_brand,
cpu_cores: system.physical_core_count(),
memory_total: system.total_memory(),
memory_used,