From f962c6ce0671b4b0b0e683148353fc3a3d1fc3c3 Mon Sep 17 00:00:00 2001 From: mtkennerly Date: Mon, 15 Apr 2024 08:41:29 -0400 Subject: [PATCH 01/66] Allow checking whether a TextEditor is focused --- widget/src/text_editor.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 92cdb2513a..5b0b1b79c2 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -319,7 +319,9 @@ where } } -struct State { +/// The state of a [`TextEditor`]. +#[derive(Debug)] +pub struct State { is_focused: bool, last_click: Option, drag_click: Option, @@ -329,6 +331,13 @@ struct State { highlighter_format_address: usize, } +impl State { + /// Returns whether the [`TextEditor`] is currently focused or not. + pub fn is_focused(&self) -> bool { + self.is_focused + } +} + impl<'a, Highlighter, Message, Theme, Renderer> Widget for TextEditor<'a, Highlighter, Message, Theme, Renderer> where From e8ec6b94b68801ce4e95ada7c311320469f92a96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 16 Apr 2024 20:22:57 +0200 Subject: [PATCH 02/66] Use `Skip` as the `MissedTickBehavior` for `tokio` futures backend --- futures/src/backend/native/tokio.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/futures/src/backend/native/tokio.rs b/futures/src/backend/native/tokio.rs index 3ab7f6757a..df91d7984e 100644 --- a/futures/src/backend/native/tokio.rs +++ b/futures/src/backend/native/tokio.rs @@ -55,13 +55,15 @@ pub mod time { let start = tokio::time::Instant::now() + self.0; + let mut interval = tokio::time::interval_at(start, self.0); + interval.set_missed_tick_behavior( + tokio::time::MissedTickBehavior::Skip, + ); + let stream = { - futures::stream::unfold( - tokio::time::interval_at(start, self.0), - |mut interval| async move { - Some((interval.tick().await, interval)) - }, - ) + futures::stream::unfold(interval, |mut interval| async move { + Some((interval.tick().await, interval)) + }) }; stream.map(tokio::time::Instant::into_std).boxed() From b6b51375cfd96e330d6ee22096dacf831a992aa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 16 Apr 2024 21:50:28 +0200 Subject: [PATCH 03/66] Implement backpressure mechanism in `iced_winit::Proxy` --- winit/src/application.rs | 79 +++++++++++----------------- winit/src/multi_window.rs | 77 ++++++++++----------------- winit/src/proxy.rs | 108 ++++++++++++++++++++++++++++++++------ 3 files changed, 152 insertions(+), 112 deletions(-) diff --git a/winit/src/application.rs b/winit/src/application.rs index 1ca80609fa..a447c9da05 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -149,13 +149,14 @@ where let event_loop = EventLoopBuilder::with_user_event() .build() .expect("Create event loop"); - let proxy = event_loop.create_proxy(); + + let (proxy, worker) = Proxy::new(event_loop.create_proxy()); let runtime = { - let proxy = Proxy::new(event_loop.create_proxy()); let executor = E::new().map_err(Error::ExecutorCreationFailed)?; + executor.spawn(worker); - Runtime::new(executor, proxy) + Runtime::new(executor, proxy.clone()) }; let (application, init_command) = { @@ -305,7 +306,7 @@ async fn run_instance( mut compositor: C, mut renderer: A::Renderer, mut runtime: Runtime, A::Message>, - mut proxy: winit::event_loop::EventLoopProxy, + mut proxy: Proxy, mut debug: Debug, mut event_receiver: mpsc::UnboundedReceiver< winit::event::Event, @@ -370,6 +371,7 @@ async fn run_instance( let mut mouse_interaction = mouse::Interaction::default(); let mut events = Vec::new(); let mut messages = Vec::new(); + let mut user_events = 0; let mut redraw_pending = false; debug.startup_finished(); @@ -396,6 +398,7 @@ async fn run_instance( } event::Event::UserEvent(message) => { messages.push(message); + user_events += 1; } event::Event::WindowEvent { event: event::WindowEvent::RedrawRequested { .. }, @@ -593,6 +596,11 @@ async fn run_instance( if should_exit { break; } + + if user_events > 0 { + proxy.free_slots(user_events); + user_events = 0; + } } if !redraw_pending { @@ -667,7 +675,7 @@ pub fn update( runtime: &mut Runtime, A::Message>, clipboard: &mut Clipboard, should_exit: &mut bool, - proxy: &mut winit::event_loop::EventLoopProxy, + proxy: &mut Proxy, debug: &mut Debug, messages: &mut Vec, window: &winit::window::Window, @@ -717,7 +725,7 @@ pub fn run_command( runtime: &mut Runtime, A::Message>, clipboard: &mut Clipboard, should_exit: &mut bool, - proxy: &mut winit::event_loop::EventLoopProxy, + proxy: &mut Proxy, debug: &mut Debug, window: &winit::window::Window, ) where @@ -742,9 +750,7 @@ pub fn run_command( clipboard::Action::Read(tag, kind) => { let message = tag(clipboard.read(kind)); - proxy - .send_event(message) - .expect("Send message to event loop"); + proxy.send(message); } clipboard::Action::Write(contents, kind) => { clipboard.write(kind, contents); @@ -774,25 +780,16 @@ pub fn run_command( let size = window.inner_size().to_logical(window.scale_factor()); - proxy - .send_event(callback(Size::new( - size.width, - size.height, - ))) - .expect("Send message to event loop"); + proxy.send(callback(Size::new(size.width, size.height))); } window::Action::FetchMaximized(_id, callback) => { - proxy - .send_event(callback(window.is_maximized())) - .expect("Send message to event loop"); + proxy.send(callback(window.is_maximized())); } window::Action::Maximize(_id, maximized) => { window.set_maximized(maximized); } window::Action::FetchMinimized(_id, callback) => { - proxy - .send_event(callback(window.is_minimized())) - .expect("Send message to event loop"); + proxy.send(callback(window.is_minimized())); } window::Action::Minimize(_id, minimized) => { window.set_minimized(minimized); @@ -808,9 +805,7 @@ pub fn run_command( }) .ok(); - proxy - .send_event(callback(position)) - .expect("Send message to event loop"); + proxy.send(callback(position)); } window::Action::Move(_id, position) => { window.set_outer_position(winit::dpi::LogicalPosition { @@ -835,9 +830,7 @@ pub fn run_command( core::window::Mode::Hidden }; - proxy - .send_event(tag(mode)) - .expect("Send message to event loop"); + proxy.send(tag(mode)); } window::Action::ToggleMaximize(_id) => { window.set_maximized(!window.is_maximized()); @@ -865,17 +858,13 @@ pub fn run_command( } } window::Action::FetchId(_id, tag) => { - proxy - .send_event(tag(window.id().into())) - .expect("Send message to event loop"); + proxy.send(tag(window.id().into())); } window::Action::RunWithHandle(_id, tag) => { use window::raw_window_handle::HasWindowHandle; if let Ok(handle) = window.window_handle() { - proxy - .send_event(tag(&handle)) - .expect("Send message to event loop"); + proxy.send(tag(&handle)); } } @@ -888,12 +877,10 @@ pub fn run_command( &debug.overlay(), ); - proxy - .send_event(tag(window::Screenshot::new( - bytes, - state.physical_size(), - ))) - .expect("Send message to event loop."); + proxy.send(tag(window::Screenshot::new( + bytes, + state.physical_size(), + ))); } }, command::Action::System(action) => match action { @@ -901,7 +888,7 @@ pub fn run_command( #[cfg(feature = "system")] { let graphics_info = compositor.fetch_information(); - let proxy = proxy.clone(); + let mut proxy = proxy.clone(); let _ = std::thread::spawn(move || { let information = @@ -909,9 +896,7 @@ pub fn run_command( let message = _tag(information); - proxy - .send_event(message) - .expect("Send message to event loop"); + proxy.send(message); }); } } @@ -934,9 +919,7 @@ pub fn run_command( match operation.finish() { operation::Outcome::None => {} operation::Outcome::Some(message) => { - proxy - .send_event(message) - .expect("Send message to event loop"); + proxy.send(message); } operation::Outcome::Chain(next) => { current_operation = Some(next); @@ -951,9 +934,7 @@ pub fn run_command( // TODO: Error handling (?) compositor.load_font(bytes); - proxy - .send_event(tagger(Ok(()))) - .expect("Send message to event loop"); + proxy.send(tagger(Ok(()))); } command::Action::Custom(_) => { log::warn!("Unsupported custom action in `iced_winit` shell"); diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 3537ac18ef..f832eb8102 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -123,13 +123,13 @@ where .build() .expect("Create event loop"); - let proxy = event_loop.create_proxy(); + let (proxy, worker) = Proxy::new(event_loop.create_proxy()); let runtime = { - let proxy = Proxy::new(event_loop.create_proxy()); let executor = E::new().map_err(Error::ExecutorCreationFailed)?; + executor.spawn(worker); - Runtime::new(executor, proxy) + Runtime::new(executor, proxy.clone()) }; let (application, init_command) = { @@ -343,7 +343,7 @@ async fn run_instance( mut application: A, mut compositor: C, mut runtime: Runtime, A::Message>, - mut proxy: winit::event_loop::EventLoopProxy, + mut proxy: Proxy, mut debug: Debug, mut event_receiver: mpsc::UnboundedReceiver>, mut control_sender: mpsc::UnboundedSender, @@ -408,6 +408,7 @@ async fn run_instance( runtime.track(application.subscription().into_recipes()); let mut messages = Vec::new(); + let mut user_events = 0; debug.startup_finished(); @@ -482,6 +483,7 @@ async fn run_instance( } event::Event::UserEvent(message) => { messages.push(message); + user_events += 1; } event::Event::WindowEvent { window_id: id, @@ -803,6 +805,11 @@ async fn run_instance( &mut window_manager, cached_interfaces, )); + + if user_events > 0 { + proxy.free_slots(user_events); + user_events = 0; + } } } _ => {} @@ -845,7 +852,7 @@ fn update( runtime: &mut Runtime, A::Message>, clipboard: &mut Clipboard, control_sender: &mut mpsc::UnboundedSender, - proxy: &mut winit::event_loop::EventLoopProxy, + proxy: &mut Proxy, debug: &mut Debug, messages: &mut Vec, window_manager: &mut WindowManager, @@ -887,7 +894,7 @@ fn run_command( runtime: &mut Runtime, A::Message>, clipboard: &mut Clipboard, control_sender: &mut mpsc::UnboundedSender, - proxy: &mut winit::event_loop::EventLoopProxy, + proxy: &mut Proxy, debug: &mut Debug, window_manager: &mut WindowManager, ui_caches: &mut FxHashMap, @@ -913,9 +920,7 @@ fn run_command( clipboard::Action::Read(tag, kind) => { let message = tag(clipboard.read(kind)); - proxy - .send_event(message) - .expect("Send message to event loop"); + proxy.send(message); } clipboard::Action::Write(contents, kind) => { clipboard.write(kind, contents); @@ -967,18 +972,12 @@ fn run_command( .to_logical(window.raw.scale_factor()); proxy - .send_event(callback(Size::new( - size.width, - size.height, - ))) - .expect("Send message to event loop"); + .send(callback(Size::new(size.width, size.height))); } } window::Action::FetchMaximized(id, callback) => { if let Some(window) = window_manager.get_mut(id) { - proxy - .send_event(callback(window.raw.is_maximized())) - .expect("Send message to event loop"); + proxy.send(callback(window.raw.is_maximized())); } } window::Action::Maximize(id, maximized) => { @@ -988,9 +987,7 @@ fn run_command( } window::Action::FetchMinimized(id, callback) => { if let Some(window) = window_manager.get_mut(id) { - proxy - .send_event(callback(window.raw.is_minimized())) - .expect("Send message to event loop"); + proxy.send(callback(window.raw.is_minimized())); } } window::Action::Minimize(id, minimized) => { @@ -1012,9 +1009,7 @@ fn run_command( }) .ok(); - proxy - .send_event(callback(position)) - .expect("Send message to event loop"); + proxy.send(callback(position)); } } window::Action::Move(id, position) => { @@ -1049,9 +1044,7 @@ fn run_command( core::window::Mode::Hidden }; - proxy - .send_event(tag(mode)) - .expect("Send message to event loop"); + proxy.send(tag(mode)); } } window::Action::ToggleMaximize(id) => { @@ -1099,9 +1092,7 @@ fn run_command( } window::Action::FetchId(id, tag) => { if let Some(window) = window_manager.get_mut(id) { - proxy - .send_event(tag(window.raw.id().into())) - .expect("Send message to event loop"); + proxy.send(tag(window.raw.id().into())); } } window::Action::RunWithHandle(id, tag) => { @@ -1111,9 +1102,7 @@ fn run_command( .get_mut(id) .and_then(|window| window.raw.window_handle().ok()) { - proxy - .send_event(tag(&handle)) - .expect("Send message to event loop"); + proxy.send(tag(&handle)); } } window::Action::Screenshot(id, tag) => { @@ -1126,12 +1115,10 @@ fn run_command( &debug.overlay(), ); - proxy - .send_event(tag(window::Screenshot::new( - bytes, - window.state.physical_size(), - ))) - .expect("Event loop doesn't exist."); + proxy.send(tag(window::Screenshot::new( + bytes, + window.state.physical_size(), + ))); } } }, @@ -1140,7 +1127,7 @@ fn run_command( #[cfg(feature = "system")] { let graphics_info = compositor.fetch_information(); - let proxy = proxy.clone(); + let mut proxy = proxy.clone(); let _ = std::thread::spawn(move || { let information = @@ -1148,9 +1135,7 @@ fn run_command( let message = _tag(information); - proxy - .send_event(message) - .expect("Event loop doesn't exist."); + proxy.send(message); }); } } @@ -1175,9 +1160,7 @@ fn run_command( match operation.finish() { operation::Outcome::None => {} operation::Outcome::Some(message) => { - proxy - .send_event(message) - .expect("Event loop doesn't exist."); + proxy.send(message); // operation completed, don't need to try to operate on rest of UIs break 'operate; @@ -1197,9 +1180,7 @@ fn run_command( // TODO: Error handling (?) compositor.load_font(bytes.clone()); - proxy - .send_event(tagger(Ok(()))) - .expect("Send message to event loop"); + proxy.send(tagger(Ok(()))); } command::Action::Custom(_) => { log::warn!("Unsupported custom action in `iced_winit` shell"); diff --git a/winit/src/proxy.rs b/winit/src/proxy.rs index 1d6c48bb25..a35e8a27f5 100644 --- a/winit/src/proxy.rs +++ b/winit/src/proxy.rs @@ -1,28 +1,101 @@ use crate::futures::futures::{ channel::mpsc, + stream, task::{Context, Poll}, - Sink, + Future, Sink, StreamExt, }; use std::pin::Pin; -/// An event loop proxy that implements `Sink`. +/// An event loop proxy with backpressure that implements `Sink`. #[derive(Debug)] pub struct Proxy { raw: winit::event_loop::EventLoopProxy, + sender: mpsc::Sender, + notifier: mpsc::Sender, } impl Clone for Proxy { fn clone(&self) -> Self { Self { raw: self.raw.clone(), + sender: self.sender.clone(), + notifier: self.notifier.clone(), } } } impl Proxy { + const MAX_SIZE: usize = 100; + /// Creates a new [`Proxy`] from an `EventLoopProxy`. - pub fn new(raw: winit::event_loop::EventLoopProxy) -> Self { - Self { raw } + pub fn new( + raw: winit::event_loop::EventLoopProxy, + ) -> (Self, impl Future) { + let (notifier, processed) = mpsc::channel(Self::MAX_SIZE); + let (sender, receiver) = mpsc::channel(Self::MAX_SIZE); + let proxy = raw.clone(); + + let worker = async move { + enum Item { + MessageProduced(T), + BatchProcessed(usize), + } + + let mut receiver = receiver.map(Item::MessageProduced); + let mut processed = processed.map(Item::BatchProcessed); + + let mut count = 0; + + loop { + if count < Self::MAX_SIZE { + let mut stream = + stream::select(receiver.by_ref(), processed.by_ref()); + + match stream.select_next_some().await { + Item::MessageProduced(message) => { + let _ = proxy.send_event(message); + + count += 1; + } + Item::BatchProcessed(amount) => { + count = count.saturating_sub(amount); + } + } + } else if let Item::BatchProcessed(amount) = + processed.select_next_some().await + { + count = count.saturating_sub(amount); + } + } + }; + + ( + Self { + raw, + sender, + notifier, + }, + worker, + ) + } + + /// Sends a `Message` to the event loop. + /// + /// Note: This skips the backpressure mechanism with an unbounded + /// channel. Use sparingly! + pub fn send(&mut self, message: Message) + where + Message: std::fmt::Debug, + { + self.raw + .send_event(message) + .expect("Send message to event loop"); + } + + /// Frees an amount of slots for additional messages to be queued in + /// this [`Proxy`]. + pub fn free_slots(&mut self, amount: usize) { + let _ = self.notifier.start_send(amount); } } @@ -30,32 +103,37 @@ impl Sink for Proxy { type Error = mpsc::SendError; fn poll_ready( - self: Pin<&mut Self>, - _cx: &mut Context<'_>, + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, ) -> Poll> { - Poll::Ready(Ok(())) + self.sender.poll_ready(cx) } fn start_send( - self: Pin<&mut Self>, + mut self: Pin<&mut Self>, message: Message, ) -> Result<(), Self::Error> { - let _ = self.raw.send_event(message); - - Ok(()) + self.sender.start_send(message) } fn poll_flush( - self: Pin<&mut Self>, - _cx: &mut Context<'_>, + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, ) -> Poll> { - Poll::Ready(Ok(())) + match self.sender.poll_ready(cx) { + Poll::Ready(Err(ref e)) if e.is_disconnected() => { + // If the receiver disconnected, we consider the sink to be flushed. + Poll::Ready(Ok(())) + } + x => x, + } } fn poll_close( - self: Pin<&mut Self>, + mut self: Pin<&mut Self>, _cx: &mut Context<'_>, ) -> Poll> { + self.sender.disconnect(); Poll::Ready(Ok(())) } } From a05b8044a9a82c1802d4d97f1723e24b9d9dad9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 17 Apr 2024 15:54:12 +0200 Subject: [PATCH 04/66] Fix `SelectNextSome` poll after termination panic in `iced_winit::Proxy` --- winit/src/proxy.rs | 37 +++++++++++++++---------------------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/winit/src/proxy.rs b/winit/src/proxy.rs index a35e8a27f5..3edc30ad5e 100644 --- a/winit/src/proxy.rs +++ b/winit/src/proxy.rs @@ -1,6 +1,6 @@ use crate::futures::futures::{ channel::mpsc, - stream, + select, task::{Context, Poll}, Future, Sink, StreamExt, }; @@ -31,40 +31,33 @@ impl Proxy { pub fn new( raw: winit::event_loop::EventLoopProxy, ) -> (Self, impl Future) { - let (notifier, processed) = mpsc::channel(Self::MAX_SIZE); - let (sender, receiver) = mpsc::channel(Self::MAX_SIZE); + let (notifier, mut processed) = mpsc::channel(Self::MAX_SIZE); + let (sender, mut receiver) = mpsc::channel(Self::MAX_SIZE); let proxy = raw.clone(); let worker = async move { - enum Item { - MessageProduced(T), - BatchProcessed(usize), - } - - let mut receiver = receiver.map(Item::MessageProduced); - let mut processed = processed.map(Item::BatchProcessed); - let mut count = 0; loop { if count < Self::MAX_SIZE { - let mut stream = - stream::select(receiver.by_ref(), processed.by_ref()); - - match stream.select_next_some().await { - Item::MessageProduced(message) => { + select! { + message = receiver.select_next_some() => { let _ = proxy.send_event(message); - count += 1; + + } + amount = processed.select_next_some() => { + count = count.saturating_sub(amount); } - Item::BatchProcessed(amount) => { + complete => break, + } + } else { + select! { + amount = processed.select_next_some() => { count = count.saturating_sub(amount); } + complete => break, } - } else if let Item::BatchProcessed(amount) = - processed.select_next_some().await - { - count = count.saturating_sub(amount); } } }; From f3a2255f830e52d68c6bf0aaebc1e4164d66f2d8 Mon Sep 17 00:00:00 2001 From: saihaze Date: Fri, 19 Apr 2024 14:53:38 +0800 Subject: [PATCH 05/66] Fix typo --- src/window/icon.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/window/icon.rs b/src/window/icon.rs index ef71c22834..7fe4ca7bd1 100644 --- a/src/window/icon.rs +++ b/src/window/icon.rs @@ -54,7 +54,7 @@ pub enum Error { InvalidError(#[from] icon::Error), /// The underlying OS failed to create the icon. - #[error("The underlying OS failted to create the window icon: {0}")] + #[error("The underlying OS failed to create the window icon: {0}")] OsError(#[from] io::Error), /// The `image` crate reported an error. From 67e181ce7bad516d2ff8fc34c989af2435fdc7b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 23 Apr 2024 02:29:04 +0200 Subject: [PATCH 06/66] Fix clip bounds with nested `scrollable` widgets --- widget/src/scrollable.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 668c5372f7..626cd07ffe 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -659,6 +659,10 @@ where let content_layout = layout.children().next().unwrap(); let content_bounds = content_layout.bounds(); + let Some(visible_bounds) = bounds.intersection(viewport) else { + return; + }; + let scrollbars = Scrollbars::new(state, self.direction, bounds, content_bounds); @@ -704,7 +708,7 @@ where // Draw inner content if scrollbars.active() { - renderer.with_layer(bounds, |renderer| { + renderer.with_layer(visible_bounds, |renderer| { renderer.with_translation( Vector::new(-translation.x, -translation.y), |renderer| { @@ -767,9 +771,9 @@ where renderer.with_layer( Rectangle { - width: (bounds.width + 2.0).min(viewport.width), - height: (bounds.height + 2.0).min(viewport.height), - ..bounds + width: (visible_bounds.width + 2.0).min(viewport.width), + height: (visible_bounds.height + 2.0).min(viewport.height), + ..visible_bounds }, |renderer| { if let Some(scrollbar) = scrollbars.y { From fdcec0319757d2f87c82787eab34c6bef8c5a799 Mon Sep 17 00:00:00 2001 From: Daniel <101683475+Koranir@users.noreply.github.com> Date: Tue, 23 Apr 2024 10:45:44 +1000 Subject: [PATCH 07/66] Don't consume unused scroll events (#2397) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Initial Commit * Update scrollable.rs * Use `let _ = ` instead of `_ =` for consistency --------- Co-authored-by: Héctor Ramón Jiménez --- widget/src/scrollable.rs | 84 +++++++++++++++++++++------------------- 1 file changed, 45 insertions(+), 39 deletions(-) diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 626cd07ffe..cdc143a2b2 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -435,15 +435,17 @@ where state.scroll(delta, self.direction, bounds, content_bounds); - notify_on_scroll( + event_status = if notify_on_scroll( state, &self.on_scroll, bounds, content_bounds, shell, - ); - - event_status = event::Status::Captured; + ) { + event::Status::Captured + } else { + event::Status::Ignored + }; } Event::Touch(event) if state.scroll_area_touched_at.is_some() @@ -481,7 +483,8 @@ where state.scroll_area_touched_at = Some(cursor_position); - notify_on_scroll( + // TODO: bubble up touch movements if not consumed. + let _ = notify_on_scroll( state, &self.on_scroll, bounds, @@ -516,7 +519,7 @@ where content_bounds, ); - notify_on_scroll( + let _ = notify_on_scroll( state, &self.on_scroll, bounds, @@ -554,7 +557,7 @@ where state.y_scroller_grabbed_at = Some(scroller_grabbed_at); - notify_on_scroll( + let _ = notify_on_scroll( state, &self.on_scroll, bounds, @@ -587,7 +590,7 @@ where content_bounds, ); - notify_on_scroll( + let _ = notify_on_scroll( state, &self.on_scroll, bounds, @@ -625,7 +628,7 @@ where state.x_scroller_grabbed_at = Some(scroller_grabbed_at); - notify_on_scroll( + let _ = notify_on_scroll( state, &self.on_scroll, bounds, @@ -965,51 +968,54 @@ pub fn scroll_to( Command::widget(operation::scrollable::scroll_to(id.0, offset)) } +/// Returns [`true`] if the viewport actually changed. fn notify_on_scroll( state: &mut State, on_scroll: &Option Message + '_>>, bounds: Rectangle, content_bounds: Rectangle, shell: &mut Shell<'_, Message>, -) { - if let Some(on_scroll) = on_scroll { - if content_bounds.width <= bounds.width - && content_bounds.height <= bounds.height - { - return; - } +) -> bool { + if content_bounds.width <= bounds.width + && content_bounds.height <= bounds.height + { + return false; + } - let viewport = Viewport { - offset_x: state.offset_x, - offset_y: state.offset_y, - bounds, - content_bounds, - }; + let viewport = Viewport { + offset_x: state.offset_x, + offset_y: state.offset_y, + bounds, + content_bounds, + }; - // Don't publish redundant viewports to shell - if let Some(last_notified) = state.last_notified { - let last_relative_offset = last_notified.relative_offset(); - let current_relative_offset = viewport.relative_offset(); + // Don't publish redundant viewports to shell + if let Some(last_notified) = state.last_notified { + let last_relative_offset = last_notified.relative_offset(); + let current_relative_offset = viewport.relative_offset(); - let last_absolute_offset = last_notified.absolute_offset(); - let current_absolute_offset = viewport.absolute_offset(); + let last_absolute_offset = last_notified.absolute_offset(); + let current_absolute_offset = viewport.absolute_offset(); - let unchanged = |a: f32, b: f32| { - (a - b).abs() <= f32::EPSILON || (a.is_nan() && b.is_nan()) - }; + let unchanged = |a: f32, b: f32| { + (a - b).abs() <= f32::EPSILON || (a.is_nan() && b.is_nan()) + }; - if unchanged(last_relative_offset.x, current_relative_offset.x) - && unchanged(last_relative_offset.y, current_relative_offset.y) - && unchanged(last_absolute_offset.x, current_absolute_offset.x) - && unchanged(last_absolute_offset.y, current_absolute_offset.y) - { - return; - } + if unchanged(last_relative_offset.x, current_relative_offset.x) + && unchanged(last_relative_offset.y, current_relative_offset.y) + && unchanged(last_absolute_offset.x, current_absolute_offset.x) + && unchanged(last_absolute_offset.y, current_absolute_offset.y) + { + return false; } + } + if let Some(on_scroll) = on_scroll { shell.publish(on_scroll(viewport)); - state.last_notified = Some(viewport); } + state.last_notified = Some(viewport); + + true } #[derive(Debug, Clone, Copy)] From 493c36ac712ef04523065b94988a88cc4db16b1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 24 Apr 2024 21:29:30 +0200 Subject: [PATCH 08/66] Make image `Cache` eviction strategy less aggressive in `iced_wgpu` Instead of trimming unconditionally at the end of a frame, we now trim the cache only when there is a cache miss. This way, images that are not visible but still a part of the layout will stay cached. Eviction will only happen when the images are not a part of the UI for two consectuive frames. --- wgpu/src/image/atlas.rs | 15 +++++++++++---- wgpu/src/image/atlas/allocator.rs | 4 ++++ wgpu/src/image/atlas/layer.rs | 8 ++++++++ wgpu/src/image/mod.rs | 2 +- wgpu/src/image/raster.rs | 9 +++++++++ wgpu/src/image/vector.rs | 8 ++++++++ 6 files changed, 41 insertions(+), 5 deletions(-) diff --git a/wgpu/src/image/atlas.rs b/wgpu/src/image/atlas.rs index ea36e06d32..ae43c3b4b6 100644 --- a/wgpu/src/image/atlas.rs +++ b/wgpu/src/image/atlas.rs @@ -94,7 +94,7 @@ impl Atlas { entry }; - log::info!("Allocated atlas entry: {entry:?}"); + log::debug!("Allocated atlas entry: {entry:?}"); // It is a webgpu requirement that: // BufferCopyView.layout.bytes_per_row % wgpu::COPY_BYTES_PER_ROW_ALIGNMENT == 0 @@ -147,13 +147,20 @@ impl Atlas { } } - log::info!("Current atlas: {self:?}"); + if log::log_enabled!(log::Level::Debug) { + log::debug!( + "Atlas layers: {} (busy: {}, allocations: {})", + self.layer_count(), + self.layers.iter().filter(|layer| !layer.is_empty()).count(), + self.layers.iter().map(Layer::allocations).sum::(), + ); + } Some(entry) } pub fn remove(&mut self, entry: &Entry) { - log::info!("Removing atlas entry: {entry:?}"); + log::debug!("Removing atlas entry: {entry:?}"); match entry { Entry::Contiguous(allocation) => { @@ -266,7 +273,7 @@ impl Atlas { } fn deallocate(&mut self, allocation: &Allocation) { - log::info!("Deallocating atlas: {allocation:?}"); + log::debug!("Deallocating atlas: {allocation:?}"); match allocation { Allocation::Full { layer } => { diff --git a/wgpu/src/image/atlas/allocator.rs b/wgpu/src/image/atlas/allocator.rs index 204a5c266a..a51ac1f5b5 100644 --- a/wgpu/src/image/atlas/allocator.rs +++ b/wgpu/src/image/atlas/allocator.rs @@ -33,6 +33,10 @@ impl Allocator { pub fn is_empty(&self) -> bool { self.allocations == 0 } + + pub fn allocations(&self) -> usize { + self.allocations + } } pub struct Region { diff --git a/wgpu/src/image/atlas/layer.rs b/wgpu/src/image/atlas/layer.rs index cf08960187..fd6788d9cf 100644 --- a/wgpu/src/image/atlas/layer.rs +++ b/wgpu/src/image/atlas/layer.rs @@ -11,4 +11,12 @@ impl Layer { pub fn is_empty(&self) -> bool { matches!(self, Layer::Empty) } + + pub fn allocations(&self) -> usize { + match self { + Layer::Empty => 0, + Layer::Busy(allocator) => allocator.allocations(), + Layer::Full => 1, + } + } } diff --git a/wgpu/src/image/mod.rs b/wgpu/src/image/mod.rs index 86731cbf97..8b831a3c1b 100644 --- a/wgpu/src/image/mod.rs +++ b/wgpu/src/image/mod.rs @@ -277,7 +277,7 @@ impl Pipeline { let texture_version = cache.layer_count(); if self.texture_version != texture_version { - log::info!("Atlas has grown. Recreating bind group..."); + log::debug!("Atlas has grown. Recreating bind group..."); self.texture = cache.create_bind_group(device, &self.texture_layout); diff --git a/wgpu/src/image/raster.rs b/wgpu/src/image/raster.rs index 441b294fde..7a837f2898 100644 --- a/wgpu/src/image/raster.rs +++ b/wgpu/src/image/raster.rs @@ -40,6 +40,7 @@ impl Memory { pub struct Cache { map: FxHashMap, hits: FxHashSet, + should_trim: bool, } impl Cache { @@ -55,6 +56,8 @@ impl Cache { Err(_) => Memory::Invalid, }; + self.should_trim = true; + self.insert(handle, memory); self.get(handle).unwrap() } @@ -86,6 +89,11 @@ impl Cache { /// Trim cache misses from cache pub fn trim(&mut self, atlas: &mut Atlas) { + // Only trim if new entries have landed in the `Cache` + if !self.should_trim { + return; + } + let hits = &self.hits; self.map.retain(|k, memory| { @@ -101,6 +109,7 @@ impl Cache { }); self.hits.clear(); + self.should_trim = false; } fn get(&mut self, handle: &image::Handle) -> Option<&mut Memory> { diff --git a/wgpu/src/image/vector.rs b/wgpu/src/image/vector.rs index d681b2e60c..c6d829afba 100644 --- a/wgpu/src/image/vector.rs +++ b/wgpu/src/image/vector.rs @@ -37,6 +37,7 @@ pub struct Cache { rasterized: FxHashMap<(u64, u32, u32, ColorFilter), atlas::Entry>, svg_hits: FxHashSet, rasterized_hits: FxHashSet<(u64, u32, u32, ColorFilter)>, + should_trim: bool, } type ColorFilter = Option<[u8; 4]>; @@ -76,6 +77,8 @@ impl Cache { } } + self.should_trim = true; + let _ = self.svgs.insert(handle.id(), svg); self.svgs.get(&handle.id()).unwrap() } @@ -176,6 +179,10 @@ impl Cache { /// Load svg and upload raster data pub fn trim(&mut self, atlas: &mut Atlas) { + if !self.should_trim { + return; + } + let svg_hits = &self.svg_hits; let rasterized_hits = &self.rasterized_hits; @@ -191,6 +198,7 @@ impl Cache { }); self.svg_hits.clear(); self.rasterized_hits.clear(); + self.should_trim = false; } } From 0c74d2645649a88799a894ed684a728d135043fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 25 Apr 2024 01:39:34 +0200 Subject: [PATCH 09/66] Implement `Stack` widget It can be used to stack elements on top of each other! --- core/src/layout.rs | 2 +- widget/src/helpers.rs | 27 +++- widget/src/lib.rs | 3 + widget/src/stack.rs | 327 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 357 insertions(+), 2 deletions(-) create mode 100644 widget/src/stack.rs diff --git a/core/src/layout.rs b/core/src/layout.rs index 95720aba16..98d056029f 100644 --- a/core/src/layout.rs +++ b/core/src/layout.rs @@ -54,7 +54,7 @@ impl<'a> Layout<'a> { } /// Returns an iterator over the [`Layout`] of the children of a [`Node`]. - pub fn children(self) -> impl Iterator> { + pub fn children(self) -> impl DoubleEndedIterator> { self.node.children().iter().map(move |node| { Layout::with_offset( Vector::new(self.position.x, self.position.y), diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 61789c193b..2afed3e63c 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -21,7 +21,7 @@ use crate::text_input::{self, TextInput}; use crate::toggler::{self, Toggler}; use crate::tooltip::{self, Tooltip}; use crate::vertical_slider::{self, VerticalSlider}; -use crate::{Column, MouseArea, Row, Space, Themer}; +use crate::{Column, MouseArea, Row, Space, Stack, Themer}; use std::borrow::Borrow; use std::ops::RangeInclusive; @@ -52,6 +52,19 @@ macro_rules! row { ); } +/// Creates a [`Stack`] with the given children. +/// +/// [`Stack`]: crate::Stack +#[macro_export] +macro_rules! stack { + () => ( + $crate::Stack::new() + ); + ($($x:expr),+ $(,)?) => ( + $crate::Stack::with_children([$($crate::core::Element::from($x)),+]) + ); +} + /// Creates a new [`Container`] with the provided content. /// /// [`Container`]: crate::Container @@ -98,6 +111,18 @@ where Row::with_children(children) } +/// Creates a new [`Stack`] with the given children. +/// +/// [`Stack`]: crate::Stack +pub fn stack<'a, Message, Theme, Renderer>( + children: impl IntoIterator>, +) -> Stack<'a, Message, Theme, Renderer> +where + Renderer: core::Renderer, +{ + Stack::with_children(children) +} + /// Creates a new [`Scrollable`] with the provided content. /// /// [`Scrollable`]: crate::Scrollable diff --git a/widget/src/lib.rs b/widget/src/lib.rs index 1eeacbae1b..00e9aaa49d 100644 --- a/widget/src/lib.rs +++ b/widget/src/lib.rs @@ -12,6 +12,7 @@ mod column; mod mouse_area; mod row; mod space; +mod stack; mod themer; pub mod button; @@ -78,6 +79,8 @@ pub use slider::Slider; #[doc(no_inline)] pub use space::Space; #[doc(no_inline)] +pub use stack::Stack; +#[doc(no_inline)] pub use text::Text; #[doc(no_inline)] pub use text_editor::TextEditor; diff --git a/widget/src/stack.rs b/widget/src/stack.rs new file mode 100644 index 0000000000..84f1f7ff75 --- /dev/null +++ b/widget/src/stack.rs @@ -0,0 +1,327 @@ +//! Distribute content vertically. +use crate::core::event::{self, Event}; +use crate::core::layout; +use crate::core::mouse; +use crate::core::overlay; +use crate::core::renderer; +use crate::core::widget::{Operation, Tree}; +use crate::core::{ + Clipboard, Element, Layout, Length, Rectangle, Shell, Size, Vector, Widget, +}; + +/// A container that displays children on top of each other. +/// +/// The first [`Element`] dictates the intrinsic [`Size`] of a [`Stack`] and +/// will be displayed as the base layer. Every consecutive [`Element`] will be +/// renderer on top; on its own layer. +/// +/// Keep in mind that too much layering will normally produce bad UX as well as +/// introduce certain rendering overhead. Use this widget sparingly! +#[allow(missing_debug_implementations)] +pub struct Stack<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> +{ + width: Length, + height: Length, + children: Vec>, +} + +impl<'a, Message, Theme, Renderer> Stack<'a, Message, Theme, Renderer> +where + Renderer: crate::core::Renderer, +{ + /// Creates an empty [`Stack`]. + pub fn new() -> Self { + Self::from_vec(Vec::new()) + } + + /// Creates a [`Stack`] with the given capacity. + pub fn with_capacity(capacity: usize) -> Self { + Self::from_vec(Vec::with_capacity(capacity)) + } + + /// Creates a [`Stack`] with the given elements. + pub fn with_children( + children: impl IntoIterator>, + ) -> Self { + let iterator = children.into_iter(); + + Self::with_capacity(iterator.size_hint().0).extend(iterator) + } + + /// Creates a [`Stack`] from an already allocated [`Vec`]. + /// + /// Keep in mind that the [`Stack`] will not inspect the [`Vec`], which means + /// it won't automatically adapt to the sizing strategy of its contents. + /// + /// If any of the children have a [`Length::Fill`] strategy, you will need to + /// call [`Stack::width`] or [`Stack::height`] accordingly. + pub fn from_vec( + children: Vec>, + ) -> Self { + Self { + width: Length::Shrink, + height: Length::Shrink, + children, + } + } + + /// Sets the width of the [`Stack`]. + pub fn width(mut self, width: impl Into) -> Self { + self.width = width.into(); + self + } + + /// Sets the height of the [`Stack`]. + pub fn height(mut self, height: impl Into) -> Self { + self.height = height.into(); + self + } + + /// Adds an element to the [`Stack`]. + pub fn push( + mut self, + child: impl Into>, + ) -> Self { + let child = child.into(); + + if self.children.is_empty() { + let child_size = child.as_widget().size_hint(); + + self.width = self.width.enclose(child_size.width); + self.height = self.height.enclose(child_size.height); + } + + self.children.push(child); + self + } + + /// Adds an element to the [`Stack`], if `Some`. + pub fn push_maybe( + self, + child: Option>>, + ) -> Self { + if let Some(child) = child { + self.push(child) + } else { + self + } + } + + /// Extends the [`Stack`] with the given children. + pub fn extend( + self, + children: impl IntoIterator>, + ) -> Self { + children.into_iter().fold(self, Self::push) + } +} + +impl<'a, Message, Renderer> Default for Stack<'a, Message, Renderer> +where + Renderer: crate::core::Renderer, +{ + fn default() -> Self { + Self::new() + } +} + +impl<'a, Message, Theme, Renderer> Widget + for Stack<'a, Message, Theme, Renderer> +where + Renderer: crate::core::Renderer, +{ + fn children(&self) -> Vec { + self.children.iter().map(Tree::new).collect() + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children(&self.children); + } + + fn size(&self) -> Size { + Size { + width: self.width, + height: self.height, + } + } + + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + if self.children.is_empty() { + return layout::Node::new(Size::ZERO); + } + + let base = self.children[0].as_widget().layout( + &mut tree.children[0], + renderer, + limits, + ); + + let size = base.size(); + let limits = layout::Limits::new(Size::ZERO, size); + + let nodes = std::iter::once(base) + .chain(self.children[1..].iter().zip(&mut tree.children[1..]).map( + |(layer, tree)| { + let node = + layer.as_widget().layout(tree, renderer, &limits); + + node + }, + )) + .collect(); + + layout::Node::with_children(size, nodes) + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn Operation, + ) { + operation.container(None, layout.bounds(), &mut |operation| { + self.children + .iter() + .zip(&mut tree.children) + .zip(layout.children()) + .for_each(|((child, state), layout)| { + child + .as_widget() + .operate(state, layout, renderer, operation); + }); + }); + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) -> event::Status { + self.children + .iter_mut() + .rev() + .zip(tree.children.iter_mut().rev()) + .zip(layout.children().rev()) + .map(|((child, state), layout)| { + child.as_widget_mut().on_event( + state, + event.clone(), + layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ) + }) + .find(|&status| status == event::Status::Captured) + .unwrap_or(event::Status::Ignored) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.children + .iter() + .rev() + .zip(tree.children.iter().rev()) + .zip(layout.children().rev()) + .map(|((child, state), layout)| { + child.as_widget().mouse_interaction( + state, layout, cursor, viewport, renderer, + ) + }) + .find(|&interaction| interaction != mouse::Interaction::Idle) + .unwrap_or_default() + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + if let Some(clipped_viewport) = layout.bounds().intersection(viewport) { + for (i, ((layer, state), layout)) in self + .children + .iter() + .zip(&tree.children) + .zip(layout.children()) + .enumerate() + { + if i > 0 { + renderer.with_layer(clipped_viewport, |renderer| { + layer.as_widget().draw( + state, + renderer, + theme, + style, + layout, + cursor, + &clipped_viewport, + ); + }); + } else { + layer.as_widget().draw( + state, + renderer, + theme, + style, + layout, + cursor, + &clipped_viewport, + ); + } + } + } + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + translation: Vector, + ) -> Option> { + overlay::from_children( + &mut self.children, + tree, + layout, + renderer, + translation, + ) + } +} + +impl<'a, Message, Theme, Renderer> From> + for Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Theme: 'a, + Renderer: crate::core::Renderer + 'a, +{ + fn from(stack: Stack<'a, Message, Theme, Renderer>) -> Self { + Self::new(stack) + } +} From 8b1c514c3816795437da2ec25c2cfc559ebe5b9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 25 Apr 2024 01:40:06 +0200 Subject: [PATCH 10/66] Showcase new `Stack` widget in `bezier_tool` example --- examples/bezier_tool/src/main.rs | 53 +++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/examples/bezier_tool/src/main.rs b/examples/bezier_tool/src/main.rs index 289c919b45..ba51a00edc 100644 --- a/examples/bezier_tool/src/main.rs +++ b/examples/bezier_tool/src/main.rs @@ -1,9 +1,11 @@ //! This example showcases an interactive `Canvas` for drawing Bézier curves. -use iced::widget::{button, column, text}; -use iced::{Alignment, Element, Length}; +use iced::alignment; +use iced::widget::{button, container, stack}; +use iced::{Element, Length, Theme}; pub fn main() -> iced::Result { iced::program("Bezier Tool - Iced", Example::update, Example::view) + .theme(|_| Theme::CatppuccinMocha) .antialiasing(true) .run() } @@ -35,16 +37,18 @@ impl Example { } fn view(&self) -> Element { - column![ - text("Bezier tool example").width(Length::Shrink).size(50), + container(stack![ self.bezier.view(&self.curves).map(Message::AddCurve), - button("Clear") - .style(button::danger) - .on_press(Message::Clear), - ] + container( + button("Clear") + .style(button::danger) + .on_press(Message::Clear) + ) + .padding(10) + .width(Length::Fill) + .align_x(alignment::Horizontal::Right), + ]) .padding(20) - .spacing(20) - .align_items(Alignment::Center) .into() } } @@ -139,22 +143,24 @@ mod bezier { &self, state: &Self::State, renderer: &Renderer, - _theme: &Theme, + theme: &Theme, bounds: Rectangle, cursor: mouse::Cursor, ) -> Vec { let content = self.state.cache.draw(renderer, bounds.size(), |frame| { - Curve::draw_all(self.curves, frame); + Curve::draw_all(self.curves, frame, theme); frame.stroke( &Path::rectangle(Point::ORIGIN, frame.size()), - Stroke::default().with_width(2.0), + Stroke::default() + .with_width(2.0) + .with_color(theme.palette().text), ); }); if let Some(pending) = state { - vec![content, pending.draw(renderer, bounds, cursor)] + vec![content, pending.draw(renderer, theme, bounds, cursor)] } else { vec![content] } @@ -182,7 +188,7 @@ mod bezier { } impl Curve { - fn draw_all(curves: &[Curve], frame: &mut Frame) { + fn draw_all(curves: &[Curve], frame: &mut Frame, theme: &Theme) { let curves = Path::new(|p| { for curve in curves { p.move_to(curve.from); @@ -190,7 +196,12 @@ mod bezier { } }); - frame.stroke(&curves, Stroke::default().with_width(2.0)); + frame.stroke( + &curves, + Stroke::default() + .with_width(2.0) + .with_color(theme.palette().text), + ); } } @@ -204,6 +215,7 @@ mod bezier { fn draw( &self, renderer: &Renderer, + theme: &Theme, bounds: Rectangle, cursor: mouse::Cursor, ) -> Geometry { @@ -213,7 +225,12 @@ mod bezier { match *self { Pending::One { from } => { let line = Path::line(from, cursor_position); - frame.stroke(&line, Stroke::default().with_width(2.0)); + frame.stroke( + &line, + Stroke::default() + .with_width(2.0) + .with_color(theme.palette().text), + ); } Pending::Two { from, to } => { let curve = Curve { @@ -222,7 +239,7 @@ mod bezier { control: cursor_position, }; - Curve::draw_all(&[curve], &mut frame); + Curve::draw_all(&[curve], &mut frame, theme); } }; } From 99434b3ecf7874d2382e7f739bc69a55768fd60b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 25 Apr 2024 01:47:07 +0200 Subject: [PATCH 11/66] Fix documentation of `stack` module --- widget/src/stack.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/widget/src/stack.rs b/widget/src/stack.rs index 84f1f7ff75..d6a1538eec 100644 --- a/widget/src/stack.rs +++ b/widget/src/stack.rs @@ -1,4 +1,4 @@ -//! Distribute content vertically. +//! Display content on top of other content. use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; From 9492da11d90893b396e8dfdae47cc54c6ab42411 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 25 Apr 2024 02:25:36 +0200 Subject: [PATCH 12/66] Use `Limits::resolve` in `Stack` widget --- widget/src/stack.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/widget/src/stack.rs b/widget/src/stack.rs index d6a1538eec..6e5dacd238 100644 --- a/widget/src/stack.rs +++ b/widget/src/stack.rs @@ -155,13 +155,14 @@ where return layout::Node::new(Size::ZERO); } + let limits = limits.width(self.width).height(self.height); let base = self.children[0].as_widget().layout( &mut tree.children[0], renderer, - limits, + &limits, ); - let size = base.size(); + let size = limits.resolve(self.width, self.height, base.size()); let limits = layout::Limits::new(Size::ZERO, size); let nodes = std::iter::once(base) From 4cd45643d7d2aa83212162f17a9b28ddae4a9340 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 25 Apr 2024 06:05:00 +0200 Subject: [PATCH 13/66] Introduce `opaque` widget helper --- core/src/mouse/interaction.rs | 1 + core/src/overlay.rs | 2 +- core/src/widget.rs | 2 +- examples/loupe/src/main.rs | 2 +- runtime/src/multi_window/state.rs | 2 +- runtime/src/program/state.rs | 2 +- widget/src/helpers.rs | 169 ++++++++++++++++++++++- widget/src/image/viewer.rs | 2 +- widget/src/mouse_area.rs | 2 +- widget/src/scrollable.rs | 2 +- widget/src/stack.rs | 2 +- winit/src/conversion.rs | 4 +- winit/src/multi_window/window_manager.rs | 2 +- 13 files changed, 182 insertions(+), 12 deletions(-) diff --git a/core/src/mouse/interaction.rs b/core/src/mouse/interaction.rs index 6ad6622926..065eb8e725 100644 --- a/core/src/mouse/interaction.rs +++ b/core/src/mouse/interaction.rs @@ -3,6 +3,7 @@ #[allow(missing_docs)] pub enum Interaction { #[default] + None, Idle, Pointer, Grab, diff --git a/core/src/overlay.rs b/core/src/overlay.rs index 03076a30b2..3a57fe1641 100644 --- a/core/src/overlay.rs +++ b/core/src/overlay.rs @@ -79,7 +79,7 @@ where _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { - mouse::Interaction::Idle + mouse::Interaction::None } /// Returns true if the cursor is over the [`Overlay`]. diff --git a/core/src/widget.rs b/core/src/widget.rs index 58a9f19be4..b02e3a4f8f 100644 --- a/core/src/widget.rs +++ b/core/src/widget.rs @@ -137,7 +137,7 @@ where _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { - mouse::Interaction::Idle + mouse::Interaction::None } /// Returns the overlay of the [`Widget`], if there is any. diff --git a/examples/loupe/src/main.rs b/examples/loupe/src/main.rs index 6a5ff123ab..42b7f47148 100644 --- a/examples/loupe/src/main.rs +++ b/examples/loupe/src/main.rs @@ -159,7 +159,7 @@ mod loupe { if cursor.is_over(layout.bounds()) { mouse::Interaction::ZoomIn } else { - mouse::Interaction::Idle + mouse::Interaction::None } } } diff --git a/runtime/src/multi_window/state.rs b/runtime/src/multi_window/state.rs index afd0451965..10366ec05b 100644 --- a/runtime/src/multi_window/state.rs +++ b/runtime/src/multi_window/state.rs @@ -48,7 +48,7 @@ where caches, queued_events: Vec::new(), queued_messages: Vec::new(), - mouse_interaction: mouse::Interaction::Idle, + mouse_interaction: mouse::Interaction::None, } } diff --git a/runtime/src/program/state.rs b/runtime/src/program/state.rs index d685b07c86..c6589c22be 100644 --- a/runtime/src/program/state.rs +++ b/runtime/src/program/state.rs @@ -47,7 +47,7 @@ where cache, queued_events: Vec::new(), queued_messages: Vec::new(), - mouse_interaction: mouse::Interaction::Idle, + mouse_interaction: mouse::Interaction::None, } } diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 2afed3e63c..fd34525195 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -5,7 +5,7 @@ use crate::combo_box::{self, ComboBox}; use crate::container::{self, Container}; use crate::core; use crate::core::widget::operation; -use crate::core::{Element, Length, Pixels}; +use crate::core::{Element, Length, Pixels, Widget}; use crate::keyed; use crate::overlay; use crate::pick_list::{self, PickList}; @@ -123,6 +123,173 @@ where Stack::with_children(children) } +/// Wraps the given widget and captures any mouse button presses inside the bounds of +/// the widget—therefore making it _opaque_. +/// +/// This helper is meant to be used to mark elements in a [`Stack`] to avoid mouse +/// events from passing through layers. +/// +/// [`Stack`]: crate::Stack +pub fn opaque<'a, Message, Theme, Renderer>( + content: impl Into>, +) -> Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Theme: 'a, + Renderer: core::Renderer + 'a, +{ + use crate::core::event::{self, Event}; + use crate::core::layout::{self, Layout}; + use crate::core::mouse; + use crate::core::renderer; + use crate::core::widget::tree::{self, Tree}; + use crate::core::{Rectangle, Shell, Size}; + + struct Opaque<'a, Message, Theme, Renderer> { + content: Element<'a, Message, Theme, Renderer>, + } + + impl<'a, Message, Theme, Renderer> Widget + for Opaque<'a, Message, Theme, Renderer> + where + Renderer: core::Renderer, + { + fn tag(&self) -> tree::Tag { + self.content.as_widget().tag() + } + + fn state(&self) -> tree::State { + self.content.as_widget().state() + } + + fn children(&self) -> Vec { + self.content.as_widget().children() + } + + fn diff(&self, tree: &mut Tree) { + self.content.as_widget().diff(tree); + } + + fn size(&self) -> Size { + self.content.as_widget().size() + } + + fn size_hint(&self) -> Size { + self.content.as_widget().size_hint() + } + + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + self.content.as_widget().layout(tree, renderer, limits) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + self.content + .as_widget() + .draw(tree, renderer, theme, style, layout, cursor, viewport); + } + + fn operate( + &self, + state: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn operation::Operation, + ) { + self.content + .as_widget() + .operate(state, layout, renderer, operation); + } + + fn on_event( + &mut self, + state: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn core::Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) -> event::Status { + let is_mouse_press = matches!( + event, + core::Event::Mouse(mouse::Event::ButtonPressed(_)) + ); + + if let core::event::Status::Captured = + self.content.as_widget_mut().on_event( + state, event, layout, cursor, renderer, clipboard, shell, + viewport, + ) + { + return event::Status::Captured; + } + + if is_mouse_press && cursor.is_over(layout.bounds()) { + event::Status::Captured + } else { + event::Status::Ignored + } + } + + fn mouse_interaction( + &self, + state: &core::widget::Tree, + layout: core::Layout<'_>, + cursor: core::mouse::Cursor, + viewport: &core::Rectangle, + renderer: &Renderer, + ) -> core::mouse::Interaction { + let interaction = self + .content + .as_widget() + .mouse_interaction(state, layout, cursor, viewport, renderer); + + if interaction == mouse::Interaction::None + && cursor.is_over(layout.bounds()) + { + mouse::Interaction::Idle + } else { + interaction + } + } + + fn overlay<'b>( + &'b mut self, + state: &'b mut core::widget::Tree, + layout: core::Layout<'_>, + renderer: &Renderer, + translation: core::Vector, + ) -> Option> + { + self.content.as_widget_mut().overlay( + state, + layout, + renderer, + translation, + ) + } + } + + Element::new(Opaque { + content: content.into(), + }) +} + /// Creates a new [`Scrollable`] with the provided content. /// /// [`Scrollable`]: crate::Scrollable diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs index 5f7bb345c7..75d73b1927 100644 --- a/widget/src/image/viewer.rs +++ b/widget/src/image/viewer.rs @@ -304,7 +304,7 @@ where } else if is_mouse_over { mouse::Interaction::Grab } else { - mouse::Interaction::Idle + mouse::Interaction::None } } diff --git a/widget/src/mouse_area.rs b/widget/src/mouse_area.rs index 9634e477d0..d7235cf602 100644 --- a/widget/src/mouse_area.rs +++ b/widget/src/mouse_area.rs @@ -232,7 +232,7 @@ where ); match (self.interaction, content_interaction) { - (Some(interaction), mouse::Interaction::Idle) + (Some(interaction), mouse::Interaction::None) if cursor.is_over(layout.bounds()) => { interaction diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index cdc143a2b2..10e81cee1a 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -857,7 +857,7 @@ where if (mouse_over_x_scrollbar || mouse_over_y_scrollbar) || state.scrollers_grabbed() { - mouse::Interaction::Idle + mouse::Interaction::None } else { let translation = state.translation(self.direction, bounds, content_bounds); diff --git a/widget/src/stack.rs b/widget/src/stack.rs index 6e5dacd238..8a0ea2ebc4 100644 --- a/widget/src/stack.rs +++ b/widget/src/stack.rs @@ -249,7 +249,7 @@ where state, layout, cursor, viewport, renderer, ) }) - .find(|&interaction| interaction != mouse::Interaction::Idle) + .find(|&interaction| interaction != mouse::Interaction::None) .unwrap_or_default() } diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index fc3d1c086b..0f83dac3cb 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -396,7 +396,9 @@ pub fn mouse_interaction( use mouse::Interaction; match interaction { - Interaction::Idle => winit::window::CursorIcon::Default, + Interaction::None | Interaction::Idle => { + winit::window::CursorIcon::Default + } Interaction::Pointer => winit::window::CursorIcon::Pointer, Interaction::Working => winit::window::CursorIcon::Progress, Interaction::Grab => winit::window::CursorIcon::Grab, diff --git a/winit/src/multi_window/window_manager.rs b/winit/src/multi_window/window_manager.rs index 71c1688b80..3a0c8556a9 100644 --- a/winit/src/multi_window/window_manager.rs +++ b/winit/src/multi_window/window_manager.rs @@ -60,7 +60,7 @@ where exit_on_close_request, surface, renderer, - mouse_interaction: mouse::Interaction::Idle, + mouse_interaction: mouse::Interaction::None, }, ); From 4fc342c97963a7a7e7983604b9399fccf017aa53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 25 Apr 2024 06:05:40 +0200 Subject: [PATCH 14/66] Simplify `modal` example :tada: --- examples/modal/src/main.rs | 363 ++++--------------------------------- 1 file changed, 35 insertions(+), 328 deletions(-) diff --git a/examples/modal/src/main.rs b/examples/modal/src/main.rs index 398728e0a5..a345924d09 100644 --- a/examples/modal/src/main.rs +++ b/examples/modal/src/main.rs @@ -2,12 +2,11 @@ use iced::event::{self, Event}; use iced::keyboard; use iced::keyboard::key; use iced::widget::{ - self, button, column, container, horizontal_space, pick_list, row, text, - text_input, + self, button, column, container, horizontal_space, mouse_area, opaque, + pick_list, row, stack, text, text_input, }; -use iced::{Alignment, Command, Element, Length, Subscription}; +use iced::{Alignment, Color, Command, Element, Length, Subscription}; -use modal::Modal; use std::fmt; pub fn main() -> iced::Result { @@ -121,7 +120,7 @@ impl App { .height(Length::Fill); if self.show_modal { - let modal = container( + let signup = container( column![ text("Sign Up").size(24), column![ @@ -162,9 +161,7 @@ impl App { .padding(10) .style(container::rounded_box); - Modal::new(content, modal) - .on_blur(Message::HideModal) - .into() + modal(content, signup, Message::HideModal) } else { content.into() } @@ -203,326 +200,36 @@ impl fmt::Display for Plan { } } -mod modal { - use iced::advanced::layout::{self, Layout}; - use iced::advanced::overlay; - use iced::advanced::renderer; - use iced::advanced::widget::{self, Widget}; - use iced::advanced::{self, Clipboard, Shell}; - use iced::alignment::Alignment; - use iced::event; - use iced::mouse; - use iced::{Color, Element, Event, Length, Point, Rectangle, Size, Vector}; - - /// A widget that centers a modal element over some base element - pub struct Modal<'a, Message, Theme, Renderer> { - base: Element<'a, Message, Theme, Renderer>, - modal: Element<'a, Message, Theme, Renderer>, - on_blur: Option, - } - - impl<'a, Message, Theme, Renderer> Modal<'a, Message, Theme, Renderer> { - /// Returns a new [`Modal`] - pub fn new( - base: impl Into>, - modal: impl Into>, - ) -> Self { - Self { - base: base.into(), - modal: modal.into(), - on_blur: None, - } - } - - /// Sets the message that will be produces when the background - /// of the [`Modal`] is pressed - pub fn on_blur(self, on_blur: Message) -> Self { - Self { - on_blur: Some(on_blur), - ..self - } - } - } - - impl<'a, Message, Theme, Renderer> Widget - for Modal<'a, Message, Theme, Renderer> - where - Renderer: advanced::Renderer, - Message: Clone, - { - fn children(&self) -> Vec { - vec![ - widget::Tree::new(&self.base), - widget::Tree::new(&self.modal), - ] - } - - fn diff(&self, tree: &mut widget::Tree) { - tree.diff_children(&[&self.base, &self.modal]); - } - - fn size(&self) -> Size { - self.base.as_widget().size() - } - - fn layout( - &self, - tree: &mut widget::Tree, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - self.base.as_widget().layout( - &mut tree.children[0], - renderer, - limits, - ) - } - - fn on_event( - &mut self, - state: &mut widget::Tree, - event: Event, - layout: Layout<'_>, - cursor: mouse::Cursor, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - viewport: &Rectangle, - ) -> event::Status { - self.base.as_widget_mut().on_event( - &mut state.children[0], - event, - layout, - cursor, - renderer, - clipboard, - shell, - viewport, - ) - } - - fn draw( - &self, - state: &widget::Tree, - renderer: &mut Renderer, - theme: &Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor: mouse::Cursor, - viewport: &Rectangle, - ) { - self.base.as_widget().draw( - &state.children[0], - renderer, - theme, - style, - layout, - cursor, - viewport, - ); - } - - fn overlay<'b>( - &'b mut self, - state: &'b mut widget::Tree, - layout: Layout<'_>, - _renderer: &Renderer, - translation: Vector, - ) -> Option> { - Some(overlay::Element::new(Box::new(Overlay { - position: layout.position() + translation, - content: &mut self.modal, - tree: &mut state.children[1], - size: layout.bounds().size(), - on_blur: self.on_blur.clone(), - }))) - } - - fn mouse_interaction( - &self, - state: &widget::Tree, - layout: Layout<'_>, - cursor: mouse::Cursor, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - self.base.as_widget().mouse_interaction( - &state.children[0], - layout, - cursor, - viewport, - renderer, - ) - } - - fn operate( - &self, - state: &mut widget::Tree, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn widget::Operation, - ) { - self.base.as_widget().operate( - &mut state.children[0], - layout, - renderer, - operation, - ); - } - } - - struct Overlay<'a, 'b, Message, Theme, Renderer> { - position: Point, - content: &'b mut Element<'a, Message, Theme, Renderer>, - tree: &'b mut widget::Tree, - size: Size, - on_blur: Option, - } - - impl<'a, 'b, Message, Theme, Renderer> - overlay::Overlay - for Overlay<'a, 'b, Message, Theme, Renderer> - where - Renderer: advanced::Renderer, - Message: Clone, - { - fn layout( - &mut self, - renderer: &Renderer, - _bounds: Size, - ) -> layout::Node { - let limits = layout::Limits::new(Size::ZERO, self.size) +fn modal<'a, Message>( + base: impl Into>, + content: impl Into>, + on_blur: Message, +) -> Element<'a, Message> +where + Message: Clone + 'a, +{ + stack![ + base.into(), + mouse_area( + container(opaque(content)) .width(Length::Fill) - .height(Length::Fill); - - let child = self - .content - .as_widget() - .layout(self.tree, renderer, &limits) - .align(Alignment::Center, Alignment::Center, limits.max()); - - layout::Node::with_children(self.size, vec![child]) - .move_to(self.position) - } - - fn on_event( - &mut self, - event: Event, - layout: Layout<'_>, - cursor: mouse::Cursor, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - let content_bounds = layout.children().next().unwrap().bounds(); - - if let Some(message) = self.on_blur.as_ref() { - if let Event::Mouse(mouse::Event::ButtonPressed( - mouse::Button::Left, - )) = &event - { - if !cursor.is_over(content_bounds) { - shell.publish(message.clone()); - return event::Status::Captured; + .height(Length::Fill) + .center_x() + .center_y() + .style(|_theme| { + container::Style { + background: Some( + Color { + a: 0.8, + ..Color::BLACK + } + .into(), + ), + ..container::Style::default() } - } - } - - self.content.as_widget_mut().on_event( - self.tree, - event, - layout.children().next().unwrap(), - cursor, - renderer, - clipboard, - shell, - &layout.bounds(), - ) - } - - fn draw( - &self, - renderer: &mut Renderer, - theme: &Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor: mouse::Cursor, - ) { - renderer.fill_quad( - renderer::Quad { - bounds: layout.bounds(), - ..renderer::Quad::default() - }, - Color { - a: 0.80, - ..Color::BLACK - }, - ); - - self.content.as_widget().draw( - self.tree, - renderer, - theme, - style, - layout.children().next().unwrap(), - cursor, - &layout.bounds(), - ); - } - - fn operate( - &mut self, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn widget::Operation, - ) { - self.content.as_widget().operate( - self.tree, - layout.children().next().unwrap(), - renderer, - operation, - ); - } - - fn mouse_interaction( - &self, - layout: Layout<'_>, - cursor: mouse::Cursor, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - self.content.as_widget().mouse_interaction( - self.tree, - layout.children().next().unwrap(), - cursor, - viewport, - renderer, - ) - } - - fn overlay<'c>( - &'c mut self, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option> { - self.content.as_widget_mut().overlay( - self.tree, - layout.children().next().unwrap(), - renderer, - Vector::ZERO, - ) - } - } - - impl<'a, Message, Theme, Renderer> From> - for Element<'a, Message, Theme, Renderer> - where - Theme: 'a, - Message: 'a + Clone, - Renderer: 'a + advanced::Renderer, - { - fn from(modal: Modal<'a, Message, Theme, Renderer>) -> Self { - Element::new(modal) - } - } + }) + ) + .on_press(on_blur) + ] + .into() } From eb49567791015be7f329d7d57d5f8c388abecd96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 26 Apr 2024 01:19:49 +0200 Subject: [PATCH 15/66] Capture scrollbar events in a `scrollable` before content events --- widget/src/scrollable.rs | 284 +++++++++++++++++++-------------------- 1 file changed, 142 insertions(+), 142 deletions(-) diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 10e81cee1a..f0a042cdef 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -350,6 +350,148 @@ where let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = scrollbars.is_mouse_over(cursor); + if let Some(scroller_grabbed_at) = state.y_scroller_grabbed_at { + match event { + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { + if let Some(scrollbar) = scrollbars.y { + let Some(cursor_position) = cursor.position() else { + return event::Status::Ignored; + }; + + state.scroll_y_to( + scrollbar.scroll_percentage_y( + scroller_grabbed_at, + cursor_position, + ), + bounds, + content_bounds, + ); + + let _ = notify_on_scroll( + state, + &self.on_scroll, + bounds, + content_bounds, + shell, + ); + + return event::Status::Captured; + } + } + _ => {} + } + } else if mouse_over_y_scrollbar { + match event { + Event::Mouse(mouse::Event::ButtonPressed( + mouse::Button::Left, + )) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + let Some(cursor_position) = cursor.position() else { + return event::Status::Ignored; + }; + + if let (Some(scroller_grabbed_at), Some(scrollbar)) = ( + scrollbars.grab_y_scroller(cursor_position), + scrollbars.y, + ) { + state.scroll_y_to( + scrollbar.scroll_percentage_y( + scroller_grabbed_at, + cursor_position, + ), + bounds, + content_bounds, + ); + + state.y_scroller_grabbed_at = Some(scroller_grabbed_at); + + let _ = notify_on_scroll( + state, + &self.on_scroll, + bounds, + content_bounds, + shell, + ); + } + + return event::Status::Captured; + } + _ => {} + } + } + + if let Some(scroller_grabbed_at) = state.x_scroller_grabbed_at { + match event { + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { + let Some(cursor_position) = cursor.position() else { + return event::Status::Ignored; + }; + + if let Some(scrollbar) = scrollbars.x { + state.scroll_x_to( + scrollbar.scroll_percentage_x( + scroller_grabbed_at, + cursor_position, + ), + bounds, + content_bounds, + ); + + let _ = notify_on_scroll( + state, + &self.on_scroll, + bounds, + content_bounds, + shell, + ); + } + + return event::Status::Captured; + } + _ => {} + } + } else if mouse_over_x_scrollbar { + match event { + Event::Mouse(mouse::Event::ButtonPressed( + mouse::Button::Left, + )) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + let Some(cursor_position) = cursor.position() else { + return event::Status::Ignored; + }; + + if let (Some(scroller_grabbed_at), Some(scrollbar)) = ( + scrollbars.grab_x_scroller(cursor_position), + scrollbars.x, + ) { + state.scroll_x_to( + scrollbar.scroll_percentage_x( + scroller_grabbed_at, + cursor_position, + ), + bounds, + content_bounds, + ); + + state.x_scroller_grabbed_at = Some(scroller_grabbed_at); + + let _ = notify_on_scroll( + state, + &self.on_scroll, + bounds, + content_bounds, + shell, + ); + + return event::Status::Captured; + } + } + _ => {} + } + } + let mut event_status = { let cursor = match cursor_over_scrollable { Some(cursor_position) @@ -501,148 +643,6 @@ where _ => {} } - if let Some(scroller_grabbed_at) = state.y_scroller_grabbed_at { - match event { - Event::Mouse(mouse::Event::CursorMoved { .. }) - | Event::Touch(touch::Event::FingerMoved { .. }) => { - if let Some(scrollbar) = scrollbars.y { - let Some(cursor_position) = cursor.position() else { - return event::Status::Ignored; - }; - - state.scroll_y_to( - scrollbar.scroll_percentage_y( - scroller_grabbed_at, - cursor_position, - ), - bounds, - content_bounds, - ); - - let _ = notify_on_scroll( - state, - &self.on_scroll, - bounds, - content_bounds, - shell, - ); - - event_status = event::Status::Captured; - } - } - _ => {} - } - } else if mouse_over_y_scrollbar { - match event { - Event::Mouse(mouse::Event::ButtonPressed( - mouse::Button::Left, - )) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - let Some(cursor_position) = cursor.position() else { - return event::Status::Ignored; - }; - - if let (Some(scroller_grabbed_at), Some(scrollbar)) = ( - scrollbars.grab_y_scroller(cursor_position), - scrollbars.y, - ) { - state.scroll_y_to( - scrollbar.scroll_percentage_y( - scroller_grabbed_at, - cursor_position, - ), - bounds, - content_bounds, - ); - - state.y_scroller_grabbed_at = Some(scroller_grabbed_at); - - let _ = notify_on_scroll( - state, - &self.on_scroll, - bounds, - content_bounds, - shell, - ); - } - - event_status = event::Status::Captured; - } - _ => {} - } - } - - if let Some(scroller_grabbed_at) = state.x_scroller_grabbed_at { - match event { - Event::Mouse(mouse::Event::CursorMoved { .. }) - | Event::Touch(touch::Event::FingerMoved { .. }) => { - let Some(cursor_position) = cursor.position() else { - return event::Status::Ignored; - }; - - if let Some(scrollbar) = scrollbars.x { - state.scroll_x_to( - scrollbar.scroll_percentage_x( - scroller_grabbed_at, - cursor_position, - ), - bounds, - content_bounds, - ); - - let _ = notify_on_scroll( - state, - &self.on_scroll, - bounds, - content_bounds, - shell, - ); - } - - event_status = event::Status::Captured; - } - _ => {} - } - } else if mouse_over_x_scrollbar { - match event { - Event::Mouse(mouse::Event::ButtonPressed( - mouse::Button::Left, - )) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - let Some(cursor_position) = cursor.position() else { - return event::Status::Ignored; - }; - - if let (Some(scroller_grabbed_at), Some(scrollbar)) = ( - scrollbars.grab_x_scroller(cursor_position), - scrollbars.x, - ) { - state.scroll_x_to( - scrollbar.scroll_percentage_x( - scroller_grabbed_at, - cursor_position, - ), - bounds, - content_bounds, - ); - - state.x_scroller_grabbed_at = Some(scroller_grabbed_at); - - let _ = notify_on_scroll( - state, - &self.on_scroll, - bounds, - content_bounds, - shell, - ); - - event_status = event::Status::Captured; - } - } - _ => {} - } - } - event_status } From 6d4155a5488da5b7fa3fd314f98c8f28c7202bd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 26 Apr 2024 01:44:03 +0200 Subject: [PATCH 16/66] Fix `Shift` scrolling for `scrollable` on macOS Apparently, macOS inverts the scrolling axes automatically now. Was this a thing before, or did an update just break user space? --- widget/src/scrollable.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index f0a042cdef..6fc00f877d 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -564,7 +564,9 @@ where let delta = match delta { mouse::ScrollDelta::Lines { x, y } => { // TODO: Configurable speed/friction (?) - let movement = if state.keyboard_modifiers.shift() { + let movement = if !cfg!(target_os = "macos") // macOS automatically inverts the axes when Shift is pressed + && state.keyboard_modifiers.shift() + { Vector::new(y, x) } else { Vector::new(x, y) From 3762c0590ceb0fc579a1e699702d7d5c2b204348 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 26 Apr 2024 15:17:10 +0200 Subject: [PATCH 17/66] Fix panic when scrolling a `TextEditor` inside a `scrollable` --- graphics/src/text/editor.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index c488a51c61..4b8f0f2ade 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -456,10 +456,14 @@ impl editor::Editor for Editor { } } Action::Scroll { lines } => { - editor.action( - font_system.raw(), - cosmic_text::Action::Scroll { lines }, - ); + let (_, height) = editor.buffer().size(); + + if height < i32::MAX as f32 { + editor.action( + font_system.raw(), + cosmic_text::Action::Scroll { lines }, + ); + } } } From 73088a6fc1567f8bd557fdf479e16b395032b019 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 26 Apr 2024 15:17:35 +0200 Subject: [PATCH 18/66] Fix out of bounds caret in `TextEditor` in some circumstances --- widget/src/text_editor.rs | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 5b0b1b79c2..7c0b98ea6d 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -569,23 +569,27 @@ where if state.is_focused { match internal.editor.cursor() { Cursor::Caret(position) => { - let position = position + translation; + let cursor = + Rectangle::new( + position + translation, + Size::new( + 1.0, + self.line_height + .to_absolute(self.text_size.unwrap_or_else( + || renderer.default_size(), + )) + .into(), + ), + ); - if bounds.contains(position) { + if let Some(clipped_cursor) = bounds.intersection(&cursor) { renderer.fill_quad( renderer::Quad { bounds: Rectangle { - x: position.x.floor(), - y: position.y, - width: 1.0, - height: self - .line_height - .to_absolute( - self.text_size.unwrap_or_else( - || renderer.default_size(), - ), - ) - .into(), + x: clipped_cursor.x.floor(), + y: clipped_cursor.y, + width: clipped_cursor.width, + height: clipped_cursor.height, }, ..renderer::Quad::default() }, From 23ef6547ad0f0cc2944664ace4be7cdacc0a23cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 27 Apr 2024 06:06:13 +0200 Subject: [PATCH 19/66] Introduce `hover` widget --- widget/src/helpers.rs | 230 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 229 insertions(+), 1 deletion(-) diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index fd34525195..c86223bc31 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -124,7 +124,7 @@ where } /// Wraps the given widget and captures any mouse button presses inside the bounds of -/// the widget—therefore making it _opaque_. +/// the widget—effectively making it _opaque_. /// /// This helper is meant to be used to mark elements in a [`Stack`] to avoid mouse /// events from passing through layers. @@ -290,6 +290,234 @@ where }) } +/// Displays a widget on top of another one, only when the base widget is hovered. +/// +/// This works analogously to a [`stack`], but it will only display the layer on top +/// when the cursor is over the base. It can be useful for removing visual clutter. +pub fn hover<'a, Message, Theme, Renderer>( + base: impl Into>, + top: impl Into>, +) -> Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Theme: 'a, + Renderer: core::Renderer + 'a, +{ + use crate::core::event::{self, Event}; + use crate::core::layout::{self, Layout}; + use crate::core::mouse; + use crate::core::renderer; + use crate::core::widget::tree::{self, Tree}; + use crate::core::{Rectangle, Shell, Size}; + + struct Hover<'a, Message, Theme, Renderer> { + base: Element<'a, Message, Theme, Renderer>, + top: Element<'a, Message, Theme, Renderer>, + is_overlay_active: bool, + } + + impl<'a, Message, Theme, Renderer> Widget + for Hover<'a, Message, Theme, Renderer> + where + Renderer: core::Renderer, + { + fn tag(&self) -> tree::Tag { + struct Tag; + tree::Tag::of::() + } + + fn children(&self) -> Vec { + vec![Tree::new(&self.base), Tree::new(&self.top)] + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children(&[&self.base, &self.top]); + } + + fn size(&self) -> Size { + self.base.as_widget().size() + } + + fn size_hint(&self) -> Size { + self.base.as_widget().size_hint() + } + + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let base = self.base.as_widget().layout( + &mut tree.children[0], + renderer, + limits, + ); + + let top = self.top.as_widget().layout( + &mut tree.children[1], + renderer, + &layout::Limits::new(Size::ZERO, base.size()), + ); + + layout::Node::with_children(base.size(), vec![base, top]) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + let mut children = layout.children().zip(&tree.children); + + let (base_layout, base_tree) = children.next().unwrap(); + + self.base.as_widget().draw( + base_tree, + renderer, + theme, + style, + base_layout, + cursor, + viewport, + ); + + if cursor.is_over(layout.bounds()) || self.is_overlay_active { + let (top_layout, top_tree) = children.next().unwrap(); + + renderer.with_layer(layout.bounds(), |renderer| { + self.top.as_widget().draw( + top_tree, renderer, theme, style, top_layout, cursor, + viewport, + ); + }); + } + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn operation::Operation, + ) { + let children = [&self.base, &self.top] + .into_iter() + .zip(layout.children().zip(&mut tree.children)); + + for (child, (layout, tree)) in children { + child.as_widget().operate(tree, layout, renderer, operation); + } + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn core::Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) -> event::Status { + let mut children = layout.children().zip(&mut tree.children); + let (base_layout, base_tree) = children.next().unwrap(); + + let top_status = if cursor.is_over(layout.bounds()) { + let (top_layout, top_tree) = children.next().unwrap(); + + self.top.as_widget_mut().on_event( + top_tree, + event.clone(), + top_layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ) + } else { + event::Status::Ignored + }; + + if top_status == event::Status::Captured { + return top_status; + } + + self.base.as_widget_mut().on_event( + base_tree, + event.clone(), + base_layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + [&self.base, &self.top] + .into_iter() + .rev() + .zip(layout.children().rev().zip(tree.children.iter().rev())) + .map(|(child, (layout, tree))| { + child.as_widget().mouse_interaction( + tree, layout, cursor, viewport, renderer, + ) + }) + .find(|&interaction| interaction != mouse::Interaction::None) + .unwrap_or_default() + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut core::widget::Tree, + layout: core::Layout<'_>, + renderer: &Renderer, + translation: core::Vector, + ) -> Option> + { + let overlay = [&mut self.base, &mut self.top] + .into_iter() + .rev() + .zip( + layout.children().rev().zip(tree.children.iter_mut().rev()), + ) + .find_map(|(child, (layout, tree))| { + child.as_widget_mut().overlay( + tree, + layout, + renderer, + translation, + ) + }); + + self.is_overlay_active = overlay.is_some(); + overlay + } + } + + Element::new(Hover { + base: base.into(), + top: top.into(), + is_overlay_active: false, + }) +} + /// Creates a new [`Scrollable`] with the provided content. /// /// [`Scrollable`]: crate::Scrollable From a16a75a71d0bc76c17c2d76d827fa8c37eb56f09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 27 Apr 2024 06:06:28 +0200 Subject: [PATCH 20/66] Use `hover` widget in `bezier_tool` example --- examples/bezier_tool/src/main.rs | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/examples/bezier_tool/src/main.rs b/examples/bezier_tool/src/main.rs index ba51a00edc..29df3eeba9 100644 --- a/examples/bezier_tool/src/main.rs +++ b/examples/bezier_tool/src/main.rs @@ -1,6 +1,6 @@ //! This example showcases an interactive `Canvas` for drawing Bézier curves. use iced::alignment; -use iced::widget::{button, container, stack}; +use iced::widget::{button, container, horizontal_space, hover}; use iced::{Element, Length, Theme}; pub fn main() -> iced::Result { @@ -37,17 +37,21 @@ impl Example { } fn view(&self) -> Element { - container(stack![ + container(hover( self.bezier.view(&self.curves).map(Message::AddCurve), - container( - button("Clear") - .style(button::danger) - .on_press(Message::Clear) - ) - .padding(10) - .width(Length::Fill) - .align_x(alignment::Horizontal::Right), - ]) + if self.curves.is_empty() { + container(horizontal_space()) + } else { + container( + button("Clear") + .style(button::danger) + .on_press(Message::Clear), + ) + .padding(10) + .width(Length::Fill) + .align_x(alignment::Horizontal::Right) + }, + )) .padding(20) .into() } From bb9244107c8fee78f9b77f5ab0e4c2c31456d6c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 27 Apr 2024 06:08:30 +0200 Subject: [PATCH 21/66] Respect `width` and `height` properties when `Stack` is empty --- widget/src/stack.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/widget/src/stack.rs b/widget/src/stack.rs index 8a0ea2ebc4..2774d0359d 100644 --- a/widget/src/stack.rs +++ b/widget/src/stack.rs @@ -152,7 +152,11 @@ where limits: &layout::Limits, ) -> layout::Node { if self.children.is_empty() { - return layout::Node::new(Size::ZERO); + return layout::Node::new(limits.resolve( + self.width, + self.height, + Size::ZERO, + )); } let limits = limits.width(self.width).height(self.height); From 40dff6b23d4666d69e27addcfb90d9557f59ed6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 27 Apr 2024 06:15:58 +0200 Subject: [PATCH 22/66] Fix `overlay` behavior in `hover` widget --- widget/src/helpers.rs | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index c86223bc31..0b5adde786 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -313,7 +313,7 @@ where struct Hover<'a, Message, Theme, Renderer> { base: Element<'a, Message, Theme, Renderer>, top: Element<'a, Message, Theme, Renderer>, - is_overlay_active: bool, + is_top_overlay_active: bool, } impl<'a, Message, Theme, Renderer> Widget @@ -387,7 +387,7 @@ where viewport, ); - if cursor.is_over(layout.bounds()) || self.is_overlay_active { + if cursor.is_over(layout.bounds()) || self.is_top_overlay_active { let (top_layout, top_tree) = children.next().unwrap(); renderer.with_layer(layout.bounds(), |renderer| { @@ -491,13 +491,10 @@ where translation: core::Vector, ) -> Option> { - let overlay = [&mut self.base, &mut self.top] + let mut overlays = [&mut self.base, &mut self.top] .into_iter() - .rev() - .zip( - layout.children().rev().zip(tree.children.iter_mut().rev()), - ) - .find_map(|(child, (layout, tree))| { + .zip(layout.children().zip(tree.children.iter_mut())) + .map(|(child, (layout, tree))| { child.as_widget_mut().overlay( tree, layout, @@ -506,15 +503,21 @@ where ) }); - self.is_overlay_active = overlay.is_some(); - overlay + if let Some(base_overlay) = overlays.next()? { + return Some(base_overlay); + } + + let top_overlay = overlays.next()?; + self.is_top_overlay_active = top_overlay.is_some(); + + top_overlay } } Element::new(Hover { base: base.into(), top: top.into(), - is_overlay_active: false, + is_top_overlay_active: false, }) } From 95ac45e33d8490f0cc5c3cdea80be78f338d44e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 27 Apr 2024 06:19:25 +0200 Subject: [PATCH 23/66] Fix ambiguous link in documentation of `hover` helper --- widget/src/helpers.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 0b5adde786..5f27b6807d 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -294,6 +294,8 @@ where /// /// This works analogously to a [`stack`], but it will only display the layer on top /// when the cursor is over the base. It can be useful for removing visual clutter. +/// +/// [`stack`]: stack() pub fn hover<'a, Message, Theme, Renderer>( base: impl Into>, top: impl Into>, From c58155a971987529515570c0d137230e9bd8f4b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 27 Apr 2024 06:20:41 +0200 Subject: [PATCH 24/66] Set proper size boundaries for `limits` in `Stack::layout` --- widget/src/stack.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/widget/src/stack.rs b/widget/src/stack.rs index 2774d0359d..5035541b0b 100644 --- a/widget/src/stack.rs +++ b/widget/src/stack.rs @@ -151,6 +151,8 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { + let limits = limits.width(self.width).height(self.height); + if self.children.is_empty() { return layout::Node::new(limits.resolve( self.width, @@ -159,7 +161,6 @@ where )); } - let limits = limits.width(self.width).height(self.height); let base = self.children[0].as_widget().layout( &mut tree.children[0], renderer, From 9c0f2dc9a5ea172afd27b13d55dead40098eb7e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 27 Apr 2024 11:21:18 +0200 Subject: [PATCH 25/66] Fix top layer clipping in `hover` widget --- widget/src/helpers.rs | 45 +++++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 5f27b6807d..3940e3898d 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -375,29 +375,32 @@ where cursor: mouse::Cursor, viewport: &Rectangle, ) { - let mut children = layout.children().zip(&tree.children); + if let Some(bounds) = layout.bounds().intersection(viewport) { + let mut children = layout.children().zip(&tree.children); - let (base_layout, base_tree) = children.next().unwrap(); - - self.base.as_widget().draw( - base_tree, - renderer, - theme, - style, - base_layout, - cursor, - viewport, - ); - - if cursor.is_over(layout.bounds()) || self.is_top_overlay_active { - let (top_layout, top_tree) = children.next().unwrap(); + let (base_layout, base_tree) = children.next().unwrap(); - renderer.with_layer(layout.bounds(), |renderer| { - self.top.as_widget().draw( - top_tree, renderer, theme, style, top_layout, cursor, - viewport, - ); - }); + self.base.as_widget().draw( + base_tree, + renderer, + theme, + style, + base_layout, + cursor, + viewport, + ); + + if cursor.is_over(layout.bounds()) || self.is_top_overlay_active + { + let (top_layout, top_tree) = children.next().unwrap(); + + renderer.with_layer(bounds, |renderer| { + self.top.as_widget().draw( + top_tree, renderer, theme, style, top_layout, + cursor, viewport, + ); + }); + } } } From 05c90775816a97e44d499aaff5e9de57b6144e8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 27 Apr 2024 11:28:43 +0200 Subject: [PATCH 26/66] Propagate mouse movement and button releases unconditionally in `hover` --- widget/src/helpers.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 3940e3898d..48c5dde400 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -434,7 +434,14 @@ where let mut children = layout.children().zip(&mut tree.children); let (base_layout, base_tree) = children.next().unwrap(); - let top_status = if cursor.is_over(layout.bounds()) { + let top_status = if matches!( + event, + Event::Mouse( + mouse::Event::CursorMoved { .. } + | mouse::Event::ButtonReleased(_) + ) + ) || cursor.is_over(layout.bounds()) + { let (top_layout, top_tree) = children.next().unwrap(); self.top.as_widget_mut().on_event( From 2dcd4f916e0ea71f925212c8277498c6f995155b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 27 Apr 2024 14:16:12 +0200 Subject: [PATCH 27/66] Retain caches in `iced_wgpu` as long as `Rc` values are alive This allows reusing a `canvas::Cache` at no cost even if it is not presented every frame. --- wgpu/src/text.rs | 13 ++++++------- wgpu/src/triangle.rs | 13 ++++++------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 0d01faca6c..f20db026fa 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -4,9 +4,9 @@ use crate::graphics::color; use crate::graphics::text::cache::{self, Cache as BufferCache}; use crate::graphics::text::{font_system, to_color, Editor, Paragraph}; -use rustc_hash::{FxHashMap, FxHashSet}; +use rustc_hash::FxHashMap; use std::collections::hash_map; -use std::rc::Rc; +use std::rc::{self, Rc}; use std::sync::atomic::{self, AtomicU64}; use std::sync::Arc; @@ -69,12 +69,12 @@ struct Upload { buffer_cache: BufferCache, transformation: Transformation, version: usize, + text: rc::Weak<[Text]>, } #[derive(Default)] pub struct Storage { uploads: FxHashMap, - recently_used: FxHashSet, } impl Storage { @@ -162,6 +162,7 @@ impl Storage { buffer_cache, transformation: new_transformation, version: 0, + text: Rc::downgrade(&cache.text), }); log::info!( @@ -171,13 +172,11 @@ impl Storage { ); } } - - let _ = self.recently_used.insert(cache.id); } pub fn trim(&mut self) { - self.uploads.retain(|id, _| self.recently_used.contains(id)); - self.recently_used.clear(); + self.uploads + .retain(|_id, upload| upload.text.strong_count() > 0); } } diff --git a/wgpu/src/triangle.rs b/wgpu/src/triangle.rs index 8470ea3960..53a5502aa4 100644 --- a/wgpu/src/triangle.rs +++ b/wgpu/src/triangle.rs @@ -6,9 +6,9 @@ use crate::graphics::mesh::{self, Mesh}; use crate::graphics::Antialiasing; use crate::Buffer; -use rustc_hash::{FxHashMap, FxHashSet}; +use rustc_hash::FxHashMap; use std::collections::hash_map; -use std::rc::Rc; +use std::rc::{self, Rc}; use std::sync::atomic::{self, AtomicU64}; const INITIAL_INDEX_COUNT: usize = 1_000; @@ -64,12 +64,12 @@ struct Upload { layer: Layer, transformation: Transformation, version: usize, + batch: rc::Weak<[Mesh]>, } #[derive(Debug, Default)] pub struct Storage { uploads: FxHashMap, - recently_used: FxHashSet, } impl Storage { @@ -134,6 +134,7 @@ impl Storage { layer, transformation: new_transformation, version: 0, + batch: Rc::downgrade(&cache.batch), }); log::info!( @@ -143,13 +144,11 @@ impl Storage { ); } } - - let _ = self.recently_used.insert(cache.id); } pub fn trim(&mut self) { - self.uploads.retain(|id, _| self.recently_used.contains(id)); - self.recently_used.clear(); + self.uploads + .retain(|_id, upload| upload.batch.strong_count() > 0); } } From aeede66963a2e512ed299bdfd502c2c63a99cca9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 27 Apr 2024 15:11:03 +0200 Subject: [PATCH 28/66] Create `the_matrix` example --- examples/the_matrix/Cargo.toml | 12 ++++ examples/the_matrix/src/main.rs | 116 ++++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 examples/the_matrix/Cargo.toml create mode 100644 examples/the_matrix/src/main.rs diff --git a/examples/the_matrix/Cargo.toml b/examples/the_matrix/Cargo.toml new file mode 100644 index 0000000000..17cf443bf9 --- /dev/null +++ b/examples/the_matrix/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "the_matrix" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez "] +edition = "2021" +publish = false + +[dependencies] +iced.workspace = true +iced.features = ["canvas", "tokio", "debug"] + +rand = "0.8" diff --git a/examples/the_matrix/src/main.rs b/examples/the_matrix/src/main.rs new file mode 100644 index 0000000000..55c9da4b34 --- /dev/null +++ b/examples/the_matrix/src/main.rs @@ -0,0 +1,116 @@ +use iced::mouse; +use iced::time::{self, Instant}; +use iced::widget::canvas; +use iced::widget::canvas::{Cache, Geometry}; +use iced::{ + Color, Element, Font, Length, Point, Rectangle, Renderer, Subscription, + Theme, +}; + +pub fn main() -> iced::Result { + iced::program("The Matrix - Iced", TheMatrix::update, TheMatrix::view) + .subscription(TheMatrix::subscription) + .antialiasing(true) + .run() +} + +struct TheMatrix { + ticks: usize, + backgrounds: Vec, +} + +#[derive(Debug, Clone, Copy)] +enum Message { + Tick(Instant), +} + +impl TheMatrix { + fn update(&mut self, message: Message) { + match message { + Message::Tick(_now) => { + self.ticks += 1; + } + } + } + + fn view(&self) -> Element { + canvas(self as &Self) + .width(Length::Fill) + .height(Length::Fill) + .into() + } + + fn subscription(&self) -> Subscription { + time::every(std::time::Duration::from_millis(50)).map(Message::Tick) + } +} + +impl Default for TheMatrix { + fn default() -> Self { + let mut backgrounds = Vec::with_capacity(30); + backgrounds.resize_with(30, Cache::default); + + Self { + ticks: 0, + backgrounds, + } + } +} + +impl canvas::Program for TheMatrix { + type State = (); + + fn draw( + &self, + _state: &Self::State, + renderer: &Renderer, + _theme: &Theme, + bounds: Rectangle, + _cursor: mouse::Cursor, + ) -> Vec { + use rand::distributions::Distribution; + use rand::Rng; + + const CELL_SIZE: f32 = 10.0; + + vec![self.backgrounds[self.ticks % self.backgrounds.len()].draw( + renderer, + bounds.size(), + |frame| { + frame.fill_rectangle(Point::ORIGIN, frame.size(), Color::BLACK); + + let mut rng = rand::thread_rng(); + let rows = (frame.height() / CELL_SIZE).ceil() as usize; + let columns = (frame.width() / CELL_SIZE).ceil() as usize; + + for row in 0..rows { + for column in 0..columns { + let position = Point::new( + column as f32 * CELL_SIZE, + row as f32 * CELL_SIZE, + ); + + let alphas = [0.05, 0.1, 0.2, 0.5]; + let weights = [10, 4, 2, 1]; + let distribution = + rand::distributions::WeightedIndex::new(weights) + .expect("Create distribution"); + + frame.fill_text(canvas::Text { + content: rng.gen_range('!'..'z').to_string(), + position, + color: Color { + a: alphas[distribution.sample(&mut rng)], + g: 1.0, + ..Color::BLACK + }, + size: CELL_SIZE.into(), + font: Font::MONOSPACE, + ..canvas::Text::default() + }); + } + } + }, + )] + } +} From 24501fd73b5ae884367a2d112ff44625058b876b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 30 Apr 2024 05:13:24 +0200 Subject: [PATCH 29/66] Fix `text` and `triangle` uploads being dropped on `canvas` cache clears --- wgpu/src/text.rs | 1 + wgpu/src/triangle.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index f20db026fa..3871266092 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -122,6 +122,7 @@ impl Storage { target_size, ); + upload.text = Rc::downgrade(&cache.text); upload.version = cache.version; upload.transformation = new_transformation; diff --git a/wgpu/src/triangle.rs b/wgpu/src/triangle.rs index 53a5502aa4..ca36de826d 100644 --- a/wgpu/src/triangle.rs +++ b/wgpu/src/triangle.rs @@ -113,6 +113,7 @@ impl Storage { new_transformation, ); + upload.batch = Rc::downgrade(&cache.batch); upload.version = cache.version; upload.transformation = new_transformation; } From b5b78d505e22cafccb4ecbf57dc61f536ca558ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 30 Apr 2024 07:57:54 +0200 Subject: [PATCH 30/66] Introduce `canvas::Cache` grouping Caches with the same `Group` will share their text atlas! --- graphics/src/cache.rs | 158 +++++++++++++++++++++++++++++++++ graphics/src/cached.rs | 24 ----- graphics/src/geometry.rs | 2 +- graphics/src/geometry/cache.rs | 83 ++++++++--------- graphics/src/lib.rs | 4 +- renderer/src/fallback.rs | 16 ++-- tiny_skia/src/geometry.rs | 5 +- wgpu/src/geometry.rs | 13 ++- wgpu/src/text.rs | 74 +++++++++++---- widget/src/canvas.rs | 1 + 10 files changed, 279 insertions(+), 101 deletions(-) create mode 100644 graphics/src/cache.rs delete mode 100644 graphics/src/cached.rs diff --git a/graphics/src/cache.rs b/graphics/src/cache.rs new file mode 100644 index 0000000000..7106c3929a --- /dev/null +++ b/graphics/src/cache.rs @@ -0,0 +1,158 @@ +//! Cache computations and efficiently reuse them. +use std::cell::RefCell; +use std::fmt; +use std::sync::atomic::{self, AtomicU64}; + +/// A simple cache that stores generated values to avoid recomputation. +/// +/// Keeps track of the last generated value after clearing. +pub struct Cache { + group: Group, + state: RefCell>, +} + +impl Cache { + /// Creates a new empty [`Cache`]. + pub fn new() -> Self { + Cache { + group: Group::unique(), + state: RefCell::new(State::Empty { previous: None }), + } + } + + /// Creates a new empty [`Cache`] with the given [`Group`]. + /// + /// Caches within the same group may reuse internal rendering storage. + /// + /// You should generally group caches that are likely to change + /// together. + pub fn with_group(group: Group) -> Self { + Cache { + group, + state: RefCell::new(State::Empty { previous: None }), + } + } + + /// Returns the [`Group`] of the [`Cache`]. + pub fn group(&self) -> Group { + self.group + } + + /// Puts the given value in the [`Cache`]. + /// + /// Notice that, given this is a cache, a mutable reference is not + /// necessary to call this method. You can safely update the cache in + /// rendering code. + pub fn put(&self, value: T) { + *self.state.borrow_mut() = State::Filled { current: value }; + } + + /// Returns a reference cell to the internal [`State`] of the [`Cache`]. + pub fn state(&self) -> &RefCell> { + &self.state + } + + /// Clears the [`Cache`]. + pub fn clear(&self) + where + T: Clone, + { + use std::ops::Deref; + + let previous = match self.state.borrow().deref() { + State::Empty { previous } => previous.clone(), + State::Filled { current } => Some(current.clone()), + }; + + *self.state.borrow_mut() = State::Empty { previous }; + } +} + +/// A cache group. +/// +/// Caches that share the same group generally change together. +/// +/// A cache group can be used to implement certain performance +/// optimizations during rendering, like batching or sharing atlases. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Group(u64); + +impl Group { + /// Generates a new unique cache [`Group`]. + pub fn unique() -> Self { + static NEXT: AtomicU64 = AtomicU64::new(0); + + Self(NEXT.fetch_add(1, atomic::Ordering::Relaxed)) + } +} + +impl fmt::Debug for Cache +where + T: fmt::Debug, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use std::ops::Deref; + + let state = self.state.borrow(); + + match state.deref() { + State::Empty { previous } => { + write!(f, "Cache::Empty {{ previous: {previous:?} }}") + } + State::Filled { current } => { + write!(f, "Cache::Filled {{ current: {current:?} }}") + } + } + } +} + +impl Default for Cache { + fn default() -> Self { + Self::new() + } +} + +/// The state of a [`Cache`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum State { + /// The [`Cache`] is empty. + Empty { + /// The previous value of the [`Cache`]. + previous: Option, + }, + /// The [`Cache`] is filled. + Filled { + /// The current value of the [`Cache`] + current: T, + }, +} + +/// A piece of data that can be cached. +pub trait Cached: Sized { + /// The type of cache produced. + type Cache: Clone; + + /// Loads the [`Cache`] into a proper instance. + /// + /// [`Cache`]: Self::Cache + fn load(cache: &Self::Cache) -> Self; + + /// Caches this value, producing its corresponding [`Cache`]. + /// + /// [`Cache`]: Self::Cache + fn cache(self, group: Group, previous: Option) -> Self::Cache; +} + +#[cfg(debug_assertions)] +impl Cached for () { + type Cache = (); + + fn load(_cache: &Self::Cache) -> Self {} + + fn cache( + self, + _group: Group, + _previous: Option, + ) -> Self::Cache { + } +} diff --git a/graphics/src/cached.rs b/graphics/src/cached.rs deleted file mode 100644 index c0e4e02969..0000000000 --- a/graphics/src/cached.rs +++ /dev/null @@ -1,24 +0,0 @@ -/// A piece of data that can be cached. -pub trait Cached: Sized { - /// The type of cache produced. - type Cache: Clone; - - /// Loads the [`Cache`] into a proper instance. - /// - /// [`Cache`]: Self::Cache - fn load(cache: &Self::Cache) -> Self; - - /// Caches this value, producing its corresponding [`Cache`]. - /// - /// [`Cache`]: Self::Cache - fn cache(self, previous: Option) -> Self::Cache; -} - -#[cfg(debug_assertions)] -impl Cached for () { - type Cache = (); - - fn load(_cache: &Self::Cache) -> Self {} - - fn cache(self, _previous: Option) -> Self::Cache {} -} diff --git a/graphics/src/geometry.rs b/graphics/src/geometry.rs index dd07097fb6..ab4a7a365e 100644 --- a/graphics/src/geometry.rs +++ b/graphics/src/geometry.rs @@ -18,8 +18,8 @@ pub use text::Text; pub use crate::gradient::{self, Gradient}; +use crate::cache::Cached; use crate::core::{self, Size}; -use crate::Cached; /// A renderer capable of drawing some [`Self::Geometry`]. pub trait Renderer: core::Renderer { diff --git a/graphics/src/geometry/cache.rs b/graphics/src/geometry/cache.rs index 665e996b04..d70cee0bfe 100644 --- a/graphics/src/geometry/cache.rs +++ b/graphics/src/geometry/cache.rs @@ -1,8 +1,8 @@ +use crate::cache::{self, Cached}; use crate::core::Size; use crate::geometry::{self, Frame}; -use crate::Cached; -use std::cell::RefCell; +pub use cache::Group; /// A simple cache that stores generated geometry to avoid recomputation. /// @@ -12,7 +12,13 @@ pub struct Cache where Renderer: geometry::Renderer, { - state: RefCell>, + raw: crate::Cache::Cache>>, +} + +#[derive(Debug, Clone)] +struct Data { + bounds: Size, + geometry: T, } impl Cache @@ -22,20 +28,25 @@ where /// Creates a new empty [`Cache`]. pub fn new() -> Self { Cache { - state: RefCell::new(State::Empty { previous: None }), + raw: cache::Cache::new(), + } + } + + /// Creates a new empty [`Cache`] with the given [`Group`]. + /// + /// Caches within the same group may reuse internal rendering storage. + /// + /// You should generally group caches that are likely to change + /// together. + pub fn with_group(group: Group) -> Self { + Cache { + raw: crate::Cache::with_group(group), } } /// Clears the [`Cache`], forcing a redraw the next time it is used. pub fn clear(&self) { - use std::ops::Deref; - - let previous = match self.state.borrow().deref() { - State::Empty { previous } => previous.clone(), - State::Filled { geometry, .. } => Some(geometry.clone()), - }; - - *self.state.borrow_mut() = State::Empty { previous }; + self.raw.clear(); } /// Draws geometry using the provided closure and stores it in the @@ -56,27 +67,30 @@ where ) -> Renderer::Geometry { use std::ops::Deref; - let previous = match self.state.borrow().deref() { - State::Empty { previous } => previous.clone(), - State::Filled { - bounds: cached_bounds, - geometry, - } => { - if *cached_bounds == bounds { - return Cached::load(geometry); + let state = self.raw.state(); + + let previous = match state.borrow().deref() { + cache::State::Empty { previous } => { + previous.as_ref().map(|data| data.geometry.clone()) + } + cache::State::Filled { current } => { + if current.bounds == bounds { + return Cached::load(¤t.geometry); } - Some(geometry.clone()) + Some(current.geometry.clone()) } }; let mut frame = Frame::new(renderer, bounds); draw_fn(&mut frame); - let geometry = frame.into_geometry().cache(previous); + let geometry = frame.into_geometry().cache(self.raw.group(), previous); let result = Cached::load(&geometry); - *self.state.borrow_mut() = State::Filled { bounds, geometry }; + *state.borrow_mut() = cache::State::Filled { + current: Data { bounds, geometry }, + }; result } @@ -85,16 +99,10 @@ where impl std::fmt::Debug for Cache where Renderer: geometry::Renderer, + ::Cache: std::fmt::Debug, { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let state = self.state.borrow(); - - match *state { - State::Empty { .. } => write!(f, "Cache::Empty"), - State::Filled { bounds, .. } => { - write!(f, "Cache::Filled {{ bounds: {bounds:?} }}") - } - } + write!(f, "{:?}", &self.raw) } } @@ -106,16 +114,3 @@ where Self::new() } } - -enum State -where - Geometry: Cached, -{ - Empty { - previous: Option, - }, - Filled { - bounds: Size, - geometry: Geometry::Cache, - }, -} diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs index 865ebd9742..b5ef55e7ab 100644 --- a/graphics/src/lib.rs +++ b/graphics/src/lib.rs @@ -9,10 +9,10 @@ )] #![cfg_attr(docsrs, feature(doc_auto_cfg))] mod antialiasing; -mod cached; mod settings; mod viewport; +pub mod cache; pub mod color; pub mod compositor; pub mod damage; @@ -27,7 +27,7 @@ pub mod text; pub mod geometry; pub use antialiasing::Antialiasing; -pub use cached::Cached; +pub use cache::Cache; pub use compositor::Compositor; pub use error::Error; pub use gradient::Gradient; diff --git a/renderer/src/fallback.rs b/renderer/src/fallback.rs index c932de0049..5f69b42015 100644 --- a/renderer/src/fallback.rs +++ b/renderer/src/fallback.rs @@ -428,8 +428,8 @@ where mod geometry { use super::Renderer; use crate::core::{Point, Radians, Rectangle, Size, Vector}; + use crate::graphics::cache::{self, Cached}; use crate::graphics::geometry::{self, Fill, Path, Stroke, Text}; - use crate::graphics::Cached; impl geometry::Renderer for Renderer where @@ -483,21 +483,25 @@ mod geometry { } } - fn cache(self, previous: Option) -> Self::Cache { + fn cache( + self, + group: cache::Group, + previous: Option, + ) -> Self::Cache { match (self, previous) { ( Self::Primary(geometry), Some(Geometry::Primary(previous)), - ) => Geometry::Primary(geometry.cache(Some(previous))), + ) => Geometry::Primary(geometry.cache(group, Some(previous))), (Self::Primary(geometry), None) => { - Geometry::Primary(geometry.cache(None)) + Geometry::Primary(geometry.cache(group, None)) } ( Self::Secondary(geometry), Some(Geometry::Secondary(previous)), - ) => Geometry::Secondary(geometry.cache(Some(previous))), + ) => Geometry::Secondary(geometry.cache(group, Some(previous))), (Self::Secondary(geometry), None) => { - Geometry::Secondary(geometry.cache(None)) + Geometry::Secondary(geometry.cache(group, None)) } _ => unreachable!(), } diff --git a/tiny_skia/src/geometry.rs b/tiny_skia/src/geometry.rs index 117daf413a..02b6e1b9c5 100644 --- a/tiny_skia/src/geometry.rs +++ b/tiny_skia/src/geometry.rs @@ -1,9 +1,10 @@ use crate::core::text::LineHeight; use crate::core::{Pixels, Point, Radians, Rectangle, Size, Vector}; +use crate::graphics::cache::{self, Cached}; use crate::graphics::geometry::fill::{self, Fill}; use crate::graphics::geometry::stroke::{self, Stroke}; use crate::graphics::geometry::{self, Path, Style}; -use crate::graphics::{Cached, Gradient, Text}; +use crate::graphics::{Gradient, Text}; use crate::Primitive; use std::rc::Rc; @@ -32,7 +33,7 @@ impl Cached for Geometry { Self::Cache(cache.clone()) } - fn cache(self, _previous: Option) -> Cache { + fn cache(self, _group: cache::Group, _previous: Option) -> Cache { match self { Self::Live { primitives, diff --git a/wgpu/src/geometry.rs b/wgpu/src/geometry.rs index 60967082bf..f6213e1db2 100644 --- a/wgpu/src/geometry.rs +++ b/wgpu/src/geometry.rs @@ -3,6 +3,7 @@ use crate::core::text::LineHeight; use crate::core::{ Pixels, Point, Radians, Rectangle, Size, Transformation, Vector, }; +use crate::graphics::cache::{self, Cached}; use crate::graphics::color; use crate::graphics::geometry::fill::{self, Fill}; use crate::graphics::geometry::{ @@ -10,7 +11,7 @@ use crate::graphics::geometry::{ }; use crate::graphics::gradient::{self, Gradient}; use crate::graphics::mesh::{self, Mesh}; -use crate::graphics::{self, Cached, Text}; +use crate::graphics::{self, Text}; use crate::text; use crate::triangle; @@ -38,7 +39,11 @@ impl Cached for Geometry { Geometry::Cached(cache.clone()) } - fn cache(self, previous: Option) -> Self::Cache { + fn cache( + self, + group: cache::Group, + previous: Option, + ) -> Self::Cache { match self { Self::Live { meshes, text } => { if let Some(mut previous) = previous { @@ -51,14 +56,14 @@ impl Cached for Geometry { if let Some(cache) = &mut previous.text { cache.update(text); } else { - previous.text = text::Cache::new(text); + previous.text = text::Cache::new(group, text); } previous } else { Cache { meshes: triangle::Cache::new(meshes), - text: text::Cache::new(text), + text: text::Cache::new(group, text), } } } diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 3871266092..5806863c58 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -1,7 +1,8 @@ use crate::core::alignment; use crate::core::{Rectangle, Size, Transformation}; +use crate::graphics::cache; use crate::graphics::color; -use crate::graphics::text::cache::{self, Cache as BufferCache}; +use crate::graphics::text::cache::{self as text_cache, Cache as BufferCache}; use crate::graphics::text::{font_system, to_color, Editor, Paragraph}; use rustc_hash::FxHashMap; @@ -35,6 +36,7 @@ pub enum Item { #[derive(Debug, Clone)] pub struct Cache { id: Id, + group: cache::Group, text: Rc<[Text]>, version: usize, } @@ -43,7 +45,7 @@ pub struct Cache { pub struct Id(u64); impl Cache { - pub fn new(text: Vec) -> Option { + pub fn new(group: cache::Group, text: Vec) -> Option { static NEXT_ID: AtomicU64 = AtomicU64::new(0); if text.is_empty() { @@ -52,6 +54,7 @@ impl Cache { Some(Self { id: Id(NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed)), + group, text: Rc::from(text), version: 0, }) @@ -65,29 +68,39 @@ impl Cache { struct Upload { renderer: glyphon::TextRenderer, - atlas: glyphon::TextAtlas, buffer_cache: BufferCache, transformation: Transformation, version: usize, text: rc::Weak<[Text]>, + _atlas: rc::Weak<()>, } #[derive(Default)] pub struct Storage { + groups: FxHashMap, uploads: FxHashMap, } +struct Group { + atlas: glyphon::TextAtlas, + previous_uploads: usize, + handle: Rc<()>, +} + impl Storage { pub fn new() -> Self { Self::default() } - fn get(&self, cache: &Cache) -> Option<&Upload> { + fn get(&self, cache: &Cache) -> Option<(&glyphon::TextAtlas, &Upload)> { if cache.text.is_empty() { return None; } - self.uploads.get(&cache.id) + self.groups + .get(&cache.group) + .map(|group| &group.atlas) + .zip(self.uploads.get(&cache.id)) } fn prepare( @@ -101,6 +114,20 @@ impl Storage { bounds: Rectangle, target_size: Size, ) { + let group_count = self.groups.len(); + + let group = self.groups.entry(cache.group).or_insert_with(|| { + log::info!("New text atlas created (total: {})", group_count + 1); + + Group { + atlas: glyphon::TextAtlas::with_color_mode( + device, queue, format, COLOR_MODE, + ), + previous_uploads: 0, + handle: Rc::new(()), + } + }); + match self.uploads.entry(cache.id) { hash_map::Entry::Occupied(entry) => { let upload = entry.into_mut(); @@ -114,7 +141,7 @@ impl Storage { queue, encoder, &mut upload.renderer, - &mut upload.atlas, + &mut group.atlas, &mut upload.buffer_cache, &cache.text, bounds, @@ -127,16 +154,11 @@ impl Storage { upload.transformation = new_transformation; upload.buffer_cache.trim(); - upload.atlas.trim(); } } hash_map::Entry::Vacant(entry) => { - let mut atlas = glyphon::TextAtlas::with_color_mode( - device, queue, format, COLOR_MODE, - ); - let mut renderer = glyphon::TextRenderer::new( - &mut atlas, + &mut group.atlas, device, wgpu::MultisampleState::default(), None, @@ -149,7 +171,7 @@ impl Storage { queue, encoder, &mut renderer, - &mut atlas, + &mut group.atlas, &mut buffer_cache, &cache.text, bounds, @@ -159,11 +181,11 @@ impl Storage { let _ = entry.insert(Upload { renderer, - atlas, buffer_cache, transformation: new_transformation, version: 0, text: Rc::downgrade(&cache.text), + _atlas: Rc::downgrade(&group.handle), }); log::info!( @@ -178,6 +200,22 @@ impl Storage { pub fn trim(&mut self) { self.uploads .retain(|_id, upload| upload.text.strong_count() > 0); + + self.groups.retain(|_id, group| { + let uploads_alive = Rc::weak_count(&group.handle); + + if uploads_alive == 0 { + return false; + } + + if uploads_alive < group.previous_uploads { + group.atlas.trim(); + } + + group.previous_uploads = uploads_alive; + + true + }); } } @@ -306,10 +344,10 @@ impl Pipeline { layer_count += 1; } Item::Cached { cache, .. } => { - if let Some(upload) = storage.get(cache) { + if let Some((atlas, upload)) = storage.get(cache) { upload .renderer - .render(&upload.atlas, render_pass) + .render(atlas, render_pass) .expect("Render cached text"); } } @@ -345,7 +383,7 @@ fn prepare( enum Allocation { Paragraph(Paragraph), Editor(Editor), - Cache(cache::KeyHash), + Cache(text_cache::KeyHash), Raw(Arc), } @@ -369,7 +407,7 @@ fn prepare( } => { let (key, _) = buffer_cache.allocate( font_system, - cache::Key { + text_cache::Key { content, size: f32::from(*size), line_height: f32::from(*line_height), diff --git a/widget/src/canvas.rs b/widget/src/canvas.rs index 42f92de055..be09f1635a 100644 --- a/widget/src/canvas.rs +++ b/widget/src/canvas.rs @@ -6,6 +6,7 @@ mod program; pub use event::Event; pub use program::Program; +pub use crate::graphics::cache::Group; pub use crate::graphics::geometry::{ fill, gradient, path, stroke, Fill, Gradient, LineCap, LineDash, LineJoin, Path, Stroke, Style, Text, From 3c7b43d031a06d59afbba83bc9d088517c096f20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 30 Apr 2024 08:06:51 +0200 Subject: [PATCH 31/66] Use shared `Cache` group in `the_matrix` example --- examples/the_matrix/src/main.rs | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/examples/the_matrix/src/main.rs b/examples/the_matrix/src/main.rs index 55c9da4b34..97ad31b967 100644 --- a/examples/the_matrix/src/main.rs +++ b/examples/the_matrix/src/main.rs @@ -1,12 +1,13 @@ use iced::mouse; use iced::time::{self, Instant}; use iced::widget::canvas; -use iced::widget::canvas::{Cache, Geometry}; use iced::{ Color, Element, Font, Length, Point, Rectangle, Renderer, Subscription, Theme, }; +use std::cell::RefCell; + pub fn main() -> iced::Result { iced::program("The Matrix - Iced", TheMatrix::update, TheMatrix::view) .subscription(TheMatrix::subscription) @@ -15,8 +16,7 @@ pub fn main() -> iced::Result { } struct TheMatrix { - ticks: usize, - backgrounds: Vec, + tick: usize, } #[derive(Debug, Clone, Copy)] @@ -28,7 +28,7 @@ impl TheMatrix { fn update(&mut self, message: Message) { match message { Message::Tick(_now) => { - self.ticks += 1; + self.tick += 1; } } } @@ -47,33 +47,35 @@ impl TheMatrix { impl Default for TheMatrix { fn default() -> Self { - let mut backgrounds = Vec::with_capacity(30); - backgrounds.resize_with(30, Cache::default); - - Self { - ticks: 0, - backgrounds, - } + Self { tick: 0 } } } impl canvas::Program for TheMatrix { - type State = (); + type State = RefCell>; fn draw( &self, - _state: &Self::State, + state: &Self::State, renderer: &Renderer, _theme: &Theme, bounds: Rectangle, _cursor: mouse::Cursor, - ) -> Vec { + ) -> Vec { use rand::distributions::Distribution; use rand::Rng; const CELL_SIZE: f32 = 10.0; - vec![self.backgrounds[self.ticks % self.backgrounds.len()].draw( + let mut caches = state.borrow_mut(); + + if caches.is_empty() { + let group = canvas::Group::unique(); + + caches.resize_with(30, || canvas::Cache::with_group(group)); + } + + vec![caches[self.tick % caches.len()].draw( renderer, bounds.size(), |frame| { From cfe4ddb86620f9b21027c8a828e80df9a8887407 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 30 Apr 2024 08:10:22 +0200 Subject: [PATCH 32/66] Fix `clippy` lint in `the_matrix` example --- examples/the_matrix/src/main.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/examples/the_matrix/src/main.rs b/examples/the_matrix/src/main.rs index 97ad31b967..4ed1db320f 100644 --- a/examples/the_matrix/src/main.rs +++ b/examples/the_matrix/src/main.rs @@ -15,6 +15,7 @@ pub fn main() -> iced::Result { .run() } +#[derive(Default)] struct TheMatrix { tick: usize, } @@ -45,12 +46,6 @@ impl TheMatrix { } } -impl Default for TheMatrix { - fn default() -> Self { - Self { tick: 0 } - } -} - impl canvas::Program for TheMatrix { type State = RefCell>; From f6c698b2dd09288203e5732c2051661ee0580e76 Mon Sep 17 00:00:00 2001 From: JL710 <76447362+JL710@users.noreply.github.com> Date: Tue, 30 Apr 2024 10:28:06 +0200 Subject: [PATCH 33/66] add missing derive(Debug) in lib.rs docs --- src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib.rs b/src/lib.rs index c96c28a381..7517dd1488 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -51,6 +51,7 @@ //! We start by modelling the __state__ of our application: //! //! ``` +//! #[derive(Default)] //! struct Counter { //! // The counter value //! value: i32, From c51b85e7ab067f5e7411eccd10a5ae192e6ee0a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 30 Apr 2024 21:59:46 +0200 Subject: [PATCH 34/66] Invalidate text uploads after atlas trimming --- examples/the_matrix/Cargo.toml | 1 + examples/the_matrix/src/main.rs | 2 ++ wgpu/src/text.rs | 36 +++++++++++++++++++++++---------- wgpu/src/triangle.rs | 2 +- 4 files changed, 29 insertions(+), 12 deletions(-) diff --git a/examples/the_matrix/Cargo.toml b/examples/the_matrix/Cargo.toml index 17cf443bf9..775e76e0d9 100644 --- a/examples/the_matrix/Cargo.toml +++ b/examples/the_matrix/Cargo.toml @@ -10,3 +10,4 @@ iced.workspace = true iced.features = ["canvas", "tokio", "debug"] rand = "0.8" +tracing-subscriber = "0.3" diff --git a/examples/the_matrix/src/main.rs b/examples/the_matrix/src/main.rs index 4ed1db320f..f3a67ac840 100644 --- a/examples/the_matrix/src/main.rs +++ b/examples/the_matrix/src/main.rs @@ -9,6 +9,8 @@ use iced::{ use std::cell::RefCell; pub fn main() -> iced::Result { + tracing_subscriber::fmt::init(); + iced::program("The Matrix - Iced", TheMatrix::update, TheMatrix::view) .subscription(TheMatrix::subscription) .antialiasing(true) diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 5806863c58..8ec1d56afd 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -71,6 +71,7 @@ struct Upload { buffer_cache: BufferCache, transformation: Transformation, version: usize, + group_version: usize, text: rc::Weak<[Text]>, _atlas: rc::Weak<()>, } @@ -83,8 +84,9 @@ pub struct Storage { struct Group { atlas: glyphon::TextAtlas, - previous_uploads: usize, - handle: Rc<()>, + version: usize, + should_trim: bool, + handle: Rc<()>, // Keeps track of active uploads } impl Storage { @@ -117,13 +119,18 @@ impl Storage { let group_count = self.groups.len(); let group = self.groups.entry(cache.group).or_insert_with(|| { - log::info!("New text atlas created (total: {})", group_count + 1); + log::debug!( + "New text atlas: {:?} (total: {})", + cache.group, + group_count + 1 + ); Group { atlas: glyphon::TextAtlas::with_color_mode( device, queue, format, COLOR_MODE, ), - previous_uploads: 0, + version: 0, + should_trim: false, handle: Rc::new(()), } }); @@ -134,6 +141,7 @@ impl Storage { if !cache.text.is_empty() && (upload.version != cache.version + || upload.group_version != group.version || upload.transformation != new_transformation) { let _ = prepare( @@ -151,9 +159,11 @@ impl Storage { upload.text = Rc::downgrade(&cache.text); upload.version = cache.version; + upload.group_version = group.version; upload.transformation = new_transformation; upload.buffer_cache.trim(); + group.should_trim = true; } } hash_map::Entry::Vacant(entry) => { @@ -184,11 +194,12 @@ impl Storage { buffer_cache, transformation: new_transformation, version: 0, + group_version: group.version, text: Rc::downgrade(&cache.text), _atlas: Rc::downgrade(&group.handle), }); - log::info!( + log::debug!( "New text upload: {} (total: {})", cache.id.0, self.uploads.len() @@ -201,18 +212,21 @@ impl Storage { self.uploads .retain(|_id, upload| upload.text.strong_count() > 0); - self.groups.retain(|_id, group| { - let uploads_alive = Rc::weak_count(&group.handle); + self.groups.retain(|id, group| { + if Rc::weak_count(&group.handle) == 0 { + log::debug!("Dropping text atlas: {id:?}"); - if uploads_alive == 0 { return false; } - if uploads_alive < group.previous_uploads { + if group.should_trim { + log::debug!("Trimming text atlas: {id:?}"); + group.atlas.trim(); - } - group.previous_uploads = uploads_alive; + group.version += 1; + group.should_trim = false; + } true }); diff --git a/wgpu/src/triangle.rs b/wgpu/src/triangle.rs index ca36de826d..b0551f5596 100644 --- a/wgpu/src/triangle.rs +++ b/wgpu/src/triangle.rs @@ -138,7 +138,7 @@ impl Storage { batch: Rc::downgrade(&cache.batch), }); - log::info!( + log::debug!( "New mesh upload: {} (total: {})", cache.id.0, self.uploads.len() From b276a603a17eda219b32f207aa53e2b6a1321a9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 30 Apr 2024 23:15:04 +0200 Subject: [PATCH 35/66] Fix cache trimming loop in `iced_wgpu::text` --- graphics/src/cache.rs | 37 ++++++++++++++++++++++++++++++++++--- wgpu/src/text.rs | 24 ++++++++++++++++++------ 2 files changed, 52 insertions(+), 9 deletions(-) diff --git a/graphics/src/cache.rs b/graphics/src/cache.rs index 7106c3929a..bbba79eb40 100644 --- a/graphics/src/cache.rs +++ b/graphics/src/cache.rs @@ -15,7 +15,7 @@ impl Cache { /// Creates a new empty [`Cache`]. pub fn new() -> Self { Cache { - group: Group::unique(), + group: Group::singleton(), state: RefCell::new(State::Empty { previous: None }), } } @@ -27,6 +27,11 @@ impl Cache { /// You should generally group caches that are likely to change /// together. pub fn with_group(group: Group) -> Self { + assert!( + !group.is_singleton(), + "The group {group:?} cannot be shared!" + ); + Cache { group, state: RefCell::new(State::Empty { previous: None }), @@ -75,14 +80,40 @@ impl Cache { /// A cache group can be used to implement certain performance /// optimizations during rendering, like batching or sharing atlases. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct Group(u64); +pub struct Group { + id: u64, + is_singleton: bool, +} impl Group { /// Generates a new unique cache [`Group`]. pub fn unique() -> Self { static NEXT: AtomicU64 = AtomicU64::new(0); - Self(NEXT.fetch_add(1, atomic::Ordering::Relaxed)) + Self { + id: NEXT.fetch_add(1, atomic::Ordering::Relaxed), + is_singleton: false, + } + } + + /// Returns `true` if the [`Group`] can only ever have a + /// single [`Cache`] in it. + /// + /// This is the default kind of [`Group`] assigned when using + /// [`Cache::new`]. + /// + /// Knowing that a [`Group`] will never be shared may be + /// useful for rendering backends to perform additional + /// optimizations. + pub fn is_singleton(self) -> bool { + self.is_singleton + } + + fn singleton() -> Self { + Self { + is_singleton: true, + ..Self::unique() + } } } diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 8ec1d56afd..2508906fba 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -157,13 +157,16 @@ impl Storage { target_size, ); + // Only trim if glyphs have changed + group.should_trim = + group.should_trim || upload.version != cache.version; + upload.text = Rc::downgrade(&cache.text); upload.version = cache.version; upload.group_version = group.version; upload.transformation = new_transformation; upload.buffer_cache.trim(); - group.should_trim = true; } } hash_map::Entry::Vacant(entry) => { @@ -213,19 +216,28 @@ impl Storage { .retain(|_id, upload| upload.text.strong_count() > 0); self.groups.retain(|id, group| { - if Rc::weak_count(&group.handle) == 0 { + let active_uploads = Rc::weak_count(&group.handle); + + if active_uploads == 0 { log::debug!("Dropping text atlas: {id:?}"); return false; } - if group.should_trim { - log::debug!("Trimming text atlas: {id:?}"); + if id.is_singleton() || group.should_trim { + log::debug!( + "Trimming text atlas: {id:?} (uploads: {active_uploads})" + ); group.atlas.trim(); - - group.version += 1; group.should_trim = false; + + // We only need to worry about glyph fighting + // when the atlas may be shared by multiple + // uploads. + if !id.is_singleton() { + group.version += 1; + } } true From 7e2d0dc931a4409e661851a1c7dffdcfd5b2f93a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 30 Apr 2024 23:51:00 +0200 Subject: [PATCH 36/66] Keep text atlases alive during temporary empty uploads --- wgpu/src/text.rs | 76 ++++++++++++++++++++++++++++-------------------- 1 file changed, 44 insertions(+), 32 deletions(-) diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 2508906fba..7e683c77cb 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -61,6 +61,10 @@ impl Cache { } pub fn update(&mut self, text: Vec) { + if self.text.is_empty() && text.is_empty() { + return; + } + self.text = Rc::from(text); self.version += 1; } @@ -139,23 +143,24 @@ impl Storage { hash_map::Entry::Occupied(entry) => { let upload = entry.into_mut(); - if !cache.text.is_empty() - && (upload.version != cache.version - || upload.group_version != group.version - || upload.transformation != new_transformation) + if upload.version != cache.version + || upload.group_version != group.version + || upload.transformation != new_transformation { - let _ = prepare( - device, - queue, - encoder, - &mut upload.renderer, - &mut group.atlas, - &mut upload.buffer_cache, - &cache.text, - bounds, - new_transformation, - target_size, - ); + if !cache.text.is_empty() { + let _ = prepare( + device, + queue, + encoder, + &mut upload.renderer, + &mut group.atlas, + &mut upload.buffer_cache, + &cache.text, + bounds, + new_transformation, + target_size, + ); + } // Only trim if glyphs have changed group.should_trim = @@ -179,18 +184,20 @@ impl Storage { let mut buffer_cache = BufferCache::new(); - let _ = prepare( - device, - queue, - encoder, - &mut renderer, - &mut group.atlas, - &mut buffer_cache, - &cache.text, - bounds, - new_transformation, - target_size, - ); + if !cache.text.is_empty() { + let _ = prepare( + device, + queue, + encoder, + &mut renderer, + &mut group.atlas, + &mut buffer_cache, + &cache.text, + bounds, + new_transformation, + target_size, + ); + } let _ = entry.insert(Upload { renderer, @@ -202,6 +209,8 @@ impl Storage { _atlas: Rc::downgrade(&group.handle), }); + group.should_trim = cache.group.is_singleton(); + log::debug!( "New text upload: {} (total: {})", cache.id.0, @@ -224,10 +233,8 @@ impl Storage { return false; } - if id.is_singleton() || group.should_trim { - log::debug!( - "Trimming text atlas: {id:?} (uploads: {active_uploads})" - ); + if group.should_trim { + log::trace!("Trimming text atlas: {id:?}"); group.atlas.trim(); group.should_trim = false; @@ -236,6 +243,11 @@ impl Storage { // when the atlas may be shared by multiple // uploads. if !id.is_singleton() { + log::debug!( + "Invalidating text atlas: {id:?} \ + (uploads: {active_uploads})" + ); + group.version += 1; } } From 62433a65e92c025cd9c36e81fc16bab77790bacb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 30 Apr 2024 23:51:41 +0200 Subject: [PATCH 37/66] Enable logging in `clock` example --- examples/clock/Cargo.toml | 1 + examples/clock/src/main.rs | 2 ++ 2 files changed, 3 insertions(+) diff --git a/examples/clock/Cargo.toml b/examples/clock/Cargo.toml index 2d3d590866..dc2e5382d8 100644 --- a/examples/clock/Cargo.toml +++ b/examples/clock/Cargo.toml @@ -10,3 +10,4 @@ iced.workspace = true iced.features = ["canvas", "tokio", "debug"] time = { version = "0.3", features = ["local-offset"] } +tracing-subscriber = "0.3" diff --git a/examples/clock/src/main.rs b/examples/clock/src/main.rs index 897f8f1b38..d717db36ce 100644 --- a/examples/clock/src/main.rs +++ b/examples/clock/src/main.rs @@ -8,6 +8,8 @@ use iced::{ }; pub fn main() -> iced::Result { + tracing_subscriber::fmt::init(); + iced::program("Clock - Iced", Clock::update, Clock::view) .subscription(Clock::subscription) .theme(Clock::theme) From 8d27af24a76d9792e22b3380e11b846fd5533805 Mon Sep 17 00:00:00 2001 From: Bajix Date: Wed, 27 Mar 2024 12:54:01 -0700 Subject: [PATCH 38/66] Utilize bytes::Bytes for images --- Cargo.toml | 1 + core/Cargo.toml | 1 + core/src/image.rs | 62 +++++------------------------------------------ 3 files changed, 8 insertions(+), 56 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2b6a0d0333..451ff840dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -136,6 +136,7 @@ iced_winit = { version = "0.13.0-dev", path = "winit" } async-std = "1.0" bitflags = "2.0" +bytes = "1.6" bytemuck = { version = "1.0", features = ["derive"] } cosmic-text = "0.10" dark-light = "1.0" diff --git a/core/Cargo.toml b/core/Cargo.toml index 7bd37021ec..3c557bca0f 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -19,6 +19,7 @@ advanced = [] [dependencies] bitflags.workspace = true +bytes.workspace = true glam.workspace = true log.workspace = true num-traits.workspace = true diff --git a/core/src/image.rs b/core/src/image.rs index dc74e5c1ea..7316ede7d2 100644 --- a/core/src/image.rs +++ b/core/src/image.rs @@ -1,10 +1,11 @@ //! Load and draw raster graphics. +pub use bytes::Bytes; + use crate::{Rectangle, Size}; use rustc_hash::FxHasher; use std::hash::{Hash, Hasher as _}; use std::path::PathBuf; -use std::sync::Arc; /// A handle of some image data. #[derive(Debug, Clone, PartialEq, Eq)] @@ -29,12 +30,12 @@ impl Handle { pub fn from_pixels( width: u32, height: u32, - pixels: impl AsRef<[u8]> + Send + Sync + 'static, + pixels: impl Into, ) -> Handle { Self::from_data(Data::Rgba { width, height, - pixels: Bytes::new(pixels), + pixels: pixels.into(), }) } @@ -44,10 +45,8 @@ impl Handle { /// /// This is useful if you already have your image loaded in-memory, maybe /// because you downloaded or generated it procedurally. - pub fn from_memory( - bytes: impl AsRef<[u8]> + Send + Sync + 'static, - ) -> Handle { - Self::from_data(Data::Bytes(Bytes::new(bytes))) + pub fn from_memory(bytes: impl Into) -> Handle { + Self::from_data(Data::Bytes(bytes.into())) } fn from_data(data: Data) -> Handle { @@ -86,55 +85,6 @@ impl Hash for Handle { } } -/// A wrapper around raw image data. -/// -/// It behaves like a `&[u8]`. -#[derive(Clone)] -pub struct Bytes(Arc + Send + Sync + 'static>); - -impl Bytes { - /// Creates new [`Bytes`] around `data`. - pub fn new(data: impl AsRef<[u8]> + Send + Sync + 'static) -> Self { - Self(Arc::new(data)) - } -} - -impl std::fmt::Debug for Bytes { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.as_ref().as_ref().fmt(f) - } -} - -impl std::hash::Hash for Bytes { - fn hash(&self, state: &mut H) { - self.0.as_ref().as_ref().hash(state); - } -} - -impl PartialEq for Bytes { - fn eq(&self, other: &Self) -> bool { - let a = self.as_ref(); - let b = other.as_ref(); - core::ptr::eq(a, b) || a == b - } -} - -impl Eq for Bytes {} - -impl AsRef<[u8]> for Bytes { - fn as_ref(&self) -> &[u8] { - self.0.as_ref().as_ref() - } -} - -impl std::ops::Deref for Bytes { - type Target = [u8]; - - fn deref(&self) -> &[u8] { - self.0.as_ref().as_ref() - } -} - /// The data of a raster image. #[derive(Clone, PartialEq, Eq, Hash)] pub enum Data { From 7c084d96958f1dacf9efae1f983bb44086fb70dc Mon Sep 17 00:00:00 2001 From: Thomas Sieverding Date: Sun, 7 Apr 2024 18:36:47 -0700 Subject: [PATCH 39/66] Utilize bytes::Bytes iced_runtime::window::Screenshot --- runtime/Cargo.toml | 1 + runtime/src/window/screenshot.rs | 16 +++++++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 2150346202..703c3ed955 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -18,6 +18,7 @@ debug = [] multi-window = [] [dependencies] +bytes.workspace = true iced_core.workspace = true iced_futures.workspace = true iced_futures.features = ["thread-pool"] diff --git a/runtime/src/window/screenshot.rs b/runtime/src/window/screenshot.rs index 21e0471888..bd277ddfcb 100644 --- a/runtime/src/window/screenshot.rs +++ b/runtime/src/window/screenshot.rs @@ -1,8 +1,8 @@ //! Take screenshots of a window. use crate::core::{Rectangle, Size}; +use bytes::Bytes; use std::fmt::{Debug, Formatter}; -use std::sync::Arc; /// Data of a screenshot, captured with `window::screenshot()`. /// @@ -10,7 +10,7 @@ use std::sync::Arc; #[derive(Clone)] pub struct Screenshot { /// The bytes of the [`Screenshot`]. - pub bytes: Arc>, + pub bytes: Bytes, /// The size of the [`Screenshot`]. pub size: Size, } @@ -28,9 +28,9 @@ impl Debug for Screenshot { impl Screenshot { /// Creates a new [`Screenshot`]. - pub fn new(bytes: Vec, size: Size) -> Self { + pub fn new(bytes: impl Into, size: Size) -> Self { Self { - bytes: Arc::new(bytes), + bytes: bytes.into(), size, } } @@ -68,7 +68,7 @@ impl Screenshot { ); Ok(Self { - bytes: Arc::new(chopped), + bytes: Bytes::from(chopped), size: Size::new(region.width, region.height), }) } @@ -80,6 +80,12 @@ impl AsRef<[u8]> for Screenshot { } } +impl Into for Screenshot { + fn into(self) -> Bytes { + self.bytes + } +} + #[derive(Debug, thiserror::Error)] /// Errors that can occur when cropping a [`Screenshot`]. pub enum CropError { From 45254ab88c6ca76759523069c2fb8734de626f02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 1 May 2024 00:55:49 +0200 Subject: [PATCH 40/66] Use `Bytes` as the `Container` of `ImageBuffer` Since we don't need to mutate images once loaded, we avoid unnecessary extra allocations. --- Cargo.toml | 2 +- core/src/image.rs | 91 +++++++++++++++++----------------------- graphics/src/image.rs | 51 +++++++++++++--------- tiny_skia/src/raster.rs | 2 +- wgpu/src/image/raster.rs | 4 +- 5 files changed, 73 insertions(+), 77 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 451ff840dd..60773da01d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -136,8 +136,8 @@ iced_winit = { version = "0.13.0-dev", path = "winit" } async-std = "1.0" bitflags = "2.0" -bytes = "1.6" bytemuck = { version = "1.0", features = ["derive"] } +bytes = "1.6" cosmic-text = "0.10" dark-light = "1.0" futures = "0.3" diff --git a/core/src/image.rs b/core/src/image.rs index 7316ede7d2..5b31fbcf8a 100644 --- a/core/src/image.rs +++ b/core/src/image.rs @@ -4,14 +4,27 @@ pub use bytes::Bytes; use crate::{Rectangle, Size}; use rustc_hash::FxHasher; -use std::hash::{Hash, Hasher as _}; +use std::hash::{Hash, Hasher}; use std::path::PathBuf; /// A handle of some image data. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Handle { - id: u64, - data: Data, +#[derive(Clone, PartialEq, Eq)] +pub enum Handle { + /// File data + Path(PathBuf), + + /// In-memory data + Bytes(Bytes), + + /// Decoded image pixels in RGBA format. + Rgba { + /// The width of the image. + width: u32, + /// The height of the image. + height: u32, + /// The pixels. + pixels: Bytes, + }, } impl Handle { @@ -19,7 +32,7 @@ impl Handle { /// /// Makes an educated guess about the image format by examining the data in the file. pub fn from_path>(path: T) -> Handle { - Self::from_data(Data::Path(path.into())) + Self::Path(path.into()) } /// Creates an image [`Handle`] containing the image pixels directly. This @@ -32,11 +45,11 @@ impl Handle { height: u32, pixels: impl Into, ) -> Handle { - Self::from_data(Data::Rgba { + Self::Rgba { width, height, pixels: pixels.into(), - }) + } } /// Creates an image [`Handle`] containing the image data directly. @@ -46,27 +59,25 @@ impl Handle { /// This is useful if you already have your image loaded in-memory, maybe /// because you downloaded or generated it procedurally. pub fn from_memory(bytes: impl Into) -> Handle { - Self::from_data(Data::Bytes(bytes.into())) - } - - fn from_data(data: Data) -> Handle { - let mut hasher = FxHasher::default(); - data.hash(&mut hasher); - - Handle { - id: hasher.finish(), - data, - } + Self::Bytes(bytes.into()) } /// Returns the unique identifier of the [`Handle`]. pub fn id(&self) -> u64 { - self.id + let mut hasher = FxHasher::default(); + self.hash(&mut hasher); + + hasher.finish() } +} - /// Returns a reference to the image [`Data`]. - pub fn data(&self) -> &Data { - &self.data +impl Hash for Handle { + fn hash(&self, state: &mut H) { + match self { + Self::Path(path) => path.hash(state), + Self::Bytes(bytes) => bytes.as_ptr().hash(state), + Self::Rgba { pixels, .. } => pixels.as_ptr().hash(state), + } } } @@ -79,38 +90,12 @@ where } } -impl Hash for Handle { - fn hash(&self, state: &mut H) { - self.id.hash(state); - } -} - -/// The data of a raster image. -#[derive(Clone, PartialEq, Eq, Hash)] -pub enum Data { - /// File data - Path(PathBuf), - - /// In-memory data - Bytes(Bytes), - - /// Decoded image pixels in RGBA format. - Rgba { - /// The width of the image. - width: u32, - /// The height of the image. - height: u32, - /// The pixels. - pixels: Bytes, - }, -} - -impl std::fmt::Debug for Data { +impl std::fmt::Debug for Handle { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Data::Path(path) => write!(f, "Path({path:?})"), - Data::Bytes(_) => write!(f, "Bytes(...)"), - Data::Rgba { width, height, .. } => { + Self::Path(path) => write!(f, "Path({path:?})"), + Self::Bytes(_) => write!(f, "Bytes(...)"), + Self::Rgba { width, height, .. } => { write!(f, "Pixels({width} * {height})") } } diff --git a/graphics/src/image.rs b/graphics/src/image.rs index c6135e9ec9..2a63053049 100644 --- a/graphics/src/image.rs +++ b/graphics/src/image.rs @@ -50,7 +50,8 @@ impl Image { /// [`Handle`]: image::Handle pub fn load( handle: &image::Handle, -) -> ::image::ImageResult<::image::DynamicImage> { +) -> ::image::ImageResult<::image::ImageBuffer<::image::Rgba, image::Bytes>> +{ use bitflags::bitflags; bitflags! { @@ -100,8 +101,8 @@ pub fn load( } } - match handle.data() { - image::Data::Path(path) => { + let (width, height, pixels) = match handle { + image::Handle::Path(path) => { let image = ::image::open(path)?; let operation = std::fs::File::open(path) @@ -110,33 +111,43 @@ pub fn load( .and_then(|mut reader| Operation::from_exif(&mut reader).ok()) .unwrap_or_else(Operation::empty); - Ok(operation.perform(image)) + let rgba = operation.perform(image).into_rgba8(); + + ( + rgba.width(), + rgba.height(), + image::Bytes::from(rgba.into_raw()), + ) } - image::Data::Bytes(bytes) => { + image::Handle::Bytes(bytes) => { let image = ::image::load_from_memory(bytes)?; let operation = Operation::from_exif(&mut std::io::Cursor::new(bytes)) .ok() .unwrap_or_else(Operation::empty); - Ok(operation.perform(image)) + let rgba = operation.perform(image).into_rgba8(); + + ( + rgba.width(), + rgba.height(), + image::Bytes::from(rgba.into_raw()), + ) } - image::Data::Rgba { + image::Handle::Rgba { width, height, pixels, - } => { - if let Some(image) = - ::image::ImageBuffer::from_vec(*width, *height, pixels.to_vec()) - { - Ok(::image::DynamicImage::ImageRgba8(image)) - } else { - Err(::image::error::ImageError::Limits( - ::image::error::LimitError::from_kind( - ::image::error::LimitErrorKind::DimensionError, - ), - )) - } - } + } => (*width, *height, pixels.clone()), + }; + + if let Some(image) = ::image::ImageBuffer::from_raw(width, height, pixels) { + Ok(image) + } else { + Err(::image::error::ImageError::Limits( + ::image::error::LimitError::from_kind( + ::image::error::LimitErrorKind::DimensionError, + ), + )) } } diff --git a/tiny_skia/src/raster.rs b/tiny_skia/src/raster.rs index 176b0da92d..59f1e4d5a9 100644 --- a/tiny_skia/src/raster.rs +++ b/tiny_skia/src/raster.rs @@ -83,7 +83,7 @@ impl Cache { let id = handle.id(); if let hash_map::Entry::Vacant(entry) = self.entries.entry(id) { - let image = graphics::image::load(handle).ok()?.into_rgba8(); + let image = graphics::image::load(handle).ok()?; let mut buffer = vec![0u32; image.width() as usize * image.height() as usize]; diff --git a/wgpu/src/image/raster.rs b/wgpu/src/image/raster.rs index 7a837f2898..60e9cbad55 100644 --- a/wgpu/src/image/raster.rs +++ b/wgpu/src/image/raster.rs @@ -10,7 +10,7 @@ use rustc_hash::{FxHashMap, FxHashSet}; #[derive(Debug)] pub enum Memory { /// Image data on host - Host(image_rs::ImageBuffer, Vec>), + Host(image_rs::ImageBuffer, image::Bytes>), /// Storage entry Device(atlas::Entry), /// Image not found @@ -51,7 +51,7 @@ impl Cache { } let memory = match graphics::image::load(handle) { - Ok(image) => Memory::Host(image.to_rgba8()), + Ok(image) => Memory::Host(image), Err(image_rs::error::ImageError::IoError(_)) => Memory::NotFound, Err(_) => Memory::Invalid, }; From f5bc336d699d0c6440f6d638a5a437baaabe1e43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 1 May 2024 01:09:31 +0200 Subject: [PATCH 41/66] Fix `clippy` lint in `runtime::window::screenshot` --- runtime/src/window/screenshot.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/runtime/src/window/screenshot.rs b/runtime/src/window/screenshot.rs index bd277ddfcb..fb31811023 100644 --- a/runtime/src/window/screenshot.rs +++ b/runtime/src/window/screenshot.rs @@ -80,9 +80,9 @@ impl AsRef<[u8]> for Screenshot { } } -impl Into for Screenshot { - fn into(self) -> Bytes { - self.bytes +impl From for Bytes { + fn from(screenshot: Screenshot) -> Self { + screenshot.bytes } } From b52c7bb610f593fffc624d461dca17ac50c81626 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 1 May 2024 01:39:43 +0200 Subject: [PATCH 42/66] Use an opaque `Id` type for `image::Handle` Hashing pointers is a terrible idea. --- core/src/image.rs | 68 ++++++++++++++++++++++++++------------ graphics/src/image.rs | 5 +-- tiny_skia/src/raster.rs | 4 +-- wgpu/src/image/raster.rs | 4 +-- widget/src/image.rs | 8 ++--- widget/src/image/viewer.rs | 6 ++-- 6 files changed, 59 insertions(+), 36 deletions(-) diff --git a/core/src/image.rs b/core/src/image.rs index 5b31fbcf8a..a0e4078722 100644 --- a/core/src/image.rs +++ b/core/src/image.rs @@ -5,19 +5,21 @@ use crate::{Rectangle, Size}; use rustc_hash::FxHasher; use std::hash::{Hash, Hasher}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; /// A handle of some image data. #[derive(Clone, PartialEq, Eq)] pub enum Handle { /// File data - Path(PathBuf), + Path(Id, PathBuf), /// In-memory data - Bytes(Bytes), + Bytes(Id, Bytes), /// Decoded image pixels in RGBA format. Rgba { + /// The id of this handle. + id: Id, /// The width of the image. width: u32, /// The height of the image. @@ -32,7 +34,9 @@ impl Handle { /// /// Makes an educated guess about the image format by examining the data in the file. pub fn from_path>(path: T) -> Handle { - Self::Path(path.into()) + let path = path.into(); + + Self::Path(Id::path(&path), path) } /// Creates an image [`Handle`] containing the image pixels directly. This @@ -46,6 +50,7 @@ impl Handle { pixels: impl Into, ) -> Handle { Self::Rgba { + id: Id::unique(), width, height, pixels: pixels.into(), @@ -59,24 +64,15 @@ impl Handle { /// This is useful if you already have your image loaded in-memory, maybe /// because you downloaded or generated it procedurally. pub fn from_memory(bytes: impl Into) -> Handle { - Self::Bytes(bytes.into()) + Self::Bytes(Id::unique(), bytes.into()) } /// Returns the unique identifier of the [`Handle`]. - pub fn id(&self) -> u64 { - let mut hasher = FxHasher::default(); - self.hash(&mut hasher); - - hasher.finish() - } -} - -impl Hash for Handle { - fn hash(&self, state: &mut H) { + pub fn id(&self) -> Id { match self { - Self::Path(path) => path.hash(state), - Self::Bytes(bytes) => bytes.as_ptr().hash(state), - Self::Rgba { pixels, .. } => pixels.as_ptr().hash(state), + Handle::Path(id, _) + | Handle::Bytes(id, _) + | Handle::Rgba { id, .. } => *id, } } } @@ -93,8 +89,8 @@ where impl std::fmt::Debug for Handle { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::Path(path) => write!(f, "Path({path:?})"), - Self::Bytes(_) => write!(f, "Bytes(...)"), + Self::Path(_, path) => write!(f, "Path({path:?})"), + Self::Bytes(_, _) => write!(f, "Bytes(...)"), Self::Rgba { width, height, .. } => { write!(f, "Pixels({width} * {height})") } @@ -102,6 +98,36 @@ impl std::fmt::Debug for Handle { } } +/// The unique identifier of some [`Handle`] data. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Id { + /// A unique identifier. + Unique(u64), + /// A hash identifier. + Hash(u64), +} + +impl Id { + fn unique() -> Self { + use std::sync::atomic::{self, AtomicU64}; + + static NEXT_ID: AtomicU64 = AtomicU64::new(0); + + Self::Unique(NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed)) + } + + fn path(path: impl AsRef) -> Self { + let hash = { + let mut hasher = FxHasher::default(); + path.as_ref().hash(&mut hasher); + + hasher.finish() + }; + + Self::Hash(hash) + } +} + /// Image filtering strategy. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] pub enum FilterMethod { @@ -119,7 +145,7 @@ pub trait Renderer: crate::Renderer { /// The image Handle to be displayed. Iced exposes its own default implementation of a [`Handle`] /// /// [`Handle`]: Self::Handle - type Handle: Clone + Hash; + type Handle: Clone; /// Returns the dimensions of an image for the given [`Handle`]. fn measure_image(&self, handle: &Self::Handle) -> Size; diff --git a/graphics/src/image.rs b/graphics/src/image.rs index 2a63053049..04c4505755 100644 --- a/graphics/src/image.rs +++ b/graphics/src/image.rs @@ -102,7 +102,7 @@ pub fn load( } let (width, height, pixels) = match handle { - image::Handle::Path(path) => { + image::Handle::Path(_, path) => { let image = ::image::open(path)?; let operation = std::fs::File::open(path) @@ -119,7 +119,7 @@ pub fn load( image::Bytes::from(rgba.into_raw()), ) } - image::Handle::Bytes(bytes) => { + image::Handle::Bytes(_, bytes) => { let image = ::image::load_from_memory(bytes)?; let operation = Operation::from_exif(&mut std::io::Cursor::new(bytes)) @@ -138,6 +138,7 @@ pub fn load( width, height, pixels, + .. } => (*width, *height, pixels.clone()), }; diff --git a/tiny_skia/src/raster.rs b/tiny_skia/src/raster.rs index 59f1e4d5a9..907fce7c40 100644 --- a/tiny_skia/src/raster.rs +++ b/tiny_skia/src/raster.rs @@ -71,8 +71,8 @@ impl Pipeline { #[derive(Debug, Default)] struct Cache { - entries: FxHashMap>, - hits: FxHashSet, + entries: FxHashMap>, + hits: FxHashSet, } impl Cache { diff --git a/wgpu/src/image/raster.rs b/wgpu/src/image/raster.rs index 60e9cbad55..4d3c3125b8 100644 --- a/wgpu/src/image/raster.rs +++ b/wgpu/src/image/raster.rs @@ -38,8 +38,8 @@ impl Memory { /// Caches image raster data #[derive(Debug, Default)] pub struct Cache { - map: FxHashMap, - hits: FxHashSet, + map: FxHashMap, + hits: FxHashSet, should_trim: bool, } diff --git a/widget/src/image.rs b/widget/src/image.rs index f673c7b3bb..21d371b700 100644 --- a/widget/src/image.rs +++ b/widget/src/image.rs @@ -11,8 +11,6 @@ use crate::core::{ ContentFit, Element, Layout, Length, Rectangle, Size, Vector, Widget, }; -use std::hash::Hash; - pub use image::{FilterMethod, Handle}; /// Creates a new [`Viewer`] with the given image `Handle`. @@ -128,7 +126,7 @@ pub fn draw( filter_method: FilterMethod, ) where Renderer: image::Renderer, - Handle: Clone + Hash, + Handle: Clone, { let Size { width, height } = renderer.measure_image(handle); let image_size = Size::new(width as f32, height as f32); @@ -167,7 +165,7 @@ impl Widget for Image where Renderer: image::Renderer, - Handle: Clone + Hash, + Handle: Clone, { fn size(&self) -> Size { Size { @@ -216,7 +214,7 @@ impl<'a, Message, Theme, Renderer, Handle> From> for Element<'a, Message, Theme, Renderer> where Renderer: image::Renderer, - Handle: Clone + Hash + 'a, + Handle: Clone + 'a, { fn from(image: Image) -> Element<'a, Message, Theme, Renderer> { Element::new(image) diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs index 75d73b1927..214cb996b9 100644 --- a/widget/src/image/viewer.rs +++ b/widget/src/image/viewer.rs @@ -10,8 +10,6 @@ use crate::core::{ Vector, Widget, }; -use std::hash::Hash; - /// A frame that displays an image with the ability to zoom in/out and pan. #[allow(missing_debug_implementations)] pub struct Viewer { @@ -94,7 +92,7 @@ impl Widget for Viewer where Renderer: image::Renderer, - Handle: Clone + Hash, + Handle: Clone, { fn tag(&self) -> tree::Tag { tree::Tag::of::() @@ -401,7 +399,7 @@ impl<'a, Message, Theme, Renderer, Handle> From> where Renderer: 'a + image::Renderer, Message: 'a, - Handle: Clone + Hash + 'a, + Handle: Clone + 'a, { fn from(viewer: Viewer) -> Element<'a, Message, Theme, Renderer> { Element::new(viewer) From 58ea914ad21ea9c5ae7b7b1c167ed084c9ddb07a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 1 May 2024 01:52:49 +0200 Subject: [PATCH 43/66] Make `image::Id` actually opaque --- core/src/image.rs | 60 +++++++++++++++++++++------------ examples/pokedex/src/main.rs | 2 +- examples/screenshot/src/main.rs | 2 +- 3 files changed, 40 insertions(+), 24 deletions(-) diff --git a/core/src/image.rs b/core/src/image.rs index a0e4078722..c44ccc30b2 100644 --- a/core/src/image.rs +++ b/core/src/image.rs @@ -10,13 +10,26 @@ use std::path::{Path, PathBuf}; /// A handle of some image data. #[derive(Clone, PartialEq, Eq)] pub enum Handle { - /// File data + /// A file handle. The image data will be read + /// from the file path. + /// + /// Use [`from_path`] to create this variant. + /// + /// [`from_path`]: Self::from_path Path(Id, PathBuf), - /// In-memory data + /// A handle pointing to some encoded image bytes in-memory. + /// + /// Use [`from_bytes`] to create this variant. + /// + /// [`from_bytes`]: Self::from_bytes Bytes(Id, Bytes), - /// Decoded image pixels in RGBA format. + /// A handle pointing to decoded image pixels in RGBA format. + /// + /// Use [`from_rgba`] to create this variant. + /// + /// [`from_rgba`]: Self::from_bytes Rgba { /// The id of this handle. id: Id, @@ -39,12 +52,24 @@ impl Handle { Self::Path(Id::path(&path), path) } - /// Creates an image [`Handle`] containing the image pixels directly. This - /// function expects the input data to be provided as a `Vec` of RGBA - /// pixels. + /// Creates an image [`Handle`] containing the encoded image data directly. + /// + /// Makes an educated guess about the image format by examining the given data. + /// + /// This is useful if you already have your image loaded in-memory, maybe + /// because you downloaded or generated it procedurally. + pub fn from_bytes(bytes: impl Into) -> Handle { + Self::Bytes(Id::unique(), bytes.into()) + } + + /// Creates an image [`Handle`] containing the decoded image pixels directly. + /// + /// This function expects the pixel data to be provided as a collection of [`Bytes`] + /// of RGBA pixels. Therefore, the length of the pixel data should always be + /// `width * height * 4`. /// /// This is useful if you have already decoded your image. - pub fn from_pixels( + pub fn from_rgba( width: u32, height: u32, pixels: impl Into, @@ -57,16 +82,6 @@ impl Handle { } } - /// Creates an image [`Handle`] containing the image data directly. - /// - /// Makes an educated guess about the image format by examining the given data. - /// - /// This is useful if you already have your image loaded in-memory, maybe - /// because you downloaded or generated it procedurally. - pub fn from_memory(bytes: impl Into) -> Handle { - Self::Bytes(Id::unique(), bytes.into()) - } - /// Returns the unique identifier of the [`Handle`]. pub fn id(&self) -> Id { match self { @@ -100,10 +115,11 @@ impl std::fmt::Debug for Handle { /// The unique identifier of some [`Handle`] data. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum Id { - /// A unique identifier. +pub struct Id(_Id); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +enum _Id { Unique(u64), - /// A hash identifier. Hash(u64), } @@ -113,7 +129,7 @@ impl Id { static NEXT_ID: AtomicU64 = AtomicU64::new(0); - Self::Unique(NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed)) + Self(_Id::Unique(NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed))) } fn path(path: impl AsRef) -> Self { @@ -124,7 +140,7 @@ impl Id { hasher.finish() }; - Self::Hash(hash) + Self(_Id::Hash(hash)) } } diff --git a/examples/pokedex/src/main.rs b/examples/pokedex/src/main.rs index 0811c08d99..35f2323642 100644 --- a/examples/pokedex/src/main.rs +++ b/examples/pokedex/src/main.rs @@ -188,7 +188,7 @@ impl Pokemon { { let bytes = reqwest::get(&url).await?.bytes().await?; - Ok(image::Handle::from_memory(bytes)) + Ok(image::Handle::from_bytes(bytes)) } #[cfg(target_arch = "wasm32")] diff --git a/examples/screenshot/src/main.rs b/examples/screenshot/src/main.rs index d887c41b50..5c175ccc0a 100644 --- a/examples/screenshot/src/main.rs +++ b/examples/screenshot/src/main.rs @@ -109,7 +109,7 @@ impl Example { fn view(&self) -> Element<'_, Message> { let image: Element = if let Some(screenshot) = &self.screenshot { - image(image::Handle::from_pixels( + image(image::Handle::from_rgba( screenshot.size.width, screenshot.size.height, screenshot.clone(), From 01b014c19fa2a3c200fb2077e31822f525f729cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 1 May 2024 01:53:25 +0200 Subject: [PATCH 44/66] Fix documentation link in `image::Handle` --- core/src/image.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/image.rs b/core/src/image.rs index c44ccc30b2..c38239bcad 100644 --- a/core/src/image.rs +++ b/core/src/image.rs @@ -29,7 +29,7 @@ pub enum Handle { /// /// Use [`from_rgba`] to create this variant. /// - /// [`from_rgba`]: Self::from_bytes + /// [`from_rgba`]: Self::from_rgba Rgba { /// The id of this handle. id: Id, From e2aee80aa1371b91d08a2aeccc814f742d4dedb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 1 May 2024 15:39:05 +0200 Subject: [PATCH 45/66] Introduce `layered_text` benchmarks --- benches/wgpu.rs | 98 ++++++++++++++++++++++++++++--------------------- 1 file changed, 56 insertions(+), 42 deletions(-) diff --git a/benches/wgpu.rs b/benches/wgpu.rs index 61b4eb6c19..3dd415dbed 100644 --- a/benches/wgpu.rs +++ b/benches/wgpu.rs @@ -3,7 +3,7 @@ use criterion::{criterion_group, criterion_main, Bencher, Criterion}; use iced::alignment; use iced::mouse; -use iced::widget::{canvas, text}; +use iced::widget::{canvas, stack, text}; use iced::{ Color, Element, Font, Length, Pixels, Point, Rectangle, Size, Theme, }; @@ -16,6 +16,13 @@ criterion_group!(benches, wgpu_benchmark); pub fn wgpu_benchmark(c: &mut Criterion) { c.bench_function("wgpu — canvas (light)", |b| benchmark(b, scene(10))); c.bench_function("wgpu — canvas (heavy)", |b| benchmark(b, scene(1_000))); + + c.bench_function("wgpu - layered text (light)", |b| { + benchmark(b, layered_text(10)); + }); + c.bench_function("wgpu - layered text (heavy)", |b| { + benchmark(b, layered_text(1_000)); + }); } fn benchmark( @@ -125,52 +132,59 @@ fn benchmark( }); } -fn scene<'a, Message: 'a, Theme: 'a>( - n: usize, -) -> Element<'a, Message, Theme, Renderer> { +fn scene<'a, Message: 'a>(n: usize) -> Element<'a, Message, Theme, Renderer> { + struct Scene { + n: usize, + } + + impl canvas::Program for Scene { + type State = canvas::Cache; + + fn draw( + &self, + cache: &Self::State, + renderer: &Renderer, + _theme: &Theme, + bounds: Rectangle, + _cursor: mouse::Cursor, + ) -> Vec> { + vec![cache.draw(renderer, bounds.size(), |frame| { + for i in 0..self.n { + frame.fill_rectangle( + Point::new(0.0, i as f32), + Size::new(10.0, 10.0), + Color::WHITE, + ); + } + + for i in 0..self.n { + frame.fill_text(canvas::Text { + content: i.to_string(), + position: Point::new(0.0, i as f32), + color: Color::BLACK, + size: Pixels::from(16), + line_height: text::LineHeight::default(), + font: Font::DEFAULT, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, + shaping: text::Shaping::Basic, + }); + } + })] + } + } + canvas(Scene { n }) .width(Length::Fill) .height(Length::Fill) .into() } -struct Scene { +fn layered_text<'a, Message: 'a>( n: usize, -} - -impl canvas::Program for Scene { - type State = canvas::Cache; - - fn draw( - &self, - cache: &Self::State, - renderer: &Renderer, - _theme: &Theme, - bounds: Rectangle, - _cursor: mouse::Cursor, - ) -> Vec> { - vec![cache.draw(renderer, bounds.size(), |frame| { - for i in 0..self.n { - frame.fill_rectangle( - Point::new(0.0, i as f32), - Size::new(10.0, 10.0), - Color::WHITE, - ); - } - - for i in 0..self.n { - frame.fill_text(canvas::Text { - content: i.to_string(), - position: Point::new(0.0, i as f32), - color: Color::BLACK, - size: Pixels::from(16), - line_height: text::LineHeight::default(), - font: Font::DEFAULT, - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Top, - shaping: text::Shaping::Basic, - }); - } - })] - } +) -> Element<'a, Message, Theme, Renderer> { + stack((0..n).map(|i| text(format!("I am paragraph {i}!")).into())) + .width(Length::Fill) + .height(Length::Fill) + .into() } From ffa6614026cfc0dbdd636ad5a334008c1ee5532d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 2 May 2024 08:44:09 +0200 Subject: [PATCH 46/66] Fix panic in `wgpu::color::convert` --- wgpu/src/color.rs | 68 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 50 insertions(+), 18 deletions(-) diff --git a/wgpu/src/color.rs b/wgpu/src/color.rs index 890f3f8961..9d593d9c40 100644 --- a/wgpu/src/color.rs +++ b/wgpu/src/color.rs @@ -1,5 +1,7 @@ use std::borrow::Cow; +use wgpu::util::DeviceExt; + pub fn convert( device: &wgpu::Device, encoder: &mut wgpu::CommandEncoder, @@ -15,28 +17,58 @@ pub fn convert( ..wgpu::SamplerDescriptor::default() }); - //sampler in 0 - let sampler_layout = + #[derive(Debug, Clone, Copy, bytemuck::Zeroable, bytemuck::Pod)] + #[repr(C)] + struct Ratio { + u: f32, + v: f32, + } + + let ratio = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("iced-wgpu::triangle::msaa ratio"), + contents: bytemuck::bytes_of(&Ratio { u: 1.0, v: 1.0 }), + usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::UNIFORM, + }); + + let constant_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("iced_wgpu.offscreen.blit.sampler_layout"), - entries: &[wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler( - wgpu::SamplerBindingType::NonFiltering, - ), - count: None, - }], + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler( + wgpu::SamplerBindingType::NonFiltering, + ), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, + ], }); - let sampler_bind_group = + let constant_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("iced_wgpu.offscreen.sampler.bind_group"), - layout: &sampler_layout, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::Sampler(&sampler), - }], + layout: &constant_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Sampler(&sampler), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: ratio.as_entire_binding(), + }, + ], }); let texture_layout = @@ -59,7 +91,7 @@ pub fn convert( let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("iced_wgpu.offscreen.blit.pipeline_layout"), - bind_group_layouts: &[&sampler_layout, &texture_layout], + bind_group_layouts: &[&constant_layout, &texture_layout], push_constant_ranges: &[], }); @@ -152,7 +184,7 @@ pub fn convert( }); pass.set_pipeline(&pipeline); - pass.set_bind_group(0, &sampler_bind_group, &[]); + pass.set_bind_group(0, &constant_bind_group, &[]); pass.set_bind_group(1, &texture_bind_group, &[]); pass.draw(0..6, 0..1); From aae8e4f5cfabfc3725ac938023fa29a6737a380c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 2 May 2024 17:23:32 +0200 Subject: [PATCH 47/66] Fix `clippy` lints for new `1.78` stable toolchain --- wgpu/src/primitive.rs | 2 +- widget/src/keyed/column.rs | 2 +- winit/src/multi_window.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/wgpu/src/primitive.rs b/wgpu/src/primitive.rs index 1313e752f5..8641f27a03 100644 --- a/wgpu/src/primitive.rs +++ b/wgpu/src/primitive.rs @@ -67,7 +67,7 @@ pub struct Storage { impl Storage { /// Returns `true` if `Storage` contains a type `T`. pub fn has(&self) -> bool { - self.pipelines.get(&TypeId::of::()).is_some() + self.pipelines.contains_key(&TypeId::of::()) } /// Inserts the data `T` in to [`Storage`]. diff --git a/widget/src/keyed/column.rs b/widget/src/keyed/column.rs index a34ce9e6f8..fdaadefaa7 100644 --- a/widget/src/keyed/column.rs +++ b/widget/src/keyed/column.rs @@ -224,7 +224,7 @@ where ); if state.keys != self.keys { - state.keys = self.keys.clone(); + state.keys.clone_from(&self.keys); } } diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index f832eb8102..2c148031c1 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -1190,7 +1190,7 @@ fn run_command( } /// Build the user interface for every window. -pub fn build_user_interfaces<'a, A: Application, C: Compositor>( +pub fn build_user_interfaces<'a, A: Application, C>( application: &'a A, debug: &mut Debug, window_manager: &mut WindowManager, From 09a6bcfffc24f5abdc8709403bab7ae1e01563f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 2 May 2024 13:15:17 +0200 Subject: [PATCH 48/66] Add `Image` rotation support Co-authored-by: DKolter <68352124+DKolter@users.noreply.github.com> --- core/src/image.rs | 2 ++ core/src/lib.rs | 2 ++ core/src/renderer/null.rs | 4 +++ core/src/rotation.rs | 37 +++++++++++++++++++ core/src/svg.rs | 2 ++ graphics/src/image.rs | 16 +++++++-- renderer/src/fallback.rs | 12 +++++-- src/lib.rs | 4 +-- tiny_skia/src/engine.rs | 49 +++++++++++++++++-------- tiny_skia/src/layer.rs | 32 ++++++++++++++--- tiny_skia/src/lib.rs | 17 +++++++-- tiny_skia/src/vector.rs | 4 ++- wgpu/src/image/mod.rs | 65 ++++++++++++++++++++++++++++----- wgpu/src/layer.rs | 13 +++++-- wgpu/src/lib.rs | 22 ++++++++++-- wgpu/src/shader/image.wgsl | 37 ++++++++++++++----- widget/src/image.rs | 73 ++++++++++++++++++++++++++++---------- widget/src/image/viewer.rs | 2 ++ widget/src/svg.rs | 65 +++++++++++++++++++++++++-------- 19 files changed, 374 insertions(+), 84 deletions(-) create mode 100644 core/src/rotation.rs diff --git a/core/src/image.rs b/core/src/image.rs index c38239bcad..5d1ab44190 100644 --- a/core/src/image.rs +++ b/core/src/image.rs @@ -173,5 +173,7 @@ pub trait Renderer: crate::Renderer { handle: Self::Handle, filter_method: FilterMethod, bounds: Rectangle, + rotation: f32, + scale: Size, ); } diff --git a/core/src/lib.rs b/core/src/lib.rs index feda4fb445..da3ddcac4e 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::RotationLayout; pub use shadow::Shadow; pub use shell::Shell; pub use size::Size; diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index fe38ec87c8..d2dcfe4dca 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -171,6 +171,8 @@ impl image::Renderer for () { _handle: Self::Handle, _filter_method: image::FilterMethod, _bounds: Rectangle, + _rotation: f32, + _scale: Size, ) { } } @@ -185,6 +187,8 @@ impl svg::Renderer for () { _handle: svg::Handle, _color: Option, _bounds: Rectangle, + _rotation: f32, + _scale: Size, ) { } } diff --git a/core/src/rotation.rs b/core/src/rotation.rs new file mode 100644 index 0000000000..821aa4941d --- /dev/null +++ b/core/src/rotation.rs @@ -0,0 +1,37 @@ +//! Control the rotation of some content (like an image) with the `RotationLayout` within a +//! space. +use crate::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, Hash, Clone, Copy, PartialEq, Eq)] +pub enum RotationLayout { + /// The layout is 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. + Keep, + /// The layout is 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. + Change, +} + +impl RotationLayout { + /// Applies the rotation to the layout while respecting the [`RotationLayout`] strategy. + /// The rotation is given in radians. + pub fn apply_to_size(&self, size: Size, rotation: f32) -> Size { + match self { + Self::Keep => size, + Self::Change => Size { + width: (size.width * rotation.cos()).abs() + + (size.height * rotation.sin()).abs(), + height: (size.width * rotation.sin()).abs() + + (size.height * rotation.cos()).abs(), + }, + } + } +} diff --git a/core/src/svg.rs b/core/src/svg.rs index 0106e0c268..74dd7f4ad7 100644 --- a/core/src/svg.rs +++ b/core/src/svg.rs @@ -100,5 +100,7 @@ pub trait Renderer: crate::Renderer { handle: Handle, color: Option, bounds: Rectangle, + rotation: f32, + scale: Size, ); } diff --git a/graphics/src/image.rs b/graphics/src/image.rs index 04c4505755..083248bf45 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, Rectangle, Size}; /// 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 in radians + rotation: f32, + + /// The scale of the image after rotation + scale: Size, }, /// A vector image. Vector { @@ -30,6 +34,12 @@ pub enum Image { /// The bounds of the image. bounds: Rectangle, + + /// The rotation of the image in radians + rotation: f32, + + /// The scale of the image after rotation + scale: Size, }, } diff --git a/renderer/src/fallback.rs b/renderer/src/fallback.rs index 5f69b42015..37e6ac43fe 100644 --- a/renderer/src/fallback.rs +++ b/renderer/src/fallback.rs @@ -154,11 +154,13 @@ where handle: Self::Handle, filter_method: image::FilterMethod, bounds: Rectangle, + rotation: f32, + scale: Size, ) { delegate!( self, renderer, - renderer.draw_image(handle, filter_method, bounds) + renderer.draw_image(handle, filter_method, bounds, rotation, scale) ); } } @@ -177,8 +179,14 @@ where handle: svg::Handle, color: Option, bounds: Rectangle, + rotation: f32, + scale: Size, ) { - delegate!(self, renderer, renderer.draw_svg(handle, color, bounds)); + delegate!( + self, + renderer, + renderer.draw_svg(handle, color, bounds, rotation, scale) + ); } } diff --git a/src/lib.rs b/src/lib.rs index 7517dd1488..9d07fed627 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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, RotationLayout, Shadow, + Size, Theme, Transformation, Vector, }; pub mod clipboard { diff --git a/tiny_skia/src/engine.rs b/tiny_skia/src/engine.rs index fbca12743b..564d37526c 100644 --- a/tiny_skia/src/engine.rs +++ b/tiny_skia/src/engine.rs @@ -539,10 +539,10 @@ impl Engine { pub fn draw_image( &mut self, image: &Image, - _transformation: Transformation, - _pixels: &mut tiny_skia::PixmapMut<'_>, - _clip_mask: &mut tiny_skia::Mask, - _clip_bounds: Rectangle, + transformation: Transformation, + pixels: &mut tiny_skia::PixmapMut<'_>, + clip_mask: &mut tiny_skia::Mask, + clip_bounds: Rectangle, ) { match image { #[cfg(feature = "image")] @@ -550,22 +550,31 @@ impl Engine { handle, filter_method, bounds, + rotation, + scale, } => { - let physical_bounds = *bounds * _transformation; + let physical_bounds = *bounds * transformation; - if !_clip_bounds.intersects(&physical_bounds) { + if !clip_bounds.intersects(&physical_bounds) { return; } - let clip_mask = (!physical_bounds.is_within(&_clip_bounds)) - .then_some(_clip_mask as &_); + let clip_mask = (!physical_bounds.is_within(&clip_bounds)) + .then_some(clip_mask as &_); + + let center = physical_bounds.center(); + let transform = into_transform(transformation) + .post_rotate_at(rotation.to_degrees(), center.x, center.y) + .post_translate(-center.x, -center.y) + .post_scale(scale.width, scale.height) + .post_translate(center.x, center.y); self.raster_pipeline.draw( handle, *filter_method, *bounds, - _pixels, - into_transform(_transformation), + pixels, + transform, clip_mask, ); } @@ -574,21 +583,31 @@ impl Engine { handle, color, bounds, + rotation, + scale, } => { - let physical_bounds = *bounds * _transformation; + let physical_bounds = *bounds * transformation; - if !_clip_bounds.intersects(&physical_bounds) { + if !clip_bounds.intersects(&physical_bounds) { return; } - let clip_mask = (!physical_bounds.is_within(&_clip_bounds)) - .then_some(_clip_mask as &_); + let clip_mask = (!physical_bounds.is_within(&clip_bounds)) + .then_some(clip_mask as &_); + + let center = physical_bounds.center(); + let transform = into_transform(transformation) + .post_rotate_at(rotation.to_degrees(), center.x, center.y) + .post_translate(-center.x, -center.y) + .post_scale(scale.width, scale.height) + .post_translate(center.x, center.y); self.vector_pipeline.draw( handle, *color, physical_bounds, - _pixels, + pixels, + transform, clip_mask, ); } diff --git a/tiny_skia/src/layer.rs b/tiny_skia/src/layer.rs index 3e42e4aa0f..e965181413 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, Rectangle, Size, + 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: f32, + scale: Size, ) { let image = Image::Raster { handle, filter_method, bounds: bounds * transformation, + rotation, + scale, }; self.images.push(image); @@ -137,11 +141,15 @@ impl Layer { color: Option, bounds: Rectangle, transformation: Transformation, + rotation: f32, + scale: Size, ) { let svg = Image::Vector { handle, color, bounds: bounds * transformation, + rotation, + scale, }; self.images.push(svg); @@ -256,6 +264,22 @@ impl Layer { Image::eq, ); + // let center = bounds.center(); + // let rotated_size = RotationLayout::Change + // .apply_to_size(bounds.size(), *rotation); + // + // let scaled_size = Size::new( + // rotated_size.width * scale.width, + // rotated_size.height * scale.height, + // ); + // + // let top_left = Point::new( + // center.x - scaled_size.width / 2.0, + // center.y - scaled_size.height / 2.0, + // ); + // + // Rectangle::new(top_left, scaled_size).expand(1.0) + damage.extend(text); damage.extend(primitives); damage.extend(images); diff --git a/tiny_skia/src/lib.rs b/tiny_skia/src/lib.rs index 4c2c9430c1..4e3ebad34c 100644 --- a/tiny_skia/src/lib.rs +++ b/tiny_skia/src/lib.rs @@ -29,7 +29,7 @@ pub use geometry::Geometry; use crate::core::renderer; use crate::core::{ - Background, Color, Font, Pixels, Point, Rectangle, Transformation, + Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation, }; use crate::engine::Engine; use crate::graphics::compositor; @@ -377,9 +377,18 @@ impl core::image::Renderer for Renderer { handle: Self::Handle, filter_method: core::image::FilterMethod, bounds: Rectangle, + rotation: f32, + scale: Size, ) { let (layer, transformation) = self.layers.current_mut(); - layer.draw_image(handle, filter_method, bounds, transformation); + layer.draw_image( + handle, + filter_method, + bounds, + transformation, + rotation, + scale, + ); } } @@ -397,9 +406,11 @@ impl core::svg::Renderer for Renderer { handle: core::svg::Handle, color: Option, bounds: Rectangle, + rotation: f32, + scale: Size, ) { let (layer, transformation) = self.layers.current_mut(); - layer.draw_svg(handle, color, bounds, transformation); + layer.draw_svg(handle, color, bounds, transformation, rotation, scale); } } diff --git a/tiny_skia/src/vector.rs b/tiny_skia/src/vector.rs index 5150cffeb9..8e3463f26a 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; @@ -34,6 +35,7 @@ impl Pipeline { color: Option, bounds: Rectangle, pixels: &mut tiny_skia::PixmapMut<'_>, + transform: Transform, clip_mask: Option<&tiny_skia::Mask>, ) { if let Some(image) = self.cache.borrow_mut().draw( @@ -46,7 +48,7 @@ impl Pipeline { bounds.y as i32, image, &tiny_skia::PixmapPaint::default(), - tiny_skia::Transform::identity(), + transform, clip_mask, ); } diff --git a/wgpu/src/image/mod.rs b/wgpu/src/image/mod.rs index 8b831a3c1b..69f8a8cadb 100644 --- a/wgpu/src/image/mod.rs +++ b/wgpu/src/image/mod.rs @@ -135,14 +135,20 @@ impl Pipeline { attributes: &wgpu::vertex_attr_array!( // Position 0 => Float32x2, - // Scale + // Center 1 => Float32x2, - // Atlas position + // Image size 2 => Float32x2, + // Rotation + 3 => Float32, + // Scale + 4 => Float32x2, + // Atlas position + 5 => Float32x2, // Atlas scale - 3 => Float32x2, + 6 => Float32x2, // Layer - 4 => Sint32, + 7 => Sint32, ), }], }, @@ -208,9 +214,10 @@ impl Pipeline { belt: &mut wgpu::util::StagingBelt, images: &Batch, transformation: Transformation, - scale: f32, + global_scale: f32, ) { - let transformation = transformation * Transformation::scale(scale); + let transformation = + transformation * Transformation::scale(global_scale); let nearest_instances: &mut Vec = &mut Vec::new(); let linear_instances: &mut Vec = &mut Vec::new(); @@ -224,6 +231,8 @@ impl Pipeline { handle, filter_method, bounds, + rotation, + scale, } => { if let Some(atlas_entry) = cache.upload_raster(device, encoder, handle) @@ -231,6 +240,8 @@ impl Pipeline { add_instances( [bounds.x, bounds.y], [bounds.width, bounds.height], + *rotation, + [scale.width, scale.height], atlas_entry, match filter_method { crate::core::image::FilterMethod::Nearest => { @@ -251,15 +262,24 @@ impl Pipeline { handle, color, bounds, + rotation, + scale, } => { let size = [bounds.width, bounds.height]; if let Some(atlas_entry) = cache.upload_vector( - device, encoder, handle, *color, size, scale, + device, + encoder, + handle, + *color, + size, + global_scale, ) { add_instances( [bounds.x, bounds.y], size, + *rotation, + [scale.width, scale.height], atlas_entry, nearest_instances, ); @@ -487,7 +507,10 @@ impl Data { #[derive(Debug, Clone, Copy, Zeroable, Pod)] struct Instance { _position: [f32; 2], + _center: [f32; 2], _size: [f32; 2], + _rotation: f32, + _scale: [f32; 2], _position_in_atlas: [f32; 2], _size_in_atlas: [f32; 2], _layer: u32, @@ -506,12 +529,27 @@ struct Uniforms { fn add_instances( image_position: [f32; 2], image_size: [f32; 2], + rotation: f32, + scale: [f32; 2], entry: &atlas::Entry, instances: &mut Vec, ) { + 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, + scale, + allocation, + instances, + ); } atlas::Entry::Fragmented { fragments, size } => { let scaling_x = image_size[0] / size.width as f32; @@ -537,7 +575,10 @@ fn add_instances( fragment_height as f32 * scaling_y, ]; - add_instance(position, size, allocation, instances); + add_instance( + position, center, size, rotation, scale, allocation, + instances, + ); } } } @@ -546,7 +587,10 @@ fn add_instances( #[inline] fn add_instance( position: [f32; 2], + center: [f32; 2], size: [f32; 2], + rotation: f32, + scale: [f32; 2], allocation: &atlas::Allocation, instances: &mut Vec, ) { @@ -556,7 +600,10 @@ fn add_instance( let instance = Instance { _position: position, + _center: center, _size: size, + _rotation: rotation, + _scale: scale, _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 9526c5a8d9..648ec476c5 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, Rectangle, Size, 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: f32, + scale: Size, ) { let image = Image::Raster { handle, filter_method, bounds: bounds * transformation, + rotation, + scale, }; self.images.push(image); @@ -133,11 +138,15 @@ impl Layer { color: Option, bounds: Rectangle, transformation: Transformation, + rotation: f32, + scale: Size, ) { let svg = Image::Vector { handle, color, bounds: bounds * transformation, + rotation, + scale, }; self.images.push(svg); diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index 178522de2a..a42d71c399 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -517,9 +517,18 @@ impl core::image::Renderer for Renderer { handle: Self::Handle, filter_method: core::image::FilterMethod, bounds: Rectangle, + rotation: f32, + scale: Size, ) { let (layer, transformation) = self.layers.current_mut(); - layer.draw_image(handle, filter_method, bounds, transformation); + layer.draw_image( + handle, + filter_method, + bounds, + transformation, + rotation, + scale, + ); } } @@ -534,9 +543,18 @@ impl core::svg::Renderer for Renderer { handle: core::svg::Handle, color_filter: Option, bounds: Rectangle, + rotation: f32, + scale: Size, ) { let (layer, transformation) = self.layers.current_mut(); - layer.draw_svg(handle, color_filter, bounds, transformation); + layer.draw_svg( + handle, + color_filter, + bounds, + transformation, + rotation, + scale, + ); } } diff --git a/wgpu/src/shader/image.wgsl b/wgpu/src/shader/image.wgsl index 7b2e523855..de9620980f 100644 --- a/wgpu/src/shader/image.wgsl +++ b/wgpu/src/shader/image.wgsl @@ -9,10 +9,13 @@ struct Globals { struct VertexInput { @builtin(vertex_index) vertex_index: u32, @location(0) pos: vec2, - @location(1) scale: vec2, - @location(2) atlas_pos: vec2, - @location(3) atlas_scale: vec2, - @location(4) layer: i32, + @location(1) center: vec2, + @location(2) image_size: vec2, + @location(3) rotation: f32, + @location(4) scale: vec2, + @location(5) atlas_pos: vec2, + @location(6) atlas_scale: vec2, + @location(7) layer: i32, } struct VertexOutput { @@ -25,24 +28,42 @@ struct VertexOutput { 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(v_pos * input.atlas_scale + input.atlas_pos); out.layer = f32(input.layer); - var transform: mat4x4 = mat4x4( + // Calculate the vertex position and move the center to the origin + v_pos = input.pos + v_pos * input.image_size - 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( + vec4(cos_rot, sin_rot, 0.0, 0.0), + vec4(-sin_rot, cos_rot, 0.0, 0.0), + vec4(0.0, 0.0, 1.0, 0.0), + vec4(0.0, 0.0, 0.0, 1.0) + ); + + // Scale the image and then translate to the final position by moving the center to the position + let scale_translate = mat4x4( vec4(input.scale.x, 0.0, 0.0, 0.0), vec4(0.0, input.scale.y, 0.0, 0.0), vec4(0.0, 0.0, 1.0, 0.0), - vec4(input.pos, 0.0, 1.0) + vec4(input.center, 0.0, 1.0) ); - out.position = globals.transform * transform * vec4(v_pos, 0.0, 1.0); + // Calculate the final position of the vertex + out.position = globals.transform * scale_translate * rotate * vec4(v_pos, 0.0, 1.0); return out; } @fragment fn fs_main(input: VertexOutput) -> @location(0) vec4 { + // Sample the texture at the given UV coordinate and layer. return textureSample(u_texture, u_sampler, input.uv, i32(input.layer)); } diff --git a/widget/src/image.rs b/widget/src/image.rs index 21d371b700..3a0a5e5393 100644 --- a/widget/src/image.rs +++ b/widget/src/image.rs @@ -1,5 +1,6 @@ //! Display images in your user interface. pub mod viewer; +use iced_renderer::core::{Point, RotationLayout}; pub use viewer::Viewer; use crate::core::image; @@ -8,7 +9,7 @@ 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, Rectangle, Size, Widget, }; pub use image::{FilterMethod, Handle}; @@ -36,6 +37,8 @@ pub struct Image { height: Length, content_fit: ContentFit, filter_method: FilterMethod, + rotation: f32, + rotation_layout: RotationLayout, } impl Image { @@ -47,6 +50,8 @@ impl Image { height: Length::Shrink, content_fit: ContentFit::Contain, filter_method: FilterMethod::default(), + rotation: 0.0, + rotation_layout: RotationLayout::Change, } } @@ -75,6 +80,18 @@ impl Image { self.filter_method = filter_method; self } + + /// Rotates the [`Image`] by the given angle in radians. + pub fn rotation(mut self, degrees: f32) -> Self { + self.rotation = degrees; + self + } + + /// Sets the [`RotationLayout`] of the [`Image`]. + pub fn rotation_layout(mut self, rotation_layout: RotationLayout) -> Self { + self.rotation_layout = rotation_layout; + self + } } /// Computes the layout of an [`Image`]. @@ -85,22 +102,25 @@ pub fn layout( width: Length, height: Length, content_fit: ContentFit, + rotation: f32, + rotation_layout: RotationLayout, ) -> layout::Node where Renderer: image::Renderer, { // 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_layout.apply_to_size(image_size, rotation); // 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 +144,45 @@ pub fn draw( handle: &Handle, content_fit: ContentFit, filter_method: FilterMethod, + rotation: f32, + rotation_layout: RotationLayout, ) where Renderer: image::Renderer, 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_layout.apply_to_size(image_size, rotation); 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 scale = Size::new( + adjusted_fit.width / rotated_size.width, + adjusted_fit.height / rotated_size.height, + ); 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 { - width: adjusted_fit.width, - height: adjusted_fit.height, - ..bounds + let position = match content_fit { + ContentFit::None => Point::new( + bounds.position().x + + (rotated_size.width - image_size.width) / 2.0, + bounds.position().y + + (rotated_size.height - image_size.height) / 2.0, + ), + _ => Point::new( + bounds.center_x() - image_size.width / 2.0, + bounds.center_y() - image_size.height / 2.0, + ), }; + let drawing_bounds = Rectangle::new(position, image_size); + renderer.draw_image( handle.clone(), filter_method, - drawing_bounds + offset, + drawing_bounds, + rotation, + scale, ); }; @@ -187,6 +220,8 @@ where self.width, self.height, self.content_fit, + self.rotation, + self.rotation_layout, ) } @@ -206,6 +241,8 @@ where &self.handle, self.content_fit, self.filter_method, + self.rotation, + self.rotation_layout, ); } } diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs index 214cb996b9..ccdfdebb16 100644 --- a/widget/src/image/viewer.rs +++ b/widget/src/image/viewer.rs @@ -341,6 +341,8 @@ where y: bounds.y, ..Rectangle::with_size(image_size) }, + 0.0, + Size::UNIT, ); }); }); diff --git a/widget/src/svg.rs b/widget/src/svg.rs index eb1421891a..21946af853 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, + RotationLayout, Size, Theme, Widget, }; use std::path::PathBuf; @@ -29,6 +29,8 @@ where height: Length, content_fit: ContentFit, class: Theme::Class<'a>, + rotation: f32, + rotation_layout: RotationLayout, } impl<'a, Theme> Svg<'a, Theme> @@ -43,6 +45,8 @@ where height: Length::Shrink, content_fit: ContentFit::Contain, class: Theme::default(), + rotation: 0.0, + rotation_layout: RotationLayout::Change, } } @@ -95,6 +99,18 @@ where self.class = class.into(); self } + + /// Rotates the [`Svg`] by the given angle in radians. + pub fn rotation(mut self, degrees: f32) -> Self { + self.rotation = degrees; + self + } + + /// Sets the [`RotationLayout`] of the [`Svg`]. + pub fn rotation_layout(mut self, rotation_layout: RotationLayout) -> Self { + self.rotation_layout = rotation_layout; + self + } } impl<'a, Message, Theme, Renderer> Widget @@ -120,11 +136,16 @@ 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_layout + .apply_to_size(image_size, self.rotation); + // 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,23 +174,35 @@ 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_layout + .apply_to_size(image_size, self.rotation); let bounds = layout.bounds(); - let adjusted_fit = self.content_fit.fit(image_size, bounds.size()); + let adjusted_fit = self.content_fit.fit(rotated_size, bounds.size()); + let scale = Size::new( + adjusted_fit.width / rotated_size.width, + adjusted_fit.height / rotated_size.height, + ); + let is_mouse_over = cursor.is_over(bounds); 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 { - width: adjusted_fit.width, - height: adjusted_fit.height, - ..bounds + let position = match self.content_fit { + ContentFit::None => Point::new( + bounds.position().x + + (rotated_size.width - image_size.width) / 2.0, + bounds.position().y + + (rotated_size.height - image_size.height) / 2.0, + ), + _ => Point::new( + bounds.center_x() - image_size.width / 2.0, + bounds.center_y() - image_size.height / 2.0, + ), }; + let drawing_bounds = Rectangle::new(position, image_size); + let status = if is_mouse_over { Status::Hovered } else { @@ -181,7 +214,9 @@ where renderer.draw_svg( self.handle.clone(), style.color, - drawing_bounds + offset, + drawing_bounds, + self.rotation, + scale, ); }; From a57313b23ecb9843856ca0ea08635b6121fcb2cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 2 May 2024 15:21:22 +0200 Subject: [PATCH 49/66] Simplify image rotation API and its internals --- core/src/angle.rs | 6 +++ core/src/content_fit.rs | 3 +- core/src/image.rs | 5 +-- core/src/lib.rs | 2 +- core/src/rectangle.rs | 16 ++++++++ core/src/renderer/null.rs | 9 ++--- core/src/rotation.rs | 71 +++++++++++++++++++++++---------- core/src/size.rs | 14 +++++++ core/src/svg.rs | 5 +-- core/src/vector.rs | 3 ++ graphics/src/image.rs | 12 ++---- renderer/src/fallback.rs | 12 +++--- src/lib.rs | 4 +- tiny_skia/src/engine.rs | 26 ++++++------ tiny_skia/src/layer.rs | 10 ++--- tiny_skia/src/lib.rs | 11 ++---- wgpu/src/image/mod.rs | 24 +++-------- wgpu/src/layer.rs | 10 ++--- wgpu/src/lib.rs | 20 +++------- wgpu/src/shader/image.wgsl | 21 +++------- widget/src/image.rs | 70 ++++++++++++++------------------ widget/src/image/viewer.rs | 7 ++-- widget/src/svg.rs | 81 ++++++++++++++++---------------------- 23 files changed, 218 insertions(+), 224 deletions(-) diff --git a/core/src/angle.rs b/core/src/angle.rs index dc3c0e93df..696307175f 100644 --- a/core/src/angle.rs +++ b/core/src/angle.rs @@ -65,6 +65,12 @@ impl From for Radians { } } +impl From for f32 { + fn from(radians: Radians) -> Self { + radians.0 + } +} + impl From for f64 { fn from(radians: Radians) -> Self { Self::from(radians.0) diff --git a/core/src/content_fit.rs b/core/src/content_fit.rs index 6bbedc7af2..56d2ffa66d 100644 --- a/core/src/content_fit.rs +++ b/core/src/content_fit.rs @@ -11,7 +11,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 +23,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. diff --git a/core/src/image.rs b/core/src/image.rs index 5d1ab44190..91a7fd36ac 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,7 +173,6 @@ pub trait Renderer: crate::Renderer { handle: Self::Handle, filter_method: FilterMethod, bounds: Rectangle, - rotation: f32, - scale: Size, + rotation: Radians, ); } diff --git a/core/src/lib.rs b/core/src/lib.rs index da3ddcac4e..32156441b7 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -65,7 +65,7 @@ pub use pixels::Pixels; pub use point::Point; pub use rectangle::Rectangle; pub use renderer::Renderer; -pub use rotation::RotationLayout; +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 2ab50137b1..fb66131a64 100644 --- a/core/src/rectangle.rs +++ b/core/src/rectangle.rs @@ -227,3 +227,19 @@ where } } } + +impl std::ops::Mul> for Rectangle +where + T: std::ops::Mul + Copy, +{ + type Output = Rectangle; + + fn mul(self, scale: Vector) -> 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 d2dcfe4dca..91519b4072 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,8 +172,7 @@ impl image::Renderer for () { _handle: Self::Handle, _filter_method: image::FilterMethod, _bounds: Rectangle, - _rotation: f32, - _scale: Size, + _rotation: Radians, ) { } } @@ -187,8 +187,7 @@ impl svg::Renderer for () { _handle: svg::Handle, _color: Option, _bounds: Rectangle, - _rotation: f32, - _scale: Size, + _rotation: Radians, ) { } } diff --git a/core/src/rotation.rs b/core/src/rotation.rs index 821aa4941d..ebb85f3cf5 100644 --- a/core/src/rotation.rs +++ b/core/src/rotation.rs @@ -1,37 +1,68 @@ -//! Control the rotation of some content (like an image) with the `RotationLayout` within a -//! space. -use crate::Size; +//! Control the rotation of some content (like an image) within a space. +use crate::{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, Hash, Clone, Copy, PartialEq, Eq)] -pub enum RotationLayout { - /// The layout is kept exactly as it was before the rotation. +#[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. - Keep, - /// The layout is adjusted to fit the rotated content. + /// + /// 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. - Change, + Solid(Radians), } -impl RotationLayout { - /// Applies the rotation to the layout while respecting the [`RotationLayout`] strategy. - /// The rotation is given in radians. - pub fn apply_to_size(&self, size: Size, rotation: f32) -> Size { +impl Rotation { + /// Returns the angle of the [`Rotation`] in [`Radians`]. + pub fn radians(self) -> Radians { + match self { + Rotation::Floating(radians) | Rotation::Solid(radians) => radians, + } + } + + /// Rotates the given [`Size`]. + pub fn apply(self, size: Size) -> Size { match self { - Self::Keep => size, - Self::Change => Size { - width: (size.width * rotation.cos()).abs() - + (size.height * rotation.sin()).abs(), - height: (size.width * rotation.sin()).abs() - + (size.height * rotation.cos()).abs(), - }, + Self::Floating(_) => size, + Self::Solid(rotation) => { + let radians = f32::from(rotation); + + Size { + width: (size.width * radians.cos()).abs() + + (size.height * radians.sin()).abs(), + height: (size.width * radians.sin()).abs() + + (size.height * radians.cos()).abs(), + } + } } } } + +impl Default for Rotation { + fn default() -> Self { + Self::Floating(Radians(0.0)) + } +} + +impl From for Rotation { + fn from(radians: Radians) -> Self { + Self::Floating(radians) + } +} + +impl From for Rotation { + fn from(radians: f32) -> Self { + Self::Floating(Radians(radians)) + } +} diff --git a/core/src/size.rs b/core/src/size.rs index c2b5671ab3..66be2d8597 100644 --- a/core/src/size.rs +++ b/core/src/size.rs @@ -113,3 +113,17 @@ where } } } + +impl std::ops::Mul> for Size +where + T: std::ops::Mul + Copy, +{ + type Output = Size; + + fn mul(self, scale: Vector) -> 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 74dd7f4ad7..01f102e320 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,7 +100,6 @@ pub trait Renderer: crate::Renderer { handle: Handle, color: Option, bounds: Rectangle, - rotation: f32, - scale: Size, + rotation: Radians, ); } diff --git a/core/src/vector.rs b/core/src/vector.rs index 1380c3b370..049e648f27 100644 --- a/core/src/vector.rs +++ b/core/src/vector.rs @@ -18,6 +18,9 @@ impl Vector { 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 std::ops::Add for Vector diff --git a/graphics/src/image.rs b/graphics/src/image.rs index 083248bf45..4fd6998d64 100644 --- a/graphics/src/image.rs +++ b/graphics/src/image.rs @@ -2,7 +2,7 @@ #[cfg(feature = "image")] pub use ::image as image_rs; -use crate::core::{image, svg, Color, Rectangle, Size}; +use crate::core::{image, svg, Color, Radians, Rectangle}; /// A raster or vector image. #[derive(Debug, Clone, PartialEq)] @@ -19,10 +19,7 @@ pub enum Image { bounds: Rectangle, /// The rotation of the image in radians - rotation: f32, - - /// The scale of the image after rotation - scale: Size, + rotation: Radians, }, /// A vector image. Vector { @@ -36,10 +33,7 @@ pub enum Image { bounds: Rectangle, /// The rotation of the image in radians - rotation: f32, - - /// The scale of the image after rotation - scale: Size, + rotation: Radians, }, } diff --git a/renderer/src/fallback.rs b/renderer/src/fallback.rs index 37e6ac43fe..a077031bc5 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,13 +154,12 @@ where handle: Self::Handle, filter_method: image::FilterMethod, bounds: Rectangle, - rotation: f32, - scale: Size, + rotation: Radians, ) { delegate!( self, renderer, - renderer.draw_image(handle, filter_method, bounds, rotation, scale) + renderer.draw_image(handle, filter_method, bounds, rotation) ); } } @@ -179,13 +178,12 @@ where handle: svg::Handle, color: Option, bounds: Rectangle, - rotation: f32, - scale: Size, + rotation: Radians, ) { delegate!( self, renderer, - renderer.draw_svg(handle, color, bounds, rotation, scale) + renderer.draw_svg(handle, color, bounds, rotation) ); } } diff --git a/src/lib.rs b/src/lib.rs index 9d07fed627..50ee7ecc1a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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, RotationLayout, 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 564d37526c..e9935bdb0b 100644 --- a/tiny_skia/src/engine.rs +++ b/tiny_skia/src/engine.rs @@ -551,7 +551,6 @@ impl Engine { filter_method, bounds, rotation, - scale, } => { let physical_bounds = *bounds * transformation; @@ -563,11 +562,13 @@ impl Engine { .then_some(clip_mask as &_); let center = physical_bounds.center(); - let transform = into_transform(transformation) - .post_rotate_at(rotation.to_degrees(), center.x, center.y) - .post_translate(-center.x, -center.y) - .post_scale(scale.width, scale.height) - .post_translate(center.x, center.y); + 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, @@ -584,7 +585,6 @@ impl Engine { color, bounds, rotation, - scale, } => { let physical_bounds = *bounds * transformation; @@ -596,11 +596,13 @@ impl Engine { .then_some(clip_mask as &_); let center = physical_bounds.center(); - let transform = into_transform(transformation) - .post_rotate_at(rotation.to_degrees(), center.x, center.y) - .post_translate(-center.x, -center.y) - .post_scale(scale.width, scale.height) - .post_translate(center.x, center.y); + 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, diff --git a/tiny_skia/src/layer.rs b/tiny_skia/src/layer.rs index e965181413..c8a31ba364 100644 --- a/tiny_skia/src/layer.rs +++ b/tiny_skia/src/layer.rs @@ -1,5 +1,5 @@ use crate::core::{ - image, renderer::Quad, svg, Background, Color, Point, Rectangle, Size, + image, renderer::Quad, svg, Background, Color, Point, Radians, Rectangle, Transformation, }; use crate::graphics::damage; @@ -121,15 +121,13 @@ impl Layer { filter_method: image::FilterMethod, bounds: Rectangle, transformation: Transformation, - rotation: f32, - scale: Size, + rotation: Radians, ) { let image = Image::Raster { handle, filter_method, bounds: bounds * transformation, rotation, - scale, }; self.images.push(image); @@ -141,15 +139,13 @@ impl Layer { color: Option, bounds: Rectangle, transformation: Transformation, - rotation: f32, - scale: Size, + rotation: Radians, ) { let svg = Image::Vector { handle, color, bounds: bounds * transformation, rotation, - scale, }; self.images.push(svg); diff --git a/tiny_skia/src/lib.rs b/tiny_skia/src/lib.rs index 4e3ebad34c..75aaaf92cd 100644 --- a/tiny_skia/src/lib.rs +++ b/tiny_skia/src/lib.rs @@ -29,7 +29,7 @@ pub use geometry::Geometry; use crate::core::renderer; use crate::core::{ - Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation, + Background, Color, Font, Pixels, Point, Radians, Rectangle, Transformation, }; use crate::engine::Engine; use crate::graphics::compositor; @@ -377,8 +377,7 @@ impl core::image::Renderer for Renderer { handle: Self::Handle, filter_method: core::image::FilterMethod, bounds: Rectangle, - rotation: f32, - scale: Size, + rotation: Radians, ) { let (layer, transformation) = self.layers.current_mut(); layer.draw_image( @@ -387,7 +386,6 @@ impl core::image::Renderer for Renderer { bounds, transformation, rotation, - scale, ); } } @@ -406,11 +404,10 @@ impl core::svg::Renderer for Renderer { handle: core::svg::Handle, color: Option, bounds: Rectangle, - rotation: f32, - scale: Size, + rotation: Radians, ) { let (layer, transformation) = self.layers.current_mut(); - layer.draw_svg(handle, color, bounds, transformation, rotation, scale); + layer.draw_svg(handle, color, bounds, transformation, rotation); } } diff --git a/wgpu/src/image/mod.rs b/wgpu/src/image/mod.rs index 69f8a8cadb..3ec341fc56 100644 --- a/wgpu/src/image/mod.rs +++ b/wgpu/src/image/mod.rs @@ -141,14 +141,12 @@ impl Pipeline { 2 => Float32x2, // Rotation 3 => Float32, - // Scale - 4 => Float32x2, // Atlas position - 5 => Float32x2, + 4 => Float32x2, // Atlas scale - 6 => Float32x2, + 5 => Float32x2, // Layer - 7 => Sint32, + 6 => Sint32, ), }], }, @@ -232,7 +230,6 @@ impl Pipeline { filter_method, bounds, rotation, - scale, } => { if let Some(atlas_entry) = cache.upload_raster(device, encoder, handle) @@ -240,8 +237,7 @@ impl Pipeline { add_instances( [bounds.x, bounds.y], [bounds.width, bounds.height], - *rotation, - [scale.width, scale.height], + f32::from(*rotation), atlas_entry, match filter_method { crate::core::image::FilterMethod::Nearest => { @@ -263,7 +259,6 @@ impl Pipeline { color, bounds, rotation, - scale, } => { let size = [bounds.width, bounds.height]; @@ -278,8 +273,7 @@ impl Pipeline { add_instances( [bounds.x, bounds.y], size, - *rotation, - [scale.width, scale.height], + f32::from(*rotation), atlas_entry, nearest_instances, ); @@ -510,7 +504,6 @@ struct Instance { _center: [f32; 2], _size: [f32; 2], _rotation: f32, - _scale: [f32; 2], _position_in_atlas: [f32; 2], _size_in_atlas: [f32; 2], _layer: u32, @@ -530,7 +523,6 @@ fn add_instances( image_position: [f32; 2], image_size: [f32; 2], rotation: f32, - scale: [f32; 2], entry: &atlas::Entry, instances: &mut Vec, ) { @@ -546,7 +538,6 @@ fn add_instances( center, image_size, rotation, - scale, allocation, instances, ); @@ -576,8 +567,7 @@ fn add_instances( ]; add_instance( - position, center, size, rotation, scale, allocation, - instances, + position, center, size, rotation, allocation, instances, ); } } @@ -590,7 +580,6 @@ fn add_instance( center: [f32; 2], size: [f32; 2], rotation: f32, - scale: [f32; 2], allocation: &atlas::Allocation, instances: &mut Vec, ) { @@ -603,7 +592,6 @@ fn add_instance( _center: center, _size: size, _rotation: rotation, - _scale: scale, _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 648ec476c5..e0242c5938 100644 --- a/wgpu/src/layer.rs +++ b/wgpu/src/layer.rs @@ -1,5 +1,5 @@ use crate::core::{ - renderer, Background, Color, Point, Rectangle, Size, Transformation, + renderer, Background, Color, Point, Radians, Rectangle, Transformation, }; use crate::graphics; use crate::graphics::color; @@ -118,15 +118,13 @@ impl Layer { filter_method: crate::core::image::FilterMethod, bounds: Rectangle, transformation: Transformation, - rotation: f32, - scale: Size, + rotation: Radians, ) { let image = Image::Raster { handle, filter_method, bounds: bounds * transformation, rotation, - scale, }; self.images.push(image); @@ -138,15 +136,13 @@ impl Layer { color: Option, bounds: Rectangle, transformation: Transformation, - rotation: f32, - scale: Size, + rotation: Radians, ) { let svg = Image::Vector { handle, color, bounds: bounds * transformation, rotation, - scale, }; self.images.push(svg); diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index a42d71c399..6920067bc2 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -61,7 +61,8 @@ pub use settings::Settings; pub use geometry::Geometry; use crate::core::{ - Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation, + Background, Color, Font, Pixels, Point, Radians, Rectangle, Size, + Transformation, Vector, }; use crate::graphics::text::{Editor, Paragraph}; use crate::graphics::Viewport; @@ -378,7 +379,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()), @@ -517,8 +517,7 @@ impl core::image::Renderer for Renderer { handle: Self::Handle, filter_method: core::image::FilterMethod, bounds: Rectangle, - rotation: f32, - scale: Size, + rotation: Radians, ) { let (layer, transformation) = self.layers.current_mut(); layer.draw_image( @@ -527,7 +526,6 @@ impl core::image::Renderer for Renderer { bounds, transformation, rotation, - scale, ); } } @@ -543,18 +541,10 @@ impl core::svg::Renderer for Renderer { handle: core::svg::Handle, color_filter: Option, bounds: Rectangle, - rotation: f32, - scale: Size, + rotation: Radians, ) { let (layer, transformation) = self.layers.current_mut(); - layer.draw_svg( - handle, - color_filter, - bounds, - transformation, - rotation, - scale, - ); + layer.draw_svg(handle, color_filter, bounds, transformation, rotation); } } diff --git a/wgpu/src/shader/image.wgsl b/wgpu/src/shader/image.wgsl index de9620980f..71bf939c4b 100644 --- a/wgpu/src/shader/image.wgsl +++ b/wgpu/src/shader/image.wgsl @@ -10,12 +10,11 @@ struct VertexInput { @builtin(vertex_index) vertex_index: u32, @location(0) pos: vec2, @location(1) center: vec2, - @location(2) image_size: vec2, + @location(2) scale: vec2, @location(3) rotation: f32, - @location(4) scale: vec2, - @location(5) atlas_pos: vec2, - @location(6) atlas_scale: vec2, - @location(7) layer: i32, + @location(4) atlas_pos: vec2, + @location(5) atlas_scale: vec2, + @location(6) layer: i32, } struct VertexOutput { @@ -36,7 +35,7 @@ fn vs_main(input: VertexInput) -> VertexOutput { out.layer = f32(input.layer); // Calculate the vertex position and move the center to the origin - v_pos = input.pos + v_pos * input.image_size - input.center; + v_pos = input.pos + v_pos * input.scale - input.center; // Apply the rotation around the center of the image let cos_rot = cos(input.rotation); @@ -48,16 +47,8 @@ fn vs_main(input: VertexInput) -> VertexOutput { vec4(0.0, 0.0, 0.0, 1.0) ); - // Scale the image and then translate to the final position by moving the center to the position - let scale_translate = mat4x4( - vec4(input.scale.x, 0.0, 0.0, 0.0), - vec4(0.0, input.scale.y, 0.0, 0.0), - vec4(0.0, 0.0, 1.0, 0.0), - vec4(input.center, 0.0, 1.0) - ); - // Calculate the final position of the vertex - out.position = globals.transform * scale_translate * rotate * vec4(v_pos, 0.0, 1.0); + out.position = globals.transform * (vec4(input.center, 0.0, 0.0) + rotate * vec4(v_pos, 0.0, 1.0)); return out; } diff --git a/widget/src/image.rs b/widget/src/image.rs index 3a0a5e5393..45209a9107 100644 --- a/widget/src/image.rs +++ b/widget/src/image.rs @@ -1,6 +1,5 @@ //! Display images in your user interface. pub mod viewer; -use iced_renderer::core::{Point, RotationLayout}; pub use viewer::Viewer; use crate::core::image; @@ -9,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, Widget, + ContentFit, Element, Layout, Length, Point, Rectangle, Rotation, Size, + Vector, Widget, }; pub use image::{FilterMethod, Handle}; @@ -37,8 +37,7 @@ pub struct Image { height: Length, content_fit: ContentFit, filter_method: FilterMethod, - rotation: f32, - rotation_layout: RotationLayout, + rotation: Rotation, } impl Image { @@ -48,10 +47,9 @@ impl Image { handle: handle.into(), width: Length::Shrink, height: Length::Shrink, - content_fit: ContentFit::Contain, + content_fit: ContentFit::default(), filter_method: FilterMethod::default(), - rotation: 0.0, - rotation_layout: RotationLayout::Change, + rotation: Rotation::default(), } } @@ -81,15 +79,9 @@ impl Image { self } - /// Rotates the [`Image`] by the given angle in radians. - pub fn rotation(mut self, degrees: f32) -> Self { - self.rotation = degrees; - self - } - - /// Sets the [`RotationLayout`] of the [`Image`]. - pub fn rotation_layout(mut self, rotation_layout: RotationLayout) -> Self { - self.rotation_layout = rotation_layout; + /// Applies the given [`Rotation`] to the [`Image`]. + pub fn rotation(mut self, rotation: impl Into) -> Self { + self.rotation = rotation.into(); self } } @@ -102,8 +94,7 @@ pub fn layout( width: Length, height: Length, content_fit: ContentFit, - rotation: f32, - rotation_layout: RotationLayout, + rotation: Rotation, ) -> layout::Node where Renderer: image::Renderer, @@ -114,7 +105,7 @@ where Size::new(image_size.width as f32, image_size.height as f32); // The rotated size of the image - let rotated_size = rotation_layout.apply_to_size(image_size, rotation); + 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, rotated_size); @@ -144,45 +135,44 @@ pub fn draw( handle: &Handle, content_fit: ContentFit, filter_method: FilterMethod, - rotation: f32, - rotation_layout: RotationLayout, + rotation: Rotation, ) where Renderer: image::Renderer, 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_layout.apply_to_size(image_size, rotation); + let rotated_size = rotation.apply(image_size); let bounds = layout.bounds(); let adjusted_fit = content_fit.fit(rotated_size, bounds.size()); - let scale = Size::new( + + let scale = Vector::new( adjusted_fit.width / rotated_size.width, adjusted_fit.height / rotated_size.height, ); - let render = |renderer: &mut Renderer| { - let position = match content_fit { - ContentFit::None => Point::new( - bounds.position().x - + (rotated_size.width - image_size.width) / 2.0, - bounds.position().y - + (rotated_size.height - image_size.height) / 2.0, - ), - _ => Point::new( - bounds.center_x() - image_size.width / 2.0, - bounds.center_y() - image_size.height / 2.0, - ), - }; + 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, image_size); + let drawing_bounds = Rectangle::new(position, final_size); + let render = |renderer: &mut Renderer| { renderer.draw_image( handle.clone(), filter_method, drawing_bounds, - rotation, - scale, + rotation.radians(), ); }; @@ -221,7 +211,6 @@ where self.height, self.content_fit, self.rotation, - self.rotation_layout, ) } @@ -242,7 +231,6 @@ where self.content_fit, self.filter_method, self.rotation, - self.rotation_layout, ); } } diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs index ccdfdebb16..ee4c0fbacd 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,8 +341,7 @@ where y: bounds.y, ..Rectangle::with_size(image_size) }, - 0.0, - Size::UNIT, + Radians(0.0), ); }); }); diff --git a/widget/src/svg.rs b/widget/src/svg.rs index 21946af853..c1fccba1b8 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, Point, Rectangle, - RotationLayout, Size, Theme, Widget, + Color, ContentFit, Element, Layout, Length, Point, Rectangle, Rotation, + Size, Theme, Vector, Widget, }; use std::path::PathBuf; @@ -29,8 +29,7 @@ where height: Length, content_fit: ContentFit, class: Theme::Class<'a>, - rotation: f32, - rotation_layout: RotationLayout, + rotation: Rotation, } impl<'a, Theme> Svg<'a, Theme> @@ -45,8 +44,7 @@ where height: Length::Shrink, content_fit: ContentFit::Contain, class: Theme::default(), - rotation: 0.0, - rotation_layout: RotationLayout::Change, + rotation: Rotation::default(), } } @@ -100,15 +98,9 @@ where self } - /// Rotates the [`Svg`] by the given angle in radians. - pub fn rotation(mut self, degrees: f32) -> Self { - self.rotation = degrees; - self - } - - /// Sets the [`RotationLayout`] of the [`Svg`]. - pub fn rotation_layout(mut self, rotation_layout: RotationLayout) -> Self { - self.rotation_layout = rotation_layout; + /// Applies the given [`Rotation`] to the [`Svg`]. + pub fn rotation(mut self, rotation: impl Into) -> Self { + self.rotation = rotation.into(); self } } @@ -137,9 +129,7 @@ where let image_size = Size::new(width as f32, height as f32); // The rotated size of the svg - let rotated_size = self - .rotation_layout - .apply_to_size(image_size, self.rotation); + 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, rotated_size); @@ -174,49 +164,46 @@ 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_layout - .apply_to_size(image_size, self.rotation); + let rotated_size = self.rotation.apply(image_size); let bounds = layout.bounds(); let adjusted_fit = self.content_fit.fit(rotated_size, bounds.size()); - let scale = Size::new( + 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 drawing_bounds = Rectangle::new(position, final_size); + let is_mouse_over = cursor.is_over(bounds); - let render = |renderer: &mut Renderer| { - let position = match self.content_fit { - ContentFit::None => Point::new( - bounds.position().x - + (rotated_size.width - image_size.width) / 2.0, - bounds.position().y - + (rotated_size.height - image_size.height) / 2.0, - ), - _ => Point::new( - bounds.center_x() - image_size.width / 2.0, - bounds.center_y() - image_size.height / 2.0, - ), - }; - - let drawing_bounds = Rectangle::new(position, image_size); - - let status = if is_mouse_over { - Status::Hovered - } else { - Status::Idle - }; - - let style = theme.style(&self.class, status); + let status = if is_mouse_over { + Status::Hovered + } else { + Status::Idle + }; + let style = theme.style(&self.class, status); + + let render = |renderer: &mut Renderer| { renderer.draw_svg( self.handle.clone(), style.color, drawing_bounds, - self.rotation, - scale, + self.rotation.radians(), ); }; From 610394b6957d9424aec1c50d927e34a0fb3fe5fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 2 May 2024 15:28:46 +0200 Subject: [PATCH 50/66] Rename `global_scale` to `scale` in `wgpu::image` --- wgpu/src/image/mod.rs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/wgpu/src/image/mod.rs b/wgpu/src/image/mod.rs index 3ec341fc56..285eb2f6e7 100644 --- a/wgpu/src/image/mod.rs +++ b/wgpu/src/image/mod.rs @@ -212,10 +212,9 @@ impl Pipeline { belt: &mut wgpu::util::StagingBelt, images: &Batch, transformation: Transformation, - global_scale: f32, + scale: f32, ) { - let transformation = - transformation * Transformation::scale(global_scale); + let transformation = transformation * Transformation::scale(scale); let nearest_instances: &mut Vec = &mut Vec::new(); let linear_instances: &mut Vec = &mut Vec::new(); @@ -263,12 +262,7 @@ impl Pipeline { let size = [bounds.width, bounds.height]; if let Some(atlas_entry) = cache.upload_vector( - device, - encoder, - handle, - *color, - size, - global_scale, + device, encoder, handle, *color, size, scale, ) { add_instances( [bounds.x, bounds.y], From efc55b655bfce98fc32e698cf3c2007e27be941a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 2 May 2024 17:14:20 +0200 Subject: [PATCH 51/66] Create `ferris` example to showcase `ContentFit` and `Rotation` --- core/src/angle.rs | 43 ++++++++++ core/src/content_fit.rs | 14 ++++ core/src/rotation.rs | 7 +- examples/ferris/Cargo.toml | 10 +++ examples/ferris/src/main.rs | 161 ++++++++++++++++++++++++++++++++++++ 5 files changed, 234 insertions(+), 1 deletion(-) create mode 100644 examples/ferris/Cargo.toml create mode 100644 examples/ferris/src/main.rs diff --git a/core/src/angle.rs b/core/src/angle.rs index 696307175f..8322273cbb 100644 --- a/core/src/angle.rs +++ b/core/src/angle.rs @@ -7,6 +7,11 @@ use std::ops::{Add, AddAssign, Div, Mul, RangeInclusive, Sub, SubAssign}; #[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(0.0)..=Self(360.0); +} + impl PartialEq for Degrees { fn eq(&self, other: &f32) -> bool { self.0.eq(other) @@ -19,6 +24,44 @@ impl PartialOrd for Degrees { } } +impl From for Degrees { + fn from(degrees: f32) -> Self { + Self(degrees) + } +} + +impl From for Degrees { + fn from(degrees: u8) -> Self { + Self(f32::from(degrees)) + } +} + +impl From for f32 { + fn from(degrees: Degrees) -> Self { + degrees.0 + } +} + +impl From for f64 { + fn from(degrees: Degrees) -> Self { + Self::from(degrees.0) + } +} + +impl num_traits::FromPrimitive for Degrees { + fn from_i64(n: i64) -> Option { + Some(Self(n as f32)) + } + + fn from_u64(n: u64) -> Option { + Some(Self(n as f32)) + } + + fn from_f64(n: f64) -> Option { + Some(Self(n as f32)) + } +} + /// Radians #[derive(Debug, Copy, Clone, PartialEq, PartialOrd)] pub struct Radians(pub f32); diff --git a/core/src/content_fit.rs b/core/src/content_fit.rs index 56d2ffa66d..1964271692 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 @@ -118,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/rotation.rs b/core/src/rotation.rs index ebb85f3cf5..f36ef0895f 100644 --- a/core/src/rotation.rs +++ b/core/src/rotation.rs @@ -1,5 +1,5 @@ //! Control the rotation of some content (like an image) within a space. -use crate::{Radians, Size}; +use crate::{Degrees, Radians, Size}; /// The strategy used to rotate the content. /// @@ -31,6 +31,11 @@ impl Rotation { } } + /// Returns the angle of the [`Rotation`] in [`Degrees`]. + pub fn degrees(self) -> Degrees { + Degrees(self.radians().0.to_degrees()) + } + /// Rotates the given [`Size`]. pub fn apply(self, size: Size) -> Size { match self { diff --git a/examples/ferris/Cargo.toml b/examples/ferris/Cargo.toml new file mode 100644 index 0000000000..c9fb8c13b8 --- /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 "] +edition = "2021" +publish = false + +[dependencies] +iced.workspace = true +iced.features = ["image"] diff --git a/examples/ferris/src/main.rs b/examples/ferris/src/main.rs new file mode 100644 index 0000000000..d846f56018 --- /dev/null +++ b/examples/ferris/src/main.rs @@ -0,0 +1,161 @@ +use iced::widget::{column, container, image, pick_list, row, slider, text}; +use iced::{ + Alignment, Color, ContentFit, Degrees, Element, Length, Rotation, Theme, +}; + +pub fn main() -> iced::Result { + iced::program("Ferris - Iced", Image::update, Image::view) + .theme(|_| Theme::TokyoNight) + .run() +} + +struct Image { + width: f32, + rotation: Rotation, + content_fit: ContentFit, +} + +#[derive(Debug, Clone, Copy)] +enum Message { + WidthChanged(f32), + RotationStrategyChanged(RotationStrategy), + RotationChanged(Degrees), + ContentFitChanged(ContentFit), +} + +impl Image { + fn update(&mut self, message: Message) { + match message { + Message::WidthChanged(width) => { + self.width = width; + } + 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; + } + } + } + + fn view(&self) -> Element { + let i_am_ferris = container( + 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) + ) + .explain(Color::WHITE), + "I am Ferris!" + ] + .spacing(20) + .align_items(Alignment::Center), + ) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y(); + + let sizing = row![ + pick_list( + [ + ContentFit::Contain, + ContentFit::Cover, + ContentFit::Fill, + ContentFit::None, + ContentFit::ScaleDown + ], + Some(self.content_fit), + Message::ContentFitChanged + ) + .width(Length::Fill), + column![ + slider(100.0..=500.0, self.width, Message::WidthChanged), + text(format!("Width: {}px", self.width)) + .size(14) + .line_height(1.0) + ] + .spacing(5) + .align_items(Alignment::Center) + ] + .spacing(10); + + let rotation = row![ + pick_list( + [RotationStrategy::Floating, RotationStrategy::Solid], + Some(match self.rotation { + Rotation::Floating(_) => RotationStrategy::Floating, + Rotation::Solid(_) => RotationStrategy::Solid, + }), + Message::RotationStrategyChanged, + ) + .width(Length::Fill), + column![ + slider( + Degrees::RANGE, + self.rotation.degrees(), + Message::RotationChanged + ), + text(format!( + "Rotation: {:.0}°", + f32::from(self.rotation.degrees()) + )) + .size(14) + .line_height(1.0) + ] + .spacing(5) + .align_items(Alignment::Center) + ] + .spacing(10); + + container(column![i_am_ferris, sizing, rotation].spacing(10)) + .padding(10) + .into() + } +} + +impl Default for Image { + fn default() -> Self { + Self { + width: 300.0, + rotation: Rotation::default(), + content_fit: ContentFit::default(), + } + } +} + +#[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", + }) + } +} From 568ac66486937a294f2a79cefea277e4eb46b81e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 2 May 2024 17:15:26 +0200 Subject: [PATCH 52/66] Remove commented code in `tiny_skia::layer` --- tiny_skia/src/layer.rs | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/tiny_skia/src/layer.rs b/tiny_skia/src/layer.rs index c8a31ba364..c907c93c2c 100644 --- a/tiny_skia/src/layer.rs +++ b/tiny_skia/src/layer.rs @@ -260,22 +260,6 @@ impl Layer { Image::eq, ); - // let center = bounds.center(); - // let rotated_size = RotationLayout::Change - // .apply_to_size(bounds.size(), *rotation); - // - // let scaled_size = Size::new( - // rotated_size.width * scale.width, - // rotated_size.height * scale.height, - // ); - // - // let top_left = Point::new( - // center.x - scaled_size.width / 2.0, - // center.y - scaled_size.height / 2.0, - // ); - // - // Rectangle::new(top_left, scaled_size).expand(1.0) - damage.extend(text); damage.extend(primitives); damage.extend(images); From 8efd32c51bc04113a393dbf85c962f7e0343a127 Mon Sep 17 00:00:00 2001 From: myuujiku Date: Thu, 2 May 2024 21:18:47 +0200 Subject: [PATCH 53/66] Apply bounds check when grabbing `image::Viewer` --- widget/src/image/viewer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs index 214cb996b9..2e6a752894 100644 --- a/widget/src/image/viewer.rs +++ b/widget/src/image/viewer.rs @@ -216,7 +216,7 @@ where event::Status::Captured } Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { - let Some(cursor_position) = cursor.position() else { + let Some(cursor_position) = cursor.position_over(bounds) else { return event::Status::Ignored; }; From eac5bcb64f17dfbb52b64ea4f95693462986bb69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 3 May 2024 07:04:57 +0200 Subject: [PATCH 54/66] Fix `Image::bounds` when rotation present in `iced_graphics` --- core/src/rectangle.rs | 16 ++++++++++++++-- core/src/rotation.rs | 14 +++----------- core/src/size.rs | 15 ++++++++++++++- examples/ferris/Cargo.toml | 2 +- graphics/src/image.rs | 7 +++++-- 5 files changed, 37 insertions(+), 17 deletions(-) diff --git a/core/src/rectangle.rs b/core/src/rectangle.rs index fb66131a64..1556e072e3 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 { /// X coordinate of the top-left corner. @@ -172,6 +172,18 @@ impl Rectangle { 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 for Rectangle { diff --git a/core/src/rotation.rs b/core/src/rotation.rs index f36ef0895f..00a8c302d3 100644 --- a/core/src/rotation.rs +++ b/core/src/rotation.rs @@ -36,20 +36,12 @@ impl Rotation { Degrees(self.radians().0.to_degrees()) } - /// Rotates the given [`Size`]. + /// 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) => { - let radians = f32::from(rotation); - - Size { - width: (size.width * radians.cos()).abs() - + (size.height * radians.sin()).abs(), - height: (size.width * radians.sin()).abs() - + (size.height * radians.cos()).abs(), - } - } + Self::Solid(rotation) => size.rotate(rotation), } } } diff --git a/core/src/size.rs b/core/src/size.rs index 66be2d8597..d74593552f 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 From<[T; 2]> for Size { diff --git a/examples/ferris/Cargo.toml b/examples/ferris/Cargo.toml index c9fb8c13b8..0d91749bda 100644 --- a/examples/ferris/Cargo.toml +++ b/examples/ferris/Cargo.toml @@ -7,4 +7,4 @@ publish = false [dependencies] iced.workspace = true -iced.features = ["image"] +iced.features = ["image", "debug"] diff --git a/graphics/src/image.rs b/graphics/src/image.rs index 4fd6998d64..9d09bf4c56 100644 --- a/graphics/src/image.rs +++ b/graphics/src/image.rs @@ -41,9 +41,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), } } } From 4010e3983d40e24a5d7b590d8fec9801651199ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 3 May 2024 07:23:55 +0200 Subject: [PATCH 55/66] Add `spin` mode to `ferris` example :crab: --- core/src/angle.rs | 26 +++++++++++++- core/src/rotation.rs | 7 ++++ examples/ferris/Cargo.toml | 2 +- examples/ferris/src/main.rs | 68 +++++++++++++++++++++++++++++-------- 4 files changed, 87 insertions(+), 16 deletions(-) diff --git a/core/src/angle.rs b/core/src/angle.rs index 8322273cbb..9c8a9b2448 100644 --- a/core/src/angle.rs +++ b/core/src/angle.rs @@ -1,7 +1,7 @@ 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)] @@ -48,6 +48,14 @@ impl From for f64 { } } +impl Mul 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 { Some(Self(n as f32)) @@ -156,6 +164,14 @@ impl Add for Radians { } } +impl Add 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; @@ -202,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 for Radians { fn eq(&self, other: &f32) -> bool { self.0.eq(other) diff --git a/core/src/rotation.rs b/core/src/rotation.rs index 00a8c302d3..afa8d79ee6 100644 --- a/core/src/rotation.rs +++ b/core/src/rotation.rs @@ -31,6 +31,13 @@ impl Rotation { } } + /// 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()) diff --git a/examples/ferris/Cargo.toml b/examples/ferris/Cargo.toml index 0d91749bda..e98341d24c 100644 --- a/examples/ferris/Cargo.toml +++ b/examples/ferris/Cargo.toml @@ -7,4 +7,4 @@ publish = false [dependencies] iced.workspace = true -iced.features = ["image", "debug"] +iced.features = ["image", "tokio", "debug"] diff --git a/examples/ferris/src/main.rs b/examples/ferris/src/main.rs index d846f56018..b7536740af 100644 --- a/examples/ferris/src/main.rs +++ b/examples/ferris/src/main.rs @@ -1,10 +1,16 @@ -use iced::widget::{column, container, image, pick_list, row, slider, text}; +use iced::time::Instant; +use iced::widget::{ + checkbox, column, container, image, pick_list, row, slider, text, +}; +use iced::window; use iced::{ - Alignment, Color, ContentFit, Degrees, Element, Length, Rotation, Theme, + 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() } @@ -13,6 +19,8 @@ struct Image { width: f32, rotation: Rotation, content_fit: ContentFit, + spin: bool, + last_tick: Instant, } #[derive(Debug, Clone, Copy)] @@ -21,6 +29,8 @@ enum Message { RotationStrategyChanged(RotationStrategy), RotationChanged(Degrees), ContentFitChanged(ContentFit), + SpinToggled(bool), + RedrawRequested(Instant), } impl Image { @@ -50,6 +60,29 @@ impl Image { 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 { + if self.spin { + window::frames().map(Message::RedrawRequested) + } else { + Subscription::none() } } @@ -111,18 +144,23 @@ impl Image { Message::RotationStrategyChanged, ) .width(Length::Fill), - column![ - slider( - Degrees::RANGE, - self.rotation.degrees(), - Message::RotationChanged - ), - text(format!( - "Rotation: {:.0}°", - f32::from(self.rotation.degrees()) - )) - .size(14) - .line_height(1.0) + row![ + column![ + slider( + Degrees::RANGE, + self.rotation.degrees(), + Message::RotationChanged + ), + text(format!( + "Rotation: {:.0}°", + f32::from(self.rotation.degrees()) + )) + .size(14) + .line_height(1.0) + ] + .spacing(5) + .align_items(Alignment::Center), + checkbox("Spin!", self.spin).on_toggle(Message::SpinToggled) ] .spacing(5) .align_items(Alignment::Center) @@ -141,6 +179,8 @@ impl Default for Image { width: 300.0, rotation: Rotation::default(), content_fit: ContentFit::default(), + spin: false, + last_tick: Instant::now(), } } } From 15057a05c118dafcb8cf90d4119e66caaa6026c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 3 May 2024 09:11:46 +0200 Subject: [PATCH 56/66] Introduce `center` widget helper ... and also make `center_x` and `center_y` set `width` and `height` to `Length::Fill`, respectively. This targets the most common use case when centering things and removes a bunch of boilerplate as a result. --- examples/checkbox/src/main.rs | 11 +--- examples/combo_box/src/main.rs | 9 +-- examples/component/src/main.rs | 8 +-- examples/custom_quad/src/main.rs | 11 +--- examples/custom_shader/src/main.rs | 9 +-- examples/custom_widget/src/main.rs | 11 +--- examples/download_progress/src/main.rs | 12 +--- examples/editor/src/main.rs | 2 +- examples/events/src/main.rs | 9 +-- examples/exit/src/main.rs | 12 +--- examples/ferris/src/main.rs | 77 ++++++++++++------------- examples/geometry/src/main.rs | 11 +--- examples/layout/src/main.rs | 20 ++----- examples/loading_spinners/src/main.rs | 10 +--- examples/loupe/src/main.rs | 10 +--- examples/modal/src/main.rs | 45 +++++---------- examples/multi_window/src/main.rs | 13 ++--- examples/pane_grid/src/main.rs | 7 +-- examples/pokedex/src/main.rs | 9 +-- examples/qr_code/src/main.rs | 14 +---- examples/screenshot/src/main.rs | 21 ++----- examples/scrollable/src/main.rs | 2 +- examples/slider/src/main.rs | 16 ++--- examples/stopwatch/src/main.rs | 11 +--- examples/styling/src/main.rs | 13 ++--- examples/svg/src/main.rs | 17 ++---- examples/system_information/src/main.rs | 9 +-- examples/toast/src/main.rs | 10 +--- examples/todos/src/main.rs | 14 ++--- examples/tooltip/src/main.rs | 11 +--- examples/tour/src/main.rs | 4 +- examples/url_handler/src/main.rs | 11 +--- examples/websocket/src/main.rs | 10 +--- tiny_skia/src/engine.rs | 32 +++++----- tiny_skia/src/lib.rs | 6 +- wgpu/src/lib.rs | 8 +-- widget/src/container.rs | 62 +++++++++++++++++++- widget/src/helpers.rs | 21 +++++++ 38 files changed, 249 insertions(+), 339 deletions(-) diff --git a/examples/checkbox/src/main.rs b/examples/checkbox/src/main.rs index 3894933644..bec4a9542e 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 2feb4522ab..ff759ab472 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 b2c71b3f74..5625f12a54 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 { - 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 c093e240ea..b3eee2184c 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 aa3dafe9b5..463b2df939 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 { diff --git a/examples/custom_widget/src/main.rs b/examples/custom_widget/src/main.rs index aa49ebd0ef..261dcb8176 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 9f4769e009..e031ac440d 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 ed16018a5c..af05031ada 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, ) -> Element<'a, Message> { - let action = button(container(content).width(30).center_x()); + let action = button(container(content).center_x().width(30)); if let Some(on_press) = on_press { tooltip( diff --git a/examples/events/src/main.rs b/examples/events/src/main.rs index bf568c945e..999ce8ef99 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 7bed272df2..2de97e2020 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/src/main.rs b/examples/ferris/src/main.rs index b7536740af..13d746dd8c 100644 --- a/examples/ferris/src/main.rs +++ b/examples/ferris/src/main.rs @@ -1,6 +1,6 @@ use iced::time::Instant; use iced::widget::{ - checkbox, column, container, image, pick_list, row, slider, text, + center, checkbox, column, container, image, pick_list, row, slider, text, }; use iced::window; use iced::{ @@ -87,28 +87,22 @@ impl Image { } fn view(&self) -> Element { - let i_am_ferris = container( - 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) - ) - .explain(Color::WHITE), - "I am Ferris!" - ] - .spacing(20) - .align_items(Alignment::Center), - ) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y(); + 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) + ) + .explain(Color::WHITE), + "I am Ferris!" + ] + .spacing(20) + .align_items(Alignment::Center); let sizing = row![ pick_list( @@ -126,13 +120,14 @@ impl Image { column![ slider(100.0..=500.0, self.width, Message::WidthChanged), text(format!("Width: {}px", self.width)) - .size(14) + .size(12) .line_height(1.0) ] - .spacing(5) + .spacing(2) .align_items(Alignment::Center) ] - .spacing(10); + .spacing(10) + .align_items(Alignment::End); let rotation = row![ pick_list( @@ -144,30 +139,34 @@ impl Image { Message::RotationStrategyChanged, ) .width(Length::Fill), - row![ - column![ + column![ + row![ slider( Degrees::RANGE, self.rotation.degrees(), Message::RotationChanged ), - text(format!( - "Rotation: {:.0}°", - f32::from(self.rotation.degrees()) - )) - .size(14) - .line_height(1.0) + checkbox("Spin!", self.spin) + .text_size(12) + .on_toggle(Message::SpinToggled) + .size(12) ] - .spacing(5) + .spacing(10) .align_items(Alignment::Center), - checkbox("Spin!", self.spin).on_toggle(Message::SpinToggled) + text(format!( + "Rotation: {:.0}°", + f32::from(self.rotation.degrees()) + )) + .size(12) + .line_height(1.0) ] - .spacing(5) + .spacing(2) .align_items(Alignment::Center) ] - .spacing(10); + .spacing(10) + .align_items(Alignment::End); - container(column![i_am_ferris, sizing, rotation].spacing(10)) + container(column![center(i_am_ferris), sizing, rotation].spacing(10)) .padding(10) .into() } diff --git a/examples/geometry/src/main.rs b/examples/geometry/src/main.rs index 31b8a4ce3f..bf7801a999 100644 --- a/examples/geometry/src/main.rs +++ b/examples/geometry/src/main.rs @@ -153,7 +153,7 @@ mod rainbow { } use iced::widget::{column, container, scrollable}; -use iced::{Element, Length}; +use iced::Element; use rainbow::rainbow; pub fn main() -> iced::Result { @@ -176,12 +176,7 @@ fn view(_state: &()) -> Element<'_, ()> { .spacing(20) .max_width(500); - let scrollable = - scrollable(container(content).width(Length::Fill).center_x()); + let scrollable = scrollable(container(content).center_x()); - container(scrollable) - .width(Length::Fill) - .height(Length::Fill) - .center_y() - .into() + container(scrollable).center_y().into() } diff --git a/examples/layout/src/main.rs b/examples/layout/src/main.rs index 66d7909118..5f14c03b74 100644 --- a/examples/layout/src/main.rs +++ b/examples/layout/src/main.rs @@ -1,8 +1,8 @@ use iced::keyboard; use iced::mouse; use iced::widget::{ - button, canvas, checkbox, column, container, horizontal_space, pick_list, - row, scrollable, text, + button, canvas, center, checkbox, column, container, horizontal_space, + pick_list, row, scrollable, text, }; use iced::{ color, Alignment, Element, Font, Length, Point, Rectangle, Renderer, @@ -76,7 +76,7 @@ impl Layout { .spacing(20) .align_items(Alignment::Center); - let example = container(if self.explain { + let example = center(if self.explain { self.example.view().explain(color!(0x0000ff)) } else { self.example.view() @@ -87,11 +87,7 @@ impl Layout { container::Style::default() .with_border(palette.background.strong.color, 4.0) }) - .padding(4) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y(); + .padding(4); let controls = row([ (!self.example.is_first()).then_some( @@ -195,12 +191,7 @@ impl Default for Example { } fn centered<'a>() -> Element<'a, Message> { - container(text("I am centered!").size(50)) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y() - .into() + center(text("I am centered!").size(50)).into() } fn column_<'a>() -> Element<'a, Message> { @@ -260,7 +251,6 @@ fn application<'a>() -> Element<'a, Message> { .align_items(Alignment::Center), ) .style(container::rounded_box) - .height(Length::Fill) .center_y(); let content = container( diff --git a/examples/loading_spinners/src/main.rs b/examples/loading_spinners/src/main.rs index eaa4d57ed4..e8d67ab5dc 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 42b7f47148..c4d3b449b2 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 { - 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 a345924d09..a012c310dd 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 5a5e70c16c..74339e0cb3 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 { let content = self.windows.get(&window).unwrap().view(window); - container(content) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y() - .into() + center(content).into() } fn theme(&self, window: window::Id) -> Self::Theme { @@ -210,6 +207,6 @@ impl Window { .align_items(Alignment::Center), ); - container(content).width(200).center_x().into() + container(content).center_x().width(200).into() } } diff --git a/examples/pane_grid/src/main.rs b/examples/pane_grid/src/main.rs index 1711278553..e74ea1ee81 100644 --- a/examples/pane_grid/src/main.rs +++ b/examples/pane_grid/src/main.rs @@ -291,12 +291,7 @@ fn view_content<'a>( .spacing(10) .align_items(Alignment::Center); - container(scrollable(content)) - .width(Length::Fill) - .height(Length::Fill) - .padding(5) - .center_y() - .into() + container(scrollable(content)).center_y().padding(5).into() } fn view_controls<'a>( diff --git a/examples/pokedex/src/main.rs b/examples/pokedex/src/main.rs index 35f2323642..be20094d35 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 b93adf0436..c6a904581c 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 5c175ccc0a..82495a1a1d 100644 --- a/examples/screenshot/src/main.rs +++ b/examples/screenshot/src/main.rs @@ -123,12 +123,10 @@ impl Example { }; let image = container(image) + .center_y() .padding(10) .style(container::rounded_box) - .width(Length::FillPortion(2)) - .height(Length::Fill) - .center_x() - .center_y(); + .width(Length::FillPortion(2)); let crop_origin_controls = row![ text("X:") @@ -213,12 +211,7 @@ impl Example { .spacing(40) }; - let side_content = container(controls) - .align_x(alignment::Horizontal::Center) - .width(Length::FillPortion(1)) - .height(Length::Fill) - .center_y() - .center_x(); + let side_content = container(controls).center_y(); let content = row![side_content, image] .spacing(10) @@ -226,13 +219,7 @@ impl Example { .height(Length::Fill) .align_items(Alignment::Center); - container(content) - .width(Length::Fill) - .height(Length::Fill) - .padding(10) - .center_x() - .center_y() - .into() + container(content).padding(10).into() } fn subscription(&self) -> Subscription { diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs index c02e754bed..bbb6497fcc 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 b3a47614c8..5ffdc9c6f3 100644 --- a/examples/slider/src/main.rs +++ b/examples/slider/src/main.rs @@ -1,5 +1,5 @@ -use iced::widget::{column, container, slider, text, vertical_slider}; -use iced::{Element, Length}; +use iced::widget::{center, column, container, slider, text, vertical_slider}; +use iced::Element; pub fn main() -> iced::Result { iced::run("Slider - Iced", Slider::update, Slider::view) @@ -54,18 +54,14 @@ impl Slider { let text = text(self.value); - container( + center( column![ - container(v_slider).width(Length::Fill).center_x(), - container(h_slider).width(Length::Fill).center_x(), - container(text).width(Length::Fill).center_x(), + container(v_slider).center_x(), + container(h_slider).center_x(), + container(text).center_x() ] .spacing(25), ) - .height(Length::Fill) - .width(Length::Fill) - .center_x() - .center_y() .into() } } diff --git a/examples/stopwatch/src/main.rs b/examples/stopwatch/src/main.rs index b9eb19cfb8..bbe9d0ffe4 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 73268da01d..57e8f47e11 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 0dcf9bc128..45b4671643 100644 --- a/examples/svg/src/main.rs +++ b/examples/svg/src/main.rs @@ -1,4 +1,4 @@ -use iced::widget::{checkbox, column, container, svg}; +use iced::widget::{center, checkbox, column, container, svg}; use iced::{color, Element, Length}; pub fn main() -> iced::Result { @@ -44,19 +44,12 @@ impl Tiger { checkbox("Apply a color filter", self.apply_color_filter) .on_toggle(Message::ToggleColorFilter); - container( - column![ - svg, - container(apply_color_filter).width(Length::Fill).center_x() - ] - .spacing(20) - .height(Length::Fill), + center( + column![svg, container(apply_color_filter).center_x()] + .spacing(20) + .height(Length::Fill), ) - .width(Length::Fill) - .height(Length::Fill) .padding(20) - .center_x() - .center_y() .into() } } diff --git a/examples/system_information/src/main.rs b/examples/system_information/src/main.rs index a6ac27a694..89a8383a23 100644 --- a/examples/system_information/src/main.rs +++ b/examples/system_information/src/main.rs @@ -1,5 +1,5 @@ use iced::widget::{button, column, container, text}; -use iced::{system, Command, Element, Length}; +use iced::{system, Command, Element}; pub fn main() -> iced::Result { iced::program("System Information - Iced", Example::update, Example::view) @@ -136,11 +136,6 @@ impl Example { } }; - container(content) - .center_x() - .center_y() - .width(Length::Fill) - .height(Length::Fill) - .into() + container(content).center().into() } } diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs index 9968962c43..0fcf08c4ba 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 7768c1d512..8119bc915a 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -1,8 +1,8 @@ use iced::alignment::{self, Alignment}; use iced::keyboard; use iced::widget::{ - self, button, checkbox, column, container, keyed_column, row, scrollable, - text, text_input, Text, + self, button, center, checkbox, column, container, keyed_column, row, + scrollable, text, text_input, Text, }; use iced::window; use iced::{Command, Element, Font, Length, Subscription}; @@ -238,7 +238,7 @@ impl Todos { .spacing(20) .max_width(800); - scrollable(container(content).padding(40).center_x()).into() + scrollable(container(content).center_x().padding(40)).into() } } } @@ -435,19 +435,16 @@ impl Filter { } fn loading_message<'a>() -> Element<'a, Message> { - container( + center( text("Loading...") .horizontal_alignment(alignment::Horizontal::Center) .size(50), ) - .width(Length::Fill) - .height(Length::Fill) - .center_y() .into() } fn empty_message(message: &str) -> Element<'_, Message> { - container( + center( text(message) .width(Length::Fill) .size(25) @@ -455,7 +452,6 @@ fn empty_message(message: &str) -> Element<'_, Message> { .color([0.7, 0.7, 0.7]), ) .height(200) - .center_y() .into() } diff --git a/examples/tooltip/src/main.rs b/examples/tooltip/src/main.rs index b660306856..f48f688a15 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 e3a2ca8796..bae6490d7a 100644 --- a/examples/tour/src/main.rs +++ b/examples/tour/src/main.rs @@ -76,11 +76,10 @@ impl Tour { } else { content }) - .width(Length::Fill) .center_x(), ); - container(scrollable).height(Length::Fill).center_y().into() + container(scrollable).center_y().into() } } @@ -670,7 +669,6 @@ fn ferris<'a>( .filter_method(filter_method) .width(width), ) - .width(Length::Fill) .center_x() } diff --git a/examples/url_handler/src/main.rs b/examples/url_handler/src/main.rs index df705b6c1f..800a188b28 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 b479fe8911..e7ececf773 100644 --- a/examples/websocket/src/main.rs +++ b/examples/websocket/src/main.rs @@ -1,9 +1,7 @@ mod echo; use iced::alignment::{self, Alignment}; -use iced::widget::{ - button, column, container, row, scrollable, text, text_input, -}; +use iced::widget::{button, center, column, row, scrollable, text, text_input}; use iced::{color, Command, Element, Length, Subscription}; use once_cell::sync::Lazy; @@ -85,14 +83,10 @@ impl WebSocket { fn view(&self) -> Element { 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/tiny_skia/src/engine.rs b/tiny_skia/src/engine.rs index e9935bdb0b..544ff61407 100644 --- a/tiny_skia/src/engine.rs +++ b/tiny_skia/src/engine.rs @@ -539,10 +539,10 @@ impl Engine { pub fn draw_image( &mut self, image: &Image, - transformation: Transformation, - pixels: &mut tiny_skia::PixmapMut<'_>, - clip_mask: &mut tiny_skia::Mask, - clip_bounds: Rectangle, + _transformation: Transformation, + _pixels: &mut tiny_skia::PixmapMut<'_>, + _clip_mask: &mut tiny_skia::Mask, + _clip_bounds: Rectangle, ) { match image { #[cfg(feature = "image")] @@ -552,19 +552,19 @@ impl Engine { bounds, rotation, } => { - let physical_bounds = *bounds * transformation; + let physical_bounds = *bounds * _transformation; - if !clip_bounds.intersects(&physical_bounds) { + if !_clip_bounds.intersects(&physical_bounds) { return; } - let clip_mask = (!physical_bounds.is_within(&clip_bounds)) - .then_some(clip_mask as &_); + 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( + let transform = into_transform(_transformation).post_rotate_at( radians.to_degrees(), center.x, center.y, @@ -574,7 +574,7 @@ impl Engine { handle, *filter_method, *bounds, - pixels, + _pixels, transform, clip_mask, ); @@ -586,19 +586,19 @@ impl Engine { bounds, rotation, } => { - let physical_bounds = *bounds * transformation; + let physical_bounds = *bounds * _transformation; - if !clip_bounds.intersects(&physical_bounds) { + if !_clip_bounds.intersects(&physical_bounds) { return; } - let clip_mask = (!physical_bounds.is_within(&clip_bounds)) - .then_some(clip_mask as &_); + 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( + let transform = into_transform(_transformation).post_rotate_at( radians.to_degrees(), center.x, center.y, @@ -608,7 +608,7 @@ impl Engine { handle, *color, physical_bounds, - pixels, + _pixels, transform, clip_mask, ); diff --git a/tiny_skia/src/lib.rs b/tiny_skia/src/lib.rs index 75aaaf92cd..e0cbfa0d0c 100644 --- a/tiny_skia/src/lib.rs +++ b/tiny_skia/src/lib.rs @@ -29,7 +29,7 @@ pub use geometry::Geometry; use crate::core::renderer; use crate::core::{ - Background, Color, Font, Pixels, Point, Radians, Rectangle, Transformation, + Background, Color, Font, Pixels, Point, Rectangle, Transformation, }; use crate::engine::Engine; use crate::graphics::compositor; @@ -377,7 +377,7 @@ impl core::image::Renderer for Renderer { handle: Self::Handle, filter_method: core::image::FilterMethod, bounds: Rectangle, - rotation: Radians, + rotation: core::Radians, ) { let (layer, transformation) = self.layers.current_mut(); layer.draw_image( @@ -404,7 +404,7 @@ impl core::svg::Renderer for Renderer { handle: core::svg::Handle, color: Option, bounds: Rectangle, - rotation: Radians, + rotation: core::Radians, ) { let (layer, transformation) = self.layers.current_mut(); layer.draw_svg(handle, color, bounds, transformation, rotation); diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index 6920067bc2..eb600dde77 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -61,8 +61,8 @@ pub use settings::Settings; pub use geometry::Geometry; use crate::core::{ - Background, Color, Font, Pixels, Point, Radians, Rectangle, Size, - Transformation, Vector, + Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation, + Vector, }; use crate::graphics::text::{Editor, Paragraph}; use crate::graphics::Viewport; @@ -517,7 +517,7 @@ impl core::image::Renderer for Renderer { handle: Self::Handle, filter_method: core::image::FilterMethod, bounds: Rectangle, - rotation: Radians, + rotation: core::Radians, ) { let (layer, transformation) = self.layers.current_mut(); layer.draw_image( @@ -541,7 +541,7 @@ impl core::svg::Renderer for Renderer { handle: core::svg::Handle, color_filter: Option, bounds: Rectangle, - rotation: Radians, + rotation: core::Radians, ) { let (layer, transformation) = self.layers.current_mut(); layer.draw_svg(handle, color_filter, bounds, transformation, rotation); diff --git a/widget/src/container.rs b/widget/src/container.rs index 21405722ef..8b6638d4d4 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -92,6 +92,49 @@ where self } + /// Sets the [`Container`] to fill the available space in the horizontal axis. + /// + /// This can be useful to quickly position content when chained with + /// alignment functions—like [`center_x`]. + /// + /// Calling this method is equivalent to calling [`width`] with a + /// [`Length::Fill`]. + /// + /// [`center_x`]: Self::center_x + /// [`width`]: Self::width + pub fn fill_x(self) -> Self { + self.width(Length::Fill) + } + + /// Sets the [`Container`] to fill the available space in the vetical axis. + /// + /// This can be useful to quickly position content when chained with + /// alignment functions—like [`center_y`]. + /// + /// Calling this method is equivalent to calling [`height`] with a + /// [`Length::Fill`]. + /// + /// [`center_y`]: Self::center_x + /// [`height`]: Self::height + pub fn fill_y(self) -> Self { + self.height(Length::Fill) + } + + /// Sets the [`Container`] to fill all the available space. + /// + /// This can be useful to quickly position content when chained with + /// alignment functions—like [`center`]. + /// + /// Calling this method is equivalent to chaining [`fill_x`] and + /// [`fill_y`]. + /// + /// [`center`]: Self::center + /// [`fill_x`]: Self::fill_x + /// [`fill_y`]: Self::fill_y + pub fn fill(self) -> Self { + self.width(Length::Fill).height(Length::Fill) + } + /// Sets the maximum width of the [`Container`]. pub fn max_width(mut self, max_width: impl Into) -> Self { self.max_width = max_width.into().0; @@ -116,18 +159,33 @@ where self } - /// Centers the contents in the horizontal axis of the [`Container`]. + /// Sets the [`Container`] to fill the available space in the horizontal axis + /// and centers its contents there. pub fn center_x(mut self) -> Self { + self.width = Length::Fill; self.horizontal_alignment = alignment::Horizontal::Center; self } - /// Centers the contents in the vertical axis of the [`Container`]. + /// Sets the [`Container`] to fill the available space in the vertical axis + /// and centers its contents there. pub fn center_y(mut self) -> Self { + self.height = Length::Fill; self.vertical_alignment = alignment::Vertical::Center; self } + /// Centers the contents in both the horizontal and vertical axes of the + /// [`Container`]. + /// + /// This is equivalent to chaining [`center_x`] and [`center_y`]. + /// + /// [`center_x`]: Self::center_x + /// [`center_y`]: Self::center_y + pub fn center(self) -> Self { + self.center_x().center_y() + } + /// Sets whether the contents of the [`Container`] should be clipped on /// overflow. pub fn clip(mut self, clip: bool) -> Self { diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 48c5dde400..fd8614f55f 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -78,6 +78,27 @@ where Container::new(content) } +/// Creates a new [`Container`] that fills all the available space +/// and centers its contents inside. +/// +/// This is equivalent to: +/// ```rust,no_run +/// # use iced_widget::Container; +/// # fn container(x: A) -> Container<'static, ()> { unreachable!() } +/// let centered = container("Centered!").center(); +/// ``` +/// +/// [`Container`]: crate::Container +pub fn center<'a, Message, Theme, Renderer>( + content: impl Into>, +) -> Container<'a, Message, Theme, Renderer> +where + Theme: container::Catalog + 'a, + Renderer: core::Renderer, +{ + container(content).fill().center() +} + /// Creates a new [`Column`] with the given children. pub fn column<'a, Message, Theme, Renderer>( children: impl IntoIterator>, From 4c658c41e9e5bf2c0c963ef49c1830257781b16e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 3 May 2024 09:25:47 +0200 Subject: [PATCH 57/66] Auto-focus `text_input` in `websocket` example --- examples/websocket/src/main.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/examples/websocket/src/main.rs b/examples/websocket/src/main.rs index e7ececf773..ba1e102992 100644 --- a/examples/websocket/src/main.rs +++ b/examples/websocket/src/main.rs @@ -1,7 +1,9 @@ mod echo; use iced::alignment::{self, Alignment}; -use iced::widget::{button, center, column, row, scrollable, text, text_input}; +use iced::widget::{ + self, button, center, column, row, scrollable, text, text_input, +}; use iced::{color, Command, Element, Length, Subscription}; use once_cell::sync::Lazy; @@ -29,7 +31,10 @@ enum Message { impl WebSocket { fn load() -> Command { - 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 { From fa9e1d96ea1924b51749b775ea0e67e69bc8a305 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 3 May 2024 13:25:58 +0200 Subject: [PATCH 58/66] Introduce dynamic `opacity` support for `Image` and `Svg` --- core/src/image.rs | 1 + core/src/renderer/null.rs | 2 ++ core/src/svg.rs | 1 + examples/ferris/src/main.rs | 45 +++++++++++++++++++++++-------------- graphics/src/image.rs | 10 +++++++-- renderer/src/fallback.rs | 12 ++++++++-- tiny_skia/src/engine.rs | 4 ++++ tiny_skia/src/layer.rs | 4 ++++ tiny_skia/src/lib.rs | 12 +++++++++- tiny_skia/src/raster.rs | 2 ++ tiny_skia/src/vector.rs | 6 ++++- wgpu/src/image/mod.rs | 22 +++++++++++++----- wgpu/src/layer.rs | 4 ++++ wgpu/src/lib.rs | 12 +++++++++- wgpu/src/shader/image.wgsl | 11 +++++---- widget/src/image.rs | 14 ++++++++++++ widget/src/image/viewer.rs | 1 + widget/src/svg.rs | 12 ++++++++++ 18 files changed, 142 insertions(+), 33 deletions(-) diff --git a/core/src/image.rs b/core/src/image.rs index 91a7fd36ac..82ecdd0f59 100644 --- a/core/src/image.rs +++ b/core/src/image.rs @@ -174,5 +174,6 @@ pub trait Renderer: crate::Renderer { filter_method: FilterMethod, bounds: Rectangle, rotation: Radians, + opacity: f32, ); } diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index 91519b4072..e8709dbc93 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -173,6 +173,7 @@ impl image::Renderer for () { _filter_method: image::FilterMethod, _bounds: Rectangle, _rotation: Radians, + _opacity: f32, ) { } } @@ -188,6 +189,7 @@ impl svg::Renderer for () { _color: Option, _bounds: Rectangle, _rotation: Radians, + _opacity: f32, ) { } } diff --git a/core/src/svg.rs b/core/src/svg.rs index 01f102e320..946b8156b1 100644 --- a/core/src/svg.rs +++ b/core/src/svg.rs @@ -101,5 +101,6 @@ pub trait Renderer: crate::Renderer { color: Option, bounds: Rectangle, rotation: Radians, + opacity: f32, ); } diff --git a/examples/ferris/src/main.rs b/examples/ferris/src/main.rs index 13d746dd8c..1d0e918130 100644 --- a/examples/ferris/src/main.rs +++ b/examples/ferris/src/main.rs @@ -17,6 +17,7 @@ pub fn main() -> iced::Result { struct Image { width: f32, + opacity: f32, rotation: Rotation, content_fit: ContentFit, spin: bool, @@ -26,6 +27,7 @@ struct Image { #[derive(Debug, Clone, Copy)] enum Message { WidthChanged(f32), + OpacityChanged(f32), RotationStrategyChanged(RotationStrategy), RotationChanged(Degrees), ContentFitChanged(ContentFit), @@ -39,6 +41,9 @@ impl Image { Message::WidthChanged(width) => { self.width = width; } + Message::OpacityChanged(opacity) => { + self.opacity = opacity; + } Message::RotationStrategyChanged(strategy) => { self.rotation = match strategy { RotationStrategy::Floating => { @@ -97,6 +102,7 @@ impl Image { .width(self.width) .content_fit(self.content_fit) .rotation(self.rotation) + .opacity(self.opacity) ) .explain(Color::WHITE), "I am Ferris!" @@ -117,14 +123,15 @@ impl Image { Message::ContentFitChanged ) .width(Length::Fill), - column![ + with_value( slider(100.0..=500.0, self.width, Message::WidthChanged), - text(format!("Width: {}px", self.width)) - .size(12) - .line_height(1.0) - ] - .spacing(2) - .align_items(Alignment::Center) + format!("Width: {}px", self.width) + ), + with_value( + slider(0.0..=1.0, self.opacity, Message::OpacityChanged) + .step(0.01), + format!("Opacity: {:.2}", self.opacity) + ) ] .spacing(10) .align_items(Alignment::End); @@ -139,7 +146,7 @@ impl Image { Message::RotationStrategyChanged, ) .width(Length::Fill), - column![ + with_value( row![ slider( Degrees::RANGE, @@ -153,15 +160,8 @@ impl Image { ] .spacing(10) .align_items(Alignment::Center), - text(format!( - "Rotation: {:.0}°", - f32::from(self.rotation.degrees()) - )) - .size(12) - .line_height(1.0) - ] - .spacing(2) - .align_items(Alignment::Center) + format!("Rotation: {:.0}°", f32::from(self.rotation.degrees())) + ) ] .spacing(10) .align_items(Alignment::End); @@ -176,6 +176,7 @@ impl Default for Image { fn default() -> Self { Self { width: 300.0, + opacity: 1.0, rotation: Rotation::default(), content_fit: ContentFit::default(), spin: false, @@ -198,3 +199,13 @@ impl std::fmt::Display for RotationStrategy { }) } } + +fn with_value<'a>( + control: impl Into>, + 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/graphics/src/image.rs b/graphics/src/image.rs index 9d09bf4c56..318592bec1 100644 --- a/graphics/src/image.rs +++ b/graphics/src/image.rs @@ -18,8 +18,11 @@ pub enum Image { /// The bounds of the image. bounds: Rectangle, - /// The rotation of the image in radians + /// The rotation of the image. rotation: Radians, + + /// The opacity of the image. + opacity: f32, }, /// A vector image. Vector { @@ -32,8 +35,11 @@ pub enum Image { /// The bounds of the image. bounds: Rectangle, - /// The rotation of the image in radians + /// The rotation of the image. rotation: Radians, + + /// The opacity of the image. + opacity: f32, }, } diff --git a/renderer/src/fallback.rs b/renderer/src/fallback.rs index a077031bc5..6a169692db 100644 --- a/renderer/src/fallback.rs +++ b/renderer/src/fallback.rs @@ -155,11 +155,18 @@ where filter_method: image::FilterMethod, bounds: Rectangle, rotation: Radians, + opacity: f32, ) { delegate!( self, renderer, - renderer.draw_image(handle, filter_method, bounds, rotation) + renderer.draw_image( + handle, + filter_method, + bounds, + rotation, + opacity + ) ); } } @@ -179,11 +186,12 @@ where color: Option, bounds: Rectangle, rotation: Radians, + opacity: f32, ) { delegate!( self, renderer, - renderer.draw_svg(handle, color, bounds, rotation) + renderer.draw_svg(handle, color, bounds, rotation, opacity) ); } } diff --git a/tiny_skia/src/engine.rs b/tiny_skia/src/engine.rs index 544ff61407..028b304fb3 100644 --- a/tiny_skia/src/engine.rs +++ b/tiny_skia/src/engine.rs @@ -551,6 +551,7 @@ impl Engine { filter_method, bounds, rotation, + opacity, } => { let physical_bounds = *bounds * _transformation; @@ -574,6 +575,7 @@ impl Engine { handle, *filter_method, *bounds, + *opacity, _pixels, transform, clip_mask, @@ -585,6 +587,7 @@ impl Engine { color, bounds, rotation, + opacity, } => { let physical_bounds = *bounds * _transformation; @@ -608,6 +611,7 @@ impl Engine { handle, *color, physical_bounds, + *opacity, _pixels, transform, clip_mask, diff --git a/tiny_skia/src/layer.rs b/tiny_skia/src/layer.rs index c907c93c2c..48fca1d80e 100644 --- a/tiny_skia/src/layer.rs +++ b/tiny_skia/src/layer.rs @@ -122,12 +122,14 @@ impl Layer { bounds: Rectangle, transformation: Transformation, rotation: Radians, + opacity: f32, ) { let image = Image::Raster { handle, filter_method, bounds: bounds * transformation, rotation, + opacity, }; self.images.push(image); @@ -140,12 +142,14 @@ impl Layer { 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 e0cbfa0d0c..1aabff00c0 100644 --- a/tiny_skia/src/lib.rs +++ b/tiny_skia/src/lib.rs @@ -378,6 +378,7 @@ impl core::image::Renderer for Renderer { filter_method: core::image::FilterMethod, bounds: Rectangle, rotation: core::Radians, + opacity: f32, ) { let (layer, transformation) = self.layers.current_mut(); layer.draw_image( @@ -386,6 +387,7 @@ impl core::image::Renderer for Renderer { bounds, transformation, rotation, + opacity, ); } } @@ -405,9 +407,17 @@ impl core::svg::Renderer for Renderer { color: Option, bounds: Rectangle, rotation: core::Radians, + opacity: f32, ) { let (layer, transformation) = self.layers.current_mut(); - layer.draw_svg(handle, color, bounds, transformation, rotation); + layer.draw_svg( + handle, + color, + bounds, + transformation, + rotation, + opacity, + ); } } diff --git a/tiny_skia/src/raster.rs b/tiny_skia/src/raster.rs index 907fce7c40..c40f55b2ff 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 8e3463f26a..bbe08cb806 100644 --- a/tiny_skia/src/vector.rs +++ b/tiny_skia/src/vector.rs @@ -34,6 +34,7 @@ impl Pipeline { handle: &Handle, color: Option, bounds: Rectangle, + opacity: f32, pixels: &mut tiny_skia::PixmapMut<'_>, transform: Transform, clip_mask: Option<&tiny_skia::Mask>, @@ -47,7 +48,10 @@ impl Pipeline { bounds.x as i32, bounds.y as i32, image, - &tiny_skia::PixmapPaint::default(), + &tiny_skia::PixmapPaint { + opacity, + ..tiny_skia::PixmapPaint::default() + }, transform, clip_mask, ); diff --git a/wgpu/src/image/mod.rs b/wgpu/src/image/mod.rs index 285eb2f6e7..063822aa82 100644 --- a/wgpu/src/image/mod.rs +++ b/wgpu/src/image/mod.rs @@ -137,16 +137,18 @@ impl Pipeline { 0 => Float32x2, // Center 1 => Float32x2, - // Image size + // Scale 2 => Float32x2, // Rotation 3 => Float32, + // Opacity + 4 => Float32, // Atlas position - 4 => Float32x2, - // Atlas scale 5 => Float32x2, + // Atlas scale + 6 => Float32x2, // Layer - 6 => Sint32, + 7 => Sint32, ), }], }, @@ -229,6 +231,7 @@ impl Pipeline { filter_method, bounds, rotation, + opacity, } => { if let Some(atlas_entry) = cache.upload_raster(device, encoder, handle) @@ -237,6 +240,7 @@ impl Pipeline { [bounds.x, bounds.y], [bounds.width, bounds.height], f32::from(*rotation), + *opacity, atlas_entry, match filter_method { crate::core::image::FilterMethod::Nearest => { @@ -258,6 +262,7 @@ impl Pipeline { color, bounds, rotation, + opacity, } => { let size = [bounds.width, bounds.height]; @@ -268,6 +273,7 @@ impl Pipeline { [bounds.x, bounds.y], size, f32::from(*rotation), + *opacity, atlas_entry, nearest_instances, ); @@ -498,6 +504,7 @@ struct Instance { _center: [f32; 2], _size: [f32; 2], _rotation: f32, + _opacity: f32, _position_in_atlas: [f32; 2], _size_in_atlas: [f32; 2], _layer: u32, @@ -517,6 +524,7 @@ fn add_instances( image_position: [f32; 2], image_size: [f32; 2], rotation: f32, + opacity: f32, entry: &atlas::Entry, instances: &mut Vec, ) { @@ -532,6 +540,7 @@ fn add_instances( center, image_size, rotation, + opacity, allocation, instances, ); @@ -561,7 +570,8 @@ fn add_instances( ]; add_instance( - position, center, size, rotation, allocation, instances, + position, center, size, rotation, opacity, allocation, + instances, ); } } @@ -574,6 +584,7 @@ fn add_instance( center: [f32; 2], size: [f32; 2], rotation: f32, + opacity: f32, allocation: &atlas::Allocation, instances: &mut Vec, ) { @@ -586,6 +597,7 @@ fn add_instance( _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 e0242c5938..9551311d3f 100644 --- a/wgpu/src/layer.rs +++ b/wgpu/src/layer.rs @@ -119,12 +119,14 @@ impl Layer { 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,12 +139,14 @@ impl Layer { 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 eb600dde77..4c16802996 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -518,6 +518,7 @@ impl core::image::Renderer for Renderer { filter_method: core::image::FilterMethod, bounds: Rectangle, rotation: core::Radians, + opacity: f32, ) { let (layer, transformation) = self.layers.current_mut(); layer.draw_image( @@ -526,6 +527,7 @@ impl core::image::Renderer for Renderer { bounds, transformation, rotation, + opacity, ); } } @@ -542,9 +544,17 @@ impl core::svg::Renderer for Renderer { color_filter: Option, bounds: Rectangle, rotation: core::Radians, + opacity: f32, ) { let (layer, transformation) = self.layers.current_mut(); - layer.draw_svg(handle, color_filter, bounds, transformation, rotation); + layer.draw_svg( + handle, + color_filter, + bounds, + transformation, + rotation, + opacity, + ); } } diff --git a/wgpu/src/shader/image.wgsl b/wgpu/src/shader/image.wgsl index 71bf939c4b..accefc1797 100644 --- a/wgpu/src/shader/image.wgsl +++ b/wgpu/src/shader/image.wgsl @@ -12,15 +12,17 @@ struct VertexInput { @location(1) center: vec2, @location(2) scale: vec2, @location(3) rotation: f32, - @location(4) atlas_pos: vec2, - @location(5) atlas_scale: vec2, - @location(6) layer: i32, + @location(4) opacity: f32, + @location(5) atlas_pos: vec2, + @location(6) atlas_scale: vec2, + @location(7) layer: i32, } struct VertexOutput { @builtin(position) position: vec4, @location(0) uv: vec2, @location(1) layer: f32, // this should be an i32, but naga currently reads that as requiring interpolation. + @location(2) opacity: f32, } @vertex @@ -33,6 +35,7 @@ fn vs_main(input: VertexInput) -> VertexOutput { // Map the vertex position to the atlas texture. out.uv = vec2(v_pos * input.atlas_scale + input.atlas_pos); out.layer = f32(input.layer); + out.opacity = input.opacity; // Calculate the vertex position and move the center to the origin v_pos = input.pos + v_pos * input.scale - input.center; @@ -56,5 +59,5 @@ fn vs_main(input: VertexInput) -> VertexOutput { @fragment fn fs_main(input: VertexOutput) -> @location(0) vec4 { // Sample the texture at the given UV coordinate and layer. - return textureSample(u_texture, u_sampler, input.uv, i32(input.layer)); + return textureSample(u_texture, u_sampler, input.uv, i32(input.layer)) * vec4(1.0, 1.0, 1.0, input.opacity); } diff --git a/widget/src/image.rs b/widget/src/image.rs index 45209a9107..80e17263b2 100644 --- a/widget/src/image.rs +++ b/widget/src/image.rs @@ -38,6 +38,7 @@ pub struct Image { content_fit: ContentFit, filter_method: FilterMethod, rotation: Rotation, + opacity: f32, } impl Image { @@ -50,6 +51,7 @@ impl Image { content_fit: ContentFit::default(), filter_method: FilterMethod::default(), rotation: Rotation::default(), + opacity: 1.0, } } @@ -84,6 +86,15 @@ impl Image { 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) -> Self { + self.opacity = opacity.into(); + self + } } /// Computes the layout of an [`Image`]. @@ -136,6 +147,7 @@ pub fn draw( content_fit: ContentFit, filter_method: FilterMethod, rotation: Rotation, + opacity: f32, ) where Renderer: image::Renderer, Handle: Clone, @@ -173,6 +185,7 @@ pub fn draw( filter_method, drawing_bounds, rotation.radians(), + opacity, ); }; @@ -231,6 +244,7 @@ where 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 5eb76452ca..8fe6f02109 100644 --- a/widget/src/image/viewer.rs +++ b/widget/src/image/viewer.rs @@ -342,6 +342,7 @@ where ..Rectangle::with_size(image_size) }, Radians(0.0), + 1.0, ); }); }); diff --git a/widget/src/svg.rs b/widget/src/svg.rs index c1fccba1b8..4551bcadcd 100644 --- a/widget/src/svg.rs +++ b/widget/src/svg.rs @@ -30,6 +30,7 @@ where content_fit: ContentFit, class: Theme::Class<'a>, rotation: Rotation, + opacity: f32, } impl<'a, Theme> Svg<'a, Theme> @@ -45,6 +46,7 @@ where content_fit: ContentFit::Contain, class: Theme::default(), rotation: Rotation::default(), + opacity: 1.0, } } @@ -103,6 +105,15 @@ where 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) -> Self { + self.opacity = opacity.into(); + self + } } impl<'a, Message, Theme, Renderer> Widget @@ -204,6 +215,7 @@ where style.color, drawing_bounds, self.rotation.radians(), + self.opacity, ); }; From afb4cb99b92a196bf4dd15a09a8f9bd191293fdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 3 May 2024 13:32:51 +0200 Subject: [PATCH 59/66] Improve layout of `ferris` example :crab: --- examples/ferris/src/main.rs | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/examples/ferris/src/main.rs b/examples/ferris/src/main.rs index 1d0e918130..0400c3765d 100644 --- a/examples/ferris/src/main.rs +++ b/examples/ferris/src/main.rs @@ -110,7 +110,7 @@ impl Image { .spacing(20) .align_items(Alignment::Center); - let sizing = row![ + let fit = row![ pick_list( [ ContentFit::Contain, @@ -123,20 +123,6 @@ impl Image { Message::ContentFitChanged ) .width(Length::Fill), - 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) - ) - ] - .spacing(10) - .align_items(Alignment::End); - - let rotation = row![ pick_list( [RotationStrategy::Floating, RotationStrategy::Solid], Some(match self.rotation { @@ -146,6 +132,20 @@ impl Image { 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( @@ -166,7 +166,7 @@ impl Image { .spacing(10) .align_items(Alignment::End); - container(column![center(i_am_ferris), sizing, rotation].spacing(10)) + container(column![fit, center(i_am_ferris), properties].spacing(10)) .padding(10) .into() } From 547446f0de076149a4c61e6a4179308b266fd9fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Mon, 6 May 2024 12:14:42 +0200 Subject: [PATCH 60/66] Fix windows fighting over shared `image::Cache` Image caches are local to each window now. --- benches/wgpu.rs | 3 ++- examples/integration/src/main.rs | 2 +- wgpu/src/engine.rs | 7 ++++-- wgpu/src/image/atlas.rs | 38 +++++++++++++++++++++++++--- wgpu/src/image/cache.rs | 43 ++++++++------------------------ wgpu/src/image/mod.rs | 38 ++++++++-------------------- wgpu/src/lib.rs | 15 ++++++++--- wgpu/src/window/compositor.rs | 1 + 8 files changed, 77 insertions(+), 70 deletions(-) diff --git a/benches/wgpu.rs b/benches/wgpu.rs index 3dd415dbed..2d308666e7 100644 --- a/benches/wgpu.rs +++ b/benches/wgpu.rs @@ -70,7 +70,8 @@ fn benchmark( Some(Antialiasing::MSAAx4), ); - let mut renderer = Renderer::new(&engine, Font::DEFAULT, Pixels::from(16)); + let mut renderer = + Renderer::new(&device, &engine, Font::DEFAULT, Pixels::from(16)); let viewport = graphics::Viewport::with_physical_size(Size::new(3840, 2160), 2.0); diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs index c4b57ecf26..c28332100d 100644 --- a/examples/integration/src/main.rs +++ b/examples/integration/src/main.rs @@ -157,7 +157,7 @@ pub fn main() -> Result<(), Box> { 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)); + Renderer::new(&device, &engine, Font::default(), Pixels::from(16)); let mut state = program::State::new( controls, diff --git a/wgpu/src/engine.rs b/wgpu/src/engine.rs index 96cd6db8d0..782fd58cec 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 ae43c3b4b6..a1ec0d7b31 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, layers: Vec, } impl Atlas { - pub fn new(device: &wgpu::Device, backend: wgpu::Backend) -> Self { + pub fn new( + device: &wgpu::Device, + backend: wgpu::Backend, + texture_layout: Arc, + ) -> 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 32118ed637..94f7071dc5 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, + ) -> 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>); - -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 063822aa82..daa2fe1611 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; #[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, constant_layout: wgpu::BindGroupLayout, - cache: cache::Shared, layers: Vec, prepare_layer: usize, } @@ -186,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( @@ -212,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, @@ -221,8 +217,6 @@ impl Pipeline { let nearest_instances: &mut Vec = &mut Vec::new(); let linear_instances: &mut Vec = &mut Vec::new(); - let mut cache = self.cache.lock(); - for image in images { match &image { #[cfg(feature = "image")] @@ -288,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, @@ -323,6 +307,7 @@ impl Pipeline { pub fn render<'a>( &'a self, + cache: &'a Cache, layer: usize, bounds: Rectangle, render_pass: &mut wgpu::RenderPass<'a>, @@ -337,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; } } diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index 4c16802996..35da321184 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -67,6 +67,8 @@ use crate::core::{ use crate::graphics::text::{Editor, Paragraph}; use crate::graphics::Viewport; +use std::cell::RefCell; + /// A [`wgpu`] graphics renderer for [`iced`]. /// /// [`wgpu`]: https://github.com/gfx-rs/wgpu-rs @@ -82,11 +84,12 @@ pub struct Renderer { // TODO: Centralize all the image feature handling #[cfg(any(feature = "svg", feature = "image"))] - image_cache: image::cache::Shared, + image_cache: RefCell, } impl Renderer { pub fn new( + _device: &wgpu::Device, _engine: &Engine, default_font: Font, default_text_size: Pixels, @@ -100,7 +103,7 @@ impl Renderer { text_storage: text::Storage::new(), #[cfg(any(feature = "svg", feature = "image"))] - image_cache: _engine.image_cache().clone(), + image_cache: RefCell::new(_engine.create_image_cache(_device)), } } @@ -191,6 +194,7 @@ impl Renderer { device, encoder, &mut engine.staging_belt, + &mut self.image_cache.borrow_mut(), &layer.images, viewport.projection(), scale_factor, @@ -246,6 +250,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::::from(Rectangle::with_size( @@ -359,6 +365,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, @@ -509,7 +516,7 @@ impl core::image::Renderer for Renderer { type Handle = core::image::Handle; fn measure_image(&self, handle: &Self::Handle) -> Size { - self.image_cache.lock().measure_image(handle) + self.image_cache.borrow_mut().measure_image(handle) } fn draw_image( @@ -535,7 +542,7 @@ impl core::image::Renderer for Renderer { #[cfg(feature = "svg")] impl core::svg::Renderer for Renderer { fn measure_svg(&self, handle: &core::svg::Handle) -> Size { - self.image_cache.lock().measure_svg(handle) + self.image_cache.borrow_mut().measure_svg(handle) } fn draw_svg( diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index 095afd48a9..baf9f3151f 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -290,6 +290,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, From ea64e4f63af7a7af1bde869ff6acd9203122b151 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Mon, 6 May 2024 12:17:43 +0200 Subject: [PATCH 61/66] Trim `image::Cache` after `wgpu::Renderer::present` --- wgpu/src/lib.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index 35da321184..ad8ac59195 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -125,6 +125,9 @@ impl Renderer { self.triangle_storage.trim(); self.text_storage.trim(); + + #[cfg(any(feature = "svg", feature = "image"))] + self.image_cache.borrow_mut().trim(); } fn prepare( From 2645524f88414393d8b3ca9c6fe801b32b5ebd33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 7 May 2024 15:50:18 +0200 Subject: [PATCH 62/66] Update `winit` to `0.30` --- .github/workflows/check.yml | 2 - Cargo.toml | 4 +- examples/integration/README.md | 19 +- examples/integration/index.html | 21 - examples/integration/src/main.rs | 577 ++++++++++++----------- src/application.rs | 19 +- wgpu/src/lib.rs | 8 +- winit/Cargo.toml | 2 + winit/src/application.rs | 383 ++++++++++----- winit/src/conversion.rs | 58 ++- winit/src/multi_window.rs | 428 ++++++++++------- winit/src/multi_window/window_manager.rs | 3 +- 12 files changed, 891 insertions(+), 633 deletions(-) delete mode 100644 examples/integration/index.html diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index ceac39e7a9..8f5e65d628 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/Cargo.toml b/Cargo.toml index 60773da01d..06f7d19858 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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/examples/integration/README.md b/examples/integration/README.md index aa3a6e94ad..bac0640cd5 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 920bc4a063..0000000000 --- a/examples/integration/index.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - Iced - wgpu + WebGL integration - - -

integration_wgpu

- - - - - diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs index c28332100d..e1c7d62fbc 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> { - #[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::().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, + device: wgpu::Device, + queue: wgpu::Queue, + surface: wgpu::Surface<'static>, + format: wgpu::TextureFormat, + engine: Engine, + renderer: Renderer, + scene: Scene, + state: program::State, + cursor_position: Option>, + 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(&device, &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/src/application.rs b/src/application.rs index 9197834b23..d12ba73dcc 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::Executor, ::Compositor, - >(settings.into(), renderer_settings); - - #[cfg(target_arch = "wasm32")] - { - use crate::futures::FutureExt; - use iced_futures::backend::wasm::wasm_bindgen::Executor; - - Executor::new() - .expect("Create Wasm executor") - .spawn(run.map(|_| ())); - - Ok(()) - } - - #[cfg(not(target_arch = "wasm32"))] - Ok(crate::futures::executor::block_on(run)?) + >(settings.into(), renderer_settings)?) } } diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index ad8ac59195..1fbdbe9a3d 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -67,8 +67,6 @@ use crate::core::{ use crate::graphics::text::{Editor, Paragraph}; use crate::graphics::Viewport; -use std::cell::RefCell; - /// A [`wgpu`] graphics renderer for [`iced`]. /// /// [`wgpu`]: https://github.com/gfx-rs/wgpu-rs @@ -84,7 +82,7 @@ pub struct Renderer { // TODO: Centralize all the image feature handling #[cfg(any(feature = "svg", feature = "image"))] - image_cache: RefCell, + image_cache: std::cell::RefCell, } impl Renderer { @@ -103,7 +101,9 @@ impl Renderer { text_storage: text::Storage::new(), #[cfg(any(feature = "svg", feature = "image"))] - image_cache: RefCell::new(_engine.create_image_cache(_device)), + image_cache: std::cell::RefCell::new( + _engine.create_image_cache(_device), + ), } } diff --git a/winit/Cargo.toml b/winit/Cargo.toml index dccb7c07f0..6d3dddde8e 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 a447c9da05..e056f4c554 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( +pub fn run( settings: Settings, 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,267 @@ 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::( + 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 { + instance: std::pin::Pin>, + context: task::Context<'static>, + boot: Option>, + sender: mpsc::UnboundedSender>, + receiver: mpsc::UnboundedReceiver, + error: Option, + #[cfg(target_arch = "wasm32")] + is_booted: std::rc::Rc>, + #[cfg(target_arch = "wasm32")] + queued_events: Vec>, + } - let window = Arc::new( - builder - .build(&event_loop) - .map_err(Error::WindowCreationFailed)?, - ); + struct BootConfig { + sender: oneshot::Sender>, + id: Option, + 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 winit::application::ApplicationHandler + for Runner + where + F: Future, + 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(); - 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())); + 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>(()) + } + }; + + #[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; + }); } - }; - } + } + + 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 compositor = C::new(graphics_settings, window.clone()).await?; - let renderer = compositor.create_renderer(); + fn user_event( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + message: Message, + ) { + self.process_event( + event_loop, + winit::event::Event::UserEvent(message), + ); + } - for font in settings.fonts { - compositor.load_font(font); + fn about_to_wait( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + ) { + self.process_event(event_loop, winit::event::Event::AboutToWait); + } } - let (mut event_sender, event_receiver) = mpsc::unbounded(); - let (control_sender, mut control_receiver) = mpsc::unbounded(); - - let mut instance = Box::pin(run_instance::( - application, - compositor, - renderer, - runtime, - proxy, - debug, - event_receiver, - control_sender, - init_command, - window, - should_be_visible, - exit_on_close_request, - )); + impl Runner + where + F: Future, + { + fn process_event( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + event: winit::event::Event, + ) { + // 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); - let mut context = task::Context::from_waker(task::noop_waker_ref()); + // 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 +435,45 @@ where event_loop.exit(); } } - }; + } + } - #[cfg(not(target_os = "windows"))] - let _ = event_loop.run(process_event); + #[cfg(not(target_arch = "wasm32"))] + { + let mut runner = runner; + let _ = event_loop.run_app(&mut runner); + + runner.error.map(Err).unwrap_or(Ok(())) + } - // TODO: Remove when unnecessary - // On Windows, we emulate an `AboutToWait` event after every `Resized` event - // since the event loop does not resume during resize interaction. - // More details: https://github.com/rust-windowing/winit/issues/3272 - #[cfg(target_os = "windows")] + #[cfg(target_arch = "wasm32")] { - let mut process_event = process_event; + use winit::platform::web::EventLoopExtWebSys; + let _ = event_loop.spawn_app(runner); - let _ = event_loop.run(move |event, event_loop| { - if matches!( - event, - winit::event::Event::WindowEvent { - event: winit::event::WindowEvent::Resized(_) - | winit::event::WindowEvent::Moved(_), - .. - } - ) { - process_event(event, event_loop); - process_event(winit::event::Event::AboutToWait, event_loop); - } else { - process_event(event, event_loop); - } - }); + Ok(()) } +} - Ok(()) +struct Boot { + window: Arc, + compositor: C, + should_be_visible: bool, + exit_on_close_request: bool, } async fn run_instance( mut application: A, - mut compositor: C, - mut renderer: A::Renderer, mut runtime: Runtime, A::Message>, mut proxy: Proxy, mut debug: Debug, + mut boot: oneshot::Receiver>, mut event_receiver: mpsc::UnboundedReceiver< winit::event::Event, >, mut control_sender: mpsc::UnboundedSender, init_command: Command, - window: Arc, - should_be_visible: bool, - exit_on_close_request: bool, + fonts: Vec>, ) where A: Application + 'static, E: Executor + 'static, @@ -326,6 +484,19 @@ async fn run_instance( 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 +651,7 @@ async fn run_instance( 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, )); diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index 0f83dac3cb..d04fc2f1e0 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, _id: Option, -) -> 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. diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 2c148031c1..145b15694f 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 + '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,277 @@ 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::( + 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()); - #[cfg(target_arch = "wasm32")] - { - use winit::platform::web::WindowExtWebSys; + struct Runner { + instance: std::pin::Pin>, + context: task::Context<'static>, + boot: Option>, + sender: mpsc::UnboundedSender>, + receiver: mpsc::UnboundedReceiver, + error: Option, + } - let canvas = main_window.canvas(); + struct BootConfig { + sender: oneshot::Sender>, + id: Option, + title: String, + window_settings: window::Settings, + graphics_settings: graphics::Settings, + } - let window = web_sys::window().unwrap(); - let document = window.document().unwrap(); - let body = document.body().unwrap(); + 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, + }; - let target = target.and_then(|target| { - body.query_selector(&format!("#{}", target)) - .ok() - .unwrap_or(None) - }); + impl winit::application::ApplicationHandler + for Runner + where + F: Future, + C: Compositor, + { + 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; + }; - 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 should_be_visible = window_settings.visible; + let exit_on_close_request = window_settings.exit_on_close_request; - let mut compositor = - executor::block_on(C::new(graphics_settings, main_window.clone()))?; + let window_attributes = conversion::window_attributes( + window_settings, + &title, + event_loop.primary_monitor(), + id, + ) + .with_visible(false); - let mut window_manager = WindowManager::new(); - let _ = window_manager.insert( - window::Id::MAIN, - main_window, - &application, - &mut compositor, - exit_on_close_request, - ); + log::debug!("Window attributes: {window_attributes:#?}"); - let (mut event_sender, event_receiver) = mpsc::unbounded(); - let (control_sender, mut control_receiver) = mpsc::unbounded(); + 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 instance = Box::pin(run_instance::( - application, - compositor, - runtime, - proxy, - debug, - event_receiver, - control_sender, - init_command, - window_manager, - should_main_be_visible, - )); + 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 context = task::Context::from_waker(task::noop_waker_ref()); + if let Err(error) = executor::block_on(finish_boot) { + self.error = Some(Error::GraphicsCreationFailed(error)); + event_loop.exit(); + } + } - let process_event = move |event, event_loop: &winit::event_loop::EventLoopWindowTarget<_>| { - if event_loop.exiting() { - return; + 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, + ), + ); + } + } } - event_sender - .start_send(Event::EventLoopAwakened(event)) - .expect("Send event"); - - loop { - let poll = instance.as_mut().poll(&mut context); - - match poll { - task::Poll::Pending => match control_receiver.try_next() { - Ok(Some(control)) => match control { - Control::ChangeFlow(flow) => { - use winit::event_loop::ControlFlow; - - match (event_loop.control_flow(), flow) { - ( - ControlFlow::WaitUntil(current), - ControlFlow::WaitUntil(new), - ) if new < current => {} - ( - ControlFlow::WaitUntil(target), - ControlFlow::Wait, - ) if target > Instant::now() => {} - _ => { - event_loop.set_control_flow(flow); + fn user_event( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + message: Message, + ) { + self.process_event( + event_loop, + Event::EventLoopAwakened(winit::event::Event::UserEvent( + message, + )), + ); + } + + fn about_to_wait( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + ) { + self.process_event( + event_loop, + Event::EventLoopAwakened(winit::event::Event::AboutToWait), + ); + } + } + + impl Runner + where + F: Future, + C: Compositor, + { + fn process_event( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + event: Event, + ) { + 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 { + window: Arc, + compositor: C, + should_be_visible: bool, + exit_on_close_request: bool, +} + enum Event { WindowCreated { id: window::Id, @@ -341,15 +432,13 @@ enum Control { async fn run_instance( mut application: A, - mut compositor: C, mut runtime: Runtime, A::Message>, mut proxy: Proxy, mut debug: Debug, + mut boot: oneshot::Receiver>, mut event_receiver: mpsc::UnboundedReceiver>, mut control_sender: mpsc::UnboundedSender, init_command: Command, - mut window_manager: WindowManager, - should_main_window_be_visible: bool, ) where A: Application + 'static, E: Executor + 'static, @@ -359,11 +448,28 @@ async fn run_instance( use winit::event; use winit::event_loop::ControlFlow; + let Boot { + window: main_window, + mut compositor, + should_be_visible, + exit_on_close_request, + } = boot.try_recv().ok().flatten().expect("Receive boot"); + + let mut window_manager = WindowManager::new(); + + let _ = window_manager.insert( + window::Id::MAIN, + main_window.clone(), + &application, + &mut compositor, + exit_on_close_request, + ); + let main_window = window_manager .get_mut(window::Id::MAIN) .expect("Get main window"); - if should_main_window_be_visible { + if should_be_visible { main_window.raw.set_visible(true); } @@ -532,7 +638,7 @@ async fn run_instance( 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 +709,7 @@ async fn run_instance( if new_mouse_interaction != window.mouse_interaction { - window.raw.set_cursor_icon( + window.raw.set_cursor( conversion::mouse_interaction( new_mouse_interaction, ), diff --git a/winit/src/multi_window/window_manager.rs b/winit/src/multi_window/window_manager.rs index 3a0c8556a9..57a7dc7e38 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 +pub struct WindowManager where + A: Application, C: Compositor, A::Theme: DefaultStyle, { From 7e7285d60f08e9d32a796e9adf7e37535a67fc43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 7 May 2024 17:00:55 +0200 Subject: [PATCH 63/66] Plug `new_events` handler to event loop --- winit/src/application.rs | 15 +++++++++++++++ winit/src/multi_window.rs | 15 +++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/winit/src/application.rs b/winit/src/application.rs index e056f4c554..3bc292552c 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -341,6 +341,21 @@ where } } + fn new_events( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + cause: winit::event::StartCause, + ) { + if self.boot.is_some() { + return; + } + + self.process_event( + event_loop, + winit::event::Event::NewEvents(cause), + ); + } + fn window_event( &mut self, event_loop: &winit::event_loop::ActiveEventLoop, diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 145b15694f..673a6f3067 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -254,6 +254,21 @@ where } } + fn new_events( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + cause: winit::event::StartCause, + ) { + if self.boot.is_some() { + return; + } + + self.process_event( + event_loop, + Event::EventLoopAwakened(winit::event::Event::NewEvents(cause)), + ); + } + fn window_event( &mut self, event_loop: &winit::event_loop::ActiveEventLoop, From 8a0701a0d95989769341846b05ffcc705ae4ee5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 7 May 2024 21:05:29 +0200 Subject: [PATCH 64/66] Introduce `ICED_PRESENT_MODE` env var to pick a `wgpu::PresentMode` --- wgpu/src/settings.rs | 14 ++++++++++++++ wgpu/src/window/compositor.rs | 25 +++++++++++++++---------- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/wgpu/src/settings.rs b/wgpu/src/settings.rs index a6aea0a55b..5e2603eee1 100644 --- a/wgpu/src/settings.rs +++ b/wgpu/src/settings.rs @@ -51,3 +51,17 @@ impl From for Settings { } } } + +pub fn present_mode_from_env() -> Option { + 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/window/compositor.rs b/wgpu/src/window/compositor.rs index baf9f3151f..2e938c7719 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 { 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 { From 5b6f3499e114c1694a5878466f8d46e7022e1bba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 7 May 2024 21:13:51 +0200 Subject: [PATCH 65/66] Document `present_mode_from_env` in `iced_wgpu` --- wgpu/src/settings.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/wgpu/src/settings.rs b/wgpu/src/settings.rs index 5e2603eee1..b3c3cf6ad6 100644 --- a/wgpu/src/settings.rs +++ b/wgpu/src/settings.rs @@ -52,6 +52,18 @@ impl From 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 { let present_mode = std::env::var("ICED_PRESENT_MODE").ok()?; From 247870f83bc12f3f4b549bb582f1058f5adcce25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 7 May 2024 21:38:31 +0200 Subject: [PATCH 66/66] Redirect `docs.iced.rs` to actual docs --- .github/workflows/document.yml | 2 ++ docs/redirect.html | 13 +++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 docs/redirect.html diff --git a/.github/workflows/document.yml b/.github/workflows/document.yml index ba482215a5..827a2ca874 100644 --- a/.github/workflows/document.yml +++ b/.github/workflows/document.yml @@ -27,6 +27,8 @@ jobs: -p iced - name: Write CNAME file run: echo 'docs.iced.rs' > ./target/doc/CNAME + - name: Copy redirect file as index.html + run: cp docs/redirect.html target/doc/index.html - name: Publish documentation if: github.ref == 'refs/heads/master' uses: peaceiris/actions-gh-pages@v3 diff --git a/docs/redirect.html b/docs/redirect.html new file mode 100644 index 0000000000..7b2cef51a4 --- /dev/null +++ b/docs/redirect.html @@ -0,0 +1,13 @@ + + + + + + + Redirecting... + + + +

If you are not redirected automatically, follow this link.

+ +