From d2e192042ffb5f166df7ded99022e99680a49760 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 15 Oct 2025 19:08:39 +0200 Subject: [PATCH 1/3] Handle changes during `RedrawRequested` in the same frame --- runtime/src/user_interface.rs | 19 ++++++ test/src/instruction.rs | 3 - widget/src/tooltip.rs | 5 +- winit/src/lib.rs | 115 ++++++++++++++++++++++------------ 4 files changed, 98 insertions(+), 44 deletions(-) diff --git a/runtime/src/user_interface.rs b/runtime/src/user_interface.rs index b63b4747..a40ab221 100644 --- a/runtime/src/user_interface.rs +++ b/runtime/src/user_interface.rs @@ -192,6 +192,7 @@ where let mut outdated = false; let mut redraw_request = window::RedrawRequest::Wait; let mut input_method = InputMethod::Disabled; + let mut has_layout_changed = false; let viewport = Rectangle::with_size(self.bounds); let mut maybe_overlay = self @@ -259,6 +260,7 @@ where shell.revalidate_layout(|| { layout = overlay.layout(renderer, bounds); + has_layout_changed = true; }); } @@ -334,6 +336,8 @@ where input_method.merge(shell.input_method()); shell.revalidate_layout(|| { + has_layout_changed = true; + self.base = self.root.as_widget_mut().layout( &mut self.state, renderer, @@ -395,6 +399,7 @@ where mouse_interaction, redraw_request, input_method, + has_layout_changed, } }, event_statuses, @@ -624,5 +629,19 @@ pub enum State { redraw_request: window::RedrawRequest, /// The current [`InputMethod`] strategy of the user interface. input_method: InputMethod, + /// Whether the layout of the [`UserInterface`] has changed. + has_layout_changed: bool, }, } + +impl State { + /// Returns whether the layout of the [`UserInterface`] has changed. + pub fn has_layout_changed(&self) -> bool { + match self { + State::Outdated => true, + State::Updated { + has_layout_changed, .. + } => *has_layout_changed, + } + } +} diff --git a/test/src/instruction.rs b/test/src/instruction.rs index b87086c5..926619c9 100644 --- a/test/src/instruction.rs +++ b/test/src/instruction.rs @@ -555,7 +555,6 @@ mod parser { fn mouse_click(input: &str) -> IResult<&str, Mouse> { let (input, _) = tag("click ")(input)?; - let (input, (button, target)) = mouse_button_at(input)?; Ok((input, Mouse::Click { button, target })) @@ -563,7 +562,6 @@ mod parser { fn mouse_press(input: &str) -> IResult<&str, Mouse> { let (input, _) = tag("press ")(input)?; - let (input, (button, target)) = mouse_button_at(input)?; Ok((input, Mouse::Press { button, target })) @@ -571,7 +569,6 @@ mod parser { fn mouse_release(input: &str) -> IResult<&str, Mouse> { let (input, _) = tag("release ")(input)?; - let (input, (button, target)) = mouse_button_at(input)?; Ok((input, Mouse::Release { button, target })) diff --git a/widget/src/tooltip.rs b/widget/src/tooltip.rs index 433844cb..c77cbc78 100644 --- a/widget/src/tooltip.rs +++ b/widget/src/tooltip.rs @@ -28,6 +28,7 @@ use crate::core::overlay; use crate::core::renderer; use crate::core::text; use crate::core::widget::{self, Widget}; +use crate::core::window; use crate::core::{ Clipboard, Element, Event, Length, Padding, Pixels, Point, Rectangle, Shell, Size, Vector, @@ -201,7 +202,9 @@ where shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) { - if let Event::Mouse(_) = event { + if let Event::Mouse(_) + | Event::Window(window::Event::RedrawRequested(_)) = event + { let state = tree.state.downcast_mut::(); let previous_state = *state; let was_idle = *state == State::Idle; diff --git a/winit/src/lib.rs b/winit/src/lib.rs index c486db23..802b446b 100644 --- a/winit/src/lib.rs +++ b/winit/src/lib.rs @@ -774,7 +774,7 @@ async fn run_instance

( continue; }; - let Some((id, window)) = + let Some((id, mut window)) = window_manager.get_mut_alias(id) else { continue; @@ -819,20 +819,61 @@ async fn run_instance

( let cursor = window.state.cursor(); - let ui = user_interfaces + let mut interface = user_interfaces .get_mut(&id) .expect("Get user interface"); let draw_span = debug::draw(id); - let (ui_state, _) = ui.update( - slice::from_ref(&redraw_event), - cursor, - &mut window.renderer, - &mut clipboard, - &mut messages, - ); + let mut change_count = 0; - ui.draw( + let state = loop { + let (state, _) = interface.update( + slice::from_ref(&redraw_event), + cursor, + &mut window.renderer, + &mut clipboard, + &mut messages, + ); + + change_count += 1; + + if messages.is_empty() + && !state.has_layout_changed() + { + break state; + } + + if change_count >= 10 { + log::warn!( + "More than 10 consecutive RedrawRequested events \ + produced layout invalidation" + ); + + break state; + } + + let caches: FxHashMap<_, _> = + ManuallyDrop::into_inner(user_interfaces) + .into_iter() + .map(|(id, interface)| { + (id, interface.into_cache()) + }) + .collect(); + + update(&mut program, &mut runtime, &mut messages); + + user_interfaces = + ManuallyDrop::new(build_user_interfaces( + &program, + &mut window_manager, + caches, + )); + + window = window_manager.get_mut(id).unwrap(); + interface = user_interfaces.get_mut(&id).unwrap(); + }; + + interface.draw( &mut window.renderer, window.state.theme(), &renderer::Style { @@ -842,23 +883,24 @@ async fn run_instance

( ); draw_span.finish(); - runtime.broadcast(subscription::Event::Interaction { - window: id, - event: redraw_event, - status: core::event::Status::Ignored, - }); - if let user_interface::State::Updated { redraw_request, input_method, mouse_interaction, - } = ui_state + .. + } = state { window.request_redraw(redraw_request); window.request_input_method(input_method); window.update_mouse(mouse_interaction); } + runtime.broadcast(subscription::Event::Interaction { + window: id, + event: redraw_event, + status: core::event::Status::Ignored, + }); + window.draw_preedit(); let present_span = debug::present(id); @@ -1071,32 +1113,14 @@ async fn run_instance

( } if !messages.is_empty() || uis_stale { - let cached_interfaces: FxHashMap< - window::Id, - user_interface::Cache, - > = ManuallyDrop::into_inner(user_interfaces) - .drain() - .map(|(id, ui)| (id, ui.into_cache())) - .collect(); + let cached_interfaces: FxHashMap<_, _> = + ManuallyDrop::into_inner(user_interfaces) + .into_iter() + .map(|(id, ui)| (id, ui.into_cache())) + .collect(); update(&mut program, &mut runtime, &mut messages); - for (id, window) in window_manager.iter_mut() { - window.state.synchronize( - &program, - id, - &window.raw, - ); - - window.raw.request_redraw(); - } - - debug::theme_changed(|| { - window_manager.first().and_then(|window| { - theme::Base::palette(window.state.theme()) - }) - }); - user_interfaces = ManuallyDrop::new(build_user_interfaces( &program, @@ -1606,6 +1630,17 @@ where C: Compositor, P::Theme: theme::Base, { + for (id, window) in window_manager.iter_mut() { + window.state.synchronize(program, id, &window.raw); + window.raw.request_redraw(); + } + + debug::theme_changed(|| { + window_manager + .first() + .and_then(|window| theme::Base::palette(window.state.theme())) + }); + cached_user_interfaces .drain() .filter_map(|(id, cache)| { From 859d6bb97220db41e7fd8e85fbfbd9ddbdb8e1a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 15 Oct 2025 20:04:44 +0200 Subject: [PATCH 2/3] Run a `Task` synchronously if immediately available --- winit/src/lib.rs | 93 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 84 insertions(+), 9 deletions(-) diff --git a/winit/src/lib.rs b/winit/src/lib.rs index 802b446b..b239260b 100644 --- a/winit/src/lib.rs +++ b/winit/src/lib.rs @@ -554,7 +554,7 @@ async fn run_instance

( log::info!("System theme: {system_theme:?}"); - loop { + 'next_event: loop { // Empty the queue if possible let event = if let Ok(event) = event_receiver.try_next() { event @@ -770,7 +770,8 @@ async fn run_instance

( event: event::WindowEvent::RedrawRequested, .. } => { - let Some(compositor) = &mut compositor else { + let Some(mut current_compositor) = compositor.as_mut() + else { continue; }; @@ -803,7 +804,7 @@ async fn run_instance

( ); layout_span.finish(); - compositor.configure_surface( + current_compositor.configure_surface( &mut window.surface, physical_size.width, physical_size.height, @@ -860,7 +861,11 @@ async fn run_instance

( }) .collect(); - update(&mut program, &mut runtime, &mut messages); + let actions = update( + &mut program, + &mut runtime, + &mut messages, + ); user_interfaces = ManuallyDrop::new(build_user_interfaces( @@ -869,6 +874,30 @@ async fn run_instance

( caches, )); + for action in actions { + run_action( + action, + &program, + &mut runtime, + &mut compositor, + &mut events, + &mut messages, + &mut clipboard, + &mut control_sender, + &mut user_interfaces, + &mut window_manager, + &mut ui_caches, + &mut is_window_opening, + &mut system_theme, + ); + } + + let Some(next_compositor) = compositor.as_mut() + else { + continue 'next_event; + }; + + current_compositor = next_compositor; window = window_manager.get_mut(id).unwrap(); interface = user_interfaces.get_mut(&id).unwrap(); }; @@ -904,7 +933,7 @@ async fn run_instance

( window.draw_preedit(); let present_span = debug::present(id); - match compositor.present( + match current_compositor.present( &mut window.renderer, &mut window.surface, window.state.viewport(), @@ -1119,7 +1148,11 @@ async fn run_instance

( .map(|(id, ui)| (id, ui.into_cache())) .collect(); - update(&mut program, &mut runtime, &mut messages); + let actions = update( + &mut program, + &mut runtime, + &mut messages, + ); user_interfaces = ManuallyDrop::new(build_user_interfaces( @@ -1127,6 +1160,24 @@ async fn run_instance

( &mut window_manager, cached_interfaces, )); + + for action in actions { + run_action( + action, + &program, + &mut runtime, + &mut compositor, + &mut events, + &mut messages, + &mut clipboard, + &mut control_sender, + &mut user_interfaces, + &mut window_manager, + &mut ui_caches, + &mut is_window_opening, + &mut system_theme, + ); + } } if let Some(redraw_at) = window_manager.redraw_at() { @@ -1176,14 +1227,36 @@ fn update( program: &mut program::Instance

, runtime: &mut Runtime, Action>, messages: &mut Vec, -) where +) -> Vec> +where P::Theme: theme::Base, { + use futures::futures; + + let mut actions = Vec::new(); + for message in messages.drain(..) { let task = runtime.enter(|| program.update(message)); - if let Some(stream) = runtime::task::into_stream(task) { - runtime.run(stream); + if let Some(mut stream) = runtime::task::into_stream(task) { + let waker = futures::task::noop_waker_ref(); + let mut context = futures::task::Context::from_waker(waker); + + // Run immediately available actions synchronously (e.g. widget operations) + loop { + match runtime.enter(|| stream.poll_next_unpin(&mut context)) { + futures::task::Poll::Ready(Some(action)) => { + actions.push(action); + } + futures::task::Poll::Ready(None) => { + break; + } + futures::task::Poll::Pending => { + runtime.run(stream); + break; + } + } + } } } @@ -1191,6 +1264,8 @@ fn update( let recipes = subscription::into_recipes(subscription.map(Action::Output)); runtime.track(recipes); + + actions } fn run_action<'a, P, C>( From 7086fa2168581cfd2bf12cbd61467490bccd6812 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 15 Oct 2025 21:59:13 +0200 Subject: [PATCH 3/3] Skip `update` on `RedrawRequested` if not needed --- winit/src/lib.rs | 87 +++++++++++++++++++++++++----------------------- 1 file changed, 45 insertions(+), 42 deletions(-) diff --git a/winit/src/lib.rs b/winit/src/lib.rs index b239260b..fc84f931 100644 --- a/winit/src/lib.rs +++ b/winit/src/lib.rs @@ -853,53 +853,56 @@ async fn run_instance

( break state; } - let caches: FxHashMap<_, _> = - ManuallyDrop::into_inner(user_interfaces) - .into_iter() - .map(|(id, interface)| { - (id, interface.into_cache()) - }) - .collect(); + if !messages.is_empty() { + let caches: FxHashMap<_, _> = + ManuallyDrop::into_inner(user_interfaces) + .into_iter() + .map(|(id, interface)| { + (id, interface.into_cache()) + }) + .collect(); - let actions = update( - &mut program, - &mut runtime, - &mut messages, - ); - - user_interfaces = - ManuallyDrop::new(build_user_interfaces( - &program, - &mut window_manager, - caches, - )); - - for action in actions { - run_action( - action, - &program, + let actions = update( + &mut program, &mut runtime, - &mut compositor, - &mut events, &mut messages, - &mut clipboard, - &mut control_sender, - &mut user_interfaces, - &mut window_manager, - &mut ui_caches, - &mut is_window_opening, - &mut system_theme, ); + + user_interfaces = + ManuallyDrop::new(build_user_interfaces( + &program, + &mut window_manager, + caches, + )); + + for action in actions { + run_action( + action, + &program, + &mut runtime, + &mut compositor, + &mut events, + &mut messages, + &mut clipboard, + &mut control_sender, + &mut user_interfaces, + &mut window_manager, + &mut ui_caches, + &mut is_window_opening, + &mut system_theme, + ); + } + + let Some(next_compositor) = compositor.as_mut() + else { + continue 'next_event; + }; + + current_compositor = next_compositor; + window = window_manager.get_mut(id).unwrap(); + interface = + user_interfaces.get_mut(&id).unwrap(); } - - let Some(next_compositor) = compositor.as_mut() - else { - continue 'next_event; - }; - - current_compositor = next_compositor; - window = window_manager.get_mut(id).unwrap(); - interface = user_interfaces.get_mut(&id).unwrap(); }; interface.draw(