summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/build.yml12
-rw-r--r--.github/workflows/document.yml1
-rw-r--r--Cargo.toml24
-rw-r--r--core/src/alignment.rs20
-rw-r--r--core/src/border.rs228
-rw-r--r--core/src/border_radius.rs22
-rw-r--r--core/src/element.rs4
-rw-r--r--core/src/image.rs85
-rw-r--r--core/src/layout/node.rs13
-rw-r--r--core/src/lib.rs4
-rw-r--r--core/src/mouse/click.rs13
-rw-r--r--core/src/overlay.rs4
-rw-r--r--core/src/overlay/element.rs4
-rw-r--r--core/src/overlay/group.rs2
-rw-r--r--core/src/padding.rs96
-rw-r--r--core/src/pixels.rs47
-rw-r--r--core/src/rectangle.rs82
-rw-r--r--core/src/renderer.rs2
-rw-r--r--core/src/renderer/null.rs47
-rw-r--r--core/src/size.rs14
-rw-r--r--core/src/svg.rs69
-rw-r--r--core/src/text.rs318
-rw-r--r--core/src/text/editor.rs8
-rw-r--r--core/src/text/paragraph.rs105
-rw-r--r--core/src/vector.rs12
-rw-r--r--core/src/widget.rs10
-rw-r--r--core/src/widget/operation.rs231
-rw-r--r--core/src/widget/operation/focusable.rs36
-rw-r--r--core/src/widget/operation/scrollable.rs45
-rw-r--r--core/src/widget/text.rs137
-rw-r--r--core/src/widget/tree.rs6
-rw-r--r--core/src/window/event.rs14
-rw-r--r--core/src/window/settings/linux.rs6
-rw-r--r--core/src/window/settings/windows.rs7
-rw-r--r--examples/arc/src/main.rs7
-rw-r--r--examples/bezier_tool/src/main.rs12
-rw-r--r--examples/clock/src/main.rs14
-rw-r--r--examples/color_palette/src/main.rs13
-rw-r--r--examples/combo_box/src/main.rs6
-rw-r--r--examples/component/src/main.rs156
-rw-r--r--examples/counter/src/main.rs4
-rw-r--r--examples/custom_quad/src/main.rs43
-rw-r--r--examples/custom_shader/src/main.rs9
-rw-r--r--examples/custom_widget/src/main.rs18
-rw-r--r--examples/download_progress/Cargo.toml4
-rw-r--r--examples/download_progress/index.html12
-rw-r--r--examples/download_progress/src/download.rs105
-rw-r--r--examples/download_progress/src/main.rs30
-rw-r--r--examples/editor/src/main.rs113
-rw-r--r--examples/events/src/main.rs17
-rw-r--r--examples/exit/src/main.rs4
-rw-r--r--examples/ferris/src/main.rs18
-rw-r--r--examples/game_of_life/src/main.rs20
-rw-r--r--examples/gradient/src/main.rs10
-rw-r--r--examples/integration/src/controls.rs6
-rw-r--r--examples/integration/src/main.rs2
-rw-r--r--examples/layout/src/main.rs28
-rw-r--r--examples/lazy/src/main.rs4
-rw-r--r--examples/loading_spinners/src/main.rs6
-rw-r--r--examples/loupe/src/main.rs4
-rw-r--r--examples/markdown/Cargo.toml (renamed from examples/component/Cargo.toml)6
-rw-r--r--examples/markdown/overview.md93
-rw-r--r--examples/markdown/src/main.rs82
-rw-r--r--examples/modal/src/main.rs39
-rw-r--r--examples/multi_window/src/main.rs29
-rw-r--r--examples/multitouch/src/main.rs7
-rw-r--r--examples/pane_grid/src/main.rs50
-rw-r--r--examples/pick_list/src/main.rs6
-rw-r--r--examples/pokedex/Cargo.toml2
-rw-r--r--examples/pokedex/src/main.rs33
-rw-r--r--examples/qr_code/src/main.rs6
-rw-r--r--examples/screenshot/src/main.rs103
-rw-r--r--examples/scrollable/src/main.rs105
-rw-r--r--examples/sierpinski_triangle/src/main.rs8
-rw-r--r--examples/slider/Cargo.toml1
-rw-r--r--examples/slider/src/main.rs43
-rw-r--r--examples/solar_system/Cargo.toml2
-rw-r--r--examples/solar_system/assets/earth.pngbin0 -> 91888 bytes
-rw-r--r--examples/solar_system/assets/moon.pngbin0 -> 105100 bytes
-rw-r--r--examples/solar_system/assets/sun.pngbin0 -> 114689 bytes
-rw-r--r--examples/solar_system/src/main.rs70
-rw-r--r--examples/stopwatch/src/main.rs16
-rw-r--r--examples/styling/src/main.rs23
-rw-r--r--examples/svg/src/main.rs26
-rw-r--r--examples/the_matrix/src/main.rs8
-rw-r--r--examples/toast/src/main.rs24
-rw-r--r--examples/todos/src/main.rs54
-rw-r--r--examples/tour/src/main.rs52
-rw-r--r--examples/vectorial_text/src/main.rs8
-rw-r--r--examples/visible_bounds/src/main.rs10
-rw-r--r--examples/websocket/src/echo.rs102
-rw-r--r--examples/websocket/src/main.rs48
-rw-r--r--futures/src/backend/native/async_std.rs2
-rw-r--r--futures/src/backend/native/smol.rs2
-rw-r--r--futures/src/backend/native/tokio.rs2
-rw-r--r--futures/src/backend/wasm/wasm_bindgen.rs2
-rw-r--r--futures/src/keyboard.rs4
-rw-r--r--futures/src/lib.rs1
-rw-r--r--futures/src/stream.rs46
-rw-r--r--futures/src/subscription.rs368
-rw-r--r--futures/src/subscription/tracker.rs6
-rw-r--r--graphics/Cargo.toml1
-rw-r--r--graphics/src/geometry.rs1
-rw-r--r--graphics/src/geometry/fill.rs2
-rw-r--r--graphics/src/geometry/frame.rs55
-rw-r--r--graphics/src/geometry/stroke.rs2
-rw-r--r--graphics/src/geometry/style.rs2
-rw-r--r--graphics/src/geometry/text.rs5
-rw-r--r--graphics/src/gradient.rs2
-rw-r--r--graphics/src/image.rs45
-rw-r--r--graphics/src/text.rs25
-rw-r--r--graphics/src/text/cache.rs4
-rw-r--r--graphics/src/text/editor.rs304
-rw-r--r--graphics/src/text/paragraph.rs240
-rw-r--r--highlighter/src/lib.rs36
-rw-r--r--renderer/src/fallback.rs69
-rw-r--r--runtime/src/clipboard.rs10
-rw-r--r--runtime/src/font.rs5
-rw-r--r--runtime/src/lib.rs9
-rw-r--r--runtime/src/multi_window/state.rs2
-rw-r--r--runtime/src/overlay/nested.rs4
-rw-r--r--runtime/src/program/state.rs2
-rw-r--r--runtime/src/task.rs251
-rw-r--r--runtime/src/user_interface.rs2
-rw-r--r--runtime/src/window.rs112
-rw-r--r--src/advanced.rs23
-rw-r--r--src/application.rs31
-rw-r--r--src/daemon.rs21
-rw-r--r--src/lib.rs561
-rw-r--r--src/program.rs111
-rw-r--r--tiny_skia/Cargo.toml2
-rw-r--r--tiny_skia/src/engine.rs42
-rw-r--r--tiny_skia/src/geometry.rs74
-rw-r--r--tiny_skia/src/layer.rs47
-rw-r--r--tiny_skia/src/lib.rs65
-rw-r--r--tiny_skia/src/text.rs8
-rw-r--r--tiny_skia/src/vector.rs34
-rw-r--r--wgpu/Cargo.toml2
-rw-r--r--wgpu/src/geometry.rs110
-rw-r--r--wgpu/src/image/mod.rs58
-rw-r--r--wgpu/src/image/vector.rs35
-rw-r--r--wgpu/src/layer.rs43
-rw-r--r--wgpu/src/lib.rs102
-rw-r--r--wgpu/src/shader/image.wgsl12
-rw-r--r--wgpu/src/shader/quad/solid.wgsl13
-rw-r--r--wgpu/src/text.rs8
-rw-r--r--widget/Cargo.toml14
-rw-r--r--widget/assets/iced-logo.svg2
-rw-r--r--widget/src/button.rs48
-rw-r--r--widget/src/canvas.rs4
-rw-r--r--widget/src/checkbox.rs16
-rw-r--r--widget/src/column.rs17
-rw-r--r--widget/src/combo_box.rs47
-rw-r--r--widget/src/container.rs162
-rw-r--r--widget/src/helpers.rs126
-rw-r--r--widget/src/image.rs13
-rw-r--r--widget/src/image/viewer.rs15
-rw-r--r--widget/src/keyed/column.rs2
-rw-r--r--widget/src/lazy.rs4
-rw-r--r--widget/src/lazy/component.rs13
-rw-r--r--widget/src/lazy/helpers.rs12
-rw-r--r--widget/src/lazy/responsive.rs3
-rw-r--r--widget/src/lib.rs6
-rw-r--r--widget/src/markdown.rs587
-rw-r--r--widget/src/mouse_area.rs51
-rw-r--r--widget/src/overlay/menu.rs35
-rw-r--r--widget/src/pane_grid.rs4
-rw-r--r--widget/src/pane_grid/content.rs2
-rw-r--r--widget/src/pane_grid/controls.rs59
-rw-r--r--widget/src/pane_grid/title_bar.rs319
-rw-r--r--widget/src/pick_list.rs14
-rw-r--r--widget/src/progress_bar.rs10
-rw-r--r--widget/src/radio.rs26
-rw-r--r--widget/src/row.rs217
-rw-r--r--widget/src/rule.rs4
-rw-r--r--widget/src/scrollable.rs728
-rw-r--r--widget/src/slider.rs42
-rw-r--r--widget/src/stack.rs93
-rw-r--r--widget/src/svg.rs10
-rw-r--r--widget/src/text.rs4
-rw-r--r--widget/src/text/rich.rs538
-rw-r--r--widget/src/text_editor.rs599
-rw-r--r--widget/src/text_input.rs225
-rw-r--r--widget/src/themer.rs4
-rw-r--r--widget/src/toggler.rs88
-rw-r--r--widget/src/vertical_slider.rs21
-rw-r--r--winit/Cargo.toml3
-rw-r--r--winit/src/clipboard.rs41
-rw-r--r--winit/src/conversion.rs22
-rw-r--r--winit/src/program.rs199
-rw-r--r--winit/src/program/window_manager.rs8
-rw-r--r--winit/src/system.rs2
192 files changed, 7663 insertions, 3270 deletions
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index ba1ab003..e7af3b03 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -19,7 +19,7 @@ jobs:
- name: Build todos binary
run: cargo build --verbose --profile release-opt --package todos
- name: Archive todos binary
- uses: actions/upload-artifact@v1
+ uses: actions/upload-artifact@v4
with:
name: todos-x86_64-unknown-linux-gnu
path: target/release-opt/todos
@@ -28,7 +28,7 @@ jobs:
- name: Rename todos .deb package
run: mv target/debian/*.deb target/debian/iced_todos-x86_64-debian-linux-gnu.deb
- name: Archive todos .deb package
- uses: actions/upload-artifact@v1
+ uses: actions/upload-artifact@v4
with:
name: todos-x86_64-debian-linux-gnu
path: target/debian/iced_todos-x86_64-debian-linux-gnu.deb
@@ -48,7 +48,7 @@ jobs:
- name: Build todos binary
run: cargo build --verbose --profile release-opt --package todos
- name: Archive todos binary
- uses: actions/upload-artifact@v1
+ uses: actions/upload-artifact@v4
with:
name: todos-x86_64-pc-windows-msvc
path: target/release-opt/todos.exe
@@ -65,7 +65,7 @@ jobs:
- name: Open binary via double-click
run: chmod +x target/release-opt/todos
- name: Archive todos binary
- uses: actions/upload-artifact@v1
+ uses: actions/upload-artifact@v4
with:
name: todos-x86_64-apple-darwin
path: target/release-opt/todos
@@ -80,14 +80,14 @@ jobs:
- name: Build todos binary for Raspberry Pi 3/4 (64 bits)
run: cross build --verbose --profile release-opt --package todos --target aarch64-unknown-linux-gnu
- name: Archive todos binary
- uses: actions/upload-artifact@v1
+ uses: actions/upload-artifact@v4
with:
name: todos-aarch64-unknown-linux-gnu
path: target/aarch64-unknown-linux-gnu/release-opt/todos
- name: Build todos binary for Raspberry Pi 2/3/4 (32 bits)
run: cross build --verbose --profile release-opt --package todos --target armv7-unknown-linux-gnueabihf
- name: Archive todos binary
- uses: actions/upload-artifact@v1
+ uses: actions/upload-artifact@v4
with:
name: todos-armv7-unknown-linux-gnueabihf
path: target/armv7-unknown-linux-gnueabihf/release-opt/todos
diff --git a/.github/workflows/document.yml b/.github/workflows/document.yml
index a213e590..57dc1375 100644
--- a/.github/workflows/document.yml
+++ b/.github/workflows/document.yml
@@ -14,6 +14,7 @@ jobs:
run: |
RUSTDOCFLAGS="--cfg docsrs" \
cargo doc --no-deps --all-features \
+ -p futures-core \
-p iced_core \
-p iced_highlighter \
-p iced_futures \
diff --git a/Cargo.toml b/Cargo.toml
index b85900cf..52464e38 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -27,13 +27,17 @@ wgpu = ["iced_renderer/wgpu", "iced_widget/wgpu"]
# Enable the `tiny-skia` software renderer backend
tiny-skia = ["iced_renderer/tiny-skia"]
# Enables the `Image` widget
-image = ["iced_widget/image", "dep:image"]
+image = ["image-without-codecs", "image/default"]
+# Enables the `Image` widget, without any built-in codecs of the `image` crate
+image-without-codecs = ["iced_widget/image", "dep:image"]
# Enables the `Svg` widget
svg = ["iced_widget/svg"]
# Enables the `Canvas` widget
canvas = ["iced_widget/canvas"]
# Enables the `QRCode` widget
qr_code = ["iced_widget/qr_code"]
+# Enables the `markdown` widget
+markdown = ["iced_widget/markdown"]
# Enables lazy widgets
lazy = ["iced_widget/lazy"]
# Enables a debug view in native platforms (press F12)
@@ -51,7 +55,7 @@ web-colors = ["iced_renderer/web-colors"]
# Enables the WebGL backend, replacing WebGPU
webgl = ["iced_renderer/webgl"]
# Enables the syntax `highlighter` module
-highlighter = ["iced_highlighter"]
+highlighter = ["iced_highlighter", "iced_widget/highlighter"]
# Enables experimental multi-window support.
multi-window = ["iced_winit/multi-window"]
# Enables the advanced module
@@ -138,14 +142,14 @@ async-std = "1.0"
bitflags = "2.0"
bytemuck = { version = "1.0", features = ["derive"] }
bytes = "1.6"
-cosmic-text = "0.10"
+cosmic-text = "0.12"
dark-light = "1.0"
futures = "0.3"
glam = "0.25"
-glyphon = { git = "https://github.com/hecrj/glyphon.git", rev = "f07e7bab705e69d39a5e6e52c73039a93c4552f8" }
+glyphon = { git = "https://github.com/hecrj/glyphon.git", rev = "feef9f5630c2adb3528937e55f7bfad2da561a65" }
guillotiere = "0.6"
half = "2.2"
-image = "0.24"
+image = { version = "0.24", default-features = false }
kamadak-exif = "0.5"
kurbo = "0.10"
log = "0.4"
@@ -155,10 +159,11 @@ num-traits = "0.2"
once_cell = "1.0"
ouroboros = "0.18"
palette = "0.7"
+pulldown-cmark = "0.11"
qrcode = { version = "0.13", default-features = false }
raw-window-handle = "0.6"
-resvg = "0.36"
-rustc-hash = "1.0"
+resvg = "0.42"
+rustc-hash = "2.0"
smol = "1.0"
smol_str = "0.2"
softbuffer = "0.4"
@@ -169,9 +174,10 @@ tiny-skia = "0.11"
tokio = "1.0"
tracing = "0.1"
unicode-segmentation = "1.0"
+url = "2.5"
wasm-bindgen-futures = "0.4"
wasm-timer = "0.2"
-web-sys = "=0.3.67"
+web-sys = "0.3.69"
web-time = "1.1"
wgpu = "0.19"
winapi = "0.3"
@@ -179,7 +185,7 @@ window_clipboard = "0.4.1"
winit = { git = "https://github.com/iced-rs/winit.git", rev = "254d6b3420ce4e674f516f7a2bd440665e05484d" }
[workspace.lints.rust]
-rust_2018_idioms = "forbid"
+rust_2018_idioms = { level = "forbid", priority = -1 }
missing_debug_implementations = "deny"
missing_docs = "deny"
unsafe_code = "deny"
diff --git a/core/src/alignment.rs b/core/src/alignment.rs
index 51b7fca9..8f01ef71 100644
--- a/core/src/alignment.rs
+++ b/core/src/alignment.rs
@@ -46,6 +46,16 @@ pub enum Horizontal {
Right,
}
+impl From<Alignment> for Horizontal {
+ fn from(alignment: Alignment) -> Self {
+ match alignment {
+ Alignment::Start => Self::Left,
+ Alignment::Center => Self::Center,
+ Alignment::End => Self::Right,
+ }
+ }
+}
+
/// The vertical [`Alignment`] of some resource.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Vertical {
@@ -58,3 +68,13 @@ pub enum Vertical {
/// Align bottom
Bottom,
}
+
+impl From<Alignment> for Vertical {
+ fn from(alignment: Alignment) -> Self {
+ match alignment {
+ Alignment::Start => Self::Top,
+ Alignment::Center => Self::Center,
+ Alignment::End => Self::Bottom,
+ }
+ }
+}
diff --git a/core/src/border.rs b/core/src/border.rs
index 2df24988..da0aaa28 100644
--- a/core/src/border.rs
+++ b/core/src/border.rs
@@ -10,40 +10,64 @@ pub struct Border {
/// The width of the border.
pub width: f32,
- /// The radius of the border.
+ /// The [`Radius`] of the border.
pub radius: Radius,
}
+/// Creates a new [`Border`] with the given [`Radius`].
+///
+/// ```
+/// # use iced_core::border::{self, Border};
+/// #
+/// assert_eq!(border::rounded(10), Border::default().rounded(10));
+/// ```
+pub fn rounded(radius: impl Into<Radius>) -> Border {
+ Border::default().rounded(radius)
+}
+
+/// Creates a new [`Border`] with the given [`Color`].
+///
+/// ```
+/// # use iced_core::border::{self, Border};
+/// # use iced_core::Color;
+/// #
+/// assert_eq!(border::color(Color::BLACK), Border::default().color(Color::BLACK));
+/// ```
+pub fn color(color: impl Into<Color>) -> Border {
+ Border::default().color(color)
+}
+
+/// Creates a new [`Border`] with the given `width`.
+///
+/// ```
+/// # use iced_core::border::{self, Border};
+/// # use iced_core::Color;
+/// #
+/// assert_eq!(border::width(10), Border::default().width(10));
+/// ```
+pub fn width(width: impl Into<Pixels>) -> Border {
+ Border::default().width(width)
+}
+
impl Border {
- /// Creates a new default rounded [`Border`] with the given [`Radius`].
- ///
- /// ```
- /// # use iced_core::Border;
- /// #
- /// assert_eq!(Border::rounded(10), Border::default().with_radius(10));
- /// ```
- pub fn rounded(radius: impl Into<Radius>) -> Self {
- Self::default().with_radius(radius)
- }
-
- /// Updates the [`Color`] of the [`Border`].
- pub fn with_color(self, color: impl Into<Color>) -> Self {
+ /// Sets the [`Color`] of the [`Border`].
+ pub fn color(self, color: impl Into<Color>) -> Self {
Self {
color: color.into(),
..self
}
}
- /// Updates the [`Radius`] of the [`Border`].
- pub fn with_radius(self, radius: impl Into<Radius>) -> Self {
+ /// Sets the [`Radius`] of the [`Border`].
+ pub fn rounded(self, radius: impl Into<Radius>) -> Self {
Self {
radius: radius.into(),
..self
}
}
- /// Updates the width of the [`Border`].
- pub fn with_width(self, width: impl Into<Pixels>) -> Self {
+ /// Sets the width of the [`Border`].
+ pub fn width(self, width: impl Into<Pixels>) -> Self {
Self {
width: width.into().0,
..self
@@ -54,11 +78,160 @@ impl Border {
/// The border radii for the corners of a graphics primitive in the order:
/// top-left, top-right, bottom-right, bottom-left.
#[derive(Debug, Clone, Copy, PartialEq, Default)]
-pub struct Radius([f32; 4]);
+pub struct Radius {
+ /// Top left radius
+ pub top_left: f32,
+ /// Top right radius
+ pub top_right: f32,
+ /// Bottom right radius
+ pub bottom_right: f32,
+ /// Bottom left radius
+ pub bottom_left: f32,
+}
+
+/// Creates a new [`Radius`] with the same value for each corner.
+pub fn radius(value: impl Into<Pixels>) -> Radius {
+ Radius::new(value)
+}
+
+/// Creates a new [`Radius`] with the given top left value.
+pub fn top_left(value: impl Into<Pixels>) -> Radius {
+ Radius::default().top_left(value)
+}
+
+/// Creates a new [`Radius`] with the given top right value.
+pub fn top_right(value: impl Into<Pixels>) -> Radius {
+ Radius::default().top_right(value)
+}
+
+/// Creates a new [`Radius`] with the given bottom right value.
+pub fn bottom_right(value: impl Into<Pixels>) -> Radius {
+ Radius::default().bottom_right(value)
+}
+
+/// Creates a new [`Radius`] with the given bottom left value.
+pub fn bottom_left(value: impl Into<Pixels>) -> Radius {
+ Radius::default().bottom_left(value)
+}
+
+/// Creates a new [`Radius`] with the given value as top left and top right.
+pub fn top(value: impl Into<Pixels>) -> Radius {
+ Radius::default().top(value)
+}
+
+/// Creates a new [`Radius`] with the given value as bottom left and bottom right.
+pub fn bottom(value: impl Into<Pixels>) -> Radius {
+ Radius::default().bottom(value)
+}
+
+/// Creates a new [`Radius`] with the given value as top left and bottom left.
+pub fn left(value: impl Into<Pixels>) -> Radius {
+ Radius::default().left(value)
+}
+
+/// Creates a new [`Radius`] with the given value as top right and bottom right.
+pub fn right(value: impl Into<Pixels>) -> Radius {
+ Radius::default().right(value)
+}
+
+impl Radius {
+ /// Creates a new [`Radius`] with the same value for each corner.
+ pub fn new(value: impl Into<Pixels>) -> Self {
+ let value = value.into().0;
+
+ Self {
+ top_left: value,
+ top_right: value,
+ bottom_right: value,
+ bottom_left: value,
+ }
+ }
+
+ /// Sets the top left value of the [`Radius`].
+ pub fn top_left(self, value: impl Into<Pixels>) -> Self {
+ Self {
+ top_left: value.into().0,
+ ..self
+ }
+ }
+
+ /// Sets the top right value of the [`Radius`].
+ pub fn top_right(self, value: impl Into<Pixels>) -> Self {
+ Self {
+ top_right: value.into().0,
+ ..self
+ }
+ }
+
+ /// Sets the bottom right value of the [`Radius`].
+ pub fn bottom_right(self, value: impl Into<Pixels>) -> Self {
+ Self {
+ bottom_right: value.into().0,
+ ..self
+ }
+ }
+
+ /// Sets the bottom left value of the [`Radius`].
+ pub fn bottom_left(self, value: impl Into<Pixels>) -> Self {
+ Self {
+ bottom_left: value.into().0,
+ ..self
+ }
+ }
+
+ /// Sets the top left and top right values of the [`Radius`].
+ pub fn top(self, value: impl Into<Pixels>) -> Self {
+ let value = value.into().0;
+
+ Self {
+ top_left: value,
+ top_right: value,
+ ..self
+ }
+ }
+
+ /// Sets the bottom left and bottom right values of the [`Radius`].
+ pub fn bottom(self, value: impl Into<Pixels>) -> Self {
+ let value = value.into().0;
+
+ Self {
+ bottom_left: value,
+ bottom_right: value,
+ ..self
+ }
+ }
+
+ /// Sets the top left and bottom left values of the [`Radius`].
+ pub fn left(self, value: impl Into<Pixels>) -> Self {
+ let value = value.into().0;
+
+ Self {
+ top_left: value,
+ bottom_left: value,
+ ..self
+ }
+ }
+
+ /// Sets the top right and bottom right values of the [`Radius`].
+ pub fn right(self, value: impl Into<Pixels>) -> Self {
+ let value = value.into().0;
+
+ Self {
+ top_right: value,
+ bottom_right: value,
+ ..self
+ }
+ }
+}
impl From<f32> for Radius {
- fn from(w: f32) -> Self {
- Self([w; 4])
+ fn from(radius: f32) -> Self {
+ Self {
+ top_left: radius,
+ top_right: radius,
+ bottom_right: radius,
+ bottom_left: radius,
+ }
}
}
@@ -80,14 +253,13 @@ impl From<i32> for Radius {
}
}
-impl From<[f32; 4]> for Radius {
- fn from(radi: [f32; 4]) -> Self {
- Self(radi)
- }
-}
-
impl From<Radius> for [f32; 4] {
fn from(radi: Radius) -> Self {
- radi.0
+ [
+ radi.top_left,
+ radi.top_right,
+ radi.bottom_right,
+ radi.bottom_left,
+ ]
}
}
diff --git a/core/src/border_radius.rs b/core/src/border_radius.rs
deleted file mode 100644
index a444dd74..00000000
--- a/core/src/border_radius.rs
+++ /dev/null
@@ -1,22 +0,0 @@
-/// The border radii for the corners of a graphics primitive in the order:
-/// top-left, top-right, bottom-right, bottom-left.
-#[derive(Debug, Clone, Copy, PartialEq, Default)]
-pub struct BorderRadius([f32; 4]);
-
-impl From<f32> for BorderRadius {
- fn from(w: f32) -> Self {
- Self([w; 4])
- }
-}
-
-impl From<[f32; 4]> for BorderRadius {
- fn from(radi: [f32; 4]) -> Self {
- Self(radi)
- }
-}
-
-impl From<BorderRadius> for [f32; 4] {
- fn from(radi: BorderRadius) -> Self {
- radi.0
- }
-}
diff --git a/core/src/element.rs b/core/src/element.rs
index 385d8295..6ebb8a15 100644
--- a/core/src/element.rs
+++ b/core/src/element.rs
@@ -304,7 +304,7 @@ where
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
- operation: &mut dyn widget::Operation<()>,
+ operation: &mut dyn widget::Operation,
) {
self.widget.operate(tree, layout, renderer, operation);
}
@@ -440,7 +440,7 @@ where
state: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
- operation: &mut dyn widget::Operation<()>,
+ operation: &mut dyn widget::Operation,
) {
self.element
.widget
diff --git a/core/src/image.rs b/core/src/image.rs
index 82ecdd0f..f985636a 100644
--- a/core/src/image.rs
+++ b/core/src/image.rs
@@ -7,6 +7,73 @@ use rustc_hash::FxHasher;
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
+/// A raster image that can be drawn.
+#[derive(Debug, Clone, PartialEq)]
+pub struct Image<H = Handle> {
+ /// The handle of the image.
+ pub handle: H,
+
+ /// The filter method of the image.
+ pub filter_method: FilterMethod,
+
+ /// The rotation to be applied to the image; on its center.
+ pub rotation: Radians,
+
+ /// The opacity of the image.
+ ///
+ /// 0 means transparent. 1 means opaque.
+ pub opacity: f32,
+
+ /// If set to `true`, the image will be snapped to the pixel grid.
+ ///
+ /// This can avoid graphical glitches, specially when using
+ /// [`FilterMethod::Nearest`].
+ pub snap: bool,
+}
+
+impl Image<Handle> {
+ /// Creates a new [`Image`] with the given handle.
+ pub fn new(handle: impl Into<Handle>) -> Self {
+ Self {
+ handle: handle.into(),
+ filter_method: FilterMethod::default(),
+ rotation: Radians(0.0),
+ opacity: 1.0,
+ snap: false,
+ }
+ }
+
+ /// Sets the filter method of the [`Image`].
+ pub fn filter_method(mut self, filter_method: FilterMethod) -> Self {
+ self.filter_method = filter_method;
+ self
+ }
+
+ /// Sets the rotation of the [`Image`].
+ pub fn rotation(mut self, rotation: impl Into<Radians>) -> Self {
+ self.rotation = rotation.into();
+ self
+ }
+
+ /// Sets the opacity of the [`Image`].
+ pub fn opacity(mut self, opacity: impl Into<f32>) -> Self {
+ self.opacity = opacity.into();
+ self
+ }
+
+ /// Sets whether the [`Image`] should be snapped to the pixel grid.
+ pub fn snap(mut self, snap: bool) -> Self {
+ self.snap = snap;
+ self
+ }
+}
+
+impl From<&Handle> for Image {
+ fn from(handle: &Handle) -> Self {
+ Image::new(handle.clone())
+ }
+}
+
/// A handle of some image data.
#[derive(Clone, PartialEq, Eq)]
pub enum Handle {
@@ -101,6 +168,12 @@ where
}
}
+impl From<&Handle> for Handle {
+ fn from(value: &Handle) -> Self {
+ value.clone()
+ }
+}
+
impl std::fmt::Debug for Handle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
@@ -166,14 +239,6 @@ pub trait Renderer: crate::Renderer {
/// Returns the dimensions of an image for the given [`Handle`].
fn measure_image(&self, handle: &Self::Handle) -> Size<u32>;
- /// Draws an image with the given [`Handle`] and inside the provided
- /// `bounds`.
- fn draw_image(
- &mut self,
- handle: Self::Handle,
- filter_method: FilterMethod,
- bounds: Rectangle,
- rotation: Radians,
- opacity: f32,
- );
+ /// Draws an [`Image`] inside the provided `bounds`.
+ fn draw_image(&mut self, image: Image<Self::Handle>, bounds: Rectangle);
}
diff --git a/core/src/layout/node.rs b/core/src/layout/node.rs
index 5743a9bd..0c0f90fb 100644
--- a/core/src/layout/node.rs
+++ b/core/src/layout/node.rs
@@ -103,12 +103,13 @@ impl Node {
}
/// Translates the [`Node`] by the given translation.
- pub fn translate(self, translation: impl Into<Vector>) -> Self {
- let translation = translation.into();
+ pub fn translate(mut self, translation: impl Into<Vector>) -> Self {
+ self.translate_mut(translation);
+ self
+ }
- Self {
- bounds: self.bounds + translation,
- ..self
- }
+ /// Translates the [`Node`] by the given translation.
+ pub fn translate_mut(&mut self, translation: impl Into<Vector>) {
+ self.bounds = self.bounds + translation.into();
}
}
diff --git a/core/src/lib.rs b/core/src/lib.rs
index 32156441..df599f45 100644
--- a/core/src/lib.rs
+++ b/core/src/lib.rs
@@ -20,6 +20,7 @@ pub mod keyboard;
pub mod layout;
pub mod mouse;
pub mod overlay;
+pub mod padding;
pub mod renderer;
pub mod svg;
pub mod text;
@@ -35,7 +36,6 @@ mod color;
mod content_fit;
mod element;
mod length;
-mod padding;
mod pixels;
mod point;
mod rectangle;
@@ -57,6 +57,7 @@ pub use element::Element;
pub use event::Event;
pub use font::Font;
pub use gradient::Gradient;
+pub use image::Image;
pub use layout::Layout;
pub use length::Length;
pub use overlay::Overlay;
@@ -69,6 +70,7 @@ pub use rotation::Rotation;
pub use shadow::Shadow;
pub use shell::Shell;
pub use size::Size;
+pub use svg::Svg;
pub use text::Text;
pub use theme::Theme;
pub use transformation::Transformation;
diff --git a/core/src/mouse/click.rs b/core/src/mouse/click.rs
index 6f3844be..07a4db5a 100644
--- a/core/src/mouse/click.rs
+++ b/core/src/mouse/click.rs
@@ -1,4 +1,5 @@
//! Track mouse clicks.
+use crate::mouse::Button;
use crate::time::Instant;
use crate::Point;
@@ -6,6 +7,7 @@ use crate::Point;
#[derive(Debug, Clone, Copy)]
pub struct Click {
kind: Kind,
+ button: Button,
position: Point,
time: Instant,
}
@@ -36,11 +38,17 @@ impl Kind {
impl Click {
/// Creates a new [`Click`] with the given position and previous last
/// [`Click`].
- pub fn new(position: Point, previous: Option<Click>) -> Click {
+ pub fn new(
+ position: Point,
+ button: Button,
+ previous: Option<Click>,
+ ) -> Click {
let time = Instant::now();
let kind = if let Some(previous) = previous {
- if previous.is_consecutive(position, time) {
+ if previous.is_consecutive(position, time)
+ && button == previous.button
+ {
previous.kind.next()
} else {
Kind::Single
@@ -51,6 +59,7 @@ impl Click {
Click {
kind,
+ button,
position,
time,
}
diff --git a/core/src/overlay.rs b/core/src/overlay.rs
index 16f867da..f09de831 100644
--- a/core/src/overlay.rs
+++ b/core/src/overlay.rs
@@ -41,7 +41,7 @@ where
&mut self,
_layout: Layout<'_>,
_renderer: &Renderer,
- _operation: &mut dyn widget::Operation<()>,
+ _operation: &mut dyn widget::Operation,
) {
}
@@ -52,7 +52,7 @@ where
/// * the computed [`Layout`] of the [`Overlay`]
/// * the current cursor position
/// * a mutable `Message` list, allowing the [`Overlay`] to produce
- /// new messages based on user interaction.
+ /// new messages based on user interaction.
/// * the `Renderer`
/// * a [`Clipboard`], if available
///
diff --git a/core/src/overlay/element.rs b/core/src/overlay/element.rs
index 61e75e8a..32e987a3 100644
--- a/core/src/overlay/element.rs
+++ b/core/src/overlay/element.rs
@@ -92,7 +92,7 @@ where
&mut self,
layout: Layout<'_>,
renderer: &Renderer,
- operation: &mut dyn widget::Operation<()>,
+ operation: &mut dyn widget::Operation,
) {
self.overlay.operate(layout, renderer, operation);
}
@@ -144,7 +144,7 @@ where
&mut self,
layout: Layout<'_>,
renderer: &Renderer,
- operation: &mut dyn widget::Operation<()>,
+ operation: &mut dyn widget::Operation,
) {
self.content.operate(layout, renderer, operation);
}
diff --git a/core/src/overlay/group.rs b/core/src/overlay/group.rs
index cd12eac9..6541d311 100644
--- a/core/src/overlay/group.rs
+++ b/core/src/overlay/group.rs
@@ -132,7 +132,7 @@ where
&mut self,
layout: Layout<'_>,
renderer: &Renderer,
- operation: &mut dyn widget::Operation<()>,
+ operation: &mut dyn widget::Operation,
) {
operation.container(None, layout.bounds(), &mut |operation| {
self.children.iter_mut().zip(layout.children()).for_each(
diff --git a/core/src/padding.rs b/core/src/padding.rs
index a63f6e29..e26cdd9b 100644
--- a/core/src/padding.rs
+++ b/core/src/padding.rs
@@ -1,4 +1,5 @@
-use crate::Size;
+//! Space stuff around the perimeter.
+use crate::{Pixels, Size};
/// An amount of space to pad for each side of a box
///
@@ -9,7 +10,6 @@ use crate::Size;
/// #
/// let padding = Padding::from(20); // 20px on all sides
/// let padding = Padding::from([10, 20]); // top/bottom, left/right
-/// let padding = Padding::from([5, 10, 15, 20]); // top, right, bottom, left
/// ```
///
/// Normally, the `padding` method of a widget will ask for an `Into<Padding>`,
@@ -31,9 +31,8 @@ use crate::Size;
///
/// let widget = Widget::new().padding(20); // 20px on all sides
/// let widget = Widget::new().padding([10, 20]); // top/bottom, left/right
-/// let widget = Widget::new().padding([5, 10, 15, 20]); // top, right, bottom, left
/// ```
-#[derive(Debug, Copy, Clone)]
+#[derive(Debug, Copy, Clone, PartialEq, Default)]
pub struct Padding {
/// Top padding
pub top: f32,
@@ -45,6 +44,31 @@ pub struct Padding {
pub left: f32,
}
+/// Create a [`Padding`] that is equal on all sides.
+pub fn all(padding: impl Into<Pixels>) -> Padding {
+ Padding::new(padding.into().0)
+}
+
+/// Create some top [`Padding`].
+pub fn top(padding: impl Into<Pixels>) -> Padding {
+ Padding::default().top(padding)
+}
+
+/// Create some bottom [`Padding`].
+pub fn bottom(padding: impl Into<Pixels>) -> Padding {
+ Padding::default().bottom(padding)
+}
+
+/// Create some left [`Padding`].
+pub fn left(padding: impl Into<Pixels>) -> Padding {
+ Padding::default().left(padding)
+}
+
+/// Create some right [`Padding`].
+pub fn right(padding: impl Into<Pixels>) -> Padding {
+ Padding::default().right(padding)
+}
+
impl Padding {
/// Padding of zero
pub const ZERO: Padding = Padding {
@@ -54,7 +78,7 @@ impl Padding {
left: 0.0,
};
- /// Create a Padding that is equal on all sides
+ /// Create a [`Padding`] that is equal on all sides.
pub const fn new(padding: f32) -> Padding {
Padding {
top: padding,
@@ -64,6 +88,46 @@ impl Padding {
}
}
+ /// Sets the [`top`] of the [`Padding`].
+ ///
+ /// [`top`]: Self::top
+ pub fn top(self, top: impl Into<Pixels>) -> Self {
+ Self {
+ top: top.into().0,
+ ..self
+ }
+ }
+
+ /// Sets the [`bottom`] of the [`Padding`].
+ ///
+ /// [`bottom`]: Self::bottom
+ pub fn bottom(self, bottom: impl Into<Pixels>) -> Self {
+ Self {
+ bottom: bottom.into().0,
+ ..self
+ }
+ }
+
+ /// Sets the [`left`] of the [`Padding`].
+ ///
+ /// [`left`]: Self::left
+ pub fn left(self, left: impl Into<Pixels>) -> Self {
+ Self {
+ left: left.into().0,
+ ..self
+ }
+ }
+
+ /// Sets the [`right`] of the [`Padding`].
+ ///
+ /// [`right`]: Self::right
+ pub fn right(self, right: impl Into<Pixels>) -> Self {
+ Self {
+ right: right.into().0,
+ ..self
+ }
+ }
+
/// Returns the total amount of vertical [`Padding`].
pub fn vertical(self) -> f32 {
self.top + self.bottom
@@ -111,17 +175,6 @@ impl From<[u16; 2]> for Padding {
}
}
-impl From<[u16; 4]> for Padding {
- fn from(p: [u16; 4]) -> Self {
- Padding {
- top: f32::from(p[0]),
- right: f32::from(p[1]),
- bottom: f32::from(p[2]),
- left: f32::from(p[3]),
- }
- }
-}
-
impl From<f32> for Padding {
fn from(p: f32) -> Self {
Padding {
@@ -144,17 +197,6 @@ impl From<[f32; 2]> for Padding {
}
}
-impl From<[f32; 4]> for Padding {
- fn from(p: [f32; 4]) -> Self {
- Padding {
- top: p[0],
- right: p[1],
- bottom: p[2],
- left: p[3],
- }
- }
-}
-
impl From<Padding> for Size {
fn from(padding: Padding) -> Self {
Self::new(padding.horizontal(), padding.vertical())
diff --git a/core/src/pixels.rs b/core/src/pixels.rs
index 425c0028..a1ea0f15 100644
--- a/core/src/pixels.rs
+++ b/core/src/pixels.rs
@@ -6,9 +6,14 @@
/// (e.g. `impl Into<Pixels>`) and, since `Pixels` implements `From` both for
/// `f32` and `u16`, you should be able to provide both integers and float
/// literals as needed.
-#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
+#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Default)]
pub struct Pixels(pub f32);
+impl Pixels {
+ /// Zero pixels.
+ pub const ZERO: Self = Self(0.0);
+}
+
impl From<f32> for Pixels {
fn from(amount: f32) -> Self {
Self(amount)
@@ -27,6 +32,30 @@ impl From<Pixels> for f32 {
}
}
+impl std::ops::Add for Pixels {
+ type Output = Pixels;
+
+ fn add(self, rhs: Self) -> Self {
+ Pixels(self.0 + rhs.0)
+ }
+}
+
+impl std::ops::Add<f32> for Pixels {
+ type Output = Pixels;
+
+ fn add(self, rhs: f32) -> Self {
+ Pixels(self.0 + rhs)
+ }
+}
+
+impl std::ops::Mul for Pixels {
+ type Output = Pixels;
+
+ fn mul(self, rhs: Self) -> Self {
+ Pixels(self.0 * rhs.0)
+ }
+}
+
impl std::ops::Mul<f32> for Pixels {
type Output = Pixels;
@@ -34,3 +63,19 @@ impl std::ops::Mul<f32> for Pixels {
Pixels(self.0 * rhs)
}
}
+
+impl std::ops::Div for Pixels {
+ type Output = Pixels;
+
+ fn div(self, rhs: Self) -> Self {
+ Pixels(self.0 / rhs.0)
+ }
+}
+
+impl std::ops::Div<f32> for Pixels {
+ type Output = Pixels;
+
+ fn div(self, rhs: f32) -> Self {
+ Pixels(self.0 / rhs)
+ }
+}
diff --git a/core/src/rectangle.rs b/core/src/rectangle.rs
index 1556e072..cff33991 100644
--- a/core/src/rectangle.rs
+++ b/core/src/rectangle.rs
@@ -1,4 +1,4 @@
-use crate::{Point, Radians, Size, Vector};
+use crate::{Padding, Point, Radians, Size, Vector};
/// An axis-aligned rectangle.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
@@ -47,6 +47,62 @@ impl Rectangle<f32> {
}
}
+ /// Creates a new square [`Rectangle`] with the center at the origin and
+ /// with the given radius.
+ pub fn with_radius(radius: f32) -> Self {
+ Self {
+ x: -radius,
+ y: -radius,
+ width: radius * 2.0,
+ height: radius * 2.0,
+ }
+ }
+
+ /// Creates a new axis-aligned [`Rectangle`] from the given vertices; returning the
+ /// rotation in [`Radians`] that must be applied to the axis-aligned [`Rectangle`]
+ /// to obtain the desired result.
+ pub fn with_vertices(
+ top_left: Point,
+ top_right: Point,
+ bottom_left: Point,
+ ) -> (Rectangle, Radians) {
+ let width = (top_right.x - top_left.x).hypot(top_right.y - top_left.y);
+
+ let height =
+ (bottom_left.x - top_left.x).hypot(bottom_left.y - top_left.y);
+
+ let rotation =
+ (top_right.y - top_left.y).atan2(top_right.x - top_left.x);
+
+ let rotation = if rotation < 0.0 {
+ 2.0 * std::f32::consts::PI + rotation
+ } else {
+ rotation
+ };
+
+ let position = {
+ let center = Point::new(
+ (top_right.x + bottom_left.x) / 2.0,
+ (top_right.y + bottom_left.y) / 2.0,
+ );
+
+ let rotation = -rotation - std::f32::consts::PI * 2.0;
+
+ Point::new(
+ center.x + (top_left.x - center.x) * rotation.cos()
+ - (top_left.y - center.y) * rotation.sin(),
+ center.y
+ + (top_left.x - center.x) * rotation.sin()
+ + (top_left.y - center.y) * rotation.cos(),
+ )
+ };
+
+ (
+ Rectangle::new(position, Size::new(width, height)),
+ Radians(rotation),
+ )
+ }
+
/// Returns the [`Point`] at the center of the [`Rectangle`].
pub fn center(&self) -> Point {
Point::new(self.center_x(), self.center_y())
@@ -164,12 +220,26 @@ impl Rectangle<f32> {
}
/// Expands the [`Rectangle`] a given amount.
- pub fn expand(self, amount: f32) -> Self {
+ pub fn expand(self, padding: impl Into<Padding>) -> Self {
+ let padding = padding.into();
+
+ Self {
+ x: self.x - padding.left,
+ y: self.y - padding.top,
+ width: self.width + padding.horizontal(),
+ height: self.height + padding.vertical(),
+ }
+ }
+
+ /// Shrinks the [`Rectangle`] a given amount.
+ pub fn shrink(self, padding: impl Into<Padding>) -> Self {
+ let padding = padding.into();
+
Self {
- x: self.x - amount,
- y: self.y - amount,
- width: self.width + amount * 2.0,
- height: self.height + amount * 2.0,
+ x: self.x + padding.left,
+ y: self.y + padding.top,
+ width: self.width - padding.horizontal(),
+ height: self.height - padding.vertical(),
}
}
diff --git a/core/src/renderer.rs b/core/src/renderer.rs
index a2785ae8..6684517f 100644
--- a/core/src/renderer.rs
+++ b/core/src/renderer.rs
@@ -69,7 +69,7 @@ pub struct Quad {
/// The bounds of the [`Quad`].
pub bounds: Rectangle,
- /// The [`Border`] of the [`Quad`].
+ /// The [`Border`] of the [`Quad`]. The border is drawn on the inside of the [`Quad`].
pub border: Border,
/// The [`Shadow`] of the [`Quad`].
diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs
index e8709dbc..bbcdd8ff 100644
--- a/core/src/renderer/null.rs
+++ b/core/src/renderer/null.rs
@@ -1,11 +1,10 @@
use crate::alignment;
-use crate::image;
+use crate::image::{self, Image};
use crate::renderer::{self, Renderer};
use crate::svg;
use crate::text::{self, Text};
use crate::{
- Background, Color, Font, Pixels, Point, Radians, Rectangle, Size,
- Transformation,
+ Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation,
};
impl Renderer for () {
@@ -77,9 +76,14 @@ impl text::Paragraph for () {
fn with_text(_text: Text<&str>) -> Self {}
+ fn with_spans<Link>(
+ _text: Text<&[text::Span<'_, Link, Self::Font>], Self::Font>,
+ ) -> Self {
+ }
+
fn resize(&mut self, _new_bounds: Size) {}
- fn compare(&self, _text: Text<&str>) -> text::Difference {
+ fn compare(&self, _text: Text<()>) -> text::Difference {
text::Difference::None
}
@@ -102,6 +106,14 @@ impl text::Paragraph for () {
fn hit_test(&self, _point: Point) -> Option<text::Hit> {
None
}
+
+ fn hit_span(&self, _point: Point) -> Option<usize> {
+ None
+ }
+
+ fn span_bounds(&self, _index: usize) -> Vec<Rectangle> {
+ vec![]
+ }
}
impl text::Editor for () {
@@ -109,6 +121,10 @@ impl text::Editor for () {
fn with_text(_text: &str) -> Self {}
+ fn is_empty(&self) -> bool {
+ true
+ }
+
fn cursor(&self) -> text::editor::Cursor {
text::editor::Cursor::Caret(Point::ORIGIN)
}
@@ -145,6 +161,7 @@ impl text::Editor for () {
_new_font: Self::Font,
_new_size: Pixels,
_new_line_height: text::LineHeight,
+ _new_wrapping: text::Wrapping,
_new_highlighter: &mut impl text::Highlighter,
) {
}
@@ -161,21 +178,13 @@ impl text::Editor for () {
}
impl image::Renderer for () {
- type Handle = ();
+ type Handle = image::Handle;
fn measure_image(&self, _handle: &Self::Handle) -> Size<u32> {
Size::default()
}
- fn draw_image(
- &mut self,
- _handle: Self::Handle,
- _filter_method: image::FilterMethod,
- _bounds: Rectangle,
- _rotation: Radians,
- _opacity: f32,
- ) {
- }
+ fn draw_image(&mut self, _image: Image, _bounds: Rectangle) {}
}
impl svg::Renderer for () {
@@ -183,13 +192,5 @@ impl svg::Renderer for () {
Size::default()
}
- fn draw_svg(
- &mut self,
- _handle: svg::Handle,
- _color: Option<Color>,
- _bounds: Rectangle,
- _rotation: Radians,
- _opacity: f32,
- ) {
- }
+ fn draw_svg(&mut self, _svg: svg::Svg, _bounds: Rectangle) {}
}
diff --git a/core/src/size.rs b/core/src/size.rs
index d7459355..95089236 100644
--- a/core/src/size.rs
+++ b/core/src/size.rs
@@ -99,6 +99,20 @@ impl<T> From<Size<T>> for Vector<T> {
}
}
+impl<T> std::ops::Add for Size<T>
+where
+ T: std::ops::Add<Output = T>,
+{
+ type Output = Size<T>;
+
+ fn add(self, rhs: Self) -> Self::Output {
+ Size {
+ width: self.width + rhs.width,
+ height: self.height + rhs.height,
+ }
+ }
+}
+
impl<T> std::ops::Sub for Size<T>
where
T: std::ops::Sub<Output = T>,
diff --git a/core/src/svg.rs b/core/src/svg.rs
index 946b8156..ac19b223 100644
--- a/core/src/svg.rs
+++ b/core/src/svg.rs
@@ -7,6 +7,66 @@ use std::hash::{Hash, Hasher as _};
use std::path::PathBuf;
use std::sync::Arc;
+/// A raster image that can be drawn.
+#[derive(Debug, Clone, PartialEq)]
+pub struct Svg<H = Handle> {
+ /// The handle of the [`Svg`].
+ pub handle: H,
+
+ /// The [`Color`] filter to be applied to the [`Svg`].
+ ///
+ /// If some [`Color`] is set, the whole [`Svg`] will be
+ /// painted with it—ignoring any intrinsic colors.
+ ///
+ /// This can be useful for coloring icons programmatically
+ /// (e.g. with a theme).
+ pub color: Option<Color>,
+
+ /// The rotation to be applied to the image; on its center.
+ pub rotation: Radians,
+
+ /// The opacity of the [`Svg`].
+ ///
+ /// 0 means transparent. 1 means opaque.
+ pub opacity: f32,
+}
+
+impl Svg<Handle> {
+ /// Creates a new [`Svg`] with the given handle.
+ pub fn new(handle: impl Into<Handle>) -> Self {
+ Self {
+ handle: handle.into(),
+ color: None,
+ rotation: Radians(0.0),
+ opacity: 1.0,
+ }
+ }
+
+ /// Sets the [`Color`] filter of the [`Svg`].
+ pub fn color(mut self, color: impl Into<Color>) -> Self {
+ self.color = Some(color.into());
+ self
+ }
+
+ /// Sets the rotation of the [`Svg`].
+ pub fn rotation(mut self, rotation: impl Into<Radians>) -> Self {
+ self.rotation = rotation.into();
+ self
+ }
+
+ /// Sets the opacity of the [`Svg`].
+ pub fn opacity(mut self, opacity: impl Into<f32>) -> Self {
+ self.opacity = opacity.into();
+ self
+ }
+}
+
+impl From<&Handle> for Svg {
+ fn from(handle: &Handle) -> Self {
+ Svg::new(handle.clone())
+ }
+}
+
/// A handle of Svg data.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Handle {
@@ -95,12 +155,5 @@ pub trait Renderer: crate::Renderer {
fn measure_svg(&self, handle: &Handle) -> Size<u32>;
/// Draws an SVG with the given [`Handle`], an optional [`Color`] filter, and inside the provided `bounds`.
- fn draw_svg(
- &mut self,
- handle: Handle,
- color: Option<Color>,
- bounds: Rectangle,
- rotation: Radians,
- opacity: f32,
- );
+ fn draw_svg(&mut self, svg: Svg, bounds: Rectangle);
}
diff --git a/core/src/text.rs b/core/src/text.rs
index b30feae0..d7b7fee4 100644
--- a/core/src/text.rs
+++ b/core/src/text.rs
@@ -1,16 +1,18 @@
//! Draw and interact with text.
-mod paragraph;
-
pub mod editor;
pub mod highlighter;
+pub mod paragraph;
pub use editor::Editor;
pub use highlighter::Highlighter;
pub use paragraph::Paragraph;
use crate::alignment;
-use crate::{Color, Pixels, Point, Rectangle, Size};
+use crate::{
+ Background, Border, Color, Padding, Pixels, Point, Rectangle, Size,
+};
+use std::borrow::Cow;
use std::hash::{Hash, Hasher};
/// A paragraph.
@@ -39,6 +41,9 @@ pub struct Text<Content = String, Font = crate::Font> {
/// The [`Shaping`] strategy of the [`Text`].
pub shaping: Shaping,
+
+ /// The [`Wrapping`] strategy of the [`Text`].
+ pub wrapping: Wrapping,
}
/// The shaping strategy of some text.
@@ -65,6 +70,22 @@ pub enum Shaping {
Advanced,
}
+/// The wrapping strategy of some text.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
+pub enum Wrapping {
+ /// No wrapping.
+ None,
+ /// Wraps at the word level.
+ ///
+ /// This is the default.
+ #[default]
+ Word,
+ /// Wraps at the glyph level.
+ Glyph,
+ /// Wraps at the word level, or fallback to glyph level if a word can't fit on a line by itself.
+ WordOrGlyph,
+}
+
/// The height of a line of text in a paragraph.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum LineHeight {
@@ -221,3 +242,294 @@ pub trait Renderer: crate::Renderer {
clip_bounds: Rectangle,
);
}
+
+/// A span of text.
+#[derive(Debug, Clone)]
+pub struct Span<'a, Link = (), Font = crate::Font> {
+ /// The [`Fragment`] of text.
+ pub text: Fragment<'a>,
+ /// The size of the [`Span`] in [`Pixels`].
+ pub size: Option<Pixels>,
+ /// The [`LineHeight`] of the [`Span`].
+ pub line_height: Option<LineHeight>,
+ /// The font of the [`Span`].
+ pub font: Option<Font>,
+ /// The [`Color`] of the [`Span`].
+ pub color: Option<Color>,
+ /// The link of the [`Span`].
+ pub link: Option<Link>,
+ /// The [`Highlight`] of the [`Span`].
+ pub highlight: Option<Highlight>,
+ /// The [`Padding`] of the [`Span`].
+ ///
+ /// Currently, it only affects the bounds of the [`Highlight`].
+ pub padding: Padding,
+ /// Whether the [`Span`] should be underlined or not.
+ pub underline: bool,
+ /// Whether the [`Span`] should be struck through or not.
+ pub strikethrough: bool,
+}
+
+/// A text highlight.
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub struct Highlight {
+ /// The [`Background`] of the highlight.
+ pub background: Background,
+ /// The [`Border`] of the highlight.
+ pub border: Border,
+}
+
+impl<'a, Link, Font> Span<'a, Link, Font> {
+ /// Creates a new [`Span`] of text with the given text fragment.
+ 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,
+ }
+ }
+
+ /// Sets the size of the [`Span`].
+ pub fn size(mut self, size: impl Into<Pixels>) -> Self {
+ self.size = Some(size.into());
+ self
+ }
+
+ /// Sets the [`LineHeight`] of the [`Span`].
+ pub fn line_height(mut self, line_height: impl Into<LineHeight>) -> Self {
+ self.line_height = Some(line_height.into());
+ self
+ }
+
+ /// Sets the font of the [`Span`].
+ pub fn font(mut self, font: impl Into<Font>) -> Self {
+ self.font = Some(font.into());
+ self
+ }
+
+ /// Sets the font of the [`Span`], if any.
+ pub fn font_maybe(mut self, font: Option<impl Into<Font>>) -> Self {
+ self.font = font.map(Into::into);
+ self
+ }
+
+ /// Sets the [`Color`] of the [`Span`].
+ pub fn color(mut self, color: impl Into<Color>) -> Self {
+ self.color = Some(color.into());
+ self
+ }
+
+ /// Sets the [`Color`] of the [`Span`], if any.
+ pub fn color_maybe(mut self, color: Option<impl Into<Color>>) -> Self {
+ self.color = color.map(Into::into);
+ self
+ }
+
+ /// Sets the link of the [`Span`].
+ pub fn link(mut self, link: impl Into<Link>) -> Self {
+ self.link = Some(link.into());
+ self
+ }
+
+ /// Sets the link of the [`Span`], if any.
+ pub fn link_maybe(mut self, link: Option<impl Into<Link>>) -> Self {
+ self.link = link.map(Into::into);
+ self
+ }
+
+ /// Sets the [`Background`] of the [`Span`].
+ pub fn background(self, background: impl Into<Background>) -> Self {
+ self.background_maybe(Some(background))
+ }
+
+ /// Sets the [`Background`] of the [`Span`], if any.
+ pub fn background_maybe(
+ mut self,
+ background: Option<impl Into<Background>>,
+ ) -> Self {
+ let Some(background) = background else {
+ return self;
+ };
+
+ match &mut self.highlight {
+ Some(highlight) => {
+ highlight.background = background.into();
+ }
+ None => {
+ self.highlight = Some(Highlight {
+ background: background.into(),
+ border: Border::default(),
+ });
+ }
+ }
+
+ self
+ }
+
+ /// Sets the [`Border`] of the [`Span`].
+ pub fn border(self, border: impl Into<Border>) -> Self {
+ self.border_maybe(Some(border))
+ }
+
+ /// Sets the [`Border`] of the [`Span`], if any.
+ pub fn border_maybe(mut self, border: Option<impl Into<Border>>) -> Self {
+ let Some(border) = border else {
+ return self;
+ };
+
+ match &mut self.highlight {
+ Some(highlight) => {
+ highlight.border = border.into();
+ }
+ None => {
+ self.highlight = Some(Highlight {
+ border: border.into(),
+ background: Background::Color(Color::TRANSPARENT),
+ });
+ }
+ }
+
+ self
+ }
+
+ /// Sets the [`Padding`] of the [`Span`].
+ ///
+ /// It only affects the [`background`] and [`border`] of the
+ /// [`Span`], currently.
+ ///
+ /// [`background`]: Self::background
+ /// [`border`]: Self::border
+ pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
+ self.padding = padding.into();
+ self
+ }
+
+ /// Sets whether the [`Span`] shoud be underlined or not.
+ pub fn underline(mut self, underline: bool) -> Self {
+ self.underline = underline;
+ self
+ }
+
+ /// Sets whether the [`Span`] shoud be struck through or not.
+ pub fn strikethrough(mut self, strikethrough: bool) -> Self {
+ self.strikethrough = strikethrough;
+ self
+ }
+
+ /// Turns the [`Span`] into a static one.
+ pub fn to_static(self) -> Span<'static, Link, Font> {
+ Span {
+ text: Cow::Owned(self.text.into_owned()),
+ size: self.size,
+ line_height: self.line_height,
+ font: self.font,
+ color: self.color,
+ link: self.link,
+ highlight: self.highlight,
+ padding: self.padding,
+ underline: self.underline,
+ strikethrough: self.strikethrough,
+ }
+ }
+}
+
+impl<'a, Link, Font> From<&'a str> for Span<'a, Link, Font> {
+ fn from(value: &'a str) -> Self {
+ Span::new(value)
+ }
+}
+
+impl<'a, Link, Font: PartialEq> PartialEq for Span<'a, Link, Font> {
+ fn eq(&self, other: &Self) -> bool {
+ self.text == other.text
+ && self.size == other.size
+ && self.line_height == other.line_height
+ && self.font == other.font
+ && self.color == other.color
+ }
+}
+
+/// A fragment of [`Text`].
+///
+/// This is just an alias to a string that may be either
+/// borrowed or owned.
+pub type Fragment<'a> = Cow<'a, str>;
+
+/// A trait for converting a value to some text [`Fragment`].
+pub trait IntoFragment<'a> {
+ /// Converts the value to some text [`Fragment`].
+ fn into_fragment(self) -> Fragment<'a>;
+}
+
+impl<'a> IntoFragment<'a> for Fragment<'a> {
+ fn into_fragment(self) -> Fragment<'a> {
+ self
+ }
+}
+
+impl<'a, 'b> IntoFragment<'a> for &'a Fragment<'b> {
+ fn into_fragment(self) -> Fragment<'a> {
+ Fragment::Borrowed(self)
+ }
+}
+
+impl<'a> IntoFragment<'a> for &'a str {
+ fn into_fragment(self) -> Fragment<'a> {
+ Fragment::Borrowed(self)
+ }
+}
+
+impl<'a> IntoFragment<'a> for &'a String {
+ fn into_fragment(self) -> Fragment<'a> {
+ Fragment::Borrowed(self.as_str())
+ }
+}
+
+impl<'a> IntoFragment<'a> for String {
+ fn into_fragment(self) -> Fragment<'a> {
+ Fragment::Owned(self)
+ }
+}
+
+macro_rules! into_fragment {
+ ($type:ty) => {
+ impl<'a> IntoFragment<'a> for $type {
+ fn into_fragment(self) -> Fragment<'a> {
+ Fragment::Owned(self.to_string())
+ }
+ }
+
+ impl<'a> IntoFragment<'a> for &$type {
+ fn into_fragment(self) -> Fragment<'a> {
+ Fragment::Owned(self.to_string())
+ }
+ }
+ };
+}
+
+into_fragment!(char);
+into_fragment!(bool);
+
+into_fragment!(u8);
+into_fragment!(u16);
+into_fragment!(u32);
+into_fragment!(u64);
+into_fragment!(u128);
+into_fragment!(usize);
+
+into_fragment!(i8);
+into_fragment!(i16);
+into_fragment!(i32);
+into_fragment!(i64);
+into_fragment!(i128);
+into_fragment!(isize);
+
+into_fragment!(f32);
+into_fragment!(f64);
diff --git a/core/src/text/editor.rs b/core/src/text/editor.rs
index fbf60696..cd30db3a 100644
--- a/core/src/text/editor.rs
+++ b/core/src/text/editor.rs
@@ -1,6 +1,6 @@
//! Edit text.
use crate::text::highlighter::{self, Highlighter};
-use crate::text::LineHeight;
+use crate::text::{LineHeight, Wrapping};
use crate::{Pixels, Point, Rectangle, Size};
use std::sync::Arc;
@@ -13,6 +13,9 @@ pub trait Editor: Sized + Default {
/// Creates a new [`Editor`] laid out with the given text.
fn with_text(text: &str) -> Self;
+ /// Returns true if the [`Editor`] has no contents.
+ fn is_empty(&self) -> bool;
+
/// Returns the current [`Cursor`] of the [`Editor`].
fn cursor(&self) -> Cursor;
@@ -47,6 +50,7 @@ pub trait Editor: Sized + Default {
new_font: Self::Font,
new_size: Pixels,
new_line_height: LineHeight,
+ new_wrapping: Wrapping,
new_highlighter: &mut impl Highlighter,
);
@@ -70,6 +74,8 @@ pub enum Action {
SelectWord,
/// Select the line at the current cursor.
SelectLine,
+ /// Select the entire buffer.
+ SelectAll,
/// Perform an [`Edit`].
Edit(Edit),
/// Click the [`Editor`] at the given [`Point`].
diff --git a/core/src/text/paragraph.rs b/core/src/text/paragraph.rs
index 8ff04015..924276c3 100644
--- a/core/src/text/paragraph.rs
+++ b/core/src/text/paragraph.rs
@@ -1,6 +1,7 @@
+//! Draw paragraphs.
use crate::alignment;
-use crate::text::{Difference, Hit, Text};
-use crate::{Point, Size};
+use crate::text::{Difference, Hit, Span, Text};
+use crate::{Point, Rectangle, Size};
/// A text paragraph.
pub trait Paragraph: Sized + Default {
@@ -10,12 +11,17 @@ pub trait Paragraph: Sized + Default {
/// Creates a new [`Paragraph`] laid out with the given [`Text`].
fn with_text(text: Text<&str, Self::Font>) -> Self;
+ /// Creates a new [`Paragraph`] laid out with the given [`Text`].
+ fn with_spans<Link>(
+ text: Text<&[Span<'_, Link, Self::Font>], Self::Font>,
+ ) -> Self;
+
/// Lays out the [`Paragraph`] with some new boundaries.
fn resize(&mut self, new_bounds: Size);
/// Compares the [`Paragraph`] with some desired [`Text`] and returns the
/// [`Difference`].
- fn compare(&self, text: Text<&str, Self::Font>) -> Difference;
+ fn compare(&self, text: Text<(), Self::Font>) -> Difference;
/// Returns the horizontal alignment of the [`Paragraph`].
fn horizontal_alignment(&self) -> alignment::Horizontal;
@@ -31,29 +37,100 @@ pub trait Paragraph: Sized + Default {
/// [`Paragraph`], returning information about the nearest character.
fn hit_test(&self, point: Point) -> Option<Hit>;
+ /// Tests whether the provided point is within the boundaries of a
+ /// [`Span`] in the [`Paragraph`], returning the index of the [`Span`]
+ /// that was hit.
+ fn hit_span(&self, point: Point) -> Option<usize>;
+
+ /// Returns all bounds for the provided [`Span`] index of the [`Paragraph`].
+ /// A [`Span`] can have multiple bounds for each line it's on.
+ fn span_bounds(&self, index: usize) -> Vec<Rectangle>;
+
/// Returns the distance to the given grapheme index in the [`Paragraph`].
fn grapheme_position(&self, line: usize, index: usize) -> Option<Point>;
- /// Updates the [`Paragraph`] to match the given [`Text`], if needed.
- fn update(&mut self, text: Text<&str, Self::Font>) {
- match self.compare(text) {
+ /// Returns the minimum width that can fit the contents of the [`Paragraph`].
+ fn min_width(&self) -> f32 {
+ self.min_bounds().width
+ }
+
+ /// Returns the minimum height that can fit the contents of the [`Paragraph`].
+ fn min_height(&self) -> f32 {
+ self.min_bounds().height
+ }
+}
+
+/// A [`Paragraph`] of plain text.
+#[derive(Debug, Clone, Default)]
+pub struct Plain<P: Paragraph> {
+ raw: P,
+ content: String,
+}
+
+impl<P: Paragraph> Plain<P> {
+ /// Creates a new [`Plain`] paragraph.
+ pub fn new(text: Text<&str, P::Font>) -> Self {
+ let content = text.content.to_owned();
+
+ Self {
+ raw: P::with_text(text),
+ content,
+ }
+ }
+
+ /// Updates the plain [`Paragraph`] to match the given [`Text`], if needed.
+ pub fn update(&mut self, text: Text<&str, P::Font>) {
+ if self.content != text.content {
+ text.content.clone_into(&mut self.content);
+ self.raw = P::with_text(text);
+ return;
+ }
+
+ match self.raw.compare(Text {
+ content: (),
+ bounds: text.bounds,
+ size: text.size,
+ line_height: text.line_height,
+ font: text.font,
+ horizontal_alignment: text.horizontal_alignment,
+ vertical_alignment: text.vertical_alignment,
+ shaping: text.shaping,
+ wrapping: text.wrapping,
+ }) {
Difference::None => {}
Difference::Bounds => {
- self.resize(text.bounds);
+ self.raw.resize(text.bounds);
}
Difference::Shape => {
- *self = Self::with_text(text);
+ self.raw = P::with_text(text);
}
}
}
- /// Returns the minimum width that can fit the contents of the [`Paragraph`].
- fn min_width(&self) -> f32 {
- self.min_bounds().width
+ /// Returns the horizontal alignment of the [`Paragraph`].
+ pub fn horizontal_alignment(&self) -> alignment::Horizontal {
+ self.raw.horizontal_alignment()
}
- /// Returns the minimum height that can fit the contents of the [`Paragraph`].
- fn min_height(&self) -> f32 {
- self.min_bounds().height
+ /// Returns the vertical alignment of the [`Paragraph`].
+ pub fn vertical_alignment(&self) -> alignment::Vertical {
+ self.raw.vertical_alignment()
+ }
+
+ /// Returns the minimum boundaries that can fit the contents of the
+ /// [`Paragraph`].
+ pub fn min_bounds(&self) -> Size {
+ self.raw.min_bounds()
+ }
+
+ /// Returns the minimum width that can fit the contents of the
+ /// [`Paragraph`].
+ pub fn min_width(&self) -> f32 {
+ self.raw.min_width()
+ }
+
+ /// Returns the cached [`Paragraph`].
+ pub fn raw(&self) -> &P {
+ &self.raw
}
}
diff --git a/core/src/vector.rs b/core/src/vector.rs
index 049e648f..ff848c4f 100644
--- a/core/src/vector.rs
+++ b/core/src/vector.rs
@@ -18,9 +18,17 @@ impl<T> Vector<T> {
impl Vector {
/// The zero [`Vector`].
pub const ZERO: Self = Self::new(0.0, 0.0);
+}
+
+impl<T> std::ops::Neg for Vector<T>
+where
+ T: std::ops::Neg<Output = T>,
+{
+ type Output = Self;
- /// The unit [`Vector`].
- pub const UNIT: Self = Self::new(0.0, 0.0);
+ fn neg(self) -> Self::Output {
+ Self::new(-self.x, -self.y)
+ }
}
impl<T> std::ops::Add for Vector<T>
diff --git a/core/src/widget.rs b/core/src/widget.rs
index 0d12deba..c5beea54 100644
--- a/core/src/widget.rs
+++ b/core/src/widget.rs
@@ -27,11 +27,11 @@ use crate::{Clipboard, Length, Rectangle, Shell, Size, Vector};
/// widget:
///
/// - [`bezier_tool`], a Paint-like tool for drawing Bézier curves using
-/// [`lyon`].
+/// [`lyon`].
/// - [`custom_widget`], a demonstration of how to build a custom widget that
-/// draws a circle.
+/// draws a circle.
/// - [`geometry`], a custom widget showcasing how to draw geometry with the
-/// `Mesh2D` primitive in [`iced_wgpu`].
+/// `Mesh2D` primitive in [`iced_wgpu`].
///
/// [examples]: https://github.com/iced-rs/iced/tree/0.12/examples
/// [`bezier_tool`]: https://github.com/iced-rs/iced/tree/0.12/examples/bezier_tool
@@ -96,7 +96,7 @@ where
Vec::new()
}
- /// Reconciliates the [`Widget`] with the provided [`Tree`].
+ /// Reconciles the [`Widget`] with the provided [`Tree`].
fn diff(&self, _tree: &mut Tree) {}
/// Applies an [`Operation`] to the [`Widget`].
@@ -105,7 +105,7 @@ where
_state: &mut Tree,
_layout: Layout<'_>,
_renderer: &Renderer,
- _operation: &mut dyn Operation<()>,
+ _operation: &mut dyn Operation,
) {
}
diff --git a/core/src/widget/operation.rs b/core/src/widget/operation.rs
index 3e4ed618..097c3601 100644
--- a/core/src/widget/operation.rs
+++ b/core/src/widget/operation.rs
@@ -12,11 +12,12 @@ use crate::{Rectangle, Vector};
use std::any::Any;
use std::fmt;
+use std::marker::PhantomData;
use std::sync::Arc;
/// A piece of logic that can traverse the widget tree of an application in
/// order to query or update some widget state.
-pub trait Operation<T>: Send {
+pub trait Operation<T = ()>: Send {
/// Operates on a widget that contains other widgets.
///
/// The `operate_on_children` function can be called to return control to
@@ -37,6 +38,7 @@ pub trait Operation<T>: Send {
_state: &mut dyn Scrollable,
_id: Option<&Id>,
_bounds: Rectangle,
+ _content_bounds: Rectangle,
_translation: Vector,
) {
}
@@ -53,6 +55,53 @@ pub trait Operation<T>: Send {
}
}
+impl<T, O> Operation<O> for Box<T>
+where
+ T: Operation<O> + ?Sized,
+{
+ fn container(
+ &mut self,
+ id: Option<&Id>,
+ bounds: Rectangle,
+ operate_on_children: &mut dyn FnMut(&mut dyn Operation<O>),
+ ) {
+ self.as_mut().container(id, bounds, operate_on_children);
+ }
+
+ fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) {
+ self.as_mut().focusable(state, id);
+ }
+
+ fn scrollable(
+ &mut self,
+ state: &mut dyn Scrollable,
+ id: Option<&Id>,
+ bounds: Rectangle,
+ content_bounds: Rectangle,
+ translation: Vector,
+ ) {
+ self.as_mut().scrollable(
+ state,
+ id,
+ bounds,
+ content_bounds,
+ translation,
+ );
+ }
+
+ fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) {
+ self.as_mut().text_input(state, id);
+ }
+
+ fn custom(&mut self, state: &mut dyn Any, id: Option<&Id>) {
+ self.as_mut().custom(state, id);
+ }
+
+ fn finish(&self) -> Outcome<O> {
+ self.as_ref().finish()
+ }
+}
+
/// The result of an [`Operation`].
pub enum Outcome<T> {
/// The [`Operation`] produced no result.
@@ -78,9 +127,69 @@ where
}
}
+/// Wraps the [`Operation`] in a black box, erasing its returning type.
+pub fn black_box<'a, T, O>(
+ operation: &'a mut dyn Operation<T>,
+) -> impl Operation<O> + 'a
+where
+ T: 'a,
+{
+ struct BlackBox<'a, T> {
+ operation: &'a mut dyn Operation<T>,
+ }
+
+ impl<'a, T, O> Operation<O> for BlackBox<'a, T> {
+ fn container(
+ &mut self,
+ id: Option<&Id>,
+ bounds: Rectangle,
+ operate_on_children: &mut dyn FnMut(&mut dyn Operation<O>),
+ ) {
+ self.operation.container(id, bounds, &mut |operation| {
+ operate_on_children(&mut BlackBox { operation });
+ });
+ }
+
+ fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) {
+ self.operation.focusable(state, id);
+ }
+
+ fn scrollable(
+ &mut self,
+ state: &mut dyn Scrollable,
+ id: Option<&Id>,
+ bounds: Rectangle,
+ content_bounds: Rectangle,
+ translation: Vector,
+ ) {
+ self.operation.scrollable(
+ state,
+ id,
+ bounds,
+ content_bounds,
+ translation,
+ );
+ }
+
+ fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) {
+ self.operation.text_input(state, id);
+ }
+
+ fn custom(&mut self, state: &mut dyn Any, id: Option<&Id>) {
+ self.operation.custom(state, id);
+ }
+
+ fn finish(&self) -> Outcome<O> {
+ Outcome::None
+ }
+ }
+
+ BlackBox { operation }
+}
+
/// Maps the output of an [`Operation`] using the given function.
pub fn map<A, B>(
- operation: Box<dyn Operation<A>>,
+ operation: impl Operation<A>,
f: impl Fn(A) -> B + Send + Sync + 'static,
) -> impl Operation<B>
where
@@ -88,13 +197,14 @@ where
B: 'static,
{
#[allow(missing_debug_implementations)]
- struct Map<A, B> {
- operation: Box<dyn Operation<A>>,
+ struct Map<O, A, B> {
+ operation: O,
f: Arc<dyn Fn(A) -> B + Send + Sync>,
}
- impl<A, B> Operation<B> for Map<A, B>
+ impl<O, A, B> Operation<B> for Map<O, A, B>
where
+ O: Operation<A>,
A: 'static,
B: 'static,
{
@@ -127,9 +237,16 @@ where
state: &mut dyn Scrollable,
id: Option<&Id>,
bounds: Rectangle,
+ content_bounds: Rectangle,
translation: Vector,
) {
- self.operation.scrollable(state, id, bounds, translation);
+ self.operation.scrollable(
+ state,
+ id,
+ bounds,
+ content_bounds,
+ translation,
+ );
}
fn focusable(
@@ -155,10 +272,7 @@ where
let Self { operation, .. } = self;
- MapRef {
- operation: operation.as_mut(),
- }
- .container(id, bounds, operate_on_children);
+ MapRef { operation }.container(id, bounds, operate_on_children);
}
fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) {
@@ -170,9 +284,16 @@ where
state: &mut dyn Scrollable,
id: Option<&Id>,
bounds: Rectangle,
+ content_bounds: Rectangle,
translation: Vector,
) {
- self.operation.scrollable(state, id, bounds, translation);
+ self.operation.scrollable(
+ state,
+ id,
+ bounds,
+ content_bounds,
+ translation,
+ );
}
fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) {
@@ -201,6 +322,94 @@ where
}
}
+/// Chains the output of an [`Operation`] with the provided function to
+/// build a new [`Operation`].
+pub fn then<A, B, O>(
+ operation: impl Operation<A> + 'static,
+ f: fn(A) -> O,
+) -> impl Operation<B>
+where
+ A: 'static,
+ B: Send + 'static,
+ O: Operation<B> + 'static,
+{
+ struct Chain<T, O, A, B>
+ where
+ T: Operation<A>,
+ O: Operation<B>,
+ {
+ operation: T,
+ next: fn(A) -> O,
+ _result: PhantomData<B>,
+ }
+
+ impl<T, O, A, B> Operation<B> for Chain<T, O, A, B>
+ where
+ T: Operation<A> + 'static,
+ O: Operation<B> + 'static,
+ A: 'static,
+ B: Send + 'static,
+ {
+ fn container(
+ &mut self,
+ id: Option<&Id>,
+ bounds: Rectangle,
+ operate_on_children: &mut dyn FnMut(&mut dyn Operation<B>),
+ ) {
+ self.operation.container(id, bounds, &mut |operation| {
+ operate_on_children(&mut black_box(operation));
+ });
+ }
+
+ fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) {
+ self.operation.focusable(state, id);
+ }
+
+ fn scrollable(
+ &mut self,
+ state: &mut dyn Scrollable,
+ id: Option<&Id>,
+ bounds: Rectangle,
+ content_bounds: Rectangle,
+ translation: crate::Vector,
+ ) {
+ self.operation.scrollable(
+ state,
+ id,
+ bounds,
+ content_bounds,
+ translation,
+ );
+ }
+
+ fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) {
+ self.operation.text_input(state, id);
+ }
+
+ fn custom(&mut self, state: &mut dyn std::any::Any, id: Option<&Id>) {
+ self.operation.custom(state, id);
+ }
+
+ fn finish(&self) -> Outcome<B> {
+ match self.operation.finish() {
+ Outcome::None => Outcome::None,
+ Outcome::Some(value) => {
+ Outcome::Chain(Box::new((self.next)(value)))
+ }
+ Outcome::Chain(operation) => {
+ Outcome::Chain(Box::new(then(operation, self.next)))
+ }
+ }
+ }
+ }
+
+ Chain {
+ operation,
+ next: f,
+ _result: PhantomData,
+ }
+}
+
/// Produces an [`Operation`] that applies the given [`Operation`] to the
/// children of a container with the given [`Id`].
pub fn scope<T: 'static>(
diff --git a/core/src/widget/operation/focusable.rs b/core/src/widget/operation/focusable.rs
index 68c22faa..867c682e 100644
--- a/core/src/widget/operation/focusable.rs
+++ b/core/src/widget/operation/focusable.rs
@@ -1,5 +1,5 @@
//! Operate on widgets that can be focused.
-use crate::widget::operation::{Operation, Outcome};
+use crate::widget::operation::{self, Operation, Outcome};
use crate::widget::Id;
use crate::Rectangle;
@@ -58,19 +58,12 @@ pub fn focus<T>(target: Id) -> impl Operation<T> {
/// Produces an [`Operation`] that generates a [`Count`] and chains it with the
/// provided function to build a new [`Operation`].
-pub fn count<T, O>(f: fn(Count) -> O) -> impl Operation<T>
-where
- O: Operation<T> + 'static,
-{
- struct CountFocusable<O> {
+pub fn count() -> impl Operation<Count> {
+ struct CountFocusable {
count: Count,
- next: fn(Count) -> O,
}
- impl<T, O> Operation<T> for CountFocusable<O>
- where
- O: Operation<T> + 'static,
- {
+ impl Operation<Count> for CountFocusable {
fn focusable(&mut self, state: &mut dyn Focusable, _id: Option<&Id>) {
if state.is_focused() {
self.count.focused = Some(self.count.total);
@@ -83,26 +76,28 @@ where
&mut self,
_id: Option<&Id>,
_bounds: Rectangle,
- operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
+ operate_on_children: &mut dyn FnMut(&mut dyn Operation<Count>),
) {
operate_on_children(self);
}
- fn finish(&self) -> Outcome<T> {
- Outcome::Chain(Box::new((self.next)(self.count)))
+ fn finish(&self) -> Outcome<Count> {
+ Outcome::Some(self.count)
}
}
CountFocusable {
count: Count::default(),
- next: f,
}
}
/// Produces an [`Operation`] that searches for the current focused widget, and
/// - if found, focuses the previous focusable widget.
/// - if not found, focuses the last focusable widget.
-pub fn focus_previous<T>() -> impl Operation<T> {
+pub fn focus_previous<T>() -> impl Operation<T>
+where
+ T: Send + 'static,
+{
struct FocusPrevious {
count: Count,
current: usize,
@@ -136,13 +131,16 @@ pub fn focus_previous<T>() -> impl Operation<T> {
}
}
- count(|count| FocusPrevious { count, current: 0 })
+ operation::then(count(), |count| FocusPrevious { count, current: 0 })
}
/// Produces an [`Operation`] that searches for the current focused widget, and
/// - if found, focuses the next focusable widget.
/// - if not found, focuses the first focusable widget.
-pub fn focus_next<T>() -> impl Operation<T> {
+pub fn focus_next<T>() -> impl Operation<T>
+where
+ T: Send + 'static,
+{
struct FocusNext {
count: Count,
current: usize,
@@ -170,7 +168,7 @@ pub fn focus_next<T>() -> impl Operation<T> {
}
}
- count(|count| FocusNext { count, current: 0 })
+ operation::then(count(), |count| FocusNext { count, current: 0 })
}
/// Produces an [`Operation`] that searches for the current focused widget
diff --git a/core/src/widget/operation/scrollable.rs b/core/src/widget/operation/scrollable.rs
index 12161255..c2fecf56 100644
--- a/core/src/widget/operation/scrollable.rs
+++ b/core/src/widget/operation/scrollable.rs
@@ -9,6 +9,14 @@ pub trait Scrollable {
/// Scroll the widget to the given [`AbsoluteOffset`] along the horizontal & vertical axis.
fn scroll_to(&mut self, offset: AbsoluteOffset);
+
+ /// Scroll the widget by the given [`AbsoluteOffset`] along the horizontal & vertical axis.
+ fn scroll_by(
+ &mut self,
+ offset: AbsoluteOffset,
+ bounds: Rectangle,
+ content_bounds: Rectangle,
+ );
}
/// Produces an [`Operation`] that snaps the widget with the given [`Id`] to
@@ -34,6 +42,7 @@ pub fn snap_to<T>(target: Id, offset: RelativeOffset) -> impl Operation<T> {
state: &mut dyn Scrollable,
id: Option<&Id>,
_bounds: Rectangle,
+ _content_bounds: Rectangle,
_translation: Vector,
) {
if Some(&self.target) == id {
@@ -68,6 +77,7 @@ pub fn scroll_to<T>(target: Id, offset: AbsoluteOffset) -> impl Operation<T> {
state: &mut dyn Scrollable,
id: Option<&Id>,
_bounds: Rectangle,
+ _content_bounds: Rectangle,
_translation: Vector,
) {
if Some(&self.target) == id {
@@ -79,6 +89,41 @@ pub fn scroll_to<T>(target: Id, offset: AbsoluteOffset) -> impl Operation<T> {
ScrollTo { target, offset }
}
+/// Produces an [`Operation`] that scrolls the widget with the given [`Id`] by
+/// the provided [`AbsoluteOffset`].
+pub fn scroll_by<T>(target: Id, offset: AbsoluteOffset) -> impl Operation<T> {
+ struct ScrollBy {
+ target: Id,
+ offset: AbsoluteOffset,
+ }
+
+ impl<T> Operation<T> for ScrollBy {
+ fn container(
+ &mut self,
+ _id: Option<&Id>,
+ _bounds: Rectangle,
+ operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
+ ) {
+ operate_on_children(self);
+ }
+
+ fn scrollable(
+ &mut self,
+ state: &mut dyn Scrollable,
+ id: Option<&Id>,
+ bounds: Rectangle,
+ content_bounds: Rectangle,
+ _translation: Vector,
+ ) {
+ if Some(&self.target) == id {
+ state.scroll_by(self.offset, bounds, content_bounds);
+ }
+ }
+ }
+
+ ScrollBy { target, offset }
+}
+
/// The amount of absolute offset in each direction of a [`Scrollable`].
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct AbsoluteOffset {
diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs
index f1f0b345..d8d6e4c6 100644
--- a/core/src/widget/text.rs
+++ b/core/src/widget/text.rs
@@ -3,16 +3,15 @@ use crate::alignment;
use crate::layout;
use crate::mouse;
use crate::renderer;
-use crate::text::{self, Paragraph};
+use crate::text;
+use crate::text::paragraph::{self, Paragraph};
use crate::widget::tree::{self, Tree};
use crate::{
Color, Element, Layout, Length, Pixels, Point, Rectangle, Size, Theme,
Widget,
};
-use std::borrow::Cow;
-
-pub use text::{LineHeight, Shaping};
+pub use text::{LineHeight, Shaping, Wrapping};
/// A paragraph of text.
#[allow(missing_debug_implementations)]
@@ -21,7 +20,7 @@ where
Theme: Catalog,
Renderer: text::Renderer,
{
- fragment: Fragment<'a>,
+ fragment: text::Fragment<'a>,
size: Option<Pixels>,
line_height: LineHeight,
width: Length,
@@ -30,6 +29,7 @@ where
vertical_alignment: alignment::Vertical,
font: Option<Renderer::Font>,
shaping: Shaping,
+ wrapping: Wrapping,
class: Theme::Class<'a>,
}
@@ -39,7 +39,7 @@ where
Renderer: text::Renderer,
{
/// Create a new fragment of [`Text`] with the given contents.
- pub fn new(fragment: impl IntoFragment<'a>) -> Self {
+ pub fn new(fragment: impl text::IntoFragment<'a>) -> Self {
Text {
fragment: fragment.into_fragment(),
size: None,
@@ -49,7 +49,8 @@ where
height: Length::Shrink,
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Top,
- shaping: Shaping::Basic,
+ shaping: Shaping::default(),
+ wrapping: Wrapping::default(),
class: Theme::default(),
}
}
@@ -86,21 +87,27 @@ where
self
}
+ /// Centers the [`Text`], both horizontally and vertically.
+ pub fn center(self) -> Self {
+ self.align_x(alignment::Horizontal::Center)
+ .align_y(alignment::Vertical::Center)
+ }
+
/// Sets the [`alignment::Horizontal`] of the [`Text`].
- pub fn horizontal_alignment(
+ pub fn align_x(
mut self,
- alignment: alignment::Horizontal,
+ alignment: impl Into<alignment::Horizontal>,
) -> Self {
- self.horizontal_alignment = alignment;
+ self.horizontal_alignment = alignment.into();
self
}
/// Sets the [`alignment::Vertical`] of the [`Text`].
- pub fn vertical_alignment(
+ pub fn align_y(
mut self,
- alignment: alignment::Vertical,
+ alignment: impl Into<alignment::Vertical>,
) -> Self {
- self.vertical_alignment = alignment;
+ self.vertical_alignment = alignment.into();
self
}
@@ -110,6 +117,12 @@ where
self
}
+ /// Sets the [`Wrapping`] strategy of the [`Text`].
+ pub fn wrapping(mut self, wrapping: Wrapping) -> Self {
+ self.wrapping = wrapping;
+ self
+ }
+
/// Sets the style of the [`Text`].
#[must_use]
pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
@@ -149,7 +162,7 @@ where
/// The internal state of a [`Text`] widget.
#[derive(Debug, Default)]
-pub struct State<P: Paragraph>(P);
+pub struct State<P: Paragraph>(pub paragraph::Plain<P>);
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Text<'a, Theme, Renderer>
@@ -162,7 +175,9 @@ where
}
fn state(&self) -> tree::State {
- tree::State::new(State(Renderer::Paragraph::default()))
+ tree::State::new(State::<Renderer::Paragraph>(
+ paragraph::Plain::default(),
+ ))
}
fn size(&self) -> Size<Length> {
@@ -191,6 +206,7 @@ where
self.horizontal_alignment,
self.vertical_alignment,
self.shaping,
+ self.wrapping,
)
}
@@ -207,7 +223,7 @@ where
let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>();
let style = theme.style(&self.class);
- draw(renderer, defaults, layout, state, style, viewport);
+ draw(renderer, defaults, layout, state.0.raw(), style, viewport);
}
}
@@ -225,6 +241,7 @@ pub fn layout<Renderer>(
horizontal_alignment: alignment::Horizontal,
vertical_alignment: alignment::Vertical,
shaping: Shaping,
+ wrapping: Wrapping,
) -> layout::Node
where
Renderer: text::Renderer,
@@ -246,6 +263,7 @@ where
horizontal_alignment,
vertical_alignment,
shaping,
+ wrapping,
});
paragraph.min_bounds()
@@ -266,13 +284,12 @@ pub fn draw<Renderer>(
renderer: &mut Renderer,
style: &renderer::Style,
layout: Layout<'_>,
- state: &State<Renderer::Paragraph>,
+ paragraph: &Renderer::Paragraph,
appearance: Style,
viewport: &Rectangle,
) where
Renderer: text::Renderer,
{
- let State(ref paragraph) = state;
let bounds = layout.bounds();
let x = match paragraph.horizontal_alignment() {
@@ -367,80 +384,42 @@ impl Catalog for Theme {
}
}
-/// A fragment of [`Text`].
-///
-/// This is just an alias to a string that may be either
-/// borrowed or owned.
-pub type Fragment<'a> = Cow<'a, str>;
-
-/// A trait for converting a value to some text [`Fragment`].
-pub trait IntoFragment<'a> {
- /// Converts the value to some text [`Fragment`].
- fn into_fragment(self) -> Fragment<'a>;
+/// The default text styling; color is inherited.
+pub fn default(_theme: &Theme) -> Style {
+ Style { color: None }
}
-impl<'a> IntoFragment<'a> for Fragment<'a> {
- fn into_fragment(self) -> Fragment<'a> {
- self
+/// Text with the default base color.
+pub fn base(theme: &Theme) -> Style {
+ Style {
+ color: Some(theme.palette().text),
}
}
-impl<'a, 'b> IntoFragment<'a> for &'a Fragment<'b> {
- fn into_fragment(self) -> Fragment<'a> {
- Fragment::Borrowed(self)
+/// Text conveying some important information, like an action.
+pub fn primary(theme: &Theme) -> Style {
+ Style {
+ color: Some(theme.palette().primary),
}
}
-impl<'a> IntoFragment<'a> for &'a str {
- fn into_fragment(self) -> Fragment<'a> {
- Fragment::Borrowed(self)
+/// Text conveying some secondary information, like a footnote.
+pub fn secondary(theme: &Theme) -> Style {
+ Style {
+ color: Some(theme.extended_palette().secondary.strong.color),
}
}
-impl<'a> IntoFragment<'a> for &'a String {
- fn into_fragment(self) -> Fragment<'a> {
- Fragment::Borrowed(self.as_str())
+/// Text conveying some positive information, like a successful event.
+pub fn success(theme: &Theme) -> Style {
+ Style {
+ color: Some(theme.palette().success),
}
}
-impl<'a> IntoFragment<'a> for String {
- fn into_fragment(self) -> Fragment<'a> {
- Fragment::Owned(self)
+/// Text conveying some negative information, like an error.
+pub fn danger(theme: &Theme) -> Style {
+ Style {
+ color: Some(theme.palette().danger),
}
}
-
-macro_rules! into_fragment {
- ($type:ty) => {
- impl<'a> IntoFragment<'a> for $type {
- fn into_fragment(self) -> Fragment<'a> {
- Fragment::Owned(self.to_string())
- }
- }
-
- impl<'a> IntoFragment<'a> for &$type {
- fn into_fragment(self) -> Fragment<'a> {
- Fragment::Owned(self.to_string())
- }
- }
- };
-}
-
-into_fragment!(char);
-into_fragment!(bool);
-
-into_fragment!(u8);
-into_fragment!(u16);
-into_fragment!(u32);
-into_fragment!(u64);
-into_fragment!(u128);
-into_fragment!(usize);
-
-into_fragment!(i8);
-into_fragment!(i16);
-into_fragment!(i32);
-into_fragment!(i64);
-into_fragment!(i128);
-into_fragment!(isize);
-
-into_fragment!(f32);
-into_fragment!(f64);
diff --git a/core/src/widget/tree.rs b/core/src/widget/tree.rs
index 6b1a1309..2600cfc6 100644
--- a/core/src/widget/tree.rs
+++ b/core/src/widget/tree.rs
@@ -46,7 +46,7 @@ impl Tree {
}
}
- /// Reconciliates the current tree with the provided [`Widget`].
+ /// Reconciles the current tree with the provided [`Widget`].
///
/// If the tag of the [`Widget`] matches the tag of the [`Tree`], then the
/// [`Widget`] proceeds with the reconciliation (i.e. [`Widget::diff`] is called).
@@ -81,7 +81,7 @@ impl Tree {
);
}
- /// Reconciliates the children of the tree with the provided list of widgets using custom
+ /// Reconciles the children of the tree with the provided list of widgets using custom
/// logic both for diffing and creating new widget state.
pub fn diff_children_custom<T>(
&mut self,
@@ -107,7 +107,7 @@ impl Tree {
}
}
-/// Reconciliates the `current_children` with the provided list of widgets using
+/// Reconciles the `current_children` with the provided list of widgets using
/// custom logic both for diffing and creating new widget state.
///
/// The algorithm will try to minimize the impact of diffing by querying the
diff --git a/core/src/window/event.rs b/core/src/window/event.rs
index a14d127f..c9532e0d 100644
--- a/core/src/window/event.rs
+++ b/core/src/window/event.rs
@@ -23,20 +23,10 @@ pub enum Event {
Closed,
/// A window was moved.
- Moved {
- /// The new logical x location of the window
- x: i32,
- /// The new logical y location of the window
- y: i32,
- },
+ Moved(Point),
/// A window was resized.
- Resized {
- /// The new logical width of the window
- width: u32,
- /// The new logical height of the window
- height: u32,
- },
+ Resized(Size),
/// A window redraw was requested.
///
diff --git a/core/src/window/settings/linux.rs b/core/src/window/settings/linux.rs
index 009b9d9e..0a1e11cd 100644
--- a/core/src/window/settings/linux.rs
+++ b/core/src/window/settings/linux.rs
@@ -8,4 +8,10 @@ pub struct PlatformSpecific {
/// As a best practice, it is suggested to select an application id that match
/// the basename of the application’s .desktop file.
pub application_id: String,
+
+ /// Whether bypass the window manager mapping for x11 windows
+ ///
+ /// This flag is particularly useful for creating UI elements that need precise
+ /// positioning and immediate display without window manager interference.
+ pub override_redirect: bool,
}
diff --git a/core/src/window/settings/windows.rs b/core/src/window/settings/windows.rs
index 88fe2fbd..a47582a6 100644
--- a/core/src/window/settings/windows.rs
+++ b/core/src/window/settings/windows.rs
@@ -8,6 +8,12 @@ pub struct PlatformSpecific {
/// Whether show or hide the window icon in the taskbar.
pub skip_taskbar: bool,
+
+ /// Shows or hides the background drop shadow for undecorated windows.
+ ///
+ /// The shadow is hidden by default.
+ /// Enabling the shadow causes a thin 1px line to appear on the top of the window.
+ pub undecorated_shadow: bool,
}
impl Default for PlatformSpecific {
@@ -15,6 +21,7 @@ impl Default for PlatformSpecific {
Self {
drag_and_drop: true,
skip_taskbar: false,
+ undecorated_shadow: false,
}
}
}
diff --git a/examples/arc/src/main.rs b/examples/arc/src/main.rs
index b1e8402a..18873259 100644
--- a/examples/arc/src/main.rs
+++ b/examples/arc/src/main.rs
@@ -4,7 +4,7 @@ use iced::mouse;
use iced::widget::canvas::{
self, stroke, Cache, Canvas, Geometry, Path, Stroke,
};
-use iced::{Element, Length, Point, Rectangle, Renderer, Subscription, Theme};
+use iced::{Element, Fill, Point, Rectangle, Renderer, Subscription, Theme};
pub fn main() -> iced::Result {
iced::application("Arc - Iced", Arc::update, Arc::view)
@@ -30,10 +30,7 @@ impl Arc {
}
fn view(&self) -> Element<Message> {
- Canvas::new(self)
- .width(Length::Fill)
- .height(Length::Fill)
- .into()
+ Canvas::new(self).width(Fill).height(Fill).into()
}
fn subscription(&self) -> Subscription<Message> {
diff --git a/examples/bezier_tool/src/main.rs b/examples/bezier_tool/src/main.rs
index eaf84b97..949bfad7 100644
--- a/examples/bezier_tool/src/main.rs
+++ b/examples/bezier_tool/src/main.rs
@@ -1,7 +1,6 @@
//! This example showcases an interactive `Canvas` for drawing Bézier curves.
-use iced::alignment;
use iced::widget::{button, container, horizontal_space, hover};
-use iced::{Element, Length, Theme};
+use iced::{Element, Fill, Theme};
pub fn main() -> iced::Result {
iced::application("Bezier Tool - Iced", Example::update, Example::view)
@@ -48,8 +47,7 @@ impl Example {
.on_press(Message::Clear),
)
.padding(10)
- .width(Length::Fill)
- .align_x(alignment::Horizontal::Right)
+ .align_right(Fill)
},
))
.padding(20)
@@ -61,7 +59,7 @@ mod bezier {
use iced::mouse;
use iced::widget::canvas::event::{self, Event};
use iced::widget::canvas::{self, Canvas, Frame, Geometry, Path, Stroke};
- use iced::{Element, Length, Point, Rectangle, Renderer, Theme};
+ use iced::{Element, Fill, Point, Rectangle, Renderer, Theme};
#[derive(Default)]
pub struct State {
@@ -74,8 +72,8 @@ mod bezier {
state: self,
curves,
})
- .width(Length::Fill)
- .height(Length::Fill)
+ .width(Fill)
+ .height(Fill)
.into()
}
diff --git a/examples/clock/src/main.rs b/examples/clock/src/main.rs
index 4584a0c7..ef3064c7 100644
--- a/examples/clock/src/main.rs
+++ b/examples/clock/src/main.rs
@@ -4,7 +4,7 @@ use iced::time;
use iced::widget::canvas::{stroke, Cache, Geometry, LineCap, Path, Stroke};
use iced::widget::{canvas, container};
use iced::{
- Degrees, Element, Font, Length, Point, Rectangle, Renderer, Subscription,
+ Degrees, Element, Fill, Font, Point, Rectangle, Renderer, Subscription,
Theme, Vector,
};
@@ -43,15 +43,9 @@ impl Clock {
}
fn view(&self) -> Element<Message> {
- let canvas = canvas(self as &Self)
- .width(Length::Fill)
- .height(Length::Fill);
-
- container(canvas)
- .width(Length::Fill)
- .height(Length::Fill)
- .padding(20)
- .into()
+ let canvas = canvas(self as &Self).width(Fill).height(Fill);
+
+ container(canvas).padding(20).into()
}
fn subscription(&self) -> Subscription<Message> {
diff --git a/examples/color_palette/src/main.rs b/examples/color_palette/src/main.rs
index e4b19731..7f21003b 100644
--- a/examples/color_palette/src/main.rs
+++ b/examples/color_palette/src/main.rs
@@ -1,10 +1,10 @@
-use iced::alignment::{self, Alignment};
+use iced::alignment;
use iced::mouse;
use iced::widget::canvas::{self, Canvas, Frame, Geometry, Path};
use iced::widget::{column, row, text, Slider};
use iced::{
- Color, Element, Font, Length, Pixels, Point, Rectangle, Renderer, Size,
- Vector,
+ Center, Color, Element, Fill, Font, Pixels, Point, Rectangle, Renderer,
+ Size, Vector,
};
use palette::{convert::FromColor, rgb::Rgb, Darken, Hsl, Lighten, ShiftHue};
use std::marker::PhantomData;
@@ -150,10 +150,7 @@ impl Theme {
}
pub fn view(&self) -> Element<Message> {
- Canvas::new(self)
- .width(Length::Fill)
- .height(Length::Fill)
- .into()
+ Canvas::new(self).width(Fill).height(Fill).into()
}
fn draw(&self, frame: &mut Frame, text_color: Color) {
@@ -320,7 +317,7 @@ impl<C: ColorSpace + Copy> ColorPicker<C> {
text(color.to_string()).width(185).size(12),
]
.spacing(10)
- .align_items(Alignment::Center)
+ .align_y(Center)
.into()
}
}
diff --git a/examples/combo_box/src/main.rs b/examples/combo_box/src/main.rs
index ff759ab4..af53b17a 100644
--- a/examples/combo_box/src/main.rs
+++ b/examples/combo_box/src/main.rs
@@ -1,7 +1,7 @@
use iced::widget::{
center, column, combo_box, scrollable, text, vertical_space,
};
-use iced::{Alignment, Element, Length};
+use iced::{Center, Element, Fill};
pub fn main() -> iced::Result {
iced::run("Combo Box - Iced", Example::update, Example::view)
@@ -64,8 +64,8 @@ impl Example {
combo_box,
vertical_space().height(150),
]
- .width(Length::Fill)
- .align_items(Alignment::Center)
+ .width(Fill)
+ .align_x(Center)
.spacing(10);
center(scrollable(content)).into()
diff --git a/examples/component/src/main.rs b/examples/component/src/main.rs
deleted file mode 100644
index 5625f12a..00000000
--- a/examples/component/src/main.rs
+++ /dev/null
@@ -1,156 +0,0 @@
-use iced::widget::center;
-use iced::Element;
-
-use numeric_input::numeric_input;
-
-pub fn main() -> iced::Result {
- iced::run("Component - Iced", Component::update, Component::view)
-}
-
-#[derive(Default)]
-struct Component {
- value: Option<u32>,
-}
-
-#[derive(Debug, Clone, Copy)]
-enum Message {
- NumericInputChanged(Option<u32>),
-}
-
-impl Component {
- fn update(&mut self, message: Message) {
- match message {
- Message::NumericInputChanged(value) => {
- self.value = value;
- }
- }
- }
-
- fn view(&self) -> Element<Message> {
- center(numeric_input(self.value, Message::NumericInputChanged))
- .padding(20)
- .into()
- }
-}
-
-mod numeric_input {
- use iced::alignment::{self, Alignment};
- use iced::widget::{button, component, row, text, text_input, Component};
- use iced::{Element, Length, Size};
-
- pub struct NumericInput<Message> {
- value: Option<u32>,
- on_change: Box<dyn Fn(Option<u32>) -> Message>,
- }
-
- pub fn numeric_input<Message>(
- value: Option<u32>,
- on_change: impl Fn(Option<u32>) -> Message + 'static,
- ) -> NumericInput<Message> {
- NumericInput::new(value, on_change)
- }
-
- #[derive(Debug, Clone)]
- pub enum Event {
- InputChanged(String),
- IncrementPressed,
- DecrementPressed,
- }
-
- impl<Message> NumericInput<Message> {
- pub fn new(
- value: Option<u32>,
- on_change: impl Fn(Option<u32>) -> Message + 'static,
- ) -> Self {
- Self {
- value,
- on_change: Box::new(on_change),
- }
- }
- }
-
- impl<Message, Theme> Component<Message, Theme> for NumericInput<Message>
- where
- Theme: text::Catalog + button::Catalog + text_input::Catalog + 'static,
- {
- type State = ();
- type Event = Event;
-
- fn update(
- &mut self,
- _state: &mut Self::State,
- event: Event,
- ) -> Option<Message> {
- match event {
- Event::IncrementPressed => Some((self.on_change)(Some(
- self.value.unwrap_or_default().saturating_add(1),
- ))),
- Event::DecrementPressed => Some((self.on_change)(Some(
- self.value.unwrap_or_default().saturating_sub(1),
- ))),
- Event::InputChanged(value) => {
- if value.is_empty() {
- Some((self.on_change)(None))
- } else {
- value
- .parse()
- .ok()
- .map(Some)
- .map(self.on_change.as_ref())
- }
- }
- }
- }
-
- fn view(&self, _state: &Self::State) -> Element<'_, Event, Theme> {
- let button = |label, on_press| {
- button(
- text(label)
- .width(Length::Fill)
- .height(Length::Fill)
- .horizontal_alignment(alignment::Horizontal::Center)
- .vertical_alignment(alignment::Vertical::Center),
- )
- .width(40)
- .height(40)
- .on_press(on_press)
- };
-
- row![
- button("-", Event::DecrementPressed),
- text_input(
- "Type a number",
- self.value
- .as_ref()
- .map(u32::to_string)
- .as_deref()
- .unwrap_or(""),
- )
- .on_input(Event::InputChanged)
- .padding(10),
- button("+", Event::IncrementPressed),
- ]
- .align_items(Alignment::Center)
- .spacing(10)
- .into()
- }
-
- fn size_hint(&self) -> Size<Length> {
- Size {
- width: Length::Fill,
- height: Length::Shrink,
- }
- }
- }
-
- impl<'a, Message, Theme> From<NumericInput<Message>>
- for Element<'a, Message, Theme>
- where
- Theme: text::Catalog + button::Catalog + text_input::Catalog + 'static,
- Message: 'a,
- {
- fn from(numeric_input: NumericInput<Message>) -> Self {
- component(numeric_input)
- }
- }
-}
diff --git a/examples/counter/src/main.rs b/examples/counter/src/main.rs
index 0dd7a976..81684c1c 100644
--- a/examples/counter/src/main.rs
+++ b/examples/counter/src/main.rs
@@ -1,5 +1,5 @@
use iced::widget::{button, column, text, Column};
-use iced::Alignment;
+use iced::Center;
pub fn main() -> iced::Result {
iced::run("A cool counter", Counter::update, Counter::view)
@@ -35,6 +35,6 @@ impl Counter {
button("Decrement").on_press(Message::Decrement)
]
.padding(20)
- .align_items(Alignment::Center)
+ .align_x(Center)
}
}
diff --git a/examples/custom_quad/src/main.rs b/examples/custom_quad/src/main.rs
index b53a40d6..dc425cc6 100644
--- a/examples/custom_quad/src/main.rs
+++ b/examples/custom_quad/src/main.rs
@@ -3,12 +3,13 @@ mod quad {
use iced::advanced::layout::{self, Layout};
use iced::advanced::renderer;
use iced::advanced::widget::{self, Widget};
+ use iced::border;
use iced::mouse;
use iced::{Border, Color, Element, Length, Rectangle, Shadow, Size};
pub struct CustomQuad {
size: f32,
- radius: [f32; 4],
+ radius: border::Radius,
border_width: f32,
shadow: Shadow,
}
@@ -16,7 +17,7 @@ mod quad {
impl CustomQuad {
pub fn new(
size: f32,
- radius: [f32; 4],
+ radius: border::Radius,
border_width: f32,
shadow: Shadow,
) -> Self {
@@ -63,7 +64,7 @@ mod quad {
renderer::Quad {
bounds: layout.bounds(),
border: Border {
- radius: self.radius.into(),
+ radius: self.radius,
width: self.border_width,
color: Color::from_rgb(1.0, 0.0, 0.0),
},
@@ -81,15 +82,16 @@ mod quad {
}
}
+use iced::border;
use iced::widget::{center, column, slider, text};
-use iced::{Alignment, Color, Element, Shadow, Vector};
+use iced::{Center, Color, Element, Shadow, Vector};
pub fn main() -> iced::Result {
iced::run("Custom Quad - Iced", Example::update, Example::view)
}
struct Example {
- radius: [f32; 4],
+ radius: border::Radius,
border_width: f32,
shadow: Shadow,
}
@@ -110,7 +112,7 @@ enum Message {
impl Example {
fn new() -> Self {
Self {
- radius: [50.0; 4],
+ radius: border::radius(50),
border_width: 0.0,
shadow: Shadow {
color: Color::from_rgba(0.0, 0.0, 0.0, 0.8),
@@ -121,19 +123,18 @@ impl Example {
}
fn update(&mut self, message: Message) {
- let [tl, tr, br, bl] = self.radius;
match message {
Message::RadiusTopLeftChanged(radius) => {
- self.radius = [radius, tr, br, bl];
+ self.radius = self.radius.top_left(radius);
}
Message::RadiusTopRightChanged(radius) => {
- self.radius = [tl, radius, br, bl];
+ self.radius = self.radius.top_right(radius);
}
Message::RadiusBottomRightChanged(radius) => {
- self.radius = [tl, tr, radius, bl];
+ self.radius = self.radius.bottom_right(radius);
}
Message::RadiusBottomLeftChanged(radius) => {
- self.radius = [tl, tr, br, radius];
+ self.radius = self.radius.bottom_left(radius);
}
Message::BorderWidthChanged(width) => {
self.border_width = width;
@@ -151,7 +152,13 @@ impl Example {
}
fn view(&self) -> Element<Message> {
- let [tl, tr, br, bl] = self.radius;
+ let border::Radius {
+ top_left,
+ top_right,
+ bottom_right,
+ bottom_left,
+ } = self.radius;
+
let Shadow {
offset: Vector { x: sx, y: sy },
blur_radius: sr,
@@ -165,12 +172,12 @@ impl Example {
self.border_width,
self.shadow
),
- text!("Radius: {tl:.2}/{tr:.2}/{br:.2}/{bl:.2}"),
- slider(1.0..=100.0, tl, Message::RadiusTopLeftChanged).step(0.01),
- slider(1.0..=100.0, tr, Message::RadiusTopRightChanged).step(0.01),
- slider(1.0..=100.0, br, Message::RadiusBottomRightChanged)
+ text!("Radius: {top_left:.2}/{top_right:.2}/{bottom_right:.2}/{bottom_left:.2}"),
+ slider(1.0..=100.0, top_left, Message::RadiusTopLeftChanged).step(0.01),
+ slider(1.0..=100.0, top_right, Message::RadiusTopRightChanged).step(0.01),
+ slider(1.0..=100.0, bottom_right, Message::RadiusBottomRightChanged)
.step(0.01),
- slider(1.0..=100.0, bl, Message::RadiusBottomLeftChanged)
+ slider(1.0..=100.0, bottom_left, Message::RadiusBottomLeftChanged)
.step(0.01),
slider(1.0..=10.0, self.border_width, Message::BorderWidthChanged)
.step(0.01),
@@ -185,7 +192,7 @@ impl Example {
.padding(20)
.spacing(20)
.max_width(500)
- .align_items(Alignment::Center);
+ .align_x(Center);
center(content).into()
}
diff --git a/examples/custom_shader/src/main.rs b/examples/custom_shader/src/main.rs
index b04a8183..5886f6bb 100644
--- a/examples/custom_shader/src/main.rs
+++ b/examples/custom_shader/src/main.rs
@@ -6,7 +6,7 @@ use iced::time::Instant;
use iced::widget::shader::wgpu;
use iced::widget::{center, checkbox, column, row, shader, slider, text};
use iced::window;
-use iced::{Alignment, Color, Element, Length, Subscription};
+use iced::{Center, Color, Element, Fill, Subscription};
fn main() -> iced::Result {
iced::application(
@@ -122,12 +122,11 @@ impl IcedCubes {
let controls = column![top_controls, bottom_controls,]
.spacing(10)
.padding(20)
- .align_items(Alignment::Center);
+ .align_x(Center);
- let shader =
- shader(&self.scene).width(Length::Fill).height(Length::Fill);
+ let shader = shader(&self.scene).width(Fill).height(Fill);
- center(column![shader, controls].align_items(Alignment::Center)).into()
+ center(column![shader, controls].align_x(Center)).into()
}
fn subscription(&self) -> Subscription<Message> {
diff --git a/examples/custom_widget/src/main.rs b/examples/custom_widget/src/main.rs
index 3cf10e22..58f3c54a 100644
--- a/examples/custom_widget/src/main.rs
+++ b/examples/custom_widget/src/main.rs
@@ -1,19 +1,11 @@
//! This example showcases a simple native custom widget that draws a circle.
mod circle {
- // For now, to implement a custom native widget you will need to add
- // `iced_native` and `iced_wgpu` to your dependencies.
- //
- // Then, you simply need to define your widget type and implement the
- // `iced_native::Widget` trait with the `iced_wgpu::Renderer`.
- //
- // Of course, you can choose to make the implementation renderer-agnostic,
- // if you wish to, by creating your own `Renderer` trait, which could be
- // implemented by `iced_wgpu` and other renderers.
use iced::advanced::layout::{self, Layout};
use iced::advanced::renderer;
use iced::advanced::widget::{self, Widget};
+ use iced::border;
use iced::mouse;
- use iced::{Border, Color, Element, Length, Rectangle, Size};
+ use iced::{Color, Element, Length, Rectangle, Size};
pub struct Circle {
radius: f32,
@@ -62,7 +54,7 @@ mod circle {
renderer.fill_quad(
renderer::Quad {
bounds: layout.bounds(),
- border: Border::rounded(self.radius),
+ border: border::rounded(self.radius),
..renderer::Quad::default()
},
Color::BLACK,
@@ -83,7 +75,7 @@ mod circle {
use circle::circle;
use iced::widget::{center, column, slider, text};
-use iced::{Alignment, Element};
+use iced::{Center, Element};
pub fn main() -> iced::Result {
iced::run("Custom Widget - Iced", Example::update, Example::view)
@@ -120,7 +112,7 @@ impl Example {
.padding(20)
.spacing(20)
.max_width(500)
- .align_items(Alignment::Center);
+ .align_x(Center);
center(content).into()
}
diff --git a/examples/download_progress/Cargo.toml b/examples/download_progress/Cargo.toml
index 18a49f66..61a1b257 100644
--- a/examples/download_progress/Cargo.toml
+++ b/examples/download_progress/Cargo.toml
@@ -10,6 +10,6 @@ iced.workspace = true
iced.features = ["tokio"]
[dependencies.reqwest]
-version = "0.11"
+version = "0.12"
default-features = false
-features = ["rustls-tls"]
+features = ["stream", "rustls-tls"]
diff --git a/examples/download_progress/index.html b/examples/download_progress/index.html
new file mode 100644
index 00000000..c79e32c1
--- /dev/null
+++ b/examples/download_progress/index.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html lang="en" style="height: 100%">
+<head>
+ <meta charset="utf-8" content="text/html; charset=utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <title>Download_Progress - Iced</title>
+ <base data-trunk-public-url />
+</head>
+<body style="height: 100%; margin: 0">
+<link data-trunk rel="rust" href="Cargo.toml" data-wasm-opt="z" data-bin="download_progress" />
+</body>
+</html>
diff --git a/examples/download_progress/src/download.rs b/examples/download_progress/src/download.rs
index d6cc1e24..a8e7b404 100644
--- a/examples/download_progress/src/download.rs
+++ b/examples/download_progress/src/download.rs
@@ -1,85 +1,62 @@
-use iced::subscription;
+use iced::futures::{SinkExt, Stream, StreamExt};
+use iced::stream::try_channel;
+use iced::Subscription;
use std::hash::Hash;
+use std::sync::Arc;
// Just a little utility function
pub fn file<I: 'static + Hash + Copy + Send + Sync, T: ToString>(
id: I,
url: T,
-) -> iced::Subscription<(I, Progress)> {
- subscription::unfold(id, State::Ready(url.to_string()), move |state| {
- download(id, state)
- })
+) -> iced::Subscription<(I, Result<Progress, Error>)> {
+ Subscription::run_with_id(
+ id,
+ download(url.to_string()).map(move |progress| (id, progress)),
+ )
}
-async fn download<I: Copy>(id: I, state: State) -> ((I, Progress), State) {
- match state {
- State::Ready(url) => {
- let response = reqwest::get(&url).await;
+fn download(url: String) -> impl Stream<Item = Result<Progress, Error>> {
+ try_channel(1, move |mut output| async move {
+ let response = reqwest::get(&url).await?;
+ let total = response.content_length().ok_or(Error::NoContentLength)?;
- match response {
- Ok(response) => {
- if let Some(total) = response.content_length() {
- (
- (id, Progress::Started),
- State::Downloading {
- response,
- total,
- downloaded: 0,
- },
- )
- } else {
- ((id, Progress::Errored), State::Finished)
- }
- }
- Err(_) => ((id, Progress::Errored), State::Finished),
- }
- }
- State::Downloading {
- mut response,
- total,
- downloaded,
- } => match response.chunk().await {
- Ok(Some(chunk)) => {
- let downloaded = downloaded + chunk.len() as u64;
+ let _ = output.send(Progress::Downloading { percent: 0.0 }).await;
+
+ let mut byte_stream = response.bytes_stream();
+ let mut downloaded = 0;
- let percentage = (downloaded as f32 / total as f32) * 100.0;
+ while let Some(next_bytes) = byte_stream.next().await {
+ let bytes = next_bytes?;
+ downloaded += bytes.len();
- (
- (id, Progress::Advanced(percentage)),
- State::Downloading {
- response,
- total,
- downloaded,
- },
- )
- }
- Ok(None) => ((id, Progress::Finished), State::Finished),
- Err(_) => ((id, Progress::Errored), State::Finished),
- },
- State::Finished => {
- // We do not let the stream die, as it would start a
- // new download repeatedly if the user is not careful
- // in case of errors.
- iced::futures::future::pending().await
+ let _ = output
+ .send(Progress::Downloading {
+ percent: 100.0 * downloaded as f32 / total as f32,
+ })
+ .await;
}
- }
+
+ let _ = output.send(Progress::Finished).await;
+
+ Ok(())
+ })
}
#[derive(Debug, Clone)]
pub enum Progress {
- Started,
- Advanced(f32),
+ Downloading { percent: f32 },
Finished,
- Errored,
}
-pub enum State {
- Ready(String),
- Downloading {
- response: reqwest::Response,
- total: u64,
- downloaded: u64,
- },
- Finished,
+#[derive(Debug, Clone)]
+pub enum Error {
+ RequestFailed(Arc<reqwest::Error>),
+ NoContentLength,
+}
+
+impl From<reqwest::Error> for Error {
+ fn from(error: reqwest::Error) -> Self {
+ Error::RequestFailed(Arc::new(error))
+ }
}
diff --git a/examples/download_progress/src/main.rs b/examples/download_progress/src/main.rs
index d91e5eab..bcc01606 100644
--- a/examples/download_progress/src/main.rs
+++ b/examples/download_progress/src/main.rs
@@ -1,7 +1,7 @@
mod download;
use iced::widget::{button, center, column, progress_bar, text, Column};
-use iced::{Alignment, Element, Subscription};
+use iced::{Center, Element, Right, Subscription};
pub fn main() -> iced::Result {
iced::application(
@@ -23,7 +23,7 @@ struct Example {
pub enum Message {
Add,
Download(usize),
- DownloadProgressed((usize, download::Progress)),
+ DownloadProgressed((usize, Result<download::Progress, download::Error>)),
}
impl Example {
@@ -69,7 +69,7 @@ impl Example {
.padding(10),
)
.spacing(20)
- .align_items(Alignment::End);
+ .align_x(Right);
center(downloads).padding(20).into()
}
@@ -114,19 +114,19 @@ impl Download {
}
}
- pub fn progress(&mut self, new_progress: download::Progress) {
+ pub fn progress(
+ &mut self,
+ new_progress: Result<download::Progress, download::Error>,
+ ) {
if let State::Downloading { progress } = &mut self.state {
match new_progress {
- download::Progress::Started => {
- *progress = 0.0;
+ Ok(download::Progress::Downloading { percent }) => {
+ *progress = percent;
}
- download::Progress::Advanced(percentage) => {
- *progress = percentage;
- }
- download::Progress::Finished => {
+ Ok(download::Progress::Finished) => {
self.state = State::Finished;
}
- download::Progress::Errored => {
+ Err(_error) => {
self.state = State::Errored;
}
}
@@ -136,7 +136,7 @@ impl Download {
pub fn subscription(&self) -> Subscription<Message> {
match self.state {
State::Downloading { .. } => {
- download::file(self.id, "https://speed.hetzner.de/100MB.bin?")
+ download::file(self.id, "https://huggingface.co/mattshumer/Reflection-Llama-3.1-70B/resolve/main/model-00001-of-00162.safetensors")
.map(Message::DownloadProgressed)
}
_ => Subscription::none(),
@@ -160,7 +160,7 @@ impl Download {
State::Finished => {
column!["Download finished!", button("Start again")]
.spacing(10)
- .align_items(Alignment::Center)
+ .align_x(Center)
.into()
}
State::Downloading { .. } => {
@@ -171,14 +171,14 @@ impl Download {
button("Try again").on_press(Message::Download(self.id)),
]
.spacing(10)
- .align_items(Alignment::Center)
+ .align_x(Center)
.into(),
};
Column::new()
.spacing(10)
.padding(10)
- .align_items(Alignment::Center)
+ .align_x(Center)
.push(progress_bar)
.push(control)
.into()
diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs
index bed9d94a..d55f9bdf 100644
--- a/examples/editor/src/main.rs
+++ b/examples/editor/src/main.rs
@@ -1,10 +1,10 @@
-use iced::highlighter::{self, Highlighter};
+use iced::highlighter;
use iced::keyboard;
use iced::widget::{
- button, column, container, horizontal_space, pick_list, row, text,
- text_editor, tooltip,
+ self, button, column, container, horizontal_space, pick_list, row, text,
+ text_editor, toggler, tooltip,
};
-use iced::{Alignment, Element, Font, Length, Subscription, Task, Theme};
+use iced::{Center, Element, Fill, Font, Task, Theme};
use std::ffi;
use std::io;
@@ -13,18 +13,17 @@ use std::sync::Arc;
pub fn main() -> iced::Result {
iced::application("Editor - Iced", Editor::update, Editor::view)
- .load(Editor::load)
- .subscription(Editor::subscription)
.theme(Editor::theme)
.font(include_bytes!("../fonts/icons.ttf").as_slice())
.default_font(Font::MONOSPACE)
- .run()
+ .run_with(Editor::new)
}
struct Editor {
file: Option<PathBuf>,
content: text_editor::Content,
theme: highlighter::Theme,
+ word_wrap: bool,
is_loading: bool,
is_dirty: bool,
}
@@ -33,6 +32,7 @@ struct Editor {
enum Message {
ActionPerformed(text_editor::Action),
ThemeSelected(highlighter::Theme),
+ WordWrapToggled(bool),
NewFile,
OpenFile,
FileOpened(Result<(PathBuf, Arc<String>), Error>),
@@ -41,20 +41,26 @@ enum Message {
}
impl Editor {
- fn new() -> Self {
- Self {
- file: None,
- content: text_editor::Content::new(),
- theme: highlighter::Theme::SolarizedDark,
- is_loading: true,
- is_dirty: false,
- }
- }
-
- fn load() -> Task<Message> {
- Task::perform(
- load_file(format!("{}/src/main.rs", env!("CARGO_MANIFEST_DIR"))),
- Message::FileOpened,
+ fn new() -> (Self, Task<Message>) {
+ (
+ Self {
+ file: None,
+ content: text_editor::Content::new(),
+ theme: highlighter::Theme::SolarizedDark,
+ word_wrap: true,
+ is_loading: true,
+ is_dirty: false,
+ },
+ Task::batch([
+ Task::perform(
+ load_file(format!(
+ "{}/src/main.rs",
+ env!("CARGO_MANIFEST_DIR")
+ )),
+ Message::FileOpened,
+ ),
+ widget::focus_next(),
+ ]),
)
}
@@ -72,6 +78,11 @@ impl Editor {
Task::none()
}
+ Message::WordWrapToggled(word_wrap) => {
+ self.word_wrap = word_wrap;
+
+ Task::none()
+ }
Message::NewFile => {
if !self.is_loading {
self.file = None;
@@ -125,15 +136,6 @@ impl Editor {
}
}
- fn subscription(&self) -> Subscription<Message> {
- keyboard::on_key_press(|key, modifiers| match key.as_ref() {
- keyboard::Key::Character("s") if modifiers.command() => {
- Some(Message::SaveFile)
- }
- _ => None,
- })
- }
-
fn view(&self) -> Element<Message> {
let controls = row![
action(new_icon(), "New file", Some(Message::NewFile)),
@@ -148,6 +150,9 @@ impl Editor {
self.is_dirty.then_some(Message::SaveFile)
),
horizontal_space(),
+ toggler(self.word_wrap)
+ .label("Word Wrap")
+ .on_toggle(Message::WordWrapToggled),
pick_list(
highlighter::Theme::ALL,
Some(self.theme),
@@ -157,7 +162,7 @@ impl Editor {
.padding([5, 10])
]
.spacing(10)
- .align_items(Alignment::Center);
+ .align_y(Center);
let status = row![
text(if let Some(path) = &self.file {
@@ -183,21 +188,33 @@ impl Editor {
column![
controls,
text_editor(&self.content)
- .height(Length::Fill)
+ .height(Fill)
.on_action(Message::ActionPerformed)
- .highlight::<Highlighter>(
- highlighter::Settings {
- theme: self.theme,
- extension: self
- .file
- .as_deref()
- .and_then(Path::extension)
- .and_then(ffi::OsStr::to_str)
- .map(str::to_string)
- .unwrap_or(String::from("rs")),
- },
- |highlight, _theme| highlight.to_format()
- ),
+ .wrapping(if self.word_wrap {
+ text::Wrapping::Word
+ } else {
+ text::Wrapping::None
+ })
+ .highlight(
+ self.file
+ .as_deref()
+ .and_then(Path::extension)
+ .and_then(ffi::OsStr::to_str)
+ .unwrap_or("rs"),
+ self.theme,
+ )
+ .key_binding(|key_press| {
+ match key_press.key.as_ref() {
+ keyboard::Key::Character("s")
+ if key_press.modifiers.command() =>
+ {
+ Some(text_editor::Binding::Custom(
+ Message::SaveFile,
+ ))
+ }
+ _ => text_editor::Binding::from_key_press(key_press),
+ }
+ }),
status,
]
.spacing(10)
@@ -214,12 +231,6 @@ impl Editor {
}
}
-impl Default for Editor {
- fn default() -> Self {
- Self::new()
- }
-}
-
#[derive(Debug, Clone)]
pub enum Error {
DialogClosed,
diff --git a/examples/events/src/main.rs b/examples/events/src/main.rs
index 2cd3c5d8..5bada9b5 100644
--- a/examples/events/src/main.rs
+++ b/examples/events/src/main.rs
@@ -1,8 +1,7 @@
-use iced::alignment;
use iced::event::{self, Event};
use iced::widget::{button, center, checkbox, text, Column};
use iced::window;
-use iced::{Alignment, Element, Length, Subscription, Task};
+use iced::{Center, Element, Fill, Subscription, Task};
pub fn main() -> iced::Result {
iced::application("Events - Iced", Events::update, Events::view)
@@ -67,17 +66,13 @@ impl Events {
let toggle = checkbox("Listen to runtime events", self.enabled)
.on_toggle(Message::Toggled);
- let exit = button(
- text("Exit")
- .width(Length::Fill)
- .horizontal_alignment(alignment::Horizontal::Center),
- )
- .width(100)
- .padding(10)
- .on_press(Message::Exit);
+ let exit = button(text("Exit").width(Fill).align_x(Center))
+ .width(100)
+ .padding(10)
+ .on_press(Message::Exit);
let content = Column::new()
- .align_items(Alignment::Center)
+ .align_x(Center)
.spacing(20)
.push(events)
.push(toggle)
diff --git a/examples/exit/src/main.rs b/examples/exit/src/main.rs
index 1f108df2..48b0864c 100644
--- a/examples/exit/src/main.rs
+++ b/examples/exit/src/main.rs
@@ -1,6 +1,6 @@
use iced::widget::{button, center, column};
use iced::window;
-use iced::{Alignment, Element, Task};
+use iced::{Center, Element, Task};
pub fn main() -> iced::Result {
iced::application("Exit - Iced", Exit::update, Exit::view).run()
@@ -44,7 +44,7 @@ impl Exit {
]
}
.spacing(10)
- .align_items(Alignment::Center);
+ .align_x(Center);
center(content).padding(20).into()
}
diff --git a/examples/ferris/src/main.rs b/examples/ferris/src/main.rs
index 88006898..eaf51354 100644
--- a/examples/ferris/src/main.rs
+++ b/examples/ferris/src/main.rs
@@ -4,8 +4,8 @@ use iced::widget::{
};
use iced::window;
use iced::{
- Alignment, Color, ContentFit, Degrees, Element, Length, Radians, Rotation,
- Subscription, Theme,
+ Bottom, Center, Color, ContentFit, Degrees, Element, Fill, Radians,
+ Rotation, Subscription, Theme,
};
pub fn main() -> iced::Result {
@@ -108,7 +108,7 @@ impl Image {
"I am Ferris!"
]
.spacing(20)
- .align_items(Alignment::Center);
+ .align_x(Center);
let fit = row![
pick_list(
@@ -122,7 +122,7 @@ impl Image {
Some(self.content_fit),
Message::ContentFitChanged
)
- .width(Length::Fill),
+ .width(Fill),
pick_list(
[RotationStrategy::Floating, RotationStrategy::Solid],
Some(match self.rotation {
@@ -131,10 +131,10 @@ impl Image {
}),
Message::RotationStrategyChanged,
)
- .width(Length::Fill),
+ .width(Fill),
]
.spacing(10)
- .align_items(Alignment::End);
+ .align_y(Bottom);
let properties = row![
with_value(
@@ -159,12 +159,12 @@ impl Image {
.size(12)
]
.spacing(10)
- .align_items(Alignment::Center),
+ .align_y(Center),
format!("Rotation: {:.0}°", f32::from(self.rotation.degrees()))
)
]
.spacing(10)
- .align_items(Alignment::End);
+ .align_y(Bottom);
container(column![fit, center(i_am_ferris), properties].spacing(10))
.padding(10)
@@ -206,6 +206,6 @@ fn with_value<'a>(
) -> Element<'a, Message> {
column![control.into(), text(value).size(12).line_height(1.0)]
.spacing(2)
- .align_items(Alignment::Center)
+ .align_x(Center)
.into()
}
diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs
index 421f862a..9dcebecc 100644
--- a/examples/game_of_life/src/main.rs
+++ b/examples/game_of_life/src/main.rs
@@ -9,7 +9,7 @@ use iced::time;
use iced::widget::{
button, checkbox, column, container, pick_list, row, slider, text,
};
-use iced::{Alignment, Element, Length, Subscription, Task, Theme};
+use iced::{Center, Element, Fill, Subscription, Task, Theme};
use std::time::Duration;
pub fn main() -> iced::Result {
@@ -135,12 +135,9 @@ impl GameOfLife {
.map(move |message| Message::Grid(message, version)),
controls,
]
- .height(Length::Fill);
+ .height(Fill);
- container(content)
- .width(Length::Fill)
- .height(Length::Fill)
- .into()
+ container(content).width(Fill).height(Fill).into()
}
}
@@ -169,7 +166,7 @@ fn view_controls<'a>(
slider(1.0..=1000.0, speed as f32, Message::SpeedChanged),
text!("x{speed}").size(16),
]
- .align_items(Alignment::Center)
+ .align_y(Center)
.spacing(10);
row![
@@ -186,7 +183,7 @@ fn view_controls<'a>(
]
.padding(10)
.spacing(20)
- .align_items(Alignment::Center)
+ .align_y(Center)
.into()
}
@@ -199,7 +196,7 @@ mod grid {
use iced::widget::canvas::event::{self, Event};
use iced::widget::canvas::{Cache, Canvas, Frame, Geometry, Path, Text};
use iced::{
- Color, Element, Length, Point, Rectangle, Renderer, Size, Theme, Vector,
+ Color, Element, Fill, Point, Rectangle, Renderer, Size, Theme, Vector,
};
use rustc_hash::{FxHashMap, FxHashSet};
use std::future::Future;
@@ -333,10 +330,7 @@ mod grid {
}
pub fn view(&self) -> Element<Message> {
- Canvas::new(self)
- .width(Length::Fill)
- .height(Length::Fill)
- .into()
+ Canvas::new(self).width(Fill).height(Fill).into()
}
pub fn clear(&mut self) {
diff --git a/examples/gradient/src/main.rs b/examples/gradient/src/main.rs
index e5b19443..b2de069f 100644
--- a/examples/gradient/src/main.rs
+++ b/examples/gradient/src/main.rs
@@ -3,7 +3,7 @@ use iced::gradient;
use iced::widget::{
checkbox, column, container, horizontal_space, row, slider, text,
};
-use iced::{Alignment, Color, Element, Length, Radians, Theme};
+use iced::{Center, Color, Element, Fill, Radians, Theme};
pub fn main() -> iced::Result {
tracing_subscriber::fmt::init();
@@ -67,8 +67,8 @@ impl Gradient {
gradient.into()
})
- .width(Length::Fill)
- .height(Length::Fill);
+ .width(Fill)
+ .height(Fill);
let angle_picker = row![
text("Angle").width(64),
@@ -77,7 +77,7 @@ impl Gradient {
]
.spacing(8)
.padding(8)
- .align_items(Alignment::Center);
+ .align_y(Center);
let transparency_toggle = iced::widget::Container::new(
checkbox("Transparent window", transparent)
@@ -129,6 +129,6 @@ fn color_picker(label: &str, color: Color) -> Element<'_, Color> {
]
.spacing(8)
.padding(8)
- .align_items(Alignment::Center)
+ .align_y(Center)
.into()
}
diff --git a/examples/integration/src/controls.rs b/examples/integration/src/controls.rs
index d0654996..0b11a323 100644
--- a/examples/integration/src/controls.rs
+++ b/examples/integration/src/controls.rs
@@ -1,7 +1,6 @@
use iced_wgpu::Renderer;
use iced_widget::{column, container, row, slider, text, text_input};
-use iced_winit::core::alignment;
-use iced_winit::core::{Color, Element, Length, Theme};
+use iced_winit::core::{Color, Element, Length::*, Theme};
use iced_winit::runtime::{Program, Task};
pub struct Controls {
@@ -86,8 +85,7 @@ impl Program for Controls {
.spacing(10),
)
.padding(10)
- .height(Length::Fill)
- .align_y(alignment::Vertical::Bottom)
+ .align_bottom(Fill)
.into()
}
}
diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs
index 9818adf3..5b64cbd1 100644
--- a/examples/integration/src/main.rs
+++ b/examples/integration/src/main.rs
@@ -68,7 +68,7 @@ pub fn main() -> Result<(), winit::error::EventLoopError> {
Size::new(physical_size.width, physical_size.height),
window.scale_factor(),
);
- let clipboard = Clipboard::connect(&window);
+ let clipboard = Clipboard::connect(window.clone());
let backend =
wgpu::util::backend_bits_from_env().unwrap_or_default();
diff --git a/examples/layout/src/main.rs b/examples/layout/src/main.rs
index 2e774415..cb33369b 100644
--- a/examples/layout/src/main.rs
+++ b/examples/layout/src/main.rs
@@ -1,3 +1,4 @@
+use iced::border;
use iced::keyboard;
use iced::mouse;
use iced::widget::{
@@ -5,7 +6,7 @@ use iced::widget::{
pick_list, row, scrollable, text,
};
use iced::{
- color, Alignment, Element, Font, Length, Point, Rectangle, Renderer,
+ color, Center, Element, Fill, Font, Length, Point, Rectangle, Renderer,
Subscription, Theme,
};
@@ -74,7 +75,7 @@ impl Layout {
pick_list(Theme::ALL, Some(&self.theme), Message::ThemeSelected),
]
.spacing(20)
- .align_items(Alignment::Center);
+ .align_y(Center);
let example = center(if self.explain {
self.example.view().explain(color!(0x0000ff))
@@ -85,7 +86,7 @@ impl Layout {
let palette = theme.extended_palette();
container::Style::default()
- .with_border(palette.background.strong.color, 4.0)
+ .border(border::color(palette.background.strong.color).width(4))
})
.padding(4);
@@ -234,13 +235,13 @@ fn application<'a>() -> Element<'a, Message> {
square(40),
]
.padding(10)
- .align_items(Alignment::Center),
+ .align_y(Center),
)
.style(|theme| {
let palette = theme.extended_palette();
container::Style::default()
- .with_border(palette.background.strong.color, 1)
+ .border(border::color(palette.background.strong.color).width(1))
});
let sidebar = container(
@@ -248,25 +249,26 @@ fn application<'a>() -> Element<'a, Message> {
.spacing(40)
.padding(10)
.width(200)
- .align_items(Alignment::Center),
+ .align_x(Center),
)
.style(container::rounded_box)
- .center_y(Length::Fill);
+ .center_y(Fill);
let content = container(
scrollable(
column![
"Content!",
- square(400),
- square(200),
- square(400),
+ row((1..10).map(|i| square(if i % 2 == 0 { 80 } else { 160 })))
+ .spacing(20)
+ .align_y(Center)
+ .wrap(),
"The end"
]
.spacing(40)
- .align_items(Alignment::Center)
- .width(Length::Fill),
+ .align_x(Center)
+ .width(Fill),
)
- .height(Length::Fill),
+ .height(Fill),
)
.padding(10);
diff --git a/examples/lazy/src/main.rs b/examples/lazy/src/main.rs
index f24c0d62..8f756210 100644
--- a/examples/lazy/src/main.rs
+++ b/examples/lazy/src/main.rs
@@ -2,7 +2,7 @@ use iced::widget::{
button, column, horizontal_space, lazy, pick_list, row, scrollable, text,
text_input,
};
-use iced::{Element, Length};
+use iced::{Element, Fill};
use std::collections::HashSet;
use std::hash::Hash;
@@ -187,7 +187,7 @@ impl App {
});
column![
- scrollable(options).height(Length::Fill),
+ scrollable(options).height(Fill),
row![
text_input("Add a new option", &self.input)
.on_input(Message::InputChanged)
diff --git a/examples/loading_spinners/src/main.rs b/examples/loading_spinners/src/main.rs
index 503f2d7a..3b178148 100644
--- a/examples/loading_spinners/src/main.rs
+++ b/examples/loading_spinners/src/main.rs
@@ -1,5 +1,5 @@
use iced::widget::{center, column, row, slider, text};
-use iced::Element;
+use iced::{Center, Element};
use std::time::Duration;
@@ -67,7 +67,7 @@ impl LoadingSpinners {
Duration::from_secs_f32(self.cycle_duration)
)
]
- .align_items(iced::Alignment::Center)
+ .align_y(Center)
.spacing(20.0),
)
})
@@ -83,7 +83,7 @@ impl LoadingSpinners {
.width(200.0),
text!("{:.2}s", self.cycle_duration),
]
- .align_items(iced::Alignment::Center)
+ .align_y(Center)
.spacing(20.0),
),
)
diff --git a/examples/loupe/src/main.rs b/examples/loupe/src/main.rs
index c4d3b449..1c748d42 100644
--- a/examples/loupe/src/main.rs
+++ b/examples/loupe/src/main.rs
@@ -1,5 +1,5 @@
use iced::widget::{button, center, column, text};
-use iced::{Alignment, Element};
+use iced::{Center, Element};
use loupe::loupe;
@@ -39,7 +39,7 @@ impl Loupe {
button("Decrement").on_press(Message::Decrement)
]
.padding(20)
- .align_items(Alignment::Center),
+ .align_x(Center),
))
.into()
}
diff --git a/examples/component/Cargo.toml b/examples/markdown/Cargo.toml
index 83b7b8a4..cb74b954 100644
--- a/examples/component/Cargo.toml
+++ b/examples/markdown/Cargo.toml
@@ -1,5 +1,5 @@
[package]
-name = "component"
+name = "markdown"
version = "0.1.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2021"
@@ -7,4 +7,6 @@ publish = false
[dependencies]
iced.workspace = true
-iced.features = ["debug", "lazy"]
+iced.features = ["markdown", "highlighter", "debug"]
+
+open = "5.3"
diff --git a/examples/markdown/overview.md b/examples/markdown/overview.md
new file mode 100644
index 00000000..66336c5b
--- /dev/null
+++ b/examples/markdown/overview.md
@@ -0,0 +1,93 @@
+# Overview
+
+Inspired by [The Elm Architecture], Iced expects you to split user interfaces into four different concepts:
+
+* __State__ — the state of your application
+* __Messages__ — user interactions or meaningful events that you care about
+* __View logic__ — a way to display your __state__ as widgets that may produce __messages__ on user interaction
+* __Update logic__ — a way to react to __messages__ and update your __state__
+
+We can build something to see how this works! Let's say we want a simple counter that can be incremented and decremented using two buttons.
+
+We start by modelling the __state__ of our application:
+
+```rust
+#[derive(Default)]
+struct Counter {
+ value: i32,
+}
+```
+
+Next, we need to define the possible user interactions of our counter: the button presses. These interactions are our __messages__:
+
+```rust
+#[derive(Debug, Clone, Copy)]
+pub enum Message {
+ Increment,
+ Decrement,
+}
+```
+
+Now, let's show the actual counter by putting it all together in our __view logic__:
+
+```rust
+use iced::widget::{button, column, text, Column};
+
+impl Counter {
+ pub fn view(&self) -> Column<Message> {
+ // We use a column: a simple vertical layout
+ column![
+ // The increment button. We tell it to produce an
+ // `Increment` message when pressed
+ button("+").on_press(Message::Increment),
+
+ // We show the value of the counter here
+ text(self.value).size(50),
+
+ // The decrement button. We tell it to produce a
+ // `Decrement` message when pressed
+ button("-").on_press(Message::Decrement),
+ ]
+ }
+}
+```
+
+Finally, we need to be able to react to any produced __messages__ and change our __state__ accordingly in our __update logic__:
+
+```rust
+impl Counter {
+ // ...
+
+ pub fn update(&mut self, message: Message) {
+ match message {
+ Message::Increment => {
+ self.value += 1;
+ }
+ Message::Decrement => {
+ self.value -= 1;
+ }
+ }
+ }
+}
+```
+
+And that's everything! We just wrote a whole user interface. Let's run it:
+
+```rust
+fn main() -> iced::Result {
+ iced::run("A cool counter", Counter::update, Counter::view)
+}
+```
+
+Iced will automatically:
+
+ 1. Take the result of our __view logic__ and layout its widgets.
+ 1. Process events from our system and produce __messages__ for our __update logic__.
+ 1. Draw the resulting user interface.
+
+Read the [book], the [documentation], and the [examples] to learn more!
+
+[book]: https://book.iced.rs/
+[documentation]: https://docs.rs/iced/
+[examples]: https://github.com/iced-rs/iced/tree/master/examples#examples
+[The Elm Architecture]: https://guide.elm-lang.org/architecture/
diff --git a/examples/markdown/src/main.rs b/examples/markdown/src/main.rs
new file mode 100644
index 00000000..5605478f
--- /dev/null
+++ b/examples/markdown/src/main.rs
@@ -0,0 +1,82 @@
+use iced::highlighter;
+use iced::widget::{self, markdown, row, scrollable, text_editor};
+use iced::{Element, Fill, Font, Task, Theme};
+
+pub fn main() -> iced::Result {
+ iced::application("Markdown - Iced", Markdown::update, Markdown::view)
+ .theme(Markdown::theme)
+ .run_with(Markdown::new)
+}
+
+struct Markdown {
+ content: text_editor::Content,
+ items: Vec<markdown::Item>,
+ theme: Theme,
+}
+
+#[derive(Debug, Clone)]
+enum Message {
+ Edit(text_editor::Action),
+ LinkClicked(markdown::Url),
+}
+
+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),
+ items: markdown::parse(INITIAL_CONTENT).collect(),
+ theme,
+ },
+ widget::focus_next(),
+ )
+ }
+
+ fn update(&mut self, message: Message) {
+ match message {
+ Message::Edit(action) => {
+ let is_edit = action.is_edit();
+
+ self.content.perform(action);
+
+ if is_edit {
+ self.items =
+ markdown::parse(&self.content.text()).collect();
+ }
+ }
+ Message::LinkClicked(link) => {
+ let _ = open::that_in_background(link.to_string());
+ }
+ }
+ }
+
+ fn view(&self) -> Element<Message> {
+ let editor = text_editor(&self.content)
+ .placeholder("Type your Markdown here...")
+ .on_action(Message::Edit)
+ .height(Fill)
+ .padding(10)
+ .font(Font::MONOSPACE)
+ .highlight("markdown", highlighter::Theme::Base16Ocean);
+
+ let preview = markdown(
+ &self.items,
+ markdown::Settings::default(),
+ markdown::Style::from_palette(self.theme.palette()),
+ )
+ .map(Message::LinkClicked);
+
+ row![editor, scrollable(preview).spacing(10).height(Fill)]
+ .spacing(10)
+ .padding(10)
+ .into()
+ }
+
+ fn theme(&self) -> Theme {
+ self.theme.clone()
+ }
+}
diff --git a/examples/modal/src/main.rs b/examples/modal/src/main.rs
index 413485e7..067ca24d 100644
--- a/examples/modal/src/main.rs
+++ b/examples/modal/src/main.rs
@@ -5,7 +5,7 @@ use iced::widget::{
self, button, center, column, container, horizontal_space, mouse_area,
opaque, pick_list, row, stack, text, text_input,
};
-use iced::{Alignment, Color, Element, Length, Subscription, Task};
+use iced::{Bottom, Color, Element, Fill, Subscription, Task};
use std::fmt;
@@ -96,18 +96,17 @@ impl App {
let content = container(
column![
row![text("Top Left"), horizontal_space(), text("Top Right")]
- .align_items(Alignment::Start)
- .height(Length::Fill),
+ .height(Fill),
center(button(text("Show Modal")).on_press(Message::ShowModal)),
row![
text("Bottom Left"),
horizontal_space(),
text("Bottom Right")
]
- .align_items(Alignment::End)
- .height(Length::Fill),
+ .align_y(Bottom)
+ .height(Fill),
]
- .height(Length::Fill),
+ .height(Fill),
)
.padding(10);
@@ -202,19 +201,21 @@ where
{
stack![
base.into(),
- mouse_area(center(opaque(content)).style(|_theme| {
- container::Style {
- background: Some(
- Color {
- a: 0.8,
- ..Color::BLACK
- }
- .into(),
- ),
- ..container::Style::default()
- }
- }))
- .on_press(on_blur)
+ opaque(
+ mouse_area(center(opaque(content)).style(|_theme| {
+ container::Style {
+ background: Some(
+ Color {
+ a: 0.8,
+ ..Color::BLACK
+ }
+ .into(),
+ ),
+ ..container::Style::default()
+ }
+ }))
+ .on_press(on_blur)
+ )
]
.into()
}
diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs
index 98e753ab..ab09116e 100644
--- a/examples/multi_window/src/main.rs
+++ b/examples/multi_window/src/main.rs
@@ -3,22 +3,18 @@ use iced::widget::{
text_input,
};
use iced::window;
-use iced::{Alignment, Element, Length, Subscription, Task, Theme, Vector};
+use iced::{Center, Element, Fill, Subscription, Task, Theme, Vector};
use std::collections::BTreeMap;
fn main() -> iced::Result {
iced::daemon(Example::title, Example::update, Example::view)
- .load(|| {
- window::open(window::Settings::default()).map(Message::WindowOpened)
- })
.subscription(Example::subscription)
.theme(Example::theme)
.scale_factor(Example::scale_factor)
- .run()
+ .run_with(Example::new)
}
-#[derive(Default)]
struct Example {
windows: BTreeMap<window::Id, Window>,
}
@@ -43,6 +39,17 @@ enum Message {
}
impl Example {
+ fn new() -> (Self, Task<Message>) {
+ let (_id, open) = window::open(window::Settings::default());
+
+ (
+ Self {
+ windows: BTreeMap::new(),
+ },
+ open.map(Message::WindowOpened),
+ )
+ }
+
fn title(&self, window: window::Id) -> String {
self.windows
.get(&window)
@@ -68,10 +75,12 @@ impl Example {
},
);
- window::open(window::Settings {
+ let (_id, open) = window::open(window::Settings {
position,
..window::Settings::default()
- })
+ });
+
+ open
})
.map(Message::WindowOpened)
}
@@ -182,8 +191,8 @@ impl Window {
let content = scrollable(
column![scale_input, title_input, new_window_button]
.spacing(50)
- .width(Length::Fill)
- .align_items(Alignment::Center),
+ .width(Fill)
+ .align_x(Center),
);
container(content).center_x(200).into()
diff --git a/examples/multitouch/src/main.rs b/examples/multitouch/src/main.rs
index 69717310..a0105a8a 100644
--- a/examples/multitouch/src/main.rs
+++ b/examples/multitouch/src/main.rs
@@ -6,7 +6,7 @@ use iced::touch;
use iced::widget::canvas::event;
use iced::widget::canvas::stroke::{self, Stroke};
use iced::widget::canvas::{self, Canvas, Geometry};
-use iced::{Color, Element, Length, Point, Rectangle, Renderer, Theme};
+use iced::{Color, Element, Fill, Point, Rectangle, Renderer, Theme};
use std::collections::HashMap;
@@ -46,10 +46,7 @@ impl Multitouch {
}
fn view(&self) -> Element<Message> {
- Canvas::new(self)
- .width(Length::Fill)
- .height(Length::Fill)
- .into()
+ Canvas::new(self).width(Fill).height(Fill).into()
}
}
diff --git a/examples/pane_grid/src/main.rs b/examples/pane_grid/src/main.rs
index db9f7a05..67f4d27f 100644
--- a/examples/pane_grid/src/main.rs
+++ b/examples/pane_grid/src/main.rs
@@ -1,10 +1,9 @@
-use iced::alignment::{self, Alignment};
use iced::keyboard;
use iced::widget::pane_grid::{self, PaneGrid};
use iced::widget::{
button, column, container, responsive, row, scrollable, text,
};
-use iced::{Color, Element, Length, Size, Subscription};
+use iced::{Center, Color, Element, Fill, Size, Subscription};
pub fn main() -> iced::Result {
iced::application("Pane Grid - Iced", Example::update, Example::view)
@@ -155,11 +154,23 @@ impl Example {
.spacing(5);
let title_bar = pane_grid::TitleBar::new(title)
- .controls(view_controls(
- id,
- total_panes,
- pane.is_pinned,
- is_maximized,
+ .controls(pane_grid::Controls::dynamic(
+ view_controls(
+ id,
+ total_panes,
+ pane.is_pinned,
+ is_maximized,
+ ),
+ button(text("X").size(14))
+ .style(button::danger)
+ .padding(3)
+ .on_press_maybe(
+ if total_panes > 1 && !pane.is_pinned {
+ Some(Message::Close(id))
+ } else {
+ None
+ },
+ ),
))
.padding(10)
.style(if is_focused {
@@ -178,16 +189,16 @@ impl Example {
style::pane_active
})
})
- .width(Length::Fill)
- .height(Length::Fill)
+ .width(Fill)
+ .height(Fill)
.spacing(10)
.on_click(Message::Clicked)
.on_drag(Message::Dragged)
.on_resize(10, Message::Resized);
container(pane_grid)
- .width(Length::Fill)
- .height(Length::Fill)
+ .width(Fill)
+ .height(Fill)
.padding(10)
.into()
}
@@ -255,15 +266,10 @@ fn view_content<'a>(
size: Size,
) -> Element<'a, Message> {
let button = |label, message| {
- button(
- text(label)
- .width(Length::Fill)
- .horizontal_alignment(alignment::Horizontal::Center)
- .size(16),
- )
- .width(Length::Fill)
- .padding(8)
- .on_press(message)
+ button(text(label).width(Fill).align_x(Center).size(16))
+ .width(Fill)
+ .padding(8)
+ .on_press(message)
};
let controls = column![
@@ -287,10 +293,10 @@ fn view_content<'a>(
let content =
column![text!("{}x{}", size.width, size.height).size(24), controls,]
.spacing(10)
- .align_items(Alignment::Center);
+ .align_x(Center);
container(scrollable(content))
- .center_y(Length::Fill)
+ .center_y(Fill)
.padding(5)
.into()
}
diff --git a/examples/pick_list/src/main.rs b/examples/pick_list/src/main.rs
index 2be6f5b0..d8b2b389 100644
--- a/examples/pick_list/src/main.rs
+++ b/examples/pick_list/src/main.rs
@@ -1,5 +1,5 @@
use iced::widget::{column, pick_list, scrollable, vertical_space};
-use iced::{Alignment, Element, Length};
+use iced::{Center, Element, Fill};
pub fn main() -> iced::Result {
iced::run("Pick List - Iced", Example::update, Example::view)
@@ -38,8 +38,8 @@ impl Example {
pick_list,
vertical_space().height(600),
]
- .width(Length::Fill)
- .align_items(Alignment::Center)
+ .width(Fill)
+ .align_x(Center)
.spacing(10);
scrollable(content).into()
diff --git a/examples/pokedex/Cargo.toml b/examples/pokedex/Cargo.toml
index bf7e1e35..1a6d5445 100644
--- a/examples/pokedex/Cargo.toml
+++ b/examples/pokedex/Cargo.toml
@@ -16,7 +16,7 @@ version = "1.0"
features = ["derive"]
[dependencies.reqwest]
-version = "0.11"
+version = "0.12"
default-features = false
features = ["json", "rustls-tls"]
diff --git a/examples/pokedex/src/main.rs b/examples/pokedex/src/main.rs
index b22ffe7f..2e972f6b 100644
--- a/examples/pokedex/src/main.rs
+++ b/examples/pokedex/src/main.rs
@@ -1,20 +1,16 @@
use iced::futures;
use iced::widget::{self, center, column, image, row, text};
-use iced::{Alignment, Element, Length, Task};
+use iced::{Center, Element, Fill, Right, Task};
pub fn main() -> iced::Result {
iced::application(Pokedex::title, Pokedex::update, Pokedex::view)
- .load(Pokedex::search)
- .run()
+ .run_with(Pokedex::new)
}
-#[derive(Debug, Default)]
+#[derive(Debug)]
enum Pokedex {
- #[default]
Loading,
- Loaded {
- pokemon: Pokemon,
- },
+ Loaded { pokemon: Pokemon },
Errored,
}
@@ -25,6 +21,10 @@ enum Message {
}
impl Pokedex {
+ fn new() -> (Self, Task<Message>) {
+ (Self::Loading, Self::search())
+ }
+
fn search() -> Task<Message> {
Task::perform(Pokemon::search(), Message::PokemonFound)
}
@@ -63,10 +63,9 @@ impl Pokedex {
}
fn view(&self) -> Element<Message> {
- let content = match self {
+ let content: Element<_> = match self {
Pokedex::Loading => {
- column![text("Searching for Pokémon...").size(40),]
- .width(Length::Shrink)
+ text("Searching for Pokémon...").size(40).into()
}
Pokedex::Loaded { pokemon } => column![
pokemon.view(),
@@ -74,13 +73,15 @@ impl Pokedex {
]
.max_width(500)
.spacing(20)
- .align_items(Alignment::End),
+ .align_x(Right)
+ .into(),
Pokedex::Errored => column![
text("Whoops! Something went wrong...").size(40),
button("Try again").on_press(Message::Search)
]
.spacing(20)
- .align_items(Alignment::End),
+ .align_x(Right)
+ .into(),
};
center(content).into()
@@ -103,17 +104,17 @@ impl Pokemon {
image::viewer(self.image.clone()),
column![
row![
- text(&self.name).size(30).width(Length::Fill),
+ text(&self.name).size(30).width(Fill),
text!("#{}", self.number).size(20).color([0.5, 0.5, 0.5]),
]
- .align_items(Alignment::Center)
+ .align_y(Center)
.spacing(20),
self.description.as_ref(),
]
.spacing(20),
]
.spacing(20)
- .align_items(Alignment::Center)
+ .align_y(Center)
.into()
}
diff --git a/examples/qr_code/src/main.rs b/examples/qr_code/src/main.rs
index b30ecf15..f1b654e0 100644
--- a/examples/qr_code/src/main.rs
+++ b/examples/qr_code/src/main.rs
@@ -1,5 +1,5 @@
use iced::widget::{center, column, pick_list, qr_code, row, text, text_input};
-use iced::{Alignment, Element, Theme};
+use iced::{Center, Element, Theme};
pub fn main() -> iced::Result {
iced::application(
@@ -58,7 +58,7 @@ impl QRGenerator {
pick_list(Theme::ALL, Some(&self.theme), Message::ThemeChanged,)
]
.spacing(10)
- .align_items(Alignment::Center);
+ .align_y(Center);
let content = column![title, input, choose_theme]
.push_maybe(
@@ -68,7 +68,7 @@ impl QRGenerator {
)
.width(700)
.spacing(20)
- .align_items(Alignment::Center);
+ .align_x(Center);
center(content).padding(20).into()
}
diff --git a/examples/screenshot/src/main.rs b/examples/screenshot/src/main.rs
index acde8367..5c105f6c 100644
--- a/examples/screenshot/src/main.rs
+++ b/examples/screenshot/src/main.rs
@@ -1,10 +1,10 @@
-use iced::alignment;
use iced::keyboard;
use iced::widget::{button, column, container, image, row, text, text_input};
use iced::window;
use iced::window::screenshot::{self, Screenshot};
use iced::{
- Alignment, ContentFit, Element, Length, Rectangle, Subscription, Task,
+ Center, ContentFit, Element, Fill, FillPortion, Rectangle, Subscription,
+ Task,
};
use ::image as img;
@@ -20,7 +20,7 @@ fn main() -> iced::Result {
#[derive(Default)]
struct Example {
- screenshot: Option<Screenshot>,
+ screenshot: Option<(Screenshot, image::Handle)>,
saved_png_path: Option<Result<String, PngError>>,
png_saving: bool,
crop_error: Option<screenshot::CropError>,
@@ -52,10 +52,17 @@ impl Example {
.map(Message::Screenshotted);
}
Message::Screenshotted(screenshot) => {
- self.screenshot = Some(screenshot);
+ self.screenshot = Some((
+ screenshot.clone(),
+ image::Handle::from_rgba(
+ screenshot.size.width,
+ screenshot.size.height,
+ screenshot.bytes,
+ ),
+ ));
}
Message::Png => {
- if let Some(screenshot) = &self.screenshot {
+ if let Some((screenshot, _handle)) = &self.screenshot {
self.png_saving = true;
return Task::perform(
@@ -81,7 +88,7 @@ impl Example {
self.height_input_value = new_value;
}
Message::Crop => {
- if let Some(screenshot) = &self.screenshot {
+ if let Some((screenshot, _handle)) = &self.screenshot {
let cropped = screenshot.crop(Rectangle::<u32> {
x: self.x_input_value.unwrap_or(0),
y: self.y_input_value.unwrap_or(0),
@@ -91,7 +98,14 @@ impl Example {
match cropped {
Ok(screenshot) => {
- self.screenshot = Some(screenshot);
+ self.screenshot = Some((
+ screenshot.clone(),
+ image::Handle::from_rgba(
+ screenshot.size.width,
+ screenshot.size.height,
+ screenshot.bytes,
+ ),
+ ));
self.crop_error = None;
}
Err(crop_error) => {
@@ -106,53 +120,41 @@ impl Example {
}
fn view(&self) -> Element<'_, Message> {
- let image: Element<Message> = if let Some(screenshot) = &self.screenshot
- {
- image(image::Handle::from_rgba(
- screenshot.size.width,
- screenshot.size.height,
- screenshot.clone(),
- ))
- .content_fit(ContentFit::Contain)
- .width(Length::Fill)
- .height(Length::Fill)
- .into()
- } else {
- text("Press the button to take a screenshot!").into()
- };
+ let image: Element<Message> =
+ if let Some((_screenshot, handle)) = &self.screenshot {
+ image(handle)
+ .content_fit(ContentFit::Contain)
+ .width(Fill)
+ .height(Fill)
+ .into()
+ } else {
+ text("Press the button to take a screenshot!").into()
+ };
let image = container(image)
- .center_y(Length::FillPortion(2))
+ .center_y(FillPortion(2))
.padding(10)
.style(container::rounded_box);
let crop_origin_controls = row![
- text("X:")
- .vertical_alignment(alignment::Vertical::Center)
- .width(30),
+ text("X:").width(30),
numeric_input("0", self.x_input_value).map(Message::XInputChanged),
- text("Y:")
- .vertical_alignment(alignment::Vertical::Center)
- .width(30),
+ text("Y:").width(30),
numeric_input("0", self.y_input_value).map(Message::YInputChanged)
]
.spacing(10)
- .align_items(Alignment::Center);
+ .align_y(Center);
let crop_dimension_controls = row![
- text("W:")
- .vertical_alignment(alignment::Vertical::Center)
- .width(30),
+ text("W:").width(30),
numeric_input("0", self.width_input_value)
.map(Message::WidthInputChanged),
- text("H:")
- .vertical_alignment(alignment::Vertical::Center)
- .width(30),
+ text("H:").width(30),
numeric_input("0", self.height_input_value)
.map(Message::HeightInputChanged)
]
.spacing(10)
- .align_items(Alignment::Center);
+ .align_y(Center);
let crop_controls =
column![crop_origin_controls, crop_dimension_controls]
@@ -162,7 +164,7 @@ impl Example {
.map(|error| text!("Crop error! \n{error}")),
)
.spacing(10)
- .align_items(Alignment::Center);
+ .align_x(Center);
let controls = {
let save_result =
@@ -178,8 +180,8 @@ impl Example {
column![
column![
button(centered_text("Screenshot!"))
- .padding([10, 20, 10, 20])
- .width(Length::Fill)
+ .padding([10, 20])
+ .width(Fill)
.on_press(Message::Screenshot),
if !self.png_saving {
button(centered_text("Save as png")).on_press_maybe(
@@ -190,8 +192,8 @@ impl Example {
.style(button::secondary)
}
.style(button::secondary)
- .padding([10, 20, 10, 20])
- .width(Length::Fill)
+ .padding([10, 20])
+ .width(Fill)
]
.spacing(10),
column![
@@ -199,23 +201,23 @@ impl Example {
button(centered_text("Crop"))
.on_press(Message::Crop)
.style(button::danger)
- .padding([10, 20, 10, 20])
- .width(Length::Fill),
+ .padding([10, 20])
+ .width(Fill),
]
.spacing(10)
- .align_items(Alignment::Center),
+ .align_x(Center),
]
.push_maybe(save_result.map(text))
.spacing(40)
};
- let side_content = container(controls).center_y(Length::Fill);
+ let side_content = container(controls).center_y(Fill);
let content = row![side_content, image]
.spacing(10)
- .width(Length::Fill)
- .height(Length::Fill)
- .align_items(Alignment::Center);
+ .width(Fill)
+ .height(Fill)
+ .align_y(Center);
container(content).padding(10).into()
}
@@ -276,8 +278,5 @@ fn numeric_input(
}
fn centered_text(content: &str) -> Element<'_, Message> {
- text(content)
- .width(Length::Fill)
- .horizontal_alignment(alignment::Horizontal::Center)
- .into()
+ text(content).width(Fill).align_x(Center).into()
}
diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs
index f2a853e1..de4f2f9a 100644
--- a/examples/scrollable/src/main.rs
+++ b/examples/scrollable/src/main.rs
@@ -1,9 +1,8 @@
-use iced::widget::scrollable::Properties;
use iced::widget::{
button, column, container, horizontal_space, progress_bar, radio, row,
- scrollable, slider, text, vertical_space, Scrollable,
+ scrollable, slider, text, vertical_space,
};
-use iced::{Alignment, Border, Color, Element, Length, Task, Theme};
+use iced::{Border, Center, Color, Element, Fill, Task, Theme};
use once_cell::sync::Lazy;
@@ -25,7 +24,7 @@ struct ScrollableDemo {
scrollbar_margin: u16,
scroller_width: u16,
current_scroll_offset: scrollable::RelativeOffset,
- alignment: scrollable::Alignment,
+ anchor: scrollable::Anchor,
}
#[derive(Debug, Clone, Eq, PartialEq, Copy)]
@@ -38,7 +37,7 @@ enum Direction {
#[derive(Debug, Clone)]
enum Message {
SwitchDirection(Direction),
- AlignmentChanged(scrollable::Alignment),
+ AlignmentChanged(scrollable::Anchor),
ScrollbarWidthChanged(u16),
ScrollbarMarginChanged(u16),
ScrollerWidthChanged(u16),
@@ -55,7 +54,7 @@ impl ScrollableDemo {
scrollbar_margin: 0,
scroller_width: 10,
current_scroll_offset: scrollable::RelativeOffset::START,
- alignment: scrollable::Alignment::Start,
+ anchor: scrollable::Anchor::Start,
}
}
@@ -72,7 +71,7 @@ impl ScrollableDemo {
}
Message::AlignmentChanged(alignment) => {
self.current_scroll_offset = scrollable::RelativeOffset::START;
- self.alignment = alignment;
+ self.anchor = alignment;
scrollable::snap_to(
SCROLLABLE_ID.clone(),
@@ -169,14 +168,14 @@ impl ScrollableDemo {
text("Scrollable alignment:"),
radio(
"Start",
- scrollable::Alignment::Start,
- Some(self.alignment),
+ scrollable::Anchor::Start,
+ Some(self.anchor),
Message::AlignmentChanged,
),
radio(
"End",
- scrollable::Alignment::End,
- Some(self.alignment),
+ scrollable::Anchor::End,
+ Some(self.anchor),
Message::AlignmentChanged,
)
]
@@ -203,7 +202,7 @@ impl ScrollableDemo {
let scrollable_content: Element<Message> =
Element::from(match self.scrollable_direction {
- Direction::Vertical => Scrollable::with_direction(
+ Direction::Vertical => scrollable(
column![
scroll_to_end_button(),
text("Beginning!"),
@@ -213,22 +212,22 @@ impl ScrollableDemo {
text("End!"),
scroll_to_beginning_button(),
]
- .align_items(Alignment::Center)
- .padding([40, 0, 40, 0])
+ .align_x(Center)
+ .padding([40, 0])
.spacing(40),
- scrollable::Direction::Vertical(
- Properties::new()
- .width(self.scrollbar_width)
- .margin(self.scrollbar_margin)
- .scroller_width(self.scroller_width)
- .alignment(self.alignment),
- ),
)
- .width(Length::Fill)
- .height(Length::Fill)
+ .direction(scrollable::Direction::Vertical(
+ scrollable::Scrollbar::new()
+ .width(self.scrollbar_width)
+ .margin(self.scrollbar_margin)
+ .scroller_width(self.scroller_width)
+ .anchor(self.anchor),
+ ))
+ .width(Fill)
+ .height(Fill)
.id(SCROLLABLE_ID.clone())
.on_scroll(Message::Scrolled),
- Direction::Horizontal => Scrollable::with_direction(
+ Direction::Horizontal => scrollable(
row![
scroll_to_end_button(),
text("Beginning!"),
@@ -239,22 +238,22 @@ impl ScrollableDemo {
scroll_to_beginning_button(),
]
.height(450)
- .align_items(Alignment::Center)
- .padding([0, 40, 0, 40])
+ .align_y(Center)
+ .padding([0, 40])
.spacing(40),
- scrollable::Direction::Horizontal(
- Properties::new()
- .width(self.scrollbar_width)
- .margin(self.scrollbar_margin)
- .scroller_width(self.scroller_width)
- .alignment(self.alignment),
- ),
)
- .width(Length::Fill)
- .height(Length::Fill)
+ .direction(scrollable::Direction::Horizontal(
+ scrollable::Scrollbar::new()
+ .width(self.scrollbar_width)
+ .margin(self.scrollbar_margin)
+ .scroller_width(self.scroller_width)
+ .anchor(self.anchor),
+ ))
+ .width(Fill)
+ .height(Fill)
.id(SCROLLABLE_ID.clone())
.on_scroll(Message::Scrolled),
- Direction::Multi => Scrollable::with_direction(
+ Direction::Multi => scrollable(
//horizontal content
row![
column![
@@ -281,24 +280,24 @@ impl ScrollableDemo {
text("Horizontal - End!"),
scroll_to_beginning_button(),
]
- .align_items(Alignment::Center)
- .padding([0, 40, 0, 40])
+ .align_y(Center)
+ .padding([0, 40])
.spacing(40),
- {
- let properties = Properties::new()
- .width(self.scrollbar_width)
- .margin(self.scrollbar_margin)
- .scroller_width(self.scroller_width)
- .alignment(self.alignment);
-
- scrollable::Direction::Both {
- horizontal: properties,
- vertical: properties,
- }
- },
)
- .width(Length::Fill)
- .height(Length::Fill)
+ .direction({
+ let scrollbar = scrollable::Scrollbar::new()
+ .width(self.scrollbar_width)
+ .margin(self.scrollbar_margin)
+ .scroller_width(self.scroller_width)
+ .anchor(self.anchor);
+
+ scrollable::Direction::Both {
+ horizontal: scrollbar,
+ vertical: scrollbar,
+ }
+ })
+ .width(Fill)
+ .height(Fill)
.id(SCROLLABLE_ID.clone())
.on_scroll(Message::Scrolled),
});
@@ -323,7 +322,7 @@ impl ScrollableDemo {
let content: Element<Message> =
column![scroll_controls, scrollable_content, progress_bars]
- .align_items(Alignment::Center)
+ .align_x(Center)
.spacing(10)
.into();
diff --git a/examples/sierpinski_triangle/src/main.rs b/examples/sierpinski_triangle/src/main.rs
index 4c751937..99e7900a 100644
--- a/examples/sierpinski_triangle/src/main.rs
+++ b/examples/sierpinski_triangle/src/main.rs
@@ -2,7 +2,7 @@ use iced::mouse;
use iced::widget::canvas::event::{self, Event};
use iced::widget::canvas::{self, Canvas, Geometry};
use iced::widget::{column, row, slider, text};
-use iced::{Color, Length, Point, Rectangle, Renderer, Size, Theme};
+use iced::{Center, Color, Fill, Point, Rectangle, Renderer, Size, Theme};
use rand::Rng;
use std::fmt::Debug;
@@ -50,9 +50,7 @@ impl SierpinskiEmulator {
fn view(&self) -> iced::Element<'_, Message> {
column![
- Canvas::new(&self.graph)
- .width(Length::Fill)
- .height(Length::Fill),
+ Canvas::new(&self.graph).width(Fill).height(Fill),
row![
text!("Iteration: {:?}", self.graph.iteration),
slider(0..=10000, self.graph.iteration, Message::IterationSet)
@@ -60,7 +58,7 @@ impl SierpinskiEmulator {
.padding(10)
.spacing(20),
]
- .align_items(iced::Alignment::Center)
+ .align_x(Center)
.into()
}
}
diff --git a/examples/slider/Cargo.toml b/examples/slider/Cargo.toml
index fad8916e..05e74d2c 100644
--- a/examples/slider/Cargo.toml
+++ b/examples/slider/Cargo.toml
@@ -7,3 +7,4 @@ publish = false
[dependencies]
iced.workspace = true
+iced.features = ["svg"]
diff --git a/examples/slider/src/main.rs b/examples/slider/src/main.rs
index 0b4c29aa..ffb5475f 100644
--- a/examples/slider/src/main.rs
+++ b/examples/slider/src/main.rs
@@ -1,5 +1,5 @@
-use iced::widget::{center, column, container, slider, text, vertical_slider};
-use iced::{Element, Length};
+use iced::widget::{column, container, iced, slider, text, vertical_slider};
+use iced::{Center, Element, Fill};
pub fn main() -> iced::Result {
iced::run("Slider - Iced", Slider::update, Slider::view)
@@ -12,19 +12,11 @@ pub enum Message {
pub struct Slider {
value: u8,
- default: u8,
- step: u8,
- shift_step: u8,
}
impl Slider {
fn new() -> Self {
- Slider {
- value: 50,
- default: 50,
- step: 5,
- shift_step: 1,
- }
+ Slider { value: 50 }
}
fn update(&mut self, message: Message) {
@@ -37,32 +29,27 @@ impl Slider {
fn view(&self) -> Element<Message> {
let h_slider = container(
- slider(0..=100, self.value, Message::SliderChanged)
- .default(self.default)
- .step(self.step)
- .shift_step(self.shift_step),
+ slider(1..=100, self.value, Message::SliderChanged)
+ .default(50)
+ .shift_step(5),
)
.width(250);
let v_slider = container(
- vertical_slider(0..=100, self.value, Message::SliderChanged)
- .default(self.default)
- .step(self.step)
- .shift_step(self.shift_step),
+ vertical_slider(1..=100, self.value, Message::SliderChanged)
+ .default(50)
+ .shift_step(5),
)
.height(200);
let text = text(self.value);
- center(
- column![
- container(v_slider).center_x(Length::Fill),
- container(h_slider).center_x(Length::Fill),
- container(text).center_x(Length::Fill)
- ]
- .spacing(25),
- )
- .into()
+ column![v_slider, h_slider, text, iced(self.value as f32),]
+ .width(Fill)
+ .align_x(Center)
+ .spacing(20)
+ .padding(20)
+ .into()
}
}
diff --git a/examples/solar_system/Cargo.toml b/examples/solar_system/Cargo.toml
index ca64da14..e2c18c50 100644
--- a/examples/solar_system/Cargo.toml
+++ b/examples/solar_system/Cargo.toml
@@ -7,7 +7,7 @@ publish = false
[dependencies]
iced.workspace = true
-iced.features = ["debug", "canvas", "tokio"]
+iced.features = ["debug", "canvas", "image", "tokio"]
rand = "0.8.3"
tracing-subscriber = "0.3"
diff --git a/examples/solar_system/assets/earth.png b/examples/solar_system/assets/earth.png
new file mode 100644
index 00000000..e81321d9
--- /dev/null
+++ b/examples/solar_system/assets/earth.png
Binary files differ
diff --git a/examples/solar_system/assets/moon.png b/examples/solar_system/assets/moon.png
new file mode 100644
index 00000000..03f10cb7
--- /dev/null
+++ b/examples/solar_system/assets/moon.png
Binary files differ
diff --git a/examples/solar_system/assets/sun.png b/examples/solar_system/assets/sun.png
new file mode 100644
index 00000000..29a928a7
--- /dev/null
+++ b/examples/solar_system/assets/sun.png
Binary files differ
diff --git a/examples/solar_system/src/main.rs b/examples/solar_system/src/main.rs
index 2a67e23e..1e74f2bd 100644
--- a/examples/solar_system/src/main.rs
+++ b/examples/solar_system/src/main.rs
@@ -7,13 +7,12 @@
//!
//! [1]: https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Basic_animations#An_animated_solar_system
use iced::mouse;
-use iced::widget::canvas;
-use iced::widget::canvas::gradient;
use iced::widget::canvas::stroke::{self, Stroke};
use iced::widget::canvas::{Geometry, Path};
+use iced::widget::{canvas, image};
use iced::window;
use iced::{
- Color, Element, Length, Point, Rectangle, Renderer, Size, Subscription,
+ Color, Element, Fill, Point, Rectangle, Renderer, Size, Subscription,
Theme, Vector,
};
@@ -52,10 +51,7 @@ impl SolarSystem {
}
fn view(&self) -> Element<Message> {
- canvas(&self.state)
- .width(Length::Fill)
- .height(Length::Fill)
- .into()
+ canvas(&self.state).width(Fill).height(Fill).into()
}
fn theme(&self) -> Theme {
@@ -69,6 +65,9 @@ impl SolarSystem {
#[derive(Debug)]
struct State {
+ sun: image::Handle,
+ earth: image::Handle,
+ moon: image::Handle,
space_cache: canvas::Cache,
system_cache: canvas::Cache,
start: Instant,
@@ -88,6 +87,15 @@ impl State {
let size = window::Settings::default().size;
State {
+ sun: image::Handle::from_bytes(
+ include_bytes!("../assets/sun.png").as_slice(),
+ ),
+ earth: image::Handle::from_bytes(
+ include_bytes!("../assets/earth.png").as_slice(),
+ ),
+ moon: image::Handle::from_bytes(
+ include_bytes!("../assets/moon.png").as_slice(),
+ ),
space_cache: canvas::Cache::default(),
system_cache: canvas::Cache::default(),
start: now,
@@ -135,6 +143,8 @@ impl<Message> canvas::Program<Message> for State {
let background =
self.space_cache.draw(renderer, bounds.size(), |frame| {
+ frame.fill_rectangle(Point::ORIGIN, frame.size(), Color::BLACK);
+
let stars = Path::new(|path| {
for (p, size) in &self.stars {
path.rectangle(*p, Size::new(*size, *size));
@@ -147,17 +157,18 @@ impl<Message> canvas::Program<Message> for State {
let system = self.system_cache.draw(renderer, bounds.size(), |frame| {
let center = frame.center();
+ frame.translate(Vector::new(center.x, center.y));
- let sun = Path::circle(center, Self::SUN_RADIUS);
- let orbit = Path::circle(center, Self::ORBIT_RADIUS);
+ frame.draw_image(
+ Rectangle::with_radius(Self::SUN_RADIUS),
+ &self.sun,
+ );
- frame.fill(&sun, Color::from_rgb8(0xF9, 0xD7, 0x1C));
+ let orbit = Path::circle(Point::ORIGIN, Self::ORBIT_RADIUS);
frame.stroke(
&orbit,
Stroke {
- style: stroke::Style::Solid(Color::from_rgba8(
- 0, 153, 255, 0.1,
- )),
+ style: stroke::Style::Solid(Color::WHITE.scale_alpha(0.1)),
width: 1.0,
line_dash: canvas::LineDash {
offset: 0,
@@ -171,30 +182,21 @@ impl<Message> canvas::Program<Message> for State {
let rotation = (2.0 * PI / 60.0) * elapsed.as_secs() as f32
+ (2.0 * PI / 60_000.0) * elapsed.subsec_millis() as f32;
- frame.with_save(|frame| {
- frame.translate(Vector::new(center.x, center.y));
- frame.rotate(rotation);
- frame.translate(Vector::new(Self::ORBIT_RADIUS, 0.0));
-
- let earth = Path::circle(Point::ORIGIN, Self::EARTH_RADIUS);
-
- let earth_fill = gradient::Linear::new(
- Point::new(-Self::EARTH_RADIUS, 0.0),
- Point::new(Self::EARTH_RADIUS, 0.0),
- )
- .add_stop(0.2, Color::from_rgb(0.15, 0.50, 1.0))
- .add_stop(0.8, Color::from_rgb(0.0, 0.20, 0.47));
+ frame.rotate(rotation);
+ frame.translate(Vector::new(Self::ORBIT_RADIUS, 0.0));
- frame.fill(&earth, earth_fill);
+ frame.draw_image(
+ Rectangle::with_radius(Self::EARTH_RADIUS),
+ canvas::Image::new(&self.earth).rotation(-rotation * 20.0),
+ );
- frame.with_save(|frame| {
- frame.rotate(rotation * 10.0);
- frame.translate(Vector::new(0.0, Self::MOON_DISTANCE));
+ frame.rotate(rotation * 10.0);
+ frame.translate(Vector::new(0.0, Self::MOON_DISTANCE));
- let moon = Path::circle(Point::ORIGIN, Self::MOON_RADIUS);
- frame.fill(&moon, Color::WHITE);
- });
- });
+ frame.draw_image(
+ Rectangle::with_radius(Self::MOON_RADIUS),
+ &self.moon,
+ );
});
vec![background, system]
diff --git a/examples/stopwatch/src/main.rs b/examples/stopwatch/src/main.rs
index bd56785a..0d824d36 100644
--- a/examples/stopwatch/src/main.rs
+++ b/examples/stopwatch/src/main.rs
@@ -1,8 +1,7 @@
-use iced::alignment;
use iced::keyboard;
use iced::time;
use iced::widget::{button, center, column, row, text};
-use iced::{Alignment, Element, Subscription, Theme};
+use iced::{Center, Element, Subscription, Theme};
use std::time::{Duration, Instant};
@@ -101,13 +100,8 @@ impl Stopwatch {
)
.size(40);
- let button = |label| {
- button(
- text(label).horizontal_alignment(alignment::Horizontal::Center),
- )
- .padding(10)
- .width(80)
- };
+ let button =
+ |label| button(text(label).align_x(Center)).padding(10).width(80);
let toggle_button = {
let label = match self.state {
@@ -124,9 +118,7 @@ impl Stopwatch {
let controls = row![toggle_button, reset_button].spacing(20);
- let content = column![duration, controls]
- .align_items(Alignment::Center)
- .spacing(20);
+ let content = column![duration, controls].align_x(Center).spacing(20);
center(content).into()
}
diff --git a/examples/styling/src/main.rs b/examples/styling/src/main.rs
index 3124493b..534f5e32 100644
--- a/examples/styling/src/main.rs
+++ b/examples/styling/src/main.rs
@@ -3,7 +3,7 @@ use iced::widget::{
row, scrollable, slider, text, text_input, toggler, vertical_rule,
vertical_space,
};
-use iced::{Alignment, Element, Length, Theme};
+use iced::{Center, Element, Fill, Theme};
pub fn main() -> iced::Result {
iced::application("Styling - Iced", Styling::update, Styling::view)
@@ -48,7 +48,7 @@ impl Styling {
let choose_theme = column![
text("Theme:"),
pick_list(Theme::ALL, Some(&self.theme), Message::ThemeChanged)
- .width(Length::Fill),
+ .width(Fill),
]
.spacing(10);
@@ -71,26 +71,21 @@ impl Styling {
vertical_space().height(800),
"You did it!"
])
- .width(Length::Fill)
+ .width(Fill)
.height(100);
let checkbox = checkbox("Check me!", self.checkbox_value)
.on_toggle(Message::CheckboxToggled);
- let toggler = toggler(
- String::from("Toggle me!"),
- self.toggler_value,
- Message::TogglerToggled,
- )
- .width(Length::Shrink)
- .spacing(10);
+ let toggler = toggler(self.toggler_value)
+ .label("Toggle me!")
+ .on_toggle(Message::TogglerToggled)
+ .spacing(10);
let content = column![
choose_theme,
horizontal_rule(38),
- row![text_input, button]
- .spacing(10)
- .align_items(Alignment::Center),
+ row![text_input, button].spacing(10).align_y(Center),
slider,
progress_bar,
row![
@@ -100,7 +95,7 @@ impl Styling {
]
.spacing(10)
.height(100)
- .align_items(Alignment::Center),
+ .align_y(Center),
]
.spacing(20)
.padding(20)
diff --git a/examples/svg/src/main.rs b/examples/svg/src/main.rs
index e071c3af..02cb85cc 100644
--- a/examples/svg/src/main.rs
+++ b/examples/svg/src/main.rs
@@ -1,5 +1,5 @@
use iced::widget::{center, checkbox, column, container, svg};
-use iced::{color, Element, Length};
+use iced::{color, Element, Fill};
pub fn main() -> iced::Result {
iced::run("SVG - Iced", Tiger::update, Tiger::view)
@@ -30,24 +30,26 @@ impl Tiger {
env!("CARGO_MANIFEST_DIR")
));
- let svg = svg(handle).width(Length::Fill).height(Length::Fill).style(
- |_theme, _status| svg::Style {
- color: if self.apply_color_filter {
- Some(color!(0x0000ff))
- } else {
- None
- },
- },
- );
+ let svg =
+ svg(handle)
+ .width(Fill)
+ .height(Fill)
+ .style(|_theme, _status| svg::Style {
+ color: if self.apply_color_filter {
+ Some(color!(0x0000ff))
+ } else {
+ None
+ },
+ });
let apply_color_filter =
checkbox("Apply a color filter", self.apply_color_filter)
.on_toggle(Message::ToggleColorFilter);
center(
- column![svg, container(apply_color_filter).center_x(Length::Fill)]
+ column![svg, container(apply_color_filter).center_x(Fill)]
.spacing(20)
- .height(Length::Fill),
+ .height(Fill),
)
.padding(20)
.into()
diff --git a/examples/the_matrix/src/main.rs b/examples/the_matrix/src/main.rs
index 2ae1cc3a..0ed52dda 100644
--- a/examples/the_matrix/src/main.rs
+++ b/examples/the_matrix/src/main.rs
@@ -2,8 +2,7 @@ use iced::mouse;
use iced::time::{self, Instant};
use iced::widget::canvas;
use iced::{
- Color, Element, Font, Length, Point, Rectangle, Renderer, Subscription,
- Theme,
+ Color, Element, Fill, Font, Point, Rectangle, Renderer, Subscription, Theme,
};
use std::cell::RefCell;
@@ -37,10 +36,7 @@ impl TheMatrix {
}
fn view(&self) -> Element<Message> {
- canvas(self as &Self)
- .width(Length::Fill)
- .height(Length::Fill)
- .into()
+ canvas(self as &Self).width(Fill).height(Fill).into()
}
fn subscription(&self) -> Subscription<Message> {
diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs
index 232133b1..8f6a836e 100644
--- a/examples/toast/src/main.rs
+++ b/examples/toast/src/main.rs
@@ -4,7 +4,7 @@ use iced::keyboard::key;
use iced::widget::{
self, button, center, column, pick_list, row, slider, text, text_input,
};
-use iced::{Alignment, Element, Length, Subscription, Task};
+use iced::{Center, Element, Fill, Subscription, Task};
use toast::{Status, Toast};
@@ -125,7 +125,7 @@ impl App {
Some(self.editing.status),
Message::Status
)
- .width(Length::Fill)
+ .width(Fill)
.into()
),
subtitle(
@@ -142,7 +142,7 @@ impl App {
.spacing(5)
.into()
),
- column![add_toast].align_items(Alignment::End)
+ column![add_toast].align_x(Center)
]
.spacing(10)
.max_width(200),
@@ -177,8 +177,8 @@ mod toast {
};
use iced::window;
use iced::{
- Alignment, Element, Length, Point, Rectangle, Renderer, Size, Theme,
- Vector,
+ Alignment, Center, Element, Fill, Length, Point, Rectangle, Renderer,
+ Size, Theme, Vector,
};
pub const DEFAULT_TIMEOUT: u64 = 5;
@@ -245,9 +245,9 @@ mod toast {
.on_press((on_close)(index))
.padding(3),
]
- .align_items(Alignment::Center)
+ .align_y(Center)
)
- .width(Length::Fill)
+ .width(Fill)
.padding(5)
.style(match toast.status {
Status::Primary => primary,
@@ -257,7 +257,7 @@ mod toast {
}),
horizontal_rule(1),
container(text(toast.body.as_str()))
- .width(Length::Fill)
+ .width(Fill)
.padding(5)
.style(container::rounded_box),
])
@@ -347,7 +347,7 @@ mod toast {
state: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
- operation: &mut dyn Operation<()>,
+ operation: &mut dyn Operation,
) {
operation.container(None, layout.bounds(), &mut |operation| {
self.content.as_widget().operate(
@@ -479,8 +479,8 @@ mod toast {
layout::flex::Axis::Vertical,
renderer,
&limits,
- Length::Fill,
- Length::Fill,
+ Fill,
+ Fill,
10.into(),
10.0,
Alignment::End,
@@ -589,7 +589,7 @@ mod toast {
&mut self,
layout: Layout<'_>,
renderer: &Renderer,
- operation: &mut dyn widget::Operation<()>,
+ operation: &mut dyn widget::Operation,
) {
operation.container(None, layout.bounds(), &mut |operation| {
self.toasts
diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs
index 6ed50d31..a5f7b36a 100644
--- a/examples/todos/src/main.rs
+++ b/examples/todos/src/main.rs
@@ -1,11 +1,10 @@
-use iced::alignment::{self, Alignment};
use iced::keyboard;
use iced::widget::{
self, button, center, checkbox, column, container, keyed_column, row,
scrollable, text, text_input, Text,
};
use iced::window;
-use iced::{Element, Font, Length, Subscription, Task as Command};
+use iced::{Center, Element, Fill, Font, Subscription, Task as Command};
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
@@ -18,16 +17,14 @@ pub fn main() -> iced::Result {
tracing_subscriber::fmt::init();
iced::application(Todos::title, Todos::update, Todos::view)
- .load(Todos::load)
.subscription(Todos::subscription)
.font(include_bytes!("../fonts/icons.ttf").as_slice())
.window_size((500.0, 800.0))
- .run()
+ .run_with(Todos::new)
}
-#[derive(Default, Debug)]
+#[derive(Debug)]
enum Todos {
- #[default]
Loading,
Loaded(State),
}
@@ -54,8 +51,11 @@ enum Message {
}
impl Todos {
- fn load() -> Command<Message> {
- Command::perform(SavedState::load(), Message::Loaded)
+ fn new() -> (Self, Command<Message>) {
+ (
+ Self::Loading,
+ Command::perform(SavedState::load(), Message::Loaded),
+ )
}
fn title(&self) -> String {
@@ -192,17 +192,18 @@ impl Todos {
..
}) => {
let title = text("todos")
- .width(Length::Fill)
+ .width(Fill)
.size(100)
.color([0.5, 0.5, 0.5])
- .horizontal_alignment(alignment::Horizontal::Center);
+ .align_x(Center);
let input = text_input("What needs to be done?", input_value)
.id(INPUT_ID.clone())
.on_input(Message::InputChanged)
.on_submit(Message::CreateTask)
.padding(15)
- .size(30);
+ .size(30)
+ .align_x(Center);
let controls = view_controls(tasks, *filter);
let filtered_tasks =
@@ -239,10 +240,7 @@ impl Todos {
.spacing(20)
.max_width(800);
- scrollable(
- container(content).center_x(Length::Fill).padding(40),
- )
- .into()
+ scrollable(container(content).center_x(Fill).padding(40)).into()
}
}
}
@@ -342,7 +340,7 @@ impl Task {
TaskState::Idle => {
let checkbox = checkbox(&self.description, self.completed)
.on_toggle(TaskMessage::Completed)
- .width(Length::Fill)
+ .width(Fill)
.size(17)
.text_shaping(text::Shaping::Advanced);
@@ -354,7 +352,7 @@ impl Task {
.style(button::text),
]
.spacing(20)
- .align_items(Alignment::Center)
+ .align_y(Center)
.into()
}
TaskState::Editing => {
@@ -370,14 +368,14 @@ impl Task {
button(
row![delete_icon(), "Delete"]
.spacing(10)
- .align_items(Alignment::Center)
+ .align_y(Center)
)
.on_press(TaskMessage::Delete)
.padding(10)
.style(button::danger)
]
.spacing(20)
- .align_items(Alignment::Center)
+ .align_y(Center)
.into()
}
}
@@ -404,17 +402,16 @@ fn view_controls(tasks: &[Task], current_filter: Filter) -> Element<Message> {
"{tasks_left} {} left",
if tasks_left == 1 { "task" } else { "tasks" }
)
- .width(Length::Fill),
+ .width(Fill),
row![
filter_button("All", Filter::All, current_filter),
filter_button("Active", Filter::Active, current_filter),
filter_button("Completed", Filter::Completed, current_filter,),
]
- .width(Length::Shrink)
.spacing(10)
]
.spacing(20)
- .align_items(Alignment::Center)
+ .align_y(Center)
.into()
}
@@ -439,20 +436,15 @@ impl Filter {
}
fn loading_message<'a>() -> Element<'a, Message> {
- center(
- text("Loading...")
- .horizontal_alignment(alignment::Horizontal::Center)
- .size(50),
- )
- .into()
+ center(text("Loading...").width(Fill).align_x(Center).size(50)).into()
}
fn empty_message(message: &str) -> Element<'_, Message> {
center(
text(message)
- .width(Length::Fill)
+ .width(Fill)
.size(25)
- .horizontal_alignment(alignment::Horizontal::Center)
+ .align_x(Center)
.color([0.7, 0.7, 0.7]),
)
.height(200)
@@ -466,7 +458,7 @@ fn icon(unicode: char) -> Text<'static> {
text(unicode.to_string())
.font(ICONS)
.width(20)
- .horizontal_alignment(alignment::Horizontal::Center)
+ .align_x(Center)
}
fn edit_icon() -> Text<'static> {
diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs
index 94ba78ee..d8c0b29a 100644
--- a/examples/tour/src/main.rs
+++ b/examples/tour/src/main.rs
@@ -1,10 +1,9 @@
-use iced::alignment::{self, Alignment};
use iced::widget::{
button, checkbox, column, container, horizontal_space, image, radio, row,
scrollable, slider, text, text_input, toggler, vertical_space,
};
use iced::widget::{Button, Column, Container, Slider};
-use iced::{Color, Element, Font, Length, Pixels};
+use iced::{Center, Color, Element, Fill, Font, Pixels};
pub fn main() -> iced::Result {
#[cfg(target_arch = "wasm32")]
@@ -173,10 +172,10 @@ impl Tour {
} else {
content
})
- .center_x(Length::Fill),
+ .center_x(Fill),
);
- container(scrollable).center_y(Length::Fill).into()
+ container(scrollable).center_y(Fill).into()
}
fn can_continue(&self) -> bool {
@@ -235,11 +234,7 @@ impl Tour {
0 to 100:",
)
.push(slider(0..=100, self.slider, Message::SliderChanged))
- .push(
- text(self.slider.to_string())
- .width(Length::Fill)
- .horizontal_alignment(alignment::Horizontal::Center),
- )
+ .push(text(self.slider.to_string()).width(Fill).align_x(Center))
}
fn rows_and_columns(&self) -> Column<Message> {
@@ -268,9 +263,7 @@ impl Tour {
let spacing_section = column![
slider(0..=80, self.spacing, Message::SpacingChanged),
- text!("{} px", self.spacing)
- .width(Length::Fill)
- .horizontal_alignment(alignment::Horizontal::Center),
+ text!("{} px", self.spacing).width(Fill).align_x(Center),
]
.spacing(10);
@@ -364,11 +357,11 @@ impl Tour {
Self::container("Toggler")
.push("A toggler is mostly used to enable or disable something.")
.push(
- Container::new(toggler(
- "Toggle me to continue...".to_owned(),
- self.toggler,
- Message::TogglerChanged,
- ))
+ Container::new(
+ toggler(self.toggler)
+ .label("Toggle me to continue...")
+ .on_toggle(Message::TogglerChanged),
+ )
.padding([0, 40]),
)
}
@@ -381,11 +374,7 @@ impl Tour {
.push("An image that tries to keep its aspect ratio.")
.push(ferris(width, filter_method))
.push(slider(100..=500, width, Message::ImageWidthChanged))
- .push(
- text!("Width: {width} px")
- .width(Length::Fill)
- .horizontal_alignment(alignment::Horizontal::Center),
- )
+ .push(text!("Width: {width} px").width(Fill).align_x(Center))
.push(
checkbox(
"Use nearest interpolation",
@@ -393,7 +382,7 @@ impl Tour {
)
.on_toggle(Message::ImageUseNearestToggled),
)
- .align_items(Alignment::Center)
+ .align_x(Center)
}
fn scrollable(&self) -> Column<Message> {
@@ -409,18 +398,13 @@ impl Tour {
.push(vertical_space().height(4096))
.push(
text("You are halfway there!")
- .width(Length::Fill)
+ .width(Fill)
.size(30)
- .horizontal_alignment(alignment::Horizontal::Center),
+ .align_x(Center),
)
.push(vertical_space().height(4096))
.push(ferris(300, image::FilterMethod::Linear))
- .push(
- text("You made it!")
- .width(Length::Fill)
- .size(50)
- .horizontal_alignment(alignment::Horizontal::Center),
- )
+ .push(text("You made it!").width(Fill).size(50).align_x(Center))
}
fn text_input(&self) -> Column<Message> {
@@ -464,8 +448,8 @@ impl Tour {
} else {
value
})
- .width(Length::Fill)
- .horizontal_alignment(alignment::Horizontal::Center),
+ .width(Fill)
+ .align_x(Center),
)
}
@@ -570,7 +554,7 @@ fn ferris<'a>(
.filter_method(filter_method)
.width(width),
)
- .center_x(Length::Fill)
+ .center_x(Fill)
}
fn padded_button<Message: Clone>(label: &str) -> Button<'_, Message> {
diff --git a/examples/vectorial_text/src/main.rs b/examples/vectorial_text/src/main.rs
index 6dd3273a..ce34d826 100644
--- a/examples/vectorial_text/src/main.rs
+++ b/examples/vectorial_text/src/main.rs
@@ -1,9 +1,9 @@
-use iced::alignment::{self, Alignment};
+use iced::alignment;
use iced::mouse;
use iced::widget::{
canvas, checkbox, column, horizontal_space, row, slider, text,
};
-use iced::{Element, Length, Point, Rectangle, Renderer, Theme, Vector};
+use iced::{Center, Element, Fill, Point, Rectangle, Renderer, Theme, Vector};
pub fn main() -> iced::Result {
iced::application(
@@ -59,7 +59,7 @@ impl VectorialText {
};
column![
- canvas(&self.state).width(Length::Fill).height(Length::Fill),
+ canvas(&self.state).width(Fill).height(Fill),
column![
checkbox("Use Japanese", self.state.use_japanese,)
.on_toggle(Message::ToggleJapanese),
@@ -85,7 +85,7 @@ impl VectorialText {
]
.spacing(20),
]
- .align_items(Alignment::Center)
+ .align_x(Center)
.spacing(10)
]
.spacing(10)
diff --git a/examples/visible_bounds/src/main.rs b/examples/visible_bounds/src/main.rs
index e46d1ff0..77fec65e 100644
--- a/examples/visible_bounds/src/main.rs
+++ b/examples/visible_bounds/src/main.rs
@@ -5,8 +5,8 @@ use iced::widget::{
};
use iced::window;
use iced::{
- Alignment, Color, Element, Font, Length, Point, Rectangle, Subscription,
- Task, Theme,
+ Center, Color, Element, Fill, Font, Point, Rectangle, Subscription, Task,
+ Theme,
};
pub fn main() -> iced::Result {
@@ -70,7 +70,7 @@ impl Example {
.color_maybe(color),
]
.height(40)
- .align_items(Alignment::Center)
+ .align_y(Center)
};
let view_bounds = |label, bounds: Option<Rectangle>| {
@@ -130,13 +130,13 @@ impl Example {
.padding(20)
)
.on_scroll(|_| Message::Scrolled)
- .width(Length::Fill)
+ .width(Fill)
.height(300),
]
.padding(20)
)
.on_scroll(|_| Message::Scrolled)
- .width(Length::Fill)
+ .width(Fill)
.height(300),
]
.spacing(10)
diff --git a/examples/websocket/src/echo.rs b/examples/websocket/src/echo.rs
index cd32cb66..14652936 100644
--- a/examples/websocket/src/echo.rs
+++ b/examples/websocket/src/echo.rs
@@ -1,87 +1,79 @@
pub mod server;
use iced::futures;
-use iced::subscription::{self, Subscription};
+use iced::stream;
use iced::widget::text;
use futures::channel::mpsc;
use futures::sink::SinkExt;
-use futures::stream::StreamExt;
+use futures::stream::{Stream, StreamExt};
use async_tungstenite::tungstenite;
use std::fmt;
-pub fn connect() -> Subscription<Event> {
- struct Connect;
+pub fn connect() -> impl Stream<Item = Event> {
+ stream::channel(100, |mut output| async move {
+ let mut state = State::Disconnected;
- subscription::channel(
- std::any::TypeId::of::<Connect>(),
- 100,
- |mut output| async move {
- let mut state = State::Disconnected;
+ loop {
+ match &mut state {
+ State::Disconnected => {
+ const ECHO_SERVER: &str = "ws://127.0.0.1:3030";
- loop {
- match &mut state {
- State::Disconnected => {
- const ECHO_SERVER: &str = "ws://127.0.0.1:3030";
-
- match async_tungstenite::tokio::connect_async(
- ECHO_SERVER,
- )
+ 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;
+ {
+ Ok((websocket, _)) => {
+ let (sender, receiver) = mpsc::channel(100);
- state = State::Connected(websocket, receiver);
- }
- Err(_) => {
- tokio::time::sleep(
- tokio::time::Duration::from_secs(1),
- )
+ let _ = output
+ .send(Event::Connected(Connection(sender)))
.await;
- let _ = output.send(Event::Disconnected).await;
- }
+ state = State::Connected(websocket, receiver);
+ }
+ Err(_) => {
+ tokio::time::sleep(
+ tokio::time::Duration::from_secs(1),
+ )
+ .await;
+
+ let _ = output.send(Event::Disconnected).await;
}
}
- 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,
+ }
+ 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;
}
- }
-
- message = input.select_next_some() => {
- let result = websocket.send(tungstenite::Message::Text(message.to_string())).await;
-
- if result.is_err() {
+ Err(_) => {
let _ = output.send(Event::Disconnected).await;
state = State::Disconnected;
}
+ Ok(_) => continue,
+ }
+ }
+
+ 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;
}
}
}
}
}
- },
- )
+ }
+ })
}
#[derive(Debug)]
diff --git a/examples/websocket/src/main.rs b/examples/websocket/src/main.rs
index 8422ce16..8b1efb41 100644
--- a/examples/websocket/src/main.rs
+++ b/examples/websocket/src/main.rs
@@ -1,20 +1,17 @@
mod echo;
-use iced::alignment::{self, Alignment};
use iced::widget::{
self, button, center, column, row, scrollable, text, text_input,
};
-use iced::{color, Element, Length, Subscription, Task};
+use iced::{color, Center, Element, Fill, Subscription, Task};
use once_cell::sync::Lazy;
pub fn main() -> iced::Result {
iced::application("WebSocket - Iced", WebSocket::update, WebSocket::view)
- .load(WebSocket::load)
.subscription(WebSocket::subscription)
- .run()
+ .run_with(WebSocket::new)
}
-#[derive(Default)]
struct WebSocket {
messages: Vec<echo::Message>,
new_message: String,
@@ -30,11 +27,18 @@ enum Message {
}
impl WebSocket {
- fn load() -> Task<Message> {
- Task::batch([
- Task::perform(echo::server::run(), |_| Message::Server),
- widget::focus_next(),
- ])
+ fn new() -> (Self, Task<Message>) {
+ (
+ Self {
+ messages: Vec::new(),
+ new_message: String::new(),
+ state: State::Disconnected,
+ },
+ Task::batch([
+ Task::perform(echo::server::run(), |_| Message::Server),
+ widget::focus_next(),
+ ]),
+ )
}
fn update(&mut self, message: Message) -> Task<Message> {
@@ -83,7 +87,7 @@ impl WebSocket {
}
fn subscription(&self) -> Subscription<Message> {
- echo::connect().map(Message::Echo)
+ Subscription::run(echo::connect).map(Message::Echo)
}
fn view(&self) -> Element<Message> {
@@ -99,7 +103,7 @@ impl WebSocket {
.spacing(10),
)
.id(MESSAGE_LOG.clone())
- .height(Length::Fill)
+ .height(Fill)
.into()
};
@@ -108,12 +112,8 @@ impl WebSocket {
.on_input(Message::NewMessageChanged)
.padding(10);
- let mut button = button(
- text("Send")
- .height(40)
- .vertical_alignment(alignment::Vertical::Center),
- )
- .padding([0, 20]);
+ let mut button = button(text("Send").height(40).align_y(Center))
+ .padding([0, 20]);
if matches!(self.state, State::Connected(_)) {
if let Some(message) = echo::Message::new(&self.new_message) {
@@ -122,13 +122,11 @@ impl WebSocket {
}
}
- row![input, button]
- .spacing(10)
- .align_items(Alignment::Center)
+ row![input, button].spacing(10).align_y(Center)
};
column![message_log, new_message_input]
- .height(Length::Fill)
+ .height(Fill)
.padding(20)
.spacing(10)
.into()
@@ -140,10 +138,4 @@ enum State {
Connected(echo::Connection),
}
-impl Default for State {
- fn default() -> Self {
- Self::Disconnected
- }
-}
-
static MESSAGE_LOG: Lazy<scrollable::Id> = Lazy::new(scrollable::Id::unique);
diff --git a/futures/src/backend/native/async_std.rs b/futures/src/backend/native/async_std.rs
index b7da5e90..86714f45 100644
--- a/futures/src/backend/native/async_std.rs
+++ b/futures/src/backend/native/async_std.rs
@@ -27,7 +27,7 @@ pub mod time {
pub fn every(
duration: std::time::Duration,
) -> Subscription<std::time::Instant> {
- Subscription::from_recipe(Every(duration))
+ subscription::from_recipe(Every(duration))
}
#[derive(Debug)]
diff --git a/futures/src/backend/native/smol.rs b/futures/src/backend/native/smol.rs
index aaf1518c..8d448e7f 100644
--- a/futures/src/backend/native/smol.rs
+++ b/futures/src/backend/native/smol.rs
@@ -26,7 +26,7 @@ pub mod time {
pub fn every(
duration: std::time::Duration,
) -> Subscription<std::time::Instant> {
- Subscription::from_recipe(Every(duration))
+ subscription::from_recipe(Every(duration))
}
#[derive(Debug)]
diff --git a/futures/src/backend/native/tokio.rs b/futures/src/backend/native/tokio.rs
index df91d798..9dc3593d 100644
--- a/futures/src/backend/native/tokio.rs
+++ b/futures/src/backend/native/tokio.rs
@@ -31,7 +31,7 @@ pub mod time {
pub fn every(
duration: std::time::Duration,
) -> Subscription<std::time::Instant> {
- Subscription::from_recipe(Every(duration))
+ subscription::from_recipe(Every(duration))
}
#[derive(Debug)]
diff --git a/futures/src/backend/wasm/wasm_bindgen.rs b/futures/src/backend/wasm/wasm_bindgen.rs
index 3228dd18..f7846c01 100644
--- a/futures/src/backend/wasm/wasm_bindgen.rs
+++ b/futures/src/backend/wasm/wasm_bindgen.rs
@@ -26,7 +26,7 @@ pub mod time {
pub fn every(
duration: std::time::Duration,
) -> Subscription<wasm_timer::Instant> {
- Subscription::from_recipe(Every(duration))
+ subscription::from_recipe(Every(duration))
}
#[derive(Debug)]
diff --git a/futures/src/keyboard.rs b/futures/src/keyboard.rs
index f0d7d757..35f6b6fa 100644
--- a/futures/src/keyboard.rs
+++ b/futures/src/keyboard.rs
@@ -6,7 +6,7 @@ use crate::subscription::{self, Subscription};
use crate::MaybeSend;
/// Listens to keyboard key presses and calls the given function
-/// map them into actual messages.
+/// to map them into actual messages.
///
/// If the function returns `None`, the key press will be simply
/// ignored.
@@ -31,7 +31,7 @@ where
}
/// Listens to keyboard key releases and calls the given function
-/// map them into actual messages.
+/// to map them into actual messages.
///
/// If the function returns `None`, the key release will be simply
/// ignored.
diff --git a/futures/src/lib.rs b/futures/src/lib.rs
index a874a618..31738823 100644
--- a/futures/src/lib.rs
+++ b/futures/src/lib.rs
@@ -15,6 +15,7 @@ pub mod backend;
pub mod event;
pub mod executor;
pub mod keyboard;
+pub mod stream;
pub mod subscription;
pub use executor::Executor;
diff --git a/futures/src/stream.rs b/futures/src/stream.rs
new file mode 100644
index 00000000..af2f8c99
--- /dev/null
+++ b/futures/src/stream.rs
@@ -0,0 +1,46 @@
+//! Create asynchronous streams of data.
+use futures::channel::mpsc;
+use futures::stream::{self, Stream, StreamExt};
+
+use std::future::Future;
+
+/// Creates a new [`Stream`] that produces the items sent from a [`Future`]
+/// to the [`mpsc::Sender`] provided to the closure.
+///
+/// This is a more ergonomic [`stream::unfold`], which allows you to go
+/// from the "world of futures" to the "world of streams" by simply looping
+/// and publishing to an async channel from inside a [`Future`].
+pub fn channel<T, F>(
+ size: usize,
+ f: impl FnOnce(mpsc::Sender<T>) -> F,
+) -> impl Stream<Item = T>
+where
+ F: Future<Output = ()>,
+{
+ let (sender, receiver) = mpsc::channel(size);
+
+ let runner = stream::once(f(sender)).filter_map(|_| async { None });
+
+ stream::select(receiver, runner)
+}
+
+/// Creates a new [`Stream`] that produces the items sent from a [`Future`]
+/// that can fail to the [`mpsc::Sender`] provided to the closure.
+pub fn try_channel<T, E, F>(
+ size: usize,
+ f: impl FnOnce(mpsc::Sender<T>) -> F,
+) -> impl Stream<Item = Result<T, E>>
+where
+ F: Future<Output = Result<(), E>>,
+{
+ let (sender, receiver) = mpsc::channel(size);
+
+ let runner = stream::once(f(sender)).filter_map(|result| async {
+ match result {
+ Ok(()) => None,
+ Err(error) => Some(Err(error)),
+ }
+ });
+
+ stream::select(receiver.map(Ok), runner)
+}
diff --git a/futures/src/subscription.rs b/futures/src/subscription.rs
index 316fc44d..d2a0c3f8 100644
--- a/futures/src/subscription.rs
+++ b/futures/src/subscription.rs
@@ -5,11 +5,9 @@ pub use tracker::Tracker;
use crate::core::event;
use crate::core::window;
-use crate::futures::{Future, Stream};
+use crate::futures::Stream;
use crate::{BoxStream, MaybeSend};
-use futures::channel::mpsc;
-use futures::never::Never;
use std::any::TypeId;
use std::hash::Hash;
@@ -61,20 +59,66 @@ pub type Hasher = rustc_hash::FxHasher;
/// A request to listen to external events.
///
-/// Besides performing async actions on demand with `Command`, most
+/// Besides performing async actions on demand with `Task`, most
/// applications also need to listen to external events passively.
///
-/// A [`Subscription`] is normally provided to some runtime, like a `Command`,
+/// A [`Subscription`] is normally provided to some runtime, like a `Task`,
/// and it will generate events as long as the user keeps requesting it.
///
/// For instance, you can use a [`Subscription`] to listen to a `WebSocket`
/// connection, keyboard presses, mouse events, time ticks, etc.
+///
+/// # The Lifetime of a [`Subscription`]
+/// Much like a [`Future`] or a [`Stream`], a [`Subscription`] does not produce any effects
+/// on its own. For a [`Subscription`] to run, it must be returned to the iced runtime—normally
+/// in the `subscription` function of an `application` or a `daemon`.
+///
+/// When a [`Subscription`] is provided to the runtime for the first time, the runtime will
+/// start running it asynchronously. Running a [`Subscription`] consists in building its underlying
+/// [`Stream`] and executing it in an async runtime.
+///
+/// Therefore, you can think of a [`Subscription`] as a "stream builder". It simply represents a way
+/// to build a certain [`Stream`] together with some way to _identify_ it.
+///
+/// Identification is important because when a specific [`Subscription`] stops being returned to the
+/// iced runtime, the runtime will kill its associated [`Stream`]. The runtime uses the identity of a
+/// [`Subscription`] to keep track of it.
+///
+/// This way, iced allows you to declaratively __subscribe__ to particular streams of data temporarily
+/// and whenever necessary.
+///
+/// ```
+/// # mod iced {
+/// # pub mod time {
+/// # pub use iced_futures::backend::default::time::every;
+/// # pub use std::time::{Duration, Instant};
+/// # }
+/// #
+/// # pub use iced_futures::Subscription;
+/// # }
+/// use iced::time::{self, Duration, Instant};
+/// use iced::Subscription;
+///
+/// struct State {
+/// timer_enabled: bool,
+/// }
+///
+/// fn subscription(state: &State) -> Subscription<Instant> {
+/// if state.timer_enabled {
+/// time::every(Duration::from_secs(1))
+/// } else {
+/// Subscription::none()
+/// }
+/// }
+/// ```
+///
+/// [`Future`]: std::future::Future
#[must_use = "`Subscription` must be returned to runtime to take effect"]
-pub struct Subscription<Message> {
- recipes: Vec<Box<dyn Recipe<Output = Message>>>,
+pub struct Subscription<T> {
+ recipes: Vec<Box<dyn Recipe<Output = T>>>,
}
-impl<Message> Subscription<Message> {
+impl<T> Subscription<T> {
/// Returns an empty [`Subscription`] that will not produce any output.
pub fn none() -> Self {
Self {
@@ -82,19 +126,102 @@ impl<Message> Subscription<Message> {
}
}
- /// Creates a [`Subscription`] from a [`Recipe`] describing it.
- pub fn from_recipe(
- recipe: impl Recipe<Output = Message> + 'static,
- ) -> Self {
- Self {
- recipes: vec![Box::new(recipe)],
- }
+ /// Returns a [`Subscription`] that will call the given function to create and
+ /// asynchronously run the given [`Stream`].
+ ///
+ /// # Creating an asynchronous worker with bidirectional communication
+ /// You can leverage this helper to create a [`Subscription`] that spawns
+ /// an asynchronous worker in the background and establish a channel of
+ /// communication with an `iced` application.
+ ///
+ /// You can achieve this by creating an `mpsc` channel inside the closure
+ /// and returning the `Sender` as a `Message` for the `Application`:
+ ///
+ /// ```
+ /// use iced_futures::subscription::{self, Subscription};
+ /// use iced_futures::stream;
+ /// use iced_futures::futures::channel::mpsc;
+ /// use iced_futures::futures::sink::SinkExt;
+ /// use iced_futures::futures::Stream;
+ ///
+ /// pub enum Event {
+ /// Ready(mpsc::Sender<Input>),
+ /// WorkFinished,
+ /// // ...
+ /// }
+ ///
+ /// enum Input {
+ /// DoSomeWork,
+ /// // ...
+ /// }
+ ///
+ /// fn some_worker() -> impl Stream<Item = Event> {
+ /// stream::channel(100, |mut output| async move {
+ /// // Create channel
+ /// let (sender, mut receiver) = mpsc::channel(100);
+ ///
+ /// // Send the sender back to the application
+ /// output.send(Event::Ready(sender)).await;
+ ///
+ /// loop {
+ /// use iced_futures::futures::StreamExt;
+ ///
+ /// // Read next input sent from `Application`
+ /// let input = receiver.select_next_some().await;
+ ///
+ /// match input {
+ /// Input::DoSomeWork => {
+ /// // Do some async work...
+ ///
+ /// // Finally, we can optionally produce a message to tell the
+ /// // `Application` the work is done
+ /// output.send(Event::WorkFinished).await;
+ /// }
+ /// }
+ /// }
+ /// })
+ /// }
+ ///
+ /// fn subscription() -> Subscription<Event> {
+ /// Subscription::run(some_worker)
+ /// }
+ /// ```
+ ///
+ /// Check out the [`websocket`] example, which showcases this pattern to maintain a `WebSocket`
+ /// connection open.
+ ///
+ /// [`websocket`]: https://github.com/iced-rs/iced/tree/0.12/examples/websocket
+ pub fn run<S>(builder: fn() -> S) -> Self
+ where
+ S: Stream<Item = T> + MaybeSend + 'static,
+ T: 'static,
+ {
+ from_recipe(Runner {
+ id: builder,
+ spawn: move |_| builder(),
+ })
+ }
+
+ /// Returns a [`Subscription`] that will create and asynchronously run the
+ /// given [`Stream`].
+ ///
+ /// The `id` will be used to uniquely identify the [`Subscription`].
+ pub fn run_with_id<I, S>(id: I, stream: S) -> Subscription<T>
+ where
+ I: Hash + 'static,
+ S: Stream<Item = T> + MaybeSend + 'static,
+ T: 'static,
+ {
+ from_recipe(Runner {
+ id,
+ spawn: move |_| stream,
+ })
}
/// Batches all the provided subscriptions and returns the resulting
/// [`Subscription`].
pub fn batch(
- subscriptions: impl IntoIterator<Item = Subscription<Message>>,
+ subscriptions: impl IntoIterator<Item = Subscription<T>>,
) -> Self {
Self {
recipes: subscriptions
@@ -104,18 +231,13 @@ impl<Message> Subscription<Message> {
}
}
- /// Returns the different recipes of the [`Subscription`].
- pub fn into_recipes(self) -> Vec<Box<dyn Recipe<Output = Message>>> {
- self.recipes
- }
-
/// Adds a value to the [`Subscription`] context.
///
/// The value will be part of the identity of a [`Subscription`].
- pub fn with<T>(mut self, value: T) -> Subscription<(T, Message)>
+ pub fn with<A>(mut self, value: A) -> Subscription<(A, T)>
where
- Message: 'static,
- T: std::hash::Hash + Clone + Send + Sync + 'static,
+ T: 'static,
+ A: std::hash::Hash + Clone + Send + Sync + 'static,
{
Subscription {
recipes: self
@@ -123,7 +245,7 @@ impl<Message> Subscription<Message> {
.drain(..)
.map(|recipe| {
Box::new(With::new(recipe, value.clone()))
- as Box<dyn Recipe<Output = (T, Message)>>
+ as Box<dyn Recipe<Output = (A, T)>>
})
.collect(),
}
@@ -136,8 +258,8 @@ impl<Message> Subscription<Message> {
/// will panic in debug mode otherwise.
pub fn map<F, A>(mut self, f: F) -> Subscription<A>
where
- Message: 'static,
- F: Fn(Message) -> A + MaybeSend + Clone + 'static,
+ T: 'static,
+ F: Fn(T) -> A + MaybeSend + Clone + 'static,
A: 'static,
{
debug_assert!(
@@ -159,7 +281,23 @@ impl<Message> Subscription<Message> {
}
}
-impl<Message> std::fmt::Debug for Subscription<Message> {
+/// Creates a [`Subscription`] from a [`Recipe`] describing it.
+pub fn from_recipe<T>(
+ recipe: impl Recipe<Output = T> + 'static,
+) -> Subscription<T> {
+ Subscription {
+ recipes: vec![Box::new(recipe)],
+ }
+}
+
+/// Returns the different recipes of the [`Subscription`].
+pub fn into_recipes<T>(
+ subscription: Subscription<T>,
+) -> Vec<Box<dyn Recipe<Output = T>>> {
+ subscription.recipes
+}
+
+impl<T> std::fmt::Debug for Subscription<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Subscription").finish()
}
@@ -175,9 +313,9 @@ impl<Message> std::fmt::Debug for Subscription<Message> {
/// The repository has a couple of [examples] that use a custom [`Recipe`]:
///
/// - [`download_progress`], a basic application that asynchronously downloads
-/// a dummy file of 100 MB and tracks the download progress.
+/// a dummy file of 100 MB and tracks the download progress.
/// - [`stopwatch`], a watch with start/stop and reset buttons showcasing how
-/// to listen to time.
+/// to listen to time.
///
/// [examples]: https://github.com/iced-rs/iced/tree/0.12/examples
/// [`download_progress`]: https://github.com/iced-rs/iced/tree/0.12/examples/download_progress
@@ -273,65 +411,13 @@ where
}
}
-/// Returns a [`Subscription`] that will call the given function to create and
-/// asynchronously run the given [`Stream`].
-pub fn run<S, Message>(builder: fn() -> S) -> Subscription<Message>
-where
- S: Stream<Item = Message> + MaybeSend + 'static,
- Message: 'static,
-{
- Subscription::from_recipe(Runner {
- id: builder,
- spawn: move |_| builder(),
- })
-}
-
-/// Returns a [`Subscription`] that will create and asynchronously run the
-/// given [`Stream`].
-///
-/// The `id` will be used to uniquely identify the [`Subscription`].
-pub fn run_with_id<I, S, Message>(id: I, stream: S) -> Subscription<Message>
-where
- I: Hash + 'static,
- S: Stream<Item = Message> + MaybeSend + 'static,
- Message: 'static,
-{
- Subscription::from_recipe(Runner {
- id,
- spawn: move |_| stream,
- })
-}
-
-/// Returns a [`Subscription`] that will create and asynchronously run a
-/// [`Stream`] that will call the provided closure to produce every `Message`.
-///
-/// The `id` will be used to uniquely identify the [`Subscription`].
-pub fn unfold<I, T, Fut, Message>(
- id: I,
- initial: T,
- mut f: impl FnMut(T) -> Fut + MaybeSend + Sync + 'static,
-) -> Subscription<Message>
+pub(crate) fn filter_map<I, F, T>(id: I, f: F) -> Subscription<T>
where
I: Hash + 'static,
- T: MaybeSend + 'static,
- Fut: Future<Output = (Message, T)> + MaybeSend + 'static,
- Message: 'static + MaybeSend,
+ F: Fn(Event) -> Option<T> + MaybeSend + 'static,
+ T: 'static + MaybeSend,
{
- use futures::future::FutureExt;
-
- run_with_id(
- id,
- futures::stream::unfold(initial, move |state| f(state).map(Some)),
- )
-}
-
-pub(crate) fn filter_map<I, F, Message>(id: I, f: F) -> Subscription<Message>
-where
- I: Hash + 'static,
- F: Fn(Event) -> Option<Message> + MaybeSend + 'static,
- Message: 'static + MaybeSend,
-{
- Subscription::from_recipe(Runner {
+ from_recipe(Runner {
id,
spawn: |events| {
use futures::future;
@@ -342,122 +428,22 @@ where
})
}
-/// Creates a [`Subscription`] that publishes the events sent from a [`Future`]
-/// to an [`mpsc::Sender`] with the given bounds.
-///
-/// # Creating an asynchronous worker with bidirectional communication
-/// You can leverage this helper to create a [`Subscription`] that spawns
-/// an asynchronous worker in the background and establish a channel of
-/// communication with an `iced` application.
-///
-/// You can achieve this by creating an `mpsc` channel inside the closure
-/// and returning the `Sender` as a `Message` for the `Application`:
-///
-/// ```
-/// use iced_futures::subscription::{self, Subscription};
-/// use iced_futures::futures::channel::mpsc;
-/// use iced_futures::futures::sink::SinkExt;
-///
-/// pub enum Event {
-/// Ready(mpsc::Sender<Input>),
-/// WorkFinished,
-/// // ...
-/// }
-///
-/// enum Input {
-/// DoSomeWork,
-/// // ...
-/// }
-///
-/// enum State {
-/// Starting,
-/// Ready(mpsc::Receiver<Input>),
-/// }
-///
-/// fn some_worker() -> Subscription<Event> {
-/// struct SomeWorker;
-///
-/// subscription::channel(std::any::TypeId::of::<SomeWorker>(), 100, |mut output| async move {
-/// let mut state = State::Starting;
-///
-/// loop {
-/// match &mut state {
-/// State::Starting => {
-/// // Create channel
-/// let (sender, receiver) = mpsc::channel(100);
-///
-/// // Send the sender back to the application
-/// output.send(Event::Ready(sender)).await;
-///
-/// // We are ready to receive messages
-/// state = State::Ready(receiver);
-/// }
-/// State::Ready(receiver) => {
-/// use iced_futures::futures::StreamExt;
-///
-/// // Read next input sent from `Application`
-/// let input = receiver.select_next_some().await;
-///
-/// match input {
-/// Input::DoSomeWork => {
-/// // Do some async work...
-///
-/// // Finally, we can optionally produce a message to tell the
-/// // `Application` the work is done
-/// output.send(Event::WorkFinished).await;
-/// }
-/// }
-/// }
-/// }
-/// }
-/// })
-/// }
-/// ```
-///
-/// Check out the [`websocket`] example, which showcases this pattern to maintain a `WebSocket`
-/// connection open.
-///
-/// [`websocket`]: https://github.com/iced-rs/iced/tree/0.12/examples/websocket
-pub fn channel<I, Fut, Message>(
- id: I,
- size: usize,
- f: impl FnOnce(mpsc::Sender<Message>) -> Fut + MaybeSend + 'static,
-) -> Subscription<Message>
-where
- I: Hash + 'static,
- Fut: Future<Output = Never> + MaybeSend + 'static,
- Message: 'static + MaybeSend,
-{
- use futures::stream::{self, StreamExt};
-
- Subscription::from_recipe(Runner {
- id,
- spawn: move |_| {
- let (sender, receiver) = mpsc::channel(size);
-
- let runner = stream::once(f(sender)).map(|_| unreachable!());
-
- stream::select(receiver, runner)
- },
- })
-}
-
-struct Runner<I, F, S, Message>
+struct Runner<I, F, S, T>
where
F: FnOnce(EventStream) -> S,
- S: Stream<Item = Message>,
+ S: Stream<Item = T>,
{
id: I,
spawn: F,
}
-impl<I, S, F, Message> Recipe for Runner<I, F, S, Message>
+impl<I, F, S, T> Recipe for Runner<I, F, S, T>
where
I: Hash + 'static,
F: FnOnce(EventStream) -> S,
- S: Stream<Item = Message> + MaybeSend + 'static,
+ S: Stream<Item = T> + MaybeSend + 'static,
{
- type Output = Message;
+ type Output = T;
fn hash(&self, state: &mut Hasher) {
std::any::TypeId::of::<I>().hash(state);
diff --git a/futures/src/subscription/tracker.rs b/futures/src/subscription/tracker.rs
index f17e3ea3..6daead24 100644
--- a/futures/src/subscription/tracker.rs
+++ b/futures/src/subscription/tracker.rs
@@ -42,10 +42,10 @@ impl Tracker {
/// method:
///
/// - If the provided [`Subscription`] contains a new [`Recipe`] that is
- /// currently not being run, it will spawn a new stream and keep it alive.
+ /// currently not being run, it will spawn a new stream and keep it alive.
/// - On the other hand, if a [`Recipe`] is currently in execution and the
- /// provided [`Subscription`] does not contain it anymore, then the
- /// [`Tracker`] will close and drop the relevant stream.
+ /// provided [`Subscription`] does not contain it anymore, then the
+ /// [`Tracker`] will close and drop the relevant stream.
///
/// It returns a list of futures that need to be spawned to materialize
/// the [`Tracker`] changes.
diff --git a/graphics/Cargo.toml b/graphics/Cargo.toml
index e8d27d07..7e2d767b 100644
--- a/graphics/Cargo.toml
+++ b/graphics/Cargo.toml
@@ -20,6 +20,7 @@ all-features = true
[features]
geometry = ["lyon_path"]
image = ["dep:image", "kamadak-exif"]
+svg = []
web-colors = []
fira-sans = []
diff --git a/graphics/src/geometry.rs b/graphics/src/geometry.rs
index ab4a7a36..2b4b45a6 100644
--- a/graphics/src/geometry.rs
+++ b/graphics/src/geometry.rs
@@ -16,6 +16,7 @@ pub use stroke::{LineCap, LineDash, LineJoin, Stroke};
pub use style::Style;
pub use text::Text;
+pub use crate::core::{Image, Svg};
pub use crate::gradient::{self, Gradient};
use crate::cache::Cached;
diff --git a/graphics/src/geometry/fill.rs b/graphics/src/geometry/fill.rs
index 670fbc12..b79a2582 100644
--- a/graphics/src/geometry/fill.rs
+++ b/graphics/src/geometry/fill.rs
@@ -7,7 +7,7 @@ use crate::core::Color;
use crate::gradient::{self, Gradient};
/// The style used to fill geometry.
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, Copy)]
pub struct Fill {
/// The color or gradient of the fill.
///
diff --git a/graphics/src/geometry/frame.rs b/graphics/src/geometry/frame.rs
index 377589d7..3dee7e75 100644
--- a/graphics/src/geometry/frame.rs
+++ b/graphics/src/geometry/frame.rs
@@ -1,6 +1,6 @@
//! Draw and generate geometry.
use crate::core::{Point, Radians, Rectangle, Size, Vector};
-use crate::geometry::{self, Fill, Path, Stroke, Text};
+use crate::geometry::{self, Fill, Image, Path, Stroke, Svg, Text};
/// The region of a surface that can be used to draw geometry.
#[allow(missing_debug_implementations)]
@@ -65,6 +65,17 @@ where
self.raw.stroke(path, stroke);
}
+ /// Draws the stroke of an axis-aligned rectangle with the provided style
+ /// given its top-left corner coordinate and its `Size` on the [`Frame`] .
+ pub fn stroke_rectangle<'a>(
+ &mut self,
+ top_left: Point,
+ size: Size,
+ stroke: impl Into<Stroke<'a>>,
+ ) {
+ self.raw.stroke_rectangle(top_left, size, stroke);
+ }
+
/// Draws the characters of the given [`Text`] on the [`Frame`], filling
/// them with the given color.
///
@@ -75,6 +86,18 @@ where
self.raw.fill_text(text);
}
+ /// Draws the given [`Image`] on the [`Frame`] inside the given bounds.
+ #[cfg(feature = "image")]
+ pub fn draw_image(&mut self, bounds: Rectangle, image: impl Into<Image>) {
+ self.raw.draw_image(bounds, image);
+ }
+
+ /// Draws the given [`Svg`] on the [`Frame`] inside the given bounds.
+ #[cfg(feature = "svg")]
+ pub fn draw_svg(&mut self, bounds: Rectangle, svg: impl Into<Svg>) {
+ self.raw.draw_svg(bounds, svg);
+ }
+
/// Stores the current transform of the [`Frame`] and executes the given
/// drawing operations, restoring the transform afterwards.
///
@@ -116,8 +139,7 @@ where
let mut frame = self.draft(region);
let result = f(&mut frame);
-
- self.paste(frame, Point::new(region.x, region.y));
+ self.paste(frame);
result
}
@@ -134,8 +156,8 @@ where
}
/// Draws the contents of the given [`Frame`] with origin at the given [`Point`].
- fn paste(&mut self, frame: Self, at: Point) {
- self.raw.paste(frame.raw, at);
+ fn paste(&mut self, frame: Self) {
+ self.raw.paste(frame.raw);
}
/// Applies a translation to the current transform of the [`Frame`].
@@ -186,9 +208,15 @@ pub trait Backend: Sized {
fn scale_nonuniform(&mut self, scale: impl Into<Vector>);
fn draft(&mut self, clip_bounds: Rectangle) -> Self;
- fn paste(&mut self, frame: Self, at: Point);
+ fn paste(&mut self, frame: Self);
fn stroke<'a>(&mut self, path: &Path, stroke: impl Into<Stroke<'a>>);
+ fn stroke_rectangle<'a>(
+ &mut self,
+ top_left: Point,
+ size: Size,
+ stroke: impl Into<Stroke<'a>>,
+ );
fn fill(&mut self, path: &Path, fill: impl Into<Fill>);
fn fill_text(&mut self, text: impl Into<Text>);
@@ -199,6 +227,9 @@ pub trait Backend: Sized {
fill: impl Into<Fill>,
);
+ fn draw_image(&mut self, bounds: Rectangle, image: impl Into<Image>);
+ fn draw_svg(&mut self, bounds: Rectangle, svg: impl Into<Svg>);
+
fn into_geometry(self) -> Self::Geometry;
}
@@ -231,9 +262,16 @@ impl Backend for () {
fn scale_nonuniform(&mut self, _scale: impl Into<Vector>) {}
fn draft(&mut self, _clip_bounds: Rectangle) -> Self {}
- fn paste(&mut self, _frame: Self, _at: Point) {}
+ fn paste(&mut self, _frame: Self) {}
fn stroke<'a>(&mut self, _path: &Path, _stroke: impl Into<Stroke<'a>>) {}
+ fn stroke_rectangle<'a>(
+ &mut self,
+ _top_left: Point,
+ _size: Size,
+ _stroke: impl Into<Stroke<'a>>,
+ ) {
+ }
fn fill(&mut self, _path: &Path, _fill: impl Into<Fill>) {}
fn fill_text(&mut self, _text: impl Into<Text>) {}
@@ -245,5 +283,8 @@ impl Backend for () {
) {
}
+ fn draw_image(&mut self, _bounds: Rectangle, _image: impl Into<Image>) {}
+ fn draw_svg(&mut self, _bounds: Rectangle, _svg: impl Into<Svg>) {}
+
fn into_geometry(self) -> Self::Geometry {}
}
diff --git a/graphics/src/geometry/stroke.rs b/graphics/src/geometry/stroke.rs
index aff49ab3..b8f4515e 100644
--- a/graphics/src/geometry/stroke.rs
+++ b/graphics/src/geometry/stroke.rs
@@ -6,7 +6,7 @@ pub use crate::geometry::Style;
use iced_core::Color;
/// The style of a stroke.
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, Copy)]
pub struct Stroke<'a> {
/// The color or gradient of the stroke.
///
diff --git a/graphics/src/geometry/style.rs b/graphics/src/geometry/style.rs
index a0f4b08a..de77eccc 100644
--- a/graphics/src/geometry/style.rs
+++ b/graphics/src/geometry/style.rs
@@ -2,7 +2,7 @@ use crate::core::Color;
use crate::geometry::Gradient;
/// The coloring style of some drawing.
-#[derive(Debug, Clone, PartialEq)]
+#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Style {
/// A solid [`Color`].
Solid(Color),
diff --git a/graphics/src/geometry/text.rs b/graphics/src/geometry/text.rs
index d314e85e..90147f87 100644
--- a/graphics/src/geometry/text.rs
+++ b/graphics/src/geometry/text.rs
@@ -43,6 +43,7 @@ impl Text {
let mut buffer = cosmic_text::BufferLine::new(
&self.content,
+ cosmic_text::LineEnding::default(),
cosmic_text::AttrsList::new(text::to_attributes(self.font)),
text::to_shaping(self.shaping),
);
@@ -50,8 +51,10 @@ impl Text {
let layout = buffer.layout(
font_system.raw(),
self.size.0,
- f32::MAX,
+ None,
cosmic_text::Wrap::None,
+ None,
+ 4,
);
let translation_x = match self.horizontal_alignment {
diff --git a/graphics/src/gradient.rs b/graphics/src/gradient.rs
index 603f1b4a..54261721 100644
--- a/graphics/src/gradient.rs
+++ b/graphics/src/gradient.rs
@@ -9,7 +9,7 @@ use bytemuck::{Pod, Zeroable};
use half::f16;
use std::cmp::Ordering;
-#[derive(Debug, Clone, PartialEq)]
+#[derive(Debug, Clone, Copy, PartialEq)]
/// A fill which linearly interpolates colors along a direction.
///
/// For a gradient which can be used as a fill for a background of a widget, see [`crate::core::Gradient`].
diff --git a/graphics/src/image.rs b/graphics/src/image.rs
index 318592be..67a5e0cf 100644
--- a/graphics/src/image.rs
+++ b/graphics/src/image.rs
@@ -2,57 +2,26 @@
#[cfg(feature = "image")]
pub use ::image as image_rs;
-use crate::core::{image, svg, Color, Radians, Rectangle};
+use crate::core::image;
+use crate::core::svg;
+use crate::core::Rectangle;
/// A raster or vector image.
#[derive(Debug, Clone, PartialEq)]
pub enum Image {
/// A raster image.
- Raster {
- /// The handle of a raster image.
- handle: image::Handle,
+ Raster(image::Image, Rectangle),
- /// The filter method of a raster image.
- filter_method: image::FilterMethod,
-
- /// The bounds of the image.
- bounds: Rectangle,
-
- /// The rotation of the image.
- rotation: Radians,
-
- /// The opacity of the image.
- opacity: f32,
- },
/// A vector image.
- Vector {
- /// The handle of a vector image.
- handle: svg::Handle,
-
- /// The [`Color`] filter
- color: Option<Color>,
-
- /// The bounds of the image.
- bounds: Rectangle,
-
- /// The rotation of the image.
- rotation: Radians,
-
- /// The opacity of the image.
- opacity: f32,
- },
+ Vector(svg::Svg, Rectangle),
}
impl Image {
/// Returns the bounds of the [`Image`].
pub fn bounds(&self) -> Rectangle {
match self {
- Image::Raster {
- bounds, rotation, ..
- }
- | Image::Vector {
- bounds, rotation, ..
- } => bounds.rotate(*rotation),
+ Image::Raster(image, bounds) => bounds.rotate(image.rotation),
+ Image::Vector(svg, bounds) => bounds.rotate(svg.rotation),
}
}
}
diff --git a/graphics/src/text.rs b/graphics/src/text.rs
index 30269e69..feb9932a 100644
--- a/graphics/src/text.rs
+++ b/graphics/src/text.rs
@@ -11,7 +11,7 @@ pub use cosmic_text;
use crate::core::alignment;
use crate::core::font::{self, Font};
-use crate::core::text::Shaping;
+use crate::core::text::{Shaping, Wrapping};
use crate::core::{Color, Pixels, Point, Rectangle, Size, Transformation};
use once_cell::sync::OnceCell;
@@ -232,13 +232,14 @@ impl PartialEq for Raw {
/// Measures the dimensions of the given [`cosmic_text::Buffer`].
pub fn measure(buffer: &cosmic_text::Buffer) -> Size {
- let (width, total_lines) = buffer
- .layout_runs()
- .fold((0.0, 0usize), |(width, total_lines), run| {
- (run.line_w.max(width), total_lines + 1)
- });
+ let (width, height) =
+ buffer
+ .layout_runs()
+ .fold((0.0, 0.0), |(width, height), run| {
+ (run.line_w.max(width), height + run.line_height)
+ });
- Size::new(width, total_lines as f32 * buffer.metrics().line_height)
+ Size::new(width, height)
}
/// Returns the attributes of the given [`Font`].
@@ -305,6 +306,16 @@ pub fn to_shaping(shaping: Shaping) -> cosmic_text::Shaping {
}
}
+/// Converts some [`Wrapping`] strategy to a [`cosmic_text::Wrap`] strategy.
+pub fn to_wrap(wrapping: Wrapping) -> cosmic_text::Wrap {
+ match wrapping {
+ Wrapping::None => cosmic_text::Wrap::None,
+ Wrapping::Word => cosmic_text::Wrap::Word,
+ Wrapping::Glyph => cosmic_text::Wrap::Glyph,
+ Wrapping::WordOrGlyph => cosmic_text::Wrap::WordOrGlyph,
+ }
+}
+
/// Converts some [`Color`] to a [`cosmic_text::Color`].
pub fn to_color(color: Color) -> cosmic_text::Color {
let [r, g, b, a] = color.into_rgba8();
diff --git a/graphics/src/text/cache.rs b/graphics/src/text/cache.rs
index 822b61c4..e64d93f1 100644
--- a/graphics/src/text/cache.rs
+++ b/graphics/src/text/cache.rs
@@ -48,8 +48,8 @@ impl Cache {
buffer.set_size(
font_system,
- key.bounds.width,
- key.bounds.height.max(key.line_height),
+ Some(key.bounds.width),
+ Some(key.bounds.height.max(key.line_height)),
);
buffer.set_text(
font_system,
diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs
index c488a51c..1f1d0050 100644
--- a/graphics/src/text/editor.rs
+++ b/graphics/src/text/editor.rs
@@ -3,7 +3,7 @@ use crate::core::text::editor::{
self, Action, Cursor, Direction, Edit, Motion,
};
use crate::core::text::highlighter::{self, Highlighter};
-use crate::core::text::LineHeight;
+use crate::core::text::{LineHeight, Wrapping};
use crate::core::{Font, Pixels, Point, Rectangle, Size};
use crate::text;
@@ -17,7 +17,7 @@ use std::sync::{self, Arc};
pub struct Editor(Option<Arc<Internal>>);
struct Internal {
- editor: cosmic_text::Editor,
+ editor: cosmic_text::Editor<'static>,
font: Font,
bounds: Size,
topmost_line_changed: Option<usize>,
@@ -32,7 +32,7 @@ impl Editor {
/// Returns the buffer of the [`Editor`].
pub fn buffer(&self) -> &cosmic_text::Buffer {
- self.internal().editor.buffer()
+ buffer_from_editor(&self.internal().editor)
}
/// Creates a [`Weak`] reference to the [`Editor`].
@@ -82,6 +82,13 @@ impl editor::Editor for Editor {
})))
}
+ fn is_empty(&self) -> bool {
+ let buffer = self.buffer();
+
+ buffer.lines.is_empty()
+ || (buffer.lines.len() == 1 && buffer.lines[0].text().is_empty())
+ }
+
fn line(&self, index: usize) -> Option<&str> {
self.buffer()
.lines
@@ -101,16 +108,10 @@ impl editor::Editor for Editor {
let internal = self.internal();
let cursor = internal.editor.cursor();
- let buffer = internal.editor.buffer();
-
- match internal.editor.select_opt() {
- Some(selection) => {
- let (start, end) = if cursor < selection {
- (cursor, selection)
- } else {
- (selection, cursor)
- };
+ let buffer = buffer_from_editor(&internal.editor);
+ match internal.editor.selection_bounds() {
+ Some((start, end)) => {
let line_height = buffer.metrics().line_height;
let selected_lines = end.line - start.line + 1;
@@ -142,7 +143,8 @@ impl editor::Editor for Editor {
width,
y: (visual_line as i32 + visual_lines_offset)
as f32
- * line_height,
+ * line_height
+ - buffer.scroll().vertical,
height: line_height,
})
} else {
@@ -224,7 +226,8 @@ impl editor::Editor for Editor {
Cursor::Caret(Point::new(
offset,
(visual_lines_offset + visual_line as i32) as f32
- * line_height,
+ * line_height
+ - buffer.scroll().vertical,
))
}
}
@@ -252,16 +255,8 @@ impl editor::Editor for Editor {
match action {
// Motion events
Action::Move(motion) => {
- if let Some(selection) = editor.select_opt() {
- let cursor = editor.cursor();
-
- let (left, right) = if cursor < selection {
- (cursor, selection)
- } else {
- (selection, cursor)
- };
-
- editor.set_select_opt(None);
+ if let Some((start, end)) = editor.selection_bounds() {
+ editor.set_selection(cosmic_text::Selection::None);
match motion {
// These motions are performed as-is even when a selection
@@ -272,17 +267,20 @@ impl editor::Editor for Editor {
| Motion::DocumentEnd => {
editor.action(
font_system.raw(),
- motion_to_action(motion),
+ cosmic_text::Action::Motion(to_motion(motion)),
);
}
// Other motions simply move the cursor to one end of the selection
_ => editor.set_cursor(match motion.direction() {
- Direction::Left => left,
- Direction::Right => right,
+ Direction::Left => start,
+ Direction::Right => end,
}),
}
} else {
- editor.action(font_system.raw(), motion_to_action(motion));
+ editor.action(
+ font_system.raw(),
+ cosmic_text::Action::Motion(to_motion(motion)),
+ );
}
}
@@ -290,99 +288,58 @@ impl editor::Editor for Editor {
Action::Select(motion) => {
let cursor = editor.cursor();
- if editor.select_opt().is_none() {
- editor.set_select_opt(Some(cursor));
+ if editor.selection_bounds().is_none() {
+ editor
+ .set_selection(cosmic_text::Selection::Normal(cursor));
}
- editor.action(font_system.raw(), motion_to_action(motion));
+ editor.action(
+ font_system.raw(),
+ cosmic_text::Action::Motion(to_motion(motion)),
+ );
// Deselect if selection matches cursor position
- if let Some(selection) = editor.select_opt() {
- let cursor = editor.cursor();
-
- if cursor.line == selection.line
- && cursor.index == selection.index
- {
- editor.set_select_opt(None);
+ if let Some((start, end)) = editor.selection_bounds() {
+ if start.line == end.line && start.index == end.index {
+ editor.set_selection(cosmic_text::Selection::None);
}
}
}
Action::SelectWord => {
- use unicode_segmentation::UnicodeSegmentation;
-
let cursor = editor.cursor();
- if let Some(line) = editor.buffer().lines.get(cursor.line) {
- let (start, end) =
- UnicodeSegmentation::unicode_word_indices(line.text())
- // Split words with dots
- .flat_map(|(i, word)| {
- word.split('.').scan(i, |current, word| {
- let start = *current;
- *current += word.len() + 1;
-
- Some((start, word))
- })
- })
- // Turn words into ranges
- .map(|(i, word)| (i, i + word.len()))
- // Find the word at cursor
- .find(|&(start, end)| {
- start <= cursor.index && cursor.index < end
- })
- // Cursor is not in a word. Let's select its punctuation cluster.
- .unwrap_or_else(|| {
- let start = line.text()[..cursor.index]
- .char_indices()
- .rev()
- .take_while(|(_, c)| {
- c.is_ascii_punctuation()
- })
- .map(|(i, _)| i)
- .last()
- .unwrap_or(cursor.index);
-
- let end = line.text()[cursor.index..]
- .char_indices()
- .skip_while(|(_, c)| {
- c.is_ascii_punctuation()
- })
- .map(|(i, _)| i + cursor.index)
- .next()
- .unwrap_or(cursor.index);
-
- (start, end)
- });
-
- if start != end {
- editor.set_cursor(cosmic_text::Cursor {
- index: start,
- ..cursor
- });
-
- editor.set_select_opt(Some(cosmic_text::Cursor {
- index: end,
- ..cursor
- }));
- }
- }
+ editor.set_selection(cosmic_text::Selection::Word(cursor));
}
Action::SelectLine => {
let cursor = editor.cursor();
- if let Some(line_length) = editor
- .buffer()
- .lines
- .get(cursor.line)
- .map(|line| line.text().len())
+ editor.set_selection(cosmic_text::Selection::Line(cursor));
+ }
+ Action::SelectAll => {
+ let buffer = buffer_from_editor(editor);
+
+ if buffer.lines.len() > 1
+ || buffer
+ .lines
+ .first()
+ .is_some_and(|line| !line.text().is_empty())
{
- editor
- .set_cursor(cosmic_text::Cursor { index: 0, ..cursor });
+ let cursor = editor.cursor();
+
+ editor.set_selection(cosmic_text::Selection::Normal(
+ cosmic_text::Cursor {
+ line: 0,
+ index: 0,
+ ..cursor
+ },
+ ));
- editor.set_select_opt(Some(cosmic_text::Cursor {
- index: line_length,
- ..cursor
- }));
+ editor.action(
+ font_system.raw(),
+ cosmic_text::Action::Motion(
+ cosmic_text::Motion::BufferEnd,
+ ),
+ );
}
}
@@ -419,10 +376,12 @@ impl editor::Editor for Editor {
}
let cursor = editor.cursor();
- let selection = editor.select_opt().unwrap_or(cursor);
+ let selection_start = editor
+ .selection_bounds()
+ .map(|(start, _)| start)
+ .unwrap_or(cursor);
- internal.topmost_line_changed =
- Some(cursor.min(selection).line);
+ internal.topmost_line_changed = Some(selection_start.line);
}
// Mouse events
@@ -445,13 +404,9 @@ impl editor::Editor for Editor {
);
// Deselect if selection matches cursor position
- if let Some(selection) = editor.select_opt() {
- let cursor = editor.cursor();
-
- if cursor.line == selection.line
- && cursor.index == selection.index
- {
- editor.set_select_opt(None);
+ if let Some((start, end)) = editor.selection_bounds() {
+ if start.line == end.line && start.index == end.index {
+ editor.set_selection(cosmic_text::Selection::None);
}
}
}
@@ -473,7 +428,7 @@ impl editor::Editor for Editor {
fn min_bounds(&self) -> Size {
let internal = self.internal();
- text::measure(internal.editor.buffer())
+ text::measure(buffer_from_editor(&internal.editor))
}
fn update(
@@ -482,6 +437,7 @@ impl editor::Editor for Editor {
new_font: Font,
new_size: Pixels,
new_line_height: LineHeight,
+ new_wrapping: Wrapping,
new_highlighter: &mut impl Highlighter,
) {
let editor =
@@ -493,10 +449,12 @@ impl editor::Editor for Editor {
let mut font_system =
text::font_system().write().expect("Write font system");
+ let buffer = buffer_mut_from_editor(&mut internal.editor);
+
if font_system.version() != internal.version {
log::trace!("Updating `FontSystem` of `Editor`...");
- for line in internal.editor.buffer_mut().lines.iter_mut() {
+ for line in buffer.lines.iter_mut() {
line.reset();
}
@@ -507,7 +465,7 @@ impl editor::Editor for Editor {
if new_font != internal.font {
log::trace!("Updating font of `Editor`...");
- for line in internal.editor.buffer_mut().lines.iter_mut() {
+ for line in buffer.lines.iter_mut() {
let _ = line.set_attrs_list(cosmic_text::AttrsList::new(
text::to_attributes(new_font),
));
@@ -517,7 +475,7 @@ impl editor::Editor for Editor {
internal.topmost_line_changed = Some(0);
}
- let metrics = internal.editor.buffer().metrics();
+ let metrics = buffer.metrics();
let new_line_height = new_line_height.to_absolute(new_size);
if new_size.0 != metrics.font_size
@@ -525,19 +483,27 @@ impl editor::Editor for Editor {
{
log::trace!("Updating `Metrics` of `Editor`...");
- internal.editor.buffer_mut().set_metrics(
+ buffer.set_metrics(
font_system.raw(),
cosmic_text::Metrics::new(new_size.0, new_line_height.0),
);
}
+ let new_wrap = text::to_wrap(new_wrapping);
+
+ if new_wrap != buffer.wrap() {
+ log::trace!("Updating `Wrap` strategy of `Editor`...");
+
+ buffer.set_wrap(font_system.raw(), new_wrap);
+ }
+
if new_bounds != internal.bounds {
log::trace!("Updating size of `Editor`...");
- internal.editor.buffer_mut().set_size(
+ buffer.set_size(
font_system.raw(),
- new_bounds.width,
- new_bounds.height,
+ Some(new_bounds.width),
+ Some(new_bounds.height),
);
internal.bounds = new_bounds;
@@ -552,7 +518,7 @@ impl editor::Editor for Editor {
new_highlighter.change_line(topmost_line_changed);
}
- internal.editor.shape_as_needed(font_system.raw());
+ internal.editor.shape_as_needed(font_system.raw(), false);
self.0 = Some(Arc::new(internal));
}
@@ -564,12 +530,13 @@ impl editor::Editor for Editor {
format_highlight: impl Fn(&H::Highlight) -> highlighter::Format<Self::Font>,
) {
let internal = self.internal();
- let buffer = internal.editor.buffer();
+ let buffer = buffer_from_editor(&internal.editor);
- let mut window = buffer.scroll() + buffer.visible_lines();
+ let scroll = buffer.scroll();
+ let mut window = (internal.bounds.height / buffer.metrics().line_height)
+ .ceil() as i32;
- let last_visible_line = buffer
- .lines
+ let last_visible_line = buffer.lines[scroll.line..]
.iter()
.enumerate()
.find_map(|(i, line)| {
@@ -583,7 +550,7 @@ impl editor::Editor for Editor {
window -= visible_lines;
None
} else {
- Some(i)
+ Some(scroll.line + i)
}
})
.unwrap_or(buffer.lines.len().saturating_sub(1));
@@ -605,7 +572,7 @@ impl editor::Editor for Editor {
let attributes = text::to_attributes(font);
- for line in &mut internal.editor.buffer_mut().lines
+ for line in &mut buffer_mut_from_editor(&mut internal.editor).lines
[current_line..=last_visible_line]
{
let mut list = cosmic_text::AttrsList::new(attributes);
@@ -631,7 +598,7 @@ impl editor::Editor for Editor {
let _ = line.set_attrs_list(list);
}
- internal.editor.shape_as_needed(font_system.raw());
+ internal.editor.shape_as_needed(font_system.raw(), false);
self.0 = Some(Arc::new(internal));
}
@@ -647,7 +614,8 @@ impl PartialEq for Internal {
fn eq(&self, other: &Self) -> bool {
self.font == other.font
&& self.bounds == other.bounds
- && self.editor.buffer().metrics() == other.editor.buffer().metrics()
+ && buffer_from_editor(&self.editor).metrics()
+ == buffer_from_editor(&other.editor).metrics()
}
}
@@ -709,7 +677,8 @@ fn highlight_line(
let layout = line
.layout_opt()
.as_ref()
- .expect("Line layout should be cached");
+ .map(Vec::as_slice)
+ .unwrap_or_default();
layout.iter().map(move |visual_line| {
let start = visual_line
@@ -752,34 +721,61 @@ fn highlight_line(
}
fn visual_lines_offset(line: usize, buffer: &cosmic_text::Buffer) -> i32 {
- let visual_lines_before_start: usize = buffer
- .lines
+ let scroll = buffer.scroll();
+
+ let start = scroll.line.min(line);
+ let end = scroll.line.max(line);
+
+ let visual_lines_offset: usize = buffer.lines[start..]
.iter()
- .take(line)
+ .take(end - start)
.map(|line| {
- line.layout_opt()
- .as_ref()
- .expect("Line layout should be cached")
- .len()
+ line.layout_opt().as_ref().map(Vec::len).unwrap_or_default()
})
.sum();
- visual_lines_before_start as i32 - buffer.scroll()
+ visual_lines_offset as i32 * if scroll.line < line { 1 } else { -1 }
}
-fn motion_to_action(motion: Motion) -> cosmic_text::Action {
+fn to_motion(motion: Motion) -> cosmic_text::Motion {
match motion {
- Motion::Left => cosmic_text::Action::Left,
- Motion::Right => cosmic_text::Action::Right,
- Motion::Up => cosmic_text::Action::Up,
- Motion::Down => cosmic_text::Action::Down,
- Motion::WordLeft => cosmic_text::Action::LeftWord,
- Motion::WordRight => cosmic_text::Action::RightWord,
- Motion::Home => cosmic_text::Action::Home,
- Motion::End => cosmic_text::Action::End,
- Motion::PageUp => cosmic_text::Action::PageUp,
- Motion::PageDown => cosmic_text::Action::PageDown,
- Motion::DocumentStart => cosmic_text::Action::BufferStart,
- Motion::DocumentEnd => cosmic_text::Action::BufferEnd,
+ Motion::Left => cosmic_text::Motion::Left,
+ Motion::Right => cosmic_text::Motion::Right,
+ Motion::Up => cosmic_text::Motion::Up,
+ Motion::Down => cosmic_text::Motion::Down,
+ Motion::WordLeft => cosmic_text::Motion::LeftWord,
+ Motion::WordRight => cosmic_text::Motion::RightWord,
+ Motion::Home => cosmic_text::Motion::Home,
+ Motion::End => cosmic_text::Motion::End,
+ Motion::PageUp => cosmic_text::Motion::PageUp,
+ Motion::PageDown => cosmic_text::Motion::PageDown,
+ Motion::DocumentStart => cosmic_text::Motion::BufferStart,
+ Motion::DocumentEnd => cosmic_text::Motion::BufferEnd,
+ }
+}
+
+fn buffer_from_editor<'a, 'b>(
+ editor: &'a impl cosmic_text::Edit<'b>,
+) -> &'a cosmic_text::Buffer
+where
+ 'b: 'a,
+{
+ match editor.buffer_ref() {
+ cosmic_text::BufferRef::Owned(buffer) => buffer,
+ cosmic_text::BufferRef::Borrowed(buffer) => buffer,
+ cosmic_text::BufferRef::Arc(buffer) => buffer,
+ }
+}
+
+fn buffer_mut_from_editor<'a, 'b>(
+ editor: &'a mut impl cosmic_text::Edit<'b>,
+) -> &'a mut cosmic_text::Buffer
+where
+ 'b: 'a,
+{
+ match editor.buffer_ref_mut() {
+ cosmic_text::BufferRef::Owned(buffer) => buffer,
+ cosmic_text::BufferRef::Borrowed(buffer) => buffer,
+ cosmic_text::BufferRef::Arc(_buffer) => unreachable!(),
}
}
diff --git a/graphics/src/text/paragraph.rs b/graphics/src/text/paragraph.rs
index 31a323ac..07ddbb82 100644
--- a/graphics/src/text/paragraph.rs
+++ b/graphics/src/text/paragraph.rs
@@ -1,8 +1,8 @@
//! Draw paragraphs.
use crate::core;
use crate::core::alignment;
-use crate::core::text::{Hit, LineHeight, Shaping, Text};
-use crate::core::{Font, Pixels, Point, Size};
+use crate::core::text::{Hit, Shaping, Span, Text, Wrapping};
+use crate::core::{Font, Point, Rectangle, Size};
use crate::text;
use std::fmt;
@@ -10,13 +10,14 @@ use std::sync::{self, Arc};
/// A bunch of text.
#[derive(Clone, PartialEq)]
-pub struct Paragraph(Option<Arc<Internal>>);
+pub struct Paragraph(Arc<Internal>);
+#[derive(Clone)]
struct Internal {
buffer: cosmic_text::Buffer,
- content: String, // TODO: Reuse from `buffer` (?)
font: Font,
shaping: Shaping,
+ wrapping: Wrapping,
horizontal_alignment: alignment::Horizontal,
vertical_alignment: alignment::Vertical,
bounds: Size,
@@ -52,9 +53,7 @@ impl Paragraph {
}
fn internal(&self) -> &Arc<Internal> {
- self.0
- .as_ref()
- .expect("paragraph should always be initialized")
+ &self.0
}
}
@@ -62,7 +61,7 @@ impl core::text::Paragraph for Paragraph {
type Font = Font;
fn with_text(text: Text<&str>) -> Self {
- log::trace!("Allocating paragraph: {}", text.content);
+ log::trace!("Allocating plain paragraph: {}", text.content);
let mut font_system =
text::font_system().write().expect("Write font system");
@@ -77,8 +76,8 @@ impl core::text::Paragraph for Paragraph {
buffer.set_size(
font_system.raw(),
- text.bounds.width,
- text.bounds.height,
+ Some(text.bounds.width),
+ Some(text.bounds.height),
);
buffer.set_text(
@@ -90,73 +89,113 @@ impl core::text::Paragraph for Paragraph {
let min_bounds = text::measure(&buffer);
- Self(Some(Arc::new(Internal {
+ Self(Arc::new(Internal {
buffer,
- content: text.content.to_owned(),
font: text.font,
horizontal_alignment: text.horizontal_alignment,
vertical_alignment: text.vertical_alignment,
shaping: text.shaping,
+ wrapping: text.wrapping,
bounds: text.bounds,
min_bounds,
version: font_system.version(),
- })))
+ }))
+ }
+
+ fn with_spans<Link>(text: Text<&[Span<'_, Link>]>) -> Self {
+ log::trace!("Allocating rich paragraph: {} spans", text.content.len());
+
+ let mut font_system =
+ text::font_system().write().expect("Write font system");
+
+ let mut buffer = cosmic_text::Buffer::new(
+ font_system.raw(),
+ cosmic_text::Metrics::new(
+ text.size.into(),
+ text.line_height.to_absolute(text.size).into(),
+ ),
+ );
+
+ buffer.set_size(
+ font_system.raw(),
+ Some(text.bounds.width),
+ Some(text.bounds.height),
+ );
+
+ buffer.set_rich_text(
+ font_system.raw(),
+ text.content.iter().enumerate().map(|(i, span)| {
+ let attrs = text::to_attributes(span.font.unwrap_or(text.font));
+
+ let attrs = match (span.size, span.line_height) {
+ (None, None) => attrs,
+ _ => {
+ let size = span.size.unwrap_or(text.size);
+
+ attrs.metrics(cosmic_text::Metrics::new(
+ size.into(),
+ span.line_height
+ .unwrap_or(text.line_height)
+ .to_absolute(size)
+ .into(),
+ ))
+ }
+ };
+
+ let attrs = if let Some(color) = span.color {
+ attrs.color(text::to_color(color))
+ } else {
+ attrs
+ };
+
+ (span.text.as_ref(), attrs.metadata(i))
+ }),
+ text::to_attributes(text.font),
+ text::to_shaping(text.shaping),
+ );
+
+ let min_bounds = text::measure(&buffer);
+
+ Self(Arc::new(Internal {
+ buffer,
+ font: text.font,
+ horizontal_alignment: text.horizontal_alignment,
+ vertical_alignment: text.vertical_alignment,
+ shaping: text.shaping,
+ wrapping: text.wrapping,
+ bounds: text.bounds,
+ min_bounds,
+ version: font_system.version(),
+ }))
}
fn resize(&mut self, new_bounds: Size) {
- let paragraph = self
- .0
- .take()
- .expect("paragraph should always be initialized");
-
- match Arc::try_unwrap(paragraph) {
- Ok(mut internal) => {
- let mut font_system =
- text::font_system().write().expect("Write font system");
-
- internal.buffer.set_size(
- font_system.raw(),
- new_bounds.width,
- new_bounds.height,
- );
-
- internal.bounds = new_bounds;
- internal.min_bounds = text::measure(&internal.buffer);
-
- self.0 = Some(Arc::new(internal));
- }
- Err(internal) => {
- let metrics = internal.buffer.metrics();
-
- // If there is a strong reference somewhere, we recompute the
- // buffer from scratch
- *self = Self::with_text(Text {
- content: &internal.content,
- bounds: internal.bounds,
- size: Pixels(metrics.font_size),
- line_height: LineHeight::Absolute(Pixels(
- metrics.line_height,
- )),
- font: internal.font,
- horizontal_alignment: internal.horizontal_alignment,
- vertical_alignment: internal.vertical_alignment,
- shaping: internal.shaping,
- });
- }
- }
+ let paragraph = Arc::make_mut(&mut self.0);
+
+ let mut font_system =
+ text::font_system().write().expect("Write font system");
+
+ paragraph.buffer.set_size(
+ font_system.raw(),
+ Some(new_bounds.width),
+ Some(new_bounds.height),
+ );
+
+ paragraph.bounds = new_bounds;
+ paragraph.min_bounds = text::measure(&paragraph.buffer);
}
- fn compare(&self, text: Text<&str>) -> core::text::Difference {
+ fn compare(&self, text: Text<()>) -> core::text::Difference {
let font_system = text::font_system().read().expect("Read font system");
let paragraph = self.internal();
let metrics = paragraph.buffer.metrics();
if paragraph.version != font_system.version
- || paragraph.content != text.content
|| metrics.font_size != text.size.0
|| metrics.line_height != text.line_height.to_absolute(text.size).0
|| paragraph.font != text.font
|| paragraph.shaping != text.shaping
+ || paragraph.wrapping != text.wrapping
|| paragraph.horizontal_alignment != text.horizontal_alignment
|| paragraph.vertical_alignment != text.vertical_alignment
{
@@ -186,6 +225,87 @@ impl core::text::Paragraph for Paragraph {
Some(Hit::CharOffset(cursor.index))
}
+ fn hit_span(&self, point: Point) -> Option<usize> {
+ let internal = self.internal();
+
+ let cursor = internal.buffer.hit(point.x, point.y)?;
+ let line = internal.buffer.lines.get(cursor.line)?;
+
+ let mut last_glyph = None;
+ let mut glyphs = line
+ .layout_opt()
+ .as_ref()?
+ .iter()
+ .flat_map(|line| line.glyphs.iter())
+ .peekable();
+
+ while let Some(glyph) = glyphs.peek() {
+ if glyph.start <= cursor.index && cursor.index < glyph.end {
+ break;
+ }
+
+ last_glyph = glyphs.next();
+ }
+
+ let glyph = match cursor.affinity {
+ cosmic_text::Affinity::Before => last_glyph,
+ cosmic_text::Affinity::After => glyphs.next(),
+ }?;
+
+ Some(glyph.metadata)
+ }
+
+ fn span_bounds(&self, index: usize) -> Vec<Rectangle> {
+ let internal = self.internal();
+
+ let mut bounds = Vec::new();
+ let mut current_bounds = None;
+
+ let glyphs = internal
+ .buffer
+ .layout_runs()
+ .flat_map(|run| {
+ let line_top = run.line_top;
+ let line_height = run.line_height;
+
+ run.glyphs
+ .iter()
+ .map(move |glyph| (line_top, line_height, glyph))
+ })
+ .skip_while(|(_, _, glyph)| glyph.metadata != index)
+ .take_while(|(_, _, glyph)| glyph.metadata == index);
+
+ for (line_top, line_height, glyph) in glyphs {
+ let y = line_top + glyph.y;
+
+ let new_bounds = || {
+ Rectangle::new(
+ Point::new(glyph.x, y),
+ Size::new(
+ glyph.w,
+ glyph.line_height_opt.unwrap_or(line_height),
+ ),
+ )
+ };
+
+ match current_bounds.as_mut() {
+ None => {
+ current_bounds = Some(new_bounds());
+ }
+ Some(current_bounds) if y != current_bounds.y => {
+ bounds.push(*current_bounds);
+ *current_bounds = new_bounds();
+ }
+ Some(current_bounds) => {
+ current_bounds.width += glyph.w;
+ }
+ }
+ }
+
+ bounds.extend(current_bounds);
+ bounds
+ }
+
fn grapheme_position(&self, line: usize, index: usize) -> Option<Point> {
use unicode_segmentation::UnicodeSegmentation;
@@ -231,7 +351,7 @@ impl core::text::Paragraph for Paragraph {
impl Default for Paragraph {
fn default() -> Self {
- Self(Some(Arc::new(Internal::default())))
+ Self(Arc::new(Internal::default()))
}
}
@@ -240,7 +360,6 @@ impl fmt::Debug for Paragraph {
let paragraph = self.internal();
f.debug_struct("Paragraph")
- .field("content", &paragraph.content)
.field("font", &paragraph.font)
.field("shaping", &paragraph.shaping)
.field("horizontal_alignment", &paragraph.horizontal_alignment)
@@ -253,8 +372,7 @@ impl fmt::Debug for Paragraph {
impl PartialEq for Internal {
fn eq(&self, other: &Self) -> bool {
- self.content == other.content
- && self.font == other.font
+ self.font == other.font
&& self.shaping == other.shaping
&& self.horizontal_alignment == other.horizontal_alignment
&& self.vertical_alignment == other.vertical_alignment
@@ -271,9 +389,9 @@ impl Default for Internal {
font_size: 1.0,
line_height: 1.0,
}),
- content: String::new(),
font: Font::default(),
shaping: Shaping::default(),
+ wrapping: Wrapping::default(),
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Top,
bounds: Size::ZERO,
@@ -298,7 +416,7 @@ pub struct Weak {
impl Weak {
/// Tries to update the reference into a [`Paragraph`].
pub fn upgrade(&self) -> Option<Paragraph> {
- self.raw.upgrade().map(Some).map(Paragraph)
+ self.raw.upgrade().map(Paragraph)
}
}
diff --git a/highlighter/src/lib.rs b/highlighter/src/lib.rs
index 7636a712..83a15cb1 100644
--- a/highlighter/src/lib.rs
+++ b/highlighter/src/lib.rs
@@ -1,8 +1,9 @@
//! A syntax highlighter for iced.
use iced_core as core;
+use crate::core::font::{self, Font};
use crate::core::text::highlighter::{self, Format};
-use crate::core::{Color, Font};
+use crate::core::Color;
use once_cell::sync::Lazy;
use std::ops::Range;
@@ -35,7 +36,7 @@ impl highlighter::Highlighter for Highlighter {
fn new(settings: &Self::Settings) -> Self {
let syntax = SYNTAXES
- .find_syntax_by_token(&settings.extension)
+ .find_syntax_by_token(&settings.token)
.unwrap_or_else(|| SYNTAXES.find_syntax_plain_text());
let highlighter = highlighting::Highlighter::new(
@@ -55,7 +56,7 @@ impl highlighter::Highlighter for Highlighter {
fn update(&mut self, new_settings: &Self::Settings) {
self.syntax = SYNTAXES
- .find_syntax_by_token(&new_settings.extension)
+ .find_syntax_by_token(&new_settings.token)
.unwrap_or_else(|| SYNTAXES.find_syntax_plain_text());
self.highlighter = highlighting::Highlighter::new(
@@ -141,11 +142,11 @@ pub struct Settings {
///
/// It dictates the color scheme that will be used for highlighting.
pub theme: Theme,
- /// The extension of the file to highlight.
+ /// The extension of the file or the name of the language to highlight.
///
- /// The [`Highlighter`] will use the extension to automatically determine
+ /// The [`Highlighter`] will use the token to automatically determine
/// the grammar to use for highlighting.
- pub extension: String,
+ pub token: String,
}
/// A highlight produced by a [`Highlighter`].
@@ -166,7 +167,28 @@ impl Highlight {
///
/// If `None`, the original font should be unchanged.
pub fn font(&self) -> Option<Font> {
- None
+ self.0.font_style.and_then(|style| {
+ let bold = style.contains(highlighting::FontStyle::BOLD);
+ let italic = style.contains(highlighting::FontStyle::ITALIC);
+
+ if bold || italic {
+ Some(Font {
+ weight: if bold {
+ font::Weight::Bold
+ } else {
+ font::Weight::Normal
+ },
+ style: if italic {
+ font::Style::Italic
+ } else {
+ font::Style::Normal
+ },
+ ..Font::MONOSPACE
+ })
+ } else {
+ None
+ }
+ })
}
/// Returns the [`Format`] of the [`Highlight`].
diff --git a/renderer/src/fallback.rs b/renderer/src/fallback.rs
index 6a169692..8cb18bde 100644
--- a/renderer/src/fallback.rs
+++ b/renderer/src/fallback.rs
@@ -3,7 +3,7 @@ use crate::core::image;
use crate::core::renderer;
use crate::core::svg;
use crate::core::{
- self, Background, Color, Point, Radians, Rectangle, Size, Transformation,
+ self, Background, Color, Image, Point, Rectangle, Size, Svg, Transformation,
};
use crate::graphics;
use crate::graphics::compositor;
@@ -149,25 +149,8 @@ where
delegate!(self, renderer, renderer.measure_image(handle))
}
- fn draw_image(
- &mut self,
- handle: Self::Handle,
- filter_method: image::FilterMethod,
- bounds: Rectangle,
- rotation: Radians,
- opacity: f32,
- ) {
- delegate!(
- self,
- renderer,
- renderer.draw_image(
- handle,
- filter_method,
- bounds,
- rotation,
- opacity
- )
- );
+ fn draw_image(&mut self, image: Image<A::Handle>, bounds: Rectangle) {
+ delegate!(self, renderer, renderer.draw_image(image, bounds));
}
}
@@ -180,19 +163,8 @@ where
delegate!(self, renderer, renderer.measure_svg(handle))
}
- fn draw_svg(
- &mut self,
- handle: svg::Handle,
- color: Option<Color>,
- bounds: Rectangle,
- rotation: Radians,
- opacity: f32,
- ) {
- delegate!(
- self,
- renderer,
- renderer.draw_svg(handle, color, bounds, rotation, opacity)
- );
+ fn draw_svg(&mut self, svg: Svg, bounds: Rectangle) {
+ delegate!(self, renderer, renderer.draw_svg(svg, bounds));
}
}
@@ -441,9 +413,9 @@ where
#[cfg(feature = "geometry")]
mod geometry {
use super::Renderer;
- use crate::core::{Point, Radians, Rectangle, Size, Vector};
+ use crate::core::{Point, Radians, Rectangle, Size, Svg, Vector};
use crate::graphics::cache::{self, Cached};
- use crate::graphics::geometry::{self, Fill, Path, Stroke, Text};
+ use crate::graphics::geometry::{self, Fill, Image, Path, Stroke, Text};
impl<A, B> geometry::Renderer for Renderer<A, B>
where
@@ -568,10 +540,31 @@ mod geometry {
delegate!(self, frame, frame.stroke(path, stroke));
}
+ fn stroke_rectangle<'a>(
+ &mut self,
+ top_left: Point,
+ size: Size,
+ stroke: impl Into<Stroke<'a>>,
+ ) {
+ delegate!(
+ self,
+ frame,
+ frame.stroke_rectangle(top_left, size, stroke)
+ );
+ }
+
fn fill_text(&mut self, text: impl Into<Text>) {
delegate!(self, frame, frame.fill_text(text));
}
+ fn draw_image(&mut self, bounds: Rectangle, image: impl Into<Image>) {
+ delegate!(self, frame, frame.draw_image(bounds, image));
+ }
+
+ fn draw_svg(&mut self, bounds: Rectangle, svg: impl Into<Svg>) {
+ delegate!(self, frame, frame.draw_svg(bounds, svg));
+ }
+
fn push_transform(&mut self) {
delegate!(self, frame, frame.push_transform());
}
@@ -587,13 +580,13 @@ mod geometry {
}
}
- fn paste(&mut self, frame: Self, at: Point) {
+ fn paste(&mut self, frame: Self) {
match (self, frame) {
(Self::Primary(target), Self::Primary(source)) => {
- target.paste(source, at);
+ target.paste(source);
}
(Self::Secondary(target), Self::Secondary(source)) => {
- target.paste(source, at);
+ target.paste(source);
}
_ => unreachable!(),
}
diff --git a/runtime/src/clipboard.rs b/runtime/src/clipboard.rs
index 19950d01..a02cc011 100644
--- a/runtime/src/clipboard.rs
+++ b/runtime/src/clipboard.rs
@@ -1,7 +1,7 @@
//! Access the clipboard.
use crate::core::clipboard::Kind;
use crate::futures::futures::channel::oneshot;
-use crate::Task;
+use crate::task::{self, Task};
/// A clipboard action to be performed by some [`Task`].
///
@@ -27,7 +27,7 @@ pub enum Action {
/// Read the current contents of the clipboard.
pub fn read() -> Task<Option<String>> {
- Task::oneshot(|channel| {
+ task::oneshot(|channel| {
crate::Action::Clipboard(Action::Read {
target: Kind::Standard,
channel,
@@ -37,7 +37,7 @@ pub fn read() -> Task<Option<String>> {
/// Read the current contents of the primary clipboard.
pub fn read_primary() -> Task<Option<String>> {
- Task::oneshot(|channel| {
+ task::oneshot(|channel| {
crate::Action::Clipboard(Action::Read {
target: Kind::Primary,
channel,
@@ -47,7 +47,7 @@ pub fn read_primary() -> Task<Option<String>> {
/// Write the given contents to the clipboard.
pub fn write<T>(contents: String) -> Task<T> {
- Task::effect(crate::Action::Clipboard(Action::Write {
+ task::effect(crate::Action::Clipboard(Action::Write {
target: Kind::Standard,
contents,
}))
@@ -55,7 +55,7 @@ pub fn write<T>(contents: String) -> Task<T> {
/// Write the given contents to the primary clipboard.
pub fn write_primary<Message>(contents: String) -> Task<Message> {
- Task::effect(crate::Action::Clipboard(Action::Write {
+ task::effect(crate::Action::Clipboard(Action::Write {
target: Kind::Primary,
contents,
}))
diff --git a/runtime/src/font.rs b/runtime/src/font.rs
index d54eb6a8..75fdfc11 100644
--- a/runtime/src/font.rs
+++ b/runtime/src/font.rs
@@ -1,5 +1,6 @@
//! Load and use fonts.
-use crate::{Action, Task};
+use crate::task::{self, Task};
+use crate::Action;
use std::borrow::Cow;
/// An error while loading a font.
@@ -8,7 +9,7 @@ pub enum Error {}
/// Load a font from its bytes.
pub fn load(bytes: impl Into<Cow<'static, [u8]>>) -> Task<Result<(), Error>> {
- Task::oneshot(|channel| Action::LoadFont {
+ task::oneshot(|channel| Action::LoadFont {
bytes: bytes.into(),
channel,
})
diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs
index b4a5e819..7230fc73 100644
--- a/runtime/src/lib.rs
+++ b/runtime/src/lib.rs
@@ -15,14 +15,13 @@ pub mod keyboard;
pub mod overlay;
pub mod program;
pub mod system;
+pub mod task;
pub mod user_interface;
pub mod window;
#[cfg(feature = "multi-window")]
pub mod multi_window;
-mod task;
-
// We disable debug capabilities on release builds unless the `debug` feature
// is explicitly enabled.
#[cfg(feature = "debug")]
@@ -60,7 +59,7 @@ pub enum Action<T> {
},
/// Run a widget operation.
- Widget(Box<dyn widget::Operation<()> + Send>),
+ Widget(Box<dyn widget::Operation>),
/// Run a clipboard action.
Clipboard(clipboard::Action),
@@ -80,7 +79,7 @@ pub enum Action<T> {
impl<T> Action<T> {
/// Creates a new [`Action::Widget`] with the given [`widget::Operation`].
- pub fn widget(operation: impl widget::Operation<()> + 'static) -> Self {
+ pub fn widget(operation: impl widget::Operation + 'static) -> Self {
Self::Widget(Box::new(operation))
}
@@ -127,5 +126,5 @@ where
/// This will normally close any application windows and
/// terminate the runtime loop.
pub fn exit<T>() -> Task<T> {
- Task::effect(Action::Exit)
+ task::effect(Action::Exit)
}
diff --git a/runtime/src/multi_window/state.rs b/runtime/src/multi_window/state.rs
index 72ce6933..0bec555f 100644
--- a/runtime/src/multi_window/state.rs
+++ b/runtime/src/multi_window/state.rs
@@ -205,7 +205,7 @@ where
pub fn operate(
&mut self,
renderer: &mut P::Renderer,
- operations: impl Iterator<Item = Box<dyn Operation<()>>>,
+ operations: impl Iterator<Item = Box<dyn Operation>>,
bounds: Size,
debug: &mut Debug,
) {
diff --git a/runtime/src/overlay/nested.rs b/runtime/src/overlay/nested.rs
index 11eee41c..da3e6929 100644
--- a/runtime/src/overlay/nested.rs
+++ b/runtime/src/overlay/nested.rs
@@ -131,13 +131,13 @@ where
&mut self,
layout: Layout<'_>,
renderer: &Renderer,
- operation: &mut dyn widget::Operation<()>,
+ operation: &mut dyn widget::Operation,
) {
fn recurse<Message, Theme, Renderer>(
element: &mut overlay::Element<'_, Message, Theme, Renderer>,
layout: Layout<'_>,
renderer: &Renderer,
- operation: &mut dyn widget::Operation<()>,
+ operation: &mut dyn widget::Operation,
) where
Renderer: renderer::Renderer,
{
diff --git a/runtime/src/program/state.rs b/runtime/src/program/state.rs
index e51ad0cb..c377814a 100644
--- a/runtime/src/program/state.rs
+++ b/runtime/src/program/state.rs
@@ -178,7 +178,7 @@ where
pub fn operate(
&mut self,
renderer: &mut P::Renderer,
- operations: impl Iterator<Item = Box<dyn Operation<()>>>,
+ operations: impl Iterator<Item = Box<dyn Operation>>,
bounds: Size,
debug: &mut Debug,
) {
diff --git a/runtime/src/task.rs b/runtime/src/task.rs
index b8a83d6d..ec8d7cc7 100644
--- a/runtime/src/task.rs
+++ b/runtime/src/task.rs
@@ -1,3 +1,4 @@
+//! Create runtime tasks.
use crate::core::widget;
use crate::futures::futures::channel::mpsc;
use crate::futures::futures::channel::oneshot;
@@ -29,24 +30,6 @@ impl<T> Task<T> {
Self::future(future::ready(value))
}
- /// Creates a new [`Task`] that runs the given [`Future`] and produces
- /// its output.
- pub fn future(future: impl Future<Output = T> + MaybeSend + 'static) -> Self
- where
- T: 'static,
- {
- Self::stream(stream::once(future))
- }
-
- /// Creates a new [`Task`] that runs the given [`Stream`] and produces
- /// each of its items.
- pub fn stream(stream: impl Stream<Item = T> + MaybeSend + 'static) -> Self
- where
- T: 'static,
- {
- Self(Some(boxed_stream(stream.map(Action::Output))))
- }
-
/// Creates a [`Task`] that runs the given [`Future`] to completion and maps its
/// output with the given closure.
pub fn perform<A>(
@@ -83,66 +66,6 @@ impl<T> Task<T> {
))))
}
- /// Creates a new [`Task`] that runs the given [`widget::Operation`] and produces
- /// its output.
- pub fn widget(operation: impl widget::Operation<T> + 'static) -> Task<T>
- where
- T: Send + 'static,
- {
- Self::channel(move |sender| {
- let operation =
- widget::operation::map(Box::new(operation), move |value| {
- let _ = sender.clone().try_send(value);
- });
-
- Action::Widget(Box::new(operation))
- })
- }
-
- /// Creates a new [`Task`] that executes the [`Action`] returned by the closure and
- /// produces the value fed to the [`oneshot::Sender`].
- pub fn oneshot(f: impl FnOnce(oneshot::Sender<T>) -> Action<T>) -> Task<T>
- where
- T: MaybeSend + 'static,
- {
- let (sender, receiver) = oneshot::channel();
-
- let action = f(sender);
-
- Self(Some(boxed_stream(
- stream::once(async move { action }).chain(
- receiver.into_stream().filter_map(|result| async move {
- Some(Action::Output(result.ok()?))
- }),
- ),
- )))
- }
-
- /// Creates a new [`Task`] that executes the [`Action`] returned by the closure and
- /// produces the values fed to the [`mpsc::Sender`].
- pub fn channel(f: impl FnOnce(mpsc::Sender<T>) -> Action<T>) -> Task<T>
- where
- T: MaybeSend + 'static,
- {
- let (sender, receiver) = mpsc::channel(1);
-
- let action = f(sender);
-
- Self(Some(boxed_stream(
- stream::once(async move { action })
- .chain(receiver.map(|result| Action::Output(result))),
- )))
- }
-
- /// Creates a new [`Task`] that executes the given [`Action`] and produces no output.
- pub fn effect(action: impl Into<Action<Never>>) -> Self {
- let action = action.into();
-
- Self(Some(boxed_stream(stream::once(async move {
- action.output().expect_err("no output")
- }))))
- }
-
/// Maps the output of a [`Task`] with the given closure.
pub fn map<O>(
self,
@@ -236,9 +159,105 @@ impl<T> Task<T> {
}
}
- /// Returns the underlying [`Stream`] of the [`Task`].
- pub fn into_stream(self) -> Option<BoxStream<Action<T>>> {
- self.0
+ /// Creates a new [`Task`] that discards the result of the current one.
+ ///
+ /// Useful if you only care about the side effects of a [`Task`].
+ pub fn discard<O>(self) -> Task<O>
+ where
+ T: MaybeSend + 'static,
+ O: MaybeSend + 'static,
+ {
+ self.then(|_| Task::none())
+ }
+
+ /// Creates a new [`Task`] that can be aborted with the returned [`Handle`].
+ pub fn abortable(self) -> (Self, Handle)
+ where
+ T: 'static,
+ {
+ match self.0 {
+ Some(stream) => {
+ let (stream, handle) = stream::abortable(stream);
+
+ (
+ Self(Some(boxed_stream(stream))),
+ Handle {
+ raw: Some(handle),
+ abort_on_drop: false,
+ },
+ )
+ }
+ None => (
+ Self(None),
+ Handle {
+ raw: None,
+ abort_on_drop: false,
+ },
+ ),
+ }
+ }
+
+ /// Creates a new [`Task`] that runs the given [`Future`] and produces
+ /// its output.
+ pub fn future(future: impl Future<Output = T> + MaybeSend + 'static) -> Self
+ where
+ T: 'static,
+ {
+ Self::stream(stream::once(future))
+ }
+
+ /// Creates a new [`Task`] that runs the given [`Stream`] and produces
+ /// each of its items.
+ pub fn stream(stream: impl Stream<Item = T> + MaybeSend + 'static) -> Self
+ where
+ T: 'static,
+ {
+ Self(Some(boxed_stream(stream.map(Action::Output))))
+ }
+}
+
+/// A handle to a [`Task`] that can be used for aborting it.
+#[derive(Debug, Clone)]
+pub struct Handle {
+ raw: Option<stream::AbortHandle>,
+ abort_on_drop: bool,
+}
+
+impl Handle {
+ /// Aborts the [`Task`] of this [`Handle`].
+ pub fn abort(&self) {
+ if let Some(handle) = &self.raw {
+ handle.abort();
+ }
+ }
+
+ /// Returns a new [`Handle`] that will call [`Handle::abort`] whenever
+ /// it is dropped.
+ ///
+ /// This can be really useful if you do not want to worry about calling
+ /// [`Handle::abort`] yourself.
+ pub fn abort_on_drop(mut self) -> Self {
+ Self {
+ raw: self.raw.take(),
+ abort_on_drop: true,
+ }
+ }
+
+ /// Returns `true` if the [`Task`] of this [`Handle`] has been aborted.
+ pub fn is_aborted(&self) -> bool {
+ if let Some(handle) = &self.raw {
+ handle.is_aborted()
+ } else {
+ true
+ }
+ }
+}
+
+impl Drop for Handle {
+ fn drop(&mut self) {
+ if self.abort_on_drop {
+ self.abort();
+ }
}
}
@@ -275,11 +294,73 @@ impl<T, E> Task<Result<T, E>> {
}
}
-impl<T> From<()> for Task<T>
-where
- T: MaybeSend + 'static,
-{
+impl<T> From<()> for Task<T> {
fn from(_value: ()) -> Self {
Self::none()
}
}
+
+/// Creates a new [`Task`] that runs the given [`widget::Operation`] and produces
+/// its output.
+pub fn widget<T>(operation: impl widget::Operation<T> + 'static) -> Task<T>
+where
+ T: Send + 'static,
+{
+ channel(move |sender| {
+ let operation =
+ widget::operation::map(Box::new(operation), move |value| {
+ let _ = sender.clone().try_send(value);
+ });
+
+ Action::Widget(Box::new(operation))
+ })
+}
+
+/// Creates a new [`Task`] that executes the [`Action`] returned by the closure and
+/// produces the value fed to the [`oneshot::Sender`].
+pub fn oneshot<T>(f: impl FnOnce(oneshot::Sender<T>) -> Action<T>) -> Task<T>
+where
+ T: MaybeSend + 'static,
+{
+ let (sender, receiver) = oneshot::channel();
+
+ let action = f(sender);
+
+ Task(Some(boxed_stream(
+ stream::once(async move { action }).chain(
+ receiver.into_stream().filter_map(|result| async move {
+ Some(Action::Output(result.ok()?))
+ }),
+ ),
+ )))
+}
+
+/// Creates a new [`Task`] that executes the [`Action`] returned by the closure and
+/// produces the values fed to the [`mpsc::Sender`].
+pub fn channel<T>(f: impl FnOnce(mpsc::Sender<T>) -> Action<T>) -> Task<T>
+where
+ T: MaybeSend + 'static,
+{
+ let (sender, receiver) = mpsc::channel(1);
+
+ let action = f(sender);
+
+ Task(Some(boxed_stream(
+ stream::once(async move { action })
+ .chain(receiver.map(|result| Action::Output(result))),
+ )))
+}
+
+/// Creates a new [`Task`] that executes the given [`Action`] and produces no output.
+pub fn effect<T>(action: impl Into<Action<Never>>) -> Task<T> {
+ let action = action.into();
+
+ Task(Some(boxed_stream(stream::once(async move {
+ action.output().expect_err("no output")
+ }))))
+}
+
+/// Returns the underlying [`Stream`] of the [`Task`].
+pub fn into_stream<T>(task: Task<T>) -> Option<BoxStream<Action<T>>> {
+ task.0
+}
diff --git a/runtime/src/user_interface.rs b/runtime/src/user_interface.rs
index 858b1a2d..11ebb381 100644
--- a/runtime/src/user_interface.rs
+++ b/runtime/src/user_interface.rs
@@ -566,7 +566,7 @@ where
pub fn operate(
&mut self,
renderer: &Renderer,
- operation: &mut dyn widget::Operation<()>,
+ operation: &mut dyn widget::Operation,
) {
self.root.as_widget().operate(
&mut self.state,
diff --git a/runtime/src/window.rs b/runtime/src/window.rs
index b04e5d59..cdf3d80a 100644
--- a/runtime/src/window.rs
+++ b/runtime/src/window.rs
@@ -11,7 +11,7 @@ use crate::core::{Point, Size};
use crate::futures::event;
use crate::futures::futures::channel::oneshot;
use crate::futures::Subscription;
-use crate::Task;
+use crate::task::{self, Task};
pub use raw_window_handle;
@@ -63,6 +63,9 @@ pub enum Action {
/// Get the current logical coordinates of the window.
GetPosition(Id, oneshot::Sender<Option<Point>>),
+ /// Get the current scale factor (DPI) of the window.
+ GetScaleFactor(Id, oneshot::Sender<f32>),
+
/// Move the window to the given logical coordinates.
///
/// Unsupported on Wayland.
@@ -144,6 +147,18 @@ pub enum Action {
/// Screenshot the viewport of the window.
Screenshot(Id, oneshot::Sender<Screenshot>),
+
+ /// Enables mouse passthrough for the given window.
+ ///
+ /// This disables mouse events for the window and passes mouse events
+ /// through to whatever window is underneath.
+ EnableMousePassthrough(Id),
+
+ /// Disable mouse passthrough for the given window.
+ ///
+ /// This enables mouse events for the window and stops mouse events
+ /// from being passed to whatever is underneath.
+ DisableMousePassthrough(Id),
}
/// Subscribes to the frames of the window of the running application.
@@ -175,7 +190,7 @@ pub fn events() -> Subscription<(Id, Event)> {
/// Subscribes to all [`Event::Closed`] occurrences in the running application.
pub fn open_events() -> Subscription<Id> {
event::listen_with(|event, _status, id| {
- if let crate::core::Event::Window(Event::Closed) = event {
+ if let crate::core::Event::Window(Event::Opened { .. }) = event {
Some(id)
} else {
None
@@ -194,6 +209,17 @@ pub fn close_events() -> Subscription<Id> {
})
}
+/// Subscribes to all [`Event::Resized`] occurrences in the running application.
+pub fn resize_events() -> Subscription<(Id, Size)> {
+ event::listen_with(|event, _status, id| {
+ if let crate::core::Event::Window(Event::Resized(size)) = event {
+ Some((id, size))
+ } else {
+ None
+ }
+ })
+}
+
/// Subscribes to all [`Event::CloseRequested`] occurences in the running application.
pub fn close_requests() -> Subscription<Id> {
event::listen_with(|event, _status, id| {
@@ -207,102 +233,112 @@ pub fn close_requests() -> Subscription<Id> {
/// Opens a new window with the given [`Settings`]; producing the [`Id`]
/// of the new window on completion.
-pub fn open(settings: Settings) -> Task<Id> {
+pub fn open(settings: Settings) -> (Id, Task<Id>) {
let id = Id::unique();
- Task::oneshot(|channel| {
- crate::Action::Window(Action::Open(id, settings, channel))
- })
+ (
+ id,
+ task::oneshot(|channel| {
+ crate::Action::Window(Action::Open(id, settings, channel))
+ }),
+ )
}
/// Closes the window with `id`.
pub fn close<T>(id: Id) -> Task<T> {
- Task::effect(crate::Action::Window(Action::Close(id)))
+ task::effect(crate::Action::Window(Action::Close(id)))
}
/// Gets the window [`Id`] of the oldest window.
pub fn get_oldest() -> Task<Option<Id>> {
- Task::oneshot(|channel| crate::Action::Window(Action::GetOldest(channel)))
+ task::oneshot(|channel| crate::Action::Window(Action::GetOldest(channel)))
}
/// Gets the window [`Id`] of the latest window.
pub fn get_latest() -> Task<Option<Id>> {
- Task::oneshot(|channel| crate::Action::Window(Action::GetLatest(channel)))
+ task::oneshot(|channel| crate::Action::Window(Action::GetLatest(channel)))
}
/// Begins dragging the window while the left mouse button is held.
pub fn drag<T>(id: Id) -> Task<T> {
- Task::effect(crate::Action::Window(Action::Drag(id)))
+ task::effect(crate::Action::Window(Action::Drag(id)))
}
/// Resizes the window to the given logical dimensions.
pub fn resize<T>(id: Id, new_size: Size) -> Task<T> {
- Task::effect(crate::Action::Window(Action::Resize(id, new_size)))
+ task::effect(crate::Action::Window(Action::Resize(id, new_size)))
}
/// Get the window's size in logical dimensions.
pub fn get_size(id: Id) -> Task<Size> {
- Task::oneshot(move |channel| {
+ task::oneshot(move |channel| {
crate::Action::Window(Action::GetSize(id, channel))
})
}
/// Gets the maximized state of the window with the given [`Id`].
pub fn get_maximized(id: Id) -> Task<bool> {
- Task::oneshot(move |channel| {
+ task::oneshot(move |channel| {
crate::Action::Window(Action::GetMaximized(id, channel))
})
}
/// Maximizes the window.
pub fn maximize<T>(id: Id, maximized: bool) -> Task<T> {
- Task::effect(crate::Action::Window(Action::Maximize(id, maximized)))
+ task::effect(crate::Action::Window(Action::Maximize(id, maximized)))
}
/// Gets the minimized state of the window with the given [`Id`].
pub fn get_minimized(id: Id) -> Task<Option<bool>> {
- Task::oneshot(move |channel| {
+ task::oneshot(move |channel| {
crate::Action::Window(Action::GetMinimized(id, channel))
})
}
/// Minimizes the window.
pub fn minimize<T>(id: Id, minimized: bool) -> Task<T> {
- Task::effect(crate::Action::Window(Action::Minimize(id, minimized)))
+ task::effect(crate::Action::Window(Action::Minimize(id, minimized)))
}
/// Gets the position in logical coordinates of the window with the given [`Id`].
pub fn get_position(id: Id) -> Task<Option<Point>> {
- Task::oneshot(move |channel| {
+ task::oneshot(move |channel| {
crate::Action::Window(Action::GetPosition(id, channel))
})
}
+/// Gets the scale factor of the window with the given [`Id`].
+pub fn get_scale_factor(id: Id) -> Task<f32> {
+ task::oneshot(move |channel| {
+ crate::Action::Window(Action::GetScaleFactor(id, channel))
+ })
+}
+
/// Moves the window to the given logical coordinates.
pub fn move_to<T>(id: Id, position: Point) -> Task<T> {
- Task::effect(crate::Action::Window(Action::Move(id, position)))
+ task::effect(crate::Action::Window(Action::Move(id, position)))
}
/// Changes the [`Mode`] of the window.
pub fn change_mode<T>(id: Id, mode: Mode) -> Task<T> {
- Task::effect(crate::Action::Window(Action::ChangeMode(id, mode)))
+ task::effect(crate::Action::Window(Action::ChangeMode(id, mode)))
}
/// Gets the current [`Mode`] of the window.
pub fn get_mode(id: Id) -> Task<Mode> {
- Task::oneshot(move |channel| {
+ task::oneshot(move |channel| {
crate::Action::Window(Action::GetMode(id, channel))
})
}
/// Toggles the window to maximized or back.
pub fn toggle_maximize<T>(id: Id) -> Task<T> {
- Task::effect(crate::Action::Window(Action::ToggleMaximize(id)))
+ task::effect(crate::Action::Window(Action::ToggleMaximize(id)))
}
/// Toggles the window decorations.
pub fn toggle_decorations<T>(id: Id) -> Task<T> {
- Task::effect(crate::Action::Window(Action::ToggleDecorations(id)))
+ task::effect(crate::Action::Window(Action::ToggleDecorations(id)))
}
/// Request user attention to the window. This has no effect if the application
@@ -315,7 +351,7 @@ pub fn request_user_attention<T>(
id: Id,
user_attention: Option<UserAttention>,
) -> Task<T> {
- Task::effect(crate::Action::Window(Action::RequestUserAttention(
+ task::effect(crate::Action::Window(Action::RequestUserAttention(
id,
user_attention,
)))
@@ -328,32 +364,32 @@ pub fn request_user_attention<T>(
/// you are certain that's what the user wants. Focus stealing can cause an extremely disruptive
/// user experience.
pub fn gain_focus<T>(id: Id) -> Task<T> {
- Task::effect(crate::Action::Window(Action::GainFocus(id)))
+ task::effect(crate::Action::Window(Action::GainFocus(id)))
}
/// Changes the window [`Level`].
pub fn change_level<T>(id: Id, level: Level) -> Task<T> {
- Task::effect(crate::Action::Window(Action::ChangeLevel(id, level)))
+ task::effect(crate::Action::Window(Action::ChangeLevel(id, level)))
}
/// Show the [system menu] at cursor position.
///
/// [system menu]: https://en.wikipedia.org/wiki/Common_menus_in_Microsoft_Windows#System_menu
pub fn show_system_menu<T>(id: Id) -> Task<T> {
- Task::effect(crate::Action::Window(Action::ShowSystemMenu(id)))
+ task::effect(crate::Action::Window(Action::ShowSystemMenu(id)))
}
/// Gets an identifier unique to the window, provided by the underlying windowing system. This is
/// not to be confused with [`Id`].
pub fn get_raw_id<Message>(id: Id) -> Task<u64> {
- Task::oneshot(|channel| {
+ task::oneshot(|channel| {
crate::Action::Window(Action::GetRawId(id, channel))
})
}
/// Changes the [`Icon`] of the window.
pub fn change_icon<T>(id: Id, icon: Icon) -> Task<T> {
- Task::effect(crate::Action::Window(Action::ChangeIcon(id, icon)))
+ task::effect(crate::Action::Window(Action::ChangeIcon(id, icon)))
}
/// Runs the given callback with the native window handle for the window with the given id.
@@ -366,7 +402,7 @@ pub fn run_with_handle<T>(
where
T: Send + 'static,
{
- Task::oneshot(move |channel| {
+ task::oneshot(move |channel| {
crate::Action::Window(Action::RunWithHandle(
id,
Box::new(move |handle| {
@@ -378,7 +414,23 @@ where
/// Captures a [`Screenshot`] from the window.
pub fn screenshot(id: Id) -> Task<Screenshot> {
- Task::oneshot(move |channel| {
+ task::oneshot(move |channel| {
crate::Action::Window(Action::Screenshot(id, channel))
})
}
+
+/// Enables mouse passthrough for the given window.
+///
+/// This disables mouse events for the window and passes mouse events
+/// through to whatever window is underneath.
+pub fn enable_mouse_passthrough<Message>(id: Id) -> Task<Message> {
+ task::effect(crate::Action::Window(Action::EnableMousePassthrough(id)))
+}
+
+/// Disable mouse passthrough for the given window.
+///
+/// This enables mouse events for the window and stops mouse events
+/// from being passed to whatever is underneath.
+pub fn disable_mouse_passthrough<Message>(id: Id) -> Task<Message> {
+ task::effect(crate::Action::Window(Action::DisableMousePassthrough(id)))
+}
diff --git a/src/advanced.rs b/src/advanced.rs
index 8d06e805..843b381a 100644
--- a/src/advanced.rs
+++ b/src/advanced.rs
@@ -1,4 +1,18 @@
//! Leverage advanced concepts like custom widgets.
+pub mod subscription {
+ //! Write your own subscriptions.
+ pub use crate::runtime::futures::subscription::{
+ from_recipe, into_recipes, Event, EventStream, Hasher, MacOS,
+ PlatformSpecific, Recipe,
+ };
+}
+
+pub mod widget {
+ //! Create custom widgets and operate on them.
+ pub use crate::core::widget::*;
+ pub use crate::runtime::task::widget as operate;
+}
+
pub use crate::core::clipboard::{self, Clipboard};
pub use crate::core::image;
pub use crate::core::layout::{self, Layout};
@@ -7,13 +21,6 @@ pub use crate::core::overlay::{self, Overlay};
pub use crate::core::renderer::{self, Renderer};
pub use crate::core::svg;
pub use crate::core::text::{self, Text};
-pub use crate::core::widget::{self, Widget};
pub use crate::core::Shell;
pub use crate::renderer::graphics;
-
-pub mod subscription {
- //! Write your own subscriptions.
- pub use crate::runtime::futures::subscription::{
- EventStream, Hasher, Recipe,
- };
-}
+pub use widget::Widget;
diff --git a/src/application.rs b/src/application.rs
index 5d16b40f..d0f77304 100644
--- a/src/application.rs
+++ b/src/application.rs
@@ -103,10 +103,6 @@ where
type Renderer = Renderer;
type Executor = iced_futures::backend::default::Executor;
- fn load(&self) -> Task<Self::Message> {
- Task::none()
- }
-
fn update(
&self,
state: &mut Self::State,
@@ -166,14 +162,14 @@ impl<P: Program> Application<P> {
Self: 'static,
P::State: Default,
{
- self.run_with(P::State::default)
+ self.raw.run(self.settings, Some(self.window))
}
/// Runs the [`Application`] with a closure that creates the initial state.
pub fn run_with<I>(self, initialize: I) -> Result
where
Self: 'static,
- I: Fn() -> P::State + Clone + 'static,
+ I: FnOnce() -> (P::State, Task<P::Message>) + 'static,
{
self.raw
.run_with(self.settings, Some(self.window), initialize)
@@ -323,20 +319,6 @@ impl<P: Program> Application<P> {
}
}
- /// Runs the [`Task`] produced by the closure at startup.
- pub fn load(
- self,
- f: impl Fn() -> Task<P::Message>,
- ) -> Application<
- impl Program<State = P::State, Message = P::Message, Theme = P::Theme>,
- > {
- Application {
- raw: program::with_load(self.raw, f),
- settings: self.settings,
- window: self.window,
- }
- }
-
/// Sets the subscription logic of the [`Application`].
pub fn subscription(
self,
@@ -435,6 +417,15 @@ pub trait Update<State, Message> {
) -> impl Into<Task<Message>>;
}
+impl<State, Message> Update<State, Message> for () {
+ fn update(
+ &self,
+ _state: &mut State,
+ _message: Message,
+ ) -> impl Into<Task<Message>> {
+ }
+}
+
impl<T, State, Message, C> Update<State, Message> for T
where
T: Fn(&mut State, Message) -> C,
diff --git a/src/daemon.rs b/src/daemon.rs
index 58293949..6a6ad133 100644
--- a/src/daemon.rs
+++ b/src/daemon.rs
@@ -55,10 +55,6 @@ where
type Renderer = Renderer;
type Executor = iced_futures::backend::default::Executor;
- fn load(&self) -> Task<Self::Message> {
- Task::none()
- }
-
fn update(
&self,
state: &mut Self::State,
@@ -116,14 +112,14 @@ impl<P: Program> Daemon<P> {
Self: 'static,
P::State: Default,
{
- self.run_with(P::State::default)
+ self.raw.run(self.settings, None)
}
/// Runs the [`Daemon`] with a closure that creates the initial state.
pub fn run_with<I>(self, initialize: I) -> Result
where
Self: 'static,
- I: Fn() -> P::State + Clone + 'static,
+ I: FnOnce() -> (P::State, Task<P::Message>) + 'static,
{
self.raw.run_with(self.settings, None, initialize)
}
@@ -176,19 +172,6 @@ impl<P: Program> Daemon<P> {
}
}
- /// Runs the [`Task`] produced by the closure at startup.
- pub fn load(
- self,
- f: impl Fn() -> Task<P::Message>,
- ) -> Daemon<
- impl Program<State = P::State, Message = P::Message, Theme = P::Theme>,
- > {
- Daemon {
- raw: program::with_load(self.raw, f),
- settings: self.settings,
- }
- }
-
/// Sets the subscription logic of the [`Daemon`].
pub fn subscription(
self,
diff --git a/src/lib.rs b/src/lib.rs
index bc3fe6ab..022f8d6e 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,170 +1,451 @@
-//! Iced is a cross-platform GUI library focused on simplicity and type-safety.
+//! iced is a cross-platform GUI library focused on simplicity and type-safety.
//! Inspired by [Elm].
//!
-//! # Features
-//! * Simple, easy-to-use, batteries-included API
-//! * Type-safe, reactive programming model
-//! * [Cross-platform support] (Windows, macOS, Linux, and the Web)
-//! * Responsive layout
-//! * Built-in widgets (including [text inputs], [scrollables], and more!)
-//! * Custom widget support (create your own!)
-//! * [Debug overlay with performance metrics]
-//! * First-class support for async actions (use futures!)
-//! * [Modular ecosystem] split into reusable parts:
-//! * A [renderer-agnostic native runtime] enabling integration with existing
-//! systems
-//! * A [built-in renderer] supporting Vulkan, Metal, DX11, and DX12
-//! * A [windowing shell]
-//! * A [web runtime] leveraging the DOM
-//!
-//! Check out the [repository] and the [examples] for more details!
-//!
-//! [Cross-platform support]: https://github.com/iced-rs/iced/blob/master/docs/images/todos_desktop.jpg?raw=true
-//! [text inputs]: https://iced.rs/examples/text_input.mp4
-//! [scrollables]: https://iced.rs/examples/scrollable.mp4
-//! [Debug overlay with performance metrics]: https://iced.rs/examples/debug.mp4
-//! [Modular ecosystem]: https://github.com/iced-rs/iced/blob/master/ECOSYSTEM.md
-//! [renderer-agnostic native runtime]: https://github.com/iced-rs/iced/tree/0.12/runtime
-//! [`wgpu`]: https://github.com/gfx-rs/wgpu-rs
-//! [built-in renderer]: https://github.com/iced-rs/iced/tree/0.12/wgpu
-//! [windowing shell]: https://github.com/iced-rs/iced/tree/0.12/winit
-//! [`dodrio`]: https://github.com/fitzgen/dodrio
-//! [web runtime]: https://github.com/iced-rs/iced_web
-//! [examples]: https://github.com/iced-rs/iced/tree/0.12/examples
-//! [repository]: https://github.com/iced-rs/iced
-//!
-//! # Overview
-//! Inspired by [The Elm Architecture], Iced expects you to split user
-//! interfaces into four different concepts:
-//!
-//! * __State__ — the state of your application
-//! * __Messages__ — user interactions or meaningful events that you care
-//! about
-//! * __View logic__ — a way to display your __state__ as widgets that
-//! may produce __messages__ on user interaction
-//! * __Update logic__ — a way to react to __messages__ and update your
-//! __state__
-//!
-//! We can build something to see how this works! Let's say we want a simple
-//! counter that can be incremented and decremented using two buttons.
-//!
-//! We start by modelling the __state__ of our application:
+//! [Elm]: https://elm-lang.org/
+//!
+//! # Disclaimer
+//! iced is __experimental__ software. If you expect the documentation to hold your hand
+//! as you learn the ropes, you are in for a frustrating experience.
+//!
+//! The library leverages Rust to its full extent: ownership, borrowing, lifetimes, futures,
+//! streams, first-class functions, trait bounds, closures, and more. This documentation
+//! is not meant to teach you any of these. Far from it, it will assume you have __mastered__
+//! all of them.
+//!
+//! Furthermore—just like Rust—iced is very unforgiving. It will not let you easily cut corners.
+//! The type signatures alone can be used to learn how to use most of the library.
+//! Everything is connected.
+//!
+//! Therefore, iced is easy to learn for __advanced__ Rust programmers; but plenty of patient
+//! beginners have learned it and had a good time with it. Since it leverages a lot of what
+//! Rust has to offer in a type-safe way, it can be a great way to discover Rust itself.
//!
+//! If you don't like the sound of that, you expect to be spoonfed, or you feel frustrated
+//! and struggle to use the library; then I recommend you to wait patiently until [the book]
+//! is finished.
+//!
+//! [the book]: https://book.iced.rs
+//!
+//! # The Pocket Guide
+//! Start by calling [`run`]:
+//!
+//! ```rust,no_run
+//! pub fn main() -> iced::Result {
+//! iced::run("A cool counter", update, view)
+//! }
+//! # fn update(state: &mut (), message: ()) {}
+//! # fn view(state: &()) -> iced::Element<()> { iced::widget::text("").into() }
//! ```
-//! #[derive(Default)]
-//! struct Counter {
-//! // The counter value
-//! value: i32,
+//!
+//! Define an `update` function to __change__ your state:
+//!
+//! ```rust
+//! fn update(counter: &mut u64, message: Message) {
+//! match message {
+//! Message::Increment => *counter += 1,
+//! }
//! }
+//! # #[derive(Clone)]
+//! # enum Message { Increment }
//! ```
//!
-//! Next, we need to define the possible user interactions of our counter:
-//! the button presses. These interactions are our __messages__:
+//! Define a `view` function to __display__ your state:
+//!
+//! ```rust
+//! use iced::widget::{button, text};
+//! use iced::Element;
//!
+//! fn view(counter: &u64) -> Element<Message> {
+//! button(text(counter)).on_press(Message::Increment).into()
+//! }
+//! # #[derive(Clone)]
+//! # enum Message { Increment }
//! ```
-//! #[derive(Debug, Clone, Copy)]
-//! pub enum Message {
+//!
+//! And create a `Message` enum to __connect__ `view` and `update` together:
+//!
+//! ```rust
+//! #[derive(Debug, Clone)]
+//! enum Message {
//! Increment,
-//! Decrement,
//! }
//! ```
//!
-//! Now, let's show the actual counter by putting it all together in our
-//! __view logic__:
+//! ## Custom State
+//! You can define your own struct for your state:
//!
+//! ```rust
+//! #[derive(Default)]
+//! struct Counter {
+//! value: u64,
+//! }
//! ```
-//! # struct Counter {
-//! # // The counter value
-//! # value: i32,
-//! # }
-//! #
-//! # #[derive(Debug, Clone, Copy)]
-//! # pub enum Message {
-//! # Increment,
-//! # Decrement,
-//! # }
-//! #
-//! use iced::widget::{button, column, text, Column};
//!
-//! impl Counter {
-//! pub fn view(&self) -> Column<Message> {
-//! // We use a column: a simple vertical layout
-//! column![
-//! // The increment button. We tell it to produce an
-//! // `Increment` message when pressed
-//! button("+").on_press(Message::Increment),
+//! But you have to change `update` and `view` accordingly:
+//!
+//! ```rust
+//! # struct Counter { value: u64 }
+//! # #[derive(Clone)]
+//! # enum Message { Increment }
+//! # use iced::widget::{button, text};
+//! # use iced::Element;
+//! fn update(counter: &mut Counter, message: Message) {
+//! match message {
+//! Message::Increment => counter.value += 1,
+//! }
+//! }
+//!
+//! fn view(counter: &Counter) -> Element<Message> {
+//! button(text(counter.value)).on_press(Message::Increment).into()
+//! }
+//! ```
+//!
+//! ## Widgets and Elements
+//! The `view` function must return an [`Element`]. An [`Element`] is just a generic [`widget`].
+//!
+//! The [`widget`] module contains a bunch of functions to help you build
+//! and use widgets.
+//!
+//! Widgets are configured using the builder pattern:
//!
-//! // We show the value of the counter here
-//! text(self.value).size(50),
+//! ```rust
+//! # struct Counter { value: u64 }
+//! # #[derive(Clone)]
+//! # enum Message { Increment }
+//! use iced::widget::{button, column, text};
+//! use iced::Element;
//!
-//! // The decrement button. We tell it to produce a
-//! // `Decrement` message when pressed
-//! button("-").on_press(Message::Decrement),
+//! fn view(counter: &Counter) -> Element<Message> {
+//! column![
+//! text(counter.value).size(20),
+//! button("Increment").on_press(Message::Increment),
+//! ]
+//! .spacing(10)
+//! .into()
+//! }
+//! ```
+//!
+//! A widget can be turned into an [`Element`] by calling `into`.
+//!
+//! Widgets and elements are generic over the message type they produce. The
+//! [`Element`] returned by `view` must have the same `Message` type as
+//! your `update`.
+//!
+//! ## Layout
+//! There is no unified layout system in iced. Instead, each widget implements
+//! its own layout strategy.
+//!
+//! Building your layout will often consist in using a combination of
+//! [rows], [columns], and [containers]:
+//!
+//! ```rust
+//! # struct State;
+//! # enum Message {}
+//! use iced::widget::{column, container, row};
+//! use iced::{Fill, Element};
+//!
+//! fn view(state: &State) -> Element<Message> {
+//! container(
+//! column![
+//! "Top",
+//! row!["Left", "Right"].spacing(10),
+//! "Bottom"
//! ]
-//! }
+//! .spacing(10)
+//! )
+//! .padding(10)
+//! .center_x(Fill)
+//! .center_y(Fill)
+//! .into()
//! }
//! ```
//!
-//! Finally, we need to be able to react to any produced __messages__ and change
-//! our __state__ accordingly in our __update logic__:
+//! Rows and columns lay out their children horizontally and vertically,
+//! respectively. [Spacing] can be easily added between elements.
+//!
+//! Containers position or align a single widget inside their bounds.
+//!
+//! [rows]: widget::Row
+//! [columns]: widget::Column
+//! [containers]: widget::Container
+//! [Spacing]: widget::Column::spacing
+//!
+//! ## Sizing
+//! The width and height of widgets can generally be defined using a [`Length`].
+//!
+//! - [`Fill`] will make the widget take all the available space in a given axis.
+//! - [`Shrink`] will make the widget use its intrinsic size.
+//!
+//! Most widgets use a [`Shrink`] sizing strategy by default, but will inherit
+//! a [`Fill`] strategy from their children.
+//!
+//! A fixed numeric [`Length`] in [`Pixels`] can also be used:
+//!
+//! ```rust
+//! # struct State;
+//! # enum Message {}
+//! use iced::widget::container;
+//! use iced::Element;
//!
+//! fn view(state: &State) -> Element<Message> {
+//! container("I am 300px tall!").height(300).into()
+//! }
//! ```
-//! # struct Counter {
-//! # // The counter value
-//! # value: i32,
-//! # }
-//! #
-//! # #[derive(Debug, Clone, Copy)]
-//! # pub enum Message {
-//! # Increment,
-//! # Decrement,
-//! # }
-//! impl Counter {
-//! // ...
//!
-//! pub fn update(&mut self, message: Message) {
-//! match message {
-//! Message::Increment => {
-//! self.value += 1;
-//! }
-//! Message::Decrement => {
-//! self.value -= 1;
+//! ## Theming
+//! The default [`Theme`] of an application can be changed by defining a `theme`
+//! function and leveraging the [`Application`] builder, instead of directly
+//! calling [`run`]:
+//!
+//! ```rust,no_run
+//! # #[derive(Default)]
+//! # struct State;
+//! use iced::Theme;
+//!
+//! pub fn main() -> iced::Result {
+//! iced::application("A cool application", update, view)
+//! .theme(theme)
+//! .run()
+//! }
+//!
+//! fn theme(state: &State) -> Theme {
+//! Theme::TokyoNight
+//! }
+//! # fn update(state: &mut State, message: ()) {}
+//! # fn view(state: &State) -> iced::Element<()> { iced::widget::text("").into() }
+//! ```
+//!
+//! The `theme` function takes the current state of the application, allowing the
+//! returned [`Theme`] to be completely dynamic—just like `view`.
+//!
+//! There are a bunch of built-in [`Theme`] variants at your disposal, but you can
+//! also [create your own](Theme::custom).
+//!
+//! ## Styling
+//! As with layout, iced does not have a unified styling system. However, all
+//! of the built-in widgets follow the same styling approach.
+//!
+//! The appearance of a widget can be changed by calling its `style` method:
+//!
+//! ```rust
+//! # struct State;
+//! # enum Message {}
+//! use iced::widget::container;
+//! use iced::Element;
+//!
+//! fn view(state: &State) -> Element<Message> {
+//! container("I am a rounded box!").style(container::rounded_box).into()
+//! }
+//! ```
+//!
+//! The `style` method of a widget takes a closure that, given the current active
+//! [`Theme`], returns the widget style:
+//!
+//! ```rust
+//! # struct State;
+//! # #[derive(Clone)]
+//! # enum Message {}
+//! use iced::widget::button;
+//! use iced::{Element, Theme};
+//!
+//! fn view(state: &State) -> Element<Message> {
+//! button("I am a styled button!").style(|theme: &Theme, status| {
+//! let palette = theme.extended_palette();
+//!
+//! match status {
+//! button::Status::Active => {
+//! button::Style::default()
+//! .with_background(palette.success.strong.color)
//! }
+//! _ => button::primary(theme, status),
//! }
+//! })
+//! .into()
+//! }
+//! ```
+//!
+//! Widgets that can be in multiple different states will also provide the closure
+//! with some [`Status`], allowing you to use a different style for each state.
+//!
+//! You can extract the [`Palette`] colors of a [`Theme`] with the [`palette`] or
+//! [`extended_palette`] methods.
+//!
+//! Most widgets provide styling functions for your convenience in their respective modules;
+//! like [`container::rounded_box`], [`button::primary`], or [`text::danger`].
+//!
+//! [`Status`]: widget::button::Status
+//! [`palette`]: Theme::palette
+//! [`extended_palette`]: Theme::extended_palette
+//! [`container::rounded_box`]: widget::container::rounded_box
+//! [`button::primary`]: widget::button::primary
+//! [`text::danger`]: widget::text::danger
+//!
+//! ## Concurrent Tasks
+//! The `update` function can _optionally_ return a [`Task`].
+//!
+//! A [`Task`] can be leveraged to perform asynchronous work, like running a
+//! future or a stream:
+//!
+//! ```rust
+//! # #[derive(Clone)]
+//! # struct Weather;
+//! use iced::Task;
+//!
+//! struct State {
+//! weather: Option<Weather>,
+//! }
+//!
+//! enum Message {
+//! FetchWeather,
+//! WeatherFetched(Weather),
+//! }
+//!
+//! fn update(state: &mut State, message: Message) -> Task<Message> {
+//! match message {
+//! Message::FetchWeather => Task::perform(
+//! fetch_weather(),
+//! Message::WeatherFetched,
+//! ),
+//! Message::WeatherFetched(weather) => {
+//! state.weather = Some(weather);
+//!
+//! Task::none()
+//! }
//! }
//! }
+//!
+//! async fn fetch_weather() -> Weather {
+//! // ...
+//! # unimplemented!()
+//! }
//! ```
//!
-//! And that's everything! We just wrote a whole user interface. Let's run it:
+//! Tasks can also be used to interact with the iced runtime. Some modules
+//! expose functions that create tasks for different purposes—like [changing
+//! window settings](window#functions), [focusing a widget](widget::focus_next), or
+//! [querying its visible bounds](widget::container::visible_bounds).
+//!
+//! Like futures and streams, tasks expose [a monadic interface](Task::then)—but they can also be
+//! [mapped](Task::map), [chained](Task::chain), [batched](Task::batch), [canceled](Task::abortable),
+//! and more.
+//!
+//! ## Passive Subscriptions
+//! Applications can subscribe to passive sources of data—like time ticks or runtime events.
+//!
+//! You will need to define a `subscription` function and use the [`Application`] builder:
//!
-//! ```no_run
+//! ```rust,no_run
//! # #[derive(Default)]
-//! # struct Counter;
-//! # impl Counter {
-//! # fn update(&mut self, _message: ()) {}
-//! # fn view(&self) -> iced::Element<()> { unimplemented!() }
-//! # }
-//! #
-//! fn main() -> iced::Result {
-//! iced::run("A cool counter", Counter::update, Counter::view)
+//! # struct State;
+//! use iced::window;
+//! use iced::{Size, Subscription};
+//!
+//! #[derive(Debug)]
+//! enum Message {
+//! WindowResized(Size),
+//! }
+//!
+//! pub fn main() -> iced::Result {
+//! iced::application("A cool application", update, view)
+//! .subscription(subscription)
+//! .run()
//! }
+//!
+//! fn subscription(state: &State) -> Subscription<Message> {
+//! window::resize_events().map(|(_id, size)| Message::WindowResized(size))
+//! }
+//! # fn update(state: &mut State, message: Message) {}
+//! # fn view(state: &State) -> iced::Element<Message> { iced::widget::text("").into() }
//! ```
//!
-//! Iced will automatically:
+//! A [`Subscription`] is [a _declarative_ builder of streams](Subscription#the-lifetime-of-a-subscription)
+//! that are not allowed to end on their own. Only the `subscription` function
+//! dictates the active subscriptions—just like `view` fully dictates the
+//! visible widgets of your user interface, at every moment.
//!
-//! 1. Take the result of our __view logic__ and layout its widgets.
-//! 1. Process events from our system and produce __messages__ for our
-//! __update logic__.
-//! 1. Draw the resulting user interface.
+//! As with tasks, some modules expose convenient functions that build a [`Subscription`] for you—like
+//! [`time::every`] which can be used to listen to time, or [`keyboard::on_key_press`] which will notify you
+//! of any key presses. But you can also create your own with [`Subscription::run`] and [`run_with_id`].
//!
-//! # Usage
-//! Use [`run`] or the [`application`] builder.
+//! [`run_with_id`]: Subscription::run_with_id
//!
-//! [Elm]: https://elm-lang.org/
-//! [The Elm Architecture]: https://guide.elm-lang.org/architecture/
-//! [`application`]: application()
+//! ## Scaling Applications
+//! The `update`, `view`, and `Message` triplet composes very nicely.
+//!
+//! A common pattern is to leverage this composability to split an
+//! application into different screens:
+//!
+//! ```rust
+//! # mod contacts {
+//! # use iced::{Element, Task};
+//! # pub struct Contacts;
+//! # impl Contacts {
+//! # pub fn update(&mut self, message: Message) -> Task<Message> { unimplemented!() }
+//! # pub fn view(&self) -> Element<Message> { unimplemented!() }
+//! # }
+//! # #[derive(Debug)]
+//! # pub enum Message {}
+//! # }
+//! # mod conversation {
+//! # use iced::{Element, Task};
+//! # pub struct Conversation;
+//! # impl Conversation {
+//! # pub fn update(&mut self, message: Message) -> Task<Message> { unimplemented!() }
+//! # pub fn view(&self) -> Element<Message> { unimplemented!() }
+//! # }
+//! # #[derive(Debug)]
+//! # pub enum Message {}
+//! # }
+//! use contacts::Contacts;
+//! use conversation::Conversation;
+//!
+//! use iced::{Element, Task};
+//!
+//! struct State {
+//! screen: Screen,
+//! }
+//!
+//! enum Screen {
+//! Contacts(Contacts),
+//! Conversation(Conversation),
+//! }
+//!
+//! enum Message {
+//! Contacts(contacts::Message),
+//! Conversation(conversation::Message)
+//! }
+//!
+//! fn update(state: &mut State, message: Message) -> Task<Message> {
+//! match message {
+//! Message::Contacts(message) => {
+//! if let Screen::Contacts(contacts) = &mut state.screen {
+//! contacts.update(message).map(Message::Contacts)
+//! } else {
+//! Task::none()
+//! }
+//! }
+//! Message::Conversation(message) => {
+//! if let Screen::Conversation(conversation) = &mut state.screen {
+//! conversation.update(message).map(Message::Conversation)
+//! } else {
+//! Task::none()
+//! }
+//! }
+//! }
+//! }
+//!
+//! fn view(state: &State) -> Element<Message> {
+//! match &state.screen {
+//! Screen::Contacts(contacts) => contacts.view().map(Message::Contacts),
+//! Screen::Conversation(conversation) => conversation.view().map(Message::Conversation),
+//! }
+//! }
+//! ```
+//!
+//! Functor methods like [`Task::map`], [`Element::map`], and [`Subscription::map`] make this
+//! approach seamless.
#![doc(
- html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg"
+ html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/bdf0430880f5c29443f5f0a0ae4895866dfef4c6/docs/logo.svg"
)]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![cfg_attr(docsrs, feature(doc_cfg))]
@@ -175,6 +456,7 @@ use iced_winit::core;
use iced_winit::runtime;
pub use iced_futures::futures;
+pub use iced_futures::stream;
#[cfg(feature = "highlighter")]
pub use iced_highlighter as highlighter;
@@ -195,13 +477,25 @@ pub use crate::core::alignment;
pub use crate::core::border;
pub use crate::core::color;
pub use crate::core::gradient;
+pub use crate::core::padding;
pub use crate::core::theme;
pub use crate::core::{
Alignment, Background, Border, Color, ContentFit, Degrees, Gradient,
Length, Padding, Pixels, Point, Radians, Rectangle, Rotation, Shadow, Size,
Theme, Transformation, Vector,
};
-pub use crate::runtime::{exit, Task};
+pub use crate::runtime::exit;
+pub use iced_futures::Subscription;
+
+pub use alignment::Horizontal::{Left, Right};
+pub use alignment::Vertical::{Bottom, Top};
+pub use Alignment::Center;
+pub use Length::{Fill, FillPortion, Shrink};
+
+pub mod task {
+ //! Create runtime tasks.
+ pub use crate::runtime::task::{Handle, Task};
+}
pub mod clipboard {
//! Access the clipboard.
@@ -255,13 +549,6 @@ pub mod mouse {
};
}
-pub mod subscription {
- //! Listen to external events in your application.
- pub use iced_futures::subscription::{
- channel, run, run_with_id, unfold, Subscription,
- };
-}
-
#[cfg(feature = "system")]
pub mod system {
//! Retrieve system information.
@@ -314,7 +601,7 @@ pub use executor::Executor;
pub use font::Font;
pub use renderer::Renderer;
pub use settings::Settings;
-pub use subscription::Subscription;
+pub use task::Task;
#[doc(inline)]
pub use application::application;
@@ -339,8 +626,6 @@ pub type Result = std::result::Result<(), Error>;
///
/// This is equivalent to chaining [`application()`] with [`Application::run`].
///
-/// [`program`]: program()
-///
/// # Example
/// ```no_run
/// use iced::widget::{button, column, text, Column};
diff --git a/src/program.rs b/src/program.rs
index 3f9d2d0c..2b697fbe 100644
--- a/src/program.rs
+++ b/src/program.rs
@@ -27,8 +27,6 @@ pub trait Program: Sized {
/// The executor of the program.
type Executor: Executor;
- fn load(&self) -> Task<Self::Message>;
-
fn update(
&self,
state: &mut Self::State,
@@ -80,7 +78,9 @@ pub trait Program: Sized {
Self: 'static,
Self::State: Default,
{
- self.run_with(settings, window_settings, Self::State::default)
+ self.run_with(settings, window_settings, || {
+ (Self::State::default(), Task::none())
+ })
}
/// Runs the [`Program`] with the given [`Settings`] and a closure that creates the initial state.
@@ -92,7 +92,7 @@ pub trait Program: Sized {
) -> Result
where
Self: 'static,
- I: Fn() -> Self::State + Clone + 'static,
+ I: FnOnce() -> (Self::State, Task<Self::Message>) + 'static,
{
use std::marker::PhantomData;
@@ -102,7 +102,9 @@ pub trait Program: Sized {
_initialize: PhantomData<I>,
}
- impl<P: Program, I: Fn() -> P::State> shell::Program for Instance<P, I> {
+ impl<P: Program, I: FnOnce() -> (P::State, Task<P::Message>)>
+ shell::Program for Instance<P, I>
+ {
type Message = P::Message;
type Theme = P::Theme;
type Renderer = P::Renderer;
@@ -112,8 +114,7 @@ pub trait Program: Sized {
fn new(
(program, initialize): Self::Flags,
) -> (Self, Task<Self::Message>) {
- let state = initialize();
- let command = program.load();
+ let (state, task) = initialize();
(
Self {
@@ -121,7 +122,7 @@ pub trait Program: Sized {
state,
_initialize: PhantomData,
},
- command,
+ task,
)
}
@@ -212,10 +213,6 @@ pub fn with_title<P: Program>(
type Renderer = P::Renderer;
type Executor = P::Executor;
- fn load(&self) -> Task<Self::Message> {
- self.program.load()
- }
-
fn title(&self, state: &Self::State, window: window::Id) -> String {
(self.title)(state, window)
}
@@ -267,80 +264,6 @@ pub fn with_title<P: Program>(
WithTitle { program, title }
}
-pub fn with_load<P: Program>(
- program: P,
- f: impl Fn() -> Task<P::Message>,
-) -> impl Program<State = P::State, Message = P::Message, Theme = P::Theme> {
- struct WithLoad<P, F> {
- program: P,
- load: F,
- }
-
- impl<P: Program, F> Program for WithLoad<P, F>
- where
- F: Fn() -> Task<P::Message>,
- {
- type State = P::State;
- type Message = P::Message;
- type Theme = P::Theme;
- type Renderer = P::Renderer;
- type Executor = P::Executor;
-
- fn load(&self) -> Task<Self::Message> {
- Task::batch([self.program.load(), (self.load)()])
- }
-
- fn update(
- &self,
- state: &mut Self::State,
- message: Self::Message,
- ) -> Task<Self::Message> {
- self.program.update(state, message)
- }
-
- fn view<'a>(
- &self,
- state: &'a Self::State,
- window: window::Id,
- ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> {
- self.program.view(state, window)
- }
-
- fn title(&self, state: &Self::State, window: window::Id) -> String {
- self.program.title(state, window)
- }
-
- fn subscription(
- &self,
- state: &Self::State,
- ) -> Subscription<Self::Message> {
- self.program.subscription(state)
- }
-
- fn theme(
- &self,
- state: &Self::State,
- window: window::Id,
- ) -> Self::Theme {
- self.program.theme(state, window)
- }
-
- fn style(
- &self,
- state: &Self::State,
- theme: &Self::Theme,
- ) -> Appearance {
- self.program.style(state, theme)
- }
-
- fn scale_factor(&self, state: &Self::State, window: window::Id) -> f64 {
- self.program.scale_factor(state, window)
- }
- }
-
- WithLoad { program, load: f }
-}
-
pub fn with_subscription<P: Program>(
program: P,
f: impl Fn(&P::State) -> Subscription<P::Message>,
@@ -367,10 +290,6 @@ pub fn with_subscription<P: Program>(
(self.subscription)(state)
}
- fn load(&self) -> Task<Self::Message> {
- self.program.load()
- }
-
fn update(
&self,
state: &mut Self::State,
@@ -445,10 +364,6 @@ pub fn with_theme<P: Program>(
(self.theme)(state, window)
}
- fn load(&self) -> Task<Self::Message> {
- self.program.load()
- }
-
fn title(&self, state: &Self::State, window: window::Id) -> String {
self.program.title(state, window)
}
@@ -519,10 +434,6 @@ pub fn with_style<P: Program>(
(self.style)(state, theme)
}
- fn load(&self) -> Task<Self::Message> {
- self.program.load()
- }
-
fn title(&self, state: &Self::State, window: window::Id) -> String {
self.program.title(state, window)
}
@@ -585,10 +496,6 @@ pub fn with_scale_factor<P: Program>(
type Renderer = P::Renderer;
type Executor = P::Executor;
- fn load(&self) -> Task<Self::Message> {
- self.program.load()
- }
-
fn title(&self, state: &Self::State, window: window::Id) -> String {
self.program.title(state, window)
}
diff --git a/tiny_skia/Cargo.toml b/tiny_skia/Cargo.toml
index 32ead3e0..323233f0 100644
--- a/tiny_skia/Cargo.toml
+++ b/tiny_skia/Cargo.toml
@@ -15,7 +15,7 @@ workspace = true
[features]
image = ["iced_graphics/image"]
-svg = ["resvg"]
+svg = ["iced_graphics/svg", "resvg"]
geometry = ["iced_graphics/geometry"]
[dependencies]
diff --git a/tiny_skia/src/engine.rs b/tiny_skia/src/engine.rs
index 028b304f..196c36cf 100644
--- a/tiny_skia/src/engine.rs
+++ b/tiny_skia/src/engine.rs
@@ -439,9 +439,13 @@ impl Engine {
let transformation = transformation * *local_transformation;
let (width, height) = buffer.size();
- let physical_bounds =
- Rectangle::new(raw.position, Size::new(width, height))
- * transformation;
+ let physical_bounds = Rectangle::new(
+ raw.position,
+ Size::new(
+ width.unwrap_or(clip_bounds.width),
+ height.unwrap_or(clip_bounds.height),
+ ),
+ ) * transformation;
if !clip_bounds.intersects(&physical_bounds) {
return;
@@ -546,13 +550,7 @@ impl Engine {
) {
match image {
#[cfg(feature = "image")]
- Image::Raster {
- handle,
- filter_method,
- bounds,
- rotation,
- opacity,
- } => {
+ Image::Raster(raster, bounds) => {
let physical_bounds = *bounds * _transformation;
if !_clip_bounds.intersects(&physical_bounds) {
@@ -563,7 +561,7 @@ impl Engine {
.then_some(_clip_mask as &_);
let center = physical_bounds.center();
- let radians = f32::from(*rotation);
+ let radians = f32::from(raster.rotation);
let transform = into_transform(_transformation).post_rotate_at(
radians.to_degrees(),
@@ -572,23 +570,17 @@ impl Engine {
);
self.raster_pipeline.draw(
- handle,
- *filter_method,
+ &raster.handle,
+ raster.filter_method,
*bounds,
- *opacity,
+ raster.opacity,
_pixels,
transform,
clip_mask,
);
}
#[cfg(feature = "svg")]
- Image::Vector {
- handle,
- color,
- bounds,
- rotation,
- opacity,
- } => {
+ Image::Vector(svg, bounds) => {
let physical_bounds = *bounds * _transformation;
if !_clip_bounds.intersects(&physical_bounds) {
@@ -599,7 +591,7 @@ impl Engine {
.then_some(_clip_mask as &_);
let center = physical_bounds.center();
- let radians = f32::from(*rotation);
+ let radians = f32::from(svg.rotation);
let transform = into_transform(_transformation).post_rotate_at(
radians.to_degrees(),
@@ -608,10 +600,10 @@ impl Engine {
);
self.vector_pipeline.draw(
- handle,
- *color,
+ &svg.handle,
+ svg.color,
physical_bounds,
- *opacity,
+ svg.opacity,
_pixels,
transform,
clip_mask,
diff --git a/tiny_skia/src/geometry.rs b/tiny_skia/src/geometry.rs
index 02b6e1b9..0d5fff62 100644
--- a/tiny_skia/src/geometry.rs
+++ b/tiny_skia/src/geometry.rs
@@ -1,10 +1,10 @@
use crate::core::text::LineHeight;
-use crate::core::{Pixels, Point, Radians, Rectangle, Size, Vector};
+use crate::core::{self, Pixels, Point, Radians, Rectangle, Size, Svg, Vector};
use crate::graphics::cache::{self, Cached};
use crate::graphics::geometry::fill::{self, Fill};
use crate::graphics::geometry::stroke::{self, Stroke};
use crate::graphics::geometry::{self, Path, Style};
-use crate::graphics::{Gradient, Text};
+use crate::graphics::{self, Gradient, Image, Text};
use crate::Primitive;
use std::rc::Rc;
@@ -13,6 +13,7 @@ use std::rc::Rc;
pub enum Geometry {
Live {
text: Vec<Text>,
+ images: Vec<graphics::Image>,
primitives: Vec<Primitive>,
clip_bounds: Rectangle,
},
@@ -22,6 +23,7 @@ pub enum Geometry {
#[derive(Debug, Clone)]
pub struct Cache {
pub text: Rc<[Text]>,
+ pub images: Rc<[graphics::Image]>,
pub primitives: Rc<[Primitive]>,
pub clip_bounds: Rectangle,
}
@@ -37,10 +39,12 @@ impl Cached for Geometry {
match self {
Self::Live {
primitives,
+ images,
text,
clip_bounds,
} => Cache {
primitives: Rc::from(primitives),
+ images: Rc::from(images),
text: Rc::from(text),
clip_bounds,
},
@@ -55,6 +59,7 @@ pub struct Frame {
transform: tiny_skia::Transform,
stack: Vec<tiny_skia::Transform>,
primitives: Vec<Primitive>,
+ images: Vec<graphics::Image>,
text: Vec<Text>,
}
@@ -68,6 +73,7 @@ impl Frame {
clip_bounds,
stack: Vec::new(),
primitives: Vec::new(),
+ images: Vec::new(),
text: Vec::new(),
transform: tiny_skia::Transform::from_translate(
clip_bounds.x,
@@ -162,6 +168,15 @@ impl geometry::frame::Backend for Frame {
});
}
+ fn stroke_rectangle<'a>(
+ &mut self,
+ top_left: Point,
+ size: Size,
+ stroke: impl Into<Stroke<'a>>,
+ ) {
+ self.stroke(&Path::rectangle(top_left, size), stroke);
+ }
+
fn fill_text(&mut self, text: impl Into<geometry::Text>) {
let text = text.into();
@@ -238,7 +253,7 @@ impl geometry::frame::Backend for Frame {
Self::with_clip(clip_bounds)
}
- fn paste(&mut self, frame: Self, _at: Point) {
+ fn paste(&mut self, frame: Self) {
self.primitives.extend(frame.primitives);
self.text.extend(frame.text);
}
@@ -269,10 +284,63 @@ impl geometry::frame::Backend for Frame {
fn into_geometry(self) -> Geometry {
Geometry::Live {
primitives: self.primitives,
+ images: self.images,
text: self.text,
clip_bounds: self.clip_bounds,
}
}
+
+ fn draw_image(&mut self, bounds: Rectangle, image: impl Into<core::Image>) {
+ let mut image = image.into();
+
+ let (bounds, external_rotation) =
+ transform_rectangle(bounds, self.transform);
+
+ image.rotation += external_rotation;
+
+ self.images.push(graphics::Image::Raster(image, bounds));
+ }
+
+ fn draw_svg(&mut self, bounds: Rectangle, svg: impl Into<Svg>) {
+ let mut svg = svg.into();
+
+ let (bounds, external_rotation) =
+ transform_rectangle(bounds, self.transform);
+
+ svg.rotation += external_rotation;
+
+ self.images.push(Image::Vector(svg, bounds));
+ }
+}
+
+fn transform_rectangle(
+ rectangle: Rectangle,
+ transform: tiny_skia::Transform,
+) -> (Rectangle, Radians) {
+ let mut top_left = tiny_skia::Point {
+ x: rectangle.x,
+ y: rectangle.y,
+ };
+
+ let mut top_right = tiny_skia::Point {
+ x: rectangle.x + rectangle.width,
+ y: rectangle.y,
+ };
+
+ let mut bottom_left = tiny_skia::Point {
+ x: rectangle.x,
+ y: rectangle.y + rectangle.height,
+ };
+
+ transform.map_point(&mut top_left);
+ transform.map_point(&mut top_right);
+ transform.map_point(&mut bottom_left);
+
+ Rectangle::with_vertices(
+ Point::new(top_left.x, top_left.y),
+ Point::new(top_right.x, top_right.y),
+ Point::new(bottom_left.x, bottom_left.y),
+ )
}
fn convert_path(path: &Path) -> Option<tiny_skia::Path> {
diff --git a/tiny_skia/src/layer.rs b/tiny_skia/src/layer.rs
index 48fca1d8..bdfd4d38 100644
--- a/tiny_skia/src/layer.rs
+++ b/tiny_skia/src/layer.rs
@@ -1,6 +1,6 @@
+use crate::core::renderer::Quad;
use crate::core::{
- image, renderer::Quad, svg, Background, Color, Point, Radians, Rectangle,
- Transformation,
+ self, Background, Color, Point, Rectangle, Svg, Transformation,
};
use crate::graphics::damage;
use crate::graphics::layer;
@@ -72,7 +72,7 @@ impl Layer {
pub fn draw_text(
&mut self,
- text: crate::core::Text,
+ text: core::Text,
position: Point,
color: Color,
clip_bounds: Rectangle,
@@ -115,42 +115,35 @@ impl Layer {
.push(Item::Cached(text, clip_bounds, transformation));
}
- pub fn draw_image(
+ pub fn draw_image(&mut self, image: Image, transformation: Transformation) {
+ match image {
+ Image::Raster(raster, bounds) => {
+ self.draw_raster(raster, bounds, transformation);
+ }
+ Image::Vector(svg, bounds) => {
+ self.draw_svg(svg, bounds, transformation);
+ }
+ }
+ }
+
+ pub fn draw_raster(
&mut self,
- handle: image::Handle,
- filter_method: image::FilterMethod,
+ image: core::Image,
bounds: Rectangle,
transformation: Transformation,
- rotation: Radians,
- opacity: f32,
) {
- let image = Image::Raster {
- handle,
- filter_method,
- bounds: bounds * transformation,
- rotation,
- opacity,
- };
+ let image = Image::Raster(image, bounds * transformation);
self.images.push(image);
}
pub fn draw_svg(
&mut self,
- handle: svg::Handle,
- color: Option<Color>,
+ svg: Svg,
bounds: Rectangle,
transformation: Transformation,
- rotation: Radians,
- opacity: f32,
) {
- let svg = Image::Vector {
- handle,
- color,
- bounds: bounds * transformation,
- rotation,
- opacity,
- };
+ let svg = Image::Vector(svg, bounds * transformation);
self.images.push(svg);
}
@@ -293,7 +286,7 @@ impl graphics::Layer for Layer {
fn flush(&mut self) {}
- fn resize(&mut self, bounds: graphics::core::Rectangle) {
+ fn resize(&mut self, bounds: Rectangle) {
self.bounds = bounds;
}
diff --git a/tiny_skia/src/lib.rs b/tiny_skia/src/lib.rs
index 1aabff00..758921d4 100644
--- a/tiny_skia/src/lib.rs
+++ b/tiny_skia/src/lib.rs
@@ -178,6 +178,16 @@ impl Renderer {
engine::adjust_clip_mask(clip_mask, clip_bounds);
}
+ for image in &layer.images {
+ self.engine.draw_image(
+ image,
+ Transformation::scale(scale_factor),
+ pixels,
+ clip_mask,
+ clip_bounds,
+ );
+ }
+
for group in &layer.text {
for text in group.as_slice() {
self.engine.draw_text(
@@ -190,16 +200,6 @@ impl Renderer {
);
}
}
-
- for image in &layer.images {
- self.engine.draw_image(
- image,
- Transformation::scale(scale_factor),
- pixels,
- clip_mask,
- clip_bounds,
- );
- }
}
if !overlay.is_empty() {
@@ -330,6 +330,7 @@ impl graphics::geometry::Renderer for Renderer {
match geometry {
Geometry::Live {
primitives,
+ images,
text,
clip_bounds,
} => {
@@ -339,6 +340,10 @@ impl graphics::geometry::Renderer for Renderer {
transformation,
);
+ for image in images {
+ layer.draw_image(image, transformation);
+ }
+
layer.draw_text_group(text, clip_bounds, transformation);
}
Geometry::Cache(cache) => {
@@ -348,6 +353,10 @@ impl graphics::geometry::Renderer for Renderer {
transformation,
);
+ for image in cache.images.iter() {
+ layer.draw_image(image.clone(), transformation);
+ }
+
layer.draw_text_cache(
cache.text,
cache.clip_bounds,
@@ -372,23 +381,9 @@ impl core::image::Renderer for Renderer {
self.engine.raster_pipeline.dimensions(handle)
}
- fn draw_image(
- &mut self,
- handle: Self::Handle,
- filter_method: core::image::FilterMethod,
- bounds: Rectangle,
- rotation: core::Radians,
- opacity: f32,
- ) {
+ fn draw_image(&mut self, image: core::Image, bounds: Rectangle) {
let (layer, transformation) = self.layers.current_mut();
- layer.draw_image(
- handle,
- filter_method,
- bounds,
- transformation,
- rotation,
- opacity,
- );
+ layer.draw_raster(image, bounds, transformation);
}
}
@@ -401,23 +396,9 @@ impl core::svg::Renderer for Renderer {
self.engine.vector_pipeline.viewport_dimensions(handle)
}
- fn draw_svg(
- &mut self,
- handle: core::svg::Handle,
- color: Option<Color>,
- bounds: Rectangle,
- rotation: core::Radians,
- opacity: f32,
- ) {
+ fn draw_svg(&mut self, svg: core::Svg, bounds: Rectangle) {
let (layer, transformation) = self.layers.current_mut();
- layer.draw_svg(
- handle,
- color,
- bounds,
- transformation,
- rotation,
- opacity,
- );
+ layer.draw_svg(svg, bounds, transformation);
}
}
diff --git a/tiny_skia/src/text.rs b/tiny_skia/src/text.rs
index c71deb10..0fc3d1f7 100644
--- a/tiny_skia/src/text.rs
+++ b/tiny_skia/src/text.rs
@@ -169,7 +169,13 @@ impl Pipeline {
font_system.raw(),
&mut self.glyph_cache,
buffer,
- Rectangle::new(position, Size::new(width, height)),
+ Rectangle::new(
+ position,
+ Size::new(
+ width.unwrap_or(pixels.width() as f32),
+ height.unwrap_or(pixels.height() as f32),
+ ),
+ ),
color,
alignment::Horizontal::Left,
alignment::Vertical::Top,
diff --git a/tiny_skia/src/vector.rs b/tiny_skia/src/vector.rs
index bbe08cb8..8a15f47f 100644
--- a/tiny_skia/src/vector.rs
+++ b/tiny_skia/src/vector.rs
@@ -1,8 +1,7 @@
use crate::core::svg::{Data, Handle};
use crate::core::{Color, Rectangle, Size};
-use crate::graphics::text;
-use resvg::usvg::{self, TreeTextToPath};
+use resvg::usvg;
use rustc_hash::{FxHashMap, FxHashSet};
use tiny_skia::Transform;
@@ -80,35 +79,28 @@ struct RasterKey {
impl Cache {
fn load(&mut self, handle: &Handle) -> Option<&usvg::Tree> {
- use usvg::TreeParsing;
-
let id = handle.id();
if let hash_map::Entry::Vacant(entry) = self.trees.entry(id) {
- let mut svg = match handle.data() {
+ let svg = match handle.data() {
Data::Path(path) => {
fs::read_to_string(path).ok().and_then(|contents| {
usvg::Tree::from_str(
&contents,
- &usvg::Options::default(),
+ &usvg::Options::default(), // TODO: Set usvg::Options::fontdb
)
.ok()
})
}
Data::Bytes(bytes) => {
- usvg::Tree::from_data(bytes, &usvg::Options::default()).ok()
+ usvg::Tree::from_data(
+ bytes,
+ &usvg::Options::default(), // TODO: Set usvg::Options::fontdb
+ )
+ .ok()
}
};
- if let Some(svg) = &mut svg {
- if svg.has_text_nodes() {
- let mut font_system =
- text::font_system().write().expect("Write font system");
-
- svg.convert_text(font_system.raw().db_mut());
- }
- }
-
let _ = entry.insert(svg);
}
@@ -118,11 +110,9 @@ impl Cache {
fn viewport_dimensions(&mut self, handle: &Handle) -> Option<Size<u32>> {
let tree = self.load(handle)?;
+ let size = tree.size();
- Some(Size::new(
- tree.size.width() as u32,
- tree.size.height() as u32,
- ))
+ Some(Size::new(size.width() as u32, size.height() as u32))
}
fn draw(
@@ -147,7 +137,7 @@ impl Cache {
let mut image = tiny_skia::Pixmap::new(size.width, size.height)?;
- let tree_size = tree.size.to_int_size();
+ let tree_size = tree.size().to_int_size();
let target_size = if size.width > size.height {
tree_size.scale_to_width(size.width)
@@ -167,7 +157,7 @@ impl Cache {
tiny_skia::Transform::default()
};
- resvg::Tree::from_usvg(tree).render(transform, &mut image.as_mut());
+ resvg::render(tree, transform, &mut image.as_mut());
if let Some([r, g, b, _]) = key.color {
// Apply color filter
diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml
index 30545fa2..b13ecb36 100644
--- a/wgpu/Cargo.toml
+++ b/wgpu/Cargo.toml
@@ -20,7 +20,7 @@ all-features = true
[features]
geometry = ["iced_graphics/geometry", "lyon"]
image = ["iced_graphics/image"]
-svg = ["resvg/text"]
+svg = ["iced_graphics/svg", "resvg/text"]
web-colors = ["iced_graphics/web-colors"]
webgl = ["wgpu/webgl"]
diff --git a/wgpu/src/geometry.rs b/wgpu/src/geometry.rs
index f6213e1d..8e6f77d7 100644
--- a/wgpu/src/geometry.rs
+++ b/wgpu/src/geometry.rs
@@ -1,7 +1,7 @@
//! Build and draw geometry.
use crate::core::text::LineHeight;
use crate::core::{
- Pixels, Point, Radians, Rectangle, Size, Transformation, Vector,
+ self, Pixels, Point, Radians, Rectangle, Size, Svg, Transformation, Vector,
};
use crate::graphics::cache::{self, Cached};
use crate::graphics::color;
@@ -11,7 +11,7 @@ use crate::graphics::geometry::{
};
use crate::graphics::gradient::{self, Gradient};
use crate::graphics::mesh::{self, Mesh};
-use crate::graphics::{self, Text};
+use crate::graphics::{Image, Text};
use crate::text;
use crate::triangle;
@@ -19,16 +19,22 @@ use lyon::geom::euclid;
use lyon::tessellation;
use std::borrow::Cow;
+use std::sync::Arc;
#[derive(Debug)]
pub enum Geometry {
- Live { meshes: Vec<Mesh>, text: Vec<Text> },
+ Live {
+ meshes: Vec<Mesh>,
+ images: Vec<Image>,
+ text: Vec<Text>,
+ },
Cached(Cache),
}
#[derive(Debug, Clone)]
pub struct Cache {
pub meshes: Option<triangle::Cache>,
+ pub images: Option<Arc<[Image]>>,
pub text: Option<text::Cache>,
}
@@ -45,7 +51,17 @@ impl Cached for Geometry {
previous: Option<Self::Cache>,
) -> Self::Cache {
match self {
- Self::Live { meshes, text } => {
+ Self::Live {
+ meshes,
+ images,
+ text,
+ } => {
+ let images = if images.is_empty() {
+ None
+ } else {
+ Some(Arc::from(images))
+ };
+
if let Some(mut previous) = previous {
if let Some(cache) = &mut previous.meshes {
cache.update(meshes);
@@ -59,10 +75,13 @@ impl Cached for Geometry {
previous.text = text::Cache::new(group, text);
}
+ previous.images = images;
+
previous
} else {
Cache {
meshes: triangle::Cache::new(meshes),
+ images,
text: text::Cache::new(group, text),
}
}
@@ -78,6 +97,7 @@ pub struct Frame {
clip_bounds: Rectangle,
buffers: BufferStack,
meshes: Vec<Mesh>,
+ images: Vec<Image>,
text: Vec<Text>,
transforms: Transforms,
fill_tessellator: tessellation::FillTessellator,
@@ -96,6 +116,7 @@ impl Frame {
clip_bounds: bounds,
buffers: BufferStack::new(),
meshes: Vec::new(),
+ images: Vec::new(),
text: Vec::new(),
transforms: Transforms {
previous: Vec::new(),
@@ -232,6 +253,44 @@ impl geometry::frame::Backend for Frame {
.expect("Stroke path");
}
+ fn stroke_rectangle<'a>(
+ &mut self,
+ top_left: Point,
+ size: Size,
+ stroke: impl Into<Stroke<'a>>,
+ ) {
+ let stroke = stroke.into();
+
+ let mut buffer = self
+ .buffers
+ .get_stroke(&self.transforms.current.transform_style(stroke.style));
+
+ let top_left = self
+ .transforms
+ .current
+ .0
+ .transform_point(lyon::math::Point::new(top_left.x, top_left.y));
+
+ let size =
+ self.transforms.current.0.transform_vector(
+ lyon::math::Vector::new(size.width, size.height),
+ );
+
+ let mut options = tessellation::StrokeOptions::default();
+ options.line_width = stroke.width;
+ options.start_cap = into_line_cap(stroke.line_cap);
+ options.end_cap = into_line_cap(stroke.line_cap);
+ options.line_join = into_line_join(stroke.line_join);
+
+ self.stroke_tessellator
+ .tessellate_rectangle(
+ &lyon::math::Box2D::new(top_left, top_left + size),
+ &options,
+ buffer.as_mut(),
+ )
+ .expect("Stroke rectangle");
+ }
+
fn fill_text(&mut self, text: impl Into<geometry::Text>) {
let text = text.into();
@@ -270,7 +329,7 @@ impl geometry::frame::Backend for Frame {
height: f32::INFINITY,
};
- self.text.push(graphics::Text::Cached {
+ self.text.push(Text::Cached {
content: text.content,
bounds,
color: text.color,
@@ -335,10 +394,11 @@ impl geometry::frame::Backend for Frame {
Frame::with_clip(clip_bounds)
}
- fn paste(&mut self, frame: Frame, _at: Point) {
+ fn paste(&mut self, frame: Frame) {
self.meshes
.extend(frame.buffers.into_meshes(frame.clip_bounds));
+ self.images.extend(frame.images);
self.text.extend(frame.text);
}
@@ -348,9 +408,32 @@ impl geometry::frame::Backend for Frame {
Geometry::Live {
meshes: self.meshes,
+ images: self.images,
text: self.text,
}
}
+
+ fn draw_image(&mut self, bounds: Rectangle, image: impl Into<core::Image>) {
+ let mut image = image.into();
+
+ let (bounds, external_rotation) =
+ self.transforms.current.transform_rectangle(bounds);
+
+ image.rotation += external_rotation;
+
+ self.images.push(Image::Raster(image, bounds));
+ }
+
+ fn draw_svg(&mut self, bounds: Rectangle, svg: impl Into<Svg>) {
+ let mut svg = svg.into();
+
+ let (bounds, external_rotation) =
+ self.transforms.current.transform_rectangle(bounds);
+
+ svg.rotation += external_rotation;
+
+ self.images.push(Image::Vector(svg, bounds));
+ }
}
enum Buffer {
@@ -518,6 +601,21 @@ impl Transform {
gradient
}
+
+ fn transform_rectangle(
+ &self,
+ rectangle: Rectangle,
+ ) -> (Rectangle, Radians) {
+ let top_left = self.transform_point(rectangle.position());
+ let top_right = self.transform_point(
+ rectangle.position() + Vector::new(rectangle.width, 0.0),
+ );
+ let bottom_left = self.transform_point(
+ rectangle.position() + Vector::new(0.0, rectangle.height),
+ );
+
+ Rectangle::with_vertices(top_left, top_right, bottom_left)
+ }
}
struct GradientVertex2DBuilder {
gradient: gradient::Packed,
diff --git a/wgpu/src/image/mod.rs b/wgpu/src/image/mod.rs
index daa2fe16..1b16022a 100644
--- a/wgpu/src/image/mod.rs
+++ b/wgpu/src/image/mod.rs
@@ -149,6 +149,8 @@ impl Pipeline {
6 => Float32x2,
// Layer
7 => Sint32,
+ // Snap
+ 8 => Uint32,
),
}],
},
@@ -212,31 +214,24 @@ impl Pipeline {
transformation: Transformation,
scale: f32,
) {
- let transformation = transformation * Transformation::scale(scale);
-
let nearest_instances: &mut Vec<Instance> = &mut Vec::new();
let linear_instances: &mut Vec<Instance> = &mut Vec::new();
for image in images {
match &image {
#[cfg(feature = "image")]
- Image::Raster {
- handle,
- filter_method,
- bounds,
- rotation,
- opacity,
- } => {
+ Image::Raster(image, bounds) => {
if let Some(atlas_entry) =
- cache.upload_raster(device, encoder, handle)
+ cache.upload_raster(device, encoder, &image.handle)
{
add_instances(
[bounds.x, bounds.y],
[bounds.width, bounds.height],
- f32::from(*rotation),
- *opacity,
+ f32::from(image.rotation),
+ image.opacity,
+ image.snap,
atlas_entry,
- match filter_method {
+ match image.filter_method {
crate::core::image::FilterMethod::Nearest => {
nearest_instances
}
@@ -251,23 +246,23 @@ impl Pipeline {
Image::Raster { .. } => {}
#[cfg(feature = "svg")]
- Image::Vector {
- handle,
- color,
- bounds,
- rotation,
- opacity,
- } => {
+ Image::Vector(svg, bounds) => {
let size = [bounds.width, bounds.height];
if let Some(atlas_entry) = cache.upload_vector(
- device, encoder, handle, *color, size, scale,
+ device,
+ encoder,
+ &svg.handle,
+ svg.color,
+ size,
+ scale,
) {
add_instances(
[bounds.x, bounds.y],
size,
- f32::from(*rotation),
- *opacity,
+ f32::from(svg.rotation),
+ svg.opacity,
+ true,
atlas_entry,
nearest_instances,
);
@@ -300,6 +295,7 @@ impl Pipeline {
nearest_instances,
linear_instances,
transformation,
+ scale,
);
self.prepare_layer += 1;
@@ -375,9 +371,12 @@ impl Layer {
nearest_instances: &[Instance],
linear_instances: &[Instance],
transformation: Transformation,
+ scale_factor: f32,
) {
let uniforms = Uniforms {
transform: transformation.into(),
+ scale_factor,
+ _padding: [0.0; 3],
};
let bytes = bytemuck::bytes_of(&uniforms);
@@ -492,6 +491,7 @@ struct Instance {
_position_in_atlas: [f32; 2],
_size_in_atlas: [f32; 2],
_layer: u32,
+ _snap: u32,
}
impl Instance {
@@ -502,6 +502,10 @@ impl Instance {
#[derive(Debug, Clone, Copy, Zeroable, Pod)]
struct Uniforms {
transform: [f32; 16],
+ scale_factor: f32,
+ // Uniforms must be aligned to their largest member,
+ // this uses a mat4x4<f32> which aligns to 16, so align to that
+ _padding: [f32; 3],
}
fn add_instances(
@@ -509,6 +513,7 @@ fn add_instances(
image_size: [f32; 2],
rotation: f32,
opacity: f32,
+ snap: bool,
entry: &atlas::Entry,
instances: &mut Vec<Instance>,
) {
@@ -525,6 +530,7 @@ fn add_instances(
image_size,
rotation,
opacity,
+ snap,
allocation,
instances,
);
@@ -554,8 +560,8 @@ fn add_instances(
];
add_instance(
- position, center, size, rotation, opacity, allocation,
- instances,
+ position, center, size, rotation, opacity, snap,
+ allocation, instances,
);
}
}
@@ -569,6 +575,7 @@ fn add_instance(
size: [f32; 2],
rotation: f32,
opacity: f32,
+ snap: bool,
allocation: &atlas::Allocation,
instances: &mut Vec<Instance>,
) {
@@ -591,6 +598,7 @@ fn add_instance(
(height as f32 - 1.0) / atlas::SIZE as f32,
],
_layer: layer as u32,
+ _snap: snap as u32,
};
instances.push(instance);
diff --git a/wgpu/src/image/vector.rs b/wgpu/src/image/vector.rs
index c6d829af..74e9924d 100644
--- a/wgpu/src/image/vector.rs
+++ b/wgpu/src/image/vector.rs
@@ -1,10 +1,9 @@
use crate::core::svg;
use crate::core::{Color, Size};
-use crate::graphics::text;
use crate::image::atlas::{self, Atlas};
use resvg::tiny_skia;
-use resvg::usvg::{self, TreeTextToPath};
+use resvg::usvg;
use rustc_hash::{FxHashMap, FxHashSet};
use std::fs;
@@ -21,7 +20,7 @@ impl Svg {
pub fn viewport_dimensions(&self) -> Size<u32> {
match self {
Svg::Loaded(tree) => {
- let size = tree.size;
+ let size = tree.size();
Size::new(size.width() as u32, size.height() as u32)
}
@@ -45,38 +44,33 @@ type ColorFilter = Option<[u8; 4]>;
impl Cache {
/// Load svg
pub fn load(&mut self, handle: &svg::Handle) -> &Svg {
- use usvg::TreeParsing;
-
if self.svgs.contains_key(&handle.id()) {
return self.svgs.get(&handle.id()).unwrap();
}
- let mut svg = match handle.data() {
+ let svg = match handle.data() {
svg::Data::Path(path) => fs::read_to_string(path)
.ok()
.and_then(|contents| {
- usvg::Tree::from_str(&contents, &usvg::Options::default())
- .ok()
+ usvg::Tree::from_str(
+ &contents,
+ &usvg::Options::default(), // TODO: Set usvg::Options::fontdb
+ )
+ .ok()
})
.map(Svg::Loaded)
.unwrap_or(Svg::NotFound),
svg::Data::Bytes(bytes) => {
- match usvg::Tree::from_data(bytes, &usvg::Options::default()) {
+ match usvg::Tree::from_data(
+ bytes,
+ &usvg::Options::default(), // TODO: Set usvg::Options::fontdb
+ ) {
Ok(tree) => Svg::Loaded(tree),
Err(_) => Svg::NotFound,
}
}
};
- if let Svg::Loaded(svg) = &mut svg {
- if svg.has_text_nodes() {
- let mut font_system =
- text::font_system().write().expect("Write font system");
-
- svg.convert_text(font_system.raw().db_mut());
- }
- }
-
self.should_trim = true;
let _ = self.svgs.insert(handle.id(), svg);
@@ -127,7 +121,7 @@ impl Cache {
// It would be cool to be able to smooth resize the `svg` example.
let mut img = tiny_skia::Pixmap::new(width, height)?;
- let tree_size = tree.size.to_int_size();
+ let tree_size = tree.size().to_int_size();
let target_size = if width > height {
tree_size.scale_to_width(width)
@@ -147,8 +141,7 @@ impl Cache {
tiny_skia::Transform::default()
};
- resvg::Tree::from_usvg(tree)
- .render(transform, &mut img.as_mut());
+ resvg::render(tree, transform, &mut img.as_mut());
let mut rgba = img.take();
diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs
index 9551311d..68d5a015 100644
--- a/wgpu/src/layer.rs
+++ b/wgpu/src/layer.rs
@@ -1,5 +1,5 @@
use crate::core::{
- renderer, Background, Color, Point, Radians, Rectangle, Transformation,
+ self, renderer, Background, Color, Point, Rectangle, Svg, Transformation,
};
use crate::graphics;
use crate::graphics::color;
@@ -20,8 +20,8 @@ pub struct Layer {
pub quads: quad::Batch,
pub triangles: triangle::Batch,
pub primitives: primitive::Batch,
- pub text: text::Batch,
pub images: image::Batch,
+ pub text: text::Batch,
pending_meshes: Vec<Mesh>,
pending_text: Vec<Text>,
}
@@ -112,42 +112,35 @@ impl Layer {
self.pending_text.push(text);
}
- pub fn draw_image(
+ pub fn draw_image(&mut self, image: Image, transformation: Transformation) {
+ match image {
+ Image::Raster(image, bounds) => {
+ self.draw_raster(image, bounds, transformation);
+ }
+ Image::Vector(svg, bounds) => {
+ self.draw_svg(svg, bounds, transformation);
+ }
+ }
+ }
+
+ pub fn draw_raster(
&mut self,
- handle: crate::core::image::Handle,
- filter_method: crate::core::image::FilterMethod,
+ image: core::Image,
bounds: Rectangle,
transformation: Transformation,
- rotation: Radians,
- opacity: f32,
) {
- let image = Image::Raster {
- handle,
- filter_method,
- bounds: bounds * transformation,
- rotation,
- opacity,
- };
+ let image = Image::Raster(image, bounds * transformation);
self.images.push(image);
}
pub fn draw_svg(
&mut self,
- handle: crate::core::svg::Handle,
- color: Option<Color>,
+ svg: Svg,
bounds: Rectangle,
transformation: Transformation,
- rotation: Radians,
- opacity: f32,
) {
- let svg = Image::Vector {
- handle,
- color,
- bounds: bounds * transformation,
- rotation,
- opacity,
- };
+ let svg = Image::Vector(svg, bounds * transformation);
self.images.push(svg);
}
diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs
index ad88ce3e..d79f0dc8 100644
--- a/wgpu/src/lib.rs
+++ b/wgpu/src/lib.rs
@@ -182,19 +182,6 @@ impl Renderer {
}
}
- if !layer.text.is_empty() {
- engine.text_pipeline.prepare(
- device,
- queue,
- &self.text_viewport,
- encoder,
- &mut self.text_storage,
- &layer.text,
- layer.bounds,
- Transformation::scale(scale_factor),
- );
- }
-
#[cfg(any(feature = "svg", feature = "image"))]
if !layer.images.is_empty() {
engine.image_pipeline.prepare(
@@ -207,6 +194,19 @@ impl Renderer {
scale_factor,
);
}
+
+ if !layer.text.is_empty() {
+ engine.text_pipeline.prepare(
+ device,
+ queue,
+ &self.text_viewport,
+ encoder,
+ &mut self.text_storage,
+ &layer.text,
+ layer.bounds,
+ Transformation::scale(scale_factor),
+ );
+ }
}
}
@@ -359,17 +359,6 @@ impl Renderer {
));
}
- if !layer.text.is_empty() {
- text_layer += engine.text_pipeline.render(
- &self.text_viewport,
- &self.text_storage,
- text_layer,
- &layer.text,
- scissor_rect,
- &mut render_pass,
- );
- }
-
#[cfg(any(feature = "svg", feature = "image"))]
if !layer.images.is_empty() {
engine.image_pipeline.render(
@@ -381,6 +370,17 @@ impl Renderer {
image_layer += 1;
}
+
+ if !layer.text.is_empty() {
+ text_layer += engine.text_pipeline.render(
+ &self.text_viewport,
+ &self.text_storage,
+ text_layer,
+ &layer.text,
+ scissor_rect,
+ &mut render_pass,
+ );
+ }
}
let _ = ManuallyDrop::into_inner(render_pass);
@@ -408,6 +408,7 @@ impl Renderer {
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Top,
shaping: core::text::Shaping::Basic,
+ wrapping: core::text::Wrapping::Word,
};
renderer.fill_text(
@@ -527,23 +528,9 @@ impl core::image::Renderer for Renderer {
self.image_cache.borrow_mut().measure_image(handle)
}
- fn draw_image(
- &mut self,
- handle: Self::Handle,
- filter_method: core::image::FilterMethod,
- bounds: Rectangle,
- rotation: core::Radians,
- opacity: f32,
- ) {
+ fn draw_image(&mut self, image: core::Image, bounds: Rectangle) {
let (layer, transformation) = self.layers.current_mut();
- layer.draw_image(
- handle,
- filter_method,
- bounds,
- transformation,
- rotation,
- opacity,
- );
+ layer.draw_raster(image, bounds, transformation);
}
}
@@ -553,23 +540,9 @@ impl core::svg::Renderer for Renderer {
self.image_cache.borrow_mut().measure_svg(handle)
}
- fn draw_svg(
- &mut self,
- handle: core::svg::Handle,
- color_filter: Option<Color>,
- bounds: Rectangle,
- rotation: core::Radians,
- opacity: f32,
- ) {
+ fn draw_svg(&mut self, svg: core::Svg, bounds: Rectangle) {
let (layer, transformation) = self.layers.current_mut();
- layer.draw_svg(
- handle,
- color_filter,
- bounds,
- transformation,
- rotation,
- opacity,
- );
+ layer.draw_svg(svg, bounds, transformation);
}
}
@@ -593,8 +566,17 @@ impl graphics::geometry::Renderer for Renderer {
let (layer, transformation) = self.layers.current_mut();
match geometry {
- Geometry::Live { meshes, text } => {
+ Geometry::Live {
+ meshes,
+ images,
+ text,
+ } => {
layer.draw_mesh_group(meshes, transformation);
+
+ for image in images {
+ layer.draw_image(image, transformation);
+ }
+
layer.draw_text_group(text, transformation);
}
Geometry::Cached(cache) => {
@@ -602,6 +584,12 @@ impl graphics::geometry::Renderer for Renderer {
layer.draw_mesh_cache(meshes, transformation);
}
+ if let Some(images) = cache.images {
+ for image in images.iter().cloned() {
+ layer.draw_image(image, transformation);
+ }
+ }
+
if let Some(text) = cache.text {
layer.draw_text_cache(text, transformation);
}
diff --git a/wgpu/src/shader/image.wgsl b/wgpu/src/shader/image.wgsl
index 0eeb100f..bc922838 100644
--- a/wgpu/src/shader/image.wgsl
+++ b/wgpu/src/shader/image.wgsl
@@ -1,5 +1,6 @@
struct Globals {
transform: mat4x4<f32>,
+ scale_factor: f32,
}
@group(0) @binding(0) var<uniform> globals: Globals;
@@ -16,6 +17,7 @@ struct VertexInput {
@location(5) atlas_pos: vec2<f32>,
@location(6) atlas_scale: vec2<f32>,
@location(7) layer: i32,
+ @location(8) snap: u32,
}
struct VertexOutput {
@@ -38,7 +40,7 @@ fn vs_main(input: VertexInput) -> VertexOutput {
out.opacity = input.opacity;
// Calculate the vertex position and move the center to the origin
- v_pos = round(input.pos) + v_pos * input.scale - input.center;
+ v_pos = input.pos + v_pos * input.scale - input.center;
// Apply the rotation around the center of the image
let cos_rot = cos(input.rotation);
@@ -51,7 +53,13 @@ fn vs_main(input: VertexInput) -> VertexOutput {
);
// Calculate the final position of the vertex
- out.position = globals.transform * (vec4<f32>(input.center, 0.0, 0.0) + rotate * vec4<f32>(v_pos, 0.0, 1.0));
+ out.position = vec4(vec2(globals.scale_factor), 1.0, 1.0) * (vec4<f32>(input.center, 0.0, 0.0) + rotate * vec4<f32>(v_pos, 0.0, 1.0));
+
+ if bool(input.snap) {
+ out.position = round(out.position);
+ }
+
+ out.position = globals.transform * out.position;
return out;
}
diff --git a/wgpu/src/shader/quad/solid.wgsl b/wgpu/src/shader/quad/solid.wgsl
index d908afbc..8eee16bb 100644
--- a/wgpu/src/shader/quad/solid.wgsl
+++ b/wgpu/src/shader/quad/solid.wgsl
@@ -30,6 +30,15 @@ fn solid_vs_main(input: SolidVertexInput) -> SolidVertexOutput {
var pos: vec2<f32> = (input.pos + min(input.shadow_offset, vec2<f32>(0.0, 0.0)) - input.shadow_blur_radius) * globals.scale;
var scale: vec2<f32> = (input.scale + vec2<f32>(abs(input.shadow_offset.x), abs(input.shadow_offset.y)) + input.shadow_blur_radius * 2.0) * globals.scale;
+ var snap: vec2<f32> = vec2<f32>(0.0, 0.0);
+
+ if input.scale.x == 1.0 {
+ snap.x = round(pos.x) - pos.x;
+ }
+
+ if input.scale.y == 1.0 {
+ snap.y = round(pos.y) - pos.y;
+ }
var min_border_radius = min(input.scale.x, input.scale.y) * 0.5;
var border_radius: vec4<f32> = vec4<f32>(
@@ -43,13 +52,13 @@ fn solid_vs_main(input: SolidVertexInput) -> SolidVertexOutput {
vec4<f32>(scale.x + 1.0, 0.0, 0.0, 0.0),
vec4<f32>(0.0, scale.y + 1.0, 0.0, 0.0),
vec4<f32>(0.0, 0.0, 1.0, 0.0),
- vec4<f32>(pos - vec2<f32>(0.5, 0.5), 0.0, 1.0)
+ vec4<f32>(pos - vec2<f32>(0.5, 0.5) + snap, 0.0, 1.0)
);
out.position = globals.transform * transform * vec4<f32>(vertex_position(input.vertex_index), 0.0, 1.0);
out.color = input.color;
out.border_color = input.border_color;
- out.pos = input.pos * globals.scale;
+ out.pos = input.pos * globals.scale + snap;
out.scale = input.scale * globals.scale;
out.border_radius = border_radius * globals.scale;
out.border_width = input.border_width * globals.scale;
diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs
index 05db5f80..bf7eae18 100644
--- a/wgpu/src/text.rs
+++ b/wgpu/src/text.rs
@@ -585,7 +585,13 @@ fn prepare(
(
buffer.as_ref(),
- Rectangle::new(raw.position, Size::new(width, height)),
+ Rectangle::new(
+ raw.position,
+ Size::new(
+ width.unwrap_or(layer_bounds.width),
+ height.unwrap_or(layer_bounds.height),
+ ),
+ ),
alignment::Horizontal::Left,
alignment::Vertical::Top,
raw.color,
diff --git a/widget/Cargo.toml b/widget/Cargo.toml
index 3c9f6a54..98a81145 100644
--- a/widget/Cargo.toml
+++ b/widget/Cargo.toml
@@ -22,8 +22,10 @@ lazy = ["ouroboros"]
image = ["iced_renderer/image"]
svg = ["iced_renderer/svg"]
canvas = ["iced_renderer/geometry"]
-qr_code = ["canvas", "qrcode"]
+qr_code = ["canvas", "dep:qrcode"]
wgpu = ["iced_renderer/wgpu"]
+markdown = ["dep:pulldown-cmark", "dep:url"]
+highlighter = ["dep:iced_highlighter"]
advanced = []
[dependencies]
@@ -31,6 +33,7 @@ iced_renderer.workspace = true
iced_runtime.workspace = true
num-traits.workspace = true
+once_cell.workspace = true
rustc-hash.workspace = true
thiserror.workspace = true
unicode-segmentation.workspace = true
@@ -40,3 +43,12 @@ ouroboros.optional = true
qrcode.workspace = true
qrcode.optional = true
+
+pulldown-cmark.workspace = true
+pulldown-cmark.optional = true
+
+iced_highlighter.workspace = true
+iced_highlighter.optional = true
+
+url.workspace = true
+url.optional = true
diff --git a/widget/assets/iced-logo.svg b/widget/assets/iced-logo.svg
new file mode 100644
index 00000000..459b7fbb
--- /dev/null
+++ b/widget/assets/iced-logo.svg
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="140" height="140" fill="none" version="1.1" viewBox="35 31 179 171"><rect x="42" y="31.001" width="169.9" height="169.9" rx="49.815" fill="url(#paint1_linear)"/><path d="m182.62 65.747-28.136 28.606-6.13-6.0291 28.136-28.606 6.13 6.0291zm-26.344 0.218-42.204 42.909-6.13-6.029 42.204-42.909 6.13 6.0291zm-61.648 23.913c5.3254-5.3831 10.65-10.765 21.569-21.867l6.13 6.0291c-10.927 11.11-16.258 16.498-21.587 21.885-4.4007 4.4488-8.8009 8.8968-16.359 16.573l31.977 8.358 25.968-26.402 6.13 6.0292-25.968 26.402 8.907 31.908 42.138-42.087 6.076 6.083-49.109 49.05-45.837-12.628-13.394-45.646 1.7714-1.801c10.928-11.111 16.258-16.499 21.588-21.886zm28.419 70.99-8.846-31.689-31.831-8.32 9.1945 31.335 31.482 8.674zm47.734-56.517 7.122-7.1221-6.08-6.0797-7.147 7.1474-30.171 30.674 6.13 6.029 30.146-30.649z" clip-rule="evenodd" fill="url(#paint2_linear)" fill-rule="evenodd"/><defs><filter id="filter0_f" x="55" y="47.001" width="144" height="168" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur" stdDeviation="2"/></filter><linearGradient id="paint0_linear" x1="127" x2="127" y1="51.001" y2="211" gradientUnits="userSpaceOnUse"><stop offset=".052083"/><stop stop-opacity=".08" offset="1"/></linearGradient><linearGradient id="paint1_linear" x1="212" x2="57.5" y1="31.001" y2="189" gradientUnits="userSpaceOnUse"><stop stop-color="#00A3FF" offset="0"/><stop stop-color="#30f" offset="1"/></linearGradient><linearGradient id="paint2_linear" x1="86.098" x2="206.01" y1="158.28" y2="35.327" gradientUnits="userSpaceOnUse"><stop stop-color="#fff" offset="0"/><stop stop-color="#fff" offset="1"/></linearGradient></defs></svg>
diff --git a/widget/src/button.rs b/widget/src/button.rs
index 5d446fea..eafa71b9 100644
--- a/widget/src/button.rs
+++ b/widget/src/button.rs
@@ -1,4 +1,5 @@
//! Allow your users to perform actions by pressing a button.
+use crate::core::border::{self, Border};
use crate::core::event::{self, Event};
use crate::core::layout;
use crate::core::mouse;
@@ -9,8 +10,8 @@ use crate::core::touch;
use crate::core::widget::tree::{self, Tree};
use crate::core::widget::Operation;
use crate::core::{
- Background, Border, Clipboard, Color, Element, Layout, Length, Padding,
- Rectangle, Shadow, Shell, Size, Theme, Vector, Widget,
+ Background, Clipboard, Color, Element, Layout, Length, Padding, Rectangle,
+ Shadow, Shell, Size, Theme, Vector, Widget,
};
/// A generic widget that produces a message when pressed.
@@ -52,7 +53,7 @@ where
Theme: Catalog,
{
content: Element<'a, Message, Theme, Renderer>,
- on_press: Option<Message>,
+ on_press: Option<OnPress<'a, Message>>,
width: Length,
height: Length,
padding: Padding,
@@ -60,6 +61,20 @@ where
class: Theme::Class<'a>,
}
+enum OnPress<'a, Message> {
+ Direct(Message),
+ Closure(Box<dyn Fn() -> Message + 'a>),
+}
+
+impl<'a, Message: Clone> OnPress<'a, Message> {
+ fn get(&self) -> Message {
+ match self {
+ OnPress::Direct(message) => message.clone(),
+ OnPress::Closure(f) => f(),
+ }
+ }
+}
+
impl<'a, Message, Theme, Renderer> Button<'a, Message, Theme, Renderer>
where
Renderer: crate::core::Renderer,
@@ -105,7 +120,23 @@ where
///
/// Unless `on_press` is called, the [`Button`] will be disabled.
pub fn on_press(mut self, on_press: Message) -> Self {
- self.on_press = Some(on_press);
+ self.on_press = Some(OnPress::Direct(on_press));
+ self
+ }
+
+ /// Sets the message that will be produced when the [`Button`] is pressed.
+ ///
+ /// This is analogous to [`Button::on_press`], but using a closure to produce
+ /// the message.
+ ///
+ /// This closure will only be called when the [`Button`] is actually pressed and,
+ /// therefore, this method is useful to reduce overhead if creating the resulting
+ /// message is slow.
+ pub fn on_press_with(
+ mut self,
+ on_press: impl Fn() -> Message + 'a,
+ ) -> Self {
+ self.on_press = Some(OnPress::Closure(Box::new(on_press)));
self
}
@@ -114,7 +145,7 @@ where
///
/// If `None`, the [`Button`] will be disabled.
pub fn on_press_maybe(mut self, on_press: Option<Message>) -> Self {
- self.on_press = on_press;
+ self.on_press = on_press.map(OnPress::Direct);
self
}
@@ -205,7 +236,7 @@ where
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
- operation: &mut dyn Operation<()>,
+ operation: &mut dyn Operation,
) {
operation.container(None, layout.bounds(), &mut |operation| {
self.content.as_widget().operate(
@@ -258,7 +289,8 @@ where
}
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
| Event::Touch(touch::Event::FingerLifted { .. }) => {
- if let Some(on_press) = self.on_press.clone() {
+ if let Some(on_press) = self.on_press.as_ref().map(OnPress::get)
+ {
let state = tree.state.downcast_mut::<State>();
if state.is_pressed {
@@ -560,7 +592,7 @@ fn styled(pair: palette::Pair) -> Style {
Style {
background: Some(Background::Color(pair.color)),
text_color: pair.text,
- border: Border::rounded(2),
+ border: border::rounded(2),
..Style::default()
}
}
diff --git a/widget/src/canvas.rs b/widget/src/canvas.rs
index 73cef087..185fa082 100644
--- a/widget/src/canvas.rs
+++ b/widget/src/canvas.rs
@@ -8,8 +8,8 @@ pub use program::Program;
pub use crate::graphics::cache::Group;
pub use crate::graphics::geometry::{
- fill, gradient, path, stroke, Fill, Gradient, LineCap, LineDash, LineJoin,
- Path, Stroke, Style, Text,
+ fill, gradient, path, stroke, Fill, Gradient, Image, LineCap, LineDash,
+ LineJoin, Path, Stroke, Style, Text,
};
use crate::core;
diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs
index 225c316d..32db5090 100644
--- a/widget/src/checkbox.rs
+++ b/widget/src/checkbox.rs
@@ -50,6 +50,7 @@ pub struct Checkbox<
text_size: Option<Pixels>,
text_line_height: text::LineHeight,
text_shaping: text::Shaping,
+ text_wrapping: text::Wrapping,
font: Option<Renderer::Font>,
icon: Icon<Renderer::Font>,
class: Theme::Class<'a>,
@@ -81,7 +82,8 @@ where
spacing: Self::DEFAULT_SPACING,
text_size: None,
text_line_height: text::LineHeight::default(),
- text_shaping: text::Shaping::Basic,
+ text_shaping: text::Shaping::default(),
+ text_wrapping: text::Wrapping::default(),
font: None,
icon: Icon {
font: Renderer::ICON_FONT,
@@ -158,6 +160,12 @@ where
self
}
+ /// Sets the [`text::Wrapping`] strategy of the [`Checkbox`].
+ pub fn text_wrapping(mut self, wrapping: text::Wrapping) -> Self {
+ self.text_wrapping = wrapping;
+ self
+ }
+
/// Sets the [`Renderer::Font`] of the text of the [`Checkbox`].
///
/// [`Renderer::Font`]: crate::core::text::Renderer
@@ -240,6 +248,7 @@ where
alignment::Horizontal::Left,
alignment::Vertical::Top,
self.text_shaping,
+ self.text_wrapping,
)
},
)
@@ -348,6 +357,7 @@ where
horizontal_alignment: alignment::Horizontal::Center,
vertical_alignment: alignment::Vertical::Center,
shaping: *shaping,
+ wrapping: text::Wrapping::default(),
},
bounds.center(),
style.icon_color,
@@ -358,12 +368,14 @@ where
{
let label_layout = children.next().unwrap();
+ let state: &widget::text::State<Renderer::Paragraph> =
+ tree.state.downcast_ref();
crate::text::draw(
renderer,
defaults,
label_layout,
- tree.state.downcast_ref(),
+ state.0.raw(),
crate::text::Style {
color: style.text_color,
},
diff --git a/widget/src/column.rs b/widget/src/column.rs
index 0b81c545..d3ea4cf7 100644
--- a/widget/src/column.rs
+++ b/widget/src/column.rs
@@ -1,4 +1,5 @@
//! Distribute content vertically.
+use crate::core::alignment::{self, Alignment};
use crate::core::event::{self, Event};
use crate::core::layout;
use crate::core::mouse;
@@ -6,8 +7,8 @@ use crate::core::overlay;
use crate::core::renderer;
use crate::core::widget::{Operation, Tree};
use crate::core::{
- Alignment, Clipboard, Element, Layout, Length, Padding, Pixels, Rectangle,
- Shell, Size, Vector, Widget,
+ Clipboard, Element, Layout, Length, Padding, Pixels, Rectangle, Shell,
+ Size, Vector, Widget,
};
/// A container that distributes its contents vertically.
@@ -19,7 +20,7 @@ pub struct Column<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
width: Length,
height: Length,
max_width: f32,
- align_items: Alignment,
+ align: Alignment,
clip: bool,
children: Vec<Element<'a, Message, Theme, Renderer>>,
}
@@ -63,7 +64,7 @@ where
width: Length::Shrink,
height: Length::Shrink,
max_width: f32::INFINITY,
- align_items: Alignment::Start,
+ align: Alignment::Start,
clip: false,
children,
}
@@ -104,8 +105,8 @@ where
}
/// Sets the horizontal alignment of the contents of the [`Column`] .
- pub fn align_items(mut self, align: Alignment) -> Self {
- self.align_items = align;
+ pub fn align_x(mut self, align: impl Into<alignment::Horizontal>) -> Self {
+ self.align = Alignment::from(align.into());
self
}
@@ -210,7 +211,7 @@ where
self.height,
self.padding,
self.spacing,
- self.align_items,
+ self.align,
&self.children,
&mut tree.children,
)
@@ -221,7 +222,7 @@ where
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
- operation: &mut dyn Operation<()>,
+ operation: &mut dyn Operation,
) {
operation.container(None, layout.bounds(), &mut |operation| {
self.children
diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs
index 253850df..62785b2c 100644
--- a/widget/src/combo_box.rs
+++ b/widget/src/combo_box.rs
@@ -208,12 +208,14 @@ where
/// The local state of a [`ComboBox`].
#[derive(Debug, Clone)]
-pub struct State<T>(RefCell<Inner<T>>);
+pub struct State<T> {
+ options: Vec<T>,
+ inner: RefCell<Inner<T>>,
+}
#[derive(Debug, Clone)]
struct Inner<T> {
value: String,
- options: Vec<T>,
option_matchers: Vec<String>,
filtered_options: Filtered<T>,
}
@@ -247,39 +249,58 @@ where
.collect(),
);
- Self(RefCell::new(Inner {
- value,
+ Self {
options,
- option_matchers,
- filtered_options,
- }))
+ inner: RefCell::new(Inner {
+ value,
+ option_matchers,
+ filtered_options,
+ }),
+ }
+ }
+
+ /// Returns the options of the [`State`].
+ ///
+ /// These are the options provided when the [`State`]
+ /// was constructed with [`State::new`].
+ pub fn options(&self) -> &[T] {
+ &self.options
}
fn value(&self) -> String {
- let inner = self.0.borrow();
+ let inner = self.inner.borrow();
inner.value.clone()
}
fn with_inner<O>(&self, f: impl FnOnce(&Inner<T>) -> O) -> O {
- let inner = self.0.borrow();
+ let inner = self.inner.borrow();
f(&inner)
}
fn with_inner_mut(&self, f: impl FnOnce(&mut Inner<T>)) {
- let mut inner = self.0.borrow_mut();
+ let mut inner = self.inner.borrow_mut();
f(&mut inner);
}
fn sync_filtered_options(&self, options: &mut Filtered<T>) {
- let inner = self.0.borrow();
+ let inner = self.inner.borrow();
inner.filtered_options.sync(options);
}
}
+impl<T> Default for State<T>
+where
+ T: Display + Clone,
+{
+ fn default() -> Self {
+ Self::new(Vec::new())
+ }
+}
+
impl<T> Filtered<T>
where
T: Clone,
@@ -431,7 +452,7 @@ where
state.filtered_options.update(
search(
- &state.options,
+ &self.state.options,
&state.option_matchers,
&state.value,
)
@@ -580,7 +601,7 @@ where
if let Some(selection) = menu.new_selection.take() {
// Clear the value and reset the options and menu
state.value = String::new();
- state.filtered_options.update(state.options.clone());
+ state.filtered_options.update(self.state.options.clone());
menu.menu = menu::State::default();
// Notify the selection
diff --git a/widget/src/container.rs b/widget/src/container.rs
index e917471f..3b794099 100644
--- a/widget/src/container.rs
+++ b/widget/src/container.rs
@@ -1,5 +1,6 @@
//! Decorate content and apply alignment.
use crate::core::alignment::{self, Alignment};
+use crate::core::border::{self, Border};
use crate::core::event::{self, Event};
use crate::core::gradient::{self, Gradient};
use crate::core::layout;
@@ -9,11 +10,11 @@ use crate::core::renderer;
use crate::core::widget::tree::{self, Tree};
use crate::core::widget::{self, Operation};
use crate::core::{
- self, Background, Border, Clipboard, Color, Element, Layout, Length,
+ self, color, Background, Clipboard, Color, Element, Layout, Length,
Padding, Pixels, Point, Rectangle, Shadow, Shell, Size, Theme, Vector,
Widget,
};
-use crate::runtime::Task;
+use crate::runtime::task::{self, Task};
/// An element decorating some content.
///
@@ -92,46 +93,6 @@ where
self
}
- /// Sets the [`Container`] to fill the available space in the horizontal axis.
- ///
- /// This can be useful to quickly position content when chained with
- /// alignment functions—like [`center_x`].
- ///
- /// Calling this method is equivalent to calling [`width`] with a
- /// [`Length::Fill`].
- ///
- /// [`center_x`]: Self::center_x
- /// [`width`]: Self::width
- pub fn fill_x(self) -> Self {
- self.width(Length::Fill)
- }
-
- /// Sets the [`Container`] to fill the available space in the vetical axis.
- ///
- /// This can be useful to quickly position content when chained with
- /// alignment functions—like [`center_y`].
- ///
- /// Calling this method is equivalent to calling [`height`] with a
- /// [`Length::Fill`].
- ///
- /// [`center_y`]: Self::center_x
- /// [`height`]: Self::height
- pub fn fill_y(self) -> Self {
- self.height(Length::Fill)
- }
-
- /// Sets the [`Container`] to fill all the available space.
- ///
- /// Calling this method is equivalent to chaining [`fill_x`] and
- /// [`fill_y`].
- ///
- /// [`center`]: Self::center
- /// [`fill_x`]: Self::fill_x
- /// [`fill_y`]: Self::fill_y
- pub fn fill(self) -> Self {
- self.width(Length::Fill).height(Length::Fill)
- }
-
/// Sets the maximum width of the [`Container`].
pub fn max_width(mut self, max_width: impl Into<Pixels>) -> Self {
self.max_width = max_width.into().0;
@@ -144,18 +105,6 @@ where
self
}
- /// Sets the content alignment for the horizontal axis of the [`Container`].
- pub fn align_x(mut self, alignment: alignment::Horizontal) -> Self {
- self.horizontal_alignment = alignment;
- self
- }
-
- /// Sets the content alignment for the vertical axis of the [`Container`].
- pub fn align_y(mut self, alignment: alignment::Vertical) -> Self {
- self.vertical_alignment = alignment;
- self
- }
-
/// Sets the width of the [`Container`] and centers its contents horizontally.
pub fn center_x(self, width: impl Into<Length>) -> Self {
self.width(width).align_x(alignment::Horizontal::Center)
@@ -179,6 +128,44 @@ where
self.center_x(length).center_y(length)
}
+ /// Aligns the contents of the [`Container`] to the left.
+ pub fn align_left(self, width: impl Into<Length>) -> Self {
+ self.width(width).align_x(alignment::Horizontal::Left)
+ }
+
+ /// Aligns the contents of the [`Container`] to the right.
+ pub fn align_right(self, width: impl Into<Length>) -> Self {
+ self.width(width).align_x(alignment::Horizontal::Right)
+ }
+
+ /// Aligns the contents of the [`Container`] to the top.
+ pub fn align_top(self, height: impl Into<Length>) -> Self {
+ self.height(height).align_y(alignment::Vertical::Top)
+ }
+
+ /// Aligns the contents of the [`Container`] to the bottom.
+ pub fn align_bottom(self, height: impl Into<Length>) -> Self {
+ self.height(height).align_y(alignment::Vertical::Bottom)
+ }
+
+ /// Sets the content alignment for the horizontal axis of the [`Container`].
+ pub fn align_x(
+ mut self,
+ alignment: impl Into<alignment::Horizontal>,
+ ) -> Self {
+ self.horizontal_alignment = alignment.into();
+ self
+ }
+
+ /// Sets the content alignment for the vertical axis of the [`Container`].
+ pub fn align_y(
+ mut self,
+ alignment: impl Into<alignment::Vertical>,
+ ) -> Self {
+ self.vertical_alignment = alignment.into();
+ self
+ }
+
/// Sets whether the contents of the [`Container`] should be clipped on
/// overflow.
pub fn clip(mut self, clip: bool) -> Self {
@@ -197,7 +184,6 @@ where
}
/// Sets the style class of the [`Container`].
- #[cfg(feature = "advanced")]
#[must_use]
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
self.class = class.into();
@@ -258,7 +244,7 @@ where
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
- operation: &mut dyn Operation<()>,
+ operation: &mut dyn Operation,
) {
operation.container(
self.id.as_ref().map(|id| &id.0),
@@ -473,6 +459,7 @@ pub fn visible_bounds(id: Id) -> Task<Option<Rectangle>> {
_state: &mut dyn widget::operation::Scrollable,
_id: Option<&widget::Id>,
bounds: Rectangle,
+ _content_bounds: Rectangle,
translation: Vector,
) {
match self.scrollables.last() {
@@ -538,7 +525,7 @@ pub fn visible_bounds(id: Id) -> Task<Option<Rectangle>> {
}
}
- Task::widget(VisibleBounds {
+ task::widget(VisibleBounds {
target: id.into(),
depth: 0,
scrollables: Vec::new(),
@@ -560,46 +547,54 @@ pub struct Style {
}
impl Style {
- /// Updates the border of the [`Style`] with the given [`Color`] and `width`.
- pub fn with_border(
- self,
- color: impl Into<Color>,
- width: impl Into<Pixels>,
- ) -> Self {
+ /// Updates the text color of the [`Style`].
+ pub fn color(self, color: impl Into<Color>) -> Self {
Self {
- border: Border {
- color: color.into(),
- width: width.into().0,
- ..Border::default()
- },
+ text_color: Some(color.into()),
+ ..self
+ }
+ }
+
+ /// Updates the border of the [`Style`].
+ pub fn border(self, border: impl Into<Border>) -> Self {
+ Self {
+ border: border.into(),
..self
}
}
/// Updates the background of the [`Style`].
- pub fn with_background(self, background: impl Into<Background>) -> Self {
+ pub fn background(self, background: impl Into<Background>) -> Self {
Self {
background: Some(background.into()),
..self
}
}
+
+ /// Updates the shadow of the [`Style`].
+ pub fn shadow(self, shadow: impl Into<Shadow>) -> Self {
+ Self {
+ shadow: shadow.into(),
+ ..self
+ }
+ }
}
impl From<Color> for Style {
fn from(color: Color) -> Self {
- Self::default().with_background(color)
+ Self::default().background(color)
}
}
impl From<Gradient> for Style {
fn from(gradient: Gradient) -> Self {
- Self::default().with_background(gradient)
+ Self::default().background(gradient)
}
}
impl From<gradient::Linear> for Style {
fn from(gradient: gradient::Linear) -> Self {
- Self::default().with_background(gradient)
+ Self::default().background(gradient)
}
}
@@ -618,6 +613,12 @@ pub trait Catalog {
/// A styling function for a [`Container`].
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
+impl<'a, Theme> From<Style> for StyleFn<'a, Theme> {
+ fn from(style: Style) -> Self {
+ Box::new(move |_theme| style)
+ }
+}
+
impl Catalog for Theme {
type Class<'a> = StyleFn<'a, Self>;
@@ -635,13 +636,18 @@ pub fn transparent<Theme>(_theme: &Theme) -> Style {
Style::default()
}
+/// A [`Container`] with the given [`Background`].
+pub fn background(background: impl Into<Background>) -> Style {
+ Style::default().background(background)
+}
+
/// A rounded [`Container`] with a background.
pub fn rounded_box(theme: &Theme) -> Style {
let palette = theme.extended_palette();
Style {
background: Some(palette.background.weak.color.into()),
- border: Border::rounded(2),
+ border: border::rounded(2),
..Style::default()
}
}
@@ -660,3 +666,13 @@ pub fn bordered_box(theme: &Theme) -> Style {
..Style::default()
}
}
+
+/// A [`Container`] with a dark background and white text.
+pub fn dark(_theme: &Theme) -> Style {
+ Style {
+ background: Some(color!(0x111111).into()),
+ text_color: Some(Color::WHITE),
+ border: border::rounded(2),
+ ..Style::default()
+ }
+}
diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs
index 62343a55..51978823 100644
--- a/widget/src/helpers.rs
+++ b/widget/src/helpers.rs
@@ -4,7 +4,8 @@ use crate::checkbox::{self, Checkbox};
use crate::combo_box::{self, ComboBox};
use crate::container::{self, Container};
use crate::core;
-use crate::core::widget::operation;
+use crate::core::widget::operation::{self, Operation};
+use crate::core::window;
use crate::core::{Element, Length, Pixels, Widget};
use crate::keyed;
use crate::overlay;
@@ -12,7 +13,8 @@ use crate::pick_list::{self, PickList};
use crate::progress_bar::{self, ProgressBar};
use crate::radio::{self, Radio};
use crate::rule::{self, Rule};
-use crate::runtime::{Action, Task};
+use crate::runtime::task::{self, Task};
+use crate::runtime::Action;
use crate::scrollable::{self, Scrollable};
use crate::slider::{self, Slider};
use crate::text::{self, Text};
@@ -111,6 +113,19 @@ macro_rules! text {
};
}
+/// Creates some [`Rich`] text with the given spans.
+///
+/// [`Rich`]: text::Rich
+#[macro_export]
+macro_rules! rich_text {
+ () => (
+ $crate::Column::new()
+ );
+ ($($x:expr),+ $(,)?) => (
+ $crate::text::Rich::from_iter([$($crate::text::Span::from($x)),+])
+ );
+}
+
/// Creates a new [`Container`] with the provided content.
///
/// [`Container`]: crate::Container
@@ -275,7 +290,7 @@ where
state: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
- operation: &mut dyn operation::Operation<()>,
+ operation: &mut dyn operation::Operation,
) {
self.content
.as_widget()
@@ -383,6 +398,7 @@ where
struct Hover<'a, Message, Theme, Renderer> {
base: Element<'a, Message, Theme, Renderer>,
top: Element<'a, Message, Theme, Renderer>,
+ is_top_focused: bool,
is_top_overlay_active: bool,
}
@@ -458,7 +474,9 @@ where
viewport,
);
- if cursor.is_over(layout.bounds()) || self.is_top_overlay_active
+ if cursor.is_over(layout.bounds())
+ || self.is_top_focused
+ || self.is_top_overlay_active
{
let (top_layout, top_tree) = children.next().unwrap();
@@ -477,7 +495,7 @@ where
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
- operation: &mut dyn operation::Operation<()>,
+ operation: &mut dyn operation::Operation,
) {
let children = [&self.base, &self.top]
.into_iter()
@@ -501,6 +519,24 @@ where
) -> event::Status {
let mut children = layout.children().zip(&mut tree.children);
let (base_layout, base_tree) = children.next().unwrap();
+ let (top_layout, top_tree) = children.next().unwrap();
+
+ if matches!(event, Event::Window(window::Event::RedrawRequested(_)))
+ {
+ let mut count_focused = operation::focusable::count();
+
+ self.top.as_widget_mut().operate(
+ top_tree,
+ top_layout,
+ renderer,
+ &mut operation::black_box(&mut count_focused),
+ );
+
+ self.is_top_focused = match count_focused.finish() {
+ operation::Outcome::Some(count) => count.focused.is_some(),
+ _ => false,
+ };
+ }
let top_status = if matches!(
event,
@@ -509,9 +545,9 @@ where
| mouse::Event::ButtonReleased(_)
)
) || cursor.is_over(layout.bounds())
+ || self.is_top_focused
+ || self.is_top_overlay_active
{
- let (top_layout, top_tree) = children.next().unwrap();
-
self.top.as_widget_mut().on_event(
top_tree,
event.clone(),
@@ -597,6 +633,7 @@ where
Element::new(Hover {
base: base.into(),
top: top.into(),
+ is_top_focused: false,
is_top_overlay_active: false,
})
}
@@ -645,8 +682,6 @@ where
}
/// Creates a new [`Text`] widget with the provided content.
-///
-/// [`Text`]: core::widget::Text
pub fn text<'a, Theme, Renderer>(
text: impl text::IntoFragment<'a>,
) -> Text<'a, Theme, Renderer>
@@ -658,8 +693,6 @@ where
}
/// Creates a new [`Text`] widget that displays the provided value.
-///
-/// [`Text`]: core::widget::Text
pub fn value<'a, Theme, Renderer>(
value: impl ToString,
) -> Text<'a, Theme, Renderer>
@@ -670,6 +703,34 @@ where
Text::new(value.to_string())
}
+/// Creates a new [`Rich`] text widget with the provided spans.
+///
+/// [`Rich`]: text::Rich
+pub fn rich_text<'a, Link, Theme, Renderer>(
+ spans: impl AsRef<[text::Span<'a, Link, Renderer::Font>]> + 'a,
+) -> text::Rich<'a, Link, Theme, Renderer>
+where
+ Link: Clone + 'static,
+ Theme: text::Catalog + 'a,
+ Renderer: core::text::Renderer,
+ Renderer::Font: 'a,
+{
+ text::Rich::with_spans(spans)
+}
+
+/// Creates a new [`Span`] of text with the provided content.
+///
+/// [`Span`]: text::Span
+pub fn span<'a, Link, Font>(
+ text: impl text::IntoFragment<'a>,
+) -> text::Span<'a, Link, Font> {
+ text::Span::new(text)
+}
+
+#[cfg(feature = "markdown")]
+#[doc(inline)]
+pub use crate::markdown::view as markdown;
+
/// Creates a new [`Checkbox`].
///
/// [`Checkbox`]: crate::Checkbox
@@ -706,15 +767,13 @@ where
///
/// [`Toggler`]: crate::Toggler
pub fn toggler<'a, Message, Theme, Renderer>(
- label: impl Into<Option<String>>,
is_checked: bool,
- f: impl Fn(bool) -> Message + 'a,
) -> Toggler<'a, Message, Theme, Renderer>
where
Theme: toggler::Catalog + 'a,
Renderer: core::text::Renderer,
{
- Toggler::new(label, is_checked, f)
+ Toggler::new(is_checked)
}
/// Creates a new [`TextInput`].
@@ -889,6 +948,41 @@ where
crate::Svg::new(handle)
}
+/// Creates an [`Element`] that displays the iced logo with the given `text_size`.
+///
+/// Useful for showing some love to your favorite GUI library in your "About" screen,
+/// for instance.
+#[cfg(feature = "svg")]
+pub fn iced<'a, Message, Theme, Renderer>(
+ text_size: impl Into<Pixels>,
+) -> Element<'a, Message, Theme, Renderer>
+where
+ Message: 'a,
+ Renderer: core::Renderer
+ + core::text::Renderer<Font = core::Font>
+ + core::svg::Renderer
+ + 'a,
+ Theme: text::Catalog + crate::svg::Catalog + 'a,
+{
+ use crate::core::{Alignment, Font};
+ use crate::svg;
+ use once_cell::sync::Lazy;
+
+ static LOGO: Lazy<svg::Handle> = Lazy::new(|| {
+ svg::Handle::from_memory(include_bytes!("../assets/iced-logo.svg"))
+ });
+
+ let text_size = text_size.into();
+
+ row![
+ svg(LOGO.clone()).width(text_size * 1.3),
+ text("iced").size(text_size).font(Font::MONOSPACE)
+ ]
+ .spacing(text_size.0 / 3.0)
+ .align_y(Alignment::Center)
+ .into()
+}
+
/// Creates a new [`Canvas`].
///
/// [`Canvas`]: crate::Canvas
@@ -930,12 +1024,12 @@ where
/// Focuses the previous focusable widget.
pub fn focus_previous<T>() -> Task<T> {
- Task::effect(Action::widget(operation::focusable::focus_previous()))
+ task::effect(Action::widget(operation::focusable::focus_previous()))
}
/// Focuses the next focusable widget.
pub fn focus_next<T>() -> Task<T> {
- Task::effect(Action::widget(operation::focusable::focus_next()))
+ task::effect(Action::widget(operation::focusable::focus_next()))
}
/// A container intercepting mouse events.
diff --git a/widget/src/image.rs b/widget/src/image.rs
index 80e17263..e04f2d6f 100644
--- a/widget/src/image.rs
+++ b/widget/src/image.rs
@@ -43,7 +43,7 @@ pub struct Image<Handle> {
impl<Handle> Image<Handle> {
/// Creates a new [`Image`] with the given path.
- pub fn new<T: Into<Handle>>(handle: T) -> Self {
+ pub fn new(handle: impl Into<Handle>) -> Self {
Image {
handle: handle.into(),
width: Length::Shrink,
@@ -181,11 +181,14 @@ pub fn draw<Renderer, Handle>(
let render = |renderer: &mut Renderer| {
renderer.draw_image(
- handle.clone(),
- filter_method,
+ image::Image {
+ handle: handle.clone(),
+ filter_method,
+ rotation: rotation.radians(),
+ opacity,
+ snap: true,
+ },
drawing_bounds,
- rotation.radians(),
- opacity,
);
};
diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs
index b8b69b60..b1aad22c 100644
--- a/widget/src/image/viewer.rs
+++ b/widget/src/image/viewer.rs
@@ -6,8 +6,8 @@ use crate::core::mouse;
use crate::core::renderer;
use crate::core::widget::tree::{self, Tree};
use crate::core::{
- Clipboard, ContentFit, Element, Layout, Length, Pixels, Point, Radians,
- Rectangle, Shell, Size, Vector, Widget,
+ Clipboard, ContentFit, Element, Image, Layout, Length, Pixels, Point,
+ Radians, Rectangle, Shell, Size, Vector, Widget,
};
/// A frame that displays an image with the ability to zoom in/out and pan.
@@ -349,11 +349,14 @@ where
let render = |renderer: &mut Renderer| {
renderer.with_translation(translation, |renderer| {
renderer.draw_image(
- self.handle.clone(),
- self.filter_method,
+ Image {
+ handle: self.handle.clone(),
+ filter_method: self.filter_method,
+ rotation: Radians(0.0),
+ opacity: 1.0,
+ snap: true,
+ },
drawing_bounds,
- Radians(0.0),
- 1.0,
);
});
};
diff --git a/widget/src/keyed/column.rs b/widget/src/keyed/column.rs
index 69991d1f..2c56c605 100644
--- a/widget/src/keyed/column.rs
+++ b/widget/src/keyed/column.rs
@@ -265,7 +265,7 @@ where
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
- operation: &mut dyn Operation<()>,
+ operation: &mut dyn Operation,
) {
operation.container(None, layout.bounds(), &mut |operation| {
self.children
diff --git a/widget/src/lazy.rs b/widget/src/lazy.rs
index 606da22d..221f9de3 100644
--- a/widget/src/lazy.rs
+++ b/widget/src/lazy.rs
@@ -4,6 +4,7 @@ pub(crate) mod helpers;
pub mod component;
pub mod responsive;
+#[allow(deprecated)]
pub use component::Component;
pub use responsive::Responsive;
@@ -29,6 +30,7 @@ use std::hash::{Hash, Hasher as H};
use std::rc::Rc;
/// A widget that only rebuilds its contents when necessary.
+#[cfg(feature = "lazy")]
#[allow(missing_debug_implementations)]
pub struct Lazy<'a, Message, Theme, Renderer, Dependency, View> {
dependency: Dependency,
@@ -182,7 +184,7 @@ where
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
- operation: &mut dyn widget::Operation<()>,
+ operation: &mut dyn widget::Operation,
) {
self.with_element(|element| {
element.as_widget().operate(
diff --git a/widget/src/lazy/component.rs b/widget/src/lazy/component.rs
index f079c0df..659bc476 100644
--- a/widget/src/lazy/component.rs
+++ b/widget/src/lazy/component.rs
@@ -1,4 +1,5 @@
//! Build and reuse custom widgets using The Elm Architecture.
+#![allow(deprecated)]
use crate::core::event;
use crate::core::layout::{self, Layout};
use crate::core::mouse;
@@ -30,6 +31,12 @@ use std::rc::Rc;
///
/// Additionally, a [`Component`] is capable of producing a `Message` to notify
/// the parent application of any relevant interactions.
+#[cfg(feature = "lazy")]
+#[deprecated(
+ since = "0.13.0",
+ note = "components introduce encapsulated state and hamper the use of a single source of truth. \
+ Instead, leverage the Elm Architecture directly, or implement a custom widget"
+)]
pub trait Component<Message, Theme = crate::Theme, Renderer = crate::Renderer> {
/// The internal state of this [`Component`].
type State: Default;
@@ -59,7 +66,7 @@ pub trait Component<Message, Theme = crate::Theme, Renderer = crate::Renderer> {
fn operate(
&self,
_state: &mut Self::State,
- _operation: &mut dyn widget::Operation<()>,
+ _operation: &mut dyn widget::Operation,
) {
}
@@ -172,7 +179,7 @@ where
fn rebuild_element_with_operation(
&self,
- operation: &mut dyn widget::Operation<()>,
+ operation: &mut dyn widget::Operation,
) {
let heads = self.state.borrow_mut().take().unwrap().into_heads();
@@ -358,7 +365,7 @@ where
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
- operation: &mut dyn widget::Operation<()>,
+ operation: &mut dyn widget::Operation,
) {
self.rebuild_element_with_operation(operation);
diff --git a/widget/src/lazy/helpers.rs b/widget/src/lazy/helpers.rs
index 4d0776ca..52e690ff 100644
--- a/widget/src/lazy/helpers.rs
+++ b/widget/src/lazy/helpers.rs
@@ -1,9 +1,11 @@
use crate::core::{self, Element, Size};
-use crate::lazy::component::{self, Component};
-use crate::lazy::{Lazy, Responsive};
+use crate::lazy::component;
use std::hash::Hash;
+#[allow(deprecated)]
+pub use crate::lazy::{Component, Lazy, Responsive};
+
/// Creates a new [`Lazy`] widget with the given data `Dependency` and a
/// closure that can turn this data into a widget tree.
#[cfg(feature = "lazy")]
@@ -21,6 +23,12 @@ where
/// Turns an implementor of [`Component`] into an [`Element`] that can be
/// embedded in any application.
#[cfg(feature = "lazy")]
+#[deprecated(
+ since = "0.13.0",
+ note = "components introduce encapsulated state and hamper the use of a single source of truth. \
+ Instead, leverage the Elm Architecture directly, or implement a custom widget"
+)]
+#[allow(deprecated)]
pub fn component<'a, C, Message, Theme, Renderer>(
component: C,
) -> Element<'a, Message, Theme, Renderer>
diff --git a/widget/src/lazy/responsive.rs b/widget/src/lazy/responsive.rs
index 27f52617..dbf281f3 100644
--- a/widget/src/lazy/responsive.rs
+++ b/widget/src/lazy/responsive.rs
@@ -21,6 +21,7 @@ use std::ops::Deref;
///
/// A [`Responsive`] widget will always try to fill all the available space of
/// its parent.
+#[cfg(feature = "lazy")]
#[allow(missing_debug_implementations)]
pub struct Responsive<
'a,
@@ -161,7 +162,7 @@ where
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
- operation: &mut dyn widget::Operation<()>,
+ operation: &mut dyn widget::Operation,
) {
let state = tree.state.downcast_mut::<State>();
let mut content = self.content.borrow_mut();
diff --git a/widget/src/lib.rs b/widget/src/lib.rs
index 00e9aaa4..a68720d6 100644
--- a/widget/src/lib.rs
+++ b/widget/src/lib.rs
@@ -43,9 +43,6 @@ pub use helpers::*;
mod lazy;
#[cfg(feature = "lazy")]
-pub use crate::lazy::{Component, Lazy, Responsive};
-
-#[cfg(feature = "lazy")]
pub use crate::lazy::helpers::*;
#[doc(no_inline)]
@@ -130,5 +127,8 @@ pub mod qr_code;
#[doc(no_inline)]
pub use qr_code::QRCode;
+#[cfg(feature = "markdown")]
+pub mod markdown;
+
pub use crate::core::theme::{self, Theme};
pub use renderer::Renderer;
diff --git a/widget/src/markdown.rs b/widget/src/markdown.rs
new file mode 100644
index 00000000..fa4ee6bf
--- /dev/null
+++ b/widget/src/markdown.rs
@@ -0,0 +1,587 @@
+//! Parse and display Markdown.
+//!
+//! You can enable the `highlighter` feature for syntax highligting
+//! in code blocks.
+//!
+//! Only the variants of [`Item`] are currently supported.
+use crate::core::border;
+use crate::core::font::{self, Font};
+use crate::core::padding;
+use crate::core::theme;
+use crate::core::{
+ self, color, Color, Element, Length, Padding, Pixels, Theme,
+};
+use crate::{column, container, rich_text, row, scrollable, span, text};
+
+use std::cell::{Cell, RefCell};
+use std::rc::Rc;
+
+pub use core::text::Highlight;
+pub use pulldown_cmark::HeadingLevel;
+pub use url::Url;
+
+/// A Markdown item.
+#[derive(Debug, Clone)]
+pub enum Item {
+ /// A heading.
+ Heading(pulldown_cmark::HeadingLevel, Text),
+ /// A paragraph.
+ Paragraph(Text),
+ /// A code block.
+ ///
+ /// You can enable the `highlighter` feature for syntax highligting.
+ CodeBlock(Text),
+ /// A list.
+ List {
+ /// The first number of the list, if it is ordered.
+ start: Option<u64>,
+ /// The items of the list.
+ items: Vec<Vec<Item>>,
+ },
+}
+
+/// A bunch of parsed Markdown text.
+#[derive(Debug, Clone)]
+pub struct Text {
+ spans: Vec<Span>,
+ last_style: Cell<Option<Style>>,
+ last_styled_spans: RefCell<Rc<[text::Span<'static, Url>]>>,
+}
+
+impl Text {
+ fn new(spans: Vec<Span>) -> Self {
+ Self {
+ spans,
+ last_style: Cell::default(),
+ last_styled_spans: RefCell::default(),
+ }
+ }
+
+ /// Returns the [`rich_text()`] spans ready to be used for the given style.
+ ///
+ /// This method performs caching for you. It will only reallocate if the [`Style`]
+ /// provided changes.
+ pub fn spans(&self, style: Style) -> Rc<[text::Span<'static, Url>]> {
+ if Some(style) != self.last_style.get() {
+ *self.last_styled_spans.borrow_mut() =
+ self.spans.iter().map(|span| span.view(&style)).collect();
+
+ self.last_style.set(Some(style));
+ }
+
+ self.last_styled_spans.borrow().clone()
+ }
+}
+
+#[derive(Debug, Clone)]
+enum Span {
+ Standard {
+ text: String,
+ strikethrough: bool,
+ link: Option<Url>,
+ strong: bool,
+ emphasis: bool,
+ code: bool,
+ },
+ #[cfg(feature = "highlighter")]
+ Highlight {
+ text: String,
+ color: Option<Color>,
+ font: Option<Font>,
+ },
+}
+
+impl Span {
+ fn view(&self, style: &Style) -> text::Span<'static, Url> {
+ match self {
+ Span::Standard {
+ text,
+ strikethrough,
+ link,
+ strong,
+ emphasis,
+ code,
+ } => {
+ let span = span(text.clone()).strikethrough(*strikethrough);
+
+ let span = if *code {
+ span.font(Font::MONOSPACE)
+ .color(style.inline_code_color)
+ .background(style.inline_code_highlight.background)
+ .border(style.inline_code_highlight.border)
+ .padding(style.inline_code_padding)
+ } else if *strong || *emphasis {
+ span.font(Font {
+ weight: if *strong {
+ font::Weight::Bold
+ } else {
+ font::Weight::Normal
+ },
+ style: if *emphasis {
+ font::Style::Italic
+ } else {
+ font::Style::Normal
+ },
+ ..Font::default()
+ })
+ } else {
+ span
+ };
+
+ let span = if let Some(link) = link.as_ref() {
+ span.color(style.link_color).link(link.clone())
+ } else {
+ span
+ };
+
+ span
+ }
+ #[cfg(feature = "highlighter")]
+ Span::Highlight { text, color, font } => {
+ span(text.clone()).color_maybe(*color).font_maybe(*font)
+ }
+ }
+ }
+}
+
+/// Parse the given Markdown content.
+pub fn parse(markdown: &str) -> impl Iterator<Item = Item> + '_ {
+ struct List {
+ start: Option<u64>,
+ items: Vec<Vec<Item>>,
+ }
+
+ let mut spans = 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();
+
+ #[cfg(feature = "highlighter")]
+ let mut highlighter = None;
+
+ let parser = pulldown_cmark::Parser::new_ext(
+ markdown,
+ pulldown_cmark::Options::ENABLE_YAML_STYLE_METADATA_BLOCKS
+ | pulldown_cmark::Options::ENABLE_PLUSES_DELIMITED_METADATA_BLOCKS
+ | pulldown_cmark::Options::ENABLE_TABLES
+ | pulldown_cmark::Options::ENABLE_STRIKETHROUGH,
+ );
+
+ let produce = |lists: &mut Vec<List>, item| {
+ if lists.is_empty() {
+ Some(item)
+ } else {
+ lists
+ .last_mut()
+ .expect("list context")
+ .items
+ .last_mut()
+ .expect("item context")
+ .push(item);
+
+ None
+ }
+ };
+
+ // We want to keep the `spans` capacity
+ #[allow(clippy::drain_collect)]
+ parser.filter_map(move |event| match event {
+ pulldown_cmark::Event::Start(tag) => match tag {
+ pulldown_cmark::Tag::Strong if !metadata && !table => {
+ strong = true;
+ None
+ }
+ pulldown_cmark::Tag::Emphasis if !metadata && !table => {
+ emphasis = true;
+ None
+ }
+ pulldown_cmark::Tag::Strikethrough if !metadata && !table => {
+ strikethrough = true;
+ None
+ }
+ pulldown_cmark::Tag::Link { dest_url, .. }
+ if !metadata && !table =>
+ {
+ match Url::parse(&dest_url) {
+ Ok(url)
+ if url.scheme() == "http"
+ || url.scheme() == "https" =>
+ {
+ link = Some(url);
+ }
+ _ => {}
+ }
+
+ None
+ }
+ pulldown_cmark::Tag::List(first_item) if !metadata && !table => {
+ lists.push(List {
+ start: first_item,
+ items: Vec::new(),
+ });
+
+ None
+ }
+ pulldown_cmark::Tag::Item => {
+ lists
+ .last_mut()
+ .expect("list context")
+ .items
+ .push(Vec::new());
+ None
+ }
+ pulldown_cmark::Tag::CodeBlock(
+ pulldown_cmark::CodeBlockKind::Fenced(_language),
+ ) if !metadata && !table => {
+ #[cfg(feature = "highlighter")]
+ {
+ use iced_highlighter::{self, Highlighter};
+ use text::Highlighter as _;
+
+ highlighter =
+ Some(Highlighter::new(&iced_highlighter::Settings {
+ theme: iced_highlighter::Theme::Base16Ocean,
+ token: _language.to_string(),
+ }));
+ }
+
+ None
+ }
+ pulldown_cmark::Tag::MetadataBlock(_) => {
+ metadata = true;
+ None
+ }
+ pulldown_cmark::Tag::Table(_) => {
+ table = true;
+ None
+ }
+ _ => None,
+ },
+ pulldown_cmark::Event::End(tag) => match tag {
+ pulldown_cmark::TagEnd::Heading(level) if !metadata && !table => {
+ produce(
+ &mut lists,
+ Item::Heading(level, Text::new(spans.drain(..).collect())),
+ )
+ }
+ pulldown_cmark::TagEnd::Strong if !metadata && !table => {
+ strong = false;
+ None
+ }
+ pulldown_cmark::TagEnd::Emphasis if !metadata && !table => {
+ emphasis = false;
+ None
+ }
+ pulldown_cmark::TagEnd::Strikethrough if !metadata && !table => {
+ strikethrough = false;
+ None
+ }
+ pulldown_cmark::TagEnd::Link if !metadata && !table => {
+ link = None;
+ None
+ }
+ pulldown_cmark::TagEnd::Paragraph if !metadata && !table => {
+ produce(
+ &mut lists,
+ Item::Paragraph(Text::new(spans.drain(..).collect())),
+ )
+ }
+ pulldown_cmark::TagEnd::Item if !metadata && !table => {
+ if spans.is_empty() {
+ None
+ } else {
+ produce(
+ &mut lists,
+ Item::Paragraph(Text::new(spans.drain(..).collect())),
+ )
+ }
+ }
+ pulldown_cmark::TagEnd::List(_) if !metadata && !table => {
+ let list = lists.pop().expect("list context");
+
+ produce(
+ &mut lists,
+ Item::List {
+ start: list.start,
+ items: list.items,
+ },
+ )
+ }
+ pulldown_cmark::TagEnd::CodeBlock if !metadata && !table => {
+ #[cfg(feature = "highlighter")]
+ {
+ highlighter = None;
+ }
+
+ produce(
+ &mut lists,
+ Item::CodeBlock(Text::new(spans.drain(..).collect())),
+ )
+ }
+ pulldown_cmark::TagEnd::MetadataBlock(_) => {
+ metadata = false;
+ None
+ }
+ pulldown_cmark::TagEnd::Table => {
+ table = false;
+ None
+ }
+ _ => None,
+ },
+ pulldown_cmark::Event::Text(text) if !metadata && !table => {
+ #[cfg(feature = "highlighter")]
+ if let Some(highlighter) = &mut highlighter {
+ use text::Highlighter as _;
+
+ for (range, highlight) in
+ highlighter.highlight_line(text.as_ref())
+ {
+ let span = Span::Highlight {
+ text: text[range].to_owned(),
+ color: highlight.color(),
+ font: highlight.font(),
+ };
+
+ spans.push(span);
+ }
+
+ return None;
+ }
+
+ let span = Span::Standard {
+ text: text.into_string(),
+ strong,
+ emphasis,
+ strikethrough,
+ link: link.clone(),
+ code: false,
+ };
+
+ spans.push(span);
+
+ None
+ }
+ pulldown_cmark::Event::Code(code) if !metadata && !table => {
+ let span = Span::Standard {
+ text: code.into_string(),
+ strong,
+ emphasis,
+ strikethrough,
+ link: link.clone(),
+ code: true,
+ };
+
+ spans.push(span);
+ None
+ }
+ pulldown_cmark::Event::SoftBreak if !metadata && !table => {
+ spans.push(Span::Standard {
+ text: String::from(" "),
+ strikethrough,
+ strong,
+ emphasis,
+ link: link.clone(),
+ code: false,
+ });
+ None
+ }
+ pulldown_cmark::Event::HardBreak if !metadata && !table => {
+ spans.push(Span::Standard {
+ text: String::from("\n"),
+ strikethrough,
+ strong,
+ emphasis,
+ link: link.clone(),
+ code: false,
+ });
+ None
+ }
+ _ => None,
+ })
+}
+
+/// Configuration controlling Markdown rendering in [`view`].
+#[derive(Debug, Clone, Copy)]
+pub struct Settings {
+ /// The base text size.
+ pub text_size: Pixels,
+ /// The text size of level 1 heading.
+ pub h1_size: Pixels,
+ /// The text size of level 2 heading.
+ pub h2_size: Pixels,
+ /// The text size of level 3 heading.
+ pub h3_size: Pixels,
+ /// The text size of level 4 heading.
+ pub h4_size: Pixels,
+ /// The text size of level 5 heading.
+ pub h5_size: Pixels,
+ /// The text size of level 6 heading.
+ pub h6_size: Pixels,
+ /// The text size used in code blocks.
+ pub code_size: Pixels,
+}
+
+impl Settings {
+ /// 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 {
+ let text_size = text_size.into();
+
+ Self {
+ text_size,
+ h1_size: text_size * 2.0,
+ h2_size: text_size * 1.75,
+ h3_size: text_size * 1.5,
+ h4_size: text_size * 1.25,
+ h5_size: text_size,
+ h6_size: text_size,
+ code_size: text_size * 0.75,
+ }
+ }
+}
+
+impl Default for Settings {
+ fn default() -> Self {
+ Self::with_text_size(16)
+ }
+}
+
+/// The text styling of some Markdown rendering in [`view`].
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub struct Style {
+ /// The [`Highlight`] to be applied to the background of inline code.
+ pub inline_code_highlight: Highlight,
+ /// The [`Padding`] to be applied to the background of inline code.
+ pub inline_code_padding: Padding,
+ /// The [`Color`] to be applied to inline code.
+ pub inline_code_color: Color,
+ /// The [`Color`] to be applied to links.
+ pub link_color: Color,
+}
+
+impl Style {
+ /// Creates a new [`Style`] from the given [`theme::Palette`].
+ pub fn from_palette(palette: theme::Palette) -> Self {
+ Self {
+ inline_code_padding: padding::left(1).right(1),
+ inline_code_highlight: Highlight {
+ background: color!(0x111).into(),
+ border: border::rounded(2),
+ },
+ inline_code_color: Color::WHITE,
+ link_color: palette.primary,
+ }
+ }
+}
+
+/// Display a bunch of Markdown items.
+///
+/// You can obtain the items with [`parse`].
+pub fn view<'a, Theme, Renderer>(
+ items: impl IntoIterator<Item = &'a Item>,
+ settings: Settings,
+ style: Style,
+) -> Element<'a, Url, Theme, Renderer>
+where
+ 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,
+ } = settings;
+
+ let spacing = text_size * 0.625;
+
+ let blocks = items.into_iter().enumerate().map(|(i, item)| match item {
+ Item::Heading(level, heading) => {
+ container(rich_text(heading.spans(style)).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, style)]
+ .spacing(spacing)
+ .into()
+ }))
+ .spacing(spacing)
+ .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, style)
+ ]
+ .spacing(spacing)
+ .into()
+ }))
+ .spacing(spacing)
+ .into(),
+ Item::CodeBlock(code) => container(
+ scrollable(
+ container(
+ rich_text(code.spans(style))
+ .font(Font::MONOSPACE)
+ .size(code_size),
+ )
+ .padding(spacing.0 / 2.0),
+ )
+ .direction(scrollable::Direction::Horizontal(
+ scrollable::Scrollbar::default()
+ .width(spacing.0 / 2.0)
+ .scroller_width(spacing.0 / 2.0),
+ )),
+ )
+ .width(Length::Fill)
+ .padding(spacing.0 / 2.0)
+ .class(Theme::code_block())
+ .into(),
+ });
+
+ Element::new(column(blocks).width(Length::Fill).spacing(text_size))
+}
+
+/// The theme catalog of Markdown items.
+pub trait Catalog:
+ container::Catalog + scrollable::Catalog + text::Catalog
+{
+ /// The styling class of a Markdown code block.
+ fn code_block<'a>() -> <Self as container::Catalog>::Class<'a>;
+}
+
+impl Catalog for Theme {
+ fn code_block<'a>() -> <Self as container::Catalog>::Class<'a> {
+ Box::new(container::dark)
+ }
+}
diff --git a/widget/src/mouse_area.rs b/widget/src/mouse_area.rs
index 17cae53b..d255ac99 100644
--- a/widget/src/mouse_area.rs
+++ b/widget/src/mouse_area.rs
@@ -1,7 +1,4 @@
//! A container for capturing mouse events.
-
-use iced_renderer::core::Point;
-
use crate::core::event::{self, Event};
use crate::core::layout;
use crate::core::mouse;
@@ -10,7 +7,8 @@ use crate::core::renderer;
use crate::core::touch;
use crate::core::widget::{tree, Operation, Tree};
use crate::core::{
- Clipboard, Element, Layout, Length, Rectangle, Shell, Size, Vector, Widget,
+ Clipboard, Element, Layout, Length, Point, Rectangle, Shell, Size, Vector,
+ Widget,
};
/// Emit messages on mouse events.
@@ -28,8 +26,9 @@ pub struct MouseArea<
on_right_release: Option<Message>,
on_middle_press: Option<Message>,
on_middle_release: Option<Message>,
+ on_scroll: Option<Box<dyn Fn(mouse::ScrollDelta) -> Message + 'a>>,
on_enter: Option<Message>,
- on_move: Option<Box<dyn Fn(Point) -> Message>>,
+ on_move: Option<Box<dyn Fn(Point) -> Message + 'a>>,
on_exit: Option<Message>,
interaction: Option<mouse::Interaction>,
}
@@ -77,6 +76,16 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> {
self
}
+ /// The message to emit when scroll wheel is used
+ #[must_use]
+ pub fn on_scroll(
+ mut self,
+ on_scroll: impl Fn(mouse::ScrollDelta) -> Message + 'a,
+ ) -> Self {
+ self.on_scroll = Some(Box::new(on_scroll));
+ self
+ }
+
/// The message to emit when the mouse enters the area.
#[must_use]
pub fn on_enter(mut self, message: Message) -> Self {
@@ -86,11 +95,8 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> {
/// The message to emit when the mouse moves in the area.
#[must_use]
- pub fn on_move<F>(mut self, build_message: F) -> Self
- where
- F: Fn(Point) -> Message + 'static,
- {
- self.on_move = Some(Box::new(build_message));
+ pub fn on_move(mut self, on_move: impl Fn(Point) -> Message + 'a) -> Self {
+ self.on_move = Some(Box::new(on_move));
self
}
@@ -113,6 +119,8 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> {
#[derive(Default)]
struct State {
is_hovered: bool,
+ bounds: Rectangle,
+ cursor_position: Option<Point>,
}
impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> {
@@ -128,6 +136,7 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> {
on_right_release: None,
on_middle_press: None,
on_middle_release: None,
+ on_scroll: None,
on_enter: None,
on_move: None,
on_exit: None,
@@ -178,7 +187,7 @@ where
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
- operation: &mut dyn Operation<()>,
+ operation: &mut dyn Operation,
) {
self.content.as_widget().operate(
&mut tree.children[0],
@@ -302,13 +311,17 @@ fn update<Message: Clone, Theme, Renderer>(
cursor: mouse::Cursor,
shell: &mut Shell<'_, Message>,
) -> event::Status {
- if let Event::Mouse(mouse::Event::CursorMoved { .. })
- | Event::Touch(touch::Event::FingerMoved { .. }) = event
- {
- let state: &mut State = tree.state.downcast_mut();
+ let state: &mut State = tree.state.downcast_mut();
+ let cursor_position = cursor.position();
+ let bounds = layout.bounds();
+
+ if state.cursor_position != cursor_position && state.bounds != bounds {
let was_hovered = state.is_hovered;
+
state.is_hovered = cursor.is_over(layout.bounds());
+ state.cursor_position = cursor_position;
+ state.bounds = bounds;
match (
widget.on_enter.as_ref(),
@@ -397,5 +410,13 @@ fn update<Message: Clone, Theme, Renderer>(
}
}
+ if let Some(on_scroll) = widget.on_scroll.as_ref() {
+ if let Event::Mouse(mouse::Event::WheelScrolled { delta }) = event {
+ shell.publish(on_scroll(delta));
+
+ return event::Status::Captured;
+ }
+ }
+
event::Status::Ignored
}
diff --git a/widget/src/overlay/menu.rs b/widget/src/overlay/menu.rs
index 98efe305..f05ae40a 100644
--- a/widget/src/overlay/menu.rs
+++ b/widget/src/overlay/menu.rs
@@ -1,5 +1,6 @@
//! Build and show dropdown menus.
use crate::core::alignment;
+use crate::core::border::{self, Border};
use crate::core::event::{self, Event};
use crate::core::layout::{self, Layout};
use crate::core::mouse;
@@ -9,8 +10,8 @@ use crate::core::text::{self, Text};
use crate::core::touch;
use crate::core::widget::Tree;
use crate::core::{
- Background, Border, Clipboard, Color, Length, Padding, Pixels, Point,
- Rectangle, Size, Theme, Vector,
+ Background, Clipboard, Color, Length, Padding, Pixels, Point, Rectangle,
+ Size, Theme, Vector,
};
use crate::core::{Element, Shell, Widget};
use crate::scrollable::{self, Scrollable};
@@ -200,21 +201,18 @@ where
class,
} = menu;
- let list = Scrollable::with_direction(
- List {
- options,
- hovered_option,
- on_selected,
- on_option_hovered,
- font,
- text_size,
- text_line_height,
- text_shaping,
- padding,
- class,
- },
- scrollable::Direction::default(),
- );
+ let list = Scrollable::new(List {
+ options,
+ hovered_option,
+ on_selected,
+ on_option_hovered,
+ font,
+ text_size,
+ text_line_height,
+ text_shaping,
+ padding,
+ class,
+ });
state.tree.diff(&list as &dyn Widget<_, _, _>);
@@ -517,7 +515,7 @@ where
width: bounds.width - style.border.width * 2.0,
..bounds
},
- border: Border::rounded(style.border.radius),
+ border: border::rounded(style.border.radius),
..renderer::Quad::default()
},
style.selected_background,
@@ -534,6 +532,7 @@ where
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Center,
shaping: self.text_shaping,
+ wrapping: text::Wrapping::default(),
},
Point::new(bounds.x + self.padding.left, bounds.center_y()),
if is_selected {
diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs
index c3da3879..710a5443 100644
--- a/widget/src/pane_grid.rs
+++ b/widget/src/pane_grid.rs
@@ -10,6 +10,7 @@
mod axis;
mod configuration;
mod content;
+mod controls;
mod direction;
mod draggable;
mod node;
@@ -22,6 +23,7 @@ pub mod state;
pub use axis::Axis;
pub use configuration::Configuration;
pub use content::Content;
+pub use controls::Controls;
pub use direction::Direction;
pub use draggable::Draggable;
pub use node::Node;
@@ -324,7 +326,7 @@ where
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
- operation: &mut dyn widget::Operation<()>,
+ operation: &mut dyn widget::Operation,
) {
operation.container(None, layout.bounds(), &mut |operation| {
self.contents
diff --git a/widget/src/pane_grid/content.rs b/widget/src/pane_grid/content.rs
index d45fc0cd..ec0676b1 100644
--- a/widget/src/pane_grid/content.rs
+++ b/widget/src/pane_grid/content.rs
@@ -214,7 +214,7 @@ where
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
- operation: &mut dyn widget::Operation<()>,
+ operation: &mut dyn widget::Operation,
) {
let body_layout = if let Some(title_bar) = &self.title_bar {
let mut children = layout.children();
diff --git a/widget/src/pane_grid/controls.rs b/widget/src/pane_grid/controls.rs
new file mode 100644
index 00000000..13b57acb
--- /dev/null
+++ b/widget/src/pane_grid/controls.rs
@@ -0,0 +1,59 @@
+use crate::container;
+use crate::core::{self, Element};
+
+/// The controls of a [`Pane`].
+///
+/// [`Pane`]: super::Pane
+#[allow(missing_debug_implementations)]
+pub struct Controls<
+ 'a,
+ Message,
+ Theme = crate::Theme,
+ Renderer = crate::Renderer,
+> where
+ Theme: container::Catalog,
+ Renderer: core::Renderer,
+{
+ pub(super) full: Element<'a, Message, Theme, Renderer>,
+ pub(super) compact: Option<Element<'a, Message, Theme, Renderer>>,
+}
+
+impl<'a, Message, Theme, Renderer> Controls<'a, Message, Theme, Renderer>
+where
+ Theme: container::Catalog,
+ Renderer: core::Renderer,
+{
+ /// Creates a new [`Controls`] with the given content.
+ pub fn new(
+ content: impl Into<Element<'a, Message, Theme, Renderer>>,
+ ) -> Self {
+ Self {
+ full: content.into(),
+ compact: None,
+ }
+ }
+
+ /// Creates a new [`Controls`] with a full and compact variant.
+ /// If there is not enough room to show the full variant without overlap,
+ /// then the compact variant will be shown instead.
+ pub fn dynamic(
+ full: impl Into<Element<'a, Message, Theme, Renderer>>,
+ compact: impl Into<Element<'a, Message, Theme, Renderer>>,
+ ) -> Self {
+ Self {
+ full: full.into(),
+ compact: Some(compact.into()),
+ }
+ }
+}
+
+impl<'a, Message, Theme, Renderer> From<Element<'a, Message, Theme, Renderer>>
+ for Controls<'a, Message, Theme, Renderer>
+where
+ Theme: container::Catalog,
+ Renderer: core::Renderer,
+{
+ fn from(value: Element<'a, Message, Theme, Renderer>) -> Self {
+ Self::new(value)
+ }
+}
diff --git a/widget/src/pane_grid/title_bar.rs b/widget/src/pane_grid/title_bar.rs
index c05f1252..5002b4f7 100644
--- a/widget/src/pane_grid/title_bar.rs
+++ b/widget/src/pane_grid/title_bar.rs
@@ -9,6 +9,7 @@ use crate::core::{
self, Clipboard, Element, Layout, Padding, Point, Rectangle, Shell, Size,
Vector,
};
+use crate::pane_grid::controls::Controls;
/// The title bar of a [`Pane`].
///
@@ -24,7 +25,7 @@ pub struct TitleBar<
Renderer: core::Renderer,
{
content: Element<'a, Message, Theme, Renderer>,
- controls: Option<Element<'a, Message, Theme, Renderer>>,
+ controls: Option<Controls<'a, Message, Theme, Renderer>>,
padding: Padding,
always_show_controls: bool,
class: Theme::Class<'a>,
@@ -51,7 +52,7 @@ where
/// Sets the controls of the [`TitleBar`].
pub fn controls(
mut self,
- controls: impl Into<Element<'a, Message, Theme, Renderer>>,
+ controls: impl Into<Controls<'a, Message, Theme, Renderer>>,
) -> Self {
self.controls = Some(controls.into());
self
@@ -104,10 +105,22 @@ where
Renderer: core::Renderer,
{
pub(super) fn state(&self) -> Tree {
- let children = if let Some(controls) = self.controls.as_ref() {
- vec![Tree::new(&self.content), Tree::new(controls)]
- } else {
- vec![Tree::new(&self.content), Tree::empty()]
+ let children = match self.controls.as_ref() {
+ Some(controls) => match controls.compact.as_ref() {
+ Some(compact) => vec![
+ Tree::new(&self.content),
+ Tree::new(&controls.full),
+ Tree::new(compact),
+ ],
+ None => vec![
+ Tree::new(&self.content),
+ Tree::new(&controls.full),
+ Tree::empty(),
+ ],
+ },
+ None => {
+ vec![Tree::new(&self.content), Tree::empty(), Tree::empty()]
+ }
};
Tree {
@@ -117,9 +130,13 @@ where
}
pub(super) fn diff(&self, tree: &mut Tree) {
- if tree.children.len() == 2 {
+ if tree.children.len() == 3 {
if let Some(controls) = self.controls.as_ref() {
- tree.children[1].diff(controls);
+ if let Some(compact) = controls.compact.as_ref() {
+ tree.children[2].diff(compact);
+ }
+
+ tree.children[1].diff(&controls.full);
}
tree.children[0].diff(&self.content);
@@ -164,18 +181,42 @@ where
if title_layout.bounds().width + controls_layout.bounds().width
> padded.bounds().width
{
- show_title = false;
+ if let Some(compact) = controls.compact.as_ref() {
+ let compact_layout = children.next().unwrap();
+
+ compact.as_widget().draw(
+ &tree.children[2],
+ renderer,
+ theme,
+ &inherited_style,
+ compact_layout,
+ cursor,
+ viewport,
+ );
+ } else {
+ show_title = false;
+
+ controls.full.as_widget().draw(
+ &tree.children[1],
+ renderer,
+ theme,
+ &inherited_style,
+ controls_layout,
+ cursor,
+ viewport,
+ );
+ }
+ } else {
+ controls.full.as_widget().draw(
+ &tree.children[1],
+ renderer,
+ theme,
+ &inherited_style,
+ controls_layout,
+ cursor,
+ viewport,
+ );
}
-
- controls.as_widget().draw(
- &tree.children[1],
- renderer,
- theme,
- &inherited_style,
- controls_layout,
- cursor,
- viewport,
- );
}
}
@@ -207,13 +248,20 @@ where
let mut children = padded.children();
let title_layout = children.next().unwrap();
- if self.controls.is_some() {
+ if let Some(controls) = self.controls.as_ref() {
let controls_layout = children.next().unwrap();
if title_layout.bounds().width + controls_layout.bounds().width
> padded.bounds().width
{
- !controls_layout.bounds().contains(cursor_position)
+ if controls.compact.is_some() {
+ let compact_layout = children.next().unwrap();
+
+ !compact_layout.bounds().contains(cursor_position)
+ && !title_layout.bounds().contains(cursor_position)
+ } else {
+ !controls_layout.bounds().contains(cursor_position)
+ }
} else {
!controls_layout.bounds().contains(cursor_position)
&& !title_layout.bounds().contains(cursor_position)
@@ -244,25 +292,73 @@ where
let title_size = title_layout.size();
let node = if let Some(controls) = &self.controls {
- let controls_layout = controls.as_widget().layout(
+ let controls_layout = controls.full.as_widget().layout(
&mut tree.children[1],
renderer,
&layout::Limits::new(Size::ZERO, max_size),
);
- let controls_size = controls_layout.size();
- let space_before_controls = max_size.width - controls_size.width;
-
- let height = title_size.height.max(controls_size.height);
-
- layout::Node::with_children(
- Size::new(max_size.width, height),
- vec![
- title_layout,
- controls_layout
- .move_to(Point::new(space_before_controls, 0.0)),
- ],
- )
+ if title_layout.bounds().width + controls_layout.bounds().width
+ > max_size.width
+ {
+ if let Some(compact) = controls.compact.as_ref() {
+ let compact_layout = compact.as_widget().layout(
+ &mut tree.children[2],
+ renderer,
+ &layout::Limits::new(Size::ZERO, max_size),
+ );
+
+ let compact_size = compact_layout.size();
+ let space_before_controls =
+ max_size.width - compact_size.width;
+
+ let height = title_size.height.max(compact_size.height);
+
+ layout::Node::with_children(
+ Size::new(max_size.width, height),
+ vec![
+ title_layout,
+ controls_layout,
+ compact_layout.move_to(Point::new(
+ space_before_controls,
+ 0.0,
+ )),
+ ],
+ )
+ } else {
+ let controls_size = controls_layout.size();
+ let space_before_controls =
+ max_size.width - controls_size.width;
+
+ let height = title_size.height.max(controls_size.height);
+
+ layout::Node::with_children(
+ Size::new(max_size.width, height),
+ vec![
+ title_layout,
+ controls_layout.move_to(Point::new(
+ space_before_controls,
+ 0.0,
+ )),
+ ],
+ )
+ }
+ } else {
+ let controls_size = controls_layout.size();
+ let space_before_controls =
+ max_size.width - controls_size.width;
+
+ let height = title_size.height.max(controls_size.height);
+
+ layout::Node::with_children(
+ Size::new(max_size.width, height),
+ vec![
+ title_layout,
+ controls_layout
+ .move_to(Point::new(space_before_controls, 0.0)),
+ ],
+ )
+ }
} else {
layout::Node::with_children(
Size::new(max_size.width, title_size.height),
@@ -278,7 +374,7 @@ where
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
- operation: &mut dyn widget::Operation<()>,
+ operation: &mut dyn widget::Operation,
) {
let mut children = layout.children();
let padded = children.next().unwrap();
@@ -293,15 +389,33 @@ where
if title_layout.bounds().width + controls_layout.bounds().width
> padded.bounds().width
{
- show_title = false;
- }
+ if let Some(compact) = controls.compact.as_ref() {
+ let compact_layout = children.next().unwrap();
- controls.as_widget().operate(
- &mut tree.children[1],
- controls_layout,
- renderer,
- operation,
- );
+ compact.as_widget().operate(
+ &mut tree.children[2],
+ compact_layout,
+ renderer,
+ operation,
+ );
+ } else {
+ show_title = false;
+
+ controls.full.as_widget().operate(
+ &mut tree.children[1],
+ controls_layout,
+ renderer,
+ operation,
+ );
+ }
+ } else {
+ controls.full.as_widget().operate(
+ &mut tree.children[1],
+ controls_layout,
+ renderer,
+ operation,
+ );
+ }
};
if show_title {
@@ -337,19 +451,45 @@ where
if title_layout.bounds().width + controls_layout.bounds().width
> padded.bounds().width
{
- show_title = false;
- }
+ if let Some(compact) = controls.compact.as_mut() {
+ let compact_layout = children.next().unwrap();
+
+ compact.as_widget_mut().on_event(
+ &mut tree.children[2],
+ event.clone(),
+ compact_layout,
+ cursor,
+ renderer,
+ clipboard,
+ shell,
+ viewport,
+ )
+ } else {
+ show_title = false;
- controls.as_widget_mut().on_event(
- &mut tree.children[1],
- event.clone(),
- controls_layout,
- cursor,
- renderer,
- clipboard,
- shell,
- viewport,
- )
+ controls.full.as_widget_mut().on_event(
+ &mut tree.children[1],
+ event.clone(),
+ controls_layout,
+ cursor,
+ renderer,
+ clipboard,
+ shell,
+ viewport,
+ )
+ }
+ } else {
+ controls.full.as_widget_mut().on_event(
+ &mut tree.children[1],
+ event.clone(),
+ controls_layout,
+ cursor,
+ renderer,
+ clipboard,
+ shell,
+ viewport,
+ )
+ }
} else {
event::Status::Ignored
};
@@ -396,18 +536,33 @@ where
if let Some(controls) = &self.controls {
let controls_layout = children.next().unwrap();
- let controls_interaction = controls.as_widget().mouse_interaction(
- &tree.children[1],
- controls_layout,
- cursor,
- viewport,
- renderer,
- );
+ let controls_interaction =
+ controls.full.as_widget().mouse_interaction(
+ &tree.children[1],
+ controls_layout,
+ cursor,
+ viewport,
+ renderer,
+ );
if title_layout.bounds().width + controls_layout.bounds().width
> padded.bounds().width
{
- controls_interaction
+ if let Some(compact) = controls.compact.as_ref() {
+ let compact_layout = children.next().unwrap();
+ let compact_interaction =
+ compact.as_widget().mouse_interaction(
+ &tree.children[2],
+ compact_layout,
+ cursor,
+ viewport,
+ renderer,
+ );
+
+ compact_interaction.max(title_interaction)
+ } else {
+ controls_interaction
+ }
} else {
controls_interaction.max(title_interaction)
}
@@ -444,12 +599,36 @@ where
controls.as_mut().and_then(|controls| {
let controls_layout = children.next()?;
- controls.as_widget_mut().overlay(
- controls_state,
- controls_layout,
- renderer,
- translation,
- )
+ if title_layout.bounds().width
+ + controls_layout.bounds().width
+ > padded.bounds().width
+ {
+ if let Some(compact) = controls.compact.as_mut() {
+ let compact_state = states.next().unwrap();
+ let compact_layout = children.next()?;
+
+ compact.as_widget_mut().overlay(
+ compact_state,
+ compact_layout,
+ renderer,
+ translation,
+ )
+ } else {
+ controls.full.as_widget_mut().overlay(
+ controls_state,
+ controls_layout,
+ renderer,
+ translation,
+ )
+ }
+ } else {
+ controls.full.as_widget_mut().overlay(
+ controls_state,
+ controls_layout,
+ renderer,
+ translation,
+ )
+ }
})
})
}
diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs
index 97de5b48..1fc9951e 100644
--- a/widget/src/pick_list.rs
+++ b/widget/src/pick_list.rs
@@ -6,7 +6,8 @@ use crate::core::layout;
use crate::core::mouse;
use crate::core::overlay;
use crate::core::renderer;
-use crate::core::text::{self, Paragraph as _, Text};
+use crate::core::text::paragraph;
+use crate::core::text::{self, Text};
use crate::core::touch;
use crate::core::widget::tree::{self, Tree};
use crate::core::{
@@ -80,7 +81,7 @@ where
padding: crate::button::DEFAULT_PADDING,
text_size: None,
text_line_height: text::LineHeight::default(),
- text_shaping: text::Shaping::Basic,
+ text_shaping: text::Shaping::default(),
font: None,
handle: Handle::default(),
class: <Theme as Catalog>::default(),
@@ -249,6 +250,7 @@ where
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Center,
shaping: self.text_shaping,
+ wrapping: text::Wrapping::default(),
};
for (option, paragraph) in options.iter().zip(state.options.iter_mut())
@@ -514,6 +516,7 @@ where
horizontal_alignment: alignment::Horizontal::Right,
vertical_alignment: alignment::Vertical::Center,
shaping,
+ wrapping: text::Wrapping::default(),
},
Point::new(
bounds.x + bounds.width - self.padding.right,
@@ -543,6 +546,7 @@ where
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Center,
shaping: self.text_shaping,
+ wrapping: text::Wrapping::default(),
},
Point::new(bounds.x + self.padding.left, bounds.center_y()),
if is_selected {
@@ -622,8 +626,8 @@ struct State<P: text::Paragraph> {
keyboard_modifiers: keyboard::Modifiers,
is_open: bool,
hovered_option: Option<usize>,
- options: Vec<P>,
- placeholder: P,
+ options: Vec<paragraph::Plain<P>>,
+ placeholder: paragraph::Plain<P>,
}
impl<P: text::Paragraph> State<P> {
@@ -635,7 +639,7 @@ impl<P: text::Paragraph> State<P> {
is_open: bool::default(),
hovered_option: Option::default(),
options: Vec::new(),
- placeholder: P::default(),
+ placeholder: paragraph::Plain::default(),
}
}
}
diff --git a/widget/src/progress_bar.rs b/widget/src/progress_bar.rs
index e7821b43..a10feea6 100644
--- a/widget/src/progress_bar.rs
+++ b/widget/src/progress_bar.rs
@@ -1,10 +1,11 @@
//! Provide progress feedback to your users.
+use crate::core::border::{self, Border};
use crate::core::layout;
use crate::core::mouse;
use crate::core::renderer;
use crate::core::widget::Tree;
use crate::core::{
- self, Background, Border, Element, Layout, Length, Rectangle, Size, Theme,
+ self, Background, Color, Element, Layout, Length, Rectangle, Size, Theme,
Widget,
};
@@ -151,7 +152,10 @@ where
width: active_progress_width,
..bounds
},
- border: Border::rounded(style.border.radius),
+ border: Border {
+ color: Color::TRANSPARENT,
+ ..style.border
+ },
..renderer::Quad::default()
},
style.bar,
@@ -255,6 +259,6 @@ fn styled(
Style {
background: background.into(),
bar: bar.into(),
- border: Border::rounded(2),
+ border: border::rounded(2),
}
}
diff --git a/widget/src/radio.rs b/widget/src/radio.rs
index 6b22961d..cfa961f3 100644
--- a/widget/src/radio.rs
+++ b/widget/src/radio.rs
@@ -1,5 +1,6 @@
//! Create choices using radio buttons.
use crate::core::alignment;
+use crate::core::border::{self, Border};
use crate::core::event::{self, Event};
use crate::core::layout;
use crate::core::mouse;
@@ -9,8 +10,8 @@ use crate::core::touch;
use crate::core::widget;
use crate::core::widget::tree::{self, Tree};
use crate::core::{
- Background, Border, Clipboard, Color, Element, Layout, Length, Pixels,
- Rectangle, Shell, Size, Theme, Widget,
+ Background, Clipboard, Color, Element, Layout, Length, Pixels, Rectangle,
+ Shell, Size, Theme, Widget,
};
/// A circular button representing a choice.
@@ -81,6 +82,7 @@ where
text_size: Option<Pixels>,
text_line_height: text::LineHeight,
text_shaping: text::Shaping,
+ text_wrapping: text::Wrapping,
font: Option<Renderer::Font>,
class: Theme::Class<'a>,
}
@@ -104,7 +106,7 @@ where
/// * the label of the [`Radio`] button
/// * the current selected value
/// * a function that will be called when the [`Radio`] is selected. It
- /// receives the value of the radio and must produce a `Message`.
+ /// receives the value of the radio and must produce a `Message`.
pub fn new<F, V>(
label: impl Into<String>,
value: V,
@@ -121,10 +123,11 @@ where
label: label.into(),
width: Length::Shrink,
size: Self::DEFAULT_SIZE,
- spacing: Self::DEFAULT_SPACING, //15
+ spacing: Self::DEFAULT_SPACING,
text_size: None,
text_line_height: text::LineHeight::default(),
- text_shaping: text::Shaping::Basic,
+ text_shaping: text::Shaping::default(),
+ text_wrapping: text::Wrapping::default(),
font: None,
class: Theme::default(),
}
@@ -169,6 +172,12 @@ where
self
}
+ /// Sets the [`text::Wrapping`] strategy of the [`Radio`] button.
+ pub fn text_wrapping(mut self, wrapping: text::Wrapping) -> Self {
+ self.text_wrapping = wrapping;
+ self
+ }
+
/// Sets the text font of the [`Radio`] button.
pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
self.font = Some(font.into());
@@ -244,6 +253,7 @@ where
alignment::Horizontal::Left,
alignment::Vertical::Top,
self.text_shaping,
+ self.text_wrapping,
)
},
)
@@ -342,7 +352,7 @@ where
width: bounds.width - dot_size,
height: bounds.height - dot_size,
},
- border: Border::rounded(dot_size / 2.0),
+ border: border::rounded(dot_size / 2.0),
..renderer::Quad::default()
},
style.dot_color,
@@ -352,12 +362,14 @@ where
{
let label_layout = children.next().unwrap();
+ let state: &widget::text::State<Renderer::Paragraph> =
+ tree.state.downcast_ref();
crate::text::draw(
renderer,
defaults,
label_layout,
- tree.state.downcast_ref(),
+ state.0.raw(),
crate::text::Style {
color: style.text_color,
},
diff --git a/widget/src/row.rs b/widget/src/row.rs
index c8fcdb61..85af912f 100644
--- a/widget/src/row.rs
+++ b/widget/src/row.rs
@@ -1,4 +1,5 @@
//! Distribute content horizontally.
+use crate::core::alignment::{self, Alignment};
use crate::core::event::{self, Event};
use crate::core::layout::{self, Layout};
use crate::core::mouse;
@@ -6,8 +7,8 @@ use crate::core::overlay;
use crate::core::renderer;
use crate::core::widget::{Operation, Tree};
use crate::core::{
- Alignment, Clipboard, Element, Length, Padding, Pixels, Rectangle, Shell,
- Size, Vector, Widget,
+ Clipboard, Element, Length, Padding, Pixels, Rectangle, Shell, Size,
+ Vector, Widget,
};
/// A container that distributes its contents horizontally.
@@ -17,7 +18,7 @@ pub struct Row<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> {
padding: Padding,
width: Length,
height: Length,
- align_items: Alignment,
+ align: Alignment,
clip: bool,
children: Vec<Element<'a, Message, Theme, Renderer>>,
}
@@ -60,7 +61,7 @@ where
padding: Padding::ZERO,
width: Length::Shrink,
height: Length::Shrink,
- align_items: Alignment::Start,
+ align: Alignment::Start,
clip: false,
children,
}
@@ -95,8 +96,8 @@ where
}
/// Sets the vertical alignment of the contents of the [`Row`] .
- pub fn align_items(mut self, align: Alignment) -> Self {
- self.align_items = align;
+ pub fn align_y(mut self, align: impl Into<alignment::Vertical>) -> Self {
+ self.align = Alignment::from(align.into());
self
}
@@ -141,6 +142,13 @@ where
) -> Self {
children.into_iter().fold(self, Self::push)
}
+
+ /// Turns the [`Row`] into a [`Wrapping`] row.
+ ///
+ /// The original alignment of the [`Row`] is preserved per row wrapped.
+ pub fn wrap(self) -> Wrapping<'a, Message, Theme, Renderer> {
+ Wrapping { row: self }
+ }
}
impl<'a, Message, Renderer> Default for Row<'a, Message, Renderer>
@@ -199,7 +207,7 @@ where
self.height,
self.padding,
self.spacing,
- self.align_items,
+ self.align,
&self.children,
&mut tree.children,
)
@@ -210,7 +218,7 @@ where
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
- operation: &mut dyn Operation<()>,
+ operation: &mut dyn Operation,
) {
operation.container(None, layout.bounds(), &mut |operation| {
self.children
@@ -338,3 +346,196 @@ where
Self::new(row)
}
}
+
+/// A [`Row`] that wraps its contents.
+///
+/// Create a [`Row`] first, and then call [`Row::wrap`] to
+/// obtain a [`Row`] that wraps its contents.
+///
+/// The original alignment of the [`Row`] is preserved per row wrapped.
+#[allow(missing_debug_implementations)]
+pub struct Wrapping<
+ 'a,
+ Message,
+ Theme = crate::Theme,
+ Renderer = crate::Renderer,
+> {
+ row: Row<'a, Message, Theme, Renderer>,
+}
+
+impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
+ for Wrapping<'a, Message, Theme, Renderer>
+where
+ Renderer: crate::core::Renderer,
+{
+ fn children(&self) -> Vec<Tree> {
+ self.row.children()
+ }
+
+ fn diff(&self, tree: &mut Tree) {
+ self.row.diff(tree);
+ }
+
+ fn size(&self) -> Size<Length> {
+ self.row.size()
+ }
+
+ fn layout(
+ &self,
+ tree: &mut Tree,
+ renderer: &Renderer,
+ limits: &layout::Limits,
+ ) -> layout::Node {
+ let limits = limits
+ .width(self.row.width)
+ .height(self.row.height)
+ .shrink(self.row.padding);
+
+ let spacing = self.row.spacing;
+ let max_width = limits.max().width;
+
+ let mut children: Vec<layout::Node> = Vec::new();
+ let mut intrinsic_size = Size::ZERO;
+ let mut row_start = 0;
+ let mut row_height = 0.0;
+ let mut x = 0.0;
+ let mut y = 0.0;
+
+ let align_factor = match self.row.align {
+ Alignment::Start => 0.0,
+ Alignment::Center => 2.0,
+ Alignment::End => 1.0,
+ };
+
+ let align = |row_start: std::ops::Range<usize>,
+ row_height: f32,
+ children: &mut Vec<layout::Node>| {
+ if align_factor != 0.0 {
+ for node in &mut children[row_start] {
+ let height = node.size().height;
+
+ node.translate_mut(Vector::new(
+ 0.0,
+ (row_height - height) / align_factor,
+ ));
+ }
+ }
+ };
+
+ for (i, child) in self.row.children.iter().enumerate() {
+ let node = child.as_widget().layout(
+ &mut tree.children[i],
+ renderer,
+ &limits,
+ );
+
+ let child_size = node.size();
+
+ if x != 0.0 && x + child_size.width > max_width {
+ intrinsic_size.width = intrinsic_size.width.max(x - spacing);
+
+ align(row_start..i, row_height, &mut children);
+
+ y += row_height + spacing;
+ x = 0.0;
+ row_start = i;
+ row_height = 0.0;
+ }
+
+ row_height = row_height.max(child_size.height);
+
+ children.push(node.move_to((
+ x + self.row.padding.left,
+ y + self.row.padding.top,
+ )));
+
+ x += child_size.width + spacing;
+ }
+
+ if x != 0.0 {
+ intrinsic_size.width = intrinsic_size.width.max(x - spacing);
+ }
+
+ intrinsic_size.height = (y - spacing).max(0.0) + row_height;
+ align(row_start..children.len(), row_height, &mut children);
+
+ let size =
+ limits.resolve(self.row.width, self.row.height, intrinsic_size);
+
+ layout::Node::with_children(size.expand(self.row.padding), children)
+ }
+
+ fn operate(
+ &self,
+ tree: &mut Tree,
+ layout: Layout<'_>,
+ renderer: &Renderer,
+ operation: &mut dyn Operation,
+ ) {
+ self.row.operate(tree, layout, renderer, operation);
+ }
+
+ fn on_event(
+ &mut self,
+ tree: &mut Tree,
+ event: Event,
+ layout: Layout<'_>,
+ cursor: mouse::Cursor,
+ renderer: &Renderer,
+ clipboard: &mut dyn Clipboard,
+ shell: &mut Shell<'_, Message>,
+ viewport: &Rectangle,
+ ) -> event::Status {
+ self.row.on_event(
+ tree, event, layout, cursor, renderer, clipboard, shell, viewport,
+ )
+ }
+
+ fn mouse_interaction(
+ &self,
+ tree: &Tree,
+ layout: Layout<'_>,
+ cursor: mouse::Cursor,
+ viewport: &Rectangle,
+ renderer: &Renderer,
+ ) -> mouse::Interaction {
+ self.row
+ .mouse_interaction(tree, layout, cursor, viewport, renderer)
+ }
+
+ fn draw(
+ &self,
+ tree: &Tree,
+ renderer: &mut Renderer,
+ theme: &Theme,
+ style: &renderer::Style,
+ layout: Layout<'_>,
+ cursor: mouse::Cursor,
+ viewport: &Rectangle,
+ ) {
+ self.row
+ .draw(tree, renderer, theme, style, layout, cursor, viewport);
+ }
+
+ fn overlay<'b>(
+ &'b mut self,
+ tree: &'b mut Tree,
+ layout: Layout<'_>,
+ renderer: &Renderer,
+ translation: Vector,
+ ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
+ self.row.overlay(tree, layout, renderer, translation)
+ }
+}
+
+impl<'a, Message, Theme, Renderer> From<Wrapping<'a, Message, Theme, Renderer>>
+ for Element<'a, Message, Theme, Renderer>
+where
+ Message: 'a,
+ Theme: 'a,
+ Renderer: crate::core::Renderer + 'a,
+{
+ fn from(row: Wrapping<'a, Message, Theme, Renderer>) -> Self {
+ Self::new(row)
+ }
+}
diff --git a/widget/src/rule.rs b/widget/src/rule.rs
index 1a536d2f..bbcd577e 100644
--- a/widget/src/rule.rs
+++ b/widget/src/rule.rs
@@ -1,6 +1,6 @@
//! Display a horizontal or vertical rule for dividing content.
use crate::core;
-use crate::core::border::{self, Border};
+use crate::core::border;
use crate::core::layout;
use crate::core::mouse;
use crate::core::renderer;
@@ -132,7 +132,7 @@ where
renderer.fill_quad(
renderer::Quad {
bounds,
- border: Border::rounded(style.radius),
+ border: border::rounded(style.radius),
..renderer::Quad::default()
},
style.color,
diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs
index c3d08223..af6a3945 100644
--- a/widget/src/scrollable.rs
+++ b/widget/src/scrollable.rs
@@ -1,21 +1,24 @@
//! Navigate an endless amount of content with a scrollbar.
-// use crate::container;
use crate::container;
+use crate::core::border::{self, Border};
use crate::core::event::{self, Event};
use crate::core::keyboard;
use crate::core::layout;
use crate::core::mouse;
use crate::core::overlay;
use crate::core::renderer;
+use crate::core::time::{Duration, Instant};
use crate::core::touch;
use crate::core::widget;
use crate::core::widget::operation::{self, Operation};
use crate::core::widget::tree::{self, Tree};
+use crate::core::window;
use crate::core::{
- self, Background, Border, Clipboard, Color, Element, Layout, Length,
+ self, Background, Clipboard, Color, Element, Layout, Length, Padding,
Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget,
};
-use crate::runtime::{Action, Task};
+use crate::runtime::task::{self, Task};
+use crate::runtime::Action;
pub use operation::scrollable::{AbsoluteOffset, RelativeOffset};
@@ -52,34 +55,51 @@ where
Self::with_direction(content, Direction::default())
}
- /// Creates a new [`Scrollable`] with the given [`Direction`].
+ /// Creates a new vertical [`Scrollable`].
pub fn with_direction(
content: impl Into<Element<'a, Message, Theme, Renderer>>,
- direction: Direction,
+ direction: impl Into<Direction>,
) -> Self {
- let content = content.into();
+ Scrollable {
+ id: None,
+ width: Length::Shrink,
+ height: Length::Shrink,
+ direction: direction.into(),
+ content: content.into(),
+ on_scroll: None,
+ class: Theme::default(),
+ }
+ .validate()
+ }
+
+ fn validate(mut self) -> Self {
+ let size_hint = self.content.as_widget().size_hint();
debug_assert!(
- direction.vertical().is_none()
- || !content.as_widget().size_hint().height.is_fill(),
+ self.direction.vertical().is_none() || !size_hint.height.is_fill(),
"scrollable content must not fill its vertical scrolling axis"
);
debug_assert!(
- direction.horizontal().is_none()
- || !content.as_widget().size_hint().width.is_fill(),
+ self.direction.horizontal().is_none() || !size_hint.width.is_fill(),
"scrollable content must not fill its horizontal scrolling axis"
);
- Scrollable {
- id: None,
- width: Length::Shrink,
- height: Length::Shrink,
- direction,
- content,
- on_scroll: None,
- class: Theme::default(),
+ if self.direction.horizontal().is_none() {
+ self.width = self.width.enclose(size_hint.width);
}
+
+ if self.direction.vertical().is_none() {
+ self.height = self.height.enclose(size_hint.height);
+ }
+
+ self
+ }
+
+ /// Creates a new [`Scrollable`] with the given [`Direction`].
+ pub fn direction(mut self, direction: impl Into<Direction>) -> Self {
+ self.direction = direction.into();
+ self.validate()
}
/// Sets the [`Id`] of the [`Scrollable`].
@@ -108,6 +128,69 @@ where
self
}
+ /// Anchors the vertical [`Scrollable`] direction to the top.
+ pub fn anchor_top(self) -> Self {
+ self.anchor_y(Anchor::Start)
+ }
+
+ /// Anchors the vertical [`Scrollable`] direction to the bottom.
+ pub fn anchor_bottom(self) -> Self {
+ self.anchor_y(Anchor::End)
+ }
+
+ /// Anchors the horizontal [`Scrollable`] direction to the left.
+ pub fn anchor_left(self) -> Self {
+ self.anchor_x(Anchor::Start)
+ }
+
+ /// Anchors the horizontal [`Scrollable`] direction to the right.
+ pub fn anchor_right(self) -> Self {
+ self.anchor_x(Anchor::End)
+ }
+
+ /// Sets the [`Anchor`] of the horizontal direction of the [`Scrollable`], if applicable.
+ pub fn anchor_x(mut self, alignment: Anchor) -> Self {
+ match &mut self.direction {
+ Direction::Horizontal(horizontal)
+ | Direction::Both { horizontal, .. } => {
+ horizontal.alignment = alignment;
+ }
+ Direction::Vertical { .. } => {}
+ }
+
+ self
+ }
+
+ /// Sets the [`Anchor`] of the vertical direction of the [`Scrollable`], if applicable.
+ pub fn anchor_y(mut self, alignment: Anchor) -> Self {
+ match &mut self.direction {
+ Direction::Vertical(vertical)
+ | Direction::Both { vertical, .. } => {
+ vertical.alignment = alignment;
+ }
+ Direction::Horizontal { .. } => {}
+ }
+
+ self
+ }
+
+ /// Embeds the [`Scrollbar`] into the [`Scrollable`], instead of floating on top of the
+ /// content.
+ ///
+ /// The `spacing` provided will be used as space between the [`Scrollbar`] and the contents
+ /// of the [`Scrollable`].
+ pub fn spacing(mut self, new_spacing: impl Into<Pixels>) -> Self {
+ match &mut self.direction {
+ Direction::Horizontal(scrollbar)
+ | Direction::Vertical(scrollbar) => {
+ scrollbar.spacing = Some(new_spacing.into().0);
+ }
+ Direction::Both { .. } => {}
+ }
+
+ self
+ }
+
/// Sets the style of this [`Scrollable`].
#[must_use]
pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
@@ -131,102 +214,133 @@ where
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Direction {
/// Vertical scrolling
- Vertical(Properties),
+ Vertical(Scrollbar),
/// Horizontal scrolling
- Horizontal(Properties),
+ Horizontal(Scrollbar),
/// Both vertical and horizontal scrolling
Both {
/// The properties of the vertical scrollbar.
- vertical: Properties,
+ vertical: Scrollbar,
/// The properties of the horizontal scrollbar.
- horizontal: Properties,
+ horizontal: Scrollbar,
},
}
impl Direction {
- /// Returns the [`Properties`] of the horizontal scrollbar, if any.
- pub fn horizontal(&self) -> Option<&Properties> {
+ /// Returns the horizontal [`Scrollbar`], if any.
+ pub fn horizontal(&self) -> Option<&Scrollbar> {
match self {
- Self::Horizontal(properties) => Some(properties),
+ Self::Horizontal(scrollbar) => Some(scrollbar),
Self::Both { horizontal, .. } => Some(horizontal),
Self::Vertical(_) => None,
}
}
- /// Returns the [`Properties`] of the vertical scrollbar, if any.
- pub fn vertical(&self) -> Option<&Properties> {
+ /// Returns the vertical [`Scrollbar`], if any.
+ pub fn vertical(&self) -> Option<&Scrollbar> {
match self {
- Self::Vertical(properties) => Some(properties),
+ Self::Vertical(scrollbar) => Some(scrollbar),
Self::Both { vertical, .. } => Some(vertical),
Self::Horizontal(_) => None,
}
}
+
+ fn align(&self, delta: Vector) -> Vector {
+ let horizontal_alignment =
+ self.horizontal().map(|p| p.alignment).unwrap_or_default();
+
+ let vertical_alignment =
+ self.vertical().map(|p| p.alignment).unwrap_or_default();
+
+ let align = |alignment: Anchor, delta: f32| match alignment {
+ Anchor::Start => delta,
+ Anchor::End => -delta,
+ };
+
+ Vector::new(
+ align(horizontal_alignment, delta.x),
+ align(vertical_alignment, delta.y),
+ )
+ }
}
impl Default for Direction {
fn default() -> Self {
- Self::Vertical(Properties::default())
+ Self::Vertical(Scrollbar::default())
}
}
-/// Properties of a scrollbar within a [`Scrollable`].
+/// A scrollbar within a [`Scrollable`].
#[derive(Debug, Clone, Copy, PartialEq)]
-pub struct Properties {
+pub struct Scrollbar {
width: f32,
margin: f32,
scroller_width: f32,
- alignment: Alignment,
+ alignment: Anchor,
+ spacing: Option<f32>,
}
-impl Default for Properties {
+impl Default for Scrollbar {
fn default() -> Self {
Self {
width: 10.0,
margin: 0.0,
scroller_width: 10.0,
- alignment: Alignment::Start,
+ alignment: Anchor::Start,
+ spacing: None,
}
}
}
-impl Properties {
- /// Creates new [`Properties`] for use in a [`Scrollable`].
+impl Scrollbar {
+ /// Creates new [`Scrollbar`] for use in a [`Scrollable`].
pub fn new() -> Self {
Self::default()
}
- /// Sets the scrollbar width of the [`Scrollable`] .
+ /// Sets the scrollbar width of the [`Scrollbar`] .
pub fn width(mut self, width: impl Into<Pixels>) -> Self {
self.width = width.into().0.max(0.0);
self
}
- /// Sets the scrollbar margin of the [`Scrollable`] .
+ /// Sets the scrollbar margin of the [`Scrollbar`] .
pub fn margin(mut self, margin: impl Into<Pixels>) -> Self {
self.margin = margin.into().0;
self
}
- /// Sets the scroller width of the [`Scrollable`] .
+ /// Sets the scroller width of the [`Scrollbar`] .
pub fn scroller_width(mut self, scroller_width: impl Into<Pixels>) -> Self {
self.scroller_width = scroller_width.into().0.max(0.0);
self
}
- /// Sets the alignment of the [`Scrollable`] .
- pub fn alignment(mut self, alignment: Alignment) -> Self {
+ /// Sets the [`Anchor`] of the [`Scrollbar`] .
+ pub fn anchor(mut self, alignment: Anchor) -> Self {
self.alignment = alignment;
self
}
+
+ /// Sets whether the [`Scrollbar`] should be embedded in the [`Scrollable`], using
+ /// the given spacing between itself and the contents.
+ ///
+ /// An embedded [`Scrollbar`] will always be displayed, will take layout space,
+ /// and will not float over the contents.
+ pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
+ self.spacing = Some(spacing.into().0);
+ self
+ }
}
-/// Alignment of the scrollable's content relative to it's [`Viewport`] in one direction.
+/// The anchor of the scroller of the [`Scrollable`] relative to its [`Viewport`]
+/// on a given axis.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
-pub enum Alignment {
- /// Content is aligned to the start of the [`Viewport`].
+pub enum Anchor {
+ /// Scroller is anchoer to the start of the [`Viewport`].
#[default]
Start,
- /// Content is aligned to the end of the [`Viewport`]
+ /// Content is aligned to the end of the [`Viewport`].
End,
}
@@ -265,29 +379,55 @@ where
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- layout::contained(limits, self.width, self.height, |limits| {
- let child_limits = layout::Limits::new(
- Size::new(limits.min().width, limits.min().height),
- Size::new(
- if self.direction.horizontal().is_some() {
- f32::INFINITY
- } else {
- limits.max().width
- },
- if self.direction.vertical().is_some() {
- f32::MAX
- } else {
- limits.max().height
- },
- ),
- );
+ let (right_padding, bottom_padding) = match self.direction {
+ Direction::Vertical(Scrollbar {
+ width,
+ margin,
+ spacing: Some(spacing),
+ ..
+ }) => (width + margin * 2.0 + spacing, 0.0),
+ Direction::Horizontal(Scrollbar {
+ width,
+ margin,
+ spacing: Some(spacing),
+ ..
+ }) => (0.0, width + margin * 2.0 + spacing),
+ _ => (0.0, 0.0),
+ };
- self.content.as_widget().layout(
- &mut tree.children[0],
- renderer,
- &child_limits,
- )
- })
+ layout::padded(
+ limits,
+ self.width,
+ self.height,
+ Padding {
+ right: right_padding,
+ bottom: bottom_padding,
+ ..Padding::ZERO
+ },
+ |limits| {
+ let child_limits = layout::Limits::new(
+ Size::new(limits.min().width, limits.min().height),
+ Size::new(
+ if self.direction.horizontal().is_some() {
+ f32::INFINITY
+ } else {
+ limits.max().width
+ },
+ if self.direction.vertical().is_some() {
+ f32::MAX
+ } else {
+ limits.max().height
+ },
+ ),
+ );
+
+ self.content.as_widget().layout(
+ &mut tree.children[0],
+ renderer,
+ &child_limits,
+ )
+ },
+ )
}
fn operate(
@@ -295,7 +435,7 @@ where
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
- operation: &mut dyn Operation<()>,
+ operation: &mut dyn Operation,
) {
let state = tree.state.downcast_mut::<State>();
@@ -309,6 +449,7 @@ where
state,
self.id.as_ref().map(|id| &id.0),
bounds,
+ content_bounds,
translation,
);
@@ -350,6 +491,24 @@ where
let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) =
scrollbars.is_mouse_over(cursor);
+ if let Some(last_scrolled) = state.last_scrolled {
+ let clear_transaction = match event {
+ Event::Mouse(
+ mouse::Event::ButtonPressed(_)
+ | mouse::Event::ButtonReleased(_)
+ | mouse::Event::CursorLeft,
+ ) => true,
+ Event::Mouse(mouse::Event::CursorMoved { .. }) => {
+ last_scrolled.elapsed() > Duration::from_millis(100)
+ }
+ _ => last_scrolled.elapsed() > Duration::from_millis(1500),
+ };
+
+ if clear_transaction {
+ state.last_scrolled = None;
+ }
+ }
+
if let Some(scroller_grabbed_at) = state.y_scroller_grabbed_at {
match event {
Event::Mouse(mouse::Event::CursorMoved { .. })
@@ -368,7 +527,7 @@ where
content_bounds,
);
- let _ = notify_on_scroll(
+ let _ = notify_scroll(
state,
&self.on_scroll,
bounds,
@@ -406,7 +565,7 @@ where
state.y_scroller_grabbed_at = Some(scroller_grabbed_at);
- let _ = notify_on_scroll(
+ let _ = notify_scroll(
state,
&self.on_scroll,
bounds,
@@ -439,7 +598,7 @@ where
content_bounds,
);
- let _ = notify_on_scroll(
+ let _ = notify_scroll(
state,
&self.on_scroll,
bounds,
@@ -477,7 +636,7 @@ where
state.x_scroller_grabbed_at = Some(scroller_grabbed_at);
- let _ = notify_on_scroll(
+ let _ = notify_scroll(
state,
&self.on_scroll,
bounds,
@@ -492,7 +651,11 @@ where
}
}
- let mut event_status = {
+ let content_status = if state.last_scrolled.is_some()
+ && matches!(event, Event::Mouse(mouse::Event::WheelScrolled { .. }))
+ {
+ event::Status::Ignored
+ } else {
let cursor = match cursor_over_scrollable {
Some(cursor_position)
if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) =>
@@ -540,10 +703,10 @@ where
state.x_scroller_grabbed_at = None;
state.y_scroller_grabbed_at = None;
- return event_status;
+ return content_status;
}
- if let event::Status::Captured = event_status {
+ if let event::Status::Captured = content_status {
return event::Status::Captured;
}
@@ -563,23 +726,41 @@ where
let delta = match delta {
mouse::ScrollDelta::Lines { x, y } => {
- // TODO: Configurable speed/friction (?)
- let movement = if !cfg!(target_os = "macos") // macOS automatically inverts the axes when Shift is pressed
- && state.keyboard_modifiers.shift()
- {
- Vector::new(y, x)
- } else {
+ let is_shift_pressed = state.keyboard_modifiers.shift();
+
+ // macOS automatically inverts the axes when Shift is pressed
+ let (x, y) =
+ if cfg!(target_os = "macos") && is_shift_pressed {
+ (y, x)
+ } else {
+ (x, y)
+ };
+
+ let is_vertical = match self.direction {
+ Direction::Vertical(_) => true,
+ Direction::Horizontal(_) => false,
+ Direction::Both { .. } => !is_shift_pressed,
+ };
+
+ let movement = if is_vertical {
Vector::new(x, y)
+ } else {
+ Vector::new(y, x)
};
- movement * 60.0
+ // TODO: Configurable speed/friction (?)
+ -movement * 60.0
}
mouse::ScrollDelta::Pixels { x, y } => Vector::new(x, y),
};
- state.scroll(delta, self.direction, bounds, content_bounds);
+ state.scroll(
+ self.direction.align(delta),
+ bounds,
+ content_bounds,
+ );
- event_status = if notify_on_scroll(
+ if notify_scroll(
state,
&self.on_scroll,
bounds,
@@ -589,7 +770,7 @@ where
event::Status::Captured
} else {
event::Status::Ignored
- };
+ }
}
Event::Touch(event)
if state.scroll_area_touched_at.is_some()
@@ -613,13 +794,12 @@ where
};
let delta = Vector::new(
- cursor_position.x - scroll_box_touched_at.x,
- cursor_position.y - scroll_box_touched_at.y,
+ scroll_box_touched_at.x - cursor_position.x,
+ scroll_box_touched_at.y - cursor_position.y,
);
state.scroll(
- delta,
- self.direction,
+ self.direction.align(delta),
bounds,
content_bounds,
);
@@ -628,7 +808,7 @@ where
Some(cursor_position);
// TODO: bubble up touch movements if not consumed.
- let _ = notify_on_scroll(
+ let _ = notify_scroll(
state,
&self.on_scroll,
bounds,
@@ -640,12 +820,21 @@ where
_ => {}
}
- event_status = event::Status::Captured;
+ event::Status::Captured
}
- _ => {}
- }
+ Event::Window(window::Event::RedrawRequested(_)) => {
+ let _ = notify_viewport(
+ state,
+ &self.on_scroll,
+ bounds,
+ content_bounds,
+ shell,
+ );
- event_status
+ event::Status::Ignored
+ }
+ _ => event::Status::Ignored,
+ }
}
fn draw(
@@ -736,7 +925,7 @@ where
let draw_scrollbar =
|renderer: &mut Renderer,
- style: Scrollbar,
+ style: Rail,
scrollbar: &internals::Scrollbar| {
if scrollbar.bounds.width > 0.0
&& scrollbar.bounds.height > 0.0
@@ -756,21 +945,23 @@ where
);
}
- if scrollbar.scroller.bounds.width > 0.0
- && scrollbar.scroller.bounds.height > 0.0
- && (style.scroller.color != Color::TRANSPARENT
- || (style.scroller.border.color
- != Color::TRANSPARENT
- && style.scroller.border.width > 0.0))
- {
- renderer.fill_quad(
- renderer::Quad {
- bounds: scrollbar.scroller.bounds,
- border: style.scroller.border,
- ..renderer::Quad::default()
- },
- style.scroller.color,
- );
+ if let Some(scroller) = scrollbar.scroller {
+ if scroller.bounds.width > 0.0
+ && scroller.bounds.height > 0.0
+ && (style.scroller.color != Color::TRANSPARENT
+ || (style.scroller.border.color
+ != Color::TRANSPARENT
+ && style.scroller.border.width > 0.0))
+ {
+ renderer.fill_quad(
+ renderer::Quad {
+ bounds: scroller.bounds,
+ border: style.scroller.border,
+ ..renderer::Quad::default()
+ },
+ style.scroller.color,
+ );
+ }
}
};
@@ -784,7 +975,7 @@ where
if let Some(scrollbar) = scrollbars.y {
draw_scrollbar(
renderer,
- style.vertical_scrollbar,
+ style.vertical_rail,
&scrollbar,
);
}
@@ -792,7 +983,7 @@ where
if let Some(scrollbar) = scrollbars.x {
draw_scrollbar(
renderer,
- style.horizontal_scrollbar,
+ style.horizontal_rail,
&scrollbar,
);
}
@@ -953,21 +1144,44 @@ impl From<Id> for widget::Id {
}
/// Produces a [`Task`] that snaps the [`Scrollable`] with the given [`Id`]
-/// to the provided `percentage` along the x & y axis.
+/// to the provided [`RelativeOffset`].
pub fn snap_to<T>(id: Id, offset: RelativeOffset) -> Task<T> {
- Task::effect(Action::widget(operation::scrollable::snap_to(id.0, offset)))
+ task::effect(Action::widget(operation::scrollable::snap_to(id.0, offset)))
}
/// Produces a [`Task`] that scrolls the [`Scrollable`] with the given [`Id`]
-/// to the provided [`AbsoluteOffset`] along the x & y axis.
+/// to the provided [`AbsoluteOffset`].
pub fn scroll_to<T>(id: Id, offset: AbsoluteOffset) -> Task<T> {
- Task::effect(Action::widget(operation::scrollable::scroll_to(
+ task::effect(Action::widget(operation::scrollable::scroll_to(
id.0, offset,
)))
}
-/// Returns [`true`] if the viewport actually changed.
-fn notify_on_scroll<Message>(
+/// Produces a [`Task`] that scrolls the [`Scrollable`] with the given [`Id`]
+/// by the provided [`AbsoluteOffset`].
+pub fn scroll_by<T>(id: Id, offset: AbsoluteOffset) -> Task<T> {
+ task::effect(Action::widget(operation::scrollable::scroll_by(
+ id.0, offset,
+ )))
+}
+
+fn notify_scroll<Message>(
+ state: &mut State,
+ on_scroll: &Option<Box<dyn Fn(Viewport) -> Message + '_>>,
+ bounds: Rectangle,
+ content_bounds: Rectangle,
+ shell: &mut Shell<'_, Message>,
+) -> bool {
+ if notify_viewport(state, on_scroll, bounds, content_bounds, shell) {
+ state.last_scrolled = Some(Instant::now());
+
+ true
+ } else {
+ false
+ }
+}
+
+fn notify_viewport<Message>(
state: &mut State,
on_scroll: &Option<Box<dyn Fn(Viewport) -> Message + '_>>,
bounds: Rectangle,
@@ -980,6 +1194,11 @@ fn notify_on_scroll<Message>(
return false;
}
+ let Some(on_scroll) = on_scroll else {
+ state.last_notified = None;
+ return false;
+ };
+
let viewport = Viewport {
offset_x: state.offset_x,
offset_y: state.offset_y,
@@ -999,7 +1218,9 @@ fn notify_on_scroll<Message>(
(a - b).abs() <= f32::EPSILON || (a.is_nan() && b.is_nan())
};
- if unchanged(last_relative_offset.x, current_relative_offset.x)
+ if last_notified.bounds == bounds
+ && last_notified.content_bounds == content_bounds
+ && unchanged(last_relative_offset.x, current_relative_offset.x)
&& unchanged(last_relative_offset.y, current_relative_offset.y)
&& unchanged(last_absolute_offset.x, current_absolute_offset.x)
&& unchanged(last_absolute_offset.y, current_absolute_offset.y)
@@ -1008,9 +1229,7 @@ fn notify_on_scroll<Message>(
}
}
- if let Some(on_scroll) = on_scroll {
- shell.publish(on_scroll(viewport));
- }
+ shell.publish(on_scroll(viewport));
state.last_notified = Some(viewport);
true
@@ -1025,6 +1244,7 @@ struct State {
x_scroller_grabbed_at: Option<f32>,
keyboard_modifiers: keyboard::Modifiers,
last_notified: Option<Viewport>,
+ last_scrolled: Option<Instant>,
}
impl Default for State {
@@ -1037,6 +1257,7 @@ impl Default for State {
x_scroller_grabbed_at: None,
keyboard_modifiers: keyboard::Modifiers::default(),
last_notified: None,
+ last_scrolled: None,
}
}
}
@@ -1049,6 +1270,15 @@ impl operation::Scrollable for State {
fn scroll_to(&mut self, offset: AbsoluteOffset) {
State::scroll_to(self, offset);
}
+
+ fn scroll_by(
+ &mut self,
+ offset: AbsoluteOffset,
+ bounds: Rectangle,
+ content_bounds: Rectangle,
+ ) {
+ State::scroll_by(self, offset, bounds, content_bounds);
+ }
}
#[derive(Debug, Clone, Copy)]
@@ -1073,13 +1303,13 @@ impl Offset {
self,
viewport: f32,
content: f32,
- alignment: Alignment,
+ alignment: Anchor,
) -> f32 {
let offset = self.absolute(viewport, content);
match alignment {
- Alignment::Start => offset,
- Alignment::End => ((content - viewport).max(0.0) - offset).max(0.0),
+ Anchor::Start => offset,
+ Anchor::End => ((content - viewport).max(0.0) - offset).max(0.0),
}
}
}
@@ -1152,34 +1382,13 @@ impl State {
pub fn scroll(
&mut self,
delta: Vector<f32>,
- direction: Direction,
bounds: Rectangle,
content_bounds: Rectangle,
) {
- let horizontal_alignment = direction
- .horizontal()
- .map(|p| p.alignment)
- .unwrap_or_default();
-
- let vertical_alignment = direction
- .vertical()
- .map(|p| p.alignment)
- .unwrap_or_default();
-
- let align = |alignment: Alignment, delta: f32| match alignment {
- Alignment::Start => delta,
- Alignment::End => -delta,
- };
-
- let delta = Vector::new(
- align(horizontal_alignment, delta.x),
- align(vertical_alignment, delta.y),
- );
-
if bounds.height < content_bounds.height {
self.offset_y = Offset::Absolute(
(self.offset_y.absolute(bounds.height, content_bounds.height)
- - delta.y)
+ + delta.y)
.clamp(0.0, content_bounds.height - bounds.height),
);
}
@@ -1187,7 +1396,7 @@ impl State {
if bounds.width < content_bounds.width {
self.offset_x = Offset::Absolute(
(self.offset_x.absolute(bounds.width, content_bounds.width)
- - delta.x)
+ + delta.x)
.clamp(0.0, content_bounds.width - bounds.width),
);
}
@@ -1233,6 +1442,16 @@ impl State {
self.offset_y = Offset::Absolute(offset.y.max(0.0));
}
+ /// Scroll by the provided [`AbsoluteOffset`].
+ pub fn scroll_by(
+ &mut self,
+ offset: AbsoluteOffset,
+ bounds: Rectangle,
+ content_bounds: Rectangle,
+ ) {
+ self.scroll(Vector::new(offset.x, offset.y), bounds, content_bounds);
+ }
+
/// Unsnaps the current scroll position, if snapped, given the bounds of the
/// [`Scrollable`] and its contents.
pub fn unsnap(&mut self, bounds: Rectangle, content_bounds: Rectangle) {
@@ -1298,16 +1517,16 @@ impl Scrollbars {
) -> Self {
let translation = state.translation(direction, bounds, content_bounds);
- let show_scrollbar_x = direction
- .horizontal()
- .filter(|_| content_bounds.width > bounds.width);
+ let show_scrollbar_x = direction.horizontal().filter(|scrollbar| {
+ scrollbar.spacing.is_some() || content_bounds.width > bounds.width
+ });
- let show_scrollbar_y = direction
- .vertical()
- .filter(|_| content_bounds.height > bounds.height);
+ let show_scrollbar_y = direction.vertical().filter(|scrollbar| {
+ scrollbar.spacing.is_some() || content_bounds.height > bounds.height
+ });
let y_scrollbar = if let Some(vertical) = show_scrollbar_y {
- let Properties {
+ let Scrollbar {
width,
margin,
scroller_width,
@@ -1341,26 +1560,35 @@ impl Scrollbars {
};
let ratio = bounds.height / content_bounds.height;
- // min height for easier grabbing with super tall content
- let scroller_height = (scrollbar_bounds.height * ratio).max(2.0);
- let scroller_offset =
- translation.y * ratio * scrollbar_bounds.height / bounds.height;
- let scroller_bounds = Rectangle {
- x: bounds.x + bounds.width
- - total_scrollbar_width / 2.0
- - scroller_width / 2.0,
- y: (scrollbar_bounds.y + scroller_offset).max(0.0),
- width: scroller_width,
- height: scroller_height,
+ let scroller = if ratio >= 1.0 {
+ None
+ } else {
+ // min height for easier grabbing with super tall content
+ let scroller_height =
+ (scrollbar_bounds.height * ratio).max(2.0);
+ let scroller_offset =
+ translation.y * ratio * scrollbar_bounds.height
+ / bounds.height;
+
+ let scroller_bounds = Rectangle {
+ x: bounds.x + bounds.width
+ - total_scrollbar_width / 2.0
+ - scroller_width / 2.0,
+ y: (scrollbar_bounds.y + scroller_offset).max(0.0),
+ width: scroller_width,
+ height: scroller_height,
+ };
+
+ Some(internals::Scroller {
+ bounds: scroller_bounds,
+ })
};
Some(internals::Scrollbar {
total_bounds: total_scrollbar_bounds,
bounds: scrollbar_bounds,
- scroller: internals::Scroller {
- bounds: scroller_bounds,
- },
+ scroller,
alignment: vertical.alignment,
})
} else {
@@ -1368,7 +1596,7 @@ impl Scrollbars {
};
let x_scrollbar = if let Some(horizontal) = show_scrollbar_x {
- let Properties {
+ let Scrollbar {
width,
margin,
scroller_width,
@@ -1402,26 +1630,34 @@ impl Scrollbars {
};
let ratio = bounds.width / content_bounds.width;
- // min width for easier grabbing with extra wide content
- let scroller_length = (scrollbar_bounds.width * ratio).max(2.0);
- let scroller_offset =
- translation.x * ratio * scrollbar_bounds.width / bounds.width;
- let scroller_bounds = Rectangle {
- x: (scrollbar_bounds.x + scroller_offset).max(0.0),
- y: bounds.y + bounds.height
- - total_scrollbar_height / 2.0
- - scroller_width / 2.0,
- width: scroller_length,
- height: scroller_width,
+ let scroller = if ratio >= 1.0 {
+ None
+ } else {
+ // min width for easier grabbing with extra wide content
+ let scroller_length = (scrollbar_bounds.width * ratio).max(2.0);
+ let scroller_offset =
+ translation.x * ratio * scrollbar_bounds.width
+ / bounds.width;
+
+ let scroller_bounds = Rectangle {
+ x: (scrollbar_bounds.x + scroller_offset).max(0.0),
+ y: bounds.y + bounds.height
+ - total_scrollbar_height / 2.0
+ - scroller_width / 2.0,
+ width: scroller_length,
+ height: scroller_width,
+ };
+
+ Some(internals::Scroller {
+ bounds: scroller_bounds,
+ })
};
Some(internals::Scrollbar {
total_bounds: total_scrollbar_bounds,
bounds: scrollbar_bounds,
- scroller: internals::Scroller {
- bounds: scroller_bounds,
- },
+ scroller,
alignment: horizontal.alignment,
})
} else {
@@ -1452,33 +1688,33 @@ impl Scrollbars {
}
fn grab_y_scroller(&self, cursor_position: Point) -> Option<f32> {
- self.y.and_then(|scrollbar| {
- if scrollbar.total_bounds.contains(cursor_position) {
- Some(if scrollbar.scroller.bounds.contains(cursor_position) {
- (cursor_position.y - scrollbar.scroller.bounds.y)
- / scrollbar.scroller.bounds.height
- } else {
- 0.5
- })
+ let scrollbar = self.y?;
+ let scroller = scrollbar.scroller?;
+
+ if scrollbar.total_bounds.contains(cursor_position) {
+ Some(if scroller.bounds.contains(cursor_position) {
+ (cursor_position.y - scroller.bounds.y) / scroller.bounds.height
} else {
- None
- }
- })
+ 0.5
+ })
+ } else {
+ None
+ }
}
fn grab_x_scroller(&self, cursor_position: Point) -> Option<f32> {
- self.x.and_then(|scrollbar| {
- if scrollbar.total_bounds.contains(cursor_position) {
- Some(if scrollbar.scroller.bounds.contains(cursor_position) {
- (cursor_position.x - scrollbar.scroller.bounds.x)
- / scrollbar.scroller.bounds.width
- } else {
- 0.5
- })
+ let scrollbar = self.x?;
+ let scroller = scrollbar.scroller?;
+
+ if scrollbar.total_bounds.contains(cursor_position) {
+ Some(if scroller.bounds.contains(cursor_position) {
+ (cursor_position.x - scroller.bounds.x) / scroller.bounds.width
} else {
- None
- }
- })
+ 0.5
+ })
+ } else {
+ None
+ }
}
fn active(&self) -> bool {
@@ -1489,14 +1725,14 @@ impl Scrollbars {
pub(super) mod internals {
use crate::core::{Point, Rectangle};
- use super::Alignment;
+ use super::Anchor;
#[derive(Debug, Copy, Clone)]
pub struct Scrollbar {
pub total_bounds: Rectangle,
pub bounds: Rectangle,
- pub scroller: Scroller,
- pub alignment: Alignment,
+ pub scroller: Option<Scroller>,
+ pub alignment: Anchor,
}
impl Scrollbar {
@@ -1511,14 +1747,18 @@ pub(super) mod internals {
grabbed_at: f32,
cursor_position: Point,
) -> f32 {
- let percentage = (cursor_position.y
- - self.bounds.y
- - self.scroller.bounds.height * grabbed_at)
- / (self.bounds.height - self.scroller.bounds.height);
-
- match self.alignment {
- Alignment::Start => percentage,
- Alignment::End => 1.0 - percentage,
+ if let Some(scroller) = self.scroller {
+ let percentage = (cursor_position.y
+ - self.bounds.y
+ - scroller.bounds.height * grabbed_at)
+ / (self.bounds.height - scroller.bounds.height);
+
+ match self.alignment {
+ Anchor::Start => percentage,
+ Anchor::End => 1.0 - percentage,
+ }
+ } else {
+ 0.0
}
}
@@ -1528,14 +1768,18 @@ pub(super) mod internals {
grabbed_at: f32,
cursor_position: Point,
) -> f32 {
- let percentage = (cursor_position.x
- - self.bounds.x
- - self.scroller.bounds.width * grabbed_at)
- / (self.bounds.width - self.scroller.bounds.width);
-
- match self.alignment {
- Alignment::Start => percentage,
- Alignment::End => 1.0 - percentage,
+ if let Some(scroller) = self.scroller {
+ let percentage = (cursor_position.x
+ - self.bounds.x
+ - scroller.bounds.width * grabbed_at)
+ / (self.bounds.width - scroller.bounds.width);
+
+ match self.alignment {
+ Anchor::Start => percentage,
+ Anchor::End => 1.0 - percentage,
+ }
+ } else {
+ 0.0
}
}
}
@@ -1569,22 +1813,22 @@ pub enum Status {
},
}
-/// The appearance of a scrolable.
+/// The appearance of a scrollable.
#[derive(Debug, Clone, Copy)]
pub struct Style {
/// The [`container::Style`] of a scrollable.
pub container: container::Style,
- /// The vertical [`Scrollbar`] appearance.
- pub vertical_scrollbar: Scrollbar,
- /// The horizontal [`Scrollbar`] appearance.
- pub horizontal_scrollbar: Scrollbar,
+ /// The vertical [`Rail`] appearance.
+ pub vertical_rail: Rail,
+ /// The horizontal [`Rail`] appearance.
+ pub horizontal_rail: Rail,
/// The [`Background`] of the gap between a horizontal and vertical scrollbar.
pub gap: Option<Background>,
}
/// The appearance of the scrollbar of a scrollable.
#[derive(Debug, Clone, Copy)]
-pub struct Scrollbar {
+pub struct Rail {
/// The [`Background`] of a scrollbar.
pub background: Option<Background>,
/// The [`Border`] of a scrollbar.
@@ -1633,27 +1877,27 @@ impl Catalog for Theme {
pub fn default(theme: &Theme, status: Status) -> Style {
let palette = theme.extended_palette();
- let scrollbar = Scrollbar {
+ let scrollbar = Rail {
background: Some(palette.background.weak.color.into()),
- border: Border::rounded(2),
+ border: border::rounded(2),
scroller: Scroller {
color: palette.background.strong.color,
- border: Border::rounded(2),
+ border: border::rounded(2),
},
};
match status {
Status::Active => Style {
container: container::Style::default(),
- vertical_scrollbar: scrollbar,
- horizontal_scrollbar: scrollbar,
+ vertical_rail: scrollbar,
+ horizontal_rail: scrollbar,
gap: None,
},
Status::Hovered {
is_horizontal_scrollbar_hovered,
is_vertical_scrollbar_hovered,
} => {
- let hovered_scrollbar = Scrollbar {
+ let hovered_scrollbar = Rail {
scroller: Scroller {
color: palette.primary.strong.color,
..scrollbar.scroller
@@ -1663,12 +1907,12 @@ pub fn default(theme: &Theme, status: Status) -> Style {
Style {
container: container::Style::default(),
- vertical_scrollbar: if is_vertical_scrollbar_hovered {
+ vertical_rail: if is_vertical_scrollbar_hovered {
hovered_scrollbar
} else {
scrollbar
},
- horizontal_scrollbar: if is_horizontal_scrollbar_hovered {
+ horizontal_rail: if is_horizontal_scrollbar_hovered {
hovered_scrollbar
} else {
scrollbar
@@ -1680,7 +1924,7 @@ pub fn default(theme: &Theme, status: Status) -> Style {
is_horizontal_scrollbar_dragged,
is_vertical_scrollbar_dragged,
} => {
- let dragged_scrollbar = Scrollbar {
+ let dragged_scrollbar = Rail {
scroller: Scroller {
color: palette.primary.base.color,
..scrollbar.scroller
@@ -1690,12 +1934,12 @@ pub fn default(theme: &Theme, status: Status) -> Style {
Style {
container: container::Style::default(),
- vertical_scrollbar: if is_vertical_scrollbar_dragged {
+ vertical_rail: if is_vertical_scrollbar_dragged {
dragged_scrollbar
} else {
scrollbar
},
- horizontal_scrollbar: if is_horizontal_scrollbar_dragged {
+ horizontal_rail: if is_horizontal_scrollbar_dragged {
dragged_scrollbar
} else {
scrollbar
diff --git a/widget/src/slider.rs b/widget/src/slider.rs
index a8f1d192..aebf68e2 100644
--- a/widget/src/slider.rs
+++ b/widget/src/slider.rs
@@ -1,5 +1,5 @@
//! Display an interactive selector of a single value from a range of values.
-use crate::core::border;
+use crate::core::border::{self, Border};
use crate::core::event::{self, Event};
use crate::core::keyboard;
use crate::core::keyboard::key::{self, Key};
@@ -9,7 +9,7 @@ use crate::core::renderer;
use crate::core::touch;
use crate::core::widget::tree::{self, Tree};
use crate::core::{
- self, Border, Clipboard, Color, Element, Layout, Length, Pixels, Point,
+ self, Background, Clipboard, Color, Element, Layout, Length, Pixels, Point,
Rectangle, Shell, Size, Theme, Widget,
};
@@ -70,8 +70,8 @@ where
/// * an inclusive range of possible values
/// * the current value of the [`Slider`]
/// * a function that will be called when the [`Slider`] is dragged.
- /// It receives the new value of the [`Slider`] and must produce a
- /// `Message`.
+ /// It receives the new value of the [`Slider`] and must produce a
+ /// `Message`.
pub fn new<F>(range: RangeInclusive<T>, value: T, on_change: F) -> Self
where
F: 'a + Fn(T) -> Message,
@@ -237,7 +237,7 @@ where
let steps = (percent * (end - start) / step).round();
let value = steps * step + start;
- T::from_f64(value)
+ T::from_f64(value.min(end))
};
new_value
@@ -408,10 +408,10 @@ where
width: offset + handle_width / 2.0,
height: style.rail.width,
},
- border: Border::rounded(style.rail.border_radius),
+ border: style.rail.border,
..renderer::Quad::default()
},
- style.rail.colors.0,
+ style.rail.backgrounds.0,
);
renderer.fill_quad(
@@ -422,10 +422,10 @@ where
width: bounds.width - offset - handle_width / 2.0,
height: style.rail.width,
},
- border: Border::rounded(style.rail.border_radius),
+ border: style.rail.border,
..renderer::Quad::default()
},
- style.rail.colors.1,
+ style.rail.backgrounds.1,
);
renderer.fill_quad(
@@ -443,7 +443,7 @@ where
},
..renderer::Quad::default()
},
- style.handle.color,
+ style.handle.background,
);
}
@@ -524,12 +524,12 @@ impl Style {
/// The appearance of a slider rail
#[derive(Debug, Clone, Copy)]
pub struct Rail {
- /// The colors of the rail of the slider.
- pub colors: (Color, Color),
+ /// The backgrounds of the rail of the slider.
+ pub backgrounds: (Background, Background),
/// The width of the stroke of a slider rail.
pub width: f32,
- /// The border radius of the corners of the rail.
- pub border_radius: border::Radius,
+ /// The border of the rail.
+ pub border: Border,
}
/// The appearance of the handle of a slider.
@@ -537,8 +537,8 @@ pub struct Rail {
pub struct Handle {
/// The shape of the handle.
pub shape: HandleShape,
- /// The [`Color`] of the handle.
- pub color: Color,
+ /// The [`Background`] of the handle.
+ pub background: Background,
/// The border width of the handle.
pub border_width: f32,
/// The border [`Color`] of the handle.
@@ -601,13 +601,17 @@ pub fn default(theme: &Theme, status: Status) -> Style {
Style {
rail: Rail {
- colors: (color, palette.secondary.base.color),
+ backgrounds: (color.into(), palette.secondary.base.color.into()),
width: 4.0,
- border_radius: 2.0.into(),
+ border: Border {
+ radius: 2.0.into(),
+ width: 0.0,
+ color: Color::TRANSPARENT,
+ },
},
handle: Handle {
shape: HandleShape::Circle { radius: 7.0 },
- color,
+ background: color.into(),
border_color: Color::TRANSPARENT,
border_width: 0.0,
},
diff --git a/widget/src/stack.rs b/widget/src/stack.rs
index efa9711d..9ccaa274 100644
--- a/widget/src/stack.rs
+++ b/widget/src/stack.rs
@@ -189,7 +189,7 @@ where
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
- operation: &mut dyn Operation<()>,
+ operation: &mut dyn Operation,
) {
operation.container(None, layout.bounds(), &mut |operation| {
self.children
@@ -209,19 +209,23 @@ where
tree: &mut Tree,
event: Event,
layout: Layout<'_>,
- cursor: mouse::Cursor,
+ mut cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) -> event::Status {
+ let is_over_scroll =
+ matches!(event, Event::Mouse(mouse::Event::WheelScrolled { .. }))
+ && cursor.is_over(layout.bounds());
+
self.children
.iter_mut()
.rev()
.zip(tree.children.iter_mut().rev())
.zip(layout.children().rev())
.map(|((child, state), layout)| {
- child.as_widget_mut().on_event(
+ let status = child.as_widget_mut().on_event(
state,
event.clone(),
layout,
@@ -230,7 +234,19 @@ where
clipboard,
shell,
viewport,
- )
+ );
+
+ if is_over_scroll && cursor != mouse::Cursor::Unavailable {
+ let interaction = child.as_widget().mouse_interaction(
+ state, layout, cursor, viewport, renderer,
+ );
+
+ if interaction != mouse::Interaction::None {
+ cursor = mouse::Cursor::Unavailable;
+ }
+ }
+
+ status
})
.find(|&status| status == event::Status::Captured)
.unwrap_or(event::Status::Ignored)
@@ -269,15 +285,53 @@ where
viewport: &Rectangle,
) {
if let Some(clipped_viewport) = layout.bounds().intersection(viewport) {
- for (i, ((layer, state), layout)) in self
+ let layers_below = if cursor.is_over(layout.bounds()) {
+ self.children
+ .iter()
+ .rev()
+ .zip(tree.children.iter().rev())
+ .zip(layout.children().rev())
+ .position(|((layer, state), layout)| {
+ let interaction = layer.as_widget().mouse_interaction(
+ state, layout, cursor, viewport, renderer,
+ );
+
+ interaction != mouse::Interaction::None
+ })
+ .map(|i| self.children.len() - i - 1)
+ .unwrap_or_default()
+ } else {
+ 0
+ };
+
+ let mut layers = self
.children
.iter()
.zip(&tree.children)
.zip(layout.children())
- .enumerate()
- {
- if i > 0 {
- renderer.with_layer(clipped_viewport, |renderer| {
+ .enumerate();
+
+ let layers = layers.by_ref();
+
+ let mut draw_layer =
+ |i,
+ layer: &Element<'a, Message, Theme, Renderer>,
+ state,
+ layout,
+ cursor| {
+ if i > 0 {
+ renderer.with_layer(clipped_viewport, |renderer| {
+ layer.as_widget().draw(
+ state,
+ renderer,
+ theme,
+ style,
+ layout,
+ cursor,
+ &clipped_viewport,
+ );
+ });
+ } else {
layer.as_widget().draw(
state,
renderer,
@@ -287,18 +341,15 @@ where
cursor,
&clipped_viewport,
);
- });
- } else {
- layer.as_widget().draw(
- state,
- renderer,
- theme,
- style,
- layout,
- cursor,
- &clipped_viewport,
- );
- }
+ }
+ };
+
+ for (i, ((layer, state), layout)) in layers.take(layers_below) {
+ draw_layer(i, layer, state, layout, mouse::Cursor::Unavailable);
+ }
+
+ for (i, ((layer, state), layout)) in layers {
+ draw_layer(i, layer, state, layout, cursor);
}
}
}
diff --git a/widget/src/svg.rs b/widget/src/svg.rs
index 4551bcad..bec0090f 100644
--- a/widget/src/svg.rs
+++ b/widget/src/svg.rs
@@ -211,11 +211,13 @@ where
let render = |renderer: &mut Renderer| {
renderer.draw_svg(
- self.handle.clone(),
- style.color,
+ svg::Svg {
+ handle: self.handle.clone(),
+ color: style.color,
+ rotation: self.rotation.radians(),
+ opacity: self.opacity,
+ },
drawing_bounds,
- self.rotation.radians(),
- self.opacity,
);
};
diff --git a/widget/src/text.rs b/widget/src/text.rs
index 0d689295..9bf7fce4 100644
--- a/widget/src/text.rs
+++ b/widget/src/text.rs
@@ -1,5 +1,9 @@
//! Draw and interact with text.
+mod rich;
+
+pub use crate::core::text::{Fragment, Highlighter, IntoFragment, Span};
pub use crate::core::widget::text::*;
+pub use rich::Rich;
/// A paragraph.
pub type Text<'a, Theme = crate::Theme, Renderer = crate::Renderer> =
diff --git a/widget/src/text/rich.rs b/widget/src/text/rich.rs
new file mode 100644
index 00000000..921c55a5
--- /dev/null
+++ b/widget/src/text/rich.rs
@@ -0,0 +1,538 @@
+use crate::core::alignment;
+use crate::core::event;
+use crate::core::layout;
+use crate::core::mouse;
+use crate::core::renderer;
+use crate::core::text::{Paragraph, Span};
+use crate::core::widget::text::{
+ self, Catalog, LineHeight, Shaping, Style, StyleFn, Wrapping,
+};
+use crate::core::widget::tree::{self, Tree};
+use crate::core::{
+ self, Clipboard, Color, Element, Event, Layout, Length, Pixels, Point,
+ Rectangle, Shell, Size, Vector, Widget,
+};
+
+/// A bunch of [`Rich`] text.
+#[allow(missing_debug_implementations)]
+pub struct Rich<'a, Link, Theme = crate::Theme, Renderer = crate::Renderer>
+where
+ Link: Clone + 'static,
+ Theme: Catalog,
+ Renderer: core::text::Renderer,
+{
+ spans: Box<dyn AsRef<[Span<'a, Link, Renderer::Font>]> + 'a>,
+ size: Option<Pixels>,
+ line_height: LineHeight,
+ width: Length,
+ height: Length,
+ font: Option<Renderer::Font>,
+ align_x: alignment::Horizontal,
+ align_y: alignment::Vertical,
+ wrapping: Wrapping,
+ class: Theme::Class<'a>,
+}
+
+impl<'a, Link, Theme, Renderer> Rich<'a, Link, Theme, Renderer>
+where
+ Link: Clone + 'static,
+ Theme: Catalog,
+ Renderer: core::text::Renderer,
+ Renderer::Font: 'a,
+{
+ /// Creates a new empty [`Rich`] text.
+ pub fn new() -> Self {
+ Self {
+ spans: Box::new([]),
+ size: None,
+ line_height: LineHeight::default(),
+ width: Length::Shrink,
+ height: Length::Shrink,
+ font: None,
+ align_x: alignment::Horizontal::Left,
+ align_y: alignment::Vertical::Top,
+ wrapping: Wrapping::default(),
+ class: Theme::default(),
+ }
+ }
+
+ /// Creates a new [`Rich`] text with the given text spans.
+ pub fn with_spans(
+ spans: impl AsRef<[Span<'a, Link, Renderer::Font>]> + 'a,
+ ) -> Self {
+ Self {
+ spans: Box::new(spans),
+ ..Self::new()
+ }
+ }
+
+ /// Sets the default size of the [`Rich`] text.
+ pub fn size(mut self, size: impl Into<Pixels>) -> Self {
+ self.size = Some(size.into());
+ self
+ }
+
+ /// Sets the defualt [`LineHeight`] of the [`Rich`] text.
+ pub fn line_height(mut self, line_height: impl Into<LineHeight>) -> Self {
+ self.line_height = line_height.into();
+ self
+ }
+
+ /// Sets the default font of the [`Rich`] text.
+ pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
+ self.font = Some(font.into());
+ self
+ }
+
+ /// Sets the width of the [`Rich`] text boundaries.
+ pub fn width(mut self, width: impl Into<Length>) -> Self {
+ self.width = width.into();
+ self
+ }
+
+ /// Sets the height of the [`Rich`] text boundaries.
+ pub fn height(mut self, height: impl Into<Length>) -> Self {
+ self.height = height.into();
+ self
+ }
+
+ /// Centers the [`Rich`] text, both horizontally and vertically.
+ pub fn center(self) -> Self {
+ self.align_x(alignment::Horizontal::Center)
+ .align_y(alignment::Vertical::Center)
+ }
+
+ /// Sets the [`alignment::Horizontal`] of the [`Rich`] text.
+ pub fn align_x(
+ mut self,
+ alignment: impl Into<alignment::Horizontal>,
+ ) -> Self {
+ self.align_x = alignment.into();
+ self
+ }
+
+ /// Sets the [`alignment::Vertical`] of the [`Rich`] text.
+ pub fn align_y(
+ mut self,
+ alignment: impl Into<alignment::Vertical>,
+ ) -> Self {
+ self.align_y = alignment.into();
+ self
+ }
+
+ /// Sets the [`Wrapping`] strategy of the [`Rich`] text.
+ pub fn wrapping(mut self, wrapping: Wrapping) -> Self {
+ self.wrapping = wrapping;
+ self
+ }
+
+ /// Sets the default style of the [`Rich`] text.
+ #[must_use]
+ pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
+ where
+ Theme::Class<'a>: From<StyleFn<'a, Theme>>,
+ {
+ self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
+ self
+ }
+
+ /// Sets the default [`Color`] of the [`Rich`] text.
+ pub fn color(self, color: impl Into<Color>) -> Self
+ where
+ Theme::Class<'a>: From<StyleFn<'a, Theme>>,
+ {
+ self.color_maybe(Some(color))
+ }
+
+ /// Sets the default [`Color`] of the [`Rich`] text, if `Some`.
+ pub fn color_maybe(self, color: Option<impl Into<Color>>) -> Self
+ where
+ Theme::Class<'a>: From<StyleFn<'a, Theme>>,
+ {
+ let color = color.map(Into::into);
+
+ self.style(move |_theme| Style { color })
+ }
+
+ /// Sets the default style class of the [`Rich`] text.
+ #[cfg(feature = "advanced")]
+ #[must_use]
+ pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
+ self.class = class.into();
+ self
+ }
+}
+
+impl<'a, Link, Theme, Renderer> Default for Rich<'a, Link, Theme, Renderer>
+where
+ Link: Clone + 'a,
+ Theme: Catalog,
+ Renderer: core::text::Renderer,
+ Renderer::Font: 'a,
+{
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+struct State<Link, P: Paragraph> {
+ spans: Vec<Span<'static, Link, P::Font>>,
+ span_pressed: Option<usize>,
+ paragraph: P,
+}
+
+impl<'a, Link, Theme, Renderer> Widget<Link, Theme, Renderer>
+ for Rich<'a, Link, Theme, Renderer>
+where
+ Link: Clone + 'static,
+ Theme: Catalog,
+ Renderer: core::text::Renderer,
+{
+ fn tag(&self) -> tree::Tag {
+ tree::Tag::of::<State<Link, Renderer::Paragraph>>()
+ }
+
+ fn state(&self) -> tree::State {
+ tree::State::new(State::<Link, _> {
+ spans: Vec::new(),
+ span_pressed: None,
+ paragraph: Renderer::Paragraph::default(),
+ })
+ }
+
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: self.width,
+ height: self.height,
+ }
+ }
+
+ fn layout(
+ &self,
+ tree: &mut Tree,
+ renderer: &Renderer,
+ limits: &layout::Limits,
+ ) -> layout::Node {
+ layout(
+ tree.state
+ .downcast_mut::<State<Link, Renderer::Paragraph>>(),
+ renderer,
+ limits,
+ self.width,
+ self.height,
+ self.spans.as_ref().as_ref(),
+ self.line_height,
+ self.size,
+ self.font,
+ self.align_x,
+ self.align_y,
+ self.wrapping,
+ )
+ }
+
+ fn draw(
+ &self,
+ tree: &Tree,
+ renderer: &mut Renderer,
+ theme: &Theme,
+ defaults: &renderer::Style,
+ layout: Layout<'_>,
+ cursor: mouse::Cursor,
+ viewport: &Rectangle,
+ ) {
+ let state = tree
+ .state
+ .downcast_ref::<State<Link, Renderer::Paragraph>>();
+
+ let style = theme.style(&self.class);
+
+ let hovered_span = cursor
+ .position_in(layout.bounds())
+ .and_then(|position| state.paragraph.hit_span(position));
+
+ for (index, span) in self.spans.as_ref().as_ref().iter().enumerate() {
+ let is_hovered_link =
+ span.link.is_some() && Some(index) == hovered_span;
+
+ if span.highlight.is_some()
+ || span.underline
+ || span.strikethrough
+ || is_hovered_link
+ {
+ let translation = layout.position() - Point::ORIGIN;
+ let regions = state.paragraph.span_bounds(index);
+
+ if let Some(highlight) = span.highlight {
+ for bounds in &regions {
+ let bounds = Rectangle::new(
+ bounds.position()
+ - Vector::new(
+ span.padding.left,
+ span.padding.top,
+ ),
+ bounds.size()
+ + Size::new(
+ span.padding.horizontal(),
+ span.padding.vertical(),
+ ),
+ );
+
+ renderer.fill_quad(
+ renderer::Quad {
+ bounds: bounds + translation,
+ border: highlight.border,
+ ..Default::default()
+ },
+ highlight.background,
+ );
+ }
+ }
+
+ if span.underline || span.strikethrough || is_hovered_link {
+ let size = span
+ .size
+ .or(self.size)
+ .unwrap_or(renderer.default_size());
+
+ let line_height = span
+ .line_height
+ .unwrap_or(self.line_height)
+ .to_absolute(size);
+
+ let color = span
+ .color
+ .or(style.color)
+ .unwrap_or(defaults.text_color);
+
+ let baseline = translation
+ + Vector::new(
+ 0.0,
+ size.0 + (line_height.0 - size.0) / 2.0,
+ );
+
+ if span.underline || is_hovered_link {
+ for bounds in &regions {
+ renderer.fill_quad(
+ renderer::Quad {
+ bounds: Rectangle::new(
+ bounds.position() + baseline
+ - Vector::new(0.0, size.0 * 0.08),
+ Size::new(bounds.width, 1.0),
+ ),
+ ..Default::default()
+ },
+ color,
+ );
+ }
+ }
+
+ if span.strikethrough {
+ for bounds in &regions {
+ renderer.fill_quad(
+ renderer::Quad {
+ bounds: Rectangle::new(
+ bounds.position() + baseline
+ - Vector::new(0.0, size.0 / 2.0),
+ Size::new(bounds.width, 1.0),
+ ),
+ ..Default::default()
+ },
+ color,
+ );
+ }
+ }
+ }
+ }
+ }
+
+ text::draw(
+ renderer,
+ defaults,
+ layout,
+ &state.paragraph,
+ style,
+ viewport,
+ );
+ }
+
+ fn on_event(
+ &mut self,
+ tree: &mut Tree,
+ event: Event,
+ layout: Layout<'_>,
+ cursor: mouse::Cursor,
+ _renderer: &Renderer,
+ _clipboard: &mut dyn Clipboard,
+ shell: &mut Shell<'_, Link>,
+ _viewport: &Rectangle,
+ ) -> event::Status {
+ match event {
+ Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
+ if let Some(position) = cursor.position_in(layout.bounds()) {
+ let state = tree
+ .state
+ .downcast_mut::<State<Link, Renderer::Paragraph>>();
+
+ if let Some(span) = state.paragraph.hit_span(position) {
+ state.span_pressed = Some(span);
+
+ return event::Status::Captured;
+ }
+ }
+ }
+ Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
+ let state = tree
+ .state
+ .downcast_mut::<State<Link, Renderer::Paragraph>>();
+
+ if let Some(span_pressed) = state.span_pressed {
+ state.span_pressed = None;
+
+ if let Some(position) = cursor.position_in(layout.bounds())
+ {
+ match state.paragraph.hit_span(position) {
+ Some(span) if span == span_pressed => {
+ if let Some(link) = self
+ .spans
+ .as_ref()
+ .as_ref()
+ .get(span)
+ .and_then(|span| span.link.clone())
+ {
+ shell.publish(link);
+ }
+ }
+ _ => {}
+ }
+ }
+ }
+ }
+ _ => {}
+ }
+
+ event::Status::Ignored
+ }
+
+ fn mouse_interaction(
+ &self,
+ tree: &Tree,
+ layout: Layout<'_>,
+ cursor: mouse::Cursor,
+ _viewport: &Rectangle,
+ _renderer: &Renderer,
+ ) -> mouse::Interaction {
+ if let Some(position) = cursor.position_in(layout.bounds()) {
+ let state = tree
+ .state
+ .downcast_ref::<State<Link, Renderer::Paragraph>>();
+
+ if let Some(span) = state
+ .paragraph
+ .hit_span(position)
+ .and_then(|span| self.spans.as_ref().as_ref().get(span))
+ {
+ if span.link.is_some() {
+ return mouse::Interaction::Pointer;
+ }
+ }
+ }
+
+ mouse::Interaction::None
+ }
+}
+
+fn layout<Link, Renderer>(
+ state: &mut State<Link, Renderer::Paragraph>,
+ renderer: &Renderer,
+ limits: &layout::Limits,
+ width: Length,
+ height: Length,
+ spans: &[Span<'_, Link, Renderer::Font>],
+ line_height: LineHeight,
+ size: Option<Pixels>,
+ font: Option<Renderer::Font>,
+ horizontal_alignment: alignment::Horizontal,
+ vertical_alignment: alignment::Vertical,
+ wrapping: Wrapping,
+) -> layout::Node
+where
+ Link: Clone,
+ Renderer: core::text::Renderer,
+{
+ layout::sized(limits, width, height, |limits| {
+ let bounds = limits.max();
+
+ let size = size.unwrap_or_else(|| renderer.default_size());
+ let font = font.unwrap_or_else(|| renderer.default_font());
+
+ let text_with_spans = || core::Text {
+ content: spans,
+ bounds,
+ size,
+ line_height,
+ font,
+ horizontal_alignment,
+ vertical_alignment,
+ shaping: Shaping::Advanced,
+ wrapping,
+ };
+
+ if state.spans != spans {
+ state.paragraph =
+ Renderer::Paragraph::with_spans(text_with_spans());
+ state.spans = spans.iter().cloned().map(Span::to_static).collect();
+ } else {
+ match state.paragraph.compare(core::Text {
+ content: (),
+ bounds,
+ size,
+ line_height,
+ font,
+ horizontal_alignment,
+ vertical_alignment,
+ shaping: Shaping::Advanced,
+ wrapping,
+ }) {
+ core::text::Difference::None => {}
+ core::text::Difference::Bounds => {
+ state.paragraph.resize(bounds);
+ }
+ core::text::Difference::Shape => {
+ state.paragraph =
+ Renderer::Paragraph::with_spans(text_with_spans());
+ }
+ }
+ }
+
+ state.paragraph.min_bounds()
+ })
+}
+
+impl<'a, Link, Theme, Renderer> FromIterator<Span<'a, Link, Renderer::Font>>
+ for Rich<'a, Link, Theme, Renderer>
+where
+ Link: Clone + 'a,
+ Theme: Catalog,
+ Renderer: core::text::Renderer,
+ Renderer::Font: 'a,
+{
+ fn from_iter<T: IntoIterator<Item = Span<'a, Link, Renderer::Font>>>(
+ spans: T,
+ ) -> Self {
+ Self::with_spans(spans.into_iter().collect::<Vec<_>>())
+ }
+}
+
+impl<'a, Link, Theme, Renderer> From<Rich<'a, Link, Theme, Renderer>>
+ for Element<'a, Link, Theme, Renderer>
+where
+ Link: Clone + 'a,
+ Theme: Catalog + 'a,
+ Renderer: core::text::Renderer + 'a,
+{
+ fn from(
+ text: Rich<'a, Link, Theme, Renderer>,
+ ) -> Element<'a, Link, Theme, Renderer> {
+ Element::new(text)
+ }
+}
diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs
index fc2ade43..e0102656 100644
--- a/widget/src/text_editor.rs
+++ b/widget/src/text_editor.rs
@@ -1,4 +1,5 @@
//! Display a multi-line text input for text editing.
+use crate::core::alignment;
use crate::core::clipboard::{self, Clipboard};
use crate::core::event::{self, Event};
use crate::core::keyboard;
@@ -8,11 +9,14 @@ use crate::core::mouse;
use crate::core::renderer;
use crate::core::text::editor::{Cursor, Editor as _};
use crate::core::text::highlighter::{self, Highlighter};
-use crate::core::text::{self, LineHeight};
+use crate::core::text::{self, LineHeight, Text, Wrapping};
+use crate::core::time::{Duration, Instant};
+use crate::core::widget::operation;
use crate::core::widget::{self, Widget};
+use crate::core::window;
use crate::core::{
- Background, Border, Color, Element, Length, Padding, Pixels, Rectangle,
- Shell, Size, Theme, Vector,
+ Background, Border, Color, Element, Length, Padding, Pixels, Point,
+ Rectangle, Shell, Size, SmolStr, Theme, Vector,
};
use std::cell::RefCell;
@@ -36,13 +40,16 @@ pub struct TextEditor<
Renderer: text::Renderer,
{
content: &'a Content<Renderer>,
+ placeholder: Option<text::Fragment<'a>>,
font: Option<Renderer::Font>,
text_size: Option<Pixels>,
line_height: LineHeight,
width: Length,
height: Length,
padding: Padding,
+ wrapping: Wrapping,
class: Theme::Class<'a>,
+ key_binding: Option<Box<dyn Fn(KeyPress) -> Option<Binding<Message>> + 'a>>,
on_edit: Option<Box<dyn Fn(Action) -> Message + 'a>>,
highlighter_settings: Highlighter::Settings,
highlighter_format: fn(
@@ -61,13 +68,16 @@ where
pub fn new(content: &'a Content<Renderer>) -> Self {
Self {
content,
+ placeholder: None,
font: None,
text_size: None,
line_height: LineHeight::default(),
width: Length::Fill,
height: Length::Shrink,
padding: Padding::new(5.0),
+ wrapping: Wrapping::default(),
class: Theme::default(),
+ key_binding: None,
on_edit: None,
highlighter_settings: (),
highlighter_format: |_highlight, _theme| {
@@ -84,6 +94,15 @@ where
Theme: Catalog,
Renderer: text::Renderer,
{
+ /// Sets the placeholder of the [`TextEditor`].
+ pub fn placeholder(
+ mut self,
+ placeholder: impl text::IntoFragment<'a>,
+ ) -> Self {
+ self.placeholder = Some(placeholder.into_fragment());
+ self
+ }
+
/// Sets the height of the [`TextEditor`].
pub fn height(mut self, height: impl Into<Length>) -> Self {
self.height = height.into();
@@ -131,9 +150,34 @@ where
self
}
+ /// Sets the [`Wrapping`] strategy of the [`TextEditor`].
+ pub fn wrapping(mut self, wrapping: Wrapping) -> Self {
+ self.wrapping = wrapping;
+ self
+ }
+
+ /// Highlights the [`TextEditor`] using the given syntax and theme.
+ #[cfg(feature = "highlighter")]
+ pub fn highlight(
+ self,
+ syntax: &str,
+ theme: iced_highlighter::Theme,
+ ) -> TextEditor<'a, iced_highlighter::Highlighter, Message, Theme, Renderer>
+ where
+ Renderer: text::Renderer<Font = crate::core::Font>,
+ {
+ self.highlight_with::<iced_highlighter::Highlighter>(
+ iced_highlighter::Settings {
+ theme,
+ token: syntax.to_owned(),
+ },
+ |highlight, _theme| highlight.to_format(),
+ )
+ }
+
/// Highlights the [`TextEditor`] with the given [`Highlighter`] and
/// a strategy to turn its highlights into some text format.
- pub fn highlight<H: text::Highlighter>(
+ pub fn highlight_with<H: text::Highlighter>(
self,
settings: H::Settings,
to_format: fn(
@@ -143,19 +187,33 @@ where
) -> TextEditor<'a, H, Message, Theme, Renderer> {
TextEditor {
content: self.content,
+ placeholder: self.placeholder,
font: self.font,
text_size: self.text_size,
line_height: self.line_height,
width: self.width,
height: self.height,
padding: self.padding,
+ wrapping: self.wrapping,
class: self.class,
+ key_binding: self.key_binding,
on_edit: self.on_edit,
highlighter_settings: settings,
highlighter_format: to_format,
}
}
+ /// Sets the closure to produce key bindings on key presses.
+ ///
+ /// See [`Binding`] for the list of available bindings.
+ pub fn key_binding(
+ mut self,
+ key_binding: impl Fn(KeyPress) -> Option<Binding<Message>> + 'a,
+ ) -> Self {
+ self.key_binding = Some(Box::new(key_binding));
+ self
+ }
+
/// Sets the style of the [`TextEditor`].
#[must_use]
pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
@@ -322,7 +380,7 @@ where
/// The state of a [`TextEditor`].
#[derive(Debug)]
pub struct State<Highlighter: text::Highlighter> {
- is_focused: bool,
+ focus: Option<Focus>,
last_click: Option<mouse::Click>,
drag_click: Option<mouse::click::Kind>,
partial_scroll: f32,
@@ -331,10 +389,55 @@ pub struct State<Highlighter: text::Highlighter> {
highlighter_format_address: usize,
}
+#[derive(Debug, Clone, Copy)]
+struct Focus {
+ updated_at: Instant,
+ now: Instant,
+ is_window_focused: bool,
+}
+
+impl Focus {
+ const CURSOR_BLINK_INTERVAL_MILLIS: u128 = 500;
+
+ fn now() -> Self {
+ let now = Instant::now();
+
+ Self {
+ updated_at: now,
+ now,
+ is_window_focused: true,
+ }
+ }
+
+ fn is_cursor_visible(&self) -> bool {
+ self.is_window_focused
+ && ((self.now - self.updated_at).as_millis()
+ / Self::CURSOR_BLINK_INTERVAL_MILLIS)
+ % 2
+ == 0
+ }
+}
+
impl<Highlighter: text::Highlighter> State<Highlighter> {
/// Returns whether the [`TextEditor`] is currently focused or not.
pub fn is_focused(&self) -> bool {
- self.is_focused
+ self.focus.is_some()
+ }
+}
+
+impl<Highlighter: text::Highlighter> operation::Focusable
+ for State<Highlighter>
+{
+ fn is_focused(&self) -> bool {
+ self.focus.is_some()
+ }
+
+ fn focus(&mut self) {
+ self.focus = Some(Focus::now());
+ }
+
+ fn unfocus(&mut self) {
+ self.focus = None;
}
}
@@ -351,7 +454,7 @@ where
fn state(&self) -> widget::tree::State {
widget::tree::State::new(State {
- is_focused: false,
+ focus: None,
last_click: None,
drag_click: None,
partial_scroll: 0.0,
@@ -402,6 +505,7 @@ where
self.font.unwrap_or_else(|| renderer.default_font()),
self.text_size.unwrap_or_else(|| renderer.default_size()),
self.line_height,
+ self.wrapping,
state.highlighter.borrow_mut().deref_mut(),
);
@@ -439,12 +543,48 @@ where
let state = tree.state.downcast_mut::<State<Highlighter>>();
+ match event {
+ Event::Window(window::Event::Unfocused) => {
+ if let Some(focus) = &mut state.focus {
+ focus.is_window_focused = false;
+ }
+ }
+ Event::Window(window::Event::Focused) => {
+ if let Some(focus) = &mut state.focus {
+ focus.is_window_focused = true;
+ focus.updated_at = Instant::now();
+
+ shell.request_redraw(window::RedrawRequest::NextFrame);
+ }
+ }
+ Event::Window(window::Event::RedrawRequested(now)) => {
+ if let Some(focus) = &mut state.focus {
+ if focus.is_window_focused {
+ focus.now = now;
+
+ let millis_until_redraw =
+ Focus::CURSOR_BLINK_INTERVAL_MILLIS
+ - (now - focus.updated_at).as_millis()
+ % Focus::CURSOR_BLINK_INTERVAL_MILLIS;
+
+ shell.request_redraw(window::RedrawRequest::At(
+ now + Duration::from_millis(
+ millis_until_redraw as u64,
+ ),
+ ));
+ }
+ }
+ }
+ _ => {}
+ }
+
let Some(update) = Update::from_event(
event,
state,
layout.bounds(),
self.padding,
cursor,
+ self.key_binding.as_deref(),
) else {
return event::Status::Ignored;
};
@@ -459,12 +599,18 @@ where
mouse::click::Kind::Triple => Action::SelectLine,
};
- state.is_focused = true;
+ state.focus = Some(Focus::now());
state.last_click = Some(click);
state.drag_click = Some(click.kind());
shell.publish(on_edit(action));
}
+ Update::Drag(position) => {
+ shell.publish(on_edit(Action::Drag(position)));
+ }
+ Update::Release => {
+ state.drag_click = None;
+ }
Update::Scroll(lines) => {
let bounds = self.content.0.borrow().editor.bounds();
@@ -479,34 +625,105 @@ where
lines: lines as i32,
}));
}
- Update::Unfocus => {
- state.is_focused = false;
- state.drag_click = None;
- }
- Update::Release => {
- state.drag_click = None;
- }
- Update::Action(action) => {
- shell.publish(on_edit(action));
- }
- Update::Copy => {
- if let Some(selection) = self.content.selection() {
- clipboard.write(clipboard::Kind::Standard, selection);
- }
- }
- Update::Cut => {
- if let Some(selection) = self.content.selection() {
- clipboard.write(clipboard::Kind::Standard, selection);
- shell.publish(on_edit(Action::Edit(Edit::Delete)));
+ Update::Binding(binding) => {
+ fn apply_binding<
+ H: text::Highlighter,
+ R: text::Renderer,
+ Message,
+ >(
+ binding: Binding<Message>,
+ content: &Content<R>,
+ state: &mut State<H>,
+ on_edit: &dyn Fn(Action) -> Message,
+ clipboard: &mut dyn Clipboard,
+ shell: &mut Shell<'_, Message>,
+ ) {
+ let mut publish = |action| shell.publish(on_edit(action));
+
+ match binding {
+ Binding::Unfocus => {
+ state.focus = None;
+ state.drag_click = None;
+ }
+ Binding::Copy => {
+ if let Some(selection) = content.selection() {
+ clipboard.write(
+ clipboard::Kind::Standard,
+ selection,
+ );
+ }
+ }
+ Binding::Cut => {
+ if let Some(selection) = content.selection() {
+ clipboard.write(
+ clipboard::Kind::Standard,
+ selection,
+ );
+
+ publish(Action::Edit(Edit::Delete));
+ }
+ }
+ Binding::Paste => {
+ if let Some(contents) =
+ clipboard.read(clipboard::Kind::Standard)
+ {
+ publish(Action::Edit(Edit::Paste(Arc::new(
+ contents,
+ ))));
+ }
+ }
+ Binding::Move(motion) => {
+ publish(Action::Move(motion));
+ }
+ Binding::Select(motion) => {
+ publish(Action::Select(motion));
+ }
+ Binding::SelectWord => {
+ publish(Action::SelectWord);
+ }
+ Binding::SelectLine => {
+ publish(Action::SelectLine);
+ }
+ Binding::SelectAll => {
+ publish(Action::SelectAll);
+ }
+ Binding::Insert(c) => {
+ publish(Action::Edit(Edit::Insert(c)));
+ }
+ Binding::Enter => {
+ publish(Action::Edit(Edit::Enter));
+ }
+ Binding::Backspace => {
+ publish(Action::Edit(Edit::Backspace));
+ }
+ Binding::Delete => {
+ publish(Action::Edit(Edit::Delete));
+ }
+ Binding::Sequence(sequence) => {
+ for binding in sequence {
+ apply_binding(
+ binding, content, state, on_edit,
+ clipboard, shell,
+ );
+ }
+ }
+ Binding::Custom(message) => {
+ shell.publish(message);
+ }
+ }
}
- }
- Update::Paste => {
- if let Some(contents) =
- clipboard.read(clipboard::Kind::Standard)
- {
- shell.publish(on_edit(Action::Edit(Edit::Paste(
- Arc::new(contents),
- ))));
+
+ apply_binding(
+ binding,
+ self.content,
+ state,
+ on_edit,
+ clipboard,
+ shell,
+ );
+
+ if let Some(focus) = &mut state.focus {
+ focus.updated_at = Instant::now();
}
}
}
@@ -522,15 +739,17 @@ where
defaults: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
- viewport: &Rectangle,
+ _viewport: &Rectangle,
) {
let bounds = layout.bounds();
let mut internal = self.content.0.borrow_mut();
let state = tree.state.downcast_ref::<State<Highlighter>>();
+ let font = self.font.unwrap_or_else(|| renderer.default_font());
+
internal.editor.highlight(
- self.font.unwrap_or_else(|| renderer.default_font()),
+ font,
state.highlighter.borrow_mut().deref_mut(),
|highlight| (self.highlighter_format)(highlight, theme),
);
@@ -540,7 +759,7 @@ where
let status = if is_disabled {
Status::Disabled
- } else if state.is_focused {
+ } else if state.focus.is_some() {
Status::Focused
} else if is_mouse_over {
Status::Hovered
@@ -559,22 +778,43 @@ where
style.background,
);
- renderer.fill_editor(
- &internal.editor,
- bounds.position()
- + Vector::new(self.padding.left, self.padding.top),
- defaults.text_color,
- *viewport,
- );
+ let text_bounds = bounds.shrink(self.padding);
+
+ if internal.editor.is_empty() {
+ if let Some(placeholder) = self.placeholder.clone() {
+ renderer.fill_text(
+ Text {
+ content: placeholder.into_owned(),
+ bounds: text_bounds.size(),
+ size: self
+ .text_size
+ .unwrap_or_else(|| renderer.default_size()),
+ line_height: self.line_height,
+ font,
+ horizontal_alignment: alignment::Horizontal::Left,
+ vertical_alignment: alignment::Vertical::Top,
+ shaping: text::Shaping::Advanced,
+ wrapping: self.wrapping,
+ },
+ text_bounds.position(),
+ style.placeholder,
+ text_bounds,
+ );
+ }
+ } else {
+ renderer.fill_editor(
+ &internal.editor,
+ text_bounds.position(),
+ defaults.text_color,
+ text_bounds,
+ );
+ }
- let translation = Vector::new(
- bounds.x + self.padding.left,
- bounds.y + self.padding.top,
- );
+ let translation = text_bounds.position() - Point::ORIGIN;
- if state.is_focused {
+ if let Some(focus) = state.focus.as_ref() {
match internal.editor.cursor() {
- Cursor::Caret(position) => {
+ Cursor::Caret(position) if focus.is_cursor_visible() => {
let cursor =
Rectangle::new(
position + translation,
@@ -588,15 +828,12 @@ where
),
);
- if let Some(clipped_cursor) = bounds.intersection(&cursor) {
+ if let Some(clipped_cursor) =
+ text_bounds.intersection(&cursor)
+ {
renderer.fill_quad(
renderer::Quad {
- bounds: Rectangle {
- x: clipped_cursor.x.floor(),
- y: clipped_cursor.y,
- width: clipped_cursor.width,
- height: clipped_cursor.height,
- },
+ bounds: clipped_cursor,
..renderer::Quad::default()
},
style.value,
@@ -605,7 +842,7 @@ where
}
Cursor::Selection(ranges) => {
for range in ranges.into_iter().filter_map(|range| {
- bounds.intersection(&(range + translation))
+ text_bounds.intersection(&(range + translation))
}) {
renderer.fill_quad(
renderer::Quad {
@@ -616,6 +853,7 @@ where
);
}
}
+ Cursor::Caret(_) => {}
}
}
}
@@ -640,6 +878,18 @@ where
mouse::Interaction::default()
}
}
+
+ fn operate(
+ &self,
+ tree: &mut widget::Tree,
+ _layout: Layout<'_>,
+ _renderer: &Renderer,
+ operation: &mut dyn widget::Operation,
+ ) {
+ let state = tree.state.downcast_mut::<State<Highlighter>>();
+
+ operation.focusable(state, None);
+ }
}
impl<'a, Highlighter, Message, Theme, Renderer>
@@ -658,27 +908,144 @@ where
}
}
-enum Update {
- Click(mouse::Click),
- Scroll(f32),
+/// A binding to an action in the [`TextEditor`].
+#[derive(Debug, Clone, PartialEq)]
+pub enum Binding<Message> {
+ /// Unfocus the [`TextEditor`].
Unfocus,
- Release,
- Action(Action),
+ /// Copy the selection of the [`TextEditor`].
Copy,
+ /// Cut the selection of the [`TextEditor`].
Cut,
+ /// Paste the clipboard contents in the [`TextEditor`].
Paste,
+ /// Apply a [`Motion`].
+ Move(Motion),
+ /// Select text with a given [`Motion`].
+ Select(Motion),
+ /// Select the word at the current cursor.
+ SelectWord,
+ /// Select the line at the current cursor.
+ SelectLine,
+ /// Select the entire buffer.
+ SelectAll,
+ /// Insert the given character.
+ Insert(char),
+ /// Break the current line.
+ Enter,
+ /// Delete the previous character.
+ Backspace,
+ /// Delete the next character.
+ Delete,
+ /// A sequence of bindings to execute.
+ Sequence(Vec<Self>),
+ /// Produce the given message.
+ Custom(Message),
+}
+
+/// A key press.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct KeyPress {
+ /// The key pressed.
+ pub key: keyboard::Key,
+ /// The state of the keyboard modifiers.
+ pub modifiers: keyboard::Modifiers,
+ /// The text produced by the key press.
+ pub text: Option<SmolStr>,
+ /// The current [`Status`] of the [`TextEditor`].
+ pub status: Status,
+}
+
+impl<Message> Binding<Message> {
+ /// Returns the default [`Binding`] for the given key press.
+ pub fn from_key_press(event: KeyPress) -> Option<Self> {
+ let KeyPress {
+ key,
+ modifiers,
+ text,
+ status,
+ } = event;
+
+ if status != Status::Focused {
+ return None;
+ }
+
+ match key.as_ref() {
+ keyboard::Key::Named(key::Named::Enter) => Some(Self::Enter),
+ keyboard::Key::Named(key::Named::Backspace) => {
+ Some(Self::Backspace)
+ }
+ keyboard::Key::Named(key::Named::Delete) => Some(Self::Delete),
+ keyboard::Key::Named(key::Named::Escape) => Some(Self::Unfocus),
+ keyboard::Key::Character("c") if modifiers.command() => {
+ Some(Self::Copy)
+ }
+ keyboard::Key::Character("x") if modifiers.command() => {
+ Some(Self::Cut)
+ }
+ keyboard::Key::Character("v")
+ if modifiers.command() && !modifiers.alt() =>
+ {
+ Some(Self::Paste)
+ }
+ keyboard::Key::Character("a") if modifiers.command() => {
+ Some(Self::SelectAll)
+ }
+ _ => {
+ if let Some(text) = text {
+ let c = text.chars().find(|c| !c.is_control())?;
+
+ Some(Self::Insert(c))
+ } else if let keyboard::Key::Named(named_key) = key.as_ref() {
+ let motion = motion(named_key)?;
+
+ let motion = if modifiers.macos_command() {
+ match motion {
+ Motion::Left => Motion::Home,
+ Motion::Right => Motion::End,
+ _ => motion,
+ }
+ } else {
+ motion
+ };
+
+ let motion = if modifiers.jump() {
+ motion.widen()
+ } else {
+ motion
+ };
+
+ Some(if modifiers.shift() {
+ Self::Select(motion)
+ } else {
+ Self::Move(motion)
+ })
+ } else {
+ None
+ }
+ }
+ }
+ }
+}
+
+enum Update<Message> {
+ Click(mouse::Click),
+ Drag(Point),
+ Release,
+ Scroll(f32),
+ Binding(Binding<Message>),
}
-impl Update {
+impl<Message> Update<Message> {
fn from_event<H: Highlighter>(
event: Event,
state: &State<H>,
bounds: Rectangle,
padding: Padding,
cursor: mouse::Cursor,
+ key_binding: Option<&dyn Fn(KeyPress) -> Option<Binding<Message>>>,
) -> Option<Self> {
- let action = |action| Some(Update::Action(action));
- let edit = |edit| action(Action::Edit(edit));
+ let binding = |binding| Some(Update::Binding(binding));
match event {
Event::Mouse(event) => match event {
@@ -689,12 +1056,13 @@ impl Update {
let click = mouse::Click::new(
cursor_position,
+ mouse::Button::Left,
state.last_click,
);
Some(Update::Click(click))
- } else if state.is_focused {
- Some(Update::Unfocus)
+ } else if state.focus.is_some() {
+ binding(Binding::Unfocus)
} else {
None
}
@@ -707,7 +1075,7 @@ impl Update {
let cursor_position = cursor.position_in(bounds)?
- Vector::new(padding.top, padding.left);
- action(Action::Drag(cursor_position))
+ Some(Update::Drag(cursor_position))
}
_ => None,
},
@@ -727,81 +1095,32 @@ impl Update {
}
_ => None,
},
- Event::Keyboard(event) => match event {
- keyboard::Event::KeyPressed {
+ Event::Keyboard(keyboard::Event::KeyPressed {
+ key,
+ modifiers,
+ text,
+ ..
+ }) => {
+ let status = if state.focus.is_some() {
+ Status::Focused
+ } else {
+ Status::Active
+ };
+
+ let key_press = KeyPress {
key,
modifiers,
text,
- ..
- } if state.is_focused => {
- match key.as_ref() {
- keyboard::Key::Named(key::Named::Enter) => {
- return edit(Edit::Enter);
- }
- keyboard::Key::Named(key::Named::Backspace) => {
- return edit(Edit::Backspace);
- }
- keyboard::Key::Named(key::Named::Delete) => {
- return edit(Edit::Delete);
- }
- keyboard::Key::Named(key::Named::Escape) => {
- return Some(Self::Unfocus);
- }
- keyboard::Key::Character("c")
- if modifiers.command() =>
- {
- return Some(Self::Copy);
- }
- keyboard::Key::Character("x")
- if modifiers.command() =>
- {
- return Some(Self::Cut);
- }
- keyboard::Key::Character("v")
- if modifiers.command() && !modifiers.alt() =>
- {
- return Some(Self::Paste);
- }
- _ => {}
- }
-
- if let Some(text) = text {
- if let Some(c) = text.chars().find(|c| !c.is_control())
- {
- return edit(Edit::Insert(c));
- }
- }
-
- if let keyboard::Key::Named(named_key) = key.as_ref() {
- if let Some(motion) = motion(named_key) {
- let motion = if modifiers.macos_command() {
- match motion {
- Motion::Left => Motion::Home,
- Motion::Right => Motion::End,
- _ => motion,
- }
- } else {
- motion
- };
-
- let motion = if modifiers.jump() {
- motion.widen()
- } else {
- motion
- };
-
- return action(if modifiers.shift() {
- Action::Select(motion)
- } else {
- Action::Move(motion)
- });
- }
- }
+ status,
+ };
- None
+ if let Some(key_binding) = key_binding {
+ key_binding(key_press)
+ } else {
+ Binding::from_key_press(key_press)
}
- _ => None,
- },
+ .map(Self::Binding)
+ }
_ => None,
}
}
diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs
index 4e89236b..d5ede524 100644
--- a/widget/src/text_input.rs
+++ b/widget/src/text_input.rs
@@ -19,7 +19,8 @@ use crate::core::keyboard::key;
use crate::core::layout;
use crate::core::mouse::{self, click};
use crate::core::renderer;
-use crate::core::text::{self, Paragraph as _, Text};
+use crate::core::text::paragraph::{self, Paragraph as _};
+use crate::core::text::{self, Text};
use crate::core::time::{Duration, Instant};
use crate::core::touch;
use crate::core::widget;
@@ -30,7 +31,8 @@ use crate::core::{
Background, Border, Color, Element, Layout, Length, Padding, Pixels, Point,
Rectangle, Shell, Size, Theme, Vector, Widget,
};
-use crate::runtime::{Action, Task};
+use crate::runtime::task::{self, Task};
+use crate::runtime::Action;
/// A field that can be filled with text.
///
@@ -72,6 +74,7 @@ pub struct TextInput<
padding: Padding,
size: Option<Pixels>,
line_height: text::LineHeight,
+ alignment: alignment::Horizontal,
on_input: Option<Box<dyn Fn(String) -> Message + 'a>>,
on_paste: Option<Box<dyn Fn(String) -> Message + 'a>>,
on_submit: Option<Message>,
@@ -101,6 +104,7 @@ where
padding: DEFAULT_PADDING,
size: None,
line_height: text::LineHeight::default(),
+ alignment: alignment::Horizontal::Left,
on_input: None,
on_paste: None,
on_submit: None,
@@ -125,11 +129,23 @@ where
/// the [`TextInput`].
///
/// If this method is not called, the [`TextInput`] will be disabled.
- pub fn on_input<F>(mut self, callback: F) -> Self
- where
- F: 'a + Fn(String) -> Message,
- {
- self.on_input = Some(Box::new(callback));
+ pub fn on_input(
+ mut self,
+ on_input: impl Fn(String) -> Message + 'a,
+ ) -> Self {
+ self.on_input = Some(Box::new(on_input));
+ self
+ }
+
+ /// Sets the message that should be produced when some text is typed into
+ /// the [`TextInput`], if `Some`.
+ ///
+ /// If `None`, the [`TextInput`] will be disabled.
+ pub fn on_input_maybe(
+ mut self,
+ on_input: Option<impl Fn(String) -> Message + 'a>,
+ ) -> Self {
+ self.on_input = on_input.map(|f| Box::new(f) as _);
self
}
@@ -140,6 +156,13 @@ where
self
}
+ /// Sets the message that should be produced when the [`TextInput`] is
+ /// focused and the enter key is pressed, if `Some`.
+ pub fn on_submit_maybe(mut self, on_submit: Option<Message>) -> Self {
+ self.on_submit = on_submit;
+ self
+ }
+
/// Sets the message that should be produced when some text is pasted into
/// the [`TextInput`].
pub fn on_paste(
@@ -150,6 +173,16 @@ where
self
}
+ /// Sets the message that should be produced when some text is pasted into
+ /// the [`TextInput`], if `Some`.
+ pub fn on_paste_maybe(
+ mut self,
+ on_paste: Option<impl Fn(String) -> Message + 'a>,
+ ) -> Self {
+ self.on_paste = on_paste.map(|f| Box::new(f) as _);
+ self
+ }
+
/// Sets the [`Font`] of the [`TextInput`].
///
/// [`Font`]: text::Renderer::Font
@@ -191,6 +224,15 @@ where
self
}
+ /// Sets the horizontal alignment of the [`TextInput`].
+ pub fn align_x(
+ mut self,
+ alignment: impl Into<alignment::Horizontal>,
+ ) -> Self {
+ self.alignment = alignment.into();
+ self
+ }
+
/// Sets the style of the [`TextInput`].
#[must_use]
pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
@@ -238,6 +280,7 @@ where
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Center,
shaping: text::Shaping::Advanced,
+ wrapping: text::Wrapping::default(),
};
state.placeholder.update(placeholder_text);
@@ -262,6 +305,7 @@ where
horizontal_alignment: alignment::Horizontal::Center,
vertical_alignment: alignment::Vertical::Center,
shaping: text::Shaping::Advanced,
+ wrapping: text::Wrapping::default(),
};
state.icon.update(icon_text);
@@ -359,7 +403,7 @@ where
let icon_layout = children_layout.next().unwrap();
renderer.fill_paragraph(
- &state.icon,
+ state.icon.raw(),
icon_layout.bounds().center(),
style.icon,
*viewport,
@@ -377,16 +421,16 @@ where
cursor::State::Index(position) => {
let (text_value_width, offset) =
measure_cursor_and_scroll_offset(
- &state.value,
+ state.value.raw(),
text_bounds,
position,
);
- let is_cursor_visible = ((focus.now - focus.updated_at)
- .as_millis()
- / CURSOR_BLINK_INTERVAL_MILLIS)
- % 2
- == 0;
+ let is_cursor_visible = !is_disabled
+ && ((focus.now - focus.updated_at).as_millis()
+ / CURSOR_BLINK_INTERVAL_MILLIS)
+ % 2
+ == 0;
let cursor = if is_cursor_visible {
Some((
@@ -414,14 +458,14 @@ where
let (left_position, left_offset) =
measure_cursor_and_scroll_offset(
- &state.value,
+ state.value.raw(),
text_bounds,
left,
);
let (right_position, right_offset) =
measure_cursor_and_scroll_offset(
- &state.value,
+ state.value.raw(),
text_bounds,
right,
);
@@ -455,9 +499,21 @@ where
};
let draw = |renderer: &mut Renderer, viewport| {
+ let paragraph = if text.is_empty() {
+ state.placeholder.raw()
+ } else {
+ state.value.raw()
+ };
+
+ let alignment_offset = alignment_offset(
+ text_bounds.width,
+ paragraph.min_width(),
+ self.alignment,
+ );
+
if let Some((cursor, color)) = cursor {
renderer.with_translation(
- Vector::new(-offset, 0.0),
+ Vector::new(alignment_offset - offset, 0.0),
|renderer| {
renderer.fill_quad(cursor, color);
},
@@ -467,13 +523,9 @@ where
}
renderer.fill_paragraph(
- if text.is_empty() {
- &state.placeholder
- } else {
- &state.value
- },
+ paragraph,
Point::new(text_bounds.x, text_bounds.center_y())
- - Vector::new(offset, 0.0),
+ + Vector::new(alignment_offset - offset, 0.0),
if text.is_empty() {
style.placeholder
} else {
@@ -510,12 +562,9 @@ where
fn diff(&self, tree: &mut Tree) {
let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
- // Unfocus text input if it becomes disabled
+ // Stop pasting if input becomes disabled
if self.on_input.is_none() {
- state.last_click = None;
- state.is_focused = None;
state.is_pasting = None;
- state.is_dragging = false;
}
}
@@ -540,7 +589,7 @@ where
tree: &mut Tree,
_layout: Layout<'_>,
_renderer: &Renderer,
- operation: &mut dyn Operation<()>,
+ operation: &mut dyn Operation,
) {
let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
@@ -576,11 +625,7 @@ where
| Event::Touch(touch::Event::FingerPressed { .. }) => {
let state = state::<Renderer>(tree);
- let click_position = if self.on_input.is_some() {
- cursor.position_over(layout.bounds())
- } else {
- None
- };
+ let click_position = cursor.position_over(layout.bounds());
state.is_focused = if click_position.is_some() {
state.is_focused.or_else(|| {
@@ -598,10 +643,24 @@ where
if let Some(cursor_position) = click_position {
let text_layout = layout.children().next().unwrap();
- let target = cursor_position.x - text_layout.bounds().x;
- let click =
- mouse::Click::new(cursor_position, state.last_click);
+ let target = {
+ let text_bounds = text_layout.bounds();
+
+ let alignment_offset = alignment_offset(
+ text_bounds.width,
+ state.value.raw().min_width(),
+ self.alignment,
+ );
+
+ cursor_position.x - text_bounds.x - alignment_offset
+ };
+
+ let click = mouse::Click::new(
+ cursor_position,
+ mouse::Button::Left,
+ state.last_click,
+ );
match click.kind() {
click::Kind::Single => {
@@ -675,7 +734,18 @@ where
if state.is_dragging {
let text_layout = layout.children().next().unwrap();
- let target = position.x - text_layout.bounds().x;
+
+ let target = {
+ let text_bounds = text_layout.bounds();
+
+ let alignment_offset = alignment_offset(
+ text_bounds.width,
+ state.value.raw().min_width(),
+ self.alignment,
+ );
+
+ position.x - text_bounds.x - alignment_offset
+ };
let value = if self.is_secure {
self.value.secure()
@@ -704,10 +774,6 @@ where
let state = state::<Renderer>(tree);
if let Some(focus) = &mut state.is_focused {
- let Some(on_input) = &self.on_input else {
- return event::Status::Ignored;
- };
-
let modifiers = state.keyboard_modifiers;
focus.updated_at = Instant::now();
@@ -731,6 +797,10 @@ where
if state.keyboard_modifiers.command()
&& !self.is_secure =>
{
+ let Some(on_input) = &self.on_input else {
+ return event::Status::Ignored;
+ };
+
if let Some((start, end)) =
state.cursor.selection(&self.value)
{
@@ -755,6 +825,10 @@ where
if state.keyboard_modifiers.command()
&& !state.keyboard_modifiers.alt() =>
{
+ let Some(on_input) = &self.on_input else {
+ return event::Status::Ignored;
+ };
+
let content = match state.is_pasting.take() {
Some(content) => content,
None => {
@@ -798,6 +872,10 @@ where
}
if let Some(text) = text {
+ let Some(on_input) = &self.on_input else {
+ return event::Status::Ignored;
+ };
+
state.is_pasting = None;
if let Some(c) =
@@ -826,6 +904,10 @@ where
}
}
keyboard::Key::Named(key::Named::Backspace) => {
+ let Some(on_input) = &self.on_input else {
+ return event::Status::Ignored;
+ };
+
if modifiers.jump()
&& state.cursor.selection(&self.value).is_none()
{
@@ -850,6 +932,10 @@ where
update_cache(state, &self.value);
}
keyboard::Key::Named(key::Named::Delete) => {
+ let Some(on_input) = &self.on_input else {
+ return event::Status::Ignored;
+ };
+
if modifiers.jump()
&& state.cursor.selection(&self.value).is_none()
{
@@ -1068,7 +1154,7 @@ where
) -> mouse::Interaction {
if cursor.is_over(layout.bounds()) {
if self.on_input.is_none() {
- mouse::Interaction::NotAllowed
+ mouse::Interaction::Idle
} else {
mouse::Interaction::Text
}
@@ -1142,13 +1228,13 @@ impl From<Id> for widget::Id {
/// Produces a [`Task`] that focuses the [`TextInput`] with the given [`Id`].
pub fn focus<T>(id: Id) -> Task<T> {
- Task::effect(Action::widget(operation::focusable::focus(id.0)))
+ task::effect(Action::widget(operation::focusable::focus(id.0)))
}
/// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the
/// end.
pub fn move_cursor_to_end<T>(id: Id) -> Task<T> {
- Task::effect(Action::widget(operation::text_input::move_cursor_to_end(
+ task::effect(Action::widget(operation::text_input::move_cursor_to_end(
id.0,
)))
}
@@ -1156,7 +1242,7 @@ pub fn move_cursor_to_end<T>(id: Id) -> Task<T> {
/// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the
/// front.
pub fn move_cursor_to_front<T>(id: Id) -> Task<T> {
- Task::effect(Action::widget(operation::text_input::move_cursor_to_front(
+ task::effect(Action::widget(operation::text_input::move_cursor_to_front(
id.0,
)))
}
@@ -1164,22 +1250,22 @@ pub fn move_cursor_to_front<T>(id: Id) -> Task<T> {
/// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the
/// provided position.
pub fn move_cursor_to<T>(id: Id, position: usize) -> Task<T> {
- Task::effect(Action::widget(operation::text_input::move_cursor_to(
+ task::effect(Action::widget(operation::text_input::move_cursor_to(
id.0, position,
)))
}
/// Produces a [`Task`] that selects all the content of the [`TextInput`] with the given [`Id`].
pub fn select_all<T>(id: Id) -> Task<T> {
- Task::effect(Action::widget(operation::text_input::select_all(id.0)))
+ task::effect(Action::widget(operation::text_input::select_all(id.0)))
}
/// The state of a [`TextInput`].
#[derive(Debug, Default, Clone)]
pub struct State<P: text::Paragraph> {
- value: P,
- placeholder: P,
- icon: P,
+ value: paragraph::Plain<P>,
+ placeholder: paragraph::Plain<P>,
+ icon: paragraph::Plain<P>,
is_focused: Option<Focus>,
is_dragging: bool,
is_pasting: Option<Value>,
@@ -1208,21 +1294,6 @@ impl<P: text::Paragraph> State<P> {
Self::default()
}
- /// Creates a new [`State`], representing a focused [`TextInput`].
- pub fn focused() -> Self {
- Self {
- value: P::default(),
- placeholder: P::default(),
- icon: P::default(),
- is_focused: None,
- is_dragging: false,
- is_pasting: None,
- last_click: None,
- cursor: Cursor::default(),
- keyboard_modifiers: keyboard::Modifiers::default(),
- }
- }
-
/// Returns whether the [`TextInput`] is currently focused or not.
pub fn is_focused(&self) -> bool {
self.is_focused.is_some()
@@ -1318,7 +1389,7 @@ fn offset<P: text::Paragraph>(
};
let (_, offset) = measure_cursor_and_scroll_offset(
- &state.value,
+ state.value.raw(),
text_bounds,
focus_position,
);
@@ -1356,6 +1427,7 @@ fn find_cursor_position<P: text::Paragraph>(
let char_offset = state
.value
+ .raw()
.hit_test(Point::new(x + offset, text_bounds.height / 2.0))
.map(text::Hit::cursor)?;
@@ -1385,7 +1457,7 @@ fn replace_paragraph<Renderer>(
let mut children_layout = layout.children();
let text_bounds = children_layout.next().unwrap().bounds();
- state.value = Renderer::Paragraph::with_text(Text {
+ state.value = paragraph::Plain::new(Text {
font,
line_height,
content: &value.to_string(),
@@ -1394,6 +1466,7 @@ fn replace_paragraph<Renderer>(
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Top,
shaping: text::Shaping::Advanced,
+ wrapping: text::Wrapping::default(),
});
}
@@ -1498,3 +1571,21 @@ pub fn default(theme: &Theme, status: Status) -> Style {
},
}
}
+
+fn alignment_offset(
+ text_bounds_width: f32,
+ text_min_width: f32,
+ alignment: alignment::Horizontal,
+) -> f32 {
+ if text_min_width > text_bounds_width {
+ 0.0
+ } else {
+ match alignment {
+ alignment::Horizontal::Left => 0.0,
+ alignment::Horizontal::Center => {
+ (text_bounds_width - text_min_width) / 2.0
+ }
+ alignment::Horizontal::Right => text_bounds_width - text_min_width,
+ }
+ }
+}
diff --git a/widget/src/themer.rs b/widget/src/themer.rs
index 9eb47d84..499a9fe8 100644
--- a/widget/src/themer.rs
+++ b/widget/src/themer.rs
@@ -104,7 +104,7 @@ where
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
- operation: &mut dyn Operation<()>,
+ operation: &mut dyn Operation,
) {
self.content
.as_widget()
@@ -236,7 +236,7 @@ where
&mut self,
layout: Layout<'_>,
renderer: &Renderer,
- operation: &mut dyn Operation<()>,
+ operation: &mut dyn Operation,
) {
self.content.operate(layout, renderer, operation);
}
diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs
index ca6e37b0..1c425dc1 100644
--- a/widget/src/toggler.rs
+++ b/widget/src/toggler.rs
@@ -26,7 +26,9 @@ use crate::core::{
///
/// let is_toggled = true;
///
-/// Toggler::new(String::from("Toggle me!"), is_toggled, |b| Message::TogglerToggled(b));
+/// Toggler::new(is_toggled)
+/// .label("Toggle me!")
+/// .on_toggle(Message::TogglerToggled);
/// ```
#[allow(missing_debug_implementations)]
pub struct Toggler<
@@ -39,14 +41,15 @@ pub struct Toggler<
Renderer: text::Renderer,
{
is_toggled: bool,
- on_toggle: Box<dyn Fn(bool) -> Message + 'a>,
- label: Option<String>,
+ on_toggle: Option<Box<dyn Fn(bool) -> Message + 'a>>,
+ label: Option<text::Fragment<'a>>,
width: Length,
size: f32,
text_size: Option<Pixels>,
text_line_height: text::LineHeight,
text_alignment: alignment::Horizontal,
text_shaping: text::Shaping,
+ text_wrapping: text::Wrapping,
spacing: f32,
font: Option<Renderer::Font>,
class: Theme::Class<'a>,
@@ -68,30 +71,54 @@ where
/// * a function that will be called when the [`Toggler`] is toggled. It
/// will receive the new state of the [`Toggler`] and must produce a
/// `Message`.
- pub fn new<F>(
- label: impl Into<Option<String>>,
- is_toggled: bool,
- f: F,
- ) -> Self
- where
- F: 'a + Fn(bool) -> Message,
- {
+ pub fn new(is_toggled: bool) -> Self {
Toggler {
is_toggled,
- on_toggle: Box::new(f),
- label: label.into(),
- width: Length::Fill,
+ on_toggle: None,
+ label: None,
+ width: Length::Shrink,
size: Self::DEFAULT_SIZE,
text_size: None,
text_line_height: text::LineHeight::default(),
text_alignment: alignment::Horizontal::Left,
- text_shaping: text::Shaping::Basic,
+ text_shaping: text::Shaping::default(),
+ text_wrapping: text::Wrapping::default(),
spacing: Self::DEFAULT_SIZE / 2.0,
font: None,
class: Theme::default(),
}
}
+ /// Sets the label of the [`Toggler`].
+ pub fn label(mut self, label: impl text::IntoFragment<'a>) -> Self {
+ self.label = Some(label.into_fragment());
+ self
+ }
+
+ /// Sets the message that should be produced when a user toggles
+ /// the [`Toggler`].
+ ///
+ /// If this method is not called, the [`Toggler`] will be disabled.
+ pub fn on_toggle(
+ mut self,
+ on_toggle: impl Fn(bool) -> Message + 'a,
+ ) -> Self {
+ self.on_toggle = Some(Box::new(on_toggle));
+ self
+ }
+
+ /// Sets the message that should be produced when a user toggles
+ /// the [`Toggler`], if `Some`.
+ ///
+ /// If `None`, the [`Toggler`] will be disabled.
+ pub fn on_toggle_maybe(
+ mut self,
+ on_toggle: Option<impl Fn(bool) -> Message + 'a>,
+ ) -> Self {
+ self.on_toggle = on_toggle.map(|on_toggle| Box::new(on_toggle) as _);
+ self
+ }
+
/// Sets the size of the [`Toggler`].
pub fn size(mut self, size: impl Into<Pixels>) -> Self {
self.size = size.into().0;
@@ -131,6 +158,12 @@ where
self
}
+ /// Sets the [`text::Wrapping`] strategy of the [`Toggler`].
+ pub fn text_wrapping(mut self, wrapping: text::Wrapping) -> Self {
+ self.text_wrapping = wrapping;
+ self
+ }
+
/// Sets the spacing between the [`Toggler`] and the text.
pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
self.spacing = spacing.into().0;
@@ -216,6 +249,7 @@ where
self.text_alignment,
alignment::Vertical::Top,
self.text_shaping,
+ self.text_wrapping,
)
} else {
layout::Node::new(Size::ZERO)
@@ -235,13 +269,17 @@ where
shell: &mut Shell<'_, Message>,
_viewport: &Rectangle,
) -> event::Status {
+ let Some(on_toggle) = &self.on_toggle else {
+ return event::Status::Ignored;
+ };
+
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
let mouse_over = cursor.is_over(layout.bounds());
if mouse_over {
- shell.publish((self.on_toggle)(!self.is_toggled));
+ shell.publish(on_toggle(!self.is_toggled));
event::Status::Captured
} else {
@@ -261,7 +299,11 @@ where
_renderer: &Renderer,
) -> mouse::Interaction {
if cursor.is_over(layout.bounds()) {
- mouse::Interaction::Pointer
+ if self.on_toggle.is_some() {
+ mouse::Interaction::Pointer
+ } else {
+ mouse::Interaction::NotAllowed
+ }
} else {
mouse::Interaction::default()
}
@@ -289,12 +331,14 @@ where
if self.label.is_some() {
let label_layout = children.next().unwrap();
+ let state: &widget::text::State<Renderer::Paragraph> =
+ tree.state.downcast_ref();
crate::text::draw(
renderer,
style,
label_layout,
- tree.state.downcast_ref(),
+ state.0.raw(),
crate::text::Style::default(),
viewport,
);
@@ -303,7 +347,9 @@ where
let bounds = toggler_layout.bounds();
let is_mouse_over = cursor.is_over(layout.bounds());
- let status = if is_mouse_over {
+ let status = if self.on_toggle.is_none() {
+ Status::Disabled
+ } else if is_mouse_over {
Status::Hovered {
is_toggled: self.is_toggled,
}
@@ -392,6 +438,8 @@ pub enum Status {
/// Indicates whether the [`Toggler`] is toggled.
is_toggled: bool,
},
+ /// The [`Toggler`] is disabled.
+ Disabled,
}
/// The appearance of a toggler.
@@ -452,6 +500,7 @@ pub fn default(theme: &Theme, status: Status) -> Style {
palette.background.strong.color
}
}
+ Status::Disabled => palette.background.weak.color,
};
let foreground = match status {
@@ -472,6 +521,7 @@ pub fn default(theme: &Theme, status: Status) -> Style {
palette.background.weak.color
}
}
+ Status::Disabled => palette.background.base.color,
};
Style {
diff --git a/widget/src/vertical_slider.rs b/widget/src/vertical_slider.rs
index defb442f..03ec374c 100644
--- a/widget/src/vertical_slider.rs
+++ b/widget/src/vertical_slider.rs
@@ -5,6 +5,7 @@ pub use crate::slider::{
default, Catalog, Handle, HandleShape, Status, Style, StyleFn,
};
+use crate::core::border::Border;
use crate::core::event::{self, Event};
use crate::core::keyboard;
use crate::core::keyboard::key::{self, Key};
@@ -14,8 +15,8 @@ use crate::core::renderer;
use crate::core::touch;
use crate::core::widget::tree::{self, Tree};
use crate::core::{
- self, Border, Clipboard, Element, Length, Pixels, Point, Rectangle, Shell,
- Size, Widget,
+ self, Clipboard, Element, Length, Pixels, Point, Rectangle, Shell, Size,
+ Widget,
};
/// An vertical bar and a handle that selects a single value from a range of
@@ -71,8 +72,8 @@ where
/// * an inclusive range of possible values
/// * the current value of the [`VerticalSlider`]
/// * a function that will be called when the [`VerticalSlider`] is dragged.
- /// It receives the new value of the [`VerticalSlider`] and must produce a
- /// `Message`.
+ /// It receives the new value of the [`VerticalSlider`] and must produce a
+ /// `Message`.
pub fn new<F>(range: RangeInclusive<T>, value: T, on_change: F) -> Self
where
F: 'a + Fn(T) -> Message,
@@ -239,7 +240,7 @@ where
let steps = (percent * (end - start) / step).round();
let value = steps * step + start;
- T::from_f64(value)
+ T::from_f64(value.min(end))
};
new_value
@@ -412,10 +413,10 @@ where
width: style.rail.width,
height: offset + handle_width / 2.0,
},
- border: Border::rounded(style.rail.border_radius),
+ border: style.rail.border,
..renderer::Quad::default()
},
- style.rail.colors.1,
+ style.rail.backgrounds.1,
);
renderer.fill_quad(
@@ -426,10 +427,10 @@ where
width: style.rail.width,
height: bounds.height - offset - handle_width / 2.0,
},
- border: Border::rounded(style.rail.border_radius),
+ border: style.rail.border,
..renderer::Quad::default()
},
- style.rail.colors.0,
+ style.rail.backgrounds.0,
);
renderer.fill_quad(
@@ -447,7 +448,7 @@ where
},
..renderer::Quad::default()
},
- style.handle.color,
+ style.handle.background,
);
}
diff --git a/winit/Cargo.toml b/winit/Cargo.toml
index 68368aa1..f5a47952 100644
--- a/winit/Cargo.toml
+++ b/winit/Cargo.toml
@@ -33,7 +33,6 @@ log.workspace = true
rustc-hash.workspace = true
thiserror.workspace = true
tracing.workspace = true
-wasm-bindgen-futures.workspace = true
window_clipboard.workspace = true
winit.workspace = true
@@ -46,4 +45,4 @@ winapi.workspace = true
[target.'cfg(target_arch = "wasm32")'.dependencies]
web-sys.workspace = true
web-sys.features = ["Document", "Window"]
-
+wasm-bindgen-futures.workspace = true
diff --git a/winit/src/clipboard.rs b/winit/src/clipboard.rs
index 5237ca01..d54a1fe0 100644
--- a/winit/src/clipboard.rs
+++ b/winit/src/clipboard.rs
@@ -1,6 +1,8 @@
//! Access the clipboard.
use crate::core::clipboard::Kind;
+use std::sync::Arc;
+use winit::window::{Window, WindowId};
/// A buffer for short-term storage and transfer within and between
/// applications.
@@ -10,18 +12,33 @@ pub struct Clipboard {
}
enum State {
- Connected(window_clipboard::Clipboard),
+ Connected {
+ clipboard: window_clipboard::Clipboard,
+ // Held until drop to satisfy the safety invariants of
+ // `window_clipboard::Clipboard`.
+ //
+ // Note that the field ordering is load-bearing.
+ #[allow(dead_code)]
+ window: Arc<Window>,
+ },
Unavailable,
}
impl Clipboard {
/// Creates a new [`Clipboard`] for the given window.
- pub fn connect(window: &winit::window::Window) -> Clipboard {
+ pub fn connect(window: Arc<Window>) -> Clipboard {
+ // SAFETY: The window handle will stay alive throughout the entire
+ // lifetime of the `window_clipboard::Clipboard` because we hold
+ // the `Arc<Window>` together with `State`, and enum variant fields
+ // get dropped in declaration order.
#[allow(unsafe_code)]
- let state = unsafe { window_clipboard::Clipboard::connect(window) }
- .ok()
- .map(State::Connected)
- .unwrap_or(State::Unavailable);
+ let clipboard =
+ unsafe { window_clipboard::Clipboard::connect(&window) };
+
+ let state = match clipboard {
+ Ok(clipboard) => State::Connected { clipboard, window },
+ Err(_) => State::Unavailable,
+ };
Clipboard { state }
}
@@ -37,7 +54,7 @@ impl Clipboard {
/// Reads the current content of the [`Clipboard`] as text.
pub fn read(&self, kind: Kind) -> Option<String> {
match &self.state {
- State::Connected(clipboard) => match kind {
+ State::Connected { clipboard, .. } => match kind {
Kind::Standard => clipboard.read().ok(),
Kind::Primary => clipboard.read_primary().and_then(Result::ok),
},
@@ -48,7 +65,7 @@ impl Clipboard {
/// Writes the given text contents to the [`Clipboard`].
pub fn write(&mut self, kind: Kind, contents: String) {
match &mut self.state {
- State::Connected(clipboard) => {
+ State::Connected { clipboard, .. } => {
let result = match kind {
Kind::Standard => clipboard.write(contents),
Kind::Primary => {
@@ -66,6 +83,14 @@ impl Clipboard {
State::Unavailable => {}
}
}
+
+ /// Returns the identifier of the window used to create the [`Clipboard`], if any.
+ pub fn window_id(&self) -> Option<WindowId> {
+ match &self.state {
+ State::Connected { window, .. } => Some(window.id()),
+ State::Unavailable => None,
+ }
+ }
}
impl crate::core::Clipboard for Clipboard {
diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs
index 0ed10c88..aaaca1a9 100644
--- a/winit/src/conversion.rs
+++ b/winit/src/conversion.rs
@@ -79,6 +79,10 @@ pub fn window_attributes(
attributes = attributes
.with_skip_taskbar(settings.platform_specific.skip_taskbar);
+
+ attributes = attributes.with_undecorated_shadow(
+ settings.platform_specific.undecorated_shadow,
+ );
}
#[cfg(target_os = "macos")]
@@ -101,10 +105,14 @@ pub fn window_attributes(
{
use winit::platform::x11::WindowAttributesExtX11;
- attributes = attributes.with_name(
- &settings.platform_specific.application_id,
- &settings.platform_specific.application_id,
- );
+ attributes = attributes
+ .with_override_redirect(
+ settings.platform_specific.override_redirect,
+ )
+ .with_name(
+ &settings.platform_specific.application_id,
+ &settings.platform_specific.application_id,
+ );
}
#[cfg(feature = "wayland")]
{
@@ -132,10 +140,10 @@ pub fn window_event(
WindowEvent::Resized(new_size) => {
let logical_size = new_size.to_logical(scale_factor);
- Some(Event::Window(window::Event::Resized {
+ Some(Event::Window(window::Event::Resized(Size {
width: logical_size.width,
height: logical_size.height,
- }))
+ })))
}
WindowEvent::CloseRequested => {
Some(Event::Window(window::Event::CloseRequested))
@@ -277,7 +285,7 @@ pub fn window_event(
let winit::dpi::LogicalPosition { x, y } =
position.to_logical(scale_factor);
- Some(Event::Window(window::Event::Moved { x, y }))
+ Some(Event::Window(window::Event::Moved(Point::new(x, y))))
}
_ => None,
}
diff --git a/winit/src/program.rs b/winit/src/program.rs
index d55aedf1..52d8eb5f 100644
--- a/winit/src/program.rs
+++ b/winit/src/program.rs
@@ -202,12 +202,25 @@ where
};
let (program, task) = runtime.enter(|| P::new(flags));
+ let is_daemon = window_settings.is_none();
- if let Some(stream) = task.into_stream() {
+ let task = if let Some(window_settings) = window_settings {
+ let mut task = Some(task);
+
+ let (_id, open) = runtime::window::open(window_settings);
+
+ open.then(move |_| task.take().unwrap_or(Task::none()))
+ } else {
+ task
+ };
+
+ if let Some(stream) = runtime::task::into_stream(task) {
runtime.run(stream);
}
- runtime.track(program.subscription().map(Action::Output).into_recipes());
+ runtime.track(subscription::into_recipes(
+ program.subscription().map(Action::Output),
+ ));
let (boot_sender, boot_receiver) = oneshot::channel();
let (event_sender, event_receiver) = mpsc::unbounded();
@@ -221,6 +234,7 @@ where
boot_receiver,
event_receiver,
control_sender,
+ is_daemon,
));
let context = task::Context::from_waker(task::noop_waker_ref());
@@ -229,7 +243,7 @@ where
instance: std::pin::Pin<Box<F>>,
context: task::Context<'static>,
id: Option<String>,
- boot: Option<BootConfig<Message, C>>,
+ boot: Option<BootConfig<C>>,
sender: mpsc::UnboundedSender<Event<Action<Message>>>,
receiver: mpsc::UnboundedReceiver<Control>,
error: Option<Error>,
@@ -240,11 +254,9 @@ where
queued_events: Vec<Event<Action<Message>>>,
}
- struct BootConfig<Message: 'static, C> {
- proxy: Proxy<Message>,
+ struct BootConfig<C> {
sender: oneshot::Sender<Boot<C>>,
fonts: Vec<Cow<'static, [u8]>>,
- window_settings: Option<window::Settings>,
graphics_settings: graphics::Settings,
}
@@ -253,10 +265,8 @@ where
context,
id: settings.id,
boot: Some(BootConfig {
- proxy,
sender: boot_sender,
fonts: settings.fonts,
- window_settings,
graphics_settings,
}),
sender: event_sender,
@@ -278,10 +288,8 @@ where
{
fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {
let Some(BootConfig {
- mut proxy,
sender,
fonts,
- window_settings,
graphics_settings,
}) = self.boot.take()
else {
@@ -299,8 +307,6 @@ where
}
};
- let clipboard = Clipboard::connect(&window);
-
let finish_boot = async move {
let mut compositor =
C::new(graphics_settings, window.clone()).await?;
@@ -310,27 +316,10 @@ where
}
sender
- .send(Boot {
- compositor,
- clipboard,
- window: window.id(),
- is_daemon: window_settings.is_none(),
- })
+ .send(Boot { compositor })
.ok()
.expect("Send boot event");
- if let Some(window_settings) = window_settings {
- let (sender, _receiver) = oneshot::channel();
-
- proxy.send_action(Action::Window(
- runtime::window::Action::Open(
- window::Id::unique(),
- window_settings,
- sender,
- ),
- ));
- }
-
Ok::<_, graphics::Error>(())
};
@@ -420,6 +409,23 @@ where
);
}
+ fn received_url(
+ &mut self,
+ event_loop: &winit::event_loop::ActiveEventLoop,
+ url: String,
+ ) {
+ self.process_event(
+ event_loop,
+ Event::EventLoopAwakened(
+ winit::event::Event::PlatformSpecific(
+ winit::event::PlatformSpecific::MacOS(
+ winit::event::MacOS::ReceivedUrl(url),
+ ),
+ ),
+ ),
+ );
+ }
+
fn about_to_wait(
&mut self,
event_loop: &winit::event_loop::ActiveEventLoop,
@@ -488,10 +494,13 @@ where
settings,
title,
monitor,
+ on_open,
} => {
let exit_on_close_request =
settings.exit_on_close_request;
+ let visible = settings.visible;
+
#[cfg(target_arch = "wasm32")]
let target =
settings.platform_specific.target.clone();
@@ -505,7 +514,8 @@ where
.or(event_loop
.primary_monitor()),
self.id.clone(),
- ),
+ )
+ .with_visible(false),
)
.expect("Create window");
@@ -561,6 +571,8 @@ where
id,
window,
exit_on_close_request,
+ make_visible: visible,
+ on_open,
},
);
}
@@ -600,20 +612,21 @@ where
struct Boot<C> {
compositor: C,
- clipboard: Clipboard,
- window: winit::window::WindowId,
- is_daemon: bool,
}
+#[derive(Debug)]
enum Event<Message: 'static> {
WindowCreated {
id: window::Id,
window: winit::window::Window,
exit_on_close_request: bool,
+ make_visible: bool,
+ on_open: oneshot::Sender<window::Id>,
},
EventLoopAwakened(winit::event::Event<Message>),
}
+#[derive(Debug)]
enum Control {
ChangeFlow(winit::event_loop::ControlFlow),
Exit,
@@ -622,6 +635,7 @@ enum Control {
settings: window::Settings,
title: String,
monitor: Option<winit::monitor::MonitorHandle>,
+ on_open: oneshot::Sender<window::Id>,
},
}
@@ -630,9 +644,10 @@ async fn run_instance<P, C>(
mut runtime: Runtime<P::Executor, Proxy<P::Message>, Action<P::Message>>,
mut proxy: Proxy<P::Message>,
mut debug: Debug,
- mut boot: oneshot::Receiver<Boot<C>>,
+ boot: oneshot::Receiver<Boot<C>>,
mut event_receiver: mpsc::UnboundedReceiver<Event<Action<P::Message>>>,
mut control_sender: mpsc::UnboundedSender<Control>,
+ is_daemon: bool,
) where
P: Program + 'static,
C: Compositor<Renderer = P::Renderer> + 'static,
@@ -641,14 +656,10 @@ async fn run_instance<P, C>(
use winit::event;
use winit::event_loop::ControlFlow;
- let Boot {
- mut compositor,
- mut clipboard,
- window: boot_window,
- is_daemon,
- } = boot.try_recv().ok().flatten().expect("Receive boot");
+ let Boot { mut compositor } = boot.await.expect("Receive boot");
let mut window_manager = WindowManager::new();
+ let mut is_window_opening = !is_daemon;
let mut events = Vec::new();
let mut messages = Vec::new();
@@ -656,15 +667,29 @@ async fn run_instance<P, C>(
let mut ui_caches = FxHashMap::default();
let mut user_interfaces = ManuallyDrop::new(FxHashMap::default());
+ let mut clipboard = Clipboard::unconnected();
debug.startup_finished();
- 'main: while let Some(event) = event_receiver.next().await {
+ loop {
+ // Empty the queue if possible
+ let event = if let Ok(event) = event_receiver.try_next() {
+ event
+ } else {
+ event_receiver.next().await
+ };
+
+ let Some(event) = event else {
+ break;
+ };
+
match event {
Event::WindowCreated {
id,
window,
exit_on_close_request,
+ make_visible,
+ on_open,
} => {
let window = window_manager.insert(
id,
@@ -689,6 +714,10 @@ async fn run_instance<P, C>(
);
let _ = ui_caches.insert(id, user_interface::Cache::default());
+ if make_visible {
+ window.raw.set_visible(true);
+ }
+
events.push((
id,
core::Event::Window(window::Event::Opened {
@@ -696,6 +725,13 @@ async fn run_instance<P, C>(
size: window.size(),
}),
));
+
+ if clipboard.window_id().is_none() {
+ clipboard = Clipboard::connect(window.raw.clone());
+ }
+
+ let _ = on_open.send(id);
+ is_window_opening = false;
}
Event::EventLoopAwakened(event) => {
match event {
@@ -725,6 +761,7 @@ async fn run_instance<P, C>(
action,
&program,
&mut compositor,
+ &mut events,
&mut messages,
&mut clipboard,
&mut control_sender,
@@ -732,6 +769,7 @@ async fn run_instance<P, C>(
&mut user_interfaces,
&mut window_manager,
&mut ui_caches,
+ &mut is_window_opening,
);
actions += 1;
}
@@ -916,10 +954,14 @@ async fn run_instance<P, C>(
window_event,
winit::event::WindowEvent::Destroyed
)
- && window_id != boot_window
+ && !is_window_opening
&& window_manager.is_empty()
{
- break 'main;
+ control_sender
+ .start_send(Control::Exit)
+ .expect("Send control action");
+
+ continue;
}
let Some((id, window)) =
@@ -933,14 +975,22 @@ async fn run_instance<P, C>(
winit::event::WindowEvent::CloseRequested
) && window.exit_on_close_request
{
- let _ = window_manager.remove(id);
- let _ = user_interfaces.remove(&id);
- let _ = ui_caches.remove(&id);
-
- events.push((
- id,
- core::Event::Window(window::Event::Closed),
- ));
+ run_action(
+ Action::Window(runtime::window::Action::Close(
+ id,
+ )),
+ &program,
+ &mut compositor,
+ &mut events,
+ &mut messages,
+ &mut clipboard,
+ &mut control_sender,
+ &mut debug,
+ &mut user_interfaces,
+ &mut window_manager,
+ &mut ui_caches,
+ &mut is_window_opening,
+ );
} else {
window.state.update(
&window.raw,
@@ -1114,19 +1164,20 @@ fn update<P: Program, E: Executor>(
let task = runtime.enter(|| program.update(message));
debug.update_finished();
- if let Some(stream) = task.into_stream() {
+ if let Some(stream) = runtime::task::into_stream(task) {
runtime.run(stream);
}
}
let subscription = program.subscription();
- runtime.track(subscription.map(Action::Output).into_recipes());
+ runtime.track(subscription::into_recipes(subscription.map(Action::Output)));
}
fn run_action<P, C>(
action: Action<P::Message>,
program: &P,
compositor: &mut C,
+ events: &mut Vec<(window::Id, core::Event)>,
messages: &mut Vec<P::Message>,
clipboard: &mut Clipboard,
control_sender: &mut mpsc::UnboundedSender<Control>,
@@ -1137,6 +1188,7 @@ fn run_action<P, C>(
>,
window_manager: &mut WindowManager<P, C>,
ui_caches: &mut FxHashMap<window::Id, user_interface::Cache>,
+ is_window_opening: &mut bool,
) where
P: Program,
C: Compositor<Renderer = P::Renderer> + 'static,
@@ -1168,14 +1220,30 @@ fn run_action<P, C>(
settings,
title: program.title(id),
monitor,
+ on_open: channel,
})
.expect("Send control action");
- let _ = channel.send(id);
+ *is_window_opening = true;
}
window::Action::Close(id) => {
- let _ = window_manager.remove(id);
let _ = ui_caches.remove(&id);
+ let _ = interfaces.remove(&id);
+
+ if let Some(window) = window_manager.remove(id) {
+ if clipboard.window_id() == Some(window.raw.id()) {
+ *clipboard = window_manager
+ .first()
+ .map(|window| window.raw.clone())
+ .map(Clipboard::connect)
+ .unwrap_or_else(Clipboard::unconnected);
+ }
+
+ events.push((
+ id,
+ core::Event::Window(core::window::Event::Closed),
+ ));
+ }
}
window::Action::GetOldest(channel) => {
let id =
@@ -1235,7 +1303,7 @@ fn run_action<P, C>(
}
}
window::Action::GetPosition(id, channel) => {
- if let Some(window) = window_manager.get_mut(id) {
+ if let Some(window) = window_manager.get(id) {
let position = window
.raw
.inner_position()
@@ -1250,6 +1318,13 @@ fn run_action<P, C>(
let _ = channel.send(position);
}
}
+ window::Action::GetScaleFactor(id, channel) => {
+ if let Some(window) = window_manager.get_mut(id) {
+ let scale_factor = window.raw.scale_factor();
+
+ let _ = channel.send(scale_factor as f32);
+ }
+ }
window::Action::Move(id, position) => {
if let Some(window) = window_manager.get_mut(id) {
window.raw.set_outer_position(
@@ -1360,6 +1435,16 @@ fn run_action<P, C>(
));
}
}
+ window::Action::EnableMousePassthrough(id) => {
+ if let Some(window) = window_manager.get_mut(id) {
+ let _ = window.raw.set_cursor_hittest(false);
+ }
+ }
+ window::Action::DisableMousePassthrough(id) => {
+ if let Some(window) = window_manager.get_mut(id) {
+ let _ = window.raw.set_cursor_hittest(true);
+ }
+ }
},
Action::System(action) => match action {
system::Action::QueryInformation(_channel) => {
diff --git a/winit/src/program/window_manager.rs b/winit/src/program/window_manager.rs
index fcbf79f6..3d22e155 100644
--- a/winit/src/program/window_manager.rs
+++ b/winit/src/program/window_manager.rs
@@ -74,12 +74,20 @@ where
self.entries.is_empty()
}
+ pub fn first(&self) -> Option<&Window<P, C>> {
+ self.entries.first_key_value().map(|(_id, window)| window)
+ }
+
pub fn iter_mut(
&mut self,
) -> impl Iterator<Item = (Id, &mut Window<P, C>)> {
self.entries.iter_mut().map(|(k, v)| (*k, v))
}
+ pub fn get(&self, id: Id) -> Option<&Window<P, C>> {
+ self.entries.get(&id)
+ }
+
pub fn get_mut(&mut self, id: Id) -> Option<&mut Window<P, C>> {
self.entries.get_mut(&id)
}
diff --git a/winit/src/system.rs b/winit/src/system.rs
index 7997f311..361135be 100644
--- a/winit/src/system.rs
+++ b/winit/src/system.rs
@@ -5,7 +5,7 @@ use crate::runtime::{self, Task};
/// Query for available system information.
pub fn fetch_information() -> Task<Information> {
- Task::oneshot(|channel| {
+ runtime::task::oneshot(|channel| {
runtime::Action::System(Action::QueryInformation(channel))
})
}