diff options
85 files changed, 2184 insertions, 1122 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 @@ -142,7 +142,7 @@ cosmic-text = "0.10" dark-light = "1.0" futures = "0.3" glam = "0.25" -glyphon = { git = "https://github.com/hecrj/glyphon.git", rev = "ceed55403ce53e120ce9d1fae17dcfe388726118" } +glyphon = { git = "https://github.com/hecrj/glyphon.git", rev = "f07e7bab705e69d39a5e6e52c73039a93c4552f8" } guillotiere = "0.6" half = "2.2" image = "0.24" @@ -172,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/ipsum.txt b/benches/ipsum.txt new file mode 100644 index 00000000..3e2d6396 --- /dev/null +++ b/benches/ipsum.txt @@ -0,0 +1,9 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur at elit mollis, dictum nunc non, tempus metus. Sed iaculis ac mauris eu lobortis. Integer elementum venenatis eros, id placerat odio feugiat vel. Maecenas consequat convallis tincidunt. Nunc eu lorem justo. Praesent quis ornare sapien. Aliquam interdum tortor ut rhoncus faucibus. Suspendisse molestie scelerisque nulla, eget sodales lacus sodales vel. Nunc placerat id arcu sodales venenatis. Praesent ullamcorper viverra nibh eget efficitur. Aliquam molestie felis vehicula, finibus sapien eget, accumsan purus. Praesent vestibulum eleifend consectetur. Sed tincidunt lectus a libero efficitur, non scelerisque lectus tincidunt. + +Cras ullamcorper tincidunt tellus non tempor. Integer pulvinar turpis quam, nec pharetra purus egestas non. Vivamus sed ipsum consequat, dignissim ante et, suscipit nibh. Quisque et mauris eu erat rutrum cursus. Pellentesque ut neque eu neque eleifend auctor ac hendrerit dolor. Morbi eget egestas ex. Integer hendrerit ipsum in enim bibendum, at vehicula ipsum dapibus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce tempus consectetur tortor, vel fermentum sem pulvinar eget. Maecenas rutrum fringilla eros a pellentesque. Cras quis magna consectetur, tristique massa vel, aliquet nunc. Aliquam erat volutpat. Suspendisse porttitor risus id auctor fermentum. Vivamus efficitur tellus sed tortor cursus tincidunt. Sed auctor varius arcu, non congue tellus vehicula finibus. + +Fusce a tincidunt urna. Nunc at quam ac enim tempor vehicula imperdiet in sapien. Donec lobortis tristique felis vel semper. Quisque vulputate felis eu enim vestibulum malesuada. Fusce a lobortis mauris, iaculis eleifend ligula. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Vivamus sodales vel elit dignissim mattis. + +Aliquam placerat vulputate dignissim. Proin pellentesque vitae arcu ut feugiat. Nunc mi felis, ornare at gravida sed, vestibulum sed urna. Duis fermentum maximus viverra. Donec imperdiet pellentesque sollicitudin. Cras non sem quis metus bibendum molestie. Duis imperdiet nec lectus eu rutrum. Mauris congue enim purus, in iaculis arcu dapibus ut. Nullam id erat tincidunt, iaculis dolor non, lobortis magna. Proin convallis scelerisque maximus. Morbi at lorem fringilla libero blandit fringilla. Ut aliquet tellus non sem dictum viverra. Aenean venenatis purus eget lacus placerat, non mollis mauris pellentesque. + +Etiam elit diam, aliquet quis suscipit non, condimentum viverra odio. Praesent mi enim, suscipit id mi in, rhoncus ultricies lorem. Nulla facilisi. Integer convallis sagittis euismod. Vestibulum porttitor sodales turpis ac accumsan. Nullam molestie turpis vel lacus tincidunt, sed finibus erat pharetra. Nullam vestibulum turpis id sollicitudin accumsan. Praesent eget posuere lacus. Donec vehicula, nisl nec suscipit porta, felis lorem gravida orci, a hendrerit tellus nibh sit amet elit. diff --git a/benches/wgpu.rs b/benches/wgpu.rs index 3dd415db..0e407253 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, stack, text}; +use iced::widget::{canvas, scrollable, stack, text}; use iced::{ Color, Element, Font, Length, Pixels, Point, Rectangle, Size, Theme, }; @@ -14,20 +14,31 @@ criterion_group!(benches, wgpu_benchmark); #[allow(unused_results)] 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 — 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)); + benchmark(b, |_| layered_text(10)); }); c.bench_function("wgpu - layered text (heavy)", |b| { - benchmark(b, layered_text(1_000)); + benchmark(b, |_| layered_text(1_000)); + }); + + c.bench_function("wgpu - dynamic text (light)", |b| { + benchmark(b, |i| dynamic_text(1_000, i)); + }); + c.bench_function("wgpu - dynamic text (heavy)", |b| { + benchmark(b, |i| dynamic_text(100_000, i)); }); } -fn benchmark( +fn benchmark<'a>( bencher: &mut Bencher<'_>, - widget: Element<'_, (), Theme, Renderer>, + view: impl Fn(usize) -> Element<'a, (), Theme, Renderer>, ) { use iced_futures::futures::executor; use iced_wgpu::graphics; @@ -70,7 +81,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); @@ -93,14 +105,17 @@ fn benchmark( let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default()); - let mut user_interface = runtime::UserInterface::build( - widget, - viewport.logical_size(), - runtime::user_interface::Cache::default(), - &mut renderer, - ); + let mut i = 0; + let mut cache = Some(runtime::user_interface::Cache::default()); bencher.iter(|| { + let mut user_interface = runtime::UserInterface::build( + view(i), + viewport.logical_size(), + cache.take().unwrap(), + &mut renderer, + ); + let _ = user_interface.draw( &mut renderer, &Theme::Dark, @@ -110,6 +125,8 @@ fn benchmark( mouse::Cursor::Unavailable, ); + cache = Some(user_interface.into_cache()); + let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None, @@ -129,6 +146,8 @@ fn benchmark( let submission = engine.submit(&queue, encoder); let _ = device.poll(wgpu::Maintain::WaitForSubmissionIndex(submission)); + + i += 1; }); } @@ -188,3 +207,22 @@ fn layered_text<'a, Message: 'a>( .height(Length::Fill) .into() } + +fn dynamic_text<'a, Message: 'a>( + n: usize, + i: usize, +) -> Element<'a, Message, Theme, Renderer> { + const LOREM_IPSUM: &str = include_str!("ipsum.txt"); + + scrollable( + text(format!( + "{}... Iteration {i}", + std::iter::repeat(LOREM_IPSUM.chars()) + .flatten() + .take(n) + .collect::<String>(), + )) + .size(10), + ) + .into() +} 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 c38239bc..82ecdd0f 100644 --- a/core/src/image.rs +++ b/core/src/image.rs @@ -1,7 +1,7 @@ //! Load and draw raster graphics. pub use bytes::Bytes; -use crate::{Rectangle, Size}; +use crate::{Radians, Rectangle, Size}; use rustc_hash::FxHasher; use std::hash::{Hash, Hasher}; @@ -173,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/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/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/window/position.rs b/core/src/window/position.rs index 73391e75..1c8e86b6 100644 --- a/core/src/window/position.rs +++ b/core/src/window/position.rs @@ -1,4 +1,4 @@ -use crate::Point; +use crate::{Point, Size}; /// The position of a window in a given screen. #[derive(Debug, Clone, Copy, PartialEq)] @@ -15,6 +15,12 @@ pub enum Position { /// at (0, 0) you would have to set the position to /// `(PADDING_X, PADDING_Y)`. Specific(Point), + /// Like [`Specific`], but the window is positioned with the specific coordinates returned by the function. + /// + /// The function receives the window size and the monitor's resolution as input. + /// + /// [`Specific`]: Self::Specific + SpecificWith(fn(Size, Size) -> Point), } impl Default for Position { 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/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/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..c20a7d67 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(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..3c7969c5 100644 --- a/examples/geometry/src/main.rs +++ b/examples/geometry/src/main.rs @@ -176,12 +176,7 @@ fn view(_state: &()) -> Element<'_, ()> { .spacing(20) .max_width(500); - let scrollable = - scrollable(container(content).width(Length::Fill).center_x()); - - container(scrollable) - .width(Length::Fill) - .height(Length::Fill) - .center_y() - .into() + let scrollable = scrollable(container(content).center_x(Length::Fill)); + + container(scrollable).center_y(Length::Fill).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..c40ac820 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,8 +251,7 @@ fn application<'a>() -> Element<'a, Message> { .align_items(Alignment::Center), ) .style(container::rounded_box) - .height(Length::Fill) - .center_y(); + .center_y(Length::Fill); let content = container( scrollable( 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 42b7f471..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() } } diff --git a/examples/modal/src/main.rs b/examples/modal/src/main.rs index a345924d..a012c310 100644 --- a/examples/modal/src/main.rs +++ b/examples/modal/src/main.rs @@ -2,8 +2,8 @@ use iced::event::{self, Event}; use iced::keyboard; use iced::keyboard::key; use iced::widget::{ - self, button, column, container, horizontal_space, mouse_area, opaque, - pick_list, row, stack, text, text_input, + self, button, center, column, container, horizontal_space, mouse_area, + opaque, pick_list, row, stack, text, text_input, }; use iced::{Alignment, Color, Command, Element, Length, Subscription}; @@ -98,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(), @@ -115,9 +109,7 @@ impl App { ] .height(Length::Fill), ) - .padding(10) - .width(Length::Fill) - .height(Length::Fill); + .padding(10); if self.show_modal { let signup = container( @@ -210,25 +202,18 @@ where { stack![ base.into(), - mouse_area( - container(opaque(content)) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y() - .style(|_theme| { - container::Style { - background: Some( - Color { - a: 0.8, - ..Color::BLACK - } - .into(), - ), - ..container::Style::default() + mouse_area(center(opaque(content)).style(|_theme| { + container::Style { + background: Some( + Color { + a: 0.8, + ..Color::BLACK } - }) - ) + .into(), + ), + ..container::Style::default() + } + })) .on_press(on_blur) ] .into() diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs index 5a5e70c1..31c2e4f6 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(200).into() } } diff --git a/examples/pane_grid/src/main.rs b/examples/pane_grid/src/main.rs index 17112785..0def1c2b 100644 --- a/examples/pane_grid/src/main.rs +++ b/examples/pane_grid/src/main.rs @@ -292,10 +292,8 @@ fn view_content<'a>( .align_items(Alignment::Center); container(scrollable(content)) - .width(Length::Fill) - .height(Length::Fill) + .center_y(Length::Fill) .padding(5) - .center_y() .into() } diff --git a/examples/pokedex/src/main.rs b/examples/pokedex/src/main.rs index 35f23236..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() } } 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 5c175ccc..bd670322 100644 --- a/examples/screenshot/src/main.rs +++ b/examples/screenshot/src/main.rs @@ -123,12 +123,9 @@ impl Example { }; let image = container(image) + .center_y(Length::FillPortion(2)) .padding(10) - .style(container::rounded_box) - .width(Length::FillPortion(2)) - .height(Length::Fill) - .center_x() - .center_y(); + .style(container::rounded_box); let crop_origin_controls = row![ text("X:") @@ -213,12 +210,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(Length::Fill); let content = row![side_content, image] .spacing(10) @@ -226,13 +218,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..0b4c29aa 100644 --- a/examples/slider/src/main.rs +++ b/examples/slider/src/main.rs @@ -1,4 +1,4 @@ -use iced::widget::{column, container, slider, text, vertical_slider}; +use iced::widget::{center, column, container, slider, text, vertical_slider}; use iced::{Element, Length}; pub fn main() -> iced::Result { @@ -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(Length::Fill), + container(h_slider).center_x(Length::Fill), + container(text).center_x(Length::Fill) ] .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..e071c3af 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(Length::Fill)] + .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..66bc4979 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::widget::{button, center, column, text}; +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() + center(content).into() } } 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..e7e05b29 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,10 @@ impl Todos { .spacing(20) .max_width(800); - scrollable(container(content).padding(40).center_x()).into() + scrollable( + container(content).center_x(Length::Fill).padding(40), + ) + .into() } } } @@ -435,19 +438,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 +455,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..59107258 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(), + .center_x(Length::Fill), ); - container(scrollable).height(Length::Fill).center_y().into() + container(scrollable).center_y(Length::Fill).into() } } @@ -670,8 +669,7 @@ fn ferris<'a>( .filter_method(filter_method) .width(width), ) - .width(Length::Fill) - .center_x() + .center_x(Length::Fill) } fn padded_button<Message: Clone>(label: &str) -> Button<'_, Message> { 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/graphics/src/image.rs b/graphics/src/image.rs index 04c45057..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), } } } diff --git a/renderer/src/fallback.rs b/renderer/src/fallback.rs index 5f69b420..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) + ); } } diff --git a/runtime/src/window.rs b/runtime/src/window.rs index 24171e3e..e32465d3 100644 --- a/runtime/src/window.rs +++ b/runtime/src/window.rs @@ -197,7 +197,7 @@ pub fn change_icon<Message>(id: Id, icon: Icon) -> Command<Message> { /// Note that if the window closes before this call is processed the callback will not be run. pub fn run_with_handle<Message>( id: Id, - f: impl FnOnce(&WindowHandle<'_>) -> Message + 'static, + f: impl FnOnce(WindowHandle<'_>) -> Message + 'static, ) -> Command<Message> { Command::single(command::Action::Window(Action::RunWithHandle( id, diff --git a/runtime/src/window/action.rs b/runtime/src/window/action.rs index e44ff5a6..07e77872 100644 --- a/runtime/src/window/action.rs +++ b/runtime/src/window/action.rs @@ -106,7 +106,7 @@ pub enum Action<T> { /// said, it's usually in the same ballpark as on Windows. ChangeIcon(Id, Icon), /// Runs the closure with the native window handle of the window with the given [`Id`]. - RunWithHandle(Id, Box<dyn FnOnce(&WindowHandle<'_>) -> T + 'static>), + RunWithHandle(Id, Box<dyn FnOnce(WindowHandle<'_>) -> T + 'static>), /// Screenshot the viewport of the window. Screenshot(Id, Box<dyn FnOnce(Screenshot) -> T + 'static>), } 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)?) } } @@ -200,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/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/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 907fce7c..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, 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/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/image/atlas.rs b/wgpu/src/image/atlas.rs index ae43c3b4..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 { @@ -421,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/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 8b831a3c..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::debug!("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/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..ad88ce3e 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; @@ -78,15 +79,17 @@ pub struct Renderer { triangle_storage: triangle::Storage, text_storage: text::Storage, + text_viewport: text::Viewport, // 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( - _engine: &Engine, + device: &wgpu::Device, + engine: &Engine, default_font: Font, default_text_size: Pixels, ) -> Self { @@ -97,9 +100,12 @@ impl Renderer { triangle_storage: triangle::Storage::new(), text_storage: text::Storage::new(), + text_viewport: engine.text_pipeline.create_viewport(device), #[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 +127,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( @@ -134,6 +143,8 @@ impl Renderer { ) { let scale_factor = viewport.scale_factor() as f32; + self.text_viewport.update(queue, viewport.physical_size()); + for layer in self.layers.iter_mut() { if !layer.quads.is_empty() { engine.quad_pipeline.prepare( @@ -175,12 +186,12 @@ impl Renderer { engine.text_pipeline.prepare( device, queue, + &self.text_viewport, encoder, &mut self.text_storage, &layer.text, layer.bounds, Transformation::scale(scale_factor), - viewport.physical_size(), ); } @@ -190,6 +201,7 @@ impl Renderer { device, encoder, &mut engine.staging_belt, + &mut self.image_cache.borrow_mut(), &layer.images, viewport.projection(), scale_factor, @@ -245,6 +257,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( @@ -347,6 +361,7 @@ impl Renderer { if !layer.text.is_empty() { text_layer += engine.text_pipeline.render( + &self.text_viewport, &self.text_storage, text_layer, &layer.text, @@ -358,6 +373,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 +394,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 +524,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 +532,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 +558,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/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..0eeb100f 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 = round(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 7e683c77..05db5f80 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -113,12 +113,13 @@ impl Storage { &mut self, device: &wgpu::Device, queue: &wgpu::Queue, + viewport: &glyphon::Viewport, encoder: &mut wgpu::CommandEncoder, format: wgpu::TextureFormat, + state: &glyphon::Cache, cache: &Cache, new_transformation: Transformation, bounds: Rectangle, - target_size: Size<u32>, ) { let group_count = self.groups.len(); @@ -131,7 +132,7 @@ impl Storage { Group { atlas: glyphon::TextAtlas::with_color_mode( - device, queue, format, COLOR_MODE, + device, queue, state, format, COLOR_MODE, ), version: 0, should_trim: false, @@ -151,6 +152,7 @@ impl Storage { let _ = prepare( device, queue, + viewport, encoder, &mut upload.renderer, &mut group.atlas, @@ -158,7 +160,6 @@ impl Storage { &cache.text, bounds, new_transformation, - target_size, ); } @@ -188,6 +189,7 @@ impl Storage { let _ = prepare( device, queue, + viewport, encoder, &mut renderer, &mut group.atlas, @@ -195,7 +197,6 @@ impl Storage { &cache.text, bounds, new_transformation, - target_size, ); } @@ -257,8 +258,23 @@ impl Storage { } } +pub struct Viewport(glyphon::Viewport); + +impl Viewport { + pub fn update(&mut self, queue: &wgpu::Queue, resolution: Size<u32>) { + self.0.update( + queue, + glyphon::Resolution { + width: resolution.width, + height: resolution.height, + }, + ); + } +} + #[allow(missing_debug_implementations)] pub struct Pipeline { + state: glyphon::Cache, format: wgpu::TextureFormat, atlas: glyphon::TextAtlas, renderers: Vec<glyphon::TextRenderer>, @@ -272,12 +288,16 @@ impl Pipeline { queue: &wgpu::Queue, format: wgpu::TextureFormat, ) -> Self { + let state = glyphon::Cache::new(device); + let atlas = glyphon::TextAtlas::with_color_mode( + device, queue, &state, format, COLOR_MODE, + ); + Pipeline { + state, format, renderers: Vec::new(), - atlas: glyphon::TextAtlas::with_color_mode( - device, queue, format, COLOR_MODE, - ), + atlas, prepare_layer: 0, cache: BufferCache::new(), } @@ -287,12 +307,12 @@ impl Pipeline { &mut self, device: &wgpu::Device, queue: &wgpu::Queue, + viewport: &Viewport, encoder: &mut wgpu::CommandEncoder, storage: &mut Storage, batch: &Batch, layer_bounds: Rectangle, layer_transformation: Transformation, - target_size: Size<u32>, ) { for item in batch { match item { @@ -313,6 +333,7 @@ impl Pipeline { let result = prepare( device, queue, + &viewport.0, encoder, renderer, &mut self.atlas, @@ -320,7 +341,6 @@ impl Pipeline { text, layer_bounds * layer_transformation, layer_transformation * *transformation, - target_size, ); match result { @@ -341,12 +361,13 @@ impl Pipeline { storage.prepare( device, queue, + &viewport.0, encoder, self.format, + &self.state, cache, layer_transformation * *transformation, layer_bounds * layer_transformation, - target_size, ); } } @@ -355,6 +376,7 @@ impl Pipeline { pub fn render<'a>( &'a self, + viewport: &'a Viewport, storage: &'a Storage, start: usize, batch: &'a Batch, @@ -376,7 +398,7 @@ impl Pipeline { let renderer = &self.renderers[start + layer_count]; renderer - .render(&self.atlas, render_pass) + .render(&self.atlas, &viewport.0, render_pass) .expect("Render text"); layer_count += 1; @@ -385,7 +407,7 @@ impl Pipeline { if let Some((atlas, upload)) = storage.get(cache) { upload .renderer - .render(atlas, render_pass) + .render(atlas, &viewport.0, render_pass) .expect("Render cached text"); } } @@ -395,6 +417,10 @@ impl Pipeline { layer_count } + pub fn create_viewport(&self, device: &wgpu::Device) -> Viewport { + Viewport(glyphon::Viewport::new(device, &self.state)) + } + pub fn end_frame(&mut self) { self.atlas.trim(); self.cache.trim(); @@ -406,6 +432,7 @@ impl Pipeline { fn prepare( device: &wgpu::Device, queue: &wgpu::Queue, + viewport: &glyphon::Viewport, encoder: &mut wgpu::CommandEncoder, renderer: &mut glyphon::TextRenderer, atlas: &mut glyphon::TextAtlas, @@ -413,7 +440,6 @@ fn prepare( sections: &[Text], layer_bounds: Rectangle, layer_transformation: Transformation, - target_size: Size<u32>, ) -> Result<(), glyphon::PrepareError> { let mut font_system = font_system().write().expect("Write font system"); let font_system = font_system.raw(); @@ -610,10 +636,7 @@ fn prepare( encoder, font_system, atlas, - glyphon::Resolution { - width: target_size.width, - height: target_size.height, - }, + viewport, text_areas, &mut glyphon::SwashCache::new(), ) 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/container.rs b/widget/src/container.rs index 21405722..51967707 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -92,6 +92,46 @@ where self } + /// Sets the [`Container`] to fill the available space in the horizontal axis. + /// + /// This can be useful to quickly position content when chained with + /// alignment functions—like [`center_x`]. + /// + /// Calling this method is equivalent to calling [`width`] with a + /// [`Length::Fill`]. + /// + /// [`center_x`]: Self::center_x + /// [`width`]: Self::width + pub fn fill_x(self) -> Self { + self.width(Length::Fill) + } + + /// Sets the [`Container`] to fill the available space in the vetical axis. + /// + /// This can be useful to quickly position content when chained with + /// alignment functions—like [`center_y`]. + /// + /// Calling this method is equivalent to calling [`height`] with a + /// [`Length::Fill`]. + /// + /// [`center_y`]: Self::center_x + /// [`height`]: Self::height + pub fn fill_y(self) -> Self { + self.height(Length::Fill) + } + + /// Sets the [`Container`] to fill all the available space. + /// + /// Calling this method is equivalent to chaining [`fill_x`] and + /// [`fill_y`]. + /// + /// [`center`]: Self::center + /// [`fill_x`]: Self::fill_x + /// [`fill_y`]: Self::fill_y + pub fn fill(self) -> Self { + self.width(Length::Fill).height(Length::Fill) + } + /// Sets the maximum width of the [`Container`]. pub fn max_width(mut self, max_width: impl Into<Pixels>) -> Self { self.max_width = max_width.into().0; @@ -116,16 +156,27 @@ where self } - /// Centers the contents in the horizontal axis of the [`Container`]. - pub fn center_x(mut self) -> Self { - self.horizontal_alignment = alignment::Horizontal::Center; - self + /// Sets the width of the [`Container`] and centers its contents horizontally. + pub fn center_x(self, width: impl Into<Length>) -> Self { + self.width(width).align_x(alignment::Horizontal::Center) } - /// Centers the contents in the vertical axis of the [`Container`]. - pub fn center_y(mut self) -> Self { - self.vertical_alignment = alignment::Vertical::Center; - self + /// Sets the height of the [`Container`] and centers its contents vertically. + pub fn center_y(self, height: impl Into<Length>) -> Self { + self.height(height).align_y(alignment::Vertical::Center) + } + + /// 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, length: impl Into<Length>) -> Self { + let length = length.into(); + + self.center_x(length).center_y(length) } /// Sets whether the contents of the [`Container`] should be clipped on diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 48c5dde4..a1ecd9d1 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -78,6 +78,28 @@ 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::core::Length; +/// # use iced_widget::Container; +/// # fn container<A>(x: A) -> Container<'static, ()> { unreachable!() } +/// let centered = container("Centered!").center(Length::Fill); +/// ``` +/// +/// [`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).center(Length::Fill) +} + /// Creates a new [`Column`] with the given children. pub fn column<'a, Message, Theme, Renderer>( children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>, diff --git a/widget/src/image.rs b/widget/src/image.rs index 21d371b7..80e17263 100644 --- a/widget/src/image.rs +++ b/widget/src/image.rs @@ -8,7 +8,8 @@ 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, }; pub use image::{FilterMethod, Handle}; @@ -36,6 +37,8 @@ pub struct Image<Handle> { height: Length, content_fit: ContentFit, filter_method: FilterMethod, + rotation: Rotation, + opacity: f32, } impl<Handle> Image<Handle> { @@ -45,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, } } @@ -75,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`]. @@ -85,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 { @@ -124,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, { 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, ); }; @@ -187,6 +223,7 @@ where self.width, self.height, self.content_fit, + self.rotation, ) } @@ -206,6 +243,8 @@ where &self.handle, self.content_fit, self.filter_method, + self.rotation, + self.opacity, ); } } diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs index 2e6a7528..8fe6f021 100644 --- a/widget/src/image/viewer.rs +++ b/widget/src/image/viewer.rs @@ -6,8 +6,8 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - Clipboard, Element, Layout, Length, Pixels, Point, Rectangle, Shell, Size, - Vector, Widget, + Clipboard, Element, Layout, Length, Pixels, Point, Radians, Rectangle, + Shell, Size, Vector, Widget, }; /// A frame that displays an image with the ability to zoom in/out and pan. @@ -341,6 +341,8 @@ where y: bounds.y, ..Rectangle::with_size(image_size) }, + Radians(0.0), + 1.0, ); }); }); 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/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 a447c9da..f7508b4c 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,12 +143,12 @@ 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"); @@ -165,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), + ); + } - let (mut event_sender, event_receiver) = mpsc::unbounded(); - let (control_sender, mut control_receiver) = mpsc::unbounded(); + 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 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 user_event( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + message: Message, + ) { + self.process_event( + event_loop, + winit::event::Event::UserEvent(message), + ); + } + + 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); } } @@ -268,54 +450,45 @@ where event_loop.exit(); } } - }; + } + } + + #[cfg(not(target_arch = "wasm32"))] + { + let mut runner = runner; + let _ = event_loop.run_app(&mut runner); - #[cfg(not(target_os = "windows"))] - let _ = event_loop.run(process_event); + 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: 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, @@ -326,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(); @@ -480,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, )); @@ -864,7 +1050,7 @@ pub fn run_command<A, C, E>( use window::raw_window_handle::HasWindowHandle; if let Ok(handle) = window.window_handle() { - proxy.send(tag(&handle)); + proxy.send(tag(handle)); } } diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index 0f83dac3..ea33e610 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. @@ -327,6 +323,35 @@ pub fn position( y: f64::from(position.y), })) } + window::Position::SpecificWith(to_position) => { + if let Some(monitor) = monitor { + let start = monitor.position(); + + let resolution: winit::dpi::LogicalSize<f32> = + monitor.size().to_logical(monitor.scale_factor()); + + let position = to_position( + size, + Size::new(resolution.width, resolution.height), + ); + + let centered: winit::dpi::PhysicalPosition<i32> = + winit::dpi::LogicalPosition { + x: position.x, + y: position.y, + } + .to_physical(monitor.scale_factor()); + + Some(winit::dpi::Position::Physical( + winit::dpi::PhysicalPosition { + x: start.x + centered.x, + y: start.y + centered.y, + }, + )) + } else { + None + } + } window::Position::Centered => { if let Some(monitor) = monitor { let start = monitor.position(); diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 2c148031..95d78b83 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,12 +115,12 @@ 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"); @@ -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>, + } - #[cfg(target_arch = "wasm32")] + struct BootConfig<C> { + sender: oneshot::Sender<Boot<C>>, + id: Option<String>, + title: String, + window_settings: window::Settings, + graphics_settings: graphics::Settings, + } + + 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)), + ); + } + + 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 mut context = task::Context::from_waker(task::noop_waker_ref()); + 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, + )), + ); + } - let process_event = move |event, event_loop: &winit::event_loop::EventLoopWindowTarget<_>| { - if event_loop.exiting() { - return; + fn about_to_wait( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + ) { + self.process_event( + event_loop, + Event::EventLoopAwakened(winit::event::Event::AboutToWait), + ); } + } - 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); + 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: 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, + &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); } @@ -532,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, ), @@ -603,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, ), @@ -1102,7 +1223,7 @@ fn run_command<A, C, E>( .get_mut(id) .and_then(|window| window.raw.window_handle().ok()) { - proxy.send(tag(&handle)); + proxy.send(tag(handle)); } } window::Action::Screenshot(id, tag) => { diff --git a/winit/src/multi_window/window_manager.rs b/winit/src/multi_window/window_manager.rs index 3a0c8556..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, { |