summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/check.yml2
-rw-r--r--.github/workflows/document.yml2
-rw-r--r--Cargo.toml5
-rw-r--r--benches/wgpu.rs101
-rw-r--r--core/Cargo.toml1
-rw-r--r--core/src/angle.rs75
-rw-r--r--core/src/content_fit.rs17
-rw-r--r--core/src/image.rs211
-rw-r--r--core/src/layout.rs2
-rw-r--r--core/src/lib.rs2
-rw-r--r--core/src/mouse/interaction.rs1
-rw-r--r--core/src/overlay.rs2
-rw-r--r--core/src/rectangle.rs32
-rw-r--r--core/src/renderer/null.rs7
-rw-r--r--core/src/rotation.rs72
-rw-r--r--core/src/size.rs29
-rw-r--r--core/src/svg.rs4
-rw-r--r--core/src/vector.rs3
-rw-r--r--core/src/widget.rs2
-rw-r--r--docs/redirect.html13
-rw-r--r--examples/bezier_tool/src/main.rs57
-rw-r--r--examples/checkbox/src/main.rs11
-rw-r--r--examples/clock/Cargo.toml1
-rw-r--r--examples/clock/src/main.rs2
-rw-r--r--examples/combo_box/src/main.rs9
-rw-r--r--examples/component/src/main.rs8
-rw-r--r--examples/custom_quad/src/main.rs11
-rw-r--r--examples/custom_shader/src/main.rs9
-rw-r--r--examples/custom_widget/src/main.rs11
-rw-r--r--examples/download_progress/src/main.rs12
-rw-r--r--examples/editor/src/main.rs2
-rw-r--r--examples/events/src/main.rs9
-rw-r--r--examples/exit/src/main.rs12
-rw-r--r--examples/ferris/Cargo.toml10
-rw-r--r--examples/ferris/src/main.rs211
-rw-r--r--examples/geometry/src/main.rs11
-rw-r--r--examples/integration/README.md19
-rw-r--r--examples/integration/index.html21
-rw-r--r--examples/integration/src/main.rs577
-rw-r--r--examples/layout/src/main.rs20
-rw-r--r--examples/loading_spinners/src/main.rs10
-rw-r--r--examples/loupe/src/main.rs12
-rw-r--r--examples/modal/src/main.rs368
-rw-r--r--examples/multi_window/src/main.rs13
-rw-r--r--examples/pane_grid/src/main.rs7
-rw-r--r--examples/pokedex/src/main.rs11
-rw-r--r--examples/qr_code/src/main.rs14
-rw-r--r--examples/screenshot/src/main.rs23
-rw-r--r--examples/scrollable/src/main.rs2
-rw-r--r--examples/slider/src/main.rs16
-rw-r--r--examples/stopwatch/src/main.rs11
-rw-r--r--examples/styling/src/main.rs13
-rw-r--r--examples/svg/src/main.rs17
-rw-r--r--examples/system_information/src/main.rs9
-rw-r--r--examples/the_matrix/Cargo.toml13
-rw-r--r--examples/the_matrix/src/main.rs115
-rw-r--r--examples/toast/src/main.rs10
-rw-r--r--examples/todos/src/main.rs14
-rw-r--r--examples/tooltip/src/main.rs11
-rw-r--r--examples/tour/src/main.rs4
-rw-r--r--examples/url_handler/src/main.rs11
-rw-r--r--examples/websocket/src/main.rs13
-rw-r--r--futures/src/backend/native/tokio.rs14
-rw-r--r--graphics/src/cache.rs189
-rw-r--r--graphics/src/cached.rs24
-rw-r--r--graphics/src/geometry.rs2
-rw-r--r--graphics/src/geometry/cache.rs83
-rw-r--r--graphics/src/image.rs75
-rw-r--r--graphics/src/lib.rs4
-rw-r--r--graphics/src/text/editor.rs12
-rw-r--r--renderer/src/fallback.rs36
-rw-r--r--runtime/Cargo.toml1
-rw-r--r--runtime/src/multi_window/state.rs2
-rw-r--r--runtime/src/program/state.rs2
-rw-r--r--runtime/src/window/screenshot.rs16
-rw-r--r--src/application.rs19
-rw-r--r--src/lib.rs5
-rw-r--r--src/window/icon.rs2
-rw-r--r--tiny_skia/src/engine.rs27
-rw-r--r--tiny_skia/src/geometry.rs5
-rw-r--r--tiny_skia/src/layer.rs16
-rw-r--r--tiny_skia/src/lib.rs22
-rw-r--r--tiny_skia/src/raster.rs8
-rw-r--r--tiny_skia/src/vector.rs10
-rw-r--r--wgpu/src/color.rs68
-rw-r--r--wgpu/src/engine.rs7
-rw-r--r--wgpu/src/geometry.rs13
-rw-r--r--wgpu/src/image/atlas.rs53
-rw-r--r--wgpu/src/image/atlas/allocator.rs4
-rw-r--r--wgpu/src/image/atlas/layer.rs8
-rw-r--r--wgpu/src/image/cache.rs43
-rw-r--r--wgpu/src/image/mod.rs91
-rw-r--r--wgpu/src/image/raster.rs17
-rw-r--r--wgpu/src/image/vector.rs8
-rw-r--r--wgpu/src/layer.rs13
-rw-r--r--wgpu/src/lib.rs42
-rw-r--r--wgpu/src/primitive.rs2
-rw-r--r--wgpu/src/settings.rs26
-rw-r--r--wgpu/src/shader/image.wgsl37
-rw-r--r--wgpu/src/text.rs178
-rw-r--r--wgpu/src/triangle.rs16
-rw-r--r--wgpu/src/window/compositor.rs26
-rw-r--r--widget/src/canvas.rs1
-rw-r--r--widget/src/container.rs62
-rw-r--r--widget/src/helpers.rs460
-rw-r--r--widget/src/image.rs87
-rw-r--r--widget/src/image/viewer.rs14
-rw-r--r--widget/src/keyed/column.rs2
-rw-r--r--widget/src/lib.rs3
-rw-r--r--widget/src/mouse_area.rs2
-rw-r--r--widget/src/scrollable.rs378
-rw-r--r--widget/src/stack.rs333
-rw-r--r--widget/src/svg.rs80
-rw-r--r--widget/src/text_editor.rs41
-rw-r--r--winit/Cargo.toml2
-rw-r--r--winit/src/application.rs477
-rw-r--r--winit/src/conversion.rs62
-rw-r--r--winit/src/multi_window.rs520
-rw-r--r--winit/src/multi_window/window_manager.rs5
-rw-r--r--winit/src/proxy.rs101
120 files changed, 4077 insertions, 2102 deletions
diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml
index ceac39e7..8f5e65d6 100644
--- a/.github/workflows/check.yml
+++ b/.github/workflows/check.yml
@@ -17,8 +17,6 @@ jobs:
run: cargo build --package tour --target wasm32-unknown-unknown
- name: Check compilation of `todos` example
run: cargo build --package todos --target wasm32-unknown-unknown
- - name: Check compilation of `integration` example
- run: cargo build --package integration --target wasm32-unknown-unknown
widget:
runs-on: ubuntu-latest
diff --git a/.github/workflows/document.yml b/.github/workflows/document.yml
index ba482215..827a2ca8 100644
--- a/.github/workflows/document.yml
+++ b/.github/workflows/document.yml
@@ -27,6 +27,8 @@ jobs:
-p iced
- name: Write CNAME file
run: echo 'docs.iced.rs' > ./target/doc/CNAME
+ - name: Copy redirect file as index.html
+ run: cp docs/redirect.html target/doc/index.html
- name: Publish documentation
if: github.ref == 'refs/heads/master'
uses: peaceiris/actions-gh-pages@v3
diff --git a/Cargo.toml b/Cargo.toml
index 2b6a0d03..06f7d198 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -137,6 +137,7 @@ iced_winit = { version = "0.13.0-dev", path = "winit" }
async-std = "1.0"
bitflags = "2.0"
bytemuck = { version = "1.0", features = ["derive"] }
+bytes = "1.6"
cosmic-text = "0.10"
dark-light = "1.0"
futures = "0.3"
@@ -171,11 +172,11 @@ unicode-segmentation = "1.0"
wasm-bindgen-futures = "0.4"
wasm-timer = "0.2"
web-sys = "=0.3.67"
-web-time = "0.2"
+web-time = "1.1"
wgpu = "0.19"
winapi = "0.3"
window_clipboard = "0.4.1"
-winit = { git = "https://github.com/iced-rs/winit.git", rev = "592bd152f6d5786fae7d918532d7db752c0d164f" }
+winit = { git = "https://github.com/iced-rs/winit.git", rev = "8affa522bc6dcc497d332a28c03491d22a22f5a7" }
[workspace.lints.rust]
rust_2018_idioms = "forbid"
diff --git a/benches/wgpu.rs b/benches/wgpu.rs
index 61b4eb6c..2d308666 100644
--- a/benches/wgpu.rs
+++ b/benches/wgpu.rs
@@ -3,7 +3,7 @@ use criterion::{criterion_group, criterion_main, Bencher, Criterion};
use iced::alignment;
use iced::mouse;
-use iced::widget::{canvas, text};
+use iced::widget::{canvas, stack, text};
use iced::{
Color, Element, Font, Length, Pixels, Point, Rectangle, Size, Theme,
};
@@ -16,6 +16,13 @@ criterion_group!(benches, wgpu_benchmark);
pub fn wgpu_benchmark(c: &mut Criterion) {
c.bench_function("wgpu — canvas (light)", |b| benchmark(b, scene(10)));
c.bench_function("wgpu — canvas (heavy)", |b| benchmark(b, scene(1_000)));
+
+ c.bench_function("wgpu - layered text (light)", |b| {
+ benchmark(b, layered_text(10));
+ });
+ c.bench_function("wgpu - layered text (heavy)", |b| {
+ benchmark(b, layered_text(1_000));
+ });
}
fn benchmark(
@@ -63,7 +70,8 @@ fn benchmark(
Some(Antialiasing::MSAAx4),
);
- let mut renderer = Renderer::new(&engine, Font::DEFAULT, Pixels::from(16));
+ let mut renderer =
+ Renderer::new(&device, &engine, Font::DEFAULT, Pixels::from(16));
let viewport =
graphics::Viewport::with_physical_size(Size::new(3840, 2160), 2.0);
@@ -125,52 +133,59 @@ fn benchmark(
});
}
-fn scene<'a, Message: 'a, Theme: 'a>(
- n: usize,
-) -> Element<'a, Message, Theme, Renderer> {
+fn scene<'a, Message: 'a>(n: usize) -> Element<'a, Message, Theme, Renderer> {
+ struct Scene {
+ n: usize,
+ }
+
+ impl<Message, Theme> canvas::Program<Message, Theme, Renderer> for Scene {
+ type State = canvas::Cache<Renderer>;
+
+ fn draw(
+ &self,
+ cache: &Self::State,
+ renderer: &Renderer,
+ _theme: &Theme,
+ bounds: Rectangle,
+ _cursor: mouse::Cursor,
+ ) -> Vec<canvas::Geometry<Renderer>> {
+ vec![cache.draw(renderer, bounds.size(), |frame| {
+ for i in 0..self.n {
+ frame.fill_rectangle(
+ Point::new(0.0, i as f32),
+ Size::new(10.0, 10.0),
+ Color::WHITE,
+ );
+ }
+
+ for i in 0..self.n {
+ frame.fill_text(canvas::Text {
+ content: i.to_string(),
+ position: Point::new(0.0, i as f32),
+ color: Color::BLACK,
+ size: Pixels::from(16),
+ line_height: text::LineHeight::default(),
+ font: Font::DEFAULT,
+ horizontal_alignment: alignment::Horizontal::Left,
+ vertical_alignment: alignment::Vertical::Top,
+ shaping: text::Shaping::Basic,
+ });
+ }
+ })]
+ }
+ }
+
canvas(Scene { n })
.width(Length::Fill)
.height(Length::Fill)
.into()
}
-struct Scene {
+fn layered_text<'a, Message: 'a>(
n: usize,
-}
-
-impl<Message, Theme> canvas::Program<Message, Theme, Renderer> for Scene {
- type State = canvas::Cache<Renderer>;
-
- fn draw(
- &self,
- cache: &Self::State,
- renderer: &Renderer,
- _theme: &Theme,
- bounds: Rectangle,
- _cursor: mouse::Cursor,
- ) -> Vec<canvas::Geometry<Renderer>> {
- vec![cache.draw(renderer, bounds.size(), |frame| {
- for i in 0..self.n {
- frame.fill_rectangle(
- Point::new(0.0, i as f32),
- Size::new(10.0, 10.0),
- Color::WHITE,
- );
- }
-
- for i in 0..self.n {
- frame.fill_text(canvas::Text {
- content: i.to_string(),
- position: Point::new(0.0, i as f32),
- color: Color::BLACK,
- size: Pixels::from(16),
- line_height: text::LineHeight::default(),
- font: Font::DEFAULT,
- horizontal_alignment: alignment::Horizontal::Left,
- vertical_alignment: alignment::Vertical::Top,
- shaping: text::Shaping::Basic,
- });
- }
- })]
- }
+) -> Element<'a, Message, Theme, Renderer> {
+ stack((0..n).map(|i| text(format!("I am paragraph {i}!")).into()))
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .into()
}
diff --git a/core/Cargo.toml b/core/Cargo.toml
index 7bd37021..3c557bca 100644
--- a/core/Cargo.toml
+++ b/core/Cargo.toml
@@ -19,6 +19,7 @@ advanced = []
[dependencies]
bitflags.workspace = true
+bytes.workspace = true
glam.workspace = true
log.workspace = true
num-traits.workspace = true
diff --git a/core/src/angle.rs b/core/src/angle.rs
index dc3c0e93..9c8a9b24 100644
--- a/core/src/angle.rs
+++ b/core/src/angle.rs
@@ -1,12 +1,17 @@
use crate::{Point, Rectangle, Vector};
use std::f32::consts::{FRAC_PI_2, PI};
-use std::ops::{Add, AddAssign, Div, Mul, RangeInclusive, Sub, SubAssign};
+use std::ops::{Add, AddAssign, Div, Mul, RangeInclusive, Rem, Sub, SubAssign};
/// Degrees
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
pub struct Degrees(pub f32);
+impl Degrees {
+ /// The range of degrees of a circle.
+ pub const RANGE: RangeInclusive<Self> = Self(0.0)..=Self(360.0);
+}
+
impl PartialEq<f32> for Degrees {
fn eq(&self, other: &f32) -> bool {
self.0.eq(other)
@@ -19,6 +24,52 @@ impl PartialOrd<f32> for Degrees {
}
}
+impl From<f32> for Degrees {
+ fn from(degrees: f32) -> Self {
+ Self(degrees)
+ }
+}
+
+impl From<u8> for Degrees {
+ fn from(degrees: u8) -> Self {
+ Self(f32::from(degrees))
+ }
+}
+
+impl From<Degrees> for f32 {
+ fn from(degrees: Degrees) -> Self {
+ degrees.0
+ }
+}
+
+impl From<Degrees> for f64 {
+ fn from(degrees: Degrees) -> Self {
+ Self::from(degrees.0)
+ }
+}
+
+impl Mul<f32> for Degrees {
+ type Output = Degrees;
+
+ fn mul(self, rhs: f32) -> Self::Output {
+ Self(self.0 * rhs)
+ }
+}
+
+impl num_traits::FromPrimitive for Degrees {
+ fn from_i64(n: i64) -> Option<Self> {
+ Some(Self(n as f32))
+ }
+
+ fn from_u64(n: u64) -> Option<Self> {
+ Some(Self(n as f32))
+ }
+
+ fn from_f64(n: f64) -> Option<Self> {
+ Some(Self(n as f32))
+ }
+}
+
/// Radians
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
pub struct Radians(pub f32);
@@ -65,6 +116,12 @@ impl From<u8> for Radians {
}
}
+impl From<Radians> for f32 {
+ fn from(radians: Radians) -> Self {
+ radians.0
+ }
+}
+
impl From<Radians> for f64 {
fn from(radians: Radians) -> Self {
Self::from(radians.0)
@@ -107,6 +164,14 @@ impl Add for Radians {
}
}
+impl Add<Degrees> for Radians {
+ type Output = Self;
+
+ fn add(self, rhs: Degrees) -> Self::Output {
+ Self(self.0 + rhs.0.to_radians())
+ }
+}
+
impl AddAssign for Radians {
fn add_assign(&mut self, rhs: Radians) {
self.0 = self.0 + rhs.0;
@@ -153,6 +218,14 @@ impl Div for Radians {
}
}
+impl Rem for Radians {
+ type Output = Self;
+
+ fn rem(self, rhs: Self) -> Self::Output {
+ Self(self.0 % rhs.0)
+ }
+}
+
impl PartialEq<f32> for Radians {
fn eq(&self, other: &f32) -> bool {
self.0.eq(other)
diff --git a/core/src/content_fit.rs b/core/src/content_fit.rs
index 6bbedc7a..19642716 100644
--- a/core/src/content_fit.rs
+++ b/core/src/content_fit.rs
@@ -1,6 +1,8 @@
//! Control the fit of some content (like an image) within a space.
use crate::Size;
+use std::fmt;
+
/// The strategy used to fit the contents of a widget to its bounding box.
///
/// Each variant of this enum is a strategy that can be applied for resolving
@@ -11,7 +13,7 @@ use crate::Size;
/// in CSS, see [Mozilla's docs][1], or run the `tour` example
///
/// [1]: https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit
-#[derive(Debug, Hash, Clone, Copy, PartialEq, Eq)]
+#[derive(Debug, Hash, Clone, Copy, PartialEq, Eq, Default)]
pub enum ContentFit {
/// Scale as big as it can be without needing to crop or hide parts.
///
@@ -23,6 +25,7 @@ pub enum ContentFit {
/// This is a great fit for when you need to display an image without losing
/// any part of it, particularly when the image itself is the focus of the
/// screen.
+ #[default]
Contain,
/// Scale the image to cover all of the bounding box, cropping if needed.
@@ -117,3 +120,15 @@ impl ContentFit {
}
}
}
+
+impl fmt::Display for ContentFit {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.write_str(match self {
+ ContentFit::Contain => "Contain",
+ ContentFit::Cover => "Cover",
+ ContentFit::Fill => "Fill",
+ ContentFit::None => "None",
+ ContentFit::ScaleDown => "Scale Down",
+ })
+ }
+}
diff --git a/core/src/image.rs b/core/src/image.rs
index dc74e5c1..82ecdd0f 100644
--- a/core/src/image.rs
+++ b/core/src/image.rs
@@ -1,16 +1,45 @@
//! Load and draw raster graphics.
-use crate::{Rectangle, Size};
+pub use bytes::Bytes;
+
+use crate::{Radians, Rectangle, Size};
use rustc_hash::FxHasher;
-use std::hash::{Hash, Hasher as _};
-use std::path::PathBuf;
-use std::sync::Arc;
+use std::hash::{Hash, Hasher};
+use std::path::{Path, PathBuf};
/// A handle of some image data.
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct Handle {
- id: u64,
- data: Data,
+#[derive(Clone, PartialEq, Eq)]
+pub enum Handle {
+ /// A file handle. The image data will be read
+ /// from the file path.
+ ///
+ /// Use [`from_path`] to create this variant.
+ ///
+ /// [`from_path`]: Self::from_path
+ Path(Id, PathBuf),
+
+ /// A handle pointing to some encoded image bytes in-memory.
+ ///
+ /// Use [`from_bytes`] to create this variant.
+ ///
+ /// [`from_bytes`]: Self::from_bytes
+ Bytes(Id, Bytes),
+
+ /// A handle pointing to decoded image pixels in RGBA format.
+ ///
+ /// Use [`from_rgba`] to create this variant.
+ ///
+ /// [`from_rgba`]: Self::from_rgba
+ Rgba {
+ /// The id of this handle.
+ id: Id,
+ /// The width of the image.
+ width: u32,
+ /// The height of the image.
+ height: u32,
+ /// The pixels.
+ pixels: Bytes,
+ },
}
impl Handle {
@@ -18,56 +47,48 @@ impl Handle {
///
/// Makes an educated guess about the image format by examining the data in the file.
pub fn from_path<T: Into<PathBuf>>(path: T) -> Handle {
- Self::from_data(Data::Path(path.into()))
- }
+ let path = path.into();
- /// Creates an image [`Handle`] containing the image pixels directly. This
- /// function expects the input data to be provided as a `Vec<u8>` of RGBA
- /// pixels.
- ///
- /// This is useful if you have already decoded your image.
- pub fn from_pixels(
- width: u32,
- height: u32,
- pixels: impl AsRef<[u8]> + Send + Sync + 'static,
- ) -> Handle {
- Self::from_data(Data::Rgba {
- width,
- height,
- pixels: Bytes::new(pixels),
- })
+ Self::Path(Id::path(&path), path)
}
- /// Creates an image [`Handle`] containing the image data directly.
+ /// Creates an image [`Handle`] containing the encoded image data directly.
///
/// Makes an educated guess about the image format by examining the given data.
///
/// This is useful if you already have your image loaded in-memory, maybe
/// because you downloaded or generated it procedurally.
- pub fn from_memory(
- bytes: impl AsRef<[u8]> + Send + Sync + 'static,
- ) -> Handle {
- Self::from_data(Data::Bytes(Bytes::new(bytes)))
+ pub fn from_bytes(bytes: impl Into<Bytes>) -> Handle {
+ Self::Bytes(Id::unique(), bytes.into())
}
- fn from_data(data: Data) -> Handle {
- let mut hasher = FxHasher::default();
- data.hash(&mut hasher);
-
- Handle {
- id: hasher.finish(),
- data,
+ /// Creates an image [`Handle`] containing the decoded image pixels directly.
+ ///
+ /// This function expects the pixel data to be provided as a collection of [`Bytes`]
+ /// of RGBA pixels. Therefore, the length of the pixel data should always be
+ /// `width * height * 4`.
+ ///
+ /// This is useful if you have already decoded your image.
+ pub fn from_rgba(
+ width: u32,
+ height: u32,
+ pixels: impl Into<Bytes>,
+ ) -> Handle {
+ Self::Rgba {
+ id: Id::unique(),
+ width,
+ height,
+ pixels: pixels.into(),
}
}
/// Returns the unique identifier of the [`Handle`].
- pub fn id(&self) -> u64 {
- self.id
- }
-
- /// Returns a reference to the image [`Data`].
- pub fn data(&self) -> &Data {
- &self.data
+ pub fn id(&self) -> Id {
+ match self {
+ Handle::Path(id, _)
+ | Handle::Bytes(id, _)
+ | Handle::Rgba { id, .. } => *id,
+ }
}
}
@@ -80,90 +101,46 @@ where
}
}
-impl Hash for Handle {
- fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
- self.id.hash(state);
- }
-}
-
-/// A wrapper around raw image data.
-///
-/// It behaves like a `&[u8]`.
-#[derive(Clone)]
-pub struct Bytes(Arc<dyn AsRef<[u8]> + Send + Sync + 'static>);
-
-impl Bytes {
- /// Creates new [`Bytes`] around `data`.
- pub fn new(data: impl AsRef<[u8]> + Send + Sync + 'static) -> Self {
- Self(Arc::new(data))
- }
-}
-
-impl std::fmt::Debug for Bytes {
+impl std::fmt::Debug for Handle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- self.0.as_ref().as_ref().fmt(f)
+ match self {
+ Self::Path(_, path) => write!(f, "Path({path:?})"),
+ Self::Bytes(_, _) => write!(f, "Bytes(...)"),
+ Self::Rgba { width, height, .. } => {
+ write!(f, "Pixels({width} * {height})")
+ }
+ }
}
}
-impl std::hash::Hash for Bytes {
- fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
- self.0.as_ref().as_ref().hash(state);
- }
-}
+/// The unique identifier of some [`Handle`] data.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct Id(_Id);
-impl PartialEq for Bytes {
- fn eq(&self, other: &Self) -> bool {
- let a = self.as_ref();
- let b = other.as_ref();
- core::ptr::eq(a, b) || a == b
- }
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+enum _Id {
+ Unique(u64),
+ Hash(u64),
}
-impl Eq for Bytes {}
-
-impl AsRef<[u8]> for Bytes {
- fn as_ref(&self) -> &[u8] {
- self.0.as_ref().as_ref()
- }
-}
+impl Id {
+ fn unique() -> Self {
+ use std::sync::atomic::{self, AtomicU64};
-impl std::ops::Deref for Bytes {
- type Target = [u8];
+ static NEXT_ID: AtomicU64 = AtomicU64::new(0);
- fn deref(&self) -> &[u8] {
- self.0.as_ref().as_ref()
+ Self(_Id::Unique(NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed)))
}
-}
-/// The data of a raster image.
-#[derive(Clone, PartialEq, Eq, Hash)]
-pub enum Data {
- /// File data
- Path(PathBuf),
+ fn path(path: impl AsRef<Path>) -> Self {
+ let hash = {
+ let mut hasher = FxHasher::default();
+ path.as_ref().hash(&mut hasher);
- /// In-memory data
- Bytes(Bytes),
-
- /// Decoded image pixels in RGBA format.
- Rgba {
- /// The width of the image.
- width: u32,
- /// The height of the image.
- height: u32,
- /// The pixels.
- pixels: Bytes,
- },
-}
+ hasher.finish()
+ };
-impl std::fmt::Debug for Data {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- Data::Path(path) => write!(f, "Path({path:?})"),
- Data::Bytes(_) => write!(f, "Bytes(...)"),
- Data::Rgba { width, height, .. } => {
- write!(f, "Pixels({width} * {height})")
- }
- }
+ Self(_Id::Hash(hash))
}
}
@@ -184,7 +161,7 @@ pub trait Renderer: crate::Renderer {
/// The image Handle to be displayed. Iced exposes its own default implementation of a [`Handle`]
///
/// [`Handle`]: Self::Handle
- type Handle: Clone + Hash;
+ type Handle: Clone;
/// Returns the dimensions of an image for the given [`Handle`].
fn measure_image(&self, handle: &Self::Handle) -> Size<u32>;
@@ -196,5 +173,7 @@ pub trait Renderer: crate::Renderer {
handle: Self::Handle,
filter_method: FilterMethod,
bounds: Rectangle,
+ rotation: Radians,
+ opacity: f32,
);
}
diff --git a/core/src/layout.rs b/core/src/layout.rs
index 95720aba..98d05602 100644
--- a/core/src/layout.rs
+++ b/core/src/layout.rs
@@ -54,7 +54,7 @@ impl<'a> Layout<'a> {
}
/// Returns an iterator over the [`Layout`] of the children of a [`Node`].
- pub fn children(self) -> impl Iterator<Item = Layout<'a>> {
+ pub fn children(self) -> impl DoubleEndedIterator<Item = Layout<'a>> {
self.node.children().iter().map(move |node| {
Layout::with_offset(
Vector::new(self.position.x, self.position.y),
diff --git a/core/src/lib.rs b/core/src/lib.rs
index feda4fb4..32156441 100644
--- a/core/src/lib.rs
+++ b/core/src/lib.rs
@@ -39,6 +39,7 @@ mod padding;
mod pixels;
mod point;
mod rectangle;
+mod rotation;
mod shadow;
mod shell;
mod size;
@@ -64,6 +65,7 @@ pub use pixels::Pixels;
pub use point::Point;
pub use rectangle::Rectangle;
pub use renderer::Renderer;
+pub use rotation::Rotation;
pub use shadow::Shadow;
pub use shell::Shell;
pub use size::Size;
diff --git a/core/src/mouse/interaction.rs b/core/src/mouse/interaction.rs
index 6ad66229..065eb8e7 100644
--- a/core/src/mouse/interaction.rs
+++ b/core/src/mouse/interaction.rs
@@ -3,6 +3,7 @@
#[allow(missing_docs)]
pub enum Interaction {
#[default]
+ None,
Idle,
Pointer,
Grab,
diff --git a/core/src/overlay.rs b/core/src/overlay.rs
index 03076a30..3a57fe16 100644
--- a/core/src/overlay.rs
+++ b/core/src/overlay.rs
@@ -79,7 +79,7 @@ where
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
- mouse::Interaction::Idle
+ mouse::Interaction::None
}
/// Returns true if the cursor is over the [`Overlay`].
diff --git a/core/src/rectangle.rs b/core/src/rectangle.rs
index 2ab50137..1556e072 100644
--- a/core/src/rectangle.rs
+++ b/core/src/rectangle.rs
@@ -1,6 +1,6 @@
-use crate::{Point, Size, Vector};
+use crate::{Point, Radians, Size, Vector};
-/// A rectangle.
+/// An axis-aligned rectangle.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Rectangle<T = f32> {
/// X coordinate of the top-left corner.
@@ -172,6 +172,18 @@ impl Rectangle<f32> {
height: self.height + amount * 2.0,
}
}
+
+ /// Rotates the [`Rectangle`] and returns the smallest [`Rectangle`]
+ /// containing it.
+ pub fn rotate(self, rotation: Radians) -> Self {
+ let size = self.size().rotate(rotation);
+ let position = Point::new(
+ self.center_x() - size.width / 2.0,
+ self.center_y() - size.height / 2.0,
+ );
+
+ Self::new(position, size)
+ }
}
impl std::ops::Mul<f32> for Rectangle<f32> {
@@ -227,3 +239,19 @@ where
}
}
}
+
+impl<T> std::ops::Mul<Vector<T>> for Rectangle<T>
+where
+ T: std::ops::Mul<Output = T> + Copy,
+{
+ type Output = Rectangle<T>;
+
+ fn mul(self, scale: Vector<T>) -> Self {
+ Rectangle {
+ x: self.x * scale.x,
+ y: self.y * scale.y,
+ width: self.width * scale.x,
+ height: self.height * scale.y,
+ }
+ }
+}
diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs
index fe38ec87..e8709dbc 100644
--- a/core/src/renderer/null.rs
+++ b/core/src/renderer/null.rs
@@ -4,7 +4,8 @@ use crate::renderer::{self, Renderer};
use crate::svg;
use crate::text::{self, Text};
use crate::{
- Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation,
+ Background, Color, Font, Pixels, Point, Radians, Rectangle, Size,
+ Transformation,
};
impl Renderer for () {
@@ -171,6 +172,8 @@ impl image::Renderer for () {
_handle: Self::Handle,
_filter_method: image::FilterMethod,
_bounds: Rectangle,
+ _rotation: Radians,
+ _opacity: f32,
) {
}
}
@@ -185,6 +188,8 @@ impl svg::Renderer for () {
_handle: svg::Handle,
_color: Option<Color>,
_bounds: Rectangle,
+ _rotation: Radians,
+ _opacity: f32,
) {
}
}
diff --git a/core/src/rotation.rs b/core/src/rotation.rs
new file mode 100644
index 00000000..afa8d79e
--- /dev/null
+++ b/core/src/rotation.rs
@@ -0,0 +1,72 @@
+//! Control the rotation of some content (like an image) within a space.
+use crate::{Degrees, Radians, Size};
+
+/// The strategy used to rotate the content.
+///
+/// This is used to control the behavior of the layout when the content is rotated
+/// by a certain angle.
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum Rotation {
+ /// The element will float while rotating. The layout will be kept exactly as it was
+ /// before the rotation.
+ ///
+ /// This is especially useful when used for animations, as it will avoid the
+ /// layout being shifted or resized when smoothly i.e. an icon.
+ ///
+ /// This is the default.
+ Floating(Radians),
+ /// The element will be solid while rotating. The layout will be adjusted to fit
+ /// the rotated content.
+ ///
+ /// This allows you to rotate an image and have the layout adjust to fit the new
+ /// size of the image.
+ Solid(Radians),
+}
+
+impl Rotation {
+ /// Returns the angle of the [`Rotation`] in [`Radians`].
+ pub fn radians(self) -> Radians {
+ match self {
+ Rotation::Floating(radians) | Rotation::Solid(radians) => radians,
+ }
+ }
+
+ /// Returns a mutable reference to the angle of the [`Rotation`] in [`Radians`].
+ pub fn radians_mut(&mut self) -> &mut Radians {
+ match self {
+ Rotation::Floating(radians) | Rotation::Solid(radians) => radians,
+ }
+ }
+
+ /// Returns the angle of the [`Rotation`] in [`Degrees`].
+ pub fn degrees(self) -> Degrees {
+ Degrees(self.radians().0.to_degrees())
+ }
+
+ /// Applies the [`Rotation`] to the given [`Size`], returning
+ /// the minimum [`Size`] containing the rotated one.
+ pub fn apply(self, size: Size) -> Size {
+ match self {
+ Self::Floating(_) => size,
+ Self::Solid(rotation) => size.rotate(rotation),
+ }
+ }
+}
+
+impl Default for Rotation {
+ fn default() -> Self {
+ Self::Floating(Radians(0.0))
+ }
+}
+
+impl From<Radians> for Rotation {
+ fn from(radians: Radians) -> Self {
+ Self::Floating(radians)
+ }
+}
+
+impl From<f32> for Rotation {
+ fn from(radians: f32) -> Self {
+ Self::Floating(Radians(radians))
+ }
+}
diff --git a/core/src/size.rs b/core/src/size.rs
index c2b5671a..d7459355 100644
--- a/core/src/size.rs
+++ b/core/src/size.rs
@@ -1,4 +1,4 @@
-use crate::Vector;
+use crate::{Radians, Vector};
/// An amount of space in 2 dimensions.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
@@ -51,6 +51,19 @@ impl Size {
height: self.height + other.height,
}
}
+
+ /// Rotates the given [`Size`] and returns the minimum [`Size`]
+ /// containing it.
+ pub fn rotate(self, rotation: Radians) -> Size {
+ let radians = f32::from(rotation);
+
+ Size {
+ width: (self.width * radians.cos()).abs()
+ + (self.height * radians.sin()).abs(),
+ height: (self.width * radians.sin()).abs()
+ + (self.height * radians.cos()).abs(),
+ }
+ }
}
impl<T> From<[T; 2]> for Size<T> {
@@ -113,3 +126,17 @@ where
}
}
}
+
+impl<T> std::ops::Mul<Vector<T>> for Size<T>
+where
+ T: std::ops::Mul<Output = T> + Copy,
+{
+ type Output = Size<T>;
+
+ fn mul(self, scale: Vector<T>) -> Self::Output {
+ Size {
+ width: self.width * scale.x,
+ height: self.height * scale.y,
+ }
+ }
+}
diff --git a/core/src/svg.rs b/core/src/svg.rs
index 0106e0c2..946b8156 100644
--- a/core/src/svg.rs
+++ b/core/src/svg.rs
@@ -1,5 +1,5 @@
//! Load and draw vector graphics.
-use crate::{Color, Rectangle, Size};
+use crate::{Color, Radians, Rectangle, Size};
use rustc_hash::FxHasher;
use std::borrow::Cow;
@@ -100,5 +100,7 @@ pub trait Renderer: crate::Renderer {
handle: Handle,
color: Option<Color>,
bounds: Rectangle,
+ rotation: Radians,
+ opacity: f32,
);
}
diff --git a/core/src/vector.rs b/core/src/vector.rs
index 1380c3b3..049e648f 100644
--- a/core/src/vector.rs
+++ b/core/src/vector.rs
@@ -18,6 +18,9 @@ impl<T> Vector<T> {
impl Vector {
/// The zero [`Vector`].
pub const ZERO: Self = Self::new(0.0, 0.0);
+
+ /// The unit [`Vector`].
+ pub const UNIT: Self = Self::new(0.0, 0.0);
}
impl<T> std::ops::Add for Vector<T>
diff --git a/core/src/widget.rs b/core/src/widget.rs
index 58a9f19b..b02e3a4f 100644
--- a/core/src/widget.rs
+++ b/core/src/widget.rs
@@ -137,7 +137,7 @@ where
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
- mouse::Interaction::Idle
+ mouse::Interaction::None
}
/// Returns the overlay of the [`Widget`], if there is any.
diff --git a/docs/redirect.html b/docs/redirect.html
new file mode 100644
index 00000000..7b2cef51
--- /dev/null
+++ b/docs/redirect.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Redirecting...</title>
+ <meta http-equiv="refresh" content="0; URL='/iced/'" />
+</head>
+<body>
+ <p>If you are not redirected automatically, follow this <a href="/iced/">link</a>.</p>
+</body>
+</html>
diff --git a/examples/bezier_tool/src/main.rs b/examples/bezier_tool/src/main.rs
index 289c919b..29df3eeb 100644
--- a/examples/bezier_tool/src/main.rs
+++ b/examples/bezier_tool/src/main.rs
@@ -1,9 +1,11 @@
//! This example showcases an interactive `Canvas` for drawing Bézier curves.
-use iced::widget::{button, column, text};
-use iced::{Alignment, Element, Length};
+use iced::alignment;
+use iced::widget::{button, container, horizontal_space, hover};
+use iced::{Element, Length, Theme};
pub fn main() -> iced::Result {
iced::program("Bezier Tool - Iced", Example::update, Example::view)
+ .theme(|_| Theme::CatppuccinMocha)
.antialiasing(true)
.run()
}
@@ -35,16 +37,22 @@ impl Example {
}
fn view(&self) -> Element<Message> {
- column![
- text("Bezier tool example").width(Length::Shrink).size(50),
+ container(hover(
self.bezier.view(&self.curves).map(Message::AddCurve),
- button("Clear")
- .style(button::danger)
- .on_press(Message::Clear),
- ]
+ if self.curves.is_empty() {
+ container(horizontal_space())
+ } else {
+ container(
+ button("Clear")
+ .style(button::danger)
+ .on_press(Message::Clear),
+ )
+ .padding(10)
+ .width(Length::Fill)
+ .align_x(alignment::Horizontal::Right)
+ },
+ ))
.padding(20)
- .spacing(20)
- .align_items(Alignment::Center)
.into()
}
}
@@ -139,22 +147,24 @@ mod bezier {
&self,
state: &Self::State,
renderer: &Renderer,
- _theme: &Theme,
+ theme: &Theme,
bounds: Rectangle,
cursor: mouse::Cursor,
) -> Vec<Geometry> {
let content =
self.state.cache.draw(renderer, bounds.size(), |frame| {
- Curve::draw_all(self.curves, frame);
+ Curve::draw_all(self.curves, frame, theme);
frame.stroke(
&Path::rectangle(Point::ORIGIN, frame.size()),
- Stroke::default().with_width(2.0),
+ Stroke::default()
+ .with_width(2.0)
+ .with_color(theme.palette().text),
);
});
if let Some(pending) = state {
- vec![content, pending.draw(renderer, bounds, cursor)]
+ vec![content, pending.draw(renderer, theme, bounds, cursor)]
} else {
vec![content]
}
@@ -182,7 +192,7 @@ mod bezier {
}
impl Curve {
- fn draw_all(curves: &[Curve], frame: &mut Frame) {
+ fn draw_all(curves: &[Curve], frame: &mut Frame, theme: &Theme) {
let curves = Path::new(|p| {
for curve in curves {
p.move_to(curve.from);
@@ -190,7 +200,12 @@ mod bezier {
}
});
- frame.stroke(&curves, Stroke::default().with_width(2.0));
+ frame.stroke(
+ &curves,
+ Stroke::default()
+ .with_width(2.0)
+ .with_color(theme.palette().text),
+ );
}
}
@@ -204,6 +219,7 @@ mod bezier {
fn draw(
&self,
renderer: &Renderer,
+ theme: &Theme,
bounds: Rectangle,
cursor: mouse::Cursor,
) -> Geometry {
@@ -213,7 +229,12 @@ mod bezier {
match *self {
Pending::One { from } => {
let line = Path::line(from, cursor_position);
- frame.stroke(&line, Stroke::default().with_width(2.0));
+ frame.stroke(
+ &line,
+ Stroke::default()
+ .with_width(2.0)
+ .with_color(theme.palette().text),
+ );
}
Pending::Two { from, to } => {
let curve = Curve {
@@ -222,7 +243,7 @@ mod bezier {
control: cursor_position,
};
- Curve::draw_all(&[curve], &mut frame);
+ Curve::draw_all(&[curve], &mut frame, theme);
}
};
}
diff --git a/examples/checkbox/src/main.rs b/examples/checkbox/src/main.rs
index 38949336..bec4a954 100644
--- a/examples/checkbox/src/main.rs
+++ b/examples/checkbox/src/main.rs
@@ -1,5 +1,5 @@
-use iced::widget::{checkbox, column, container, row, text};
-use iced::{Element, Font, Length};
+use iced::widget::{center, checkbox, column, row, text};
+use iced::{Element, Font};
const ICON_FONT: Font = Font::with_name("icons");
@@ -68,11 +68,6 @@ impl Example {
let content =
column![default_checkbox, checkboxes, custom_checkbox].spacing(20);
- container(content)
- .width(Length::Fill)
- .height(Length::Fill)
- .center_x()
- .center_y()
- .into()
+ center(content).into()
}
}
diff --git a/examples/clock/Cargo.toml b/examples/clock/Cargo.toml
index 2d3d5908..dc2e5382 100644
--- a/examples/clock/Cargo.toml
+++ b/examples/clock/Cargo.toml
@@ -10,3 +10,4 @@ iced.workspace = true
iced.features = ["canvas", "tokio", "debug"]
time = { version = "0.3", features = ["local-offset"] }
+tracing-subscriber = "0.3"
diff --git a/examples/clock/src/main.rs b/examples/clock/src/main.rs
index 897f8f1b..d717db36 100644
--- a/examples/clock/src/main.rs
+++ b/examples/clock/src/main.rs
@@ -8,6 +8,8 @@ use iced::{
};
pub fn main() -> iced::Result {
+ tracing_subscriber::fmt::init();
+
iced::program("Clock - Iced", Clock::update, Clock::view)
.subscription(Clock::subscription)
.theme(Clock::theme)
diff --git a/examples/combo_box/src/main.rs b/examples/combo_box/src/main.rs
index 2feb4522..ff759ab4 100644
--- a/examples/combo_box/src/main.rs
+++ b/examples/combo_box/src/main.rs
@@ -1,5 +1,5 @@
use iced::widget::{
- column, combo_box, container, scrollable, text, vertical_space,
+ center, column, combo_box, scrollable, text, vertical_space,
};
use iced::{Alignment, Element, Length};
@@ -68,12 +68,7 @@ impl Example {
.align_items(Alignment::Center)
.spacing(10);
- container(scrollable(content))
- .width(Length::Fill)
- .height(Length::Fill)
- .center_x()
- .center_y()
- .into()
+ center(scrollable(content)).into()
}
}
diff --git a/examples/component/src/main.rs b/examples/component/src/main.rs
index b2c71b3f..5625f12a 100644
--- a/examples/component/src/main.rs
+++ b/examples/component/src/main.rs
@@ -1,5 +1,5 @@
-use iced::widget::container;
-use iced::{Element, Length};
+use iced::widget::center;
+use iced::Element;
use numeric_input::numeric_input;
@@ -27,10 +27,8 @@ impl Component {
}
fn view(&self) -> Element<Message> {
- container(numeric_input(self.value, Message::NumericInputChanged))
+ center(numeric_input(self.value, Message::NumericInputChanged))
.padding(20)
- .height(Length::Fill)
- .center_y()
.into()
}
}
diff --git a/examples/custom_quad/src/main.rs b/examples/custom_quad/src/main.rs
index c093e240..b3eee218 100644
--- a/examples/custom_quad/src/main.rs
+++ b/examples/custom_quad/src/main.rs
@@ -81,8 +81,8 @@ mod quad {
}
}
-use iced::widget::{column, container, slider, text};
-use iced::{Alignment, Color, Element, Length, Shadow, Vector};
+use iced::widget::{center, column, slider, text};
+use iced::{Alignment, Color, Element, Shadow, Vector};
pub fn main() -> iced::Result {
iced::run("Custom Quad - Iced", Example::update, Example::view)
@@ -187,12 +187,7 @@ impl Example {
.max_width(500)
.align_items(Alignment::Center);
- container(content)
- .width(Length::Fill)
- .height(Length::Fill)
- .center_x()
- .center_y()
- .into()
+ center(content).into()
}
}
diff --git a/examples/custom_shader/src/main.rs b/examples/custom_shader/src/main.rs
index aa3dafe9..463b2df9 100644
--- a/examples/custom_shader/src/main.rs
+++ b/examples/custom_shader/src/main.rs
@@ -4,7 +4,7 @@ use scene::Scene;
use iced::time::Instant;
use iced::widget::shader::wgpu;
-use iced::widget::{checkbox, column, container, row, shader, slider, text};
+use iced::widget::{center, checkbox, column, row, shader, slider, text};
use iced::window;
use iced::{Alignment, Color, Element, Length, Subscription};
@@ -123,12 +123,7 @@ impl IcedCubes {
let shader =
shader(&self.scene).width(Length::Fill).height(Length::Fill);
- container(column![shader, controls].align_items(Alignment::Center))
- .width(Length::Fill)
- .height(Length::Fill)
- .center_x()
- .center_y()
- .into()
+ center(column![shader, controls].align_items(Alignment::Center)).into()
}
fn subscription(&self) -> Subscription<Message> {
diff --git a/examples/custom_widget/src/main.rs b/examples/custom_widget/src/main.rs
index aa49ebd0..261dcb81 100644
--- a/examples/custom_widget/src/main.rs
+++ b/examples/custom_widget/src/main.rs
@@ -82,8 +82,8 @@ mod circle {
}
use circle::circle;
-use iced::widget::{column, container, slider, text};
-use iced::{Alignment, Element, Length};
+use iced::widget::{center, column, slider, text};
+use iced::{Alignment, Element};
pub fn main() -> iced::Result {
iced::run("Custom Widget - Iced", Example::update, Example::view)
@@ -122,12 +122,7 @@ impl Example {
.max_width(500)
.align_items(Alignment::Center);
- container(content)
- .width(Length::Fill)
- .height(Length::Fill)
- .center_x()
- .center_y()
- .into()
+ center(content).into()
}
}
diff --git a/examples/download_progress/src/main.rs b/examples/download_progress/src/main.rs
index 9f4769e0..e031ac44 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, column, container, progress_bar, text, Column};
-use iced::{Alignment, Element, Length, Subscription};
+use iced::widget::{button, center, column, progress_bar, text, Column};
+use iced::{Alignment, Element, Subscription};
pub fn main() -> iced::Result {
iced::program("Download Progress - Iced", Example::update, Example::view)
@@ -67,13 +67,7 @@ impl Example {
.spacing(20)
.align_items(Alignment::End);
- container(downloads)
- .width(Length::Fill)
- .height(Length::Fill)
- .center_x()
- .center_y()
- .padding(20)
- .into()
+ center(downloads).padding(20).into()
}
}
diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs
index ed16018a..af05031a 100644
--- a/examples/editor/src/main.rs
+++ b/examples/editor/src/main.rs
@@ -277,7 +277,7 @@ fn action<'a, Message: Clone + 'a>(
label: &'a str,
on_press: Option<Message>,
) -> Element<'a, Message> {
- let action = button(container(content).width(30).center_x());
+ let action = button(container(content).center_x().width(30));
if let Some(on_press) = on_press {
tooltip(
diff --git a/examples/events/src/main.rs b/examples/events/src/main.rs
index bf568c94..999ce8ef 100644
--- a/examples/events/src/main.rs
+++ b/examples/events/src/main.rs
@@ -1,6 +1,6 @@
use iced::alignment;
use iced::event::{self, Event};
-use iced::widget::{button, checkbox, container, text, Column};
+use iced::widget::{button, center, checkbox, text, Column};
use iced::window;
use iced::{Alignment, Command, Element, Length, Subscription};
@@ -84,11 +84,6 @@ impl Events {
.push(toggle)
.push(exit);
- container(content)
- .width(Length::Fill)
- .height(Length::Fill)
- .center_x()
- .center_y()
- .into()
+ center(content).into()
}
}
diff --git a/examples/exit/src/main.rs b/examples/exit/src/main.rs
index 7bed272d..2de97e20 100644
--- a/examples/exit/src/main.rs
+++ b/examples/exit/src/main.rs
@@ -1,6 +1,6 @@
-use iced::widget::{button, column, container};
+use iced::widget::{button, center, column};
use iced::window;
-use iced::{Alignment, Command, Element, Length};
+use iced::{Alignment, Command, Element};
pub fn main() -> iced::Result {
iced::program("Exit - Iced", Exit::update, Exit::view).run()
@@ -46,12 +46,6 @@ impl Exit {
.spacing(10)
.align_items(Alignment::Center);
- container(content)
- .width(Length::Fill)
- .height(Length::Fill)
- .padding(20)
- .center_x()
- .center_y()
- .into()
+ center(content).padding(20).into()
}
}
diff --git a/examples/ferris/Cargo.toml b/examples/ferris/Cargo.toml
new file mode 100644
index 00000000..e98341d2
--- /dev/null
+++ b/examples/ferris/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "ferris"
+version = "0.1.0"
+authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
+edition = "2021"
+publish = false
+
+[dependencies]
+iced.workspace = true
+iced.features = ["image", "tokio", "debug"]
diff --git a/examples/ferris/src/main.rs b/examples/ferris/src/main.rs
new file mode 100644
index 00000000..0400c376
--- /dev/null
+++ b/examples/ferris/src/main.rs
@@ -0,0 +1,211 @@
+use iced::time::Instant;
+use iced::widget::{
+ center, checkbox, column, container, image, pick_list, row, slider, text,
+};
+use iced::window;
+use iced::{
+ Alignment, Color, ContentFit, Degrees, Element, Length, Radians, Rotation,
+ Subscription, Theme,
+};
+
+pub fn main() -> iced::Result {
+ iced::program("Ferris - Iced", Image::update, Image::view)
+ .subscription(Image::subscription)
+ .theme(|_| Theme::TokyoNight)
+ .run()
+}
+
+struct Image {
+ width: f32,
+ opacity: f32,
+ rotation: Rotation,
+ content_fit: ContentFit,
+ spin: bool,
+ last_tick: Instant,
+}
+
+#[derive(Debug, Clone, Copy)]
+enum Message {
+ WidthChanged(f32),
+ OpacityChanged(f32),
+ RotationStrategyChanged(RotationStrategy),
+ RotationChanged(Degrees),
+ ContentFitChanged(ContentFit),
+ SpinToggled(bool),
+ RedrawRequested(Instant),
+}
+
+impl Image {
+ fn update(&mut self, message: Message) {
+ match message {
+ Message::WidthChanged(width) => {
+ self.width = width;
+ }
+ Message::OpacityChanged(opacity) => {
+ self.opacity = opacity;
+ }
+ Message::RotationStrategyChanged(strategy) => {
+ self.rotation = match strategy {
+ RotationStrategy::Floating => {
+ Rotation::Floating(self.rotation.radians())
+ }
+ RotationStrategy::Solid => {
+ Rotation::Solid(self.rotation.radians())
+ }
+ };
+ }
+ Message::RotationChanged(rotation) => {
+ self.rotation = match self.rotation {
+ Rotation::Floating(_) => {
+ Rotation::Floating(rotation.into())
+ }
+ Rotation::Solid(_) => Rotation::Solid(rotation.into()),
+ };
+ }
+ Message::ContentFitChanged(content_fit) => {
+ self.content_fit = content_fit;
+ }
+ Message::SpinToggled(spin) => {
+ self.spin = spin;
+ self.last_tick = Instant::now();
+ }
+ Message::RedrawRequested(now) => {
+ const ROTATION_SPEED: Degrees = Degrees(360.0);
+
+ let delta = (now - self.last_tick).as_millis() as f32 / 1_000.0;
+
+ *self.rotation.radians_mut() = (self.rotation.radians()
+ + ROTATION_SPEED * delta)
+ % (2.0 * Radians::PI);
+
+ self.last_tick = now;
+ }
+ }
+ }
+
+ fn subscription(&self) -> Subscription<Message> {
+ if self.spin {
+ window::frames().map(Message::RedrawRequested)
+ } else {
+ Subscription::none()
+ }
+ }
+
+ fn view(&self) -> Element<Message> {
+ let i_am_ferris = column![
+ "Hello!",
+ Element::from(
+ image(format!(
+ "{}/../tour/images/ferris.png",
+ env!("CARGO_MANIFEST_DIR")
+ ))
+ .width(self.width)
+ .content_fit(self.content_fit)
+ .rotation(self.rotation)
+ .opacity(self.opacity)
+ )
+ .explain(Color::WHITE),
+ "I am Ferris!"
+ ]
+ .spacing(20)
+ .align_items(Alignment::Center);
+
+ let fit = row![
+ pick_list(
+ [
+ ContentFit::Contain,
+ ContentFit::Cover,
+ ContentFit::Fill,
+ ContentFit::None,
+ ContentFit::ScaleDown
+ ],
+ Some(self.content_fit),
+ Message::ContentFitChanged
+ )
+ .width(Length::Fill),
+ pick_list(
+ [RotationStrategy::Floating, RotationStrategy::Solid],
+ Some(match self.rotation {
+ Rotation::Floating(_) => RotationStrategy::Floating,
+ Rotation::Solid(_) => RotationStrategy::Solid,
+ }),
+ Message::RotationStrategyChanged,
+ )
+ .width(Length::Fill),
+ ]
+ .spacing(10)
+ .align_items(Alignment::End);
+
+ let properties = row![
+ with_value(
+ slider(100.0..=500.0, self.width, Message::WidthChanged),
+ format!("Width: {}px", self.width)
+ ),
+ with_value(
+ slider(0.0..=1.0, self.opacity, Message::OpacityChanged)
+ .step(0.01),
+ format!("Opacity: {:.2}", self.opacity)
+ ),
+ with_value(
+ row![
+ slider(
+ Degrees::RANGE,
+ self.rotation.degrees(),
+ Message::RotationChanged
+ ),
+ checkbox("Spin!", self.spin)
+ .text_size(12)
+ .on_toggle(Message::SpinToggled)
+ .size(12)
+ ]
+ .spacing(10)
+ .align_items(Alignment::Center),
+ format!("Rotation: {:.0}°", f32::from(self.rotation.degrees()))
+ )
+ ]
+ .spacing(10)
+ .align_items(Alignment::End);
+
+ container(column![fit, center(i_am_ferris), properties].spacing(10))
+ .padding(10)
+ .into()
+ }
+}
+
+impl Default for Image {
+ fn default() -> Self {
+ Self {
+ width: 300.0,
+ opacity: 1.0,
+ rotation: Rotation::default(),
+ content_fit: ContentFit::default(),
+ spin: false,
+ last_tick: Instant::now(),
+ }
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+enum RotationStrategy {
+ Floating,
+ Solid,
+}
+
+impl std::fmt::Display for RotationStrategy {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.write_str(match self {
+ Self::Floating => "Floating",
+ Self::Solid => "Solid",
+ })
+ }
+}
+
+fn with_value<'a>(
+ control: impl Into<Element<'a, Message>>,
+ value: String,
+) -> Element<'a, Message> {
+ column![control.into(), text(value).size(12).line_height(1.0)]
+ .spacing(2)
+ .align_items(Alignment::Center)
+ .into()
+}
diff --git a/examples/geometry/src/main.rs b/examples/geometry/src/main.rs
index 31b8a4ce..bf7801a9 100644
--- a/examples/geometry/src/main.rs
+++ b/examples/geometry/src/main.rs
@@ -153,7 +153,7 @@ mod rainbow {
}
use iced::widget::{column, container, scrollable};
-use iced::{Element, Length};
+use iced::Element;
use rainbow::rainbow;
pub fn main() -> iced::Result {
@@ -176,12 +176,7 @@ fn view(_state: &()) -> Element<'_, ()> {
.spacing(20)
.max_width(500);
- let scrollable =
- scrollable(container(content).width(Length::Fill).center_x());
+ let scrollable = scrollable(container(content).center_x());
- container(scrollable)
- .width(Length::Fill)
- .height(Length::Fill)
- .center_y()
- .into()
+ container(scrollable).center_y().into()
}
diff --git a/examples/integration/README.md b/examples/integration/README.md
index aa3a6e94..bac0640c 100644
--- a/examples/integration/README.md
+++ b/examples/integration/README.md
@@ -10,25 +10,8 @@ The __[`main`]__ file contains all the code of the example.
You can run it with `cargo run`:
```
-cargo run --package integration_wgpu
+cargo run --package integration
```
-### How to run this example with WebGL backend
-NOTE: Currently, WebGL backend is still experimental, so expect bugs.
-
-```sh
-# 0. Install prerequisites
-cargo install wasm-bindgen-cli https
-# 1. cd to the current folder
-# 2. Compile wasm module
-cargo build -p integration_wgpu --target wasm32-unknown-unknown
-# 3. Invoke wasm-bindgen
-wasm-bindgen ../../target/wasm32-unknown-unknown/debug/integration_wgpu.wasm --out-dir . --target web --no-typescript
-# 4. run http server
-http
-# 5. Open 127.0.0.1:8000 in browser
-```
-
-
[`main`]: src/main.rs
[`wgpu`]: https://github.com/gfx-rs/wgpu
diff --git a/examples/integration/index.html b/examples/integration/index.html
deleted file mode 100644
index 920bc4a0..00000000
--- a/examples/integration/index.html
+++ /dev/null
@@ -1,21 +0,0 @@
-<!DOCTYPE html>
-<html>
- <head>
- <meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
- <title>Iced - wgpu + WebGL integration</title>
- </head>
- <body>
- <h1>integration_wgpu</h1>
- <canvas id="iced_canvas"></canvas>
- <script type="module">
- import init from "./integration.js";
- init('./integration_bg.wasm');
- </script>
- <style>
- body {
- width: 100%;
- text-align: center;
- }
- </style>
- </body>
-</html>
diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs
index c4b57ecf..e1c7d62f 100644
--- a/examples/integration/src/main.rs
+++ b/examples/integration/src/main.rs
@@ -18,305 +18,342 @@ use iced_winit::winit;
use iced_winit::Clipboard;
use winit::{
- event::{Event, WindowEvent},
+ event::WindowEvent,
event_loop::{ControlFlow, EventLoop},
keyboard::ModifiersState,
};
use std::sync::Arc;
-#[cfg(target_arch = "wasm32")]
-use wasm_bindgen::JsCast;
-#[cfg(target_arch = "wasm32")]
-use web_sys::HtmlCanvasElement;
-#[cfg(target_arch = "wasm32")]
-use winit::platform::web::WindowBuilderExtWebSys;
-
-pub fn main() -> Result<(), Box<dyn std::error::Error>> {
- #[cfg(target_arch = "wasm32")]
- let canvas_element = {
- console_log::init().expect("Initialize logger");
-
- std::panic::set_hook(Box::new(console_error_panic_hook::hook));
-
- web_sys::window()
- .and_then(|win| win.document())
- .and_then(|doc| doc.get_element_by_id("iced_canvas"))
- .and_then(|element| element.dyn_into::<HtmlCanvasElement>().ok())
- .expect("Get canvas element")
- };
-
- #[cfg(not(target_arch = "wasm32"))]
+pub fn main() -> Result<(), winit::error::EventLoopError> {
tracing_subscriber::fmt::init();
// Initialize winit
let event_loop = EventLoop::new()?;
- #[cfg(target_arch = "wasm32")]
- let window = winit::window::WindowBuilder::new()
- .with_canvas(Some(canvas_element))
- .build(&event_loop)?;
-
- #[cfg(not(target_arch = "wasm32"))]
- let window = winit::window::Window::new(&event_loop)?;
-
- let window = Arc::new(window);
-
- let physical_size = window.inner_size();
- let mut viewport = Viewport::with_physical_size(
- Size::new(physical_size.width, physical_size.height),
- window.scale_factor(),
- );
- let mut cursor_position = None;
- let mut modifiers = ModifiersState::default();
- let mut clipboard = Clipboard::connect(&window);
-
- // Initialize wgpu
- #[cfg(target_arch = "wasm32")]
- let default_backend = wgpu::Backends::GL;
- #[cfg(not(target_arch = "wasm32"))]
- let default_backend = wgpu::Backends::PRIMARY;
-
- let backend =
- wgpu::util::backend_bits_from_env().unwrap_or(default_backend);
-
- let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
- backends: backend,
- ..Default::default()
- });
- let surface = instance.create_surface(window.clone())?;
-
- let (format, adapter, device, queue) =
- futures::futures::executor::block_on(async {
- let adapter = wgpu::util::initialize_adapter_from_env_or_default(
- &instance,
- Some(&surface),
- )
- .await
- .expect("Create adapter");
-
- let adapter_features = adapter.features();
-
- #[cfg(target_arch = "wasm32")]
- let needed_limits = wgpu::Limits::downlevel_webgl2_defaults()
- .using_resolution(adapter.limits());
-
- #[cfg(not(target_arch = "wasm32"))]
- let needed_limits = wgpu::Limits::default();
-
- let capabilities = surface.get_capabilities(&adapter);
-
- let (device, queue) = adapter
- .request_device(
- &wgpu::DeviceDescriptor {
- label: None,
- required_features: adapter_features
- & wgpu::Features::default(),
- required_limits: needed_limits,
+ #[allow(clippy::large_enum_variant)]
+ enum Runner {
+ Loading,
+ Ready {
+ window: Arc<winit::window::Window>,
+ device: wgpu::Device,
+ queue: wgpu::Queue,
+ surface: wgpu::Surface<'static>,
+ format: wgpu::TextureFormat,
+ engine: Engine,
+ renderer: Renderer,
+ scene: Scene,
+ state: program::State<Controls>,
+ cursor_position: Option<winit::dpi::PhysicalPosition<f64>>,
+ clipboard: Clipboard,
+ viewport: Viewport,
+ modifiers: ModifiersState,
+ resized: bool,
+ debug: Debug,
+ },
+ }
+
+ impl winit::application::ApplicationHandler for Runner {
+ fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {
+ if let Self::Loading = self {
+ let window = Arc::new(
+ event_loop
+ .create_window(
+ winit::window::WindowAttributes::default(),
+ )
+ .expect("Create window"),
+ );
+
+ let physical_size = window.inner_size();
+ let viewport = Viewport::with_physical_size(
+ Size::new(physical_size.width, physical_size.height),
+ window.scale_factor(),
+ );
+ let clipboard = Clipboard::connect(&window);
+
+ let backend =
+ wgpu::util::backend_bits_from_env().unwrap_or_default();
+
+ let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
+ backends: backend,
+ ..Default::default()
+ });
+ let surface = instance
+ .create_surface(window.clone())
+ .expect("Create window surface");
+
+ let (format, adapter, device, queue) =
+ futures::futures::executor::block_on(async {
+ let adapter =
+ wgpu::util::initialize_adapter_from_env_or_default(
+ &instance,
+ Some(&surface),
+ )
+ .await
+ .expect("Create adapter");
+
+ let adapter_features = adapter.features();
+
+ let capabilities = surface.get_capabilities(&adapter);
+
+ let (device, queue) = adapter
+ .request_device(
+ &wgpu::DeviceDescriptor {
+ label: None,
+ required_features: adapter_features
+ & wgpu::Features::default(),
+ required_limits: wgpu::Limits::default(),
+ },
+ None,
+ )
+ .await
+ .expect("Request device");
+
+ (
+ capabilities
+ .formats
+ .iter()
+ .copied()
+ .find(wgpu::TextureFormat::is_srgb)
+ .or_else(|| {
+ capabilities.formats.first().copied()
+ })
+ .expect("Get preferred format"),
+ adapter,
+ device,
+ queue,
+ )
+ });
+
+ surface.configure(
+ &device,
+ &wgpu::SurfaceConfiguration {
+ usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
+ format,
+ width: physical_size.width,
+ height: physical_size.height,
+ present_mode: wgpu::PresentMode::AutoVsync,
+ alpha_mode: wgpu::CompositeAlphaMode::Auto,
+ view_formats: vec![],
+ desired_maximum_frame_latency: 2,
},
- None,
- )
- .await
- .expect("Request device");
-
- (
- capabilities
- .formats
- .iter()
- .copied()
- .find(wgpu::TextureFormat::is_srgb)
- .or_else(|| capabilities.formats.first().copied())
- .expect("Get preferred format"),
- adapter,
+ );
+
+ // Initialize scene and GUI controls
+ let scene = Scene::new(&device, format);
+ let controls = Controls::new();
+
+ // Initialize iced
+ let mut debug = Debug::new();
+ let engine =
+ Engine::new(&adapter, &device, &queue, format, None);
+ let mut renderer = Renderer::new(
+ &device,
+ &engine,
+ Font::default(),
+ Pixels::from(16),
+ );
+
+ let state = program::State::new(
+ controls,
+ viewport.logical_size(),
+ &mut renderer,
+ &mut debug,
+ );
+
+ // You should change this if you want to render continuously
+ event_loop.set_control_flow(ControlFlow::Wait);
+
+ *self = Self::Ready {
+ window,
+ device,
+ queue,
+ surface,
+ format,
+ engine,
+ renderer,
+ scene,
+ state,
+ cursor_position: None,
+ modifiers: ModifiersState::default(),
+ clipboard,
+ viewport,
+ resized: false,
+ debug,
+ };
+ }
+ }
+
+ fn window_event(
+ &mut self,
+ event_loop: &winit::event_loop::ActiveEventLoop,
+ _window_id: winit::window::WindowId,
+ event: WindowEvent,
+ ) {
+ let Self::Ready {
+ window,
device,
queue,
- )
- });
-
- surface.configure(
- &device,
- &wgpu::SurfaceConfiguration {
- usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
- format,
- width: physical_size.width,
- height: physical_size.height,
- present_mode: wgpu::PresentMode::AutoVsync,
- alpha_mode: wgpu::CompositeAlphaMode::Auto,
- view_formats: vec![],
- desired_maximum_frame_latency: 2,
- },
- );
-
- let mut resized = false;
-
- // Initialize scene and GUI controls
- let scene = Scene::new(&device, format);
- let controls = Controls::new();
-
- // Initialize iced
- let mut debug = Debug::new();
- let mut engine = Engine::new(&adapter, &device, &queue, format, None);
- let mut renderer =
- Renderer::new(&engine, Font::default(), Pixels::from(16));
-
- let mut state = program::State::new(
- controls,
- viewport.logical_size(),
- &mut renderer,
- &mut debug,
- );
-
- // Run event loop
- event_loop.run(move |event, window_target| {
- // You should change this if you want to render continuously
- window_target.set_control_flow(ControlFlow::Wait);
-
- match event {
- Event::WindowEvent {
- event: WindowEvent::RedrawRequested,
- ..
- } => {
- if resized {
- let size = window.inner_size();
-
- viewport = Viewport::with_physical_size(
- Size::new(size.width, size.height),
- window.scale_factor(),
- );
-
- surface.configure(
- &device,
- &wgpu::SurfaceConfiguration {
- format,
- usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
- width: size.width,
- height: size.height,
- present_mode: wgpu::PresentMode::AutoVsync,
- alpha_mode: wgpu::CompositeAlphaMode::Auto,
- view_formats: vec![],
- desired_maximum_frame_latency: 2,
- },
- );
-
- resized = false;
- }
-
- match surface.get_current_texture() {
- Ok(frame) => {
- let mut encoder = device.create_command_encoder(
- &wgpu::CommandEncoderDescriptor { label: None },
+ surface,
+ format,
+ engine,
+ renderer,
+ scene,
+ state,
+ viewport,
+ cursor_position,
+ modifiers,
+ clipboard,
+ resized,
+ debug,
+ } = self
+ else {
+ return;
+ };
+
+ match event {
+ WindowEvent::RedrawRequested => {
+ if *resized {
+ let size = window.inner_size();
+
+ *viewport = Viewport::with_physical_size(
+ Size::new(size.width, size.height),
+ window.scale_factor(),
);
- let program = state.program();
-
- let view = frame.texture.create_view(
- &wgpu::TextureViewDescriptor::default(),
+ surface.configure(
+ device,
+ &wgpu::SurfaceConfiguration {
+ format: *format,
+ usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
+ width: size.width,
+ height: size.height,
+ present_mode: wgpu::PresentMode::AutoVsync,
+ alpha_mode: wgpu::CompositeAlphaMode::Auto,
+ view_formats: vec![],
+ desired_maximum_frame_latency: 2,
+ },
);
- {
- // We clear the frame
- let mut render_pass = Scene::clear(
- &view,
- &mut encoder,
- program.background_color(),
+ *resized = false;
+ }
+
+ match surface.get_current_texture() {
+ Ok(frame) => {
+ let mut encoder = device.create_command_encoder(
+ &wgpu::CommandEncoderDescriptor { label: None },
);
- // Draw the scene
- scene.draw(&mut render_pass);
- }
+ let program = state.program();
- // And then iced on top
- renderer.present(
- &mut engine,
- &device,
- &queue,
- &mut encoder,
- None,
- frame.texture.format(),
- &view,
- &viewport,
- &debug.overlay(),
- );
+ let view = frame.texture.create_view(
+ &wgpu::TextureViewDescriptor::default(),
+ );
+
+ {
+ // We clear the frame
+ let mut render_pass = Scene::clear(
+ &view,
+ &mut encoder,
+ program.background_color(),
+ );
+
+ // Draw the scene
+ scene.draw(&mut render_pass);
+ }
+
+ // And then iced on top
+ renderer.present(
+ engine,
+ device,
+ queue,
+ &mut encoder,
+ None,
+ frame.texture.format(),
+ &view,
+ viewport,
+ &debug.overlay(),
+ );
- // Then we submit the work
- queue.submit(Some(encoder.finish()));
- frame.present();
+ // Then we submit the work
+ engine.submit(queue, encoder);
+ frame.present();
- // Update the mouse cursor
- window.set_cursor_icon(
- iced_winit::conversion::mouse_interaction(
- state.mouse_interaction(),
- ),
- );
- }
- Err(error) => match error {
- wgpu::SurfaceError::OutOfMemory => {
- panic!(
- "Swapchain error: {error}. \
- Rendering cannot continue."
- )
- }
- _ => {
- // Try rendering again next frame.
- window.request_redraw();
+ // Update the mouse cursor
+ window.set_cursor(
+ iced_winit::conversion::mouse_interaction(
+ state.mouse_interaction(),
+ ),
+ );
}
- },
- }
- }
- Event::WindowEvent { event, .. } => {
- match event {
- WindowEvent::CursorMoved { position, .. } => {
- cursor_position = Some(position);
- }
- WindowEvent::ModifiersChanged(new_modifiers) => {
- modifiers = new_modifiers.state();
- }
- WindowEvent::Resized(_) => {
- resized = true;
- }
- WindowEvent::CloseRequested => {
- window_target.exit();
+ Err(error) => match error {
+ wgpu::SurfaceError::OutOfMemory => {
+ panic!(
+ "Swapchain error: {error}. \
+ Rendering cannot continue."
+ )
+ }
+ _ => {
+ // Try rendering again next frame.
+ window.request_redraw();
+ }
+ },
}
- _ => {}
}
-
- // Map window event to iced event
- if let Some(event) = iced_winit::conversion::window_event(
- window::Id::MAIN,
- event,
- window.scale_factor(),
- modifiers,
- ) {
- state.queue_event(event);
+ WindowEvent::CursorMoved { position, .. } => {
+ *cursor_position = Some(position);
+ }
+ WindowEvent::ModifiersChanged(new_modifiers) => {
+ *modifiers = new_modifiers.state();
}
+ WindowEvent::Resized(_) => {
+ *resized = true;
+ }
+ WindowEvent::CloseRequested => {
+ event_loop.exit();
+ }
+ _ => {}
}
- _ => {}
- }
- // If there are events pending
- if !state.is_queue_empty() {
- // We update iced
- let _ = state.update(
- viewport.logical_size(),
- cursor_position
- .map(|p| {
- conversion::cursor_position(p, viewport.scale_factor())
- })
- .map(mouse::Cursor::Available)
- .unwrap_or(mouse::Cursor::Unavailable),
- &mut renderer,
- &Theme::Dark,
- &renderer::Style {
- text_color: Color::WHITE,
- },
- &mut clipboard,
- &mut debug,
- );
-
- // and request a redraw
- window.request_redraw();
+ // Map window event to iced event
+ if let Some(event) = iced_winit::conversion::window_event(
+ window::Id::MAIN,
+ event,
+ window.scale_factor(),
+ *modifiers,
+ ) {
+ state.queue_event(event);
+ }
+
+ // If there are events pending
+ if !state.is_queue_empty() {
+ // We update iced
+ let _ = state.update(
+ viewport.logical_size(),
+ cursor_position
+ .map(|p| {
+ conversion::cursor_position(
+ p,
+ viewport.scale_factor(),
+ )
+ })
+ .map(mouse::Cursor::Available)
+ .unwrap_or(mouse::Cursor::Unavailable),
+ renderer,
+ &Theme::Dark,
+ &renderer::Style {
+ text_color: Color::WHITE,
+ },
+ clipboard,
+ debug,
+ );
+
+ // and request a redraw
+ window.request_redraw();
+ }
}
- })?;
+ }
- Ok(())
+ let mut runner = Runner::Loading;
+ event_loop.run_app(&mut runner)
}
diff --git a/examples/layout/src/main.rs b/examples/layout/src/main.rs
index 66d79091..5f14c03b 100644
--- a/examples/layout/src/main.rs
+++ b/examples/layout/src/main.rs
@@ -1,8 +1,8 @@
use iced::keyboard;
use iced::mouse;
use iced::widget::{
- button, canvas, checkbox, column, container, horizontal_space, pick_list,
- row, scrollable, text,
+ button, canvas, center, checkbox, column, container, horizontal_space,
+ pick_list, row, scrollable, text,
};
use iced::{
color, Alignment, Element, Font, Length, Point, Rectangle, Renderer,
@@ -76,7 +76,7 @@ impl Layout {
.spacing(20)
.align_items(Alignment::Center);
- let example = container(if self.explain {
+ let example = center(if self.explain {
self.example.view().explain(color!(0x0000ff))
} else {
self.example.view()
@@ -87,11 +87,7 @@ impl Layout {
container::Style::default()
.with_border(palette.background.strong.color, 4.0)
})
- .padding(4)
- .width(Length::Fill)
- .height(Length::Fill)
- .center_x()
- .center_y();
+ .padding(4);
let controls = row([
(!self.example.is_first()).then_some(
@@ -195,12 +191,7 @@ impl Default for Example {
}
fn centered<'a>() -> Element<'a, Message> {
- container(text("I am centered!").size(50))
- .width(Length::Fill)
- .height(Length::Fill)
- .center_x()
- .center_y()
- .into()
+ center(text("I am centered!").size(50)).into()
}
fn column_<'a>() -> Element<'a, Message> {
@@ -260,7 +251,6 @@ fn application<'a>() -> Element<'a, Message> {
.align_items(Alignment::Center),
)
.style(container::rounded_box)
- .height(Length::Fill)
.center_y();
let content = container(
diff --git a/examples/loading_spinners/src/main.rs b/examples/loading_spinners/src/main.rs
index eaa4d57e..e8d67ab5 100644
--- a/examples/loading_spinners/src/main.rs
+++ b/examples/loading_spinners/src/main.rs
@@ -1,5 +1,5 @@
-use iced::widget::{column, container, row, slider, text};
-use iced::{Element, Length};
+use iced::widget::{center, column, row, slider, text};
+use iced::Element;
use std::time::Duration;
@@ -73,7 +73,7 @@ impl LoadingSpinners {
})
.spacing(20);
- container(
+ center(
column.push(
row![
text("Cycle duration:"),
@@ -87,10 +87,6 @@ impl LoadingSpinners {
.spacing(20.0),
),
)
- .width(Length::Fill)
- .height(Length::Fill)
- .center_x()
- .center_y()
.into()
}
}
diff --git a/examples/loupe/src/main.rs b/examples/loupe/src/main.rs
index 6a5ff123..c4d3b449 100644
--- a/examples/loupe/src/main.rs
+++ b/examples/loupe/src/main.rs
@@ -1,5 +1,5 @@
-use iced::widget::{button, column, container, text};
-use iced::{Alignment, Element, Length};
+use iced::widget::{button, center, column, text};
+use iced::{Alignment, Element};
use loupe::loupe;
@@ -31,7 +31,7 @@ impl Loupe {
}
fn view(&self) -> Element<Message> {
- container(loupe(
+ center(loupe(
3.0,
column![
button("Increment").on_press(Message::Increment),
@@ -41,10 +41,6 @@ impl Loupe {
.padding(20)
.align_items(Alignment::Center),
))
- .width(Length::Fill)
- .height(Length::Fill)
- .center_x()
- .center_y()
.into()
}
}
@@ -159,7 +155,7 @@ mod loupe {
if cursor.is_over(layout.bounds()) {
mouse::Interaction::ZoomIn
} else {
- mouse::Interaction::Idle
+ mouse::Interaction::None
}
}
}
diff --git a/examples/modal/src/main.rs b/examples/modal/src/main.rs
index 398728e0..a012c310 100644
--- a/examples/modal/src/main.rs
+++ b/examples/modal/src/main.rs
@@ -2,12 +2,11 @@ use iced::event::{self, Event};
use iced::keyboard;
use iced::keyboard::key;
use iced::widget::{
- self, button, column, container, horizontal_space, pick_list, row, text,
- text_input,
+ self, button, center, column, container, horizontal_space, mouse_area,
+ opaque, pick_list, row, stack, text, text_input,
};
-use iced::{Alignment, Command, Element, Length, Subscription};
+use iced::{Alignment, Color, Command, Element, Length, Subscription};
-use modal::Modal;
use std::fmt;
pub fn main() -> iced::Result {
@@ -99,13 +98,7 @@ impl App {
row![text("Top Left"), horizontal_space(), text("Top Right")]
.align_items(Alignment::Start)
.height(Length::Fill),
- container(
- button(text("Show Modal")).on_press(Message::ShowModal)
- )
- .center_x()
- .center_y()
- .width(Length::Fill)
- .height(Length::Fill),
+ center(button(text("Show Modal")).on_press(Message::ShowModal)),
row![
text("Bottom Left"),
horizontal_space(),
@@ -116,12 +109,10 @@ impl App {
]
.height(Length::Fill),
)
- .padding(10)
- .width(Length::Fill)
- .height(Length::Fill);
+ .padding(10);
if self.show_modal {
- let modal = container(
+ let signup = container(
column![
text("Sign Up").size(24),
column![
@@ -162,9 +153,7 @@ impl App {
.padding(10)
.style(container::rounded_box);
- Modal::new(content, modal)
- .on_blur(Message::HideModal)
- .into()
+ modal(content, signup, Message::HideModal)
} else {
content.into()
}
@@ -203,326 +192,29 @@ impl fmt::Display for Plan {
}
}
-mod modal {
- use iced::advanced::layout::{self, Layout};
- use iced::advanced::overlay;
- use iced::advanced::renderer;
- use iced::advanced::widget::{self, Widget};
- use iced::advanced::{self, Clipboard, Shell};
- use iced::alignment::Alignment;
- use iced::event;
- use iced::mouse;
- use iced::{Color, Element, Event, Length, Point, Rectangle, Size, Vector};
-
- /// A widget that centers a modal element over some base element
- pub struct Modal<'a, Message, Theme, Renderer> {
- base: Element<'a, Message, Theme, Renderer>,
- modal: Element<'a, Message, Theme, Renderer>,
- on_blur: Option<Message>,
- }
-
- impl<'a, Message, Theme, Renderer> Modal<'a, Message, Theme, Renderer> {
- /// Returns a new [`Modal`]
- pub fn new(
- base: impl Into<Element<'a, Message, Theme, Renderer>>,
- modal: impl Into<Element<'a, Message, Theme, Renderer>>,
- ) -> Self {
- Self {
- base: base.into(),
- modal: modal.into(),
- on_blur: None,
- }
- }
-
- /// Sets the message that will be produces when the background
- /// of the [`Modal`] is pressed
- pub fn on_blur(self, on_blur: Message) -> Self {
- Self {
- on_blur: Some(on_blur),
- ..self
- }
- }
- }
-
- impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
- for Modal<'a, Message, Theme, Renderer>
- where
- Renderer: advanced::Renderer,
- Message: Clone,
- {
- fn children(&self) -> Vec<widget::Tree> {
- vec![
- widget::Tree::new(&self.base),
- widget::Tree::new(&self.modal),
- ]
- }
-
- fn diff(&self, tree: &mut widget::Tree) {
- tree.diff_children(&[&self.base, &self.modal]);
- }
-
- fn size(&self) -> Size<Length> {
- self.base.as_widget().size()
- }
-
- fn layout(
- &self,
- tree: &mut widget::Tree,
- renderer: &Renderer,
- limits: &layout::Limits,
- ) -> layout::Node {
- self.base.as_widget().layout(
- &mut tree.children[0],
- renderer,
- limits,
- )
- }
-
- fn on_event(
- &mut self,
- state: &mut widget::Tree,
- event: Event,
- layout: Layout<'_>,
- cursor: mouse::Cursor,
- renderer: &Renderer,
- clipboard: &mut dyn Clipboard,
- shell: &mut Shell<'_, Message>,
- viewport: &Rectangle,
- ) -> event::Status {
- self.base.as_widget_mut().on_event(
- &mut state.children[0],
- event,
- layout,
- cursor,
- renderer,
- clipboard,
- shell,
- viewport,
- )
- }
-
- fn draw(
- &self,
- state: &widget::Tree,
- renderer: &mut Renderer,
- theme: &Theme,
- style: &renderer::Style,
- layout: Layout<'_>,
- cursor: mouse::Cursor,
- viewport: &Rectangle,
- ) {
- self.base.as_widget().draw(
- &state.children[0],
- renderer,
- theme,
- style,
- layout,
- cursor,
- viewport,
- );
- }
-
- fn overlay<'b>(
- &'b mut self,
- state: &'b mut widget::Tree,
- layout: Layout<'_>,
- _renderer: &Renderer,
- translation: Vector,
- ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
- Some(overlay::Element::new(Box::new(Overlay {
- position: layout.position() + translation,
- content: &mut self.modal,
- tree: &mut state.children[1],
- size: layout.bounds().size(),
- on_blur: self.on_blur.clone(),
- })))
- }
-
- fn mouse_interaction(
- &self,
- state: &widget::Tree,
- layout: Layout<'_>,
- cursor: mouse::Cursor,
- viewport: &Rectangle,
- renderer: &Renderer,
- ) -> mouse::Interaction {
- self.base.as_widget().mouse_interaction(
- &state.children[0],
- layout,
- cursor,
- viewport,
- renderer,
- )
- }
-
- fn operate(
- &self,
- state: &mut widget::Tree,
- layout: Layout<'_>,
- renderer: &Renderer,
- operation: &mut dyn widget::Operation<Message>,
- ) {
- self.base.as_widget().operate(
- &mut state.children[0],
- layout,
- renderer,
- operation,
- );
- }
- }
-
- struct Overlay<'a, 'b, Message, Theme, Renderer> {
- position: Point,
- content: &'b mut Element<'a, Message, Theme, Renderer>,
- tree: &'b mut widget::Tree,
- size: Size,
- on_blur: Option<Message>,
- }
-
- impl<'a, 'b, Message, Theme, Renderer>
- overlay::Overlay<Message, Theme, Renderer>
- for Overlay<'a, 'b, Message, Theme, Renderer>
- where
- Renderer: advanced::Renderer,
- Message: Clone,
- {
- fn layout(
- &mut self,
- renderer: &Renderer,
- _bounds: Size,
- ) -> layout::Node {
- let limits = layout::Limits::new(Size::ZERO, self.size)
- .width(Length::Fill)
- .height(Length::Fill);
-
- let child = self
- .content
- .as_widget()
- .layout(self.tree, renderer, &limits)
- .align(Alignment::Center, Alignment::Center, limits.max());
-
- layout::Node::with_children(self.size, vec![child])
- .move_to(self.position)
- }
-
- fn on_event(
- &mut self,
- event: Event,
- layout: Layout<'_>,
- cursor: mouse::Cursor,
- renderer: &Renderer,
- clipboard: &mut dyn Clipboard,
- shell: &mut Shell<'_, Message>,
- ) -> event::Status {
- let content_bounds = layout.children().next().unwrap().bounds();
-
- if let Some(message) = self.on_blur.as_ref() {
- if let Event::Mouse(mouse::Event::ButtonPressed(
- mouse::Button::Left,
- )) = &event
- {
- if !cursor.is_over(content_bounds) {
- shell.publish(message.clone());
- return event::Status::Captured;
+fn modal<'a, Message>(
+ base: impl Into<Element<'a, Message>>,
+ content: impl Into<Element<'a, Message>>,
+ on_blur: Message,
+) -> Element<'a, Message>
+where
+ Message: Clone + 'a,
+{
+ stack![
+ base.into(),
+ mouse_area(center(opaque(content)).style(|_theme| {
+ container::Style {
+ background: Some(
+ Color {
+ a: 0.8,
+ ..Color::BLACK
}
- }
+ .into(),
+ ),
+ ..container::Style::default()
}
-
- self.content.as_widget_mut().on_event(
- self.tree,
- event,
- layout.children().next().unwrap(),
- cursor,
- renderer,
- clipboard,
- shell,
- &layout.bounds(),
- )
- }
-
- fn draw(
- &self,
- renderer: &mut Renderer,
- theme: &Theme,
- style: &renderer::Style,
- layout: Layout<'_>,
- cursor: mouse::Cursor,
- ) {
- renderer.fill_quad(
- renderer::Quad {
- bounds: layout.bounds(),
- ..renderer::Quad::default()
- },
- Color {
- a: 0.80,
- ..Color::BLACK
- },
- );
-
- self.content.as_widget().draw(
- self.tree,
- renderer,
- theme,
- style,
- layout.children().next().unwrap(),
- cursor,
- &layout.bounds(),
- );
- }
-
- fn operate(
- &mut self,
- layout: Layout<'_>,
- renderer: &Renderer,
- operation: &mut dyn widget::Operation<Message>,
- ) {
- self.content.as_widget().operate(
- self.tree,
- layout.children().next().unwrap(),
- renderer,
- operation,
- );
- }
-
- fn mouse_interaction(
- &self,
- layout: Layout<'_>,
- cursor: mouse::Cursor,
- viewport: &Rectangle,
- renderer: &Renderer,
- ) -> mouse::Interaction {
- self.content.as_widget().mouse_interaction(
- self.tree,
- layout.children().next().unwrap(),
- cursor,
- viewport,
- renderer,
- )
- }
-
- fn overlay<'c>(
- &'c mut self,
- layout: Layout<'_>,
- renderer: &Renderer,
- ) -> Option<overlay::Element<'c, Message, Theme, Renderer>> {
- self.content.as_widget_mut().overlay(
- self.tree,
- layout.children().next().unwrap(),
- renderer,
- Vector::ZERO,
- )
- }
- }
-
- impl<'a, Message, Theme, Renderer> From<Modal<'a, Message, Theme, Renderer>>
- for Element<'a, Message, Theme, Renderer>
- where
- Theme: 'a,
- Message: 'a + Clone,
- Renderer: 'a + advanced::Renderer,
- {
- fn from(modal: Modal<'a, Message, Theme, Renderer>) -> Self {
- Element::new(modal)
- }
- }
+ }))
+ .on_press(on_blur)
+ ]
+ .into()
}
diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs
index 5a5e70c1..74339e0c 100644
--- a/examples/multi_window/src/main.rs
+++ b/examples/multi_window/src/main.rs
@@ -1,7 +1,9 @@
use iced::event;
use iced::executor;
use iced::multi_window::{self, Application};
-use iced::widget::{button, column, container, scrollable, text, text_input};
+use iced::widget::{
+ button, center, column, container, scrollable, text, text_input,
+};
use iced::window;
use iced::{
Alignment, Command, Element, Length, Point, Settings, Subscription, Theme,
@@ -128,12 +130,7 @@ impl multi_window::Application for Example {
fn view(&self, window: window::Id) -> Element<Message> {
let content = self.windows.get(&window).unwrap().view(window);
- container(content)
- .width(Length::Fill)
- .height(Length::Fill)
- .center_x()
- .center_y()
- .into()
+ center(content).into()
}
fn theme(&self, window: window::Id) -> Self::Theme {
@@ -210,6 +207,6 @@ impl Window {
.align_items(Alignment::Center),
);
- container(content).width(200).center_x().into()
+ container(content).center_x().width(200).into()
}
}
diff --git a/examples/pane_grid/src/main.rs b/examples/pane_grid/src/main.rs
index 17112785..e74ea1ee 100644
--- a/examples/pane_grid/src/main.rs
+++ b/examples/pane_grid/src/main.rs
@@ -291,12 +291,7 @@ fn view_content<'a>(
.spacing(10)
.align_items(Alignment::Center);
- container(scrollable(content))
- .width(Length::Fill)
- .height(Length::Fill)
- .padding(5)
- .center_y()
- .into()
+ container(scrollable(content)).center_y().padding(5).into()
}
fn view_controls<'a>(
diff --git a/examples/pokedex/src/main.rs b/examples/pokedex/src/main.rs
index 0811c08d..be20094d 100644
--- a/examples/pokedex/src/main.rs
+++ b/examples/pokedex/src/main.rs
@@ -1,5 +1,5 @@
use iced::futures;
-use iced::widget::{self, column, container, image, row, text};
+use iced::widget::{self, center, column, image, row, text};
use iced::{Alignment, Command, Element, Length};
pub fn main() -> iced::Result {
@@ -83,12 +83,7 @@ impl Pokedex {
.align_items(Alignment::End),
};
- container(content)
- .width(Length::Fill)
- .height(Length::Fill)
- .center_x()
- .center_y()
- .into()
+ center(content).into()
}
}
@@ -188,7 +183,7 @@ impl Pokemon {
{
let bytes = reqwest::get(&url).await?.bytes().await?;
- Ok(image::Handle::from_memory(bytes))
+ Ok(image::Handle::from_bytes(bytes))
}
#[cfg(target_arch = "wasm32")]
diff --git a/examples/qr_code/src/main.rs b/examples/qr_code/src/main.rs
index b93adf04..c6a90458 100644
--- a/examples/qr_code/src/main.rs
+++ b/examples/qr_code/src/main.rs
@@ -1,7 +1,5 @@
-use iced::widget::{
- column, container, pick_list, qr_code, row, text, text_input,
-};
-use iced::{Alignment, Element, Length, Theme};
+use iced::widget::{center, column, pick_list, qr_code, row, text, text_input};
+use iced::{Alignment, Element, Theme};
pub fn main() -> iced::Result {
iced::program(
@@ -72,13 +70,7 @@ impl QRGenerator {
.spacing(20)
.align_items(Alignment::Center);
- container(content)
- .width(Length::Fill)
- .height(Length::Fill)
- .padding(20)
- .center_x()
- .center_y()
- .into()
+ center(content).padding(20).into()
}
fn theme(&self) -> Theme {
diff --git a/examples/screenshot/src/main.rs b/examples/screenshot/src/main.rs
index d887c41b..82495a1a 100644
--- a/examples/screenshot/src/main.rs
+++ b/examples/screenshot/src/main.rs
@@ -109,7 +109,7 @@ impl Example {
fn view(&self) -> Element<'_, Message> {
let image: Element<Message> = if let Some(screenshot) = &self.screenshot
{
- image(image::Handle::from_pixels(
+ image(image::Handle::from_rgba(
screenshot.size.width,
screenshot.size.height,
screenshot.clone(),
@@ -123,12 +123,10 @@ impl Example {
};
let image = container(image)
+ .center_y()
.padding(10)
.style(container::rounded_box)
- .width(Length::FillPortion(2))
- .height(Length::Fill)
- .center_x()
- .center_y();
+ .width(Length::FillPortion(2));
let crop_origin_controls = row![
text("X:")
@@ -213,12 +211,7 @@ impl Example {
.spacing(40)
};
- let side_content = container(controls)
- .align_x(alignment::Horizontal::Center)
- .width(Length::FillPortion(1))
- .height(Length::Fill)
- .center_y()
- .center_x();
+ let side_content = container(controls).center_y();
let content = row![side_content, image]
.spacing(10)
@@ -226,13 +219,7 @@ impl Example {
.height(Length::Fill)
.align_items(Alignment::Center);
- container(content)
- .width(Length::Fill)
- .height(Length::Fill)
- .padding(10)
- .center_x()
- .center_y()
- .into()
+ container(content).padding(10).into()
}
fn subscription(&self) -> Subscription<Message> {
diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs
index c02e754b..bbb6497f 100644
--- a/examples/scrollable/src/main.rs
+++ b/examples/scrollable/src/main.rs
@@ -327,7 +327,7 @@ impl ScrollableDemo {
.spacing(10)
.into();
- container(content).padding(20).center_x().center_y().into()
+ container(content).padding(20).into()
}
fn theme(&self) -> Theme {
diff --git a/examples/slider/src/main.rs b/examples/slider/src/main.rs
index b3a47614..5ffdc9c6 100644
--- a/examples/slider/src/main.rs
+++ b/examples/slider/src/main.rs
@@ -1,5 +1,5 @@
-use iced::widget::{column, container, slider, text, vertical_slider};
-use iced::{Element, Length};
+use iced::widget::{center, column, container, slider, text, vertical_slider};
+use iced::Element;
pub fn main() -> iced::Result {
iced::run("Slider - Iced", Slider::update, Slider::view)
@@ -54,18 +54,14 @@ impl Slider {
let text = text(self.value);
- container(
+ center(
column![
- container(v_slider).width(Length::Fill).center_x(),
- container(h_slider).width(Length::Fill).center_x(),
- container(text).width(Length::Fill).center_x(),
+ container(v_slider).center_x(),
+ container(h_slider).center_x(),
+ container(text).center_x()
]
.spacing(25),
)
- .height(Length::Fill)
- .width(Length::Fill)
- .center_x()
- .center_y()
.into()
}
}
diff --git a/examples/stopwatch/src/main.rs b/examples/stopwatch/src/main.rs
index b9eb19cf..bbe9d0ff 100644
--- a/examples/stopwatch/src/main.rs
+++ b/examples/stopwatch/src/main.rs
@@ -1,8 +1,8 @@
use iced::alignment;
use iced::keyboard;
use iced::time;
-use iced::widget::{button, column, container, row, text};
-use iced::{Alignment, Element, Length, Subscription, Theme};
+use iced::widget::{button, center, column, row, text};
+use iced::{Alignment, Element, Subscription, Theme};
use std::time::{Duration, Instant};
@@ -128,12 +128,7 @@ impl Stopwatch {
.align_items(Alignment::Center)
.spacing(20);
- container(content)
- .width(Length::Fill)
- .height(Length::Fill)
- .center_x()
- .center_y()
- .into()
+ center(content).into()
}
fn theme(&self) -> Theme {
diff --git a/examples/styling/src/main.rs b/examples/styling/src/main.rs
index 73268da0..57e8f47e 100644
--- a/examples/styling/src/main.rs
+++ b/examples/styling/src/main.rs
@@ -1,7 +1,7 @@
use iced::widget::{
- button, checkbox, column, container, horizontal_rule, pick_list,
- progress_bar, row, scrollable, slider, text, text_input, toggler,
- vertical_rule, vertical_space,
+ button, center, checkbox, column, horizontal_rule, pick_list, progress_bar,
+ row, scrollable, slider, text, text_input, toggler, vertical_rule,
+ vertical_space,
};
use iced::{Alignment, Element, Length, Theme};
@@ -106,12 +106,7 @@ impl Styling {
.padding(20)
.max_width(600);
- container(content)
- .width(Length::Fill)
- .height(Length::Fill)
- .center_x()
- .center_y()
- .into()
+ center(content).into()
}
fn theme(&self) -> Theme {
diff --git a/examples/svg/src/main.rs b/examples/svg/src/main.rs
index 0dcf9bc1..45b46716 100644
--- a/examples/svg/src/main.rs
+++ b/examples/svg/src/main.rs
@@ -1,4 +1,4 @@
-use iced::widget::{checkbox, column, container, svg};
+use iced::widget::{center, checkbox, column, container, svg};
use iced::{color, Element, Length};
pub fn main() -> iced::Result {
@@ -44,19 +44,12 @@ impl Tiger {
checkbox("Apply a color filter", self.apply_color_filter)
.on_toggle(Message::ToggleColorFilter);
- container(
- column![
- svg,
- container(apply_color_filter).width(Length::Fill).center_x()
- ]
- .spacing(20)
- .height(Length::Fill),
+ center(
+ column![svg, container(apply_color_filter).center_x()]
+ .spacing(20)
+ .height(Length::Fill),
)
- .width(Length::Fill)
- .height(Length::Fill)
.padding(20)
- .center_x()
- .center_y()
.into()
}
}
diff --git a/examples/system_information/src/main.rs b/examples/system_information/src/main.rs
index a6ac27a6..89a8383a 100644
--- a/examples/system_information/src/main.rs
+++ b/examples/system_information/src/main.rs
@@ -1,5 +1,5 @@
use iced::widget::{button, column, container, text};
-use iced::{system, Command, Element, Length};
+use iced::{system, Command, Element};
pub fn main() -> iced::Result {
iced::program("System Information - Iced", Example::update, Example::view)
@@ -136,11 +136,6 @@ impl Example {
}
};
- container(content)
- .center_x()
- .center_y()
- .width(Length::Fill)
- .height(Length::Fill)
- .into()
+ container(content).center().into()
}
}
diff --git a/examples/the_matrix/Cargo.toml b/examples/the_matrix/Cargo.toml
new file mode 100644
index 00000000..775e76e0
--- /dev/null
+++ b/examples/the_matrix/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "the_matrix"
+version = "0.1.0"
+authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
+edition = "2021"
+publish = false
+
+[dependencies]
+iced.workspace = true
+iced.features = ["canvas", "tokio", "debug"]
+
+rand = "0.8"
+tracing-subscriber = "0.3"
diff --git a/examples/the_matrix/src/main.rs b/examples/the_matrix/src/main.rs
new file mode 100644
index 00000000..f3a67ac8
--- /dev/null
+++ b/examples/the_matrix/src/main.rs
@@ -0,0 +1,115 @@
+use iced::mouse;
+use iced::time::{self, Instant};
+use iced::widget::canvas;
+use iced::{
+ Color, Element, Font, Length, Point, Rectangle, Renderer, Subscription,
+ Theme,
+};
+
+use std::cell::RefCell;
+
+pub fn main() -> iced::Result {
+ tracing_subscriber::fmt::init();
+
+ iced::program("The Matrix - Iced", TheMatrix::update, TheMatrix::view)
+ .subscription(TheMatrix::subscription)
+ .antialiasing(true)
+ .run()
+}
+
+#[derive(Default)]
+struct TheMatrix {
+ tick: usize,
+}
+
+#[derive(Debug, Clone, Copy)]
+enum Message {
+ Tick(Instant),
+}
+
+impl TheMatrix {
+ fn update(&mut self, message: Message) {
+ match message {
+ Message::Tick(_now) => {
+ self.tick += 1;
+ }
+ }
+ }
+
+ fn view(&self) -> Element<Message> {
+ canvas(self as &Self)
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .into()
+ }
+
+ fn subscription(&self) -> Subscription<Message> {
+ time::every(std::time::Duration::from_millis(50)).map(Message::Tick)
+ }
+}
+
+impl<Message> canvas::Program<Message> for TheMatrix {
+ type State = RefCell<Vec<canvas::Cache>>;
+
+ fn draw(
+ &self,
+ state: &Self::State,
+ renderer: &Renderer,
+ _theme: &Theme,
+ bounds: Rectangle,
+ _cursor: mouse::Cursor,
+ ) -> Vec<canvas::Geometry> {
+ use rand::distributions::Distribution;
+ use rand::Rng;
+
+ const CELL_SIZE: f32 = 10.0;
+
+ let mut caches = state.borrow_mut();
+
+ if caches.is_empty() {
+ let group = canvas::Group::unique();
+
+ caches.resize_with(30, || canvas::Cache::with_group(group));
+ }
+
+ vec![caches[self.tick % caches.len()].draw(
+ renderer,
+ bounds.size(),
+ |frame| {
+ frame.fill_rectangle(Point::ORIGIN, frame.size(), Color::BLACK);
+
+ let mut rng = rand::thread_rng();
+ let rows = (frame.height() / CELL_SIZE).ceil() as usize;
+ let columns = (frame.width() / CELL_SIZE).ceil() as usize;
+
+ for row in 0..rows {
+ for column in 0..columns {
+ let position = Point::new(
+ column as f32 * CELL_SIZE,
+ row as f32 * CELL_SIZE,
+ );
+
+ let alphas = [0.05, 0.1, 0.2, 0.5];
+ let weights = [10, 4, 2, 1];
+ let distribution =
+ rand::distributions::WeightedIndex::new(weights)
+ .expect("Create distribution");
+
+ frame.fill_text(canvas::Text {
+ content: rng.gen_range('!'..'z').to_string(),
+ position,
+ color: Color {
+ a: alphas[distribution.sample(&mut rng)],
+ g: 1.0,
+ ..Color::BLACK
+ },
+ size: CELL_SIZE.into(),
+ font: Font::MONOSPACE,
+ ..canvas::Text::default()
+ });
+ }
+ }
+ },
+ )]
+ }
+}
diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs
index 9968962c..0fcf08c4 100644
--- a/examples/toast/src/main.rs
+++ b/examples/toast/src/main.rs
@@ -2,7 +2,7 @@ use iced::event::{self, Event};
use iced::keyboard;
use iced::keyboard::key;
use iced::widget::{
- self, button, column, container, pick_list, row, slider, text, text_input,
+ self, button, center, column, pick_list, row, slider, text, text_input,
};
use iced::{Alignment, Command, Element, Length, Subscription};
@@ -102,7 +102,7 @@ impl App {
.then_some(Message::Add),
);
- let content = container(
+ let content = center(
column![
subtitle(
"Title",
@@ -146,11 +146,7 @@ impl App {
]
.spacing(10)
.max_width(200),
- )
- .width(Length::Fill)
- .height(Length::Fill)
- .center_x()
- .center_y();
+ );
toast::Manager::new(content, &self.toasts, Message::Close)
.timeout(self.timeout_secs)
diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs
index 7768c1d5..8119bc91 100644
--- a/examples/todos/src/main.rs
+++ b/examples/todos/src/main.rs
@@ -1,8 +1,8 @@
use iced::alignment::{self, Alignment};
use iced::keyboard;
use iced::widget::{
- self, button, checkbox, column, container, keyed_column, row, scrollable,
- text, text_input, Text,
+ self, button, center, checkbox, column, container, keyed_column, row,
+ scrollable, text, text_input, Text,
};
use iced::window;
use iced::{Command, Element, Font, Length, Subscription};
@@ -238,7 +238,7 @@ impl Todos {
.spacing(20)
.max_width(800);
- scrollable(container(content).padding(40).center_x()).into()
+ scrollable(container(content).center_x().padding(40)).into()
}
}
}
@@ -435,19 +435,16 @@ impl Filter {
}
fn loading_message<'a>() -> Element<'a, Message> {
- container(
+ center(
text("Loading...")
.horizontal_alignment(alignment::Horizontal::Center)
.size(50),
)
- .width(Length::Fill)
- .height(Length::Fill)
- .center_y()
.into()
}
fn empty_message(message: &str) -> Element<'_, Message> {
- container(
+ center(
text(message)
.width(Length::Fill)
.size(25)
@@ -455,7 +452,6 @@ fn empty_message(message: &str) -> Element<'_, Message> {
.color([0.7, 0.7, 0.7]),
)
.height(200)
- .center_y()
.into()
}
diff --git a/examples/tooltip/src/main.rs b/examples/tooltip/src/main.rs
index b6603068..f48f688a 100644
--- a/examples/tooltip/src/main.rs
+++ b/examples/tooltip/src/main.rs
@@ -1,6 +1,6 @@
use iced::widget::tooltip::Position;
-use iced::widget::{button, container, tooltip};
-use iced::{Element, Length};
+use iced::widget::{button, center, container, tooltip};
+use iced::Element;
pub fn main() -> iced::Result {
iced::run("Tooltip - Iced", Tooltip::update, Tooltip::view)
@@ -43,12 +43,7 @@ impl Tooltip {
.gap(10)
.style(container::rounded_box);
- container(tooltip)
- .width(Length::Fill)
- .height(Length::Fill)
- .center_x()
- .center_y()
- .into()
+ center(tooltip).into()
}
}
diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs
index e3a2ca87..bae6490d 100644
--- a/examples/tour/src/main.rs
+++ b/examples/tour/src/main.rs
@@ -76,11 +76,10 @@ impl Tour {
} else {
content
})
- .width(Length::Fill)
.center_x(),
);
- container(scrollable).height(Length::Fill).center_y().into()
+ container(scrollable).center_y().into()
}
}
@@ -670,7 +669,6 @@ fn ferris<'a>(
.filter_method(filter_method)
.width(width),
)
- .width(Length::Fill)
.center_x()
}
diff --git a/examples/url_handler/src/main.rs b/examples/url_handler/src/main.rs
index df705b6c..800a188b 100644
--- a/examples/url_handler/src/main.rs
+++ b/examples/url_handler/src/main.rs
@@ -1,6 +1,6 @@
use iced::event::{self, Event};
-use iced::widget::{container, text};
-use iced::{Element, Length, Subscription};
+use iced::widget::{center, text};
+use iced::{Element, Subscription};
pub fn main() -> iced::Result {
iced::program("URL Handler - Iced", App::update, App::view)
@@ -44,11 +44,6 @@ impl App {
None => text("No URL received yet!"),
};
- container(content.size(48))
- .width(Length::Fill)
- .height(Length::Fill)
- .center_x()
- .center_y()
- .into()
+ center(content.size(48)).into()
}
}
diff --git a/examples/websocket/src/main.rs b/examples/websocket/src/main.rs
index b479fe89..ba1e1029 100644
--- a/examples/websocket/src/main.rs
+++ b/examples/websocket/src/main.rs
@@ -2,7 +2,7 @@ mod echo;
use iced::alignment::{self, Alignment};
use iced::widget::{
- button, column, container, row, scrollable, text, text_input,
+ self, button, center, column, row, scrollable, text, text_input,
};
use iced::{color, Command, Element, Length, Subscription};
use once_cell::sync::Lazy;
@@ -31,7 +31,10 @@ enum Message {
impl WebSocket {
fn load() -> Command<Message> {
- Command::perform(echo::server::run(), |_| Message::Server)
+ Command::batch([
+ Command::perform(echo::server::run(), |_| Message::Server),
+ widget::focus_next(),
+ ])
}
fn update(&mut self, message: Message) -> Command<Message> {
@@ -85,14 +88,10 @@ impl WebSocket {
fn view(&self) -> Element<Message> {
let message_log: Element<_> = if self.messages.is_empty() {
- container(
+ center(
text("Your messages will appear here...")
.color(color!(0x888888)),
)
- .width(Length::Fill)
- .height(Length::Fill)
- .center_x()
- .center_y()
.into()
} else {
scrollable(
diff --git a/futures/src/backend/native/tokio.rs b/futures/src/backend/native/tokio.rs
index 3ab7f675..df91d798 100644
--- a/futures/src/backend/native/tokio.rs
+++ b/futures/src/backend/native/tokio.rs
@@ -55,13 +55,15 @@ pub mod time {
let start = tokio::time::Instant::now() + self.0;
+ let mut interval = tokio::time::interval_at(start, self.0);
+ interval.set_missed_tick_behavior(
+ tokio::time::MissedTickBehavior::Skip,
+ );
+
let stream = {
- futures::stream::unfold(
- tokio::time::interval_at(start, self.0),
- |mut interval| async move {
- Some((interval.tick().await, interval))
- },
- )
+ futures::stream::unfold(interval, |mut interval| async move {
+ Some((interval.tick().await, interval))
+ })
};
stream.map(tokio::time::Instant::into_std).boxed()
diff --git a/graphics/src/cache.rs b/graphics/src/cache.rs
new file mode 100644
index 00000000..bbba79eb
--- /dev/null
+++ b/graphics/src/cache.rs
@@ -0,0 +1,189 @@
+//! Cache computations and efficiently reuse them.
+use std::cell::RefCell;
+use std::fmt;
+use std::sync::atomic::{self, AtomicU64};
+
+/// A simple cache that stores generated values to avoid recomputation.
+///
+/// Keeps track of the last generated value after clearing.
+pub struct Cache<T> {
+ group: Group,
+ state: RefCell<State<T>>,
+}
+
+impl<T> Cache<T> {
+ /// Creates a new empty [`Cache`].
+ pub fn new() -> Self {
+ Cache {
+ group: Group::singleton(),
+ state: RefCell::new(State::Empty { previous: None }),
+ }
+ }
+
+ /// Creates a new empty [`Cache`] with the given [`Group`].
+ ///
+ /// Caches within the same group may reuse internal rendering storage.
+ ///
+ /// You should generally group caches that are likely to change
+ /// together.
+ pub fn with_group(group: Group) -> Self {
+ assert!(
+ !group.is_singleton(),
+ "The group {group:?} cannot be shared!"
+ );
+
+ Cache {
+ group,
+ state: RefCell::new(State::Empty { previous: None }),
+ }
+ }
+
+ /// Returns the [`Group`] of the [`Cache`].
+ pub fn group(&self) -> Group {
+ self.group
+ }
+
+ /// Puts the given value in the [`Cache`].
+ ///
+ /// Notice that, given this is a cache, a mutable reference is not
+ /// necessary to call this method. You can safely update the cache in
+ /// rendering code.
+ pub fn put(&self, value: T) {
+ *self.state.borrow_mut() = State::Filled { current: value };
+ }
+
+ /// Returns a reference cell to the internal [`State`] of the [`Cache`].
+ pub fn state(&self) -> &RefCell<State<T>> {
+ &self.state
+ }
+
+ /// Clears the [`Cache`].
+ pub fn clear(&self)
+ where
+ T: Clone,
+ {
+ use std::ops::Deref;
+
+ let previous = match self.state.borrow().deref() {
+ State::Empty { previous } => previous.clone(),
+ State::Filled { current } => Some(current.clone()),
+ };
+
+ *self.state.borrow_mut() = State::Empty { previous };
+ }
+}
+
+/// A cache group.
+///
+/// Caches that share the same group generally change together.
+///
+/// A cache group can be used to implement certain performance
+/// optimizations during rendering, like batching or sharing atlases.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct Group {
+ id: u64,
+ is_singleton: bool,
+}
+
+impl Group {
+ /// Generates a new unique cache [`Group`].
+ pub fn unique() -> Self {
+ static NEXT: AtomicU64 = AtomicU64::new(0);
+
+ Self {
+ id: NEXT.fetch_add(1, atomic::Ordering::Relaxed),
+ is_singleton: false,
+ }
+ }
+
+ /// Returns `true` if the [`Group`] can only ever have a
+ /// single [`Cache`] in it.
+ ///
+ /// This is the default kind of [`Group`] assigned when using
+ /// [`Cache::new`].
+ ///
+ /// Knowing that a [`Group`] will never be shared may be
+ /// useful for rendering backends to perform additional
+ /// optimizations.
+ pub fn is_singleton(self) -> bool {
+ self.is_singleton
+ }
+
+ fn singleton() -> Self {
+ Self {
+ is_singleton: true,
+ ..Self::unique()
+ }
+ }
+}
+
+impl<T> fmt::Debug for Cache<T>
+where
+ T: fmt::Debug,
+{
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ use std::ops::Deref;
+
+ let state = self.state.borrow();
+
+ match state.deref() {
+ State::Empty { previous } => {
+ write!(f, "Cache::Empty {{ previous: {previous:?} }}")
+ }
+ State::Filled { current } => {
+ write!(f, "Cache::Filled {{ current: {current:?} }}")
+ }
+ }
+ }
+}
+
+impl<T> Default for Cache<T> {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+/// The state of a [`Cache`].
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum State<T> {
+ /// The [`Cache`] is empty.
+ Empty {
+ /// The previous value of the [`Cache`].
+ previous: Option<T>,
+ },
+ /// The [`Cache`] is filled.
+ Filled {
+ /// The current value of the [`Cache`]
+ current: T,
+ },
+}
+
+/// A piece of data that can be cached.
+pub trait Cached: Sized {
+ /// The type of cache produced.
+ type Cache: Clone;
+
+ /// Loads the [`Cache`] into a proper instance.
+ ///
+ /// [`Cache`]: Self::Cache
+ fn load(cache: &Self::Cache) -> Self;
+
+ /// Caches this value, producing its corresponding [`Cache`].
+ ///
+ /// [`Cache`]: Self::Cache
+ fn cache(self, group: Group, previous: Option<Self::Cache>) -> Self::Cache;
+}
+
+#[cfg(debug_assertions)]
+impl Cached for () {
+ type Cache = ();
+
+ fn load(_cache: &Self::Cache) -> Self {}
+
+ fn cache(
+ self,
+ _group: Group,
+ _previous: Option<Self::Cache>,
+ ) -> Self::Cache {
+ }
+}
diff --git a/graphics/src/cached.rs b/graphics/src/cached.rs
deleted file mode 100644
index c0e4e029..00000000
--- a/graphics/src/cached.rs
+++ /dev/null
@@ -1,24 +0,0 @@
-/// A piece of data that can be cached.
-pub trait Cached: Sized {
- /// The type of cache produced.
- type Cache: Clone;
-
- /// Loads the [`Cache`] into a proper instance.
- ///
- /// [`Cache`]: Self::Cache
- fn load(cache: &Self::Cache) -> Self;
-
- /// Caches this value, producing its corresponding [`Cache`].
- ///
- /// [`Cache`]: Self::Cache
- fn cache(self, previous: Option<Self::Cache>) -> Self::Cache;
-}
-
-#[cfg(debug_assertions)]
-impl Cached for () {
- type Cache = ();
-
- fn load(_cache: &Self::Cache) -> Self {}
-
- fn cache(self, _previous: Option<Self::Cache>) -> Self::Cache {}
-}
diff --git a/graphics/src/geometry.rs b/graphics/src/geometry.rs
index dd07097f..ab4a7a36 100644
--- a/graphics/src/geometry.rs
+++ b/graphics/src/geometry.rs
@@ -18,8 +18,8 @@ pub use text::Text;
pub use crate::gradient::{self, Gradient};
+use crate::cache::Cached;
use crate::core::{self, Size};
-use crate::Cached;
/// A renderer capable of drawing some [`Self::Geometry`].
pub trait Renderer: core::Renderer {
diff --git a/graphics/src/geometry/cache.rs b/graphics/src/geometry/cache.rs
index 665e996b..d70cee0b 100644
--- a/graphics/src/geometry/cache.rs
+++ b/graphics/src/geometry/cache.rs
@@ -1,8 +1,8 @@
+use crate::cache::{self, Cached};
use crate::core::Size;
use crate::geometry::{self, Frame};
-use crate::Cached;
-use std::cell::RefCell;
+pub use cache::Group;
/// A simple cache that stores generated geometry to avoid recomputation.
///
@@ -12,7 +12,13 @@ pub struct Cache<Renderer>
where
Renderer: geometry::Renderer,
{
- state: RefCell<State<Renderer::Geometry>>,
+ raw: crate::Cache<Data<<Renderer::Geometry as Cached>::Cache>>,
+}
+
+#[derive(Debug, Clone)]
+struct Data<T> {
+ bounds: Size,
+ geometry: T,
}
impl<Renderer> Cache<Renderer>
@@ -22,20 +28,25 @@ where
/// Creates a new empty [`Cache`].
pub fn new() -> Self {
Cache {
- state: RefCell::new(State::Empty { previous: None }),
+ raw: cache::Cache::new(),
+ }
+ }
+
+ /// Creates a new empty [`Cache`] with the given [`Group`].
+ ///
+ /// Caches within the same group may reuse internal rendering storage.
+ ///
+ /// You should generally group caches that are likely to change
+ /// together.
+ pub fn with_group(group: Group) -> Self {
+ Cache {
+ raw: crate::Cache::with_group(group),
}
}
/// Clears the [`Cache`], forcing a redraw the next time it is used.
pub fn clear(&self) {
- use std::ops::Deref;
-
- let previous = match self.state.borrow().deref() {
- State::Empty { previous } => previous.clone(),
- State::Filled { geometry, .. } => Some(geometry.clone()),
- };
-
- *self.state.borrow_mut() = State::Empty { previous };
+ self.raw.clear();
}
/// Draws geometry using the provided closure and stores it in the
@@ -56,27 +67,30 @@ where
) -> Renderer::Geometry {
use std::ops::Deref;
- let previous = match self.state.borrow().deref() {
- State::Empty { previous } => previous.clone(),
- State::Filled {
- bounds: cached_bounds,
- geometry,
- } => {
- if *cached_bounds == bounds {
- return Cached::load(geometry);
+ let state = self.raw.state();
+
+ let previous = match state.borrow().deref() {
+ cache::State::Empty { previous } => {
+ previous.as_ref().map(|data| data.geometry.clone())
+ }
+ cache::State::Filled { current } => {
+ if current.bounds == bounds {
+ return Cached::load(&current.geometry);
}
- Some(geometry.clone())
+ Some(current.geometry.clone())
}
};
let mut frame = Frame::new(renderer, bounds);
draw_fn(&mut frame);
- let geometry = frame.into_geometry().cache(previous);
+ let geometry = frame.into_geometry().cache(self.raw.group(), previous);
let result = Cached::load(&geometry);
- *self.state.borrow_mut() = State::Filled { bounds, geometry };
+ *state.borrow_mut() = cache::State::Filled {
+ current: Data { bounds, geometry },
+ };
result
}
@@ -85,16 +99,10 @@ where
impl<Renderer> std::fmt::Debug for Cache<Renderer>
where
Renderer: geometry::Renderer,
+ <Renderer::Geometry as Cached>::Cache: std::fmt::Debug,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- let state = self.state.borrow();
-
- match *state {
- State::Empty { .. } => write!(f, "Cache::Empty"),
- State::Filled { bounds, .. } => {
- write!(f, "Cache::Filled {{ bounds: {bounds:?} }}")
- }
- }
+ write!(f, "{:?}", &self.raw)
}
}
@@ -106,16 +114,3 @@ where
Self::new()
}
}
-
-enum State<Geometry>
-where
- Geometry: Cached,
-{
- Empty {
- previous: Option<Geometry::Cache>,
- },
- Filled {
- bounds: Size,
- geometry: Geometry::Cache,
- },
-}
diff --git a/graphics/src/image.rs b/graphics/src/image.rs
index c6135e9e..318592be 100644
--- a/graphics/src/image.rs
+++ b/graphics/src/image.rs
@@ -2,9 +2,7 @@
#[cfg(feature = "image")]
pub use ::image as image_rs;
-use crate::core::image;
-use crate::core::svg;
-use crate::core::{Color, Rectangle};
+use crate::core::{image, svg, Color, Radians, Rectangle};
/// A raster or vector image.
#[derive(Debug, Clone, PartialEq)]
@@ -19,6 +17,12 @@ pub enum Image {
/// 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 {
@@ -30,6 +34,12 @@ pub enum Image {
/// The bounds of the image.
bounds: Rectangle,
+
+ /// The rotation of the image.
+ rotation: Radians,
+
+ /// The opacity of the image.
+ opacity: f32,
},
}
@@ -37,9 +47,12 @@ impl Image {
/// Returns the bounds of the [`Image`].
pub fn bounds(&self) -> Rectangle {
match self {
- Image::Raster { bounds, .. } | Image::Vector { bounds, .. } => {
- *bounds
+ Image::Raster {
+ bounds, rotation, ..
}
+ | Image::Vector {
+ bounds, rotation, ..
+ } => bounds.rotate(*rotation),
}
}
}
@@ -50,7 +63,8 @@ impl Image {
/// [`Handle`]: image::Handle
pub fn load(
handle: &image::Handle,
-) -> ::image::ImageResult<::image::DynamicImage> {
+) -> ::image::ImageResult<::image::ImageBuffer<::image::Rgba<u8>, image::Bytes>>
+{
use bitflags::bitflags;
bitflags! {
@@ -100,8 +114,8 @@ pub fn load(
}
}
- match handle.data() {
- image::Data::Path(path) => {
+ let (width, height, pixels) = match handle {
+ image::Handle::Path(_, path) => {
let image = ::image::open(path)?;
let operation = std::fs::File::open(path)
@@ -110,33 +124,44 @@ pub fn load(
.and_then(|mut reader| Operation::from_exif(&mut reader).ok())
.unwrap_or_else(Operation::empty);
- Ok(operation.perform(image))
+ let rgba = operation.perform(image).into_rgba8();
+
+ (
+ rgba.width(),
+ rgba.height(),
+ image::Bytes::from(rgba.into_raw()),
+ )
}
- image::Data::Bytes(bytes) => {
+ image::Handle::Bytes(_, bytes) => {
let image = ::image::load_from_memory(bytes)?;
let operation =
Operation::from_exif(&mut std::io::Cursor::new(bytes))
.ok()
.unwrap_or_else(Operation::empty);
- Ok(operation.perform(image))
+ let rgba = operation.perform(image).into_rgba8();
+
+ (
+ rgba.width(),
+ rgba.height(),
+ image::Bytes::from(rgba.into_raw()),
+ )
}
- image::Data::Rgba {
+ image::Handle::Rgba {
width,
height,
pixels,
- } => {
- if let Some(image) =
- ::image::ImageBuffer::from_vec(*width, *height, pixels.to_vec())
- {
- Ok(::image::DynamicImage::ImageRgba8(image))
- } else {
- Err(::image::error::ImageError::Limits(
- ::image::error::LimitError::from_kind(
- ::image::error::LimitErrorKind::DimensionError,
- ),
- ))
- }
- }
+ ..
+ } => (*width, *height, pixels.clone()),
+ };
+
+ if let Some(image) = ::image::ImageBuffer::from_raw(width, height, pixels) {
+ Ok(image)
+ } else {
+ Err(::image::error::ImageError::Limits(
+ ::image::error::LimitError::from_kind(
+ ::image::error::LimitErrorKind::DimensionError,
+ ),
+ ))
}
}
diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs
index 865ebd97..b5ef55e7 100644
--- a/graphics/src/lib.rs
+++ b/graphics/src/lib.rs
@@ -9,10 +9,10 @@
)]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
mod antialiasing;
-mod cached;
mod settings;
mod viewport;
+pub mod cache;
pub mod color;
pub mod compositor;
pub mod damage;
@@ -27,7 +27,7 @@ pub mod text;
pub mod geometry;
pub use antialiasing::Antialiasing;
-pub use cached::Cached;
+pub use cache::Cache;
pub use compositor::Compositor;
pub use error::Error;
pub use gradient::Gradient;
diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs
index c488a51c..4b8f0f2a 100644
--- a/graphics/src/text/editor.rs
+++ b/graphics/src/text/editor.rs
@@ -456,10 +456,14 @@ impl editor::Editor for Editor {
}
}
Action::Scroll { lines } => {
- editor.action(
- font_system.raw(),
- cosmic_text::Action::Scroll { lines },
- );
+ let (_, height) = editor.buffer().size();
+
+ if height < i32::MAX as f32 {
+ editor.action(
+ font_system.raw(),
+ cosmic_text::Action::Scroll { lines },
+ );
+ }
}
}
diff --git a/renderer/src/fallback.rs b/renderer/src/fallback.rs
index c932de00..6a169692 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, Rectangle, Size, Transformation,
+ self, Background, Color, Point, Radians, Rectangle, Size, Transformation,
};
use crate::graphics;
use crate::graphics::compositor;
@@ -154,11 +154,19 @@ where
handle: Self::Handle,
filter_method: image::FilterMethod,
bounds: Rectangle,
+ rotation: Radians,
+ opacity: f32,
) {
delegate!(
self,
renderer,
- renderer.draw_image(handle, filter_method, bounds)
+ renderer.draw_image(
+ handle,
+ filter_method,
+ bounds,
+ rotation,
+ opacity
+ )
);
}
}
@@ -177,8 +185,14 @@ where
handle: svg::Handle,
color: Option<Color>,
bounds: Rectangle,
+ rotation: Radians,
+ opacity: f32,
) {
- delegate!(self, renderer, renderer.draw_svg(handle, color, bounds));
+ delegate!(
+ self,
+ renderer,
+ renderer.draw_svg(handle, color, bounds, rotation, opacity)
+ );
}
}
@@ -428,8 +442,8 @@ where
mod geometry {
use super::Renderer;
use crate::core::{Point, Radians, Rectangle, Size, Vector};
+ use crate::graphics::cache::{self, Cached};
use crate::graphics::geometry::{self, Fill, Path, Stroke, Text};
- use crate::graphics::Cached;
impl<A, B> geometry::Renderer for Renderer<A, B>
where
@@ -483,21 +497,25 @@ mod geometry {
}
}
- fn cache(self, previous: Option<Self::Cache>) -> Self::Cache {
+ fn cache(
+ self,
+ group: cache::Group,
+ previous: Option<Self::Cache>,
+ ) -> Self::Cache {
match (self, previous) {
(
Self::Primary(geometry),
Some(Geometry::Primary(previous)),
- ) => Geometry::Primary(geometry.cache(Some(previous))),
+ ) => Geometry::Primary(geometry.cache(group, Some(previous))),
(Self::Primary(geometry), None) => {
- Geometry::Primary(geometry.cache(None))
+ Geometry::Primary(geometry.cache(group, None))
}
(
Self::Secondary(geometry),
Some(Geometry::Secondary(previous)),
- ) => Geometry::Secondary(geometry.cache(Some(previous))),
+ ) => Geometry::Secondary(geometry.cache(group, Some(previous))),
(Self::Secondary(geometry), None) => {
- Geometry::Secondary(geometry.cache(None))
+ Geometry::Secondary(geometry.cache(group, None))
}
_ => unreachable!(),
}
diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml
index 21503462..703c3ed9 100644
--- a/runtime/Cargo.toml
+++ b/runtime/Cargo.toml
@@ -18,6 +18,7 @@ debug = []
multi-window = []
[dependencies]
+bytes.workspace = true
iced_core.workspace = true
iced_futures.workspace = true
iced_futures.features = ["thread-pool"]
diff --git a/runtime/src/multi_window/state.rs b/runtime/src/multi_window/state.rs
index afd04519..10366ec0 100644
--- a/runtime/src/multi_window/state.rs
+++ b/runtime/src/multi_window/state.rs
@@ -48,7 +48,7 @@ where
caches,
queued_events: Vec::new(),
queued_messages: Vec::new(),
- mouse_interaction: mouse::Interaction::Idle,
+ mouse_interaction: mouse::Interaction::None,
}
}
diff --git a/runtime/src/program/state.rs b/runtime/src/program/state.rs
index d685b07c..c6589c22 100644
--- a/runtime/src/program/state.rs
+++ b/runtime/src/program/state.rs
@@ -47,7 +47,7 @@ where
cache,
queued_events: Vec::new(),
queued_messages: Vec::new(),
- mouse_interaction: mouse::Interaction::Idle,
+ mouse_interaction: mouse::Interaction::None,
}
}
diff --git a/runtime/src/window/screenshot.rs b/runtime/src/window/screenshot.rs
index 21e04718..fb318110 100644
--- a/runtime/src/window/screenshot.rs
+++ b/runtime/src/window/screenshot.rs
@@ -1,8 +1,8 @@
//! Take screenshots of a window.
use crate::core::{Rectangle, Size};
+use bytes::Bytes;
use std::fmt::{Debug, Formatter};
-use std::sync::Arc;
/// Data of a screenshot, captured with `window::screenshot()`.
///
@@ -10,7 +10,7 @@ use std::sync::Arc;
#[derive(Clone)]
pub struct Screenshot {
/// The bytes of the [`Screenshot`].
- pub bytes: Arc<Vec<u8>>,
+ pub bytes: Bytes,
/// The size of the [`Screenshot`].
pub size: Size<u32>,
}
@@ -28,9 +28,9 @@ impl Debug for Screenshot {
impl Screenshot {
/// Creates a new [`Screenshot`].
- pub fn new(bytes: Vec<u8>, size: Size<u32>) -> Self {
+ pub fn new(bytes: impl Into<Bytes>, size: Size<u32>) -> Self {
Self {
- bytes: Arc::new(bytes),
+ bytes: bytes.into(),
size,
}
}
@@ -68,7 +68,7 @@ impl Screenshot {
);
Ok(Self {
- bytes: Arc::new(chopped),
+ bytes: Bytes::from(chopped),
size: Size::new(region.width, region.height),
})
}
@@ -80,6 +80,12 @@ impl AsRef<[u8]> for Screenshot {
}
}
+impl From<Screenshot> for Bytes {
+ fn from(screenshot: Screenshot) -> Self {
+ screenshot.bytes
+ }
+}
+
#[derive(Debug, thiserror::Error)]
/// Errors that can occur when cropping a [`Screenshot`].
pub enum CropError {
diff --git a/src/application.rs b/src/application.rs
index 9197834b..d12ba73d 100644
--- a/src/application.rs
+++ b/src/application.rs
@@ -212,26 +212,11 @@ where
..crate::graphics::Settings::default()
};
- let run = crate::shell::application::run::<
+ Ok(crate::shell::application::run::<
Instance<Self>,
Self::Executor,
<Self::Renderer as compositor::Default>::Compositor,
- >(settings.into(), renderer_settings);
-
- #[cfg(target_arch = "wasm32")]
- {
- use crate::futures::FutureExt;
- use iced_futures::backend::wasm::wasm_bindgen::Executor;
-
- Executor::new()
- .expect("Create Wasm executor")
- .spawn(run.map(|_| ()));
-
- Ok(())
- }
-
- #[cfg(not(target_arch = "wasm32"))]
- Ok(crate::futures::executor::block_on(run)?)
+ >(settings.into(), renderer_settings)?)
}
}
diff --git a/src/lib.rs b/src/lib.rs
index c96c28a3..50ee7ecc 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -51,6 +51,7 @@
//! We start by modelling the __state__ of our application:
//!
//! ```
+//! #[derive(Default)]
//! struct Counter {
//! // The counter value
//! value: i32,
@@ -199,8 +200,8 @@ pub use crate::core::gradient;
pub use crate::core::theme;
pub use crate::core::{
Alignment, Background, Border, Color, ContentFit, Degrees, Gradient,
- Length, Padding, Pixels, Point, Radians, Rectangle, Shadow, Size, Theme,
- Transformation, Vector,
+ Length, Padding, Pixels, Point, Radians, Rectangle, Rotation, Shadow, Size,
+ Theme, Transformation, Vector,
};
pub mod clipboard {
diff --git a/src/window/icon.rs b/src/window/icon.rs
index ef71c228..7fe4ca7b 100644
--- a/src/window/icon.rs
+++ b/src/window/icon.rs
@@ -54,7 +54,7 @@ pub enum Error {
InvalidError(#[from] icon::Error),
/// The underlying OS failed to create the icon.
- #[error("The underlying OS failted to create the window icon: {0}")]
+ #[error("The underlying OS failed to create the window icon: {0}")]
OsError(#[from] io::Error),
/// The `image` crate reported an error.
diff --git a/tiny_skia/src/engine.rs b/tiny_skia/src/engine.rs
index fbca1274..028b304f 100644
--- a/tiny_skia/src/engine.rs
+++ b/tiny_skia/src/engine.rs
@@ -550,6 +550,8 @@ impl Engine {
handle,
filter_method,
bounds,
+ rotation,
+ opacity,
} => {
let physical_bounds = *bounds * _transformation;
@@ -560,12 +562,22 @@ impl Engine {
let clip_mask = (!physical_bounds.is_within(&_clip_bounds))
.then_some(_clip_mask as &_);
+ let center = physical_bounds.center();
+ let radians = f32::from(*rotation);
+
+ let transform = into_transform(_transformation).post_rotate_at(
+ radians.to_degrees(),
+ center.x,
+ center.y,
+ );
+
self.raster_pipeline.draw(
handle,
*filter_method,
*bounds,
+ *opacity,
_pixels,
- into_transform(_transformation),
+ transform,
clip_mask,
);
}
@@ -574,6 +586,8 @@ impl Engine {
handle,
color,
bounds,
+ rotation,
+ opacity,
} => {
let physical_bounds = *bounds * _transformation;
@@ -584,11 +598,22 @@ impl Engine {
let clip_mask = (!physical_bounds.is_within(&_clip_bounds))
.then_some(_clip_mask as &_);
+ let center = physical_bounds.center();
+ let radians = f32::from(*rotation);
+
+ let transform = into_transform(_transformation).post_rotate_at(
+ radians.to_degrees(),
+ center.x,
+ center.y,
+ );
+
self.vector_pipeline.draw(
handle,
*color,
physical_bounds,
+ *opacity,
_pixels,
+ transform,
clip_mask,
);
}
diff --git a/tiny_skia/src/geometry.rs b/tiny_skia/src/geometry.rs
index 117daf41..02b6e1b9 100644
--- a/tiny_skia/src/geometry.rs
+++ b/tiny_skia/src/geometry.rs
@@ -1,9 +1,10 @@
use crate::core::text::LineHeight;
use crate::core::{Pixels, Point, Radians, Rectangle, Size, 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::{Cached, Gradient, Text};
+use crate::graphics::{Gradient, Text};
use crate::Primitive;
use std::rc::Rc;
@@ -32,7 +33,7 @@ impl Cached for Geometry {
Self::Cache(cache.clone())
}
- fn cache(self, _previous: Option<Cache>) -> Cache {
+ fn cache(self, _group: cache::Group, _previous: Option<Cache>) -> Cache {
match self {
Self::Live {
primitives,
diff --git a/tiny_skia/src/layer.rs b/tiny_skia/src/layer.rs
index 3e42e4aa..48fca1d8 100644
--- a/tiny_skia/src/layer.rs
+++ b/tiny_skia/src/layer.rs
@@ -1,7 +1,7 @@
-use crate::core::image;
-use crate::core::renderer::Quad;
-use crate::core::svg;
-use crate::core::{Background, Color, Point, Rectangle, Transformation};
+use crate::core::{
+ image, renderer::Quad, svg, Background, Color, Point, Radians, Rectangle,
+ Transformation,
+};
use crate::graphics::damage;
use crate::graphics::layer;
use crate::graphics::text::{Editor, Paragraph, Text};
@@ -121,11 +121,15 @@ impl Layer {
filter_method: image::FilterMethod,
bounds: Rectangle,
transformation: Transformation,
+ rotation: Radians,
+ opacity: f32,
) {
let image = Image::Raster {
handle,
filter_method,
bounds: bounds * transformation,
+ rotation,
+ opacity,
};
self.images.push(image);
@@ -137,11 +141,15 @@ impl Layer {
color: Option<Color>,
bounds: Rectangle,
transformation: Transformation,
+ rotation: Radians,
+ opacity: f32,
) {
let svg = Image::Vector {
handle,
color,
bounds: bounds * transformation,
+ rotation,
+ opacity,
};
self.images.push(svg);
diff --git a/tiny_skia/src/lib.rs b/tiny_skia/src/lib.rs
index 4c2c9430..1aabff00 100644
--- a/tiny_skia/src/lib.rs
+++ b/tiny_skia/src/lib.rs
@@ -377,9 +377,18 @@ impl core::image::Renderer for Renderer {
handle: Self::Handle,
filter_method: core::image::FilterMethod,
bounds: Rectangle,
+ rotation: core::Radians,
+ opacity: f32,
) {
let (layer, transformation) = self.layers.current_mut();
- layer.draw_image(handle, filter_method, bounds, transformation);
+ layer.draw_image(
+ handle,
+ filter_method,
+ bounds,
+ transformation,
+ rotation,
+ opacity,
+ );
}
}
@@ -397,9 +406,18 @@ impl core::svg::Renderer for Renderer {
handle: core::svg::Handle,
color: Option<Color>,
bounds: Rectangle,
+ rotation: core::Radians,
+ opacity: f32,
) {
let (layer, transformation) = self.layers.current_mut();
- layer.draw_svg(handle, color, bounds, transformation);
+ layer.draw_svg(
+ handle,
+ color,
+ bounds,
+ transformation,
+ rotation,
+ opacity,
+ );
}
}
diff --git a/tiny_skia/src/raster.rs b/tiny_skia/src/raster.rs
index 176b0da9..c40f55b2 100644
--- a/tiny_skia/src/raster.rs
+++ b/tiny_skia/src/raster.rs
@@ -31,6 +31,7 @@ impl Pipeline {
handle: &raster::Handle,
filter_method: raster::FilterMethod,
bounds: Rectangle,
+ opacity: f32,
pixels: &mut tiny_skia::PixmapMut<'_>,
transform: tiny_skia::Transform,
clip_mask: Option<&tiny_skia::Mask>,
@@ -56,6 +57,7 @@ impl Pipeline {
image,
&tiny_skia::PixmapPaint {
quality,
+ opacity,
..Default::default()
},
transform,
@@ -71,8 +73,8 @@ impl Pipeline {
#[derive(Debug, Default)]
struct Cache {
- entries: FxHashMap<u64, Option<Entry>>,
- hits: FxHashSet<u64>,
+ entries: FxHashMap<raster::Id, Option<Entry>>,
+ hits: FxHashSet<raster::Id>,
}
impl Cache {
@@ -83,7 +85,7 @@ impl Cache {
let id = handle.id();
if let hash_map::Entry::Vacant(entry) = self.entries.entry(id) {
- let image = graphics::image::load(handle).ok()?.into_rgba8();
+ let image = graphics::image::load(handle).ok()?;
let mut buffer =
vec![0u32; image.width() as usize * image.height() as usize];
diff --git a/tiny_skia/src/vector.rs b/tiny_skia/src/vector.rs
index 5150cffe..bbe08cb8 100644
--- a/tiny_skia/src/vector.rs
+++ b/tiny_skia/src/vector.rs
@@ -4,6 +4,7 @@ use crate::graphics::text;
use resvg::usvg::{self, TreeTextToPath};
use rustc_hash::{FxHashMap, FxHashSet};
+use tiny_skia::Transform;
use std::cell::RefCell;
use std::collections::hash_map;
@@ -33,7 +34,9 @@ impl Pipeline {
handle: &Handle,
color: Option<Color>,
bounds: Rectangle,
+ opacity: f32,
pixels: &mut tiny_skia::PixmapMut<'_>,
+ transform: Transform,
clip_mask: Option<&tiny_skia::Mask>,
) {
if let Some(image) = self.cache.borrow_mut().draw(
@@ -45,8 +48,11 @@ impl Pipeline {
bounds.x as i32,
bounds.y as i32,
image,
- &tiny_skia::PixmapPaint::default(),
- tiny_skia::Transform::identity(),
+ &tiny_skia::PixmapPaint {
+ opacity,
+ ..tiny_skia::PixmapPaint::default()
+ },
+ transform,
clip_mask,
);
}
diff --git a/wgpu/src/color.rs b/wgpu/src/color.rs
index 890f3f89..9d593d9c 100644
--- a/wgpu/src/color.rs
+++ b/wgpu/src/color.rs
@@ -1,5 +1,7 @@
use std::borrow::Cow;
+use wgpu::util::DeviceExt;
+
pub fn convert(
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
@@ -15,28 +17,58 @@ pub fn convert(
..wgpu::SamplerDescriptor::default()
});
- //sampler in 0
- let sampler_layout =
+ #[derive(Debug, Clone, Copy, bytemuck::Zeroable, bytemuck::Pod)]
+ #[repr(C)]
+ struct Ratio {
+ u: f32,
+ v: f32,
+ }
+
+ let ratio = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
+ label: Some("iced-wgpu::triangle::msaa ratio"),
+ contents: bytemuck::bytes_of(&Ratio { u: 1.0, v: 1.0 }),
+ usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::UNIFORM,
+ });
+
+ let constant_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("iced_wgpu.offscreen.blit.sampler_layout"),
- entries: &[wgpu::BindGroupLayoutEntry {
- binding: 0,
- visibility: wgpu::ShaderStages::FRAGMENT,
- ty: wgpu::BindingType::Sampler(
- wgpu::SamplerBindingType::NonFiltering,
- ),
- count: None,
- }],
+ entries: &[
+ wgpu::BindGroupLayoutEntry {
+ binding: 0,
+ visibility: wgpu::ShaderStages::FRAGMENT,
+ ty: wgpu::BindingType::Sampler(
+ wgpu::SamplerBindingType::NonFiltering,
+ ),
+ count: None,
+ },
+ wgpu::BindGroupLayoutEntry {
+ binding: 1,
+ visibility: wgpu::ShaderStages::VERTEX,
+ ty: wgpu::BindingType::Buffer {
+ ty: wgpu::BufferBindingType::Uniform,
+ has_dynamic_offset: false,
+ min_binding_size: None,
+ },
+ count: None,
+ },
+ ],
});
- let sampler_bind_group =
+ let constant_bind_group =
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("iced_wgpu.offscreen.sampler.bind_group"),
- layout: &sampler_layout,
- entries: &[wgpu::BindGroupEntry {
- binding: 0,
- resource: wgpu::BindingResource::Sampler(&sampler),
- }],
+ layout: &constant_layout,
+ entries: &[
+ wgpu::BindGroupEntry {
+ binding: 0,
+ resource: wgpu::BindingResource::Sampler(&sampler),
+ },
+ wgpu::BindGroupEntry {
+ binding: 1,
+ resource: ratio.as_entire_binding(),
+ },
+ ],
});
let texture_layout =
@@ -59,7 +91,7 @@ pub fn convert(
let pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("iced_wgpu.offscreen.blit.pipeline_layout"),
- bind_group_layouts: &[&sampler_layout, &texture_layout],
+ bind_group_layouts: &[&constant_layout, &texture_layout],
push_constant_ranges: &[],
});
@@ -152,7 +184,7 @@ pub fn convert(
});
pass.set_pipeline(&pipeline);
- pass.set_bind_group(0, &sampler_bind_group, &[]);
+ pass.set_bind_group(0, &constant_bind_group, &[]);
pass.set_bind_group(1, &texture_bind_group, &[]);
pass.draw(0..6, 0..1);
diff --git a/wgpu/src/engine.rs b/wgpu/src/engine.rs
index 96cd6db8..782fd58c 100644
--- a/wgpu/src/engine.rs
+++ b/wgpu/src/engine.rs
@@ -59,8 +59,11 @@ impl Engine {
}
#[cfg(any(feature = "image", feature = "svg"))]
- pub fn image_cache(&self) -> &crate::image::cache::Shared {
- self.image_pipeline.cache()
+ pub fn create_image_cache(
+ &self,
+ device: &wgpu::Device,
+ ) -> crate::image::Cache {
+ self.image_pipeline.create_cache(device)
}
pub fn submit(
diff --git a/wgpu/src/geometry.rs b/wgpu/src/geometry.rs
index 60967082..f6213e1d 100644
--- a/wgpu/src/geometry.rs
+++ b/wgpu/src/geometry.rs
@@ -3,6 +3,7 @@ use crate::core::text::LineHeight;
use crate::core::{
Pixels, Point, Radians, Rectangle, Size, Transformation, Vector,
};
+use crate::graphics::cache::{self, Cached};
use crate::graphics::color;
use crate::graphics::geometry::fill::{self, Fill};
use crate::graphics::geometry::{
@@ -10,7 +11,7 @@ use crate::graphics::geometry::{
};
use crate::graphics::gradient::{self, Gradient};
use crate::graphics::mesh::{self, Mesh};
-use crate::graphics::{self, Cached, Text};
+use crate::graphics::{self, Text};
use crate::text;
use crate::triangle;
@@ -38,7 +39,11 @@ impl Cached for Geometry {
Geometry::Cached(cache.clone())
}
- fn cache(self, previous: Option<Self::Cache>) -> Self::Cache {
+ fn cache(
+ self,
+ group: cache::Group,
+ previous: Option<Self::Cache>,
+ ) -> Self::Cache {
match self {
Self::Live { meshes, text } => {
if let Some(mut previous) = previous {
@@ -51,14 +56,14 @@ impl Cached for Geometry {
if let Some(cache) = &mut previous.text {
cache.update(text);
} else {
- previous.text = text::Cache::new(text);
+ previous.text = text::Cache::new(group, text);
}
previous
} else {
Cache {
meshes: triangle::Cache::new(meshes),
- text: text::Cache::new(text),
+ text: text::Cache::new(group, text),
}
}
}
diff --git a/wgpu/src/image/atlas.rs b/wgpu/src/image/atlas.rs
index ea36e06d..a1ec0d7b 100644
--- a/wgpu/src/image/atlas.rs
+++ b/wgpu/src/image/atlas.rs
@@ -15,15 +15,23 @@ pub const SIZE: u32 = 2048;
use crate::core::Size;
use crate::graphics::color;
+use std::sync::Arc;
+
#[derive(Debug)]
pub struct Atlas {
texture: wgpu::Texture,
texture_view: wgpu::TextureView,
+ texture_bind_group: wgpu::BindGroup,
+ texture_layout: Arc<wgpu::BindGroupLayout>,
layers: Vec<Layer>,
}
impl Atlas {
- pub fn new(device: &wgpu::Device, backend: wgpu::Backend) -> Self {
+ pub fn new(
+ device: &wgpu::Device,
+ backend: wgpu::Backend,
+ texture_layout: Arc<wgpu::BindGroupLayout>,
+ ) -> Self {
let layers = match backend {
// On the GL backend we start with 2 layers, to help wgpu figure
// out that this texture is `GL_TEXTURE_2D_ARRAY` rather than `GL_TEXTURE_2D`
@@ -60,15 +68,27 @@ impl Atlas {
..Default::default()
});
+ let texture_bind_group =
+ device.create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some("iced_wgpu::image texture atlas bind group"),
+ layout: &texture_layout,
+ entries: &[wgpu::BindGroupEntry {
+ binding: 0,
+ resource: wgpu::BindingResource::TextureView(&texture_view),
+ }],
+ });
+
Atlas {
texture,
texture_view,
+ texture_bind_group,
+ texture_layout,
layers,
}
}
- pub fn view(&self) -> &wgpu::TextureView {
- &self.texture_view
+ pub fn bind_group(&self) -> &wgpu::BindGroup {
+ &self.texture_bind_group
}
pub fn layer_count(&self) -> usize {
@@ -94,7 +114,7 @@ impl Atlas {
entry
};
- log::info!("Allocated atlas entry: {entry:?}");
+ log::debug!("Allocated atlas entry: {entry:?}");
// It is a webgpu requirement that:
// BufferCopyView.layout.bytes_per_row % wgpu::COPY_BYTES_PER_ROW_ALIGNMENT == 0
@@ -147,13 +167,20 @@ impl Atlas {
}
}
- log::info!("Current atlas: {self:?}");
+ if log::log_enabled!(log::Level::Debug) {
+ log::debug!(
+ "Atlas layers: {} (busy: {}, allocations: {})",
+ self.layer_count(),
+ self.layers.iter().filter(|layer| !layer.is_empty()).count(),
+ self.layers.iter().map(Layer::allocations).sum::<usize>(),
+ );
+ }
Some(entry)
}
pub fn remove(&mut self, entry: &Entry) {
- log::info!("Removing atlas entry: {entry:?}");
+ log::debug!("Removing atlas entry: {entry:?}");
match entry {
Entry::Contiguous(allocation) => {
@@ -266,7 +293,7 @@ impl Atlas {
}
fn deallocate(&mut self, allocation: &Allocation) {
- log::info!("Deallocating atlas: {allocation:?}");
+ log::debug!("Deallocating atlas: {allocation:?}");
match allocation {
Allocation::Full { layer } => {
@@ -414,5 +441,17 @@ impl Atlas {
dimension: Some(wgpu::TextureViewDimension::D2Array),
..Default::default()
});
+
+ self.texture_bind_group =
+ device.create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some("iced_wgpu::image texture atlas bind group"),
+ layout: &self.texture_layout,
+ entries: &[wgpu::BindGroupEntry {
+ binding: 0,
+ resource: wgpu::BindingResource::TextureView(
+ &self.texture_view,
+ ),
+ }],
+ });
}
}
diff --git a/wgpu/src/image/atlas/allocator.rs b/wgpu/src/image/atlas/allocator.rs
index 204a5c26..a51ac1f5 100644
--- a/wgpu/src/image/atlas/allocator.rs
+++ b/wgpu/src/image/atlas/allocator.rs
@@ -33,6 +33,10 @@ impl Allocator {
pub fn is_empty(&self) -> bool {
self.allocations == 0
}
+
+ pub fn allocations(&self) -> usize {
+ self.allocations
+ }
}
pub struct Region {
diff --git a/wgpu/src/image/atlas/layer.rs b/wgpu/src/image/atlas/layer.rs
index cf089601..fd6788d9 100644
--- a/wgpu/src/image/atlas/layer.rs
+++ b/wgpu/src/image/atlas/layer.rs
@@ -11,4 +11,12 @@ impl Layer {
pub fn is_empty(&self) -> bool {
matches!(self, Layer::Empty)
}
+
+ pub fn allocations(&self) -> usize {
+ match self {
+ Layer::Empty => 0,
+ Layer::Busy(allocator) => allocator.allocations(),
+ Layer::Full => 1,
+ }
+ }
}
diff --git a/wgpu/src/image/cache.rs b/wgpu/src/image/cache.rs
index 32118ed6..94f7071d 100644
--- a/wgpu/src/image/cache.rs
+++ b/wgpu/src/image/cache.rs
@@ -1,8 +1,7 @@
use crate::core::{self, Size};
use crate::image::atlas::{self, Atlas};
-use std::cell::{RefCell, RefMut};
-use std::rc::Rc;
+use std::sync::Arc;
#[derive(Debug)]
pub struct Cache {
@@ -14,9 +13,13 @@ pub struct Cache {
}
impl Cache {
- pub fn new(device: &wgpu::Device, backend: wgpu::Backend) -> Self {
+ pub fn new(
+ device: &wgpu::Device,
+ backend: wgpu::Backend,
+ layout: Arc<wgpu::BindGroupLayout>,
+ ) -> Self {
Self {
- atlas: Atlas::new(device, backend),
+ atlas: Atlas::new(device, backend, layout),
#[cfg(feature = "image")]
raster: crate::image::raster::Cache::default(),
#[cfg(feature = "svg")]
@@ -24,6 +27,10 @@ impl Cache {
}
}
+ pub fn bind_group(&self) -> &wgpu::BindGroup {
+ self.atlas.bind_group()
+ }
+
pub fn layer_count(&self) -> usize {
self.atlas.layer_count()
}
@@ -69,21 +76,6 @@ impl Cache {
)
}
- pub fn create_bind_group(
- &self,
- device: &wgpu::Device,
- layout: &wgpu::BindGroupLayout,
- ) -> wgpu::BindGroup {
- device.create_bind_group(&wgpu::BindGroupDescriptor {
- label: Some("iced_wgpu::image texture atlas bind group"),
- layout,
- entries: &[wgpu::BindGroupEntry {
- binding: 0,
- resource: wgpu::BindingResource::TextureView(self.atlas.view()),
- }],
- })
- }
-
pub fn trim(&mut self) {
#[cfg(feature = "image")]
self.raster.trim(&mut self.atlas);
@@ -92,16 +84,3 @@ impl Cache {
self.vector.trim(&mut self.atlas);
}
}
-
-#[derive(Debug, Clone)]
-pub struct Shared(Rc<RefCell<Cache>>);
-
-impl Shared {
- pub fn new(cache: Cache) -> Self {
- Self(Rc::new(RefCell::new(cache)))
- }
-
- pub fn lock(&self) -> RefMut<'_, Cache> {
- self.0.borrow_mut()
- }
-}
diff --git a/wgpu/src/image/mod.rs b/wgpu/src/image/mod.rs
index 86731cbf..daa2fe16 100644
--- a/wgpu/src/image/mod.rs
+++ b/wgpu/src/image/mod.rs
@@ -13,7 +13,9 @@ use crate::core::{Rectangle, Size, Transformation};
use crate::Buffer;
use bytemuck::{Pod, Zeroable};
+
use std::mem;
+use std::sync::Arc;
pub use crate::graphics::Image;
@@ -22,13 +24,11 @@ pub type Batch = Vec<Image>;
#[derive(Debug)]
pub struct Pipeline {
pipeline: wgpu::RenderPipeline,
+ backend: wgpu::Backend,
nearest_sampler: wgpu::Sampler,
linear_sampler: wgpu::Sampler,
- texture: wgpu::BindGroup,
- texture_version: usize,
- texture_layout: wgpu::BindGroupLayout,
+ texture_layout: Arc<wgpu::BindGroupLayout>,
constant_layout: wgpu::BindGroupLayout,
- cache: cache::Shared,
layers: Vec<Layer>,
prepare_layer: usize,
}
@@ -135,14 +135,20 @@ impl Pipeline {
attributes: &wgpu::vertex_attr_array!(
// Position
0 => Float32x2,
- // Scale
+ // Center
1 => Float32x2,
- // Atlas position
+ // Scale
2 => Float32x2,
+ // Rotation
+ 3 => Float32,
+ // Opacity
+ 4 => Float32,
+ // Atlas position
+ 5 => Float32x2,
// Atlas scale
- 3 => Float32x2,
+ 6 => Float32x2,
// Layer
- 4 => Sint32,
+ 7 => Sint32,
),
}],
},
@@ -180,25 +186,20 @@ impl Pipeline {
multiview: None,
});
- let cache = Cache::new(device, backend);
- let texture = cache.create_bind_group(device, &texture_layout);
-
Pipeline {
pipeline,
+ backend,
nearest_sampler,
linear_sampler,
- texture,
- texture_version: cache.layer_count(),
- texture_layout,
+ texture_layout: Arc::new(texture_layout),
constant_layout,
- cache: cache::Shared::new(cache),
layers: Vec::new(),
prepare_layer: 0,
}
}
- pub fn cache(&self) -> &cache::Shared {
- &self.cache
+ pub fn create_cache(&self, device: &wgpu::Device) -> Cache {
+ Cache::new(device, self.backend, self.texture_layout.clone())
}
pub fn prepare(
@@ -206,6 +207,7 @@ impl Pipeline {
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
belt: &mut wgpu::util::StagingBelt,
+ cache: &mut Cache,
images: &Batch,
transformation: Transformation,
scale: f32,
@@ -215,8 +217,6 @@ impl Pipeline {
let nearest_instances: &mut Vec<Instance> = &mut Vec::new();
let linear_instances: &mut Vec<Instance> = &mut Vec::new();
- let mut cache = self.cache.lock();
-
for image in images {
match &image {
#[cfg(feature = "image")]
@@ -224,6 +224,8 @@ impl Pipeline {
handle,
filter_method,
bounds,
+ rotation,
+ opacity,
} => {
if let Some(atlas_entry) =
cache.upload_raster(device, encoder, handle)
@@ -231,6 +233,8 @@ impl Pipeline {
add_instances(
[bounds.x, bounds.y],
[bounds.width, bounds.height],
+ f32::from(*rotation),
+ *opacity,
atlas_entry,
match filter_method {
crate::core::image::FilterMethod::Nearest => {
@@ -251,6 +255,8 @@ impl Pipeline {
handle,
color,
bounds,
+ rotation,
+ opacity,
} => {
let size = [bounds.width, bounds.height];
@@ -260,6 +266,8 @@ impl Pipeline {
add_instances(
[bounds.x, bounds.y],
size,
+ f32::from(*rotation),
+ *opacity,
atlas_entry,
nearest_instances,
);
@@ -274,16 +282,6 @@ impl Pipeline {
return;
}
- let texture_version = cache.layer_count();
-
- if self.texture_version != texture_version {
- log::info!("Atlas has grown. Recreating bind group...");
-
- self.texture =
- cache.create_bind_group(device, &self.texture_layout);
- self.texture_version = texture_version;
- }
-
if self.layers.len() <= self.prepare_layer {
self.layers.push(Layer::new(
device,
@@ -309,6 +307,7 @@ impl Pipeline {
pub fn render<'a>(
&'a self,
+ cache: &'a Cache,
layer: usize,
bounds: Rectangle<u32>,
render_pass: &mut wgpu::RenderPass<'a>,
@@ -323,14 +322,13 @@ impl Pipeline {
bounds.height,
);
- render_pass.set_bind_group(1, &self.texture, &[]);
+ render_pass.set_bind_group(1, cache.bind_group(), &[]);
layer.render(render_pass);
}
}
pub fn end_frame(&mut self) {
- self.cache.lock().trim();
self.prepare_layer = 0;
}
}
@@ -487,7 +485,10 @@ impl Data {
#[derive(Debug, Clone, Copy, Zeroable, Pod)]
struct Instance {
_position: [f32; 2],
+ _center: [f32; 2],
_size: [f32; 2],
+ _rotation: f32,
+ _opacity: f32,
_position_in_atlas: [f32; 2],
_size_in_atlas: [f32; 2],
_layer: u32,
@@ -506,12 +507,27 @@ struct Uniforms {
fn add_instances(
image_position: [f32; 2],
image_size: [f32; 2],
+ rotation: f32,
+ opacity: f32,
entry: &atlas::Entry,
instances: &mut Vec<Instance>,
) {
+ let center = [
+ image_position[0] + image_size[0] / 2.0,
+ image_position[1] + image_size[1] / 2.0,
+ ];
+
match entry {
atlas::Entry::Contiguous(allocation) => {
- add_instance(image_position, image_size, allocation, instances);
+ add_instance(
+ image_position,
+ center,
+ image_size,
+ rotation,
+ opacity,
+ allocation,
+ instances,
+ );
}
atlas::Entry::Fragmented { fragments, size } => {
let scaling_x = image_size[0] / size.width as f32;
@@ -537,7 +553,10 @@ fn add_instances(
fragment_height as f32 * scaling_y,
];
- add_instance(position, size, allocation, instances);
+ add_instance(
+ position, center, size, rotation, opacity, allocation,
+ instances,
+ );
}
}
}
@@ -546,7 +565,10 @@ fn add_instances(
#[inline]
fn add_instance(
position: [f32; 2],
+ center: [f32; 2],
size: [f32; 2],
+ rotation: f32,
+ opacity: f32,
allocation: &atlas::Allocation,
instances: &mut Vec<Instance>,
) {
@@ -556,7 +578,10 @@ fn add_instance(
let instance = Instance {
_position: position,
+ _center: center,
_size: size,
+ _rotation: rotation,
+ _opacity: opacity,
_position_in_atlas: [
(x as f32 + 0.5) / atlas::SIZE as f32,
(y as f32 + 0.5) / atlas::SIZE as f32,
diff --git a/wgpu/src/image/raster.rs b/wgpu/src/image/raster.rs
index 441b294f..4d3c3125 100644
--- a/wgpu/src/image/raster.rs
+++ b/wgpu/src/image/raster.rs
@@ -10,7 +10,7 @@ use rustc_hash::{FxHashMap, FxHashSet};
#[derive(Debug)]
pub enum Memory {
/// Image data on host
- Host(image_rs::ImageBuffer<image_rs::Rgba<u8>, Vec<u8>>),
+ Host(image_rs::ImageBuffer<image_rs::Rgba<u8>, image::Bytes>),
/// Storage entry
Device(atlas::Entry),
/// Image not found
@@ -38,8 +38,9 @@ impl Memory {
/// Caches image raster data
#[derive(Debug, Default)]
pub struct Cache {
- map: FxHashMap<u64, Memory>,
- hits: FxHashSet<u64>,
+ map: FxHashMap<image::Id, Memory>,
+ hits: FxHashSet<image::Id>,
+ should_trim: bool,
}
impl Cache {
@@ -50,11 +51,13 @@ impl Cache {
}
let memory = match graphics::image::load(handle) {
- Ok(image) => Memory::Host(image.to_rgba8()),
+ Ok(image) => Memory::Host(image),
Err(image_rs::error::ImageError::IoError(_)) => Memory::NotFound,
Err(_) => Memory::Invalid,
};
+ self.should_trim = true;
+
self.insert(handle, memory);
self.get(handle).unwrap()
}
@@ -86,6 +89,11 @@ impl Cache {
/// Trim cache misses from cache
pub fn trim(&mut self, atlas: &mut Atlas) {
+ // Only trim if new entries have landed in the `Cache`
+ if !self.should_trim {
+ return;
+ }
+
let hits = &self.hits;
self.map.retain(|k, memory| {
@@ -101,6 +109,7 @@ impl Cache {
});
self.hits.clear();
+ self.should_trim = false;
}
fn get(&mut self, handle: &image::Handle) -> Option<&mut Memory> {
diff --git a/wgpu/src/image/vector.rs b/wgpu/src/image/vector.rs
index d681b2e6..c6d829af 100644
--- a/wgpu/src/image/vector.rs
+++ b/wgpu/src/image/vector.rs
@@ -37,6 +37,7 @@ pub struct Cache {
rasterized: FxHashMap<(u64, u32, u32, ColorFilter), atlas::Entry>,
svg_hits: FxHashSet<u64>,
rasterized_hits: FxHashSet<(u64, u32, u32, ColorFilter)>,
+ should_trim: bool,
}
type ColorFilter = Option<[u8; 4]>;
@@ -76,6 +77,8 @@ impl Cache {
}
}
+ self.should_trim = true;
+
let _ = self.svgs.insert(handle.id(), svg);
self.svgs.get(&handle.id()).unwrap()
}
@@ -176,6 +179,10 @@ impl Cache {
/// Load svg and upload raster data
pub fn trim(&mut self, atlas: &mut Atlas) {
+ if !self.should_trim {
+ return;
+ }
+
let svg_hits = &self.svg_hits;
let rasterized_hits = &self.rasterized_hits;
@@ -191,6 +198,7 @@ impl Cache {
});
self.svg_hits.clear();
self.rasterized_hits.clear();
+ self.should_trim = false;
}
}
diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs
index 9526c5a8..9551311d 100644
--- a/wgpu/src/layer.rs
+++ b/wgpu/src/layer.rs
@@ -1,5 +1,6 @@
-use crate::core::renderer;
-use crate::core::{Background, Color, Point, Rectangle, Transformation};
+use crate::core::{
+ renderer, Background, Color, Point, Radians, Rectangle, Transformation,
+};
use crate::graphics;
use crate::graphics::color;
use crate::graphics::layer;
@@ -117,11 +118,15 @@ impl Layer {
filter_method: crate::core::image::FilterMethod,
bounds: Rectangle,
transformation: Transformation,
+ rotation: Radians,
+ opacity: f32,
) {
let image = Image::Raster {
handle,
filter_method,
bounds: bounds * transformation,
+ rotation,
+ opacity,
};
self.images.push(image);
@@ -133,11 +138,15 @@ impl Layer {
color: Option<Color>,
bounds: Rectangle,
transformation: Transformation,
+ rotation: Radians,
+ opacity: f32,
) {
let svg = Image::Vector {
handle,
color,
bounds: bounds * transformation,
+ rotation,
+ opacity,
};
self.images.push(svg);
diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs
index 178522de..1fbdbe9a 100644
--- a/wgpu/src/lib.rs
+++ b/wgpu/src/lib.rs
@@ -62,6 +62,7 @@ pub use geometry::Geometry;
use crate::core::{
Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation,
+ Vector,
};
use crate::graphics::text::{Editor, Paragraph};
use crate::graphics::Viewport;
@@ -81,11 +82,12 @@ pub struct Renderer {
// TODO: Centralize all the image feature handling
#[cfg(any(feature = "svg", feature = "image"))]
- image_cache: image::cache::Shared,
+ image_cache: std::cell::RefCell<image::Cache>,
}
impl Renderer {
pub fn new(
+ _device: &wgpu::Device,
_engine: &Engine,
default_font: Font,
default_text_size: Pixels,
@@ -99,7 +101,9 @@ impl Renderer {
text_storage: text::Storage::new(),
#[cfg(any(feature = "svg", feature = "image"))]
- image_cache: _engine.image_cache().clone(),
+ image_cache: std::cell::RefCell::new(
+ _engine.create_image_cache(_device),
+ ),
}
}
@@ -121,6 +125,9 @@ impl Renderer {
self.triangle_storage.trim();
self.text_storage.trim();
+
+ #[cfg(any(feature = "svg", feature = "image"))]
+ self.image_cache.borrow_mut().trim();
}
fn prepare(
@@ -190,6 +197,7 @@ impl Renderer {
device,
encoder,
&mut engine.staging_belt,
+ &mut self.image_cache.borrow_mut(),
&layer.images,
viewport.projection(),
scale_factor,
@@ -245,6 +253,8 @@ impl Renderer {
#[cfg(any(feature = "svg", feature = "image"))]
let mut image_layer = 0;
+ #[cfg(any(feature = "svg", feature = "image"))]
+ let image_cache = self.image_cache.borrow();
let scale_factor = viewport.scale_factor() as f32;
let physical_bounds = Rectangle::<f32>::from(Rectangle::with_size(
@@ -358,6 +368,7 @@ impl Renderer {
#[cfg(any(feature = "svg", feature = "image"))]
if !layer.images.is_empty() {
engine.image_pipeline.render(
+ &image_cache,
image_layer,
scissor_rect,
&mut render_pass,
@@ -378,7 +389,6 @@ impl Renderer {
use crate::core::alignment;
use crate::core::text::Renderer as _;
use crate::core::Renderer as _;
- use crate::core::Vector;
self.with_layer(
Rectangle::with_size(viewport.logical_size()),
@@ -509,7 +519,7 @@ impl core::image::Renderer for Renderer {
type Handle = core::image::Handle;
fn measure_image(&self, handle: &Self::Handle) -> Size<u32> {
- self.image_cache.lock().measure_image(handle)
+ self.image_cache.borrow_mut().measure_image(handle)
}
fn draw_image(
@@ -517,16 +527,25 @@ impl core::image::Renderer for Renderer {
handle: Self::Handle,
filter_method: core::image::FilterMethod,
bounds: Rectangle,
+ rotation: core::Radians,
+ opacity: f32,
) {
let (layer, transformation) = self.layers.current_mut();
- layer.draw_image(handle, filter_method, bounds, transformation);
+ layer.draw_image(
+ handle,
+ filter_method,
+ bounds,
+ transformation,
+ rotation,
+ opacity,
+ );
}
}
#[cfg(feature = "svg")]
impl core::svg::Renderer for Renderer {
fn measure_svg(&self, handle: &core::svg::Handle) -> Size<u32> {
- self.image_cache.lock().measure_svg(handle)
+ self.image_cache.borrow_mut().measure_svg(handle)
}
fn draw_svg(
@@ -534,9 +553,18 @@ impl core::svg::Renderer for Renderer {
handle: core::svg::Handle,
color_filter: Option<Color>,
bounds: Rectangle,
+ rotation: core::Radians,
+ opacity: f32,
) {
let (layer, transformation) = self.layers.current_mut();
- layer.draw_svg(handle, color_filter, bounds, transformation);
+ layer.draw_svg(
+ handle,
+ color_filter,
+ bounds,
+ transformation,
+ rotation,
+ opacity,
+ );
}
}
diff --git a/wgpu/src/primitive.rs b/wgpu/src/primitive.rs
index 1313e752..8641f27a 100644
--- a/wgpu/src/primitive.rs
+++ b/wgpu/src/primitive.rs
@@ -67,7 +67,7 @@ pub struct Storage {
impl Storage {
/// Returns `true` if `Storage` contains a type `T`.
pub fn has<T: 'static>(&self) -> bool {
- self.pipelines.get(&TypeId::of::<T>()).is_some()
+ self.pipelines.contains_key(&TypeId::of::<T>())
}
/// Inserts the data `T` in to [`Storage`].
diff --git a/wgpu/src/settings.rs b/wgpu/src/settings.rs
index a6aea0a5..b3c3cf6a 100644
--- a/wgpu/src/settings.rs
+++ b/wgpu/src/settings.rs
@@ -51,3 +51,29 @@ impl From<graphics::Settings> for Settings {
}
}
}
+
+/// Obtains a [`wgpu::PresentMode`] from the current environment
+/// configuration, if set.
+///
+/// The value returned by this function can be changed by setting
+/// the `ICED_PRESENT_MODE` env variable. The possible values are:
+///
+/// - `vsync` → [`wgpu::PresentMode::AutoVsync`]
+/// - `no_vsync` → [`wgpu::PresentMode::AutoNoVsync`]
+/// - `immediate` → [`wgpu::PresentMode::Immediate`]
+/// - `fifo` → [`wgpu::PresentMode::Fifo`]
+/// - `fifo_relaxed` → [`wgpu::PresentMode::FifoRelaxed`]
+/// - `mailbox` → [`wgpu::PresentMode::Mailbox`]
+pub fn present_mode_from_env() -> Option<wgpu::PresentMode> {
+ let present_mode = std::env::var("ICED_PRESENT_MODE").ok()?;
+
+ match present_mode.to_lowercase().as_str() {
+ "vsync" => Some(wgpu::PresentMode::AutoVsync),
+ "no_vsync" => Some(wgpu::PresentMode::AutoNoVsync),
+ "immediate" => Some(wgpu::PresentMode::Immediate),
+ "fifo" => Some(wgpu::PresentMode::Fifo),
+ "fifo_relaxed" => Some(wgpu::PresentMode::FifoRelaxed),
+ "mailbox" => Some(wgpu::PresentMode::Mailbox),
+ _ => None,
+ }
+}
diff --git a/wgpu/src/shader/image.wgsl b/wgpu/src/shader/image.wgsl
index 7b2e5238..accefc17 100644
--- a/wgpu/src/shader/image.wgsl
+++ b/wgpu/src/shader/image.wgsl
@@ -9,40 +9,55 @@ struct Globals {
struct VertexInput {
@builtin(vertex_index) vertex_index: u32,
@location(0) pos: vec2<f32>,
- @location(1) scale: vec2<f32>,
- @location(2) atlas_pos: vec2<f32>,
- @location(3) atlas_scale: vec2<f32>,
- @location(4) layer: i32,
+ @location(1) center: vec2<f32>,
+ @location(2) scale: vec2<f32>,
+ @location(3) rotation: f32,
+ @location(4) opacity: f32,
+ @location(5) atlas_pos: vec2<f32>,
+ @location(6) atlas_scale: vec2<f32>,
+ @location(7) layer: i32,
}
struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) uv: vec2<f32>,
@location(1) layer: f32, // this should be an i32, but naga currently reads that as requiring interpolation.
+ @location(2) opacity: f32,
}
@vertex
fn vs_main(input: VertexInput) -> VertexOutput {
var out: VertexOutput;
- let v_pos = vertex_position(input.vertex_index);
+ // Generate a vertex position in the range [0, 1] from the vertex index.
+ var v_pos = vertex_position(input.vertex_index);
+ // Map the vertex position to the atlas texture.
out.uv = vec2<f32>(v_pos * input.atlas_scale + input.atlas_pos);
out.layer = f32(input.layer);
+ out.opacity = input.opacity;
- var transform: mat4x4<f32> = mat4x4<f32>(
- vec4<f32>(input.scale.x, 0.0, 0.0, 0.0),
- vec4<f32>(0.0, input.scale.y, 0.0, 0.0),
+ // Calculate the vertex position and move the center to the origin
+ 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);
+ let sin_rot = sin(input.rotation);
+ let rotate = mat4x4<f32>(
+ vec4<f32>(cos_rot, sin_rot, 0.0, 0.0),
+ vec4<f32>(-sin_rot, cos_rot, 0.0, 0.0),
vec4<f32>(0.0, 0.0, 1.0, 0.0),
- vec4<f32>(input.pos, 0.0, 1.0)
+ vec4<f32>(0.0, 0.0, 0.0, 1.0)
);
- out.position = globals.transform * transform * vec4<f32>(v_pos, 0.0, 1.0);
+ // 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));
return out;
}
@fragment
fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
- return textureSample(u_texture, u_sampler, input.uv, i32(input.layer));
+ // Sample the texture at the given UV coordinate and layer.
+ return textureSample(u_texture, u_sampler, input.uv, i32(input.layer)) * vec4<f32>(1.0, 1.0, 1.0, input.opacity);
}
diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs
index 0d01faca..7e683c77 100644
--- a/wgpu/src/text.rs
+++ b/wgpu/src/text.rs
@@ -1,12 +1,13 @@
use crate::core::alignment;
use crate::core::{Rectangle, Size, Transformation};
+use crate::graphics::cache;
use crate::graphics::color;
-use crate::graphics::text::cache::{self, Cache as BufferCache};
+use crate::graphics::text::cache::{self as text_cache, Cache as BufferCache};
use crate::graphics::text::{font_system, to_color, Editor, Paragraph};
-use rustc_hash::{FxHashMap, FxHashSet};
+use rustc_hash::FxHashMap;
use std::collections::hash_map;
-use std::rc::Rc;
+use std::rc::{self, Rc};
use std::sync::atomic::{self, AtomicU64};
use std::sync::Arc;
@@ -35,6 +36,7 @@ pub enum Item {
#[derive(Debug, Clone)]
pub struct Cache {
id: Id,
+ group: cache::Group,
text: Rc<[Text]>,
version: usize,
}
@@ -43,7 +45,7 @@ pub struct Cache {
pub struct Id(u64);
impl Cache {
- pub fn new(text: Vec<Text>) -> Option<Self> {
+ pub fn new(group: cache::Group, text: Vec<Text>) -> Option<Self> {
static NEXT_ID: AtomicU64 = AtomicU64::new(0);
if text.is_empty() {
@@ -52,12 +54,17 @@ impl Cache {
Some(Self {
id: Id(NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed)),
+ group,
text: Rc::from(text),
version: 0,
})
}
pub fn update(&mut self, text: Vec<Text>) {
+ if self.text.is_empty() && text.is_empty() {
+ return;
+ }
+
self.text = Rc::from(text);
self.version += 1;
}
@@ -65,16 +72,25 @@ impl Cache {
struct Upload {
renderer: glyphon::TextRenderer,
- atlas: glyphon::TextAtlas,
buffer_cache: BufferCache,
transformation: Transformation,
version: usize,
+ group_version: usize,
+ text: rc::Weak<[Text]>,
+ _atlas: rc::Weak<()>,
}
#[derive(Default)]
pub struct Storage {
+ groups: FxHashMap<cache::Group, Group>,
uploads: FxHashMap<Id, Upload>,
- recently_used: FxHashSet<Id>,
+}
+
+struct Group {
+ atlas: glyphon::TextAtlas,
+ version: usize,
+ should_trim: bool,
+ handle: Rc<()>, // Keeps track of active uploads
}
impl Storage {
@@ -82,12 +98,15 @@ impl Storage {
Self::default()
}
- fn get(&self, cache: &Cache) -> Option<&Upload> {
+ fn get(&self, cache: &Cache) -> Option<(&glyphon::TextAtlas, &Upload)> {
if cache.text.is_empty() {
return None;
}
- self.uploads.get(&cache.id)
+ self.groups
+ .get(&cache.group)
+ .map(|group| &group.atlas)
+ .zip(self.uploads.get(&cache.id))
}
fn prepare(
@@ -101,41 +120,63 @@ impl Storage {
bounds: Rectangle,
target_size: Size<u32>,
) {
+ let group_count = self.groups.len();
+
+ let group = self.groups.entry(cache.group).or_insert_with(|| {
+ log::debug!(
+ "New text atlas: {:?} (total: {})",
+ cache.group,
+ group_count + 1
+ );
+
+ Group {
+ atlas: glyphon::TextAtlas::with_color_mode(
+ device, queue, format, COLOR_MODE,
+ ),
+ version: 0,
+ should_trim: false,
+ handle: Rc::new(()),
+ }
+ });
+
match self.uploads.entry(cache.id) {
hash_map::Entry::Occupied(entry) => {
let upload = entry.into_mut();
- if !cache.text.is_empty()
- && (upload.version != cache.version
- || upload.transformation != new_transformation)
+ if upload.version != cache.version
+ || upload.group_version != group.version
+ || upload.transformation != new_transformation
{
- let _ = prepare(
- device,
- queue,
- encoder,
- &mut upload.renderer,
- &mut upload.atlas,
- &mut upload.buffer_cache,
- &cache.text,
- bounds,
- new_transformation,
- target_size,
- );
+ if !cache.text.is_empty() {
+ let _ = prepare(
+ device,
+ queue,
+ encoder,
+ &mut upload.renderer,
+ &mut group.atlas,
+ &mut upload.buffer_cache,
+ &cache.text,
+ bounds,
+ new_transformation,
+ target_size,
+ );
+ }
+
+ // Only trim if glyphs have changed
+ group.should_trim =
+ group.should_trim || upload.version != cache.version;
+ upload.text = Rc::downgrade(&cache.text);
upload.version = cache.version;
+ upload.group_version = group.version;
upload.transformation = new_transformation;
upload.buffer_cache.trim();
- upload.atlas.trim();
}
}
hash_map::Entry::Vacant(entry) => {
- let mut atlas = glyphon::TextAtlas::with_color_mode(
- device, queue, format, COLOR_MODE,
- );
-
let mut renderer = glyphon::TextRenderer::new(
- &mut atlas,
+ &mut group.atlas,
device,
wgpu::MultisampleState::default(),
None,
@@ -143,41 +184,76 @@ impl Storage {
let mut buffer_cache = BufferCache::new();
- let _ = prepare(
- device,
- queue,
- encoder,
- &mut renderer,
- &mut atlas,
- &mut buffer_cache,
- &cache.text,
- bounds,
- new_transformation,
- target_size,
- );
+ if !cache.text.is_empty() {
+ let _ = prepare(
+ device,
+ queue,
+ encoder,
+ &mut renderer,
+ &mut group.atlas,
+ &mut buffer_cache,
+ &cache.text,
+ bounds,
+ new_transformation,
+ target_size,
+ );
+ }
let _ = entry.insert(Upload {
renderer,
- atlas,
buffer_cache,
transformation: new_transformation,
version: 0,
+ group_version: group.version,
+ text: Rc::downgrade(&cache.text),
+ _atlas: Rc::downgrade(&group.handle),
});
- log::info!(
+ group.should_trim = cache.group.is_singleton();
+
+ log::debug!(
"New text upload: {} (total: {})",
cache.id.0,
self.uploads.len()
);
}
}
-
- let _ = self.recently_used.insert(cache.id);
}
pub fn trim(&mut self) {
- self.uploads.retain(|id, _| self.recently_used.contains(id));
- self.recently_used.clear();
+ self.uploads
+ .retain(|_id, upload| upload.text.strong_count() > 0);
+
+ self.groups.retain(|id, group| {
+ let active_uploads = Rc::weak_count(&group.handle);
+
+ if active_uploads == 0 {
+ log::debug!("Dropping text atlas: {id:?}");
+
+ return false;
+ }
+
+ if group.should_trim {
+ log::trace!("Trimming text atlas: {id:?}");
+
+ group.atlas.trim();
+ group.should_trim = false;
+
+ // We only need to worry about glyph fighting
+ // when the atlas may be shared by multiple
+ // uploads.
+ if !id.is_singleton() {
+ log::debug!(
+ "Invalidating text atlas: {id:?} \
+ (uploads: {active_uploads})"
+ );
+
+ group.version += 1;
+ }
+ }
+
+ true
+ });
}
}
@@ -306,10 +382,10 @@ impl Pipeline {
layer_count += 1;
}
Item::Cached { cache, .. } => {
- if let Some(upload) = storage.get(cache) {
+ if let Some((atlas, upload)) = storage.get(cache) {
upload
.renderer
- .render(&upload.atlas, render_pass)
+ .render(atlas, render_pass)
.expect("Render cached text");
}
}
@@ -345,7 +421,7 @@ fn prepare(
enum Allocation {
Paragraph(Paragraph),
Editor(Editor),
- Cache(cache::KeyHash),
+ Cache(text_cache::KeyHash),
Raw(Arc<glyphon::Buffer>),
}
@@ -369,7 +445,7 @@ fn prepare(
} => {
let (key, _) = buffer_cache.allocate(
font_system,
- cache::Key {
+ text_cache::Key {
content,
size: f32::from(*size),
line_height: f32::from(*line_height),
diff --git a/wgpu/src/triangle.rs b/wgpu/src/triangle.rs
index 8470ea39..b0551f55 100644
--- a/wgpu/src/triangle.rs
+++ b/wgpu/src/triangle.rs
@@ -6,9 +6,9 @@ use crate::graphics::mesh::{self, Mesh};
use crate::graphics::Antialiasing;
use crate::Buffer;
-use rustc_hash::{FxHashMap, FxHashSet};
+use rustc_hash::FxHashMap;
use std::collections::hash_map;
-use std::rc::Rc;
+use std::rc::{self, Rc};
use std::sync::atomic::{self, AtomicU64};
const INITIAL_INDEX_COUNT: usize = 1_000;
@@ -64,12 +64,12 @@ struct Upload {
layer: Layer,
transformation: Transformation,
version: usize,
+ batch: rc::Weak<[Mesh]>,
}
#[derive(Debug, Default)]
pub struct Storage {
uploads: FxHashMap<Id, Upload>,
- recently_used: FxHashSet<Id>,
}
impl Storage {
@@ -113,6 +113,7 @@ impl Storage {
new_transformation,
);
+ upload.batch = Rc::downgrade(&cache.batch);
upload.version = cache.version;
upload.transformation = new_transformation;
}
@@ -134,22 +135,21 @@ impl Storage {
layer,
transformation: new_transformation,
version: 0,
+ batch: Rc::downgrade(&cache.batch),
});
- log::info!(
+ log::debug!(
"New mesh upload: {} (total: {})",
cache.id.0,
self.uploads.len()
);
}
}
-
- let _ = self.recently_used.insert(cache.id);
}
pub fn trim(&mut self) {
- self.uploads.retain(|id, _| self.recently_used.contains(id));
- self.recently_used.clear();
+ self.uploads
+ .retain(|_id, upload| upload.batch.strong_count() > 0);
}
}
diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs
index 095afd48..2e938c77 100644
--- a/wgpu/src/window/compositor.rs
+++ b/wgpu/src/window/compositor.rs
@@ -4,7 +4,8 @@ use crate::graphics::color;
use crate::graphics::compositor;
use crate::graphics::error;
use crate::graphics::{self, Viewport};
-use crate::{Engine, Renderer, Settings};
+use crate::settings::{self, Settings};
+use crate::{Engine, Renderer};
/// A window graphics backend for iced powered by `wgpu`.
#[allow(missing_debug_implementations)]
@@ -270,15 +271,19 @@ impl graphics::Compositor for Compositor {
backend: Option<&str>,
) -> Result<Self, graphics::Error> {
match backend {
- None | Some("wgpu") => Ok(new(
- Settings {
- backends: wgpu::util::backend_bits_from_env()
- .unwrap_or(wgpu::Backends::all()),
- ..settings.into()
- },
- compatible_window,
- )
- .await?),
+ None | Some("wgpu") => {
+ let mut settings = Settings::from(settings);
+
+ if let Some(backends) = wgpu::util::backend_bits_from_env() {
+ settings.backends = backends;
+ }
+
+ if let Some(present_mode) = settings::present_mode_from_env() {
+ settings.present_mode = present_mode;
+ }
+
+ Ok(new(settings, compatible_window).await?)
+ }
Some(backend) => Err(graphics::Error::GraphicsAdapterNotFound {
backend: "wgpu",
reason: error::Reason::DidNotMatch {
@@ -290,6 +295,7 @@ impl graphics::Compositor for Compositor {
fn create_renderer(&self) -> Self::Renderer {
Renderer::new(
+ &self.device,
&self.engine,
self.settings.default_font,
self.settings.default_text_size,
diff --git a/widget/src/canvas.rs b/widget/src/canvas.rs
index 42f92de0..be09f163 100644
--- a/widget/src/canvas.rs
+++ b/widget/src/canvas.rs
@@ -6,6 +6,7 @@ mod program;
pub use event::Event;
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,
diff --git a/widget/src/container.rs b/widget/src/container.rs
index 21405722..8b6638d4 100644
--- a/widget/src/container.rs
+++ b/widget/src/container.rs
@@ -92,6 +92,49 @@ 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.
+ ///
+ /// This can be useful to quickly position content when chained with
+ /// alignment functions—like [`center`].
+ ///
+ /// 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;
@@ -116,18 +159,33 @@ where
self
}
- /// Centers the contents in the horizontal axis of the [`Container`].
+ /// Sets the [`Container`] to fill the available space in the horizontal axis
+ /// and centers its contents there.
pub fn center_x(mut self) -> Self {
+ self.width = Length::Fill;
self.horizontal_alignment = alignment::Horizontal::Center;
self
}
- /// Centers the contents in the vertical axis of the [`Container`].
+ /// Sets the [`Container`] to fill the available space in the vertical axis
+ /// and centers its contents there.
pub fn center_y(mut self) -> Self {
+ self.height = Length::Fill;
self.vertical_alignment = alignment::Vertical::Center;
self
}
+ /// Centers the contents in both the horizontal and vertical axes of the
+ /// [`Container`].
+ ///
+ /// This is equivalent to chaining [`center_x`] and [`center_y`].
+ ///
+ /// [`center_x`]: Self::center_x
+ /// [`center_y`]: Self::center_y
+ pub fn center(self) -> Self {
+ self.center_x().center_y()
+ }
+
/// Sets whether the contents of the [`Container`] should be clipped on
/// overflow.
pub fn clip(mut self, clip: bool) -> Self {
diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs
index 61789c19..fd8614f5 100644
--- a/widget/src/helpers.rs
+++ b/widget/src/helpers.rs
@@ -5,7 +5,7 @@ use crate::combo_box::{self, ComboBox};
use crate::container::{self, Container};
use crate::core;
use crate::core::widget::operation;
-use crate::core::{Element, Length, Pixels};
+use crate::core::{Element, Length, Pixels, Widget};
use crate::keyed;
use crate::overlay;
use crate::pick_list::{self, PickList};
@@ -21,7 +21,7 @@ use crate::text_input::{self, TextInput};
use crate::toggler::{self, Toggler};
use crate::tooltip::{self, Tooltip};
use crate::vertical_slider::{self, VerticalSlider};
-use crate::{Column, MouseArea, Row, Space, Themer};
+use crate::{Column, MouseArea, Row, Space, Stack, Themer};
use std::borrow::Borrow;
use std::ops::RangeInclusive;
@@ -52,6 +52,19 @@ macro_rules! row {
);
}
+/// Creates a [`Stack`] with the given children.
+///
+/// [`Stack`]: crate::Stack
+#[macro_export]
+macro_rules! stack {
+ () => (
+ $crate::Stack::new()
+ );
+ ($($x:expr),+ $(,)?) => (
+ $crate::Stack::with_children([$($crate::core::Element::from($x)),+])
+ );
+}
+
/// Creates a new [`Container`] with the provided content.
///
/// [`Container`]: crate::Container
@@ -65,6 +78,27 @@ where
Container::new(content)
}
+/// Creates a new [`Container`] that fills all the available space
+/// and centers its contents inside.
+///
+/// This is equivalent to:
+/// ```rust,no_run
+/// # use iced_widget::Container;
+/// # fn container<A>(x: A) -> Container<'static, ()> { unreachable!() }
+/// let centered = container("Centered!").center();
+/// ```
+///
+/// [`Container`]: crate::Container
+pub fn center<'a, Message, Theme, Renderer>(
+ content: impl Into<Element<'a, Message, Theme, Renderer>>,
+) -> Container<'a, Message, Theme, Renderer>
+where
+ Theme: container::Catalog + 'a,
+ Renderer: core::Renderer,
+{
+ container(content).fill().center()
+}
+
/// Creates a new [`Column`] with the given children.
pub fn column<'a, Message, Theme, Renderer>(
children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
@@ -98,6 +132,428 @@ where
Row::with_children(children)
}
+/// Creates a new [`Stack`] with the given children.
+///
+/// [`Stack`]: crate::Stack
+pub fn stack<'a, Message, Theme, Renderer>(
+ children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
+) -> Stack<'a, Message, Theme, Renderer>
+where
+ Renderer: core::Renderer,
+{
+ Stack::with_children(children)
+}
+
+/// Wraps the given widget and captures any mouse button presses inside the bounds of
+/// the widget—effectively making it _opaque_.
+///
+/// This helper is meant to be used to mark elements in a [`Stack`] to avoid mouse
+/// events from passing through layers.
+///
+/// [`Stack`]: crate::Stack
+pub fn opaque<'a, Message, Theme, Renderer>(
+ content: impl Into<Element<'a, Message, Theme, Renderer>>,
+) -> Element<'a, Message, Theme, Renderer>
+where
+ Message: 'a,
+ Theme: 'a,
+ Renderer: core::Renderer + 'a,
+{
+ use crate::core::event::{self, Event};
+ use crate::core::layout::{self, Layout};
+ use crate::core::mouse;
+ use crate::core::renderer;
+ use crate::core::widget::tree::{self, Tree};
+ use crate::core::{Rectangle, Shell, Size};
+
+ struct Opaque<'a, Message, Theme, Renderer> {
+ content: Element<'a, Message, Theme, Renderer>,
+ }
+
+ impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
+ for Opaque<'a, Message, Theme, Renderer>
+ where
+ Renderer: core::Renderer,
+ {
+ fn tag(&self) -> tree::Tag {
+ self.content.as_widget().tag()
+ }
+
+ fn state(&self) -> tree::State {
+ self.content.as_widget().state()
+ }
+
+ fn children(&self) -> Vec<Tree> {
+ self.content.as_widget().children()
+ }
+
+ fn diff(&self, tree: &mut Tree) {
+ self.content.as_widget().diff(tree);
+ }
+
+ fn size(&self) -> Size<Length> {
+ self.content.as_widget().size()
+ }
+
+ fn size_hint(&self) -> Size<Length> {
+ self.content.as_widget().size_hint()
+ }
+
+ fn layout(
+ &self,
+ tree: &mut Tree,
+ renderer: &Renderer,
+ limits: &layout::Limits,
+ ) -> layout::Node {
+ self.content.as_widget().layout(tree, renderer, limits)
+ }
+
+ fn draw(
+ &self,
+ tree: &Tree,
+ renderer: &mut Renderer,
+ theme: &Theme,
+ style: &renderer::Style,
+ layout: Layout<'_>,
+ cursor: mouse::Cursor,
+ viewport: &Rectangle,
+ ) {
+ self.content
+ .as_widget()
+ .draw(tree, renderer, theme, style, layout, cursor, viewport);
+ }
+
+ fn operate(
+ &self,
+ state: &mut Tree,
+ layout: Layout<'_>,
+ renderer: &Renderer,
+ operation: &mut dyn operation::Operation<Message>,
+ ) {
+ self.content
+ .as_widget()
+ .operate(state, layout, renderer, operation);
+ }
+
+ fn on_event(
+ &mut self,
+ state: &mut Tree,
+ event: Event,
+ layout: Layout<'_>,
+ cursor: mouse::Cursor,
+ renderer: &Renderer,
+ clipboard: &mut dyn core::Clipboard,
+ shell: &mut Shell<'_, Message>,
+ viewport: &Rectangle,
+ ) -> event::Status {
+ let is_mouse_press = matches!(
+ event,
+ core::Event::Mouse(mouse::Event::ButtonPressed(_))
+ );
+
+ if let core::event::Status::Captured =
+ self.content.as_widget_mut().on_event(
+ state, event, layout, cursor, renderer, clipboard, shell,
+ viewport,
+ )
+ {
+ return event::Status::Captured;
+ }
+
+ if is_mouse_press && cursor.is_over(layout.bounds()) {
+ event::Status::Captured
+ } else {
+ event::Status::Ignored
+ }
+ }
+
+ fn mouse_interaction(
+ &self,
+ state: &core::widget::Tree,
+ layout: core::Layout<'_>,
+ cursor: core::mouse::Cursor,
+ viewport: &core::Rectangle,
+ renderer: &Renderer,
+ ) -> core::mouse::Interaction {
+ let interaction = self
+ .content
+ .as_widget()
+ .mouse_interaction(state, layout, cursor, viewport, renderer);
+
+ if interaction == mouse::Interaction::None
+ && cursor.is_over(layout.bounds())
+ {
+ mouse::Interaction::Idle
+ } else {
+ interaction
+ }
+ }
+
+ fn overlay<'b>(
+ &'b mut self,
+ state: &'b mut core::widget::Tree,
+ layout: core::Layout<'_>,
+ renderer: &Renderer,
+ translation: core::Vector,
+ ) -> Option<core::overlay::Element<'b, Message, Theme, Renderer>>
+ {
+ self.content.as_widget_mut().overlay(
+ state,
+ layout,
+ renderer,
+ translation,
+ )
+ }
+ }
+
+ Element::new(Opaque {
+ content: content.into(),
+ })
+}
+
+/// Displays a widget on top of another one, only when the base widget is hovered.
+///
+/// This works analogously to a [`stack`], but it will only display the layer on top
+/// when the cursor is over the base. It can be useful for removing visual clutter.
+///
+/// [`stack`]: stack()
+pub fn hover<'a, Message, Theme, Renderer>(
+ base: impl Into<Element<'a, Message, Theme, Renderer>>,
+ top: impl Into<Element<'a, Message, Theme, Renderer>>,
+) -> Element<'a, Message, Theme, Renderer>
+where
+ Message: 'a,
+ Theme: 'a,
+ Renderer: core::Renderer + 'a,
+{
+ use crate::core::event::{self, Event};
+ use crate::core::layout::{self, Layout};
+ use crate::core::mouse;
+ use crate::core::renderer;
+ use crate::core::widget::tree::{self, Tree};
+ use crate::core::{Rectangle, Shell, Size};
+
+ struct Hover<'a, Message, Theme, Renderer> {
+ base: Element<'a, Message, Theme, Renderer>,
+ top: Element<'a, Message, Theme, Renderer>,
+ is_top_overlay_active: bool,
+ }
+
+ impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
+ for Hover<'a, Message, Theme, Renderer>
+ where
+ Renderer: core::Renderer,
+ {
+ fn tag(&self) -> tree::Tag {
+ struct Tag;
+ tree::Tag::of::<Tag>()
+ }
+
+ fn children(&self) -> Vec<Tree> {
+ vec![Tree::new(&self.base), Tree::new(&self.top)]
+ }
+
+ fn diff(&self, tree: &mut Tree) {
+ tree.diff_children(&[&self.base, &self.top]);
+ }
+
+ fn size(&self) -> Size<Length> {
+ self.base.as_widget().size()
+ }
+
+ fn size_hint(&self) -> Size<Length> {
+ self.base.as_widget().size_hint()
+ }
+
+ fn layout(
+ &self,
+ tree: &mut Tree,
+ renderer: &Renderer,
+ limits: &layout::Limits,
+ ) -> layout::Node {
+ let base = self.base.as_widget().layout(
+ &mut tree.children[0],
+ renderer,
+ limits,
+ );
+
+ let top = self.top.as_widget().layout(
+ &mut tree.children[1],
+ renderer,
+ &layout::Limits::new(Size::ZERO, base.size()),
+ );
+
+ layout::Node::with_children(base.size(), vec![base, top])
+ }
+
+ fn draw(
+ &self,
+ tree: &Tree,
+ renderer: &mut Renderer,
+ theme: &Theme,
+ style: &renderer::Style,
+ layout: Layout<'_>,
+ cursor: mouse::Cursor,
+ viewport: &Rectangle,
+ ) {
+ if let Some(bounds) = layout.bounds().intersection(viewport) {
+ let mut children = layout.children().zip(&tree.children);
+
+ let (base_layout, base_tree) = children.next().unwrap();
+
+ self.base.as_widget().draw(
+ base_tree,
+ renderer,
+ theme,
+ style,
+ base_layout,
+ cursor,
+ viewport,
+ );
+
+ if cursor.is_over(layout.bounds()) || self.is_top_overlay_active
+ {
+ let (top_layout, top_tree) = children.next().unwrap();
+
+ renderer.with_layer(bounds, |renderer| {
+ self.top.as_widget().draw(
+ top_tree, renderer, theme, style, top_layout,
+ cursor, viewport,
+ );
+ });
+ }
+ }
+ }
+
+ fn operate(
+ &self,
+ tree: &mut Tree,
+ layout: Layout<'_>,
+ renderer: &Renderer,
+ operation: &mut dyn operation::Operation<Message>,
+ ) {
+ let children = [&self.base, &self.top]
+ .into_iter()
+ .zip(layout.children().zip(&mut tree.children));
+
+ for (child, (layout, tree)) in children {
+ child.as_widget().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 core::Clipboard,
+ shell: &mut Shell<'_, Message>,
+ viewport: &Rectangle,
+ ) -> event::Status {
+ let mut children = layout.children().zip(&mut tree.children);
+ let (base_layout, base_tree) = children.next().unwrap();
+
+ let top_status = if matches!(
+ event,
+ Event::Mouse(
+ mouse::Event::CursorMoved { .. }
+ | mouse::Event::ButtonReleased(_)
+ )
+ ) || cursor.is_over(layout.bounds())
+ {
+ let (top_layout, top_tree) = children.next().unwrap();
+
+ self.top.as_widget_mut().on_event(
+ top_tree,
+ event.clone(),
+ top_layout,
+ cursor,
+ renderer,
+ clipboard,
+ shell,
+ viewport,
+ )
+ } else {
+ event::Status::Ignored
+ };
+
+ if top_status == event::Status::Captured {
+ return top_status;
+ }
+
+ self.base.as_widget_mut().on_event(
+ base_tree,
+ event.clone(),
+ base_layout,
+ cursor,
+ renderer,
+ clipboard,
+ shell,
+ viewport,
+ )
+ }
+
+ fn mouse_interaction(
+ &self,
+ tree: &Tree,
+ layout: Layout<'_>,
+ cursor: mouse::Cursor,
+ viewport: &Rectangle,
+ renderer: &Renderer,
+ ) -> mouse::Interaction {
+ [&self.base, &self.top]
+ .into_iter()
+ .rev()
+ .zip(layout.children().rev().zip(tree.children.iter().rev()))
+ .map(|(child, (layout, tree))| {
+ child.as_widget().mouse_interaction(
+ tree, layout, cursor, viewport, renderer,
+ )
+ })
+ .find(|&interaction| interaction != mouse::Interaction::None)
+ .unwrap_or_default()
+ }
+
+ fn overlay<'b>(
+ &'b mut self,
+ tree: &'b mut core::widget::Tree,
+ layout: core::Layout<'_>,
+ renderer: &Renderer,
+ translation: core::Vector,
+ ) -> Option<core::overlay::Element<'b, Message, Theme, Renderer>>
+ {
+ let mut overlays = [&mut self.base, &mut self.top]
+ .into_iter()
+ .zip(layout.children().zip(tree.children.iter_mut()))
+ .map(|(child, (layout, tree))| {
+ child.as_widget_mut().overlay(
+ tree,
+ layout,
+ renderer,
+ translation,
+ )
+ });
+
+ if let Some(base_overlay) = overlays.next()? {
+ return Some(base_overlay);
+ }
+
+ let top_overlay = overlays.next()?;
+ self.is_top_overlay_active = top_overlay.is_some();
+
+ top_overlay
+ }
+ }
+
+ Element::new(Hover {
+ base: base.into(),
+ top: top.into(),
+ is_top_overlay_active: false,
+ })
+}
+
/// Creates a new [`Scrollable`] with the provided content.
///
/// [`Scrollable`]: crate::Scrollable
diff --git a/widget/src/image.rs b/widget/src/image.rs
index f673c7b3..80e17263 100644
--- a/widget/src/image.rs
+++ b/widget/src/image.rs
@@ -8,11 +8,10 @@ use crate::core::mouse;
use crate::core::renderer;
use crate::core::widget::Tree;
use crate::core::{
- ContentFit, Element, Layout, Length, Rectangle, Size, Vector, Widget,
+ ContentFit, Element, Layout, Length, Point, Rectangle, Rotation, Size,
+ Vector, Widget,
};
-use std::hash::Hash;
-
pub use image::{FilterMethod, Handle};
/// Creates a new [`Viewer`] with the given image `Handle`.
@@ -38,6 +37,8 @@ pub struct Image<Handle> {
height: Length,
content_fit: ContentFit,
filter_method: FilterMethod,
+ rotation: Rotation,
+ opacity: f32,
}
impl<Handle> Image<Handle> {
@@ -47,8 +48,10 @@ impl<Handle> Image<Handle> {
handle: handle.into(),
width: Length::Shrink,
height: Length::Shrink,
- content_fit: ContentFit::Contain,
+ content_fit: ContentFit::default(),
filter_method: FilterMethod::default(),
+ rotation: Rotation::default(),
+ opacity: 1.0,
}
}
@@ -77,6 +80,21 @@ impl<Handle> Image<Handle> {
self.filter_method = filter_method;
self
}
+
+ /// Applies the given [`Rotation`] to the [`Image`].
+ pub fn rotation(mut self, rotation: impl Into<Rotation>) -> Self {
+ self.rotation = rotation.into();
+ self
+ }
+
+ /// Sets the opacity of the [`Image`].
+ ///
+ /// It should be in the [0.0, 1.0] range—`0.0` meaning completely transparent,
+ /// and `1.0` meaning completely opaque.
+ pub fn opacity(mut self, opacity: impl Into<f32>) -> Self {
+ self.opacity = opacity.into();
+ self
+ }
}
/// Computes the layout of an [`Image`].
@@ -87,22 +105,24 @@ pub fn layout<Renderer, Handle>(
width: Length,
height: Length,
content_fit: ContentFit,
+ rotation: Rotation,
) -> layout::Node
where
Renderer: image::Renderer<Handle = Handle>,
{
// The raw w/h of the underlying image
- let image_size = {
- let Size { width, height } = renderer.measure_image(handle);
+ let image_size = renderer.measure_image(handle);
+ let image_size =
+ Size::new(image_size.width as f32, image_size.height as f32);
- Size::new(width as f32, height as f32)
- };
+ // The rotated size of the image
+ let rotated_size = rotation.apply(image_size);
// The size to be available to the widget prior to `Shrink`ing
- let raw_size = limits.resolve(width, height, image_size);
+ let raw_size = limits.resolve(width, height, rotated_size);
// The uncropped size of the image when fit to the bounds above
- let full_size = content_fit.fit(image_size, raw_size);
+ let full_size = content_fit.fit(rotated_size, raw_size);
// Shrink the widget to fit the resized image, if requested
let final_size = Size {
@@ -126,32 +146,46 @@ pub fn draw<Renderer, Handle>(
handle: &Handle,
content_fit: ContentFit,
filter_method: FilterMethod,
+ rotation: Rotation,
+ opacity: f32,
) where
Renderer: image::Renderer<Handle = Handle>,
- Handle: Clone + Hash,
+ Handle: Clone,
{
let Size { width, height } = renderer.measure_image(handle);
let image_size = Size::new(width as f32, height as f32);
+ let rotated_size = rotation.apply(image_size);
let bounds = layout.bounds();
- let adjusted_fit = content_fit.fit(image_size, bounds.size());
+ let adjusted_fit = content_fit.fit(rotated_size, bounds.size());
- let render = |renderer: &mut Renderer| {
- let offset = Vector::new(
- (bounds.width - adjusted_fit.width).max(0.0) / 2.0,
- (bounds.height - adjusted_fit.height).max(0.0) / 2.0,
- );
+ let scale = Vector::new(
+ adjusted_fit.width / rotated_size.width,
+ adjusted_fit.height / rotated_size.height,
+ );
- let drawing_bounds = Rectangle {
- width: adjusted_fit.width,
- height: adjusted_fit.height,
- ..bounds
- };
+ let final_size = image_size * scale;
+ let position = match content_fit {
+ ContentFit::None => Point::new(
+ bounds.x + (rotated_size.width - adjusted_fit.width) / 2.0,
+ bounds.y + (rotated_size.height - adjusted_fit.height) / 2.0,
+ ),
+ _ => Point::new(
+ bounds.center_x() - final_size.width / 2.0,
+ bounds.center_y() - final_size.height / 2.0,
+ ),
+ };
+
+ let drawing_bounds = Rectangle::new(position, final_size);
+
+ let render = |renderer: &mut Renderer| {
renderer.draw_image(
handle.clone(),
filter_method,
- drawing_bounds + offset,
+ drawing_bounds,
+ rotation.radians(),
+ opacity,
);
};
@@ -167,7 +201,7 @@ impl<Message, Theme, Renderer, Handle> Widget<Message, Theme, Renderer>
for Image<Handle>
where
Renderer: image::Renderer<Handle = Handle>,
- Handle: Clone + Hash,
+ Handle: Clone,
{
fn size(&self) -> Size<Length> {
Size {
@@ -189,6 +223,7 @@ where
self.width,
self.height,
self.content_fit,
+ self.rotation,
)
}
@@ -208,6 +243,8 @@ where
&self.handle,
self.content_fit,
self.filter_method,
+ self.rotation,
+ self.opacity,
);
}
}
@@ -216,7 +253,7 @@ impl<'a, Message, Theme, Renderer, Handle> From<Image<Handle>>
for Element<'a, Message, Theme, Renderer>
where
Renderer: image::Renderer<Handle = Handle>,
- Handle: Clone + Hash + 'a,
+ Handle: Clone + 'a,
{
fn from(image: Image<Handle>) -> Element<'a, Message, Theme, Renderer> {
Element::new(image)
diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs
index e57857c1..03a9d895 100644
--- a/widget/src/image/viewer.rs
+++ b/widget/src/image/viewer.rs
@@ -6,12 +6,10 @@ 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, Rectangle,
+ Clipboard, ContentFit, Element, Layout, Length, Pixels, Point, Radians, Rectangle,
Shell, Size, Vector, Widget,
};
-use std::hash::Hash;
-
/// A frame that displays an image with the ability to zoom in/out and pan.
#[allow(missing_debug_implementations)]
pub struct Viewer<Handle> {
@@ -102,7 +100,7 @@ impl<Message, Theme, Renderer, Handle> Widget<Message, Theme, Renderer>
for Viewer<Handle>
where
Renderer: image::Renderer<Handle = Handle>,
- Handle: Clone + Hash,
+ Handle: Clone,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<State>()
@@ -222,7 +220,7 @@ where
event::Status::Captured
}
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
- let Some(cursor_position) = cursor.position() else {
+ let Some(cursor_position) = cursor.position_over(bounds) else {
return event::Status::Ignored;
};
@@ -308,7 +306,7 @@ where
} else if is_mouse_over {
mouse::Interaction::Grab
} else {
- mouse::Interaction::Idle
+ mouse::Interaction::None
}
}
@@ -354,6 +352,8 @@ where
self.handle.clone(),
self.filter_method,
drawing_bounds,
+ Radians(0.0),
+ 1.0,
);
});
};
@@ -414,7 +414,7 @@ impl<'a, Message, Theme, Renderer, Handle> From<Viewer<Handle>>
where
Renderer: 'a + image::Renderer<Handle = Handle>,
Message: 'a,
- Handle: Clone + Hash + 'a,
+ Handle: Clone + 'a,
{
fn from(viewer: Viewer<Handle>) -> Element<'a, Message, Theme, Renderer> {
Element::new(viewer)
diff --git a/widget/src/keyed/column.rs b/widget/src/keyed/column.rs
index a34ce9e6..fdaadefa 100644
--- a/widget/src/keyed/column.rs
+++ b/widget/src/keyed/column.rs
@@ -224,7 +224,7 @@ where
);
if state.keys != self.keys {
- state.keys = self.keys.clone();
+ state.keys.clone_from(&self.keys);
}
}
diff --git a/widget/src/lib.rs b/widget/src/lib.rs
index 1eeacbae..00e9aaa4 100644
--- a/widget/src/lib.rs
+++ b/widget/src/lib.rs
@@ -12,6 +12,7 @@ mod column;
mod mouse_area;
mod row;
mod space;
+mod stack;
mod themer;
pub mod button;
@@ -78,6 +79,8 @@ pub use slider::Slider;
#[doc(no_inline)]
pub use space::Space;
#[doc(no_inline)]
+pub use stack::Stack;
+#[doc(no_inline)]
pub use text::Text;
#[doc(no_inline)]
pub use text_editor::TextEditor;
diff --git a/widget/src/mouse_area.rs b/widget/src/mouse_area.rs
index 9634e477..d7235cf6 100644
--- a/widget/src/mouse_area.rs
+++ b/widget/src/mouse_area.rs
@@ -232,7 +232,7 @@ where
);
match (self.interaction, content_interaction) {
- (Some(interaction), mouse::Interaction::Idle)
+ (Some(interaction), mouse::Interaction::None)
if cursor.is_over(layout.bounds()) =>
{
interaction
diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs
index 668c5372..6fc00f87 100644
--- a/widget/src/scrollable.rs
+++ b/widget/src/scrollable.rs
@@ -350,6 +350,148 @@ where
let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) =
scrollbars.is_mouse_over(cursor);
+ if let Some(scroller_grabbed_at) = state.y_scroller_grabbed_at {
+ match event {
+ Event::Mouse(mouse::Event::CursorMoved { .. })
+ | Event::Touch(touch::Event::FingerMoved { .. }) => {
+ if let Some(scrollbar) = scrollbars.y {
+ let Some(cursor_position) = cursor.position() else {
+ return event::Status::Ignored;
+ };
+
+ state.scroll_y_to(
+ scrollbar.scroll_percentage_y(
+ scroller_grabbed_at,
+ cursor_position,
+ ),
+ bounds,
+ content_bounds,
+ );
+
+ let _ = notify_on_scroll(
+ state,
+ &self.on_scroll,
+ bounds,
+ content_bounds,
+ shell,
+ );
+
+ return event::Status::Captured;
+ }
+ }
+ _ => {}
+ }
+ } else if mouse_over_y_scrollbar {
+ match event {
+ Event::Mouse(mouse::Event::ButtonPressed(
+ mouse::Button::Left,
+ ))
+ | Event::Touch(touch::Event::FingerPressed { .. }) => {
+ let Some(cursor_position) = cursor.position() else {
+ return event::Status::Ignored;
+ };
+
+ if let (Some(scroller_grabbed_at), Some(scrollbar)) = (
+ scrollbars.grab_y_scroller(cursor_position),
+ scrollbars.y,
+ ) {
+ state.scroll_y_to(
+ scrollbar.scroll_percentage_y(
+ scroller_grabbed_at,
+ cursor_position,
+ ),
+ bounds,
+ content_bounds,
+ );
+
+ state.y_scroller_grabbed_at = Some(scroller_grabbed_at);
+
+ let _ = notify_on_scroll(
+ state,
+ &self.on_scroll,
+ bounds,
+ content_bounds,
+ shell,
+ );
+ }
+
+ return event::Status::Captured;
+ }
+ _ => {}
+ }
+ }
+
+ if let Some(scroller_grabbed_at) = state.x_scroller_grabbed_at {
+ match event {
+ Event::Mouse(mouse::Event::CursorMoved { .. })
+ | Event::Touch(touch::Event::FingerMoved { .. }) => {
+ let Some(cursor_position) = cursor.position() else {
+ return event::Status::Ignored;
+ };
+
+ if let Some(scrollbar) = scrollbars.x {
+ state.scroll_x_to(
+ scrollbar.scroll_percentage_x(
+ scroller_grabbed_at,
+ cursor_position,
+ ),
+ bounds,
+ content_bounds,
+ );
+
+ let _ = notify_on_scroll(
+ state,
+ &self.on_scroll,
+ bounds,
+ content_bounds,
+ shell,
+ );
+ }
+
+ return event::Status::Captured;
+ }
+ _ => {}
+ }
+ } else if mouse_over_x_scrollbar {
+ match event {
+ Event::Mouse(mouse::Event::ButtonPressed(
+ mouse::Button::Left,
+ ))
+ | Event::Touch(touch::Event::FingerPressed { .. }) => {
+ let Some(cursor_position) = cursor.position() else {
+ return event::Status::Ignored;
+ };
+
+ if let (Some(scroller_grabbed_at), Some(scrollbar)) = (
+ scrollbars.grab_x_scroller(cursor_position),
+ scrollbars.x,
+ ) {
+ state.scroll_x_to(
+ scrollbar.scroll_percentage_x(
+ scroller_grabbed_at,
+ cursor_position,
+ ),
+ bounds,
+ content_bounds,
+ );
+
+ state.x_scroller_grabbed_at = Some(scroller_grabbed_at);
+
+ let _ = notify_on_scroll(
+ state,
+ &self.on_scroll,
+ bounds,
+ content_bounds,
+ shell,
+ );
+
+ return event::Status::Captured;
+ }
+ }
+ _ => {}
+ }
+ }
+
let mut event_status = {
let cursor = match cursor_over_scrollable {
Some(cursor_position)
@@ -422,7 +564,9 @@ where
let delta = match delta {
mouse::ScrollDelta::Lines { x, y } => {
// TODO: Configurable speed/friction (?)
- let movement = if state.keyboard_modifiers.shift() {
+ 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 {
Vector::new(x, y)
@@ -435,15 +579,17 @@ where
state.scroll(delta, self.direction, bounds, content_bounds);
- notify_on_scroll(
+ event_status = if notify_on_scroll(
state,
&self.on_scroll,
bounds,
content_bounds,
shell,
- );
-
- event_status = event::Status::Captured;
+ ) {
+ event::Status::Captured
+ } else {
+ event::Status::Ignored
+ };
}
Event::Touch(event)
if state.scroll_area_touched_at.is_some()
@@ -481,7 +627,8 @@ where
state.scroll_area_touched_at =
Some(cursor_position);
- notify_on_scroll(
+ // TODO: bubble up touch movements if not consumed.
+ let _ = notify_on_scroll(
state,
&self.on_scroll,
bounds,
@@ -498,148 +645,6 @@ where
_ => {}
}
- if let Some(scroller_grabbed_at) = state.y_scroller_grabbed_at {
- match event {
- Event::Mouse(mouse::Event::CursorMoved { .. })
- | Event::Touch(touch::Event::FingerMoved { .. }) => {
- if let Some(scrollbar) = scrollbars.y {
- let Some(cursor_position) = cursor.position() else {
- return event::Status::Ignored;
- };
-
- state.scroll_y_to(
- scrollbar.scroll_percentage_y(
- scroller_grabbed_at,
- cursor_position,
- ),
- bounds,
- content_bounds,
- );
-
- notify_on_scroll(
- state,
- &self.on_scroll,
- bounds,
- content_bounds,
- shell,
- );
-
- event_status = event::Status::Captured;
- }
- }
- _ => {}
- }
- } else if mouse_over_y_scrollbar {
- match event {
- Event::Mouse(mouse::Event::ButtonPressed(
- mouse::Button::Left,
- ))
- | Event::Touch(touch::Event::FingerPressed { .. }) => {
- let Some(cursor_position) = cursor.position() else {
- return event::Status::Ignored;
- };
-
- if let (Some(scroller_grabbed_at), Some(scrollbar)) = (
- scrollbars.grab_y_scroller(cursor_position),
- scrollbars.y,
- ) {
- state.scroll_y_to(
- scrollbar.scroll_percentage_y(
- scroller_grabbed_at,
- cursor_position,
- ),
- bounds,
- content_bounds,
- );
-
- state.y_scroller_grabbed_at = Some(scroller_grabbed_at);
-
- notify_on_scroll(
- state,
- &self.on_scroll,
- bounds,
- content_bounds,
- shell,
- );
- }
-
- event_status = event::Status::Captured;
- }
- _ => {}
- }
- }
-
- if let Some(scroller_grabbed_at) = state.x_scroller_grabbed_at {
- match event {
- Event::Mouse(mouse::Event::CursorMoved { .. })
- | Event::Touch(touch::Event::FingerMoved { .. }) => {
- let Some(cursor_position) = cursor.position() else {
- return event::Status::Ignored;
- };
-
- if let Some(scrollbar) = scrollbars.x {
- state.scroll_x_to(
- scrollbar.scroll_percentage_x(
- scroller_grabbed_at,
- cursor_position,
- ),
- bounds,
- content_bounds,
- );
-
- notify_on_scroll(
- state,
- &self.on_scroll,
- bounds,
- content_bounds,
- shell,
- );
- }
-
- event_status = event::Status::Captured;
- }
- _ => {}
- }
- } else if mouse_over_x_scrollbar {
- match event {
- Event::Mouse(mouse::Event::ButtonPressed(
- mouse::Button::Left,
- ))
- | Event::Touch(touch::Event::FingerPressed { .. }) => {
- let Some(cursor_position) = cursor.position() else {
- return event::Status::Ignored;
- };
-
- if let (Some(scroller_grabbed_at), Some(scrollbar)) = (
- scrollbars.grab_x_scroller(cursor_position),
- scrollbars.x,
- ) {
- state.scroll_x_to(
- scrollbar.scroll_percentage_x(
- scroller_grabbed_at,
- cursor_position,
- ),
- bounds,
- content_bounds,
- );
-
- state.x_scroller_grabbed_at = Some(scroller_grabbed_at);
-
- notify_on_scroll(
- state,
- &self.on_scroll,
- bounds,
- content_bounds,
- shell,
- );
-
- event_status = event::Status::Captured;
- }
- }
- _ => {}
- }
- }
-
event_status
}
@@ -659,6 +664,10 @@ where
let content_layout = layout.children().next().unwrap();
let content_bounds = content_layout.bounds();
+ let Some(visible_bounds) = bounds.intersection(viewport) else {
+ return;
+ };
+
let scrollbars =
Scrollbars::new(state, self.direction, bounds, content_bounds);
@@ -704,7 +713,7 @@ where
// Draw inner content
if scrollbars.active() {
- renderer.with_layer(bounds, |renderer| {
+ renderer.with_layer(visible_bounds, |renderer| {
renderer.with_translation(
Vector::new(-translation.x, -translation.y),
|renderer| {
@@ -767,9 +776,9 @@ where
renderer.with_layer(
Rectangle {
- width: (bounds.width + 2.0).min(viewport.width),
- height: (bounds.height + 2.0).min(viewport.height),
- ..bounds
+ width: (visible_bounds.width + 2.0).min(viewport.width),
+ height: (visible_bounds.height + 2.0).min(viewport.height),
+ ..visible_bounds
},
|renderer| {
if let Some(scrollbar) = scrollbars.y {
@@ -850,7 +859,7 @@ where
if (mouse_over_x_scrollbar || mouse_over_y_scrollbar)
|| state.scrollers_grabbed()
{
- mouse::Interaction::Idle
+ mouse::Interaction::None
} else {
let translation =
state.translation(self.direction, bounds, content_bounds);
@@ -961,51 +970,54 @@ pub fn scroll_to<Message: 'static>(
Command::widget(operation::scrollable::scroll_to(id.0, offset))
}
+/// Returns [`true`] if the viewport actually changed.
fn notify_on_scroll<Message>(
state: &mut State,
on_scroll: &Option<Box<dyn Fn(Viewport) -> Message + '_>>,
bounds: Rectangle,
content_bounds: Rectangle,
shell: &mut Shell<'_, Message>,
-) {
- if let Some(on_scroll) = on_scroll {
- if content_bounds.width <= bounds.width
- && content_bounds.height <= bounds.height
- {
- return;
- }
+) -> bool {
+ if content_bounds.width <= bounds.width
+ && content_bounds.height <= bounds.height
+ {
+ return false;
+ }
- let viewport = Viewport {
- offset_x: state.offset_x,
- offset_y: state.offset_y,
- bounds,
- content_bounds,
- };
+ let viewport = Viewport {
+ offset_x: state.offset_x,
+ offset_y: state.offset_y,
+ bounds,
+ content_bounds,
+ };
- // Don't publish redundant viewports to shell
- if let Some(last_notified) = state.last_notified {
- let last_relative_offset = last_notified.relative_offset();
- let current_relative_offset = viewport.relative_offset();
+ // Don't publish redundant viewports to shell
+ if let Some(last_notified) = state.last_notified {
+ let last_relative_offset = last_notified.relative_offset();
+ let current_relative_offset = viewport.relative_offset();
- let last_absolute_offset = last_notified.absolute_offset();
- let current_absolute_offset = viewport.absolute_offset();
+ let last_absolute_offset = last_notified.absolute_offset();
+ let current_absolute_offset = viewport.absolute_offset();
- let unchanged = |a: f32, b: f32| {
- (a - b).abs() <= f32::EPSILON || (a.is_nan() && b.is_nan())
- };
+ let unchanged = |a: f32, b: f32| {
+ (a - b).abs() <= f32::EPSILON || (a.is_nan() && b.is_nan())
+ };
- if 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)
- {
- return;
- }
+ if 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)
+ {
+ return false;
}
+ }
+ if let Some(on_scroll) = on_scroll {
shell.publish(on_scroll(viewport));
- state.last_notified = Some(viewport);
}
+ state.last_notified = Some(viewport);
+
+ true
}
#[derive(Debug, Clone, Copy)]
diff --git a/widget/src/stack.rs b/widget/src/stack.rs
new file mode 100644
index 00000000..5035541b
--- /dev/null
+++ b/widget/src/stack.rs
@@ -0,0 +1,333 @@
+//! Display content on top of other content.
+use crate::core::event::{self, Event};
+use crate::core::layout;
+use crate::core::mouse;
+use crate::core::overlay;
+use crate::core::renderer;
+use crate::core::widget::{Operation, Tree};
+use crate::core::{
+ Clipboard, Element, Layout, Length, Rectangle, Shell, Size, Vector, Widget,
+};
+
+/// A container that displays children on top of each other.
+///
+/// The first [`Element`] dictates the intrinsic [`Size`] of a [`Stack`] and
+/// will be displayed as the base layer. Every consecutive [`Element`] will be
+/// renderer on top; on its own layer.
+///
+/// Keep in mind that too much layering will normally produce bad UX as well as
+/// introduce certain rendering overhead. Use this widget sparingly!
+#[allow(missing_debug_implementations)]
+pub struct Stack<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
+{
+ width: Length,
+ height: Length,
+ children: Vec<Element<'a, Message, Theme, Renderer>>,
+}
+
+impl<'a, Message, Theme, Renderer> Stack<'a, Message, Theme, Renderer>
+where
+ Renderer: crate::core::Renderer,
+{
+ /// Creates an empty [`Stack`].
+ pub fn new() -> Self {
+ Self::from_vec(Vec::new())
+ }
+
+ /// Creates a [`Stack`] with the given capacity.
+ pub fn with_capacity(capacity: usize) -> Self {
+ Self::from_vec(Vec::with_capacity(capacity))
+ }
+
+ /// Creates a [`Stack`] with the given elements.
+ pub fn with_children(
+ children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
+ ) -> Self {
+ let iterator = children.into_iter();
+
+ Self::with_capacity(iterator.size_hint().0).extend(iterator)
+ }
+
+ /// Creates a [`Stack`] from an already allocated [`Vec`].
+ ///
+ /// Keep in mind that the [`Stack`] will not inspect the [`Vec`], which means
+ /// it won't automatically adapt to the sizing strategy of its contents.
+ ///
+ /// If any of the children have a [`Length::Fill`] strategy, you will need to
+ /// call [`Stack::width`] or [`Stack::height`] accordingly.
+ pub fn from_vec(
+ children: Vec<Element<'a, Message, Theme, Renderer>>,
+ ) -> Self {
+ Self {
+ width: Length::Shrink,
+ height: Length::Shrink,
+ children,
+ }
+ }
+
+ /// Sets the width of the [`Stack`].
+ pub fn width(mut self, width: impl Into<Length>) -> Self {
+ self.width = width.into();
+ self
+ }
+
+ /// Sets the height of the [`Stack`].
+ pub fn height(mut self, height: impl Into<Length>) -> Self {
+ self.height = height.into();
+ self
+ }
+
+ /// Adds an element to the [`Stack`].
+ pub fn push(
+ mut self,
+ child: impl Into<Element<'a, Message, Theme, Renderer>>,
+ ) -> Self {
+ let child = child.into();
+
+ if self.children.is_empty() {
+ let child_size = child.as_widget().size_hint();
+
+ self.width = self.width.enclose(child_size.width);
+ self.height = self.height.enclose(child_size.height);
+ }
+
+ self.children.push(child);
+ self
+ }
+
+ /// Adds an element to the [`Stack`], if `Some`.
+ pub fn push_maybe(
+ self,
+ child: Option<impl Into<Element<'a, Message, Theme, Renderer>>>,
+ ) -> Self {
+ if let Some(child) = child {
+ self.push(child)
+ } else {
+ self
+ }
+ }
+
+ /// Extends the [`Stack`] with the given children.
+ pub fn extend(
+ self,
+ children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
+ ) -> Self {
+ children.into_iter().fold(self, Self::push)
+ }
+}
+
+impl<'a, Message, Renderer> Default for Stack<'a, Message, Renderer>
+where
+ Renderer: crate::core::Renderer,
+{
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
+ for Stack<'a, Message, Theme, Renderer>
+where
+ Renderer: crate::core::Renderer,
+{
+ fn children(&self) -> Vec<Tree> {
+ self.children.iter().map(Tree::new).collect()
+ }
+
+ fn diff(&self, tree: &mut Tree) {
+ tree.diff_children(&self.children);
+ }
+
+ 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 {
+ let limits = limits.width(self.width).height(self.height);
+
+ if self.children.is_empty() {
+ return layout::Node::new(limits.resolve(
+ self.width,
+ self.height,
+ Size::ZERO,
+ ));
+ }
+
+ let base = self.children[0].as_widget().layout(
+ &mut tree.children[0],
+ renderer,
+ &limits,
+ );
+
+ let size = limits.resolve(self.width, self.height, base.size());
+ let limits = layout::Limits::new(Size::ZERO, size);
+
+ let nodes = std::iter::once(base)
+ .chain(self.children[1..].iter().zip(&mut tree.children[1..]).map(
+ |(layer, tree)| {
+ let node =
+ layer.as_widget().layout(tree, renderer, &limits);
+
+ node
+ },
+ ))
+ .collect();
+
+ layout::Node::with_children(size, nodes)
+ }
+
+ fn operate(
+ &self,
+ tree: &mut Tree,
+ layout: Layout<'_>,
+ renderer: &Renderer,
+ operation: &mut dyn Operation<Message>,
+ ) {
+ operation.container(None, layout.bounds(), &mut |operation| {
+ self.children
+ .iter()
+ .zip(&mut tree.children)
+ .zip(layout.children())
+ .for_each(|((child, state), layout)| {
+ child
+ .as_widget()
+ .operate(state, 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.children
+ .iter_mut()
+ .rev()
+ .zip(tree.children.iter_mut().rev())
+ .zip(layout.children().rev())
+ .map(|((child, state), layout)| {
+ child.as_widget_mut().on_event(
+ state,
+ event.clone(),
+ layout,
+ cursor,
+ renderer,
+ clipboard,
+ shell,
+ viewport,
+ )
+ })
+ .find(|&status| status == event::Status::Captured)
+ .unwrap_or(event::Status::Ignored)
+ }
+
+ fn mouse_interaction(
+ &self,
+ tree: &Tree,
+ layout: Layout<'_>,
+ cursor: mouse::Cursor,
+ viewport: &Rectangle,
+ renderer: &Renderer,
+ ) -> mouse::Interaction {
+ self.children
+ .iter()
+ .rev()
+ .zip(tree.children.iter().rev())
+ .zip(layout.children().rev())
+ .map(|((child, state), layout)| {
+ child.as_widget().mouse_interaction(
+ state, layout, cursor, viewport, renderer,
+ )
+ })
+ .find(|&interaction| interaction != mouse::Interaction::None)
+ .unwrap_or_default()
+ }
+
+ fn draw(
+ &self,
+ tree: &Tree,
+ renderer: &mut Renderer,
+ theme: &Theme,
+ style: &renderer::Style,
+ layout: Layout<'_>,
+ cursor: mouse::Cursor,
+ viewport: &Rectangle,
+ ) {
+ if let Some(clipped_viewport) = layout.bounds().intersection(viewport) {
+ for (i, ((layer, state), layout)) in self
+ .children
+ .iter()
+ .zip(&tree.children)
+ .zip(layout.children())
+ .enumerate()
+ {
+ 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,
+ theme,
+ style,
+ layout,
+ cursor,
+ &clipped_viewport,
+ );
+ }
+ }
+ }
+ }
+
+ fn overlay<'b>(
+ &'b mut self,
+ tree: &'b mut Tree,
+ layout: Layout<'_>,
+ renderer: &Renderer,
+ translation: Vector,
+ ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
+ overlay::from_children(
+ &mut self.children,
+ tree,
+ layout,
+ renderer,
+ translation,
+ )
+ }
+}
+
+impl<'a, Message, Theme, Renderer> From<Stack<'a, Message, Theme, Renderer>>
+ for Element<'a, Message, Theme, Renderer>
+where
+ Message: 'a,
+ Theme: 'a,
+ Renderer: crate::core::Renderer + 'a,
+{
+ fn from(stack: Stack<'a, Message, Theme, Renderer>) -> Self {
+ Self::new(stack)
+ }
+}
diff --git a/widget/src/svg.rs b/widget/src/svg.rs
index eb142189..4551bcad 100644
--- a/widget/src/svg.rs
+++ b/widget/src/svg.rs
@@ -5,8 +5,8 @@ use crate::core::renderer;
use crate::core::svg;
use crate::core::widget::Tree;
use crate::core::{
- Color, ContentFit, Element, Layout, Length, Rectangle, Size, Theme, Vector,
- Widget,
+ Color, ContentFit, Element, Layout, Length, Point, Rectangle, Rotation,
+ Size, Theme, Vector, Widget,
};
use std::path::PathBuf;
@@ -29,6 +29,8 @@ where
height: Length,
content_fit: ContentFit,
class: Theme::Class<'a>,
+ rotation: Rotation,
+ opacity: f32,
}
impl<'a, Theme> Svg<'a, Theme>
@@ -43,6 +45,8 @@ where
height: Length::Shrink,
content_fit: ContentFit::Contain,
class: Theme::default(),
+ rotation: Rotation::default(),
+ opacity: 1.0,
}
}
@@ -95,6 +99,21 @@ where
self.class = class.into();
self
}
+
+ /// Applies the given [`Rotation`] to the [`Svg`].
+ pub fn rotation(mut self, rotation: impl Into<Rotation>) -> Self {
+ self.rotation = rotation.into();
+ self
+ }
+
+ /// Sets the opacity of the [`Svg`].
+ ///
+ /// It should be in the [0.0, 1.0] range—`0.0` meaning completely transparent,
+ /// and `1.0` meaning completely opaque.
+ pub fn opacity(mut self, opacity: impl Into<f32>) -> Self {
+ self.opacity = opacity.into();
+ self
+ }
}
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
@@ -120,11 +139,14 @@ where
let Size { width, height } = renderer.measure_svg(&self.handle);
let image_size = Size::new(width as f32, height as f32);
+ // The rotated size of the svg
+ let rotated_size = self.rotation.apply(image_size);
+
// The size to be available to the widget prior to `Shrink`ing
- let raw_size = limits.resolve(self.width, self.height, image_size);
+ let raw_size = limits.resolve(self.width, self.height, rotated_size);
// The uncropped size of the image when fit to the bounds above
- let full_size = self.content_fit.fit(image_size, raw_size);
+ let full_size = self.content_fit.fit(rotated_size, raw_size);
// Shrink the widget to fit the resized image, if requested
let final_size = Size {
@@ -153,35 +175,47 @@ where
) {
let Size { width, height } = renderer.measure_svg(&self.handle);
let image_size = Size::new(width as f32, height as f32);
+ let rotated_size = self.rotation.apply(image_size);
let bounds = layout.bounds();
- let adjusted_fit = self.content_fit.fit(image_size, bounds.size());
- let is_mouse_over = cursor.is_over(bounds);
+ let adjusted_fit = self.content_fit.fit(rotated_size, bounds.size());
+ let scale = Vector::new(
+ adjusted_fit.width / rotated_size.width,
+ adjusted_fit.height / rotated_size.height,
+ );
+
+ let final_size = image_size * scale;
+
+ let position = match self.content_fit {
+ ContentFit::None => Point::new(
+ bounds.x + (rotated_size.width - adjusted_fit.width) / 2.0,
+ bounds.y + (rotated_size.height - adjusted_fit.height) / 2.0,
+ ),
+ _ => Point::new(
+ bounds.center_x() - final_size.width / 2.0,
+ bounds.center_y() - final_size.height / 2.0,
+ ),
+ };
- let render = |renderer: &mut Renderer| {
- let offset = Vector::new(
- (bounds.width - adjusted_fit.width).max(0.0) / 2.0,
- (bounds.height - adjusted_fit.height).max(0.0) / 2.0,
- );
+ let drawing_bounds = Rectangle::new(position, final_size);
- let drawing_bounds = Rectangle {
- width: adjusted_fit.width,
- height: adjusted_fit.height,
- ..bounds
- };
+ let is_mouse_over = cursor.is_over(bounds);
- let status = if is_mouse_over {
- Status::Hovered
- } else {
- Status::Idle
- };
+ let status = if is_mouse_over {
+ Status::Hovered
+ } else {
+ Status::Idle
+ };
- let style = theme.style(&self.class, status);
+ let style = theme.style(&self.class, status);
+ let render = |renderer: &mut Renderer| {
renderer.draw_svg(
self.handle.clone(),
style.color,
- drawing_bounds + offset,
+ drawing_bounds,
+ self.rotation.radians(),
+ self.opacity,
);
};
diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs
index 92cdb251..7c0b98ea 100644
--- a/widget/src/text_editor.rs
+++ b/widget/src/text_editor.rs
@@ -319,7 +319,9 @@ where
}
}
-struct State<Highlighter: text::Highlighter> {
+/// The state of a [`TextEditor`].
+#[derive(Debug)]
+pub struct State<Highlighter: text::Highlighter> {
is_focused: bool,
last_click: Option<mouse::Click>,
drag_click: Option<mouse::click::Kind>,
@@ -329,6 +331,13 @@ struct State<Highlighter: text::Highlighter> {
highlighter_format_address: usize,
}
+impl<Highlighter: text::Highlighter> State<Highlighter> {
+ /// Returns whether the [`TextEditor`] is currently focused or not.
+ pub fn is_focused(&self) -> bool {
+ self.is_focused
+ }
+}
+
impl<'a, Highlighter, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for TextEditor<'a, Highlighter, Message, Theme, Renderer>
where
@@ -560,23 +569,27 @@ where
if state.is_focused {
match internal.editor.cursor() {
Cursor::Caret(position) => {
- let position = position + translation;
+ let cursor =
+ Rectangle::new(
+ position + translation,
+ Size::new(
+ 1.0,
+ self.line_height
+ .to_absolute(self.text_size.unwrap_or_else(
+ || renderer.default_size(),
+ ))
+ .into(),
+ ),
+ );
- if bounds.contains(position) {
+ if let Some(clipped_cursor) = bounds.intersection(&cursor) {
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle {
- x: position.x.floor(),
- y: position.y,
- width: 1.0,
- height: self
- .line_height
- .to_absolute(
- self.text_size.unwrap_or_else(
- || renderer.default_size(),
- ),
- )
- .into(),
+ x: clipped_cursor.x.floor(),
+ y: clipped_cursor.y,
+ width: clipped_cursor.width,
+ height: clipped_cursor.height,
},
..renderer::Quad::default()
},
diff --git a/winit/Cargo.toml b/winit/Cargo.toml
index dccb7c07..6d3dddde 100644
--- a/winit/Cargo.toml
+++ b/winit/Cargo.toml
@@ -25,6 +25,7 @@ wayland-csd-adwaita = ["winit/wayland-csd-adwaita"]
multi-window = ["iced_runtime/multi-window"]
[dependencies]
+iced_futures.workspace = true
iced_graphics.workspace = true
iced_runtime.workspace = true
@@ -32,6 +33,7 @@ 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
diff --git a/winit/src/application.rs b/winit/src/application.rs
index 1ca80609..3bc29255 100644
--- a/winit/src/application.rs
+++ b/winit/src/application.rs
@@ -22,7 +22,9 @@ use crate::runtime::{Command, Debug};
use crate::{Clipboard, Error, Proxy, Settings};
use futures::channel::mpsc;
+use futures::channel::oneshot;
+use std::borrow::Cow;
use std::mem::ManuallyDrop;
use std::sync::Arc;
@@ -129,7 +131,7 @@ pub fn default(theme: &Theme) -> Appearance {
/// Runs an [`Application`] with an executor, compositor, and the provided
/// settings.
-pub async fn run<A, E, C>(
+pub fn run<A, E, C>(
settings: Settings<A::Flags>,
graphics_settings: graphics::Settings,
) -> Result<(), Error>
@@ -141,21 +143,22 @@ where
{
use futures::task;
use futures::Future;
- use winit::event_loop::EventLoopBuilder;
+ use winit::event_loop::EventLoop;
let mut debug = Debug::new();
debug.startup_started();
- let event_loop = EventLoopBuilder::with_user_event()
+ let event_loop = EventLoop::with_user_event()
.build()
.expect("Create event loop");
- let proxy = event_loop.create_proxy();
+
+ let (proxy, worker) = Proxy::new(event_loop.create_proxy());
let runtime = {
- let proxy = Proxy::new(event_loop.create_proxy());
let executor = E::new().map_err(Error::ExecutorCreationFailed)?;
+ executor.spawn(worker);
- Runtime::new(executor, proxy)
+ Runtime::new(executor, proxy.clone())
};
let (application, init_command) = {
@@ -164,102 +167,282 @@ where
runtime.enter(|| A::new(flags))
};
- #[cfg(target_arch = "wasm32")]
- let target = settings.window.platform_specific.target.clone();
+ let id = settings.id;
+ let title = application.title();
- let should_be_visible = settings.window.visible;
- let exit_on_close_request = settings.window.exit_on_close_request;
+ let (boot_sender, boot_receiver) = oneshot::channel();
+ let (event_sender, event_receiver) = mpsc::unbounded();
+ let (control_sender, control_receiver) = mpsc::unbounded();
- let builder = conversion::window_settings(
- settings.window,
- &application.title(),
- event_loop.primary_monitor(),
- settings.id,
- )
- .with_visible(false);
+ let instance = Box::pin(run_instance::<A, E, C>(
+ application,
+ runtime,
+ proxy,
+ debug,
+ boot_receiver,
+ event_receiver,
+ control_sender,
+ init_command,
+ settings.fonts,
+ ));
- log::debug!("Window builder: {builder:#?}");
+ let context = task::Context::from_waker(task::noop_waker_ref());
+
+ struct Runner<Message: 'static, F, C> {
+ instance: std::pin::Pin<Box<F>>,
+ context: task::Context<'static>,
+ boot: Option<BootConfig<C>>,
+ sender: mpsc::UnboundedSender<winit::event::Event<Message>>,
+ receiver: mpsc::UnboundedReceiver<winit::event_loop::ControlFlow>,
+ error: Option<Error>,
+ #[cfg(target_arch = "wasm32")]
+ is_booted: std::rc::Rc<std::cell::RefCell<bool>>,
+ #[cfg(target_arch = "wasm32")]
+ queued_events: Vec<winit::event::Event<Message>>,
+ }
- let window = Arc::new(
- builder
- .build(&event_loop)
- .map_err(Error::WindowCreationFailed)?,
- );
+ struct BootConfig<C> {
+ sender: oneshot::Sender<Boot<C>>,
+ id: Option<String>,
+ title: String,
+ window_settings: window::Settings,
+ graphics_settings: graphics::Settings,
+ }
- #[cfg(target_arch = "wasm32")]
+ let runner = Runner {
+ instance,
+ context,
+ boot: Some(BootConfig {
+ sender: boot_sender,
+ id,
+ title,
+ window_settings: settings.window,
+ graphics_settings,
+ }),
+ sender: event_sender,
+ receiver: control_receiver,
+ error: None,
+ #[cfg(target_arch = "wasm32")]
+ is_booted: std::rc::Rc::new(std::cell::RefCell::new(false)),
+ #[cfg(target_arch = "wasm32")]
+ queued_events: Vec::new(),
+ };
+
+ impl<Message, F, C> winit::application::ApplicationHandler<Message>
+ for Runner<Message, F, C>
+ where
+ F: Future<Output = ()>,
+ C: Compositor + 'static,
{
- use winit::platform::web::WindowExtWebSys;
+ fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {
+ let Some(BootConfig {
+ sender,
+ id,
+ title,
+ window_settings,
+ graphics_settings,
+ }) = self.boot.take()
+ else {
+ return;
+ };
- let canvas = window.canvas().expect("Get window canvas");
- let _ = canvas.set_attribute(
- "style",
- "display: block; width: 100%; height: 100%",
- );
+ let should_be_visible = window_settings.visible;
+ let exit_on_close_request = window_settings.exit_on_close_request;
+
+ #[cfg(target_arch = "wasm32")]
+ let target = window_settings.platform_specific.target.clone();
+
+ let window_attributes = conversion::window_attributes(
+ window_settings,
+ &title,
+ event_loop.primary_monitor(),
+ id,
+ )
+ .with_visible(false);
+
+ log::debug!("Window attributes: {window_attributes:#?}");
+
+ let window = match event_loop.create_window(window_attributes) {
+ Ok(window) => Arc::new(window),
+ Err(error) => {
+ self.error = Some(Error::WindowCreationFailed(error));
+ event_loop.exit();
+ return;
+ }
+ };
+
+ let finish_boot = {
+ let window = window.clone();
+
+ async move {
+ let compositor =
+ C::new(graphics_settings, window.clone()).await?;
- let window = web_sys::window().unwrap();
- let document = window.document().unwrap();
- let body = document.body().unwrap();
-
- let target = target.and_then(|target| {
- body.query_selector(&format!("#{target}"))
- .ok()
- .unwrap_or(None)
- });
-
- match target {
- Some(node) => {
- let _ = node
- .replace_with_with_node_1(&canvas)
- .expect(&format!("Could not replace #{}", node.id()));
+ sender
+ .send(Boot {
+ window,
+ compositor,
+ should_be_visible,
+ exit_on_close_request,
+ })
+ .ok()
+ .expect("Send boot event");
+
+ Ok::<_, graphics::Error>(())
+ }
+ };
+
+ #[cfg(not(target_arch = "wasm32"))]
+ if let Err(error) = futures::executor::block_on(finish_boot) {
+ self.error = Some(Error::GraphicsCreationFailed(error));
+ event_loop.exit();
}
- None => {
- let _ = body
- .append_child(&canvas)
- .expect("Append canvas to HTML body");
+
+ #[cfg(target_arch = "wasm32")]
+ {
+ use winit::platform::web::WindowExtWebSys;
+
+ let canvas = window.canvas().expect("Get window canvas");
+ let _ = canvas.set_attribute(
+ "style",
+ "display: block; width: 100%; height: 100%",
+ );
+
+ let window = web_sys::window().unwrap();
+ let document = window.document().unwrap();
+ let body = document.body().unwrap();
+
+ let target = target.and_then(|target| {
+ body.query_selector(&format!("#{target}"))
+ .ok()
+ .unwrap_or(None)
+ });
+
+ match target {
+ Some(node) => {
+ let _ = node.replace_with_with_node_1(&canvas).expect(
+ &format!("Could not replace #{}", node.id()),
+ );
+ }
+ None => {
+ let _ = body
+ .append_child(&canvas)
+ .expect("Append canvas to HTML body");
+ }
+ };
+
+ let is_booted = self.is_booted.clone();
+
+ wasm_bindgen_futures::spawn_local(async move {
+ finish_boot.await.expect("Finish boot!");
+
+ *is_booted.borrow_mut() = true;
+ });
}
- };
- }
+ }
- let mut compositor = C::new(graphics_settings, window.clone()).await?;
- let renderer = compositor.create_renderer();
+ fn new_events(
+ &mut self,
+ event_loop: &winit::event_loop::ActiveEventLoop,
+ cause: winit::event::StartCause,
+ ) {
+ if self.boot.is_some() {
+ return;
+ }
- for font in settings.fonts {
- compositor.load_font(font);
- }
+ self.process_event(
+ event_loop,
+ winit::event::Event::NewEvents(cause),
+ );
+ }
+
+ fn window_event(
+ &mut self,
+ event_loop: &winit::event_loop::ActiveEventLoop,
+ window_id: winit::window::WindowId,
+ event: winit::event::WindowEvent,
+ ) {
+ #[cfg(target_os = "windows")]
+ let is_move_or_resize = matches!(
+ event,
+ winit::event::WindowEvent::Resized(_)
+ | winit::event::WindowEvent::Moved(_)
+ );
+
+ self.process_event(
+ event_loop,
+ winit::event::Event::WindowEvent { window_id, event },
+ );
+
+ // TODO: Remove when unnecessary
+ // On Windows, we emulate an `AboutToWait` event after every `Resized` event
+ // since the event loop does not resume during resize interaction.
+ // More details: https://github.com/rust-windowing/winit/issues/3272
+ #[cfg(target_os = "windows")]
+ {
+ if is_move_or_resize {
+ self.process_event(
+ event_loop,
+ winit::event::Event::AboutToWait,
+ );
+ }
+ }
+ }
- let (mut event_sender, event_receiver) = mpsc::unbounded();
- let (control_sender, mut control_receiver) = mpsc::unbounded();
+ fn user_event(
+ &mut self,
+ event_loop: &winit::event_loop::ActiveEventLoop,
+ message: Message,
+ ) {
+ self.process_event(
+ event_loop,
+ winit::event::Event::UserEvent(message),
+ );
+ }
- let mut instance = Box::pin(run_instance::<A, E, C>(
- application,
- compositor,
- renderer,
- runtime,
- proxy,
- debug,
- event_receiver,
- control_sender,
- init_command,
- window,
- should_be_visible,
- exit_on_close_request,
- ));
+ fn about_to_wait(
+ &mut self,
+ event_loop: &winit::event_loop::ActiveEventLoop,
+ ) {
+ self.process_event(event_loop, winit::event::Event::AboutToWait);
+ }
+ }
- let mut context = task::Context::from_waker(task::noop_waker_ref());
+ impl<Message, F, C> Runner<Message, F, C>
+ where
+ F: Future<Output = ()>,
+ {
+ fn process_event(
+ &mut self,
+ event_loop: &winit::event_loop::ActiveEventLoop,
+ event: winit::event::Event<Message>,
+ ) {
+ // On Wasm, events may start being processed before the compositor
+ // boots up. We simply queue them and process them once ready.
+ #[cfg(target_arch = "wasm32")]
+ if !*self.is_booted.borrow() {
+ self.queued_events.push(event);
+ return;
+ } else if !self.queued_events.is_empty() {
+ let queued_events = std::mem::take(&mut self.queued_events);
+
+ // This won't infinitely recurse, since we `mem::take`
+ for event in queued_events {
+ self.process_event(event_loop, event);
+ }
+ }
- let process_event =
- move |event, event_loop: &winit::event_loop::EventLoopWindowTarget<_>| {
if event_loop.exiting() {
return;
}
- event_sender.start_send(event).expect("Send event");
+ self.sender.start_send(event).expect("Send event");
- let poll = instance.as_mut().poll(&mut context);
+ let poll = self.instance.as_mut().poll(&mut self.context);
match poll {
task::Poll::Pending => {
- if let Ok(Some(flow)) = control_receiver.try_next() {
+ if let Ok(Some(flow)) = self.receiver.try_next() {
event_loop.set_control_flow(flow);
}
}
@@ -267,54 +450,45 @@ where
event_loop.exit();
}
}
- };
+ }
+ }
- #[cfg(not(target_os = "windows"))]
- let _ = event_loop.run(process_event);
+ #[cfg(not(target_arch = "wasm32"))]
+ {
+ let mut runner = runner;
+ let _ = event_loop.run_app(&mut runner);
+
+ runner.error.map(Err).unwrap_or(Ok(()))
+ }
- // TODO: Remove when unnecessary
- // On Windows, we emulate an `AboutToWait` event after every `Resized` event
- // since the event loop does not resume during resize interaction.
- // More details: https://github.com/rust-windowing/winit/issues/3272
- #[cfg(target_os = "windows")]
+ #[cfg(target_arch = "wasm32")]
{
- let mut process_event = process_event;
+ use winit::platform::web::EventLoopExtWebSys;
+ let _ = event_loop.spawn_app(runner);
- let _ = event_loop.run(move |event, event_loop| {
- if matches!(
- event,
- winit::event::Event::WindowEvent {
- event: winit::event::WindowEvent::Resized(_)
- | winit::event::WindowEvent::Moved(_),
- ..
- }
- ) {
- process_event(event, event_loop);
- process_event(winit::event::Event::AboutToWait, event_loop);
- } else {
- process_event(event, event_loop);
- }
- });
+ Ok(())
}
+}
- Ok(())
+struct Boot<C> {
+ window: Arc<winit::window::Window>,
+ compositor: C,
+ should_be_visible: bool,
+ exit_on_close_request: bool,
}
async fn run_instance<A, E, C>(
mut application: A,
- mut compositor: C,
- mut renderer: A::Renderer,
mut runtime: Runtime<E, Proxy<A::Message>, A::Message>,
- mut proxy: winit::event_loop::EventLoopProxy<A::Message>,
+ mut proxy: Proxy<A::Message>,
mut debug: Debug,
+ mut boot: oneshot::Receiver<Boot<C>>,
mut event_receiver: mpsc::UnboundedReceiver<
winit::event::Event<A::Message>,
>,
mut control_sender: mpsc::UnboundedSender<winit::event_loop::ControlFlow>,
init_command: Command<A::Message>,
- window: Arc<winit::window::Window>,
- should_be_visible: bool,
- exit_on_close_request: bool,
+ fonts: Vec<Cow<'static, [u8]>>,
) where
A: Application + 'static,
E: Executor + 'static,
@@ -325,6 +499,19 @@ async fn run_instance<A, E, C>(
use winit::event;
use winit::event_loop::ControlFlow;
+ let Boot {
+ window,
+ mut compositor,
+ should_be_visible,
+ exit_on_close_request,
+ } = boot.try_recv().ok().flatten().expect("Receive boot");
+
+ let mut renderer = compositor.create_renderer();
+
+ for font in fonts {
+ compositor.load_font(font);
+ }
+
let mut state = State::new(&application, &window);
let mut viewport_version = state.viewport_version();
let physical_size = state.physical_size();
@@ -370,6 +557,7 @@ async fn run_instance<A, E, C>(
let mut mouse_interaction = mouse::Interaction::default();
let mut events = Vec::new();
let mut messages = Vec::new();
+ let mut user_events = 0;
let mut redraw_pending = false;
debug.startup_finished();
@@ -396,6 +584,7 @@ async fn run_instance<A, E, C>(
}
event::Event::UserEvent(message) => {
messages.push(message);
+ user_events += 1;
}
event::Event::WindowEvent {
event: event::WindowEvent::RedrawRequested { .. },
@@ -477,7 +666,7 @@ async fn run_instance<A, E, C>(
debug.draw_finished();
if new_mouse_interaction != mouse_interaction {
- window.set_cursor_icon(conversion::mouse_interaction(
+ window.set_cursor(conversion::mouse_interaction(
new_mouse_interaction,
));
@@ -593,6 +782,11 @@ async fn run_instance<A, E, C>(
if should_exit {
break;
}
+
+ if user_events > 0 {
+ proxy.free_slots(user_events);
+ user_events = 0;
+ }
}
if !redraw_pending {
@@ -667,7 +861,7 @@ pub fn update<A: Application, C, E: Executor>(
runtime: &mut Runtime<E, Proxy<A::Message>, A::Message>,
clipboard: &mut Clipboard,
should_exit: &mut bool,
- proxy: &mut winit::event_loop::EventLoopProxy<A::Message>,
+ proxy: &mut Proxy<A::Message>,
debug: &mut Debug,
messages: &mut Vec<A::Message>,
window: &winit::window::Window,
@@ -717,7 +911,7 @@ pub fn run_command<A, C, E>(
runtime: &mut Runtime<E, Proxy<A::Message>, A::Message>,
clipboard: &mut Clipboard,
should_exit: &mut bool,
- proxy: &mut winit::event_loop::EventLoopProxy<A::Message>,
+ proxy: &mut Proxy<A::Message>,
debug: &mut Debug,
window: &winit::window::Window,
) where
@@ -742,9 +936,7 @@ pub fn run_command<A, C, E>(
clipboard::Action::Read(tag, kind) => {
let message = tag(clipboard.read(kind));
- proxy
- .send_event(message)
- .expect("Send message to event loop");
+ proxy.send(message);
}
clipboard::Action::Write(contents, kind) => {
clipboard.write(kind, contents);
@@ -774,25 +966,16 @@ pub fn run_command<A, C, E>(
let size =
window.inner_size().to_logical(window.scale_factor());
- proxy
- .send_event(callback(Size::new(
- size.width,
- size.height,
- )))
- .expect("Send message to event loop");
+ proxy.send(callback(Size::new(size.width, size.height)));
}
window::Action::FetchMaximized(_id, callback) => {
- proxy
- .send_event(callback(window.is_maximized()))
- .expect("Send message to event loop");
+ proxy.send(callback(window.is_maximized()));
}
window::Action::Maximize(_id, maximized) => {
window.set_maximized(maximized);
}
window::Action::FetchMinimized(_id, callback) => {
- proxy
- .send_event(callback(window.is_minimized()))
- .expect("Send message to event loop");
+ proxy.send(callback(window.is_minimized()));
}
window::Action::Minimize(_id, minimized) => {
window.set_minimized(minimized);
@@ -808,9 +991,7 @@ pub fn run_command<A, C, E>(
})
.ok();
- proxy
- .send_event(callback(position))
- .expect("Send message to event loop");
+ proxy.send(callback(position));
}
window::Action::Move(_id, position) => {
window.set_outer_position(winit::dpi::LogicalPosition {
@@ -835,9 +1016,7 @@ pub fn run_command<A, C, E>(
core::window::Mode::Hidden
};
- proxy
- .send_event(tag(mode))
- .expect("Send message to event loop");
+ proxy.send(tag(mode));
}
window::Action::ToggleMaximize(_id) => {
window.set_maximized(!window.is_maximized());
@@ -865,17 +1044,13 @@ pub fn run_command<A, C, E>(
}
}
window::Action::FetchId(_id, tag) => {
- proxy
- .send_event(tag(window.id().into()))
- .expect("Send message to event loop");
+ proxy.send(tag(window.id().into()));
}
window::Action::RunWithHandle(_id, tag) => {
use window::raw_window_handle::HasWindowHandle;
if let Ok(handle) = window.window_handle() {
- proxy
- .send_event(tag(&handle))
- .expect("Send message to event loop");
+ proxy.send(tag(&handle));
}
}
@@ -888,12 +1063,10 @@ pub fn run_command<A, C, E>(
&debug.overlay(),
);
- proxy
- .send_event(tag(window::Screenshot::new(
- bytes,
- state.physical_size(),
- )))
- .expect("Send message to event loop.");
+ proxy.send(tag(window::Screenshot::new(
+ bytes,
+ state.physical_size(),
+ )));
}
},
command::Action::System(action) => match action {
@@ -901,7 +1074,7 @@ pub fn run_command<A, C, E>(
#[cfg(feature = "system")]
{
let graphics_info = compositor.fetch_information();
- let proxy = proxy.clone();
+ let mut proxy = proxy.clone();
let _ = std::thread::spawn(move || {
let information =
@@ -909,9 +1082,7 @@ pub fn run_command<A, C, E>(
let message = _tag(information);
- proxy
- .send_event(message)
- .expect("Send message to event loop");
+ proxy.send(message);
});
}
}
@@ -934,9 +1105,7 @@ pub fn run_command<A, C, E>(
match operation.finish() {
operation::Outcome::None => {}
operation::Outcome::Some(message) => {
- proxy
- .send_event(message)
- .expect("Send message to event loop");
+ proxy.send(message);
}
operation::Outcome::Chain(next) => {
current_operation = Some(next);
@@ -951,9 +1120,7 @@ pub fn run_command<A, C, E>(
// TODO: Error handling (?)
compositor.load_font(bytes);
- proxy
- .send_event(tagger(Ok(())))
- .expect("Send message to event loop");
+ proxy.send(tagger(Ok(())));
}
command::Action::Custom(_) => {
log::warn!("Unsupported custom action in `iced_winit` shell");
diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs
index fc3d1c08..d04fc2f1 100644
--- a/winit/src/conversion.rs
+++ b/winit/src/conversion.rs
@@ -8,16 +8,16 @@ use crate::core::touch;
use crate::core::window;
use crate::core::{Event, Point, Size};
-/// Converts some [`window::Settings`] into a `WindowBuilder` from `winit`.
-pub fn window_settings(
+/// Converts some [`window::Settings`] into some `WindowAttributes` from `winit`.
+pub fn window_attributes(
settings: window::Settings,
title: &str,
primary_monitor: Option<winit::monitor::MonitorHandle>,
_id: Option<String>,
-) -> winit::window::WindowBuilder {
- let mut window_builder = winit::window::WindowBuilder::new();
+) -> winit::window::WindowAttributes {
+ let mut attributes = winit::window::WindowAttributes::default();
- window_builder = window_builder
+ attributes = attributes
.with_title(title)
.with_inner_size(winit::dpi::LogicalSize {
width: settings.size.width,
@@ -39,23 +39,21 @@ pub fn window_settings(
if let Some(position) =
position(primary_monitor.as_ref(), settings.size, settings.position)
{
- window_builder = window_builder.with_position(position);
+ attributes = attributes.with_position(position);
}
if let Some(min_size) = settings.min_size {
- window_builder =
- window_builder.with_min_inner_size(winit::dpi::LogicalSize {
- width: min_size.width,
- height: min_size.height,
- });
+ attributes = attributes.with_min_inner_size(winit::dpi::LogicalSize {
+ width: min_size.width,
+ height: min_size.height,
+ });
}
if let Some(max_size) = settings.max_size {
- window_builder =
- window_builder.with_max_inner_size(winit::dpi::LogicalSize {
- width: max_size.width,
- height: max_size.height,
- });
+ attributes = attributes.with_max_inner_size(winit::dpi::LogicalSize {
+ width: max_size.width,
+ height: max_size.height,
+ });
}
#[cfg(any(
@@ -65,35 +63,33 @@ pub fn window_settings(
target_os = "openbsd"
))]
{
- // `with_name` is available on both `WindowBuilderExtWayland` and `WindowBuilderExtX11` and they do
- // exactly the same thing. We arbitrarily choose `WindowBuilderExtWayland` here.
- use ::winit::platform::wayland::WindowBuilderExtWayland;
+ use ::winit::platform::wayland::WindowAttributesExtWayland;
if let Some(id) = _id {
- window_builder = window_builder.with_name(id.clone(), id);
+ attributes = attributes.with_name(id.clone(), id);
}
}
#[cfg(target_os = "windows")]
{
- use winit::platform::windows::WindowBuilderExtWindows;
+ use winit::platform::windows::WindowAttributesExtWindows;
#[allow(unsafe_code)]
unsafe {
- window_builder = window_builder
+ attributes = attributes
.with_parent_window(settings.platform_specific.parent);
}
- window_builder = window_builder
+ attributes = attributes
.with_drag_and_drop(settings.platform_specific.drag_and_drop);
- window_builder = window_builder
+ attributes = attributes
.with_skip_taskbar(settings.platform_specific.skip_taskbar);
}
#[cfg(target_os = "macos")]
{
- use winit::platform::macos::WindowBuilderExtMacOS;
+ use winit::platform::macos::WindowAttributesExtMacOS;
- window_builder = window_builder
+ attributes = attributes
.with_title_hidden(settings.platform_specific.title_hidden)
.with_titlebar_transparent(
settings.platform_specific.titlebar_transparent,
@@ -107,25 +103,25 @@ pub fn window_settings(
{
#[cfg(feature = "x11")]
{
- use winit::platform::x11::WindowBuilderExtX11;
+ use winit::platform::x11::WindowAttributesExtX11;
- window_builder = window_builder.with_name(
+ attributes = attributes.with_name(
&settings.platform_specific.application_id,
&settings.platform_specific.application_id,
);
}
#[cfg(feature = "wayland")]
{
- use winit::platform::wayland::WindowBuilderExtWayland;
+ use winit::platform::wayland::WindowAttributesExtWayland;
- window_builder = window_builder.with_name(
+ attributes = attributes.with_name(
&settings.platform_specific.application_id,
&settings.platform_specific.application_id,
);
}
}
- window_builder
+ attributes
}
/// Converts a winit window event into an iced event.
@@ -396,7 +392,9 @@ pub fn mouse_interaction(
use mouse::Interaction;
match interaction {
- Interaction::Idle => winit::window::CursorIcon::Default,
+ Interaction::None | Interaction::Idle => {
+ winit::window::CursorIcon::Default
+ }
Interaction::Pointer => winit::window::CursorIcon::Pointer,
Interaction::Working => winit::window::CursorIcon::Progress,
Interaction::Grab => winit::window::CursorIcon::Grab,
diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs
index 3537ac18..673a6f30 100644
--- a/winit/src/multi_window.rs
+++ b/winit/src/multi_window.rs
@@ -12,6 +12,7 @@ use crate::core::widget::operation;
use crate::core::window;
use crate::core::{Point, Size};
use crate::futures::futures::channel::mpsc;
+use crate::futures::futures::channel::oneshot;
use crate::futures::futures::executor;
use crate::futures::futures::task;
use crate::futures::futures::{Future, StreamExt};
@@ -114,22 +115,22 @@ where
C: Compositor<Renderer = A::Renderer> + 'static,
A::Theme: DefaultStyle,
{
- use winit::event_loop::EventLoopBuilder;
+ use winit::event_loop::EventLoop;
let mut debug = Debug::new();
debug.startup_started();
- let event_loop = EventLoopBuilder::with_user_event()
+ let event_loop = EventLoop::with_user_event()
.build()
.expect("Create event loop");
- let proxy = event_loop.create_proxy();
+ let (proxy, worker) = Proxy::new(event_loop.create_proxy());
let runtime = {
- let proxy = Proxy::new(event_loop.create_proxy());
let executor = E::new().map_err(Error::ExecutorCreationFailed)?;
+ executor.spawn(worker);
- Runtime::new(executor, proxy)
+ Runtime::new(executor, proxy.clone())
};
let (application, init_command) = {
@@ -138,187 +139,292 @@ where
runtime.enter(|| A::new(flags))
};
- let should_main_be_visible = settings.window.visible;
- let exit_on_close_request = settings.window.exit_on_close_request;
+ let id = settings.id;
+ let title = application.title(window::Id::MAIN);
- let builder = conversion::window_settings(
- settings.window,
- &application.title(window::Id::MAIN),
- event_loop.primary_monitor(),
- settings.id,
- )
- .with_visible(false);
+ let (boot_sender, boot_receiver) = oneshot::channel();
+ let (event_sender, event_receiver) = mpsc::unbounded();
+ let (control_sender, control_receiver) = mpsc::unbounded();
- log::info!("Window builder: {:#?}", builder);
+ let instance = Box::pin(run_instance::<A, E, C>(
+ application,
+ runtime,
+ proxy,
+ debug,
+ boot_receiver,
+ event_receiver,
+ control_sender,
+ init_command,
+ ));
- let main_window = Arc::new(
- builder
- .build(&event_loop)
- .map_err(Error::WindowCreationFailed)?,
- );
+ let context = task::Context::from_waker(task::noop_waker_ref());
+
+ struct Runner<Message: 'static, F, C> {
+ instance: std::pin::Pin<Box<F>>,
+ context: task::Context<'static>,
+ boot: Option<BootConfig<C>>,
+ sender: mpsc::UnboundedSender<Event<Message>>,
+ receiver: mpsc::UnboundedReceiver<Control>,
+ error: Option<Error>,
+ }
+
+ struct BootConfig<C> {
+ sender: oneshot::Sender<Boot<C>>,
+ id: Option<String>,
+ title: String,
+ window_settings: window::Settings,
+ graphics_settings: graphics::Settings,
+ }
- #[cfg(target_arch = "wasm32")]
+ let mut runner = Runner {
+ instance,
+ context,
+ boot: Some(BootConfig {
+ sender: boot_sender,
+ id,
+ title,
+ window_settings: settings.window,
+ graphics_settings,
+ }),
+ sender: event_sender,
+ receiver: control_receiver,
+ error: None,
+ };
+
+ impl<Message, F, C> winit::application::ApplicationHandler<Message>
+ for Runner<Message, F, C>
+ where
+ F: Future<Output = ()>,
+ C: Compositor,
{
- use winit::platform::web::WindowExtWebSys;
+ fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {
+ let Some(BootConfig {
+ sender,
+ id,
+ title,
+ window_settings,
+ graphics_settings,
+ }) = self.boot.take()
+ else {
+ return;
+ };
- let canvas = main_window.canvas();
+ let should_be_visible = window_settings.visible;
+ let exit_on_close_request = window_settings.exit_on_close_request;
- let window = web_sys::window().unwrap();
- let document = window.document().unwrap();
- let body = document.body().unwrap();
+ let window_attributes = conversion::window_attributes(
+ window_settings,
+ &title,
+ event_loop.primary_monitor(),
+ id,
+ )
+ .with_visible(false);
- let target = target.and_then(|target| {
- body.query_selector(&format!("#{}", target))
- .ok()
- .unwrap_or(None)
- });
+ log::debug!("Window attributes: {window_attributes:#?}");
- match target {
- Some(node) => {
- let _ = node
- .replace_with_with_node_1(&canvas)
- .expect(&format!("Could not replace #{}", node.id()));
- }
- None => {
- let _ = body
- .append_child(&canvas)
- .expect("Append canvas to HTML body");
- }
- };
- }
+ let window = match event_loop.create_window(window_attributes) {
+ Ok(window) => Arc::new(window),
+ Err(error) => {
+ self.error = Some(Error::WindowCreationFailed(error));
+ event_loop.exit();
+ return;
+ }
+ };
- let mut compositor =
- executor::block_on(C::new(graphics_settings, main_window.clone()))?;
+ let finish_boot = async move {
+ let compositor =
+ C::new(graphics_settings, window.clone()).await?;
+
+ sender
+ .send(Boot {
+ window,
+ compositor,
+ should_be_visible,
+ exit_on_close_request,
+ })
+ .ok()
+ .expect("Send boot event");
+
+ Ok::<_, graphics::Error>(())
+ };
- let mut window_manager = WindowManager::new();
- let _ = window_manager.insert(
- window::Id::MAIN,
- main_window,
- &application,
- &mut compositor,
- exit_on_close_request,
- );
+ if let Err(error) = executor::block_on(finish_boot) {
+ self.error = Some(Error::GraphicsCreationFailed(error));
+ event_loop.exit();
+ }
+ }
- let (mut event_sender, event_receiver) = mpsc::unbounded();
- let (control_sender, mut control_receiver) = mpsc::unbounded();
+ fn new_events(
+ &mut self,
+ event_loop: &winit::event_loop::ActiveEventLoop,
+ cause: winit::event::StartCause,
+ ) {
+ if self.boot.is_some() {
+ return;
+ }
- let mut instance = Box::pin(run_instance::<A, E, C>(
- application,
- compositor,
- runtime,
- proxy,
- debug,
- event_receiver,
- control_sender,
- init_command,
- window_manager,
- should_main_be_visible,
- ));
+ self.process_event(
+ event_loop,
+ Event::EventLoopAwakened(winit::event::Event::NewEvents(cause)),
+ );
+ }
- let mut context = task::Context::from_waker(task::noop_waker_ref());
+ fn window_event(
+ &mut self,
+ event_loop: &winit::event_loop::ActiveEventLoop,
+ window_id: winit::window::WindowId,
+ event: winit::event::WindowEvent,
+ ) {
+ #[cfg(target_os = "windows")]
+ let is_move_or_resize = matches!(
+ event,
+ winit::event::WindowEvent::Resized(_)
+ | winit::event::WindowEvent::Moved(_)
+ );
+
+ self.process_event(
+ event_loop,
+ Event::EventLoopAwakened(winit::event::Event::WindowEvent {
+ window_id,
+ event,
+ }),
+ );
+
+ // TODO: Remove when unnecessary
+ // On Windows, we emulate an `AboutToWait` event after every `Resized` event
+ // since the event loop does not resume during resize interaction.
+ // More details: https://github.com/rust-windowing/winit/issues/3272
+ #[cfg(target_os = "windows")]
+ {
+ if is_move_or_resize {
+ self.process_event(
+ event_loop,
+ Event::EventLoopAwakened(
+ winit::event::Event::AboutToWait,
+ ),
+ );
+ }
+ }
+ }
- let process_event = move |event, event_loop: &winit::event_loop::EventLoopWindowTarget<_>| {
- if event_loop.exiting() {
- return;
+ fn user_event(
+ &mut self,
+ event_loop: &winit::event_loop::ActiveEventLoop,
+ message: Message,
+ ) {
+ self.process_event(
+ event_loop,
+ Event::EventLoopAwakened(winit::event::Event::UserEvent(
+ message,
+ )),
+ );
}
- event_sender
- .start_send(Event::EventLoopAwakened(event))
- .expect("Send event");
-
- loop {
- let poll = instance.as_mut().poll(&mut context);
-
- match poll {
- task::Poll::Pending => match control_receiver.try_next() {
- Ok(Some(control)) => match control {
- Control::ChangeFlow(flow) => {
- use winit::event_loop::ControlFlow;
-
- match (event_loop.control_flow(), flow) {
- (
- ControlFlow::WaitUntil(current),
- ControlFlow::WaitUntil(new),
- ) if new < current => {}
- (
- ControlFlow::WaitUntil(target),
- ControlFlow::Wait,
- ) if target > Instant::now() => {}
- _ => {
- event_loop.set_control_flow(flow);
+ fn about_to_wait(
+ &mut self,
+ event_loop: &winit::event_loop::ActiveEventLoop,
+ ) {
+ self.process_event(
+ event_loop,
+ Event::EventLoopAwakened(winit::event::Event::AboutToWait),
+ );
+ }
+ }
+
+ impl<Message, F, C> Runner<Message, F, C>
+ where
+ F: Future<Output = ()>,
+ C: Compositor,
+ {
+ fn process_event(
+ &mut self,
+ event_loop: &winit::event_loop::ActiveEventLoop,
+ event: Event<Message>,
+ ) {
+ if event_loop.exiting() {
+ return;
+ }
+
+ self.sender.start_send(event).expect("Send event");
+
+ loop {
+ let poll = self.instance.as_mut().poll(&mut self.context);
+
+ match poll {
+ task::Poll::Pending => match self.receiver.try_next() {
+ Ok(Some(control)) => match control {
+ Control::ChangeFlow(flow) => {
+ use winit::event_loop::ControlFlow;
+
+ match (event_loop.control_flow(), flow) {
+ (
+ ControlFlow::WaitUntil(current),
+ ControlFlow::WaitUntil(new),
+ ) if new < current => {}
+ (
+ ControlFlow::WaitUntil(target),
+ ControlFlow::Wait,
+ ) if target > Instant::now() => {}
+ _ => {
+ event_loop.set_control_flow(flow);
+ }
}
}
- }
- Control::CreateWindow {
- id,
- settings,
- title,
- monitor,
- } => {
- let exit_on_close_request =
- settings.exit_on_close_request;
-
- let window = conversion::window_settings(
- settings, &title, monitor, None,
- )
- .build(event_loop)
- .expect("Failed to build window");
-
- event_sender
- .start_send(Event::WindowCreated {
- id,
- window,
- exit_on_close_request,
- })
- .expect("Send event");
- }
- Control::Exit => {
- event_loop.exit();
+ Control::CreateWindow {
+ id,
+ settings,
+ title,
+ monitor,
+ } => {
+ let exit_on_close_request =
+ settings.exit_on_close_request;
+
+ let window = event_loop
+ .create_window(
+ conversion::window_attributes(
+ settings, &title, monitor, None,
+ ),
+ )
+ .expect("Create window");
+
+ self.process_event(
+ event_loop,
+ Event::WindowCreated {
+ id,
+ window,
+ exit_on_close_request,
+ },
+ );
+ }
+ Control::Exit => {
+ event_loop.exit();
+ }
+ },
+ _ => {
+ break;
}
},
- _ => {
+ task::Poll::Ready(_) => {
+ event_loop.exit();
break;
}
- },
- task::Poll::Ready(_) => {
- event_loop.exit();
- break;
- }
- };
- }
- };
-
- #[cfg(not(target_os = "windows"))]
- let _ = event_loop.run(process_event);
-
- // TODO: Remove when unnecessary
- // On Windows, we emulate an `AboutToWait` event after every `Resized` event
- // since the event loop does not resume during resize interaction.
- // More details: https://github.com/rust-windowing/winit/issues/3272
- #[cfg(target_os = "windows")]
- {
- let mut process_event = process_event;
-
- let _ = event_loop.run(move |event, event_loop| {
- if matches!(
- event,
- winit::event::Event::WindowEvent {
- event: winit::event::WindowEvent::Resized(_)
- | winit::event::WindowEvent::Moved(_),
- ..
- }
- ) {
- process_event(event, event_loop);
- process_event(winit::event::Event::AboutToWait, event_loop);
- } else {
- process_event(event, event_loop);
+ };
}
- });
+ }
}
+ let _ = event_loop.run_app(&mut runner);
+
Ok(())
}
+struct Boot<C> {
+ window: Arc<winit::window::Window>,
+ compositor: C,
+ should_be_visible: bool,
+ exit_on_close_request: bool,
+}
+
enum Event<Message: 'static> {
WindowCreated {
id: window::Id,
@@ -341,15 +447,13 @@ enum Control {
async fn run_instance<A, E, C>(
mut application: A,
- mut compositor: C,
mut runtime: Runtime<E, Proxy<A::Message>, A::Message>,
- mut proxy: winit::event_loop::EventLoopProxy<A::Message>,
+ mut proxy: Proxy<A::Message>,
mut debug: Debug,
+ mut boot: oneshot::Receiver<Boot<C>>,
mut event_receiver: mpsc::UnboundedReceiver<Event<A::Message>>,
mut control_sender: mpsc::UnboundedSender<Control>,
init_command: Command<A::Message>,
- mut window_manager: WindowManager<A, C>,
- should_main_window_be_visible: bool,
) where
A: Application + 'static,
E: Executor + 'static,
@@ -359,11 +463,28 @@ async fn run_instance<A, E, C>(
use winit::event;
use winit::event_loop::ControlFlow;
+ let Boot {
+ window: main_window,
+ mut compositor,
+ should_be_visible,
+ exit_on_close_request,
+ } = boot.try_recv().ok().flatten().expect("Receive boot");
+
+ let mut window_manager = WindowManager::new();
+
+ let _ = window_manager.insert(
+ window::Id::MAIN,
+ main_window.clone(),
+ &application,
+ &mut compositor,
+ exit_on_close_request,
+ );
+
let main_window = window_manager
.get_mut(window::Id::MAIN)
.expect("Get main window");
- if should_main_window_be_visible {
+ if should_be_visible {
main_window.raw.set_visible(true);
}
@@ -408,6 +529,7 @@ async fn run_instance<A, E, C>(
runtime.track(application.subscription().into_recipes());
let mut messages = Vec::new();
+ let mut user_events = 0;
debug.startup_finished();
@@ -482,6 +604,7 @@ async fn run_instance<A, E, C>(
}
event::Event::UserEvent(message) => {
messages.push(message);
+ user_events += 1;
}
event::Event::WindowEvent {
window_id: id,
@@ -530,7 +653,7 @@ async fn run_instance<A, E, C>(
debug.draw_finished();
if new_mouse_interaction != window.mouse_interaction {
- window.raw.set_cursor_icon(
+ window.raw.set_cursor(
conversion::mouse_interaction(
new_mouse_interaction,
),
@@ -601,7 +724,7 @@ async fn run_instance<A, E, C>(
if new_mouse_interaction != window.mouse_interaction
{
- window.raw.set_cursor_icon(
+ window.raw.set_cursor(
conversion::mouse_interaction(
new_mouse_interaction,
),
@@ -803,6 +926,11 @@ async fn run_instance<A, E, C>(
&mut window_manager,
cached_interfaces,
));
+
+ if user_events > 0 {
+ proxy.free_slots(user_events);
+ user_events = 0;
+ }
}
}
_ => {}
@@ -845,7 +973,7 @@ fn update<A: Application, C, E: Executor>(
runtime: &mut Runtime<E, Proxy<A::Message>, A::Message>,
clipboard: &mut Clipboard,
control_sender: &mut mpsc::UnboundedSender<Control>,
- proxy: &mut winit::event_loop::EventLoopProxy<A::Message>,
+ proxy: &mut Proxy<A::Message>,
debug: &mut Debug,
messages: &mut Vec<A::Message>,
window_manager: &mut WindowManager<A, C>,
@@ -887,7 +1015,7 @@ fn run_command<A, C, E>(
runtime: &mut Runtime<E, Proxy<A::Message>, A::Message>,
clipboard: &mut Clipboard,
control_sender: &mut mpsc::UnboundedSender<Control>,
- proxy: &mut winit::event_loop::EventLoopProxy<A::Message>,
+ proxy: &mut Proxy<A::Message>,
debug: &mut Debug,
window_manager: &mut WindowManager<A, C>,
ui_caches: &mut FxHashMap<window::Id, user_interface::Cache>,
@@ -913,9 +1041,7 @@ fn run_command<A, C, E>(
clipboard::Action::Read(tag, kind) => {
let message = tag(clipboard.read(kind));
- proxy
- .send_event(message)
- .expect("Send message to event loop");
+ proxy.send(message);
}
clipboard::Action::Write(contents, kind) => {
clipboard.write(kind, contents);
@@ -967,18 +1093,12 @@ fn run_command<A, C, E>(
.to_logical(window.raw.scale_factor());
proxy
- .send_event(callback(Size::new(
- size.width,
- size.height,
- )))
- .expect("Send message to event loop");
+ .send(callback(Size::new(size.width, size.height)));
}
}
window::Action::FetchMaximized(id, callback) => {
if let Some(window) = window_manager.get_mut(id) {
- proxy
- .send_event(callback(window.raw.is_maximized()))
- .expect("Send message to event loop");
+ proxy.send(callback(window.raw.is_maximized()));
}
}
window::Action::Maximize(id, maximized) => {
@@ -988,9 +1108,7 @@ fn run_command<A, C, E>(
}
window::Action::FetchMinimized(id, callback) => {
if let Some(window) = window_manager.get_mut(id) {
- proxy
- .send_event(callback(window.raw.is_minimized()))
- .expect("Send message to event loop");
+ proxy.send(callback(window.raw.is_minimized()));
}
}
window::Action::Minimize(id, minimized) => {
@@ -1012,9 +1130,7 @@ fn run_command<A, C, E>(
})
.ok();
- proxy
- .send_event(callback(position))
- .expect("Send message to event loop");
+ proxy.send(callback(position));
}
}
window::Action::Move(id, position) => {
@@ -1049,9 +1165,7 @@ fn run_command<A, C, E>(
core::window::Mode::Hidden
};
- proxy
- .send_event(tag(mode))
- .expect("Send message to event loop");
+ proxy.send(tag(mode));
}
}
window::Action::ToggleMaximize(id) => {
@@ -1099,9 +1213,7 @@ fn run_command<A, C, E>(
}
window::Action::FetchId(id, tag) => {
if let Some(window) = window_manager.get_mut(id) {
- proxy
- .send_event(tag(window.raw.id().into()))
- .expect("Send message to event loop");
+ proxy.send(tag(window.raw.id().into()));
}
}
window::Action::RunWithHandle(id, tag) => {
@@ -1111,9 +1223,7 @@ fn run_command<A, C, E>(
.get_mut(id)
.and_then(|window| window.raw.window_handle().ok())
{
- proxy
- .send_event(tag(&handle))
- .expect("Send message to event loop");
+ proxy.send(tag(&handle));
}
}
window::Action::Screenshot(id, tag) => {
@@ -1126,12 +1236,10 @@ fn run_command<A, C, E>(
&debug.overlay(),
);
- proxy
- .send_event(tag(window::Screenshot::new(
- bytes,
- window.state.physical_size(),
- )))
- .expect("Event loop doesn't exist.");
+ proxy.send(tag(window::Screenshot::new(
+ bytes,
+ window.state.physical_size(),
+ )));
}
}
},
@@ -1140,7 +1248,7 @@ fn run_command<A, C, E>(
#[cfg(feature = "system")]
{
let graphics_info = compositor.fetch_information();
- let proxy = proxy.clone();
+ let mut proxy = proxy.clone();
let _ = std::thread::spawn(move || {
let information =
@@ -1148,9 +1256,7 @@ fn run_command<A, C, E>(
let message = _tag(information);
- proxy
- .send_event(message)
- .expect("Event loop doesn't exist.");
+ proxy.send(message);
});
}
}
@@ -1175,9 +1281,7 @@ fn run_command<A, C, E>(
match operation.finish() {
operation::Outcome::None => {}
operation::Outcome::Some(message) => {
- proxy
- .send_event(message)
- .expect("Event loop doesn't exist.");
+ proxy.send(message);
// operation completed, don't need to try to operate on rest of UIs
break 'operate;
@@ -1197,9 +1301,7 @@ fn run_command<A, C, E>(
// TODO: Error handling (?)
compositor.load_font(bytes.clone());
- proxy
- .send_event(tagger(Ok(())))
- .expect("Send message to event loop");
+ proxy.send(tagger(Ok(())));
}
command::Action::Custom(_) => {
log::warn!("Unsupported custom action in `iced_winit` shell");
@@ -1209,7 +1311,7 @@ fn run_command<A, C, E>(
}
/// Build the user interface for every window.
-pub fn build_user_interfaces<'a, A: Application, C: Compositor>(
+pub fn build_user_interfaces<'a, A: Application, C>(
application: &'a A,
debug: &mut Debug,
window_manager: &mut WindowManager<A, C>,
diff --git a/winit/src/multi_window/window_manager.rs b/winit/src/multi_window/window_manager.rs
index 71c1688b..57a7dc7e 100644
--- a/winit/src/multi_window/window_manager.rs
+++ b/winit/src/multi_window/window_manager.rs
@@ -9,8 +9,9 @@ use std::sync::Arc;
use winit::monitor::MonitorHandle;
#[allow(missing_debug_implementations)]
-pub struct WindowManager<A: Application, C: Compositor>
+pub struct WindowManager<A, C>
where
+ A: Application,
C: Compositor<Renderer = A::Renderer>,
A::Theme: DefaultStyle,
{
@@ -60,7 +61,7 @@ where
exit_on_close_request,
surface,
renderer,
- mouse_interaction: mouse::Interaction::Idle,
+ mouse_interaction: mouse::Interaction::None,
},
);
diff --git a/winit/src/proxy.rs b/winit/src/proxy.rs
index 1d6c48bb..3edc30ad 100644
--- a/winit/src/proxy.rs
+++ b/winit/src/proxy.rs
@@ -1,28 +1,94 @@
use crate::futures::futures::{
channel::mpsc,
+ select,
task::{Context, Poll},
- Sink,
+ Future, Sink, StreamExt,
};
use std::pin::Pin;
-/// An event loop proxy that implements `Sink`.
+/// An event loop proxy with backpressure that implements `Sink`.
#[derive(Debug)]
pub struct Proxy<Message: 'static> {
raw: winit::event_loop::EventLoopProxy<Message>,
+ sender: mpsc::Sender<Message>,
+ notifier: mpsc::Sender<usize>,
}
impl<Message: 'static> Clone for Proxy<Message> {
fn clone(&self) -> Self {
Self {
raw: self.raw.clone(),
+ sender: self.sender.clone(),
+ notifier: self.notifier.clone(),
}
}
}
impl<Message: 'static> Proxy<Message> {
+ const MAX_SIZE: usize = 100;
+
/// Creates a new [`Proxy`] from an `EventLoopProxy`.
- pub fn new(raw: winit::event_loop::EventLoopProxy<Message>) -> Self {
- Self { raw }
+ pub fn new(
+ raw: winit::event_loop::EventLoopProxy<Message>,
+ ) -> (Self, impl Future<Output = ()>) {
+ let (notifier, mut processed) = mpsc::channel(Self::MAX_SIZE);
+ let (sender, mut receiver) = mpsc::channel(Self::MAX_SIZE);
+ let proxy = raw.clone();
+
+ let worker = async move {
+ let mut count = 0;
+
+ loop {
+ if count < Self::MAX_SIZE {
+ select! {
+ message = receiver.select_next_some() => {
+ let _ = proxy.send_event(message);
+ count += 1;
+
+ }
+ amount = processed.select_next_some() => {
+ count = count.saturating_sub(amount);
+ }
+ complete => break,
+ }
+ } else {
+ select! {
+ amount = processed.select_next_some() => {
+ count = count.saturating_sub(amount);
+ }
+ complete => break,
+ }
+ }
+ }
+ };
+
+ (
+ Self {
+ raw,
+ sender,
+ notifier,
+ },
+ worker,
+ )
+ }
+
+ /// Sends a `Message` to the event loop.
+ ///
+ /// Note: This skips the backpressure mechanism with an unbounded
+ /// channel. Use sparingly!
+ pub fn send(&mut self, message: Message)
+ where
+ Message: std::fmt::Debug,
+ {
+ self.raw
+ .send_event(message)
+ .expect("Send message to event loop");
+ }
+
+ /// Frees an amount of slots for additional messages to be queued in
+ /// this [`Proxy`].
+ pub fn free_slots(&mut self, amount: usize) {
+ let _ = self.notifier.start_send(amount);
}
}
@@ -30,32 +96,37 @@ impl<Message: 'static> Sink<Message> for Proxy<Message> {
type Error = mpsc::SendError;
fn poll_ready(
- self: Pin<&mut Self>,
- _cx: &mut Context<'_>,
+ mut self: Pin<&mut Self>,
+ cx: &mut Context<'_>,
) -> Poll<Result<(), Self::Error>> {
- Poll::Ready(Ok(()))
+ self.sender.poll_ready(cx)
}
fn start_send(
- self: Pin<&mut Self>,
+ mut self: Pin<&mut Self>,
message: Message,
) -> Result<(), Self::Error> {
- let _ = self.raw.send_event(message);
-
- Ok(())
+ self.sender.start_send(message)
}
fn poll_flush(
- self: Pin<&mut Self>,
- _cx: &mut Context<'_>,
+ mut self: Pin<&mut Self>,
+ cx: &mut Context<'_>,
) -> Poll<Result<(), Self::Error>> {
- Poll::Ready(Ok(()))
+ match self.sender.poll_ready(cx) {
+ Poll::Ready(Err(ref e)) if e.is_disconnected() => {
+ // If the receiver disconnected, we consider the sink to be flushed.
+ Poll::Ready(Ok(()))
+ }
+ x => x,
+ }
}
fn poll_close(
- self: Pin<&mut Self>,
+ mut self: Pin<&mut Self>,
_cx: &mut Context<'_>,
) -> Poll<Result<(), Self::Error>> {
+ self.sender.disconnect();
Poll::Ready(Ok(()))
}
}