diff --git a/Cargo.toml b/Cargo.toml index 109aaff9b..4fb4d1ebe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -146,3 +146,7 @@ members = [ "crates/kas-view", "examples/mandlebrot", ] + +[patch.crates-io.winit] +git = "https://github.com/rust-windowing/winit.git" +rev = "cb58c49a90f17734e0405627130674d47c0b8f40" diff --git a/crates/kas-core/src/action.rs b/crates/kas-core/src/action.rs index c272e94da..c810b624c 100644 --- a/crates/kas-core/src/action.rs +++ b/crates/kas-core/src/action.rs @@ -35,10 +35,6 @@ bitflags! { /// /// Implies window redraw. const REGION_MOVED = 1 << 4; - /* - /// A pop-up opened/closed/needs resizing - Popup, - */ /// Reset size of all widgets without recalculating requirements const SET_RECT = 1 << 8; /// Resize all widgets in the window diff --git a/crates/kas-core/src/core/widget.rs b/crates/kas-core/src/core/widget.rs index 283b1d2fd..112221540 100644 --- a/crates/kas-core/src/core/widget.rs +++ b/crates/kas-core/src/core/widget.rs @@ -149,7 +149,7 @@ pub trait Events: Widget + Sized { /// /// Note: to implement `hover_highlight`, simply request a redraw on /// focus gain and loss. To implement `cursor_icon`, call - /// `cx.set_cursor_icon(EXPR);` on focus gain. + /// `cx.set_hover_cursor(EXPR);` on focus gain. /// /// [`#widget`]: macros::widget #[inline] diff --git a/crates/kas-core/src/draw/draw_shared.rs b/crates/kas-core/src/draw/draw_shared.rs index 1295187cc..469c57488 100644 --- a/crates/kas-core/src/draw/draw_shared.rs +++ b/crates/kas-core/src/draw/draw_shared.rs @@ -9,7 +9,6 @@ use super::color::Rgba; use super::{DrawImpl, PassId}; use crate::cast::Cast; use crate::geom::{Quad, Rect, Size}; -use crate::shell::Platform; use crate::text::{Effect, TextDisplay}; use crate::theme::RasterConfig; use std::any::Any; @@ -76,15 +75,14 @@ pub struct AllocError; pub struct SharedState { /// The shell's [`DrawSharedImpl`] object pub draw: DS, - platform: Platform, } #[cfg_attr(not(feature = "internal_doc"), doc(hidden))] #[cfg_attr(doc_cfg, doc(cfg(internal_doc)))] impl SharedState { /// Construct (this is only called by the shell) - pub fn new(draw: DS, platform: Platform) -> Self { - SharedState { draw, platform } + pub fn new(draw: DS) -> Self { + SharedState { draw } } } @@ -92,9 +90,6 @@ impl SharedState { /// /// All methods concern management of resources for drawing. pub trait DrawShared { - /// Get the platform - fn platform(&self) -> Platform; - /// Allocate an image /// /// Use [`SharedState::image_upload`] to set contents of the new image. @@ -121,11 +116,6 @@ pub trait DrawShared { } impl DrawShared for SharedState { - #[inline] - fn platform(&self) -> Platform { - self.platform - } - #[inline] fn image_alloc(&mut self, size: (u32, u32)) -> Result { self.draw diff --git a/crates/kas-core/src/event/cx/config.rs b/crates/kas-core/src/event/cx/config.rs index 0130834ea..20c0cff74 100644 --- a/crates/kas-core/src/event/cx/config.rs +++ b/crates/kas-core/src/event/cx/config.rs @@ -6,11 +6,9 @@ //! Configuration context use super::Pending; -use crate::draw::DrawShared; use crate::event::EventState; use crate::geom::{Rect, Size}; use crate::layout::AlignPair; -use crate::shell::Platform; use crate::text::TextApi; use crate::theme::{Feature, SizeCx, TextClass, ThemeSize}; use crate::{Action, Node, WidgetId}; @@ -26,7 +24,6 @@ use std::ops::{Deref, DerefMut}; #[must_use] pub struct ConfigCx<'a> { sh: &'a dyn ThemeSize, - ds: &'a mut dyn DrawShared, pub(crate) ev: &'a mut EventState, } @@ -34,13 +31,8 @@ impl<'a> ConfigCx<'a> { /// Construct #[cfg_attr(not(feature = "internal_doc"), doc(hidden))] #[cfg_attr(doc_cfg, doc(cfg(internal_doc)))] - pub fn new(sh: &'a dyn ThemeSize, ds: &'a mut dyn DrawShared, ev: &'a mut EventState) -> Self { - ConfigCx { sh, ds, ev } - } - - /// Get the platform - pub fn platform(&self) -> Platform { - self.ds.platform() + pub fn new(sh: &'a dyn ThemeSize, ev: &'a mut EventState) -> Self { + ConfigCx { sh, ev } } /// Access a [`SizeCx`] @@ -49,12 +41,6 @@ impl<'a> ConfigCx<'a> { SizeCx::new(self.sh) } - /// Access [`DrawShared`] - #[inline] - pub fn draw_shared(&mut self) -> &mut dyn DrawShared { - self.ds - } - /// Access [`EventState`] #[inline] pub fn ev_state(&mut self) -> &mut EventState { diff --git a/crates/kas-core/src/event/cx/cx_pub.rs b/crates/kas-core/src/event/cx/cx_pub.rs index 599938dac..9588cf683 100644 --- a/crates/kas-core/src/event/cx/cx_pub.rs +++ b/crates/kas-core/src/event/cx/cx_pub.rs @@ -15,6 +15,8 @@ use crate::draw::DrawShared; use crate::event::config::ChangeConfig; use crate::geom::{Offset, Vec2}; use crate::theme::{SizeCx, ThemeControl}; +#[cfg(all(wayland_platform, feature = "clipboard"))] +use crate::util::warn_about_error; use crate::{Action, Erased, WidgetId, Window, WindowId}; #[allow(unused)] use crate::{Events, Layout}; // for doc-links @@ -34,6 +36,11 @@ impl std::ops::BitOrAssign for EventState { /// Public API impl EventState { + /// Get the platform + pub fn platform(&self) -> Platform { + self.platform + } + /// True when the window has focus #[inline] pub fn window_has_focus(&self) -> bool { @@ -526,8 +533,9 @@ impl EventState { /// cases, calling this method may be ineffective. The cursor is /// automatically "unset" when the widget is no longer hovered. /// - /// If a mouse grab ([`Press::grab`]) is active, its icon takes precedence. - pub fn set_cursor_icon(&mut self, icon: CursorIcon) { + /// See also [`Self::update_grab_cursor`]: if a mouse grab + /// ([`Press::grab`]) is active, its icon takes precedence. + pub fn set_hover_cursor(&mut self, icon: CursorIcon) { // Note: this is acted on by EventState::update self.hover_icon = icon; } @@ -601,11 +609,6 @@ impl EventState { pub fn request_update(&mut self, id: WidgetId) { self.pending.push_back(Pending::Update(id)); } - - /// Request set_rect of the given path - pub fn request_set_rect(&mut self, id: WidgetId) { - self.pending.push_back(Pending::SetRect(id)); - } } /// Public API @@ -617,7 +620,7 @@ impl<'a> EventCx<'a> { /// This is a shortcut to [`ConfigCx::configure`]. #[inline] pub fn configure(&mut self, mut widget: Node<'_>, id: WidgetId) { - self.config_cx(|cx| widget._configure(cx, id)); + widget._configure(&mut self.config_cx(), id); } /// Update a widget @@ -627,7 +630,7 @@ impl<'a> EventCx<'a> { /// data, it should call this (or [`ConfigCx::update`]) after mutating the state. #[inline] pub fn update(&mut self, mut widget: Node<'_>) { - self.config_cx(|cx| widget._update(cx)); + widget._update(&mut self.config_cx()); } /// Get the index of the last child visited @@ -735,7 +738,8 @@ impl<'a> EventCx<'a> { pub(crate) fn add_popup(&mut self, popup: crate::PopupDescriptor) -> WindowId { log::trace!(target: "kas_core::event", "add_popup: {popup:?}"); - let id = self.shell.add_popup(popup.clone()); + let parent_id = self.window.window_id(); + let id = self.shell.add_popup(parent_id, popup.clone()); let nav_focus = self.nav_focus.clone(); self.popups.push((id, popup, nav_focus)); self.clear_nav_focus(); @@ -791,7 +795,7 @@ impl<'a> EventCx<'a> { /// This calls [`winit::window::Window::drag_window`](https://docs.rs/winit/latest/winit/window/struct.Window.html#method.drag_window). Errors are ignored. pub fn drag_window(&self) { #[cfg(winit)] - if let Some(ww) = self.shell.winit_window() { + if let Some(ww) = self.window.winit_window() { if let Err(e) = ww.drag_window() { log::warn!("EventCx::drag_window: {e}"); } @@ -803,7 +807,7 @@ impl<'a> EventCx<'a> { /// This calls [`winit::window::Window::drag_resize_window`](https://docs.rs/winit/latest/winit/window/struct.Window.html#method.drag_resize_window). Errors are ignored. pub fn drag_resize_window(&self, direction: ResizeDirection) { #[cfg(winit)] - if let Some(ww) = self.shell.winit_window() { + if let Some(ww) = self.window.winit_window() { if let Err(e) = ww.drag_resize_window(direction) { log::warn!("EventCx::drag_resize_window: {e}"); } @@ -816,12 +820,29 @@ impl<'a> EventCx<'a> { /// may wish to log an appropriate warning message. #[inline] pub fn get_clipboard(&mut self) -> Option { + #[cfg(all(wayland_platform, feature = "clipboard"))] + if let Some(cb) = self.window.wayland_clipboard() { + return match cb.load() { + Ok(s) => Some(s), + Err(e) => { + warn_about_error("Failed to get clipboard contents", &e); + None + } + }; + } + self.shell.get_clipboard() } /// Attempt to set clipboard contents #[inline] pub fn set_clipboard(&mut self, content: String) { + #[cfg(all(wayland_platform, feature = "clipboard"))] + if let Some(cb) = self.window.wayland_clipboard() { + cb.store(content); + return; + } + self.shell.set_clipboard(content) } @@ -831,6 +852,17 @@ impl<'a> EventCx<'a> { /// paste on middle-click. This method does nothing on other platforms. #[inline] pub fn get_primary(&mut self) -> Option { + #[cfg(all(wayland_platform, feature = "clipboard"))] + if let Some(cb) = self.window.wayland_clipboard() { + return match cb.load_primary() { + Ok(s) => Some(s), + Err(e) => { + warn_about_error("Failed to get clipboard contents", &e); + None + } + }; + } + self.shell.get_primary() } @@ -840,6 +872,12 @@ impl<'a> EventCx<'a> { /// paste on middle-click. This method does nothing on other platforms. #[inline] pub fn set_primary(&mut self, content: String) { + #[cfg(all(wayland_platform, feature = "clipboard"))] + if let Some(cb) = self.window.wayland_clipboard() { + cb.store_primary(content); + return; + } + self.shell.set_primary(content) } @@ -849,38 +887,25 @@ impl<'a> EventCx<'a> { self.shell.adjust_theme(Box::new(f)); } - /// Access a [`SizeCx`] + /// Get a [`SizeCx`] /// /// Warning: sizes are calculated using the window's current scale factor. /// This may change, even without user action, since some platforms /// always initialize windows with scale factor 1. /// See also notes on [`Events::configure`]. - pub fn size_cx T, T>(&mut self, f: F) -> T { - let mut result = None; - self.shell.size_and_draw_shared(Box::new(|size, _| { - result = Some(f(SizeCx::new(size))); - })); - result.expect("ShellWindow::size_and_draw_shared impl failed to call function argument") - } - - /// Access a [`ConfigCx`] - pub fn config_cx T, T>(&mut self, f: F) -> T { - let mut result = None; - self.shell - .size_and_draw_shared(Box::new(|size, draw_shared| { - let mut cx = ConfigCx::new(size, draw_shared, self.state); - result = Some(f(&mut cx)); - })); - result.expect("ShellWindow::size_and_draw_shared impl failed to call function argument") - } - - /// Access a [`DrawShared`] - pub fn draw_shared T, T>(&mut self, f: F) -> T { - let mut result = None; - self.shell.size_and_draw_shared(Box::new(|_, draw_shared| { - result = Some(f(draw_shared)); - })); - result.expect("ShellWindow::size_and_draw_shared impl failed to call function argument") + pub fn size_cx(&self) -> SizeCx<'_> { + SizeCx::new(self.window.theme_size()) + } + + /// Get a [`ConfigCx`] + pub fn config_cx(&mut self) -> ConfigCx<'_> { + let size = self.window.theme_size(); + ConfigCx::new(size, self.state) + } + + /// Get a [`DrawShared`] + pub fn draw_shared(&mut self) -> &mut dyn DrawShared { + self.shell.draw_shared() } /// Directly access Winit Window @@ -888,7 +913,7 @@ impl<'a> EventCx<'a> { /// This is a temporary API, allowing e.g. to minimize the window. #[cfg(winit)] pub fn winit_window(&self) -> Option<&winit::window::Window> { - self.shell.winit_window() + self.window.winit_window() } /// Update the mouse cursor used during a grab @@ -896,10 +921,10 @@ impl<'a> EventCx<'a> { /// This only succeeds if widget `id` has an active mouse-grab (see /// [`Press::grab`]). The cursor will be reset when the mouse-grab /// ends. - pub fn update_grab_cursor(&mut self, id: WidgetId, icon: CursorIcon) { + pub fn set_grab_cursor(&mut self, id: &WidgetId, icon: CursorIcon) { if let Some(ref grab) = self.mouse_grab { - if grab.start_id == id { - self.shell.set_cursor_icon(icon); + if grab.start_id == *id { + self.window.set_cursor_icon(icon); } } } diff --git a/crates/kas-core/src/event/cx/cx_shell.rs b/crates/kas-core/src/event/cx/cx_shell.rs index 335dde4d8..ddd921eae 100644 --- a/crates/kas-core/src/event/cx/cx_shell.rs +++ b/crates/kas-core/src/event/cx/cx_shell.rs @@ -11,9 +11,7 @@ use std::time::{Duration, Instant}; use super::*; use crate::cast::traits::*; -use crate::draw::DrawShared; use crate::geom::{Coord, DVec2}; -use crate::shell::ShellWindow; use crate::theme::ThemeSize; use crate::{Action, NavAdvance, WidgetId, Window}; @@ -28,9 +26,10 @@ const FAKE_MOUSE_BUTTON: MouseButton = MouseButton::Other(0); impl EventState { /// Construct per-window event state #[inline] - pub(crate) fn new(config: WindowConfig) -> Self { + pub(crate) fn new(config: WindowConfig, platform: Platform) -> Self { EventState { config, + platform, disabled: vec![], window_has_focus: false, modifiers: ModifiersState::empty(), @@ -75,7 +74,6 @@ impl EventState { pub(crate) fn full_configure( &mut self, sizer: &dyn ThemeSize, - draw_shared: &mut dyn DrawShared, wid: WindowId, win: &mut Window, data: &A, @@ -91,7 +89,7 @@ impl EventState { self.new_access_layer(id.clone(), false); - ConfigCx::new(sizer, draw_shared, self).configure(win.as_node(data), id); + ConfigCx::new(sizer, self).configure(win.as_node(data), id); let hover = win.find_id(data, self.last_mouse_coord); self.set_hover(hover); @@ -120,13 +118,17 @@ impl EventState { /// /// Invokes the given closure on this [`EventCx`]. #[inline] - pub(crate) fn with(&mut self, shell: &mut dyn ShellWindow, messages: &mut ErasedStack, f: F) - where - F: FnOnce(&mut EventCx), - { + pub(crate) fn with<'a, F: FnOnce(&mut EventCx)>( + &'a mut self, + shell: &'a mut dyn ShellSharedErased, + window: &'a dyn WindowDataErased, + messages: &'a mut ErasedStack, + f: F, + ) { let mut cx = EventCx { state: self, shell, + window, messages, last_child: None, scroll: Scroll::None, @@ -135,122 +137,110 @@ impl EventState { } /// Handle all pending items before event loop sleeps - pub(crate) fn flush_pending( - &mut self, - shell: &mut dyn ShellWindow, - messages: &mut ErasedStack, + pub(crate) fn flush_pending<'a, A>( + &'a mut self, + shell: &'a mut dyn ShellSharedErased, + window: &'a dyn WindowDataErased, + messages: &'a mut ErasedStack, win: &mut Window, data: &A, ) -> Action { let old_hover_icon = self.hover_icon; - let mut cx = EventCx { - state: self, - shell, - messages, - last_child: None, - scroll: Scroll::None, - }; - - while let Some((id, wid)) = cx.popup_removed.pop() { - cx.send_event(win.as_node(data), id, Event::PopupClosed(wid)); - } + self.with(shell, window, messages, |cx| { + while let Some((id, wid)) = cx.popup_removed.pop() { + cx.send_event(win.as_node(data), id, Event::PopupClosed(wid)); + } - cx.flush_mouse_grab_motion(win.as_node(data)); - for i in 0..cx.touch_grab.len() { - let action = cx.touch_grab[i].flush_click_move(); - cx.state.action |= action; - if let Some((id, event)) = cx.touch_grab[i].flush_grab_move() { - cx.send_event(win.as_node(data), id, event); + cx.flush_mouse_grab_motion(); + for i in 0..cx.touch_grab.len() { + let action = cx.touch_grab[i].flush_click_move(); + cx.state.action |= action; } - } - for gi in 0..cx.pan_grab.len() { - let grab = &mut cx.pan_grab[gi]; - debug_assert!(grab.mode != GrabMode::Grab); - assert!(grab.n > 0); - - // Terminology: pi are old coordinates, qi are new coords - let (p1, q1) = (DVec2::conv(grab.coords[0].0), DVec2::conv(grab.coords[0].1)); - grab.coords[0].0 = grab.coords[0].1; - - let alpha; - let delta; - - if grab.mode == GrabMode::PanOnly || grab.n == 1 { - alpha = DVec2(1.0, 0.0); - delta = q1 - p1; - } else { - // We don't use more than two touches: information would be - // redundant (although it could be averaged). - let (p2, q2) = (DVec2::conv(grab.coords[1].0), DVec2::conv(grab.coords[1].1)); - grab.coords[1].0 = grab.coords[1].1; - let (pd, qd) = (p2 - p1, q2 - q1); - - alpha = match grab.mode { - GrabMode::PanFull => qd.complex_div(pd), - GrabMode::PanScale => DVec2((qd.sum_square() / pd.sum_square()).sqrt(), 0.0), - GrabMode::PanRotate => { - let a = qd.complex_div(pd); - a / a.sum_square().sqrt() - } - _ => unreachable!(), - }; + for gi in 0..cx.pan_grab.len() { + let grab = &mut cx.pan_grab[gi]; + debug_assert!(grab.mode != GrabMode::Grab); + assert!(grab.n > 0); - // Average delta from both movements: - delta = (q1 - alpha.complex_mul(p1) + q2 - alpha.complex_mul(p2)) * 0.5; - } + // Terminology: pi are old coordinates, qi are new coords + let (p1, q1) = (DVec2::conv(grab.coords[0].0), DVec2::conv(grab.coords[0].1)); + grab.coords[0].0 = grab.coords[0].1; - let id = grab.id.clone(); - if alpha != DVec2(1.0, 0.0) || delta != DVec2::ZERO { - let event = Event::Pan { alpha, delta }; - cx.send_event(win.as_node(data), id, event); - } - } + let alpha; + let delta; - // Warning: infinite loops are possible here if widgets always queue a - // new pending event when evaluating one of these: - while let Some(item) = cx.pending.pop_front() { - log::trace!(target: "kas_core::event", "update: handling Pending::{item:?}"); - match item { - Pending::Configure(id) => { - win.as_node(data) - .find_node(&id, |node| cx.configure(node, id.clone())); - - let hover = win.find_id(data, cx.state.last_mouse_coord); - cx.state.set_hover(hover); - } - Pending::Update(id) => { - win.as_node(data).find_node(&id, |node| cx.update(node)); + if grab.mode == GrabMode::PanOnly || grab.n == 1 { + alpha = DVec2(1.0, 0.0); + delta = q1 - p1; + } else { + // We don't use more than two touches: information would be + // redundant (although it could be averaged). + let (p2, q2) = (DVec2::conv(grab.coords[1].0), DVec2::conv(grab.coords[1].1)); + grab.coords[1].0 = grab.coords[1].1; + let (pd, qd) = (p2 - p1, q2 - q1); + + alpha = match grab.mode { + GrabMode::PanFull => qd.complex_div(pd), + GrabMode::PanScale => { + DVec2((qd.sum_square() / pd.sum_square()).sqrt(), 0.0) + } + GrabMode::PanRotate => { + let a = qd.complex_div(pd); + a / a.sum_square().sqrt() + } + _ => unreachable!(), + }; + + // Average delta from both movements: + delta = (q1 - alpha.complex_mul(p1) + q2 - alpha.complex_mul(p2)) * 0.5; } - Pending::Send(id, event) => { - if matches!(&event, &Event::MouseHover(false)) { - cx.hover_icon = Default::default(); - } + + let id = grab.id.clone(); + if alpha != DVec2(1.0, 0.0) || delta != DVec2::ZERO { + let event = Event::Pan { alpha, delta }; cx.send_event(win.as_node(data), id, event); } - Pending::SetRect(_id) => { - // TODO(opt): set only this child - cx.send_action(Action::SET_RECT); - } - Pending::NextNavFocus { - target, - reverse, - source, - } => { - cx.next_nav_focus_impl(win.as_node(data), target, reverse, source); - } } - } - // Poll futures last. This means that any newly pushed future should - // get polled from the same update() call. - cx.poll_futures(win.as_node(data)); + // Warning: infinite loops are possible here if widgets always queue a + // new pending event when evaluating one of these: + while let Some(item) = cx.pending.pop_front() { + log::trace!(target: "kas_core::event", "update: handling Pending::{item:?}"); + match item { + Pending::Configure(id) => { + win.as_node(data) + .find_node(&id, |node| cx.configure(node, id.clone())); + + let hover = win.find_id(data, cx.state.last_mouse_coord); + cx.state.set_hover(hover); + } + Pending::Update(id) => { + win.as_node(data).find_node(&id, |node| cx.update(node)); + } + Pending::Send(id, event) => { + if matches!(&event, &Event::MouseHover(false)) { + cx.hover_icon = Default::default(); + } + cx.send_event(win.as_node(data), id, event); + } + Pending::NextNavFocus { + target, + reverse, + source, + } => { + cx.next_nav_focus_impl(win.as_node(data), target, reverse, source); + } + } + } - drop(cx); + // Poll futures last. This means that any newly pushed future should + // get polled from the same update() call. + cx.poll_futures(win.as_node(data)); + }); if self.hover_icon != old_hover_icon && self.mouse_grab.is_none() { - shell.set_cursor_icon(self.hover_icon); + window.set_cursor_icon(self.hover_icon); } std::mem::take(&mut self.action) @@ -383,28 +373,39 @@ impl<'a> EventCx<'a> { let coord = position.cast_approx(); // Update hovered win - let cur_id = win.find_id(data, coord); - let delta = coord - self.last_mouse_coord; - self.set_hover(cur_id.clone()); + let id = win.find_id(data, coord); + self.set_hover(id.clone()); if let Some(grab) = self.state.mouse_grab.as_mut() { - if !grab.mode.is_pan() { - grab.cur_id = cur_id; - grab.coord = coord; - grab.delta += delta; - } else if let Some(pan) = - self.state.pan_grab.get_mut(usize::conv(grab.pan_grab.0)) - { - pan.coords[usize::conv(grab.pan_grab.1)].1 = coord; + match grab.details { + GrabDetails::Click { ref mut cur_id } => { + *cur_id = id; + } + GrabDetails::Grab => { + let target = grab.start_id.clone(); + let press = Press { + source: PressSource::Mouse(grab.button, grab.repetitions), + id, + coord, + }; + let delta = coord - self.last_mouse_coord; + let event = Event::PressMove { press, delta }; + self.send_event(win.as_node(data), target, event); + } + GrabDetails::Pan(g) => { + if let Some(pan) = self.state.pan_grab.get_mut(usize::conv(g.0)) { + pan.coords[usize::conv(g.1)].1 = coord; + } + } } - } else if let Some(id) = self.popups.last().map(|(_, p, _)| p.id.clone()) { + } else if let Some(popup_id) = self.popups.last().map(|(_, p, _)| p.id.clone()) { let press = Press { source: PressSource::Mouse(FAKE_MOUSE_BUTTON, 0), - id: cur_id, + id, coord, }; let event = Event::CursorMove { press }; - self.send_event(win.as_node(data), id, event); + self.send_event(win.as_node(data), popup_id, event); } else { // We don't forward move events without a grab } @@ -423,8 +424,6 @@ impl<'a> EventCx<'a> { } } MouseWheel { delta, .. } => { - self.flush_mouse_grab_motion(win.as_node(data)); - self.last_click_button = FAKE_MOUSE_BUTTON; let event = Event::Scroll(match delta { @@ -441,8 +440,6 @@ impl<'a> EventCx<'a> { } } MouseInput { state, button, .. } => { - self.flush_mouse_grab_motion(win.as_node(data)); - let coord = self.last_mouse_coord; if state == ElementState::Pressed { @@ -527,6 +524,19 @@ impl<'a> EventCx<'a> { grab.cur_id = cur_id; grab.coord = coord; + + if grab.last_move != grab.coord { + let delta = grab.coord - grab.last_move; + let target = grab.start_id.clone(); + let press = Press { + source: PressSource::Touch(grab.id), + id: grab.cur_id.clone(), + coord: grab.coord, + }; + let event = Event::PressMove { press, delta }; + grab.last_move = grab.coord; + self.send_event(win.as_node(data), target, event); + } } else { pan_grab = Some(grab.pan_grab); } @@ -545,9 +555,6 @@ impl<'a> EventCx<'a> { ev @ (TouchPhase::Ended | TouchPhase::Cancelled) => { if let Some(mut grab) = self.remove_touch(touch.id) { self.send_action(grab.flush_click_move()); - if let Some((id, event)) = grab.flush_grab_move() { - self.send_event(win.as_node(data), id, event); - } if grab.mode == GrabMode::Grab { let id = grab.cur_id.clone(); diff --git a/crates/kas-core/src/event/cx/mod.rs b/crates/kas-core/src/event/cx/mod.rs index 64517cec7..02cee28e4 100644 --- a/crates/kas-core/src/event/cx/mod.rs +++ b/crates/kas-core/src/event/cx/mod.rs @@ -20,8 +20,8 @@ use std::u16; use super::config::WindowConfig; use super::*; use crate::cast::Cast; -use crate::geom::{Coord, Offset}; -use crate::shell::ShellWindow; +use crate::geom::Coord; +use crate::shell::{Platform, ShellSharedErased, WindowDataErased}; use crate::util::WidgetHierarchy; use crate::LayoutExt; use crate::{Action, Erased, ErasedStack, NavAdvance, Node, Widget, WidgetId, WindowId}; @@ -35,20 +35,20 @@ pub use config::ConfigCx; pub use press::{GrabBuilder, Press, PressSource}; /// Controls the types of events delivered by [`Press::grab`] -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] pub enum GrabMode { /// Deliver [`Event::PressEnd`] only for each grabbed press Click, /// Deliver [`Event::PressMove`] and [`Event::PressEnd`] for each grabbed press Grab, - /// Deliver [`Event::Pan`] events, with scaling and rotation - PanFull, - /// Deliver [`Event::Pan`] events, with scaling - PanScale, - /// Deliver [`Event::Pan`] events, with rotation - PanRotate, /// Deliver [`Event::Pan`] events, without scaling or rotation PanOnly, + /// Deliver [`Event::Pan`] events, with rotation + PanRotate, + /// Deliver [`Event::Pan`] events, with scaling + PanScale, + /// Deliver [`Event::Pan`] events, with scaling and rotation + PanFull, } impl GrabMode { @@ -58,33 +58,36 @@ impl GrabMode { } } +#[derive(Clone, Debug)] +enum GrabDetails { + Click { cur_id: Option }, + Grab, + Pan((u16, u16)), +} + +impl GrabDetails { + fn is_pan(&self) -> bool { + matches!(self, GrabDetails::Pan(_)) + } +} + #[derive(Clone, Debug)] struct MouseGrab { button: MouseButton, repetitions: u32, start_id: WidgetId, - cur_id: Option, depress: Option, - mode: GrabMode, - pan_grab: (u16, u16), - coord: Coord, - delta: Offset, + details: GrabDetails, } impl<'a> EventCx<'a> { - fn flush_mouse_grab_motion(&mut self, widget: Node<'_>) { + fn flush_mouse_grab_motion(&mut self) { if let Some(grab) = self.mouse_grab.as_mut() { - let delta = grab.delta; - if delta == Offset::ZERO { - return; - } - grab.delta = Offset::ZERO; - - match grab.mode { - GrabMode::Click => { - if grab.start_id == grab.cur_id { - if grab.depress != grab.cur_id { - grab.depress = grab.cur_id.clone(); + match grab.details { + GrabDetails::Click { ref cur_id } => { + if grab.start_id == cur_id { + if grab.depress != *cur_id { + grab.depress = cur_id.clone(); self.action |= Action::REDRAW; } } else if grab.depress.is_some() { @@ -92,16 +95,6 @@ impl<'a> EventCx<'a> { self.action |= Action::REDRAW; } } - GrabMode::Grab => { - let target = grab.start_id.clone(); - let press = Press { - source: PressSource::Mouse(grab.button, grab.repetitions), - id: grab.cur_id.clone(), - coord: grab.coord, - }; - let event = Event::PressMove { press, delta }; - self.send_event(widget, target, event); - } _ => (), } } @@ -136,23 +129,6 @@ impl TouchGrab { } Action::empty() } - - fn flush_grab_move(&mut self) -> Option<(WidgetId, Event)> { - if self.mode == GrabMode::Grab && self.last_move != self.coord { - let delta = self.coord - self.last_move; - let target = self.start_id.clone(); - let press = Press { - source: PressSource::Touch(self.id), - id: self.cur_id.clone(), - coord: self.coord, - }; - let event = Event::PressMove { press, delta }; - self.last_move = self.coord; - Some((target, event)) - } else { - None - } - } } const MAX_PAN_GRABS: usize = 2; @@ -172,7 +148,6 @@ enum Pending { Configure(WidgetId), Update(WidgetId), Send(WidgetId, Event), - SetRect(WidgetId), NextNavFocus { target: Option, reverse: bool, @@ -201,6 +176,7 @@ type AccessLayer = (bool, HashMap); // `SmallVec` is used to keep contents in local memory. pub struct EventState { config: WindowConfig, + platform: Platform, disabled: Vec, window_has_focus: bool, modifiers: ModifiersState, @@ -258,6 +234,8 @@ impl EventState { break; } + debug_assert_eq!(grab.mode, mode); + let index = grab.n; if usize::from(index) < MAX_PAN_GRABS { grab.coords[usize::from(index)] = (coord, coord); @@ -286,9 +264,10 @@ impl EventState { log::trace!("remove_pan: index={index}"); self.pan_grab.remove(index); if let Some(grab) = &mut self.mouse_grab { - let p0 = grab.pan_grab.0; - if usize::from(p0) >= index && p0 != u16::MAX { - grab.pan_grab.0 = p0 - 1; + if let GrabDetails::Pan(ref mut g) = grab.details { + if usize::from(g.0) >= index { + g.0 -= 1; + } } } for grab in self.touch_grab.iter_mut() { @@ -418,7 +397,8 @@ impl EventState { #[must_use] pub struct EventCx<'a> { state: &'a mut EventState, - shell: &'a mut dyn ShellWindow, + shell: &'a mut dyn ShellSharedErased, + window: &'a dyn WindowDataErased, messages: &'a mut ErasedStack, last_child: Option, scroll: Scroll, @@ -543,10 +523,10 @@ impl<'a> EventCx<'a> { fn remove_mouse_grab(&mut self, success: bool) -> Option<(WidgetId, Event)> { if let Some(grab) = self.mouse_grab.take() { log::trace!("remove_mouse_grab: start_id={}", grab.start_id); - self.shell.set_cursor_icon(self.hover_icon); + self.window.set_cursor_icon(self.hover_icon); self.send_action(Action::REDRAW); // redraw(..) - if grab.mode.is_pan() { - self.remove_pan_grab(grab.pan_grab); + if let GrabDetails::Pan(g) = grab.details { + self.remove_pan_grab(g); // Pan grabs do not receive Event::PressEnd None } else { diff --git a/crates/kas-core/src/event/cx/press.rs b/crates/kas-core/src/event/cx/press.rs index 1d72aa79e..40ba77793 100644 --- a/crates/kas-core/src/event/cx/press.rs +++ b/crates/kas-core/src/event/cx/press.rs @@ -6,9 +6,10 @@ //! Event handling: events #[allow(unused)] use super::{Event, EventState}; // for doc-links -use super::{EventCx, GrabMode, IsUsed, MouseGrab, Pending, TouchGrab}; -use crate::event::{CursorIcon, MouseButton, Used}; -use crate::geom::{Coord, Offset}; +use super::{EventCx, GrabMode, IsUsed, MouseGrab, TouchGrab}; +use crate::event::cx::GrabDetails; +use crate::event::{CursorIcon, MouseButton, Unused, Used}; +use crate::geom::Coord; use crate::{Action, WidgetId}; /// Source of `EventChild::Press` @@ -152,6 +153,21 @@ impl GrabBuilder { } /// Complete the grab, providing the [`EventCx`] + /// + /// In case of an existing grab for the same [`source`](Press::source), + /// - If the [`WidgetId`] differs this fails (returns [`Unused`]) + /// - If the [`MouseButton`] differs this fails (technically this is a + /// different `source`, but simultaneous grabs of multiple mouse buttons + /// are not supported). + /// - If one grab is a [pan](GrabMode::is_pan) and the other is not, this fails + /// - [`GrabMode::Click`] may be upgraded to [`GrabMode::Grab`] + /// - Changing from one pan mode to another is an error + /// - Mouse button repetitions may be increased; decreasing is an error + /// - A [`CursorIcon`] may be set + /// - The depress target is re-set to the grabbing widget + /// + /// Note: error conditions are only checked in debug builds. These cases + /// may need revision. pub fn with_cx(self, cx: &mut EventCx) -> IsUsed { let GrabBuilder { id, @@ -161,48 +177,71 @@ impl GrabBuilder { cursor, } = self; log::trace!(target: "kas_core::event", "grab_press: start_id={id}, source={source:?}"); - let mut pan_grab = (u16::MAX, 0); match source { PressSource::Mouse(button, repetitions) => { - if let Some((id, event)) = cx.remove_mouse_grab(false) { - cx.pending.push_back(Pending::Send(id, event)); + let details = match mode { + GrabMode::Click => GrabDetails::Click { + cur_id: Some(id.clone()), + }, + GrabMode::Grab => GrabDetails::Grab, + mode => { + assert!(mode.is_pan()); + let g = cx.set_pan_on(id.clone(), mode, false, coord); + GrabDetails::Pan(g) + } + }; + if let Some(ref mut grab) = cx.mouse_grab { + if grab.start_id != id + || grab.button != button + || grab.details.is_pan() != mode.is_pan() + { + return Unused; + } + + debug_assert!(repetitions >= grab.repetitions); + grab.repetitions = repetitions; + grab.depress = Some(id); + grab.details = details; + } else { + cx.mouse_grab = Some(MouseGrab { + button, + repetitions, + start_id: id.clone(), + depress: Some(id), + details, + }); } - if mode.is_pan() { - pan_grab = cx.set_pan_on(id.clone(), mode, false, coord); - } - cx.mouse_grab = Some(MouseGrab { - button, - repetitions, - start_id: id.clone(), - cur_id: Some(id.clone()), - depress: Some(id), - mode, - pan_grab, - coord, - delta: Offset::ZERO, - }); if let Some(icon) = cursor { - cx.shell.set_cursor_icon(icon); + cx.window.set_cursor_icon(icon); } } PressSource::Touch(touch_id) => { - if cx.remove_touch(touch_id).is_some() { - #[cfg(debug_assertions)] - log::error!(target: "kas_core::event", "grab_press: touch_id conflict!"); - } - if mode.is_pan() { - pan_grab = cx.set_pan_on(id.clone(), mode, true, coord); + if let Some(grab) = cx.get_touch(touch_id) { + if grab.mode.is_pan() != mode.is_pan() { + return Unused; + } + + grab.depress = Some(id.clone()); + grab.cur_id = Some(id); + grab.last_move = coord; + grab.coord = coord; + grab.mode = grab.mode.max(mode); + } else { + let mut pan_grab = (u16::MAX, 0); + if mode.is_pan() { + pan_grab = cx.set_pan_on(id.clone(), mode, true, coord); + } + cx.touch_grab.push(TouchGrab { + id: touch_id, + start_id: id.clone(), + depress: Some(id.clone()), + cur_id: Some(id), + last_move: coord, + coord, + mode, + pan_grab, + }); } - cx.touch_grab.push(TouchGrab { - id: touch_id, - start_id: id.clone(), - depress: Some(id.clone()), - cur_id: Some(id), - last_move: coord, - coord, - mode, - pan_grab, - }); } } diff --git a/crates/kas-core/src/root.rs b/crates/kas-core/src/root.rs index b599ba707..c31cfc1da 100644 --- a/crates/kas-core/src/root.rs +++ b/crates/kas-core/src/root.rs @@ -252,7 +252,16 @@ impl_scope! { fn handle_scroll(&mut self, cx: &mut EventCx, data: &Data, _: Scroll) { // Something was scrolled; update pop-up translations - cx.config_cx(|cx| self.resize_popups(cx, data)); + self.resize_popups(&mut cx.config_cx(), data); + } + } + + impl std::fmt::Debug for Self { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Window") + .field("core", &self.core) + .field("title", &self.title_bar.title()) + .finish() } } } @@ -403,7 +412,7 @@ impl Window { ) { let index = self.popups.len(); self.popups.push((id, popup, Offset::ZERO)); - cx.config_cx(|cx| self.resize_popup(cx, data, index)); + self.resize_popup(&mut cx.config_cx(), data, index); cx.send_action(Action::REDRAW); } diff --git a/crates/kas-core/src/shell/common.rs b/crates/kas-core/src/shell/common.rs index 4185bc579..fe277689a 100644 --- a/crates/kas-core/src/shell/common.rs +++ b/crates/kas-core/src/shell/common.rs @@ -6,13 +6,9 @@ //! Public shell stuff common to all backends use crate::draw::{color::Rgba, DrawIface, WindowCommon}; -use crate::draw::{DrawImpl, DrawShared, DrawSharedImpl}; -use crate::event::CursorIcon; +use crate::draw::{DrawImpl, DrawSharedImpl}; use crate::geom::Size; -use crate::theme::{ThemeControl, ThemeSize}; -use crate::{Action, Window, WindowId}; use raw_window_handle as raw; -use std::any::TypeId; use thiserror::Error; /// Possible failures from constructing a [`Shell`](super::Shell) @@ -225,95 +221,3 @@ pub trait WindowSurface { /// Present frame fn present(&mut self, shared: &mut Self::Shared, clear_color: Rgba); } - -/// Window management interface -/// -/// Note: previously, this was implemented by a dependent crate. Now, it is not, -/// which might suggest this trait is no longer needed, however `EventCx` still -/// needs type erasure over `S: WindowSurface` and `T: Theme`. -pub(crate) trait ShellWindow { - /// Add a pop-up - /// - /// A pop-up may be presented as an overlay layer in the current window or - /// via a new borderless window. - /// - /// Pop-ups support position hints: they are placed *next to* the specified - /// `rect`, preferably in the given `direction`. - /// - /// Returns `None` if window creation is not currently available (but note - /// that `Some` result does not guarantee the operation succeeded). - fn add_popup(&mut self, popup: crate::PopupDescriptor) -> WindowId; - - /// Add a window - /// - /// Toolkits typically allow windows to be added directly, before start of - /// the event loop (e.g. `kas_wgpu::Toolkit::add`). - /// - /// This method is an alternative allowing a window to be added from an - /// event handler, albeit without error handling. - /// - /// Safety: this method *should* require generic parameter `Data` (data type - /// passed to the `Shell`). Realising this would require adding this type - /// parameter to `EventCx` and thus to all widgets (not necessarily the - /// type accepted by the widget as input). As an alternative we require the - /// caller to type-cast `Window` to `Window<()>` and pass in - /// `TypeId::of::()`. - unsafe fn add_window(&mut self, window: Window<()>, data_type_id: TypeId) -> WindowId; - - /// Close a window - fn close_window(&mut self, id: WindowId); - - /// Attempt to get clipboard contents - /// - /// In case of failure, paste actions will simply fail. The implementation - /// may wish to log an appropriate warning message. - fn get_clipboard(&mut self) -> Option; - - /// Attempt to set clipboard contents - fn set_clipboard(&mut self, content: String); - - /// Get contents of primary buffer - /// - /// Linux has a "primary buffer" with implicit copy on text selection and - /// paste on middle-click. This method does nothing on other platforms. - fn get_primary(&mut self) -> Option; - - /// Set contents of primary buffer - /// - /// Linux has a "primary buffer" with implicit copy on text selection and - /// paste on middle-click. This method does nothing on other platforms. - fn set_primary(&mut self, content: String); - - /// Adjust the theme - /// - /// Note: theme adjustments apply to all windows, as does the [`Action`] - /// returned from the closure. - // - // TODO(opt): pass f by value, not boxed - fn adjust_theme<'s>(&'s mut self, f: Box Action + 's>); - - /// Access [`ThemeSize`] and [`DrawShared`] objects - /// - /// Implementations should call the given function argument once; not doing - /// so is memory-safe but will cause panics in `EventCx` methods. - /// User-code *must not* depend on `f` being called for memory safety. - fn size_and_draw_shared<'s>( - &'s mut self, - f: Box, - ); - - /// Set the mouse cursor - fn set_cursor_icon(&mut self, icon: CursorIcon); - - /// Get the platform - fn platform(&self) -> Platform; - - /// Directly access Winit Window - /// - /// This is a temporary API, allowing e.g. to minimize the window. - #[cfg(winit)] - fn winit_window(&self) -> Option<&winit::window::Window>; - - /// Access a Waker - fn waker(&self) -> &std::task::Waker; -} diff --git a/crates/kas-core/src/shell/event_loop.rs b/crates/kas-core/src/shell/event_loop.rs index b996f71ca..b0aae0735 100644 --- a/crates/kas-core/src/shell/event_loop.rs +++ b/crates/kas-core/src/shell/event_loop.rs @@ -5,18 +5,16 @@ //! Event loop and handling +use super::{Pending, SharedState}; +use super::{ProxyAction, Window, WindowSurface}; +use kas::theme::Theme; +use kas::{Action, AppData, WindowId}; use std::collections::HashMap; -use std::time::{Duration, Instant}; - +use std::time::Instant; use winit::event::{Event, StartCause}; use winit::event_loop::{ControlFlow, EventLoopWindowTarget}; use winit::window as ww; -use super::{PendingAction, SharedState}; -use super::{ProxyAction, Window, WindowSurface}; -use kas::theme::Theme; -use kas::{Action, AppData, WindowId}; - /// Event-loop data structure (i.e. all run-time state) pub(super) struct Loop> where @@ -25,7 +23,7 @@ where /// State is suspended until we receive Event::Resumed suspended: bool, /// Window states - windows: HashMap>, + windows: HashMap>>, popups: HashMap, /// Translates our WindowId to winit's id_map: HashMap, @@ -33,15 +31,16 @@ where shared: SharedState, /// Timer resumes: (time, window identifier) resumes: Vec<(Instant, WindowId)>, - /// Frame rate counter - frame_count: (Instant, u32), } impl> Loop where T::Window: kas::theme::Window, { - pub(super) fn new(mut windows: Vec>, shared: SharedState) -> Self { + pub(super) fn new( + mut windows: Vec>>, + shared: SharedState, + ) -> Self { Loop { suspended: true, windows: windows.drain(..).map(|w| (w.window_id, w)).collect(), @@ -49,7 +48,6 @@ where id_map: Default::default(), shared, resumes: vec![], - frame_count: (Instant::now(), 0), } } @@ -57,12 +55,11 @@ where &mut self, event: Event, elwt: &EventLoopWindowTarget, - control_flow: &mut ControlFlow, ) { match event { Event::NewEvents(cause) => { // MainEventsCleared will reset control_flow (but not when it is Poll) - *control_flow = ControlFlow::Wait; + elwt.set_control_flow(ControlFlow::Wait); match cause { StartCause::ResumeTimeReached { @@ -99,11 +96,13 @@ where } Event::WindowEvent { window_id, event } => { - self.flush_pending(elwt, control_flow); + self.flush_pending(elwt); if let Some(id) = self.id_map.get(&window_id) { if let Some(window) = self.windows.get_mut(id) { - window.handle_event(&mut self.shared, event); + if window.handle_event(&mut self.shared, event) { + elwt.set_control_flow(ControlFlow::Poll); + } } } } @@ -156,57 +155,29 @@ where Event::Resumed => (), Event::AboutToWait => { - self.flush_pending(elwt, control_flow); + self.flush_pending(elwt); self.resumes.sort_by_key(|item| item.0); - let is_exit = matches!(control_flow, ControlFlow::ExitWithCode(_)); - *control_flow = if is_exit || self.windows.is_empty() { - self.shared.on_exit(); - debug_assert!(!is_exit || matches!(control_flow, ControlFlow::ExitWithCode(0))); - ControlFlow::ExitWithCode(0) - } else if *control_flow == ControlFlow::Poll { - ControlFlow::Poll + if self.windows.is_empty() { + elwt.exit(); + } else if matches!(elwt.control_flow(), ControlFlow::Poll) { } else if let Some((instant, _)) = self.resumes.first() { - ControlFlow::WaitUntil(*instant) + elwt.set_control_flow(ControlFlow::WaitUntil(*instant)); } else { - ControlFlow::Wait + elwt.set_control_flow(ControlFlow::Wait); }; } - Event::RedrawRequested(id) => { - // We must conclude pending actions (such as resize) before drawing. - self.flush_pending(elwt, control_flow); - - if let Some(id) = self.id_map.get(&id) { - if let Some(window) = self.windows.get_mut(id) { - if window.do_draw(&mut self.shared).is_err() { - *control_flow = ControlFlow::Poll; - } - } - } - - const SECOND: Duration = Duration::from_secs(1); - self.frame_count.1 += 1; - let now = Instant::now(); - if self.frame_count.0 + SECOND <= now { - log::debug!("Frame rate: {} per second", self.frame_count.1); - self.frame_count.0 = now; - self.frame_count.1 = 0; - } + Event::LoopExiting => { + self.shared.on_exit(); } - - Event::LoopExiting => (), } } - fn flush_pending( - &mut self, - elwt: &EventLoopWindowTarget, - control_flow: &mut ControlFlow, - ) { - while let Some(pending) = self.shared.shell.pending.pop() { + fn flush_pending(&mut self, elwt: &EventLoopWindowTarget) { + while let Some(pending) = self.shared.shell.pending.pop_front() { match pending { - PendingAction::AddPopup(parent_id, id, popup) => { + Pending::AddPopup(parent_id, id, popup) => { log::debug!("Pending: adding overlay"); // TODO: support pop-ups as a special window, where available self.windows.get_mut(&parent_id).unwrap().add_popup( @@ -216,9 +187,8 @@ where ); self.popups.insert(id, parent_id); } - PendingAction::AddWindow(id, widget) => { - log::debug!("Pending: adding window {}", widget.title()); - let mut window = Window::new(&self.shared, id, widget); + Pending::AddWindow(id, mut window) => { + log::debug!("Pending: adding window {}", window.widget.title()); if !self.suspended { match window.resume(&mut self.shared, elwt) { Ok(winit_id) => { @@ -231,7 +201,7 @@ where } self.windows.insert(id, window); } - PendingAction::CloseWindow(target) => { + Pending::CloseWindow(target) => { let mut win_id = target; if let Some(id) = self.popups.remove(&target) { win_id = id; @@ -240,11 +210,11 @@ where window.send_close(&mut self.shared, target); } } - PendingAction::Action(action) => { + Pending::Action(action) => { if action.contains(Action::CLOSE | Action::EXIT) { self.windows.clear(); self.id_map.clear(); - *control_flow = ControlFlow::Poll; + elwt.set_control_flow(ControlFlow::Poll); } else { for (_, window) in self.windows.iter_mut() { window.handle_action(&mut self.shared, action); diff --git a/crates/kas-core/src/shell/mod.rs b/crates/kas-core/src/shell/mod.rs index 65536b189..0ffdfc31f 100644 --- a/crates/kas-core/src/shell/mod.rs +++ b/crates/kas-core/src/shell/mod.rs @@ -13,11 +13,12 @@ mod common; #[cfg(winit)] use crate::WindowId; #[cfg(winit)] use event_loop::Loop as EventLoop; -#[cfg(winit)] use shared::{SharedState, ShellShared}; +#[cfg(winit)] +pub(crate) use shared::{SharedState, ShellSharedErased}; #[cfg(winit)] use shell::PlatformWrapper; -#[cfg(winit)] use window::Window; +#[cfg(winit)] +pub(crate) use window::{Window, WindowDataErased}; -pub(crate) use common::ShellWindow; pub use common::{Error, Platform, Result}; #[cfg_attr(not(feature = "internal_doc"), doc(hidden))] pub use common::{GraphicalShell, WindowSurface}; @@ -29,31 +30,17 @@ pub extern crate raw_window_handle; // TODO(opt): Clippy is probably right that we shouldn't copy a large value // around (also applies when constructing a shell::Window). #[allow(clippy::large_enum_variant)] +#[crate::autoimpl(Debug)] #[cfg(winit)] -enum PendingAction { +enum Pending> { AddPopup(WindowId, WindowId, kas::PopupDescriptor), - AddWindow(WindowId, kas::Window), + // NOTE: we don't need S, T here if we construct the Window later. + // But this way we can pass a single boxed value. + AddWindow(WindowId, Box>), CloseWindow(WindowId), Action(kas::Action), } -#[cfg(winit)] -impl std::fmt::Debug for PendingAction { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - PendingAction::AddPopup(parent_id, id, popup) => f - .debug_tuple("AddPopup") - .field(&parent_id) - .field(&id) - .field(&popup) - .finish(), - PendingAction::AddWindow(id, _) => f.debug_tuple("AddWindow").field(&id).finish(), - PendingAction::CloseWindow(id) => f.debug_tuple("CloseWindow").field(&id).finish(), - PendingAction::Action(action) => f.debug_tuple("Action").field(&action).finish(), - } - } -} - #[cfg(winit)] #[derive(Debug)] enum ProxyAction { @@ -62,3 +49,221 @@ enum ProxyAction { Message(kas::erased::SendErased), WakeAsync, } + +#[cfg(test)] +mod test { + use super::*; + + struct Draw; + impl crate::draw::DrawImpl for Draw { + fn common_mut(&mut self) -> &mut crate::draw::WindowCommon { + todo!() + } + + fn new_pass( + &mut self, + _: crate::draw::PassId, + _: crate::prelude::Rect, + _: crate::prelude::Offset, + _: crate::draw::PassType, + ) -> crate::draw::PassId { + todo!() + } + + fn get_clip_rect(&self, _: crate::draw::PassId) -> crate::prelude::Rect { + todo!() + } + + fn rect( + &mut self, + _: crate::draw::PassId, + _: crate::geom::Quad, + _: crate::draw::color::Rgba, + ) { + todo!() + } + + fn frame( + &mut self, + _: crate::draw::PassId, + _: crate::geom::Quad, + _: crate::geom::Quad, + _: crate::draw::color::Rgba, + ) { + todo!() + } + } + impl crate::draw::DrawRoundedImpl for Draw { + fn rounded_line( + &mut self, + _: crate::draw::PassId, + _: crate::geom::Vec2, + _: crate::geom::Vec2, + _: f32, + _: crate::draw::color::Rgba, + ) { + todo!() + } + + fn circle( + &mut self, + _: crate::draw::PassId, + _: crate::geom::Quad, + _: f32, + _: crate::draw::color::Rgba, + ) { + todo!() + } + + fn circle_2col( + &mut self, + _: crate::draw::PassId, + _: crate::geom::Quad, + _: crate::draw::color::Rgba, + _: crate::draw::color::Rgba, + ) { + todo!() + } + + fn rounded_frame( + &mut self, + _: crate::draw::PassId, + _: crate::geom::Quad, + _: crate::geom::Quad, + _: f32, + _: crate::draw::color::Rgba, + ) { + todo!() + } + + fn rounded_frame_2col( + &mut self, + _: crate::draw::PassId, + _: crate::geom::Quad, + _: crate::geom::Quad, + _: crate::draw::color::Rgba, + _: crate::draw::color::Rgba, + ) { + todo!() + } + } + + struct DrawShared; + impl crate::draw::DrawSharedImpl for DrawShared { + type Draw = Draw; + + fn set_raster_config(&mut self, _: &crate::theme::RasterConfig) { + todo!() + } + + fn image_alloc( + &mut self, + _: (u32, u32), + ) -> std::result::Result { + todo!() + } + + fn image_upload(&mut self, _: crate::draw::ImageId, _: &[u8], _: crate::draw::ImageFormat) { + todo!() + } + + fn image_free(&mut self, _: crate::draw::ImageId) { + todo!() + } + + fn image_size(&self, _: crate::draw::ImageId) -> Option<(u32, u32)> { + todo!() + } + + fn draw_image( + &self, + _: &mut Self::Draw, + _: crate::draw::PassId, + _: crate::draw::ImageId, + _: crate::geom::Quad, + ) { + todo!() + } + + fn draw_text( + &mut self, + _: &mut Self::Draw, + _: crate::draw::PassId, + _: crate::prelude::Rect, + _: &kas_text::TextDisplay, + _: crate::draw::color::Rgba, + ) { + todo!() + } + + fn draw_text_effects( + &mut self, + _: &mut Self::Draw, + _: crate::draw::PassId, + _: crate::prelude::Rect, + _: &kas_text::TextDisplay, + _: crate::draw::color::Rgba, + _: &[kas_text::Effect<()>], + ) { + todo!() + } + + fn draw_text_effects_rgba( + &mut self, + _: &mut Self::Draw, + _: crate::draw::PassId, + _: crate::prelude::Rect, + _: &kas_text::TextDisplay, + _: &[kas_text::Effect], + ) { + todo!() + } + } + + struct Surface; + impl WindowSurface for Surface { + type Shared = DrawShared; + + fn new( + _: &mut Self::Shared, + _: crate::prelude::Size, + _: W, + ) -> Result + where + Self: Sized, + { + todo!() + } + + fn size(&self) -> crate::prelude::Size { + todo!() + } + + fn do_resize(&mut self, _: &mut Self::Shared, _: crate::prelude::Size) -> bool { + todo!() + } + + fn draw_iface<'iface>( + &'iface mut self, + _: &'iface mut crate::draw::SharedState, + ) -> crate::draw::DrawIface<'iface, Self::Shared> { + todo!() + } + + fn common_mut(&mut self) -> &mut crate::draw::WindowCommon { + todo!() + } + + fn present(&mut self, _: &mut Self::Shared, _: crate::draw::color::Rgba) { + todo!() + } + } + + #[test] + fn size_of_pending() { + assert_eq!( + std::mem::size_of::>(), + 32 + ); + } +} diff --git a/crates/kas-core/src/shell/shared.rs b/crates/kas-core/src/shell/shared.rs index 488dac6d3..02f674bdd 100644 --- a/crates/kas-core/src/shell/shared.rs +++ b/crates/kas-core/src/shell/shared.rs @@ -5,37 +5,39 @@ //! Shared state -use std::num::NonZeroU32; -use std::task::Waker; - -use super::{PendingAction, Platform, WindowSurface}; +use super::{Pending, Platform, WindowSurface}; use kas::config::Options; +use kas::draw::DrawShared; use kas::shell::Error; -use kas::theme::Theme; +use kas::theme::{Theme, ThemeControl}; use kas::util::warn_about_error; -use kas::{draw, AppData, ErasedStack, WindowId}; +use kas::{draw, Action, AppData, ErasedStack, WindowId}; +use std::any::TypeId; use std::cell::RefCell; +use std::collections::VecDeque; +use std::num::NonZeroU32; use std::rc::Rc; +use std::task::Waker; #[cfg(feature = "clipboard")] use arboard::Clipboard; /// Shell interface state -pub(super) struct ShellShared { +pub(crate) struct ShellShared> { pub(super) platform: Platform, + pub(super) config: Rc>, #[cfg(feature = "clipboard")] clipboard: Option, - pub(super) draw: draw::SharedState, + pub(super) draw: draw::SharedState, pub(super) theme: T, - pub(super) pending: Vec>, + pub(super) pending: VecDeque>, pub(super) waker: Waker, window_id: u32, } /// State shared between windows -pub struct SharedState { - pub(super) shell: ShellShared, +pub(crate) struct SharedState> { + pub(super) shell: ShellShared, pub(super) data: Data, - pub(super) config: Rc>, /// Estimated scale factor (from last window constructed or available screens) pub(super) scale_factor: f64, options: Options, @@ -55,7 +57,7 @@ where config: Rc>, ) -> Result { let platform = pw.platform(); - let mut draw = kas::draw::SharedState::new(draw_shared, platform); + let mut draw = kas::draw::SharedState::new(draw_shared); theme.init(&mut draw); #[cfg(feature = "clipboard")] @@ -70,16 +72,16 @@ where Ok(SharedState { shell: ShellShared { platform, + config, #[cfg(feature = "clipboard")] clipboard, draw, theme, - pending: vec![], + pending: Default::default(), waker: pw.create_waker(), window_id: 0, }, data, - config, scale_factor: pw.guess_scale_factor(), options, }) @@ -89,14 +91,14 @@ where pub(crate) fn handle_messages(&mut self, messages: &mut ErasedStack) { if messages.reset_and_has_any() { let action = self.data.handle_messages(messages); - self.shell.pending.push(PendingAction::Action(action)); + self.shell.pending.push_back(Pending::Action(action)); } } - pub fn on_exit(&self) { + pub(crate) fn on_exit(&self) { match self .options - .write_config(&self.config.borrow(), &self.shell.theme) + .write_config(&self.shell.config.borrow(), &self.shell.theme) { Ok(()) => (), Err(error) => warn_about_error("Failed to save config", &error), @@ -104,19 +106,137 @@ where } } -impl ShellShared { +impl> ShellShared { /// Return the next window identifier /// /// TODO(opt): this should recycle used identifiers since WidgetId does not /// efficiently represent large numbers. - pub fn next_window_id(&mut self) -> WindowId { + pub(crate) fn next_window_id(&mut self) -> WindowId { let id = self.window_id + 1; self.window_id = id; WindowId::new(NonZeroU32::new(id).unwrap()) } +} - #[inline] - pub fn get_clipboard(&mut self) -> Option { +pub(crate) trait ShellSharedErased { + /// Add a pop-up + /// + /// A pop-up may be presented as an overlay layer in the current window or + /// via a new borderless window. + /// + /// Pop-ups support position hints: they are placed *next to* the specified + /// `rect`, preferably in the given `direction`. + /// + /// Returns `None` if window creation is not currently available (but note + /// that `Some` result does not guarantee the operation succeeded). + fn add_popup(&mut self, parent_id: WindowId, popup: crate::PopupDescriptor) -> WindowId; + + /// Add a window + /// + /// Toolkits typically allow windows to be added directly, before start of + /// the event loop (e.g. `kas_wgpu::Toolkit::add`). + /// + /// This method is an alternative allowing a window to be added from an + /// event handler, albeit without error handling. + /// + /// Safety: this method *should* require generic parameter `Data` (data type + /// passed to the `Shell`). Realising this would require adding this type + /// parameter to `EventCx` and thus to all widgets (not necessarily the + /// type accepted by the widget as input). As an alternative we require the + /// caller to type-cast `Window` to `Window<()>` and pass in + /// `TypeId::of::()`. + unsafe fn add_window(&mut self, window: kas::Window<()>, data_type_id: TypeId) -> WindowId; + + /// Close a window + fn close_window(&mut self, id: WindowId); + + /// Attempt to get clipboard contents + /// + /// In case of failure, paste actions will simply fail. The implementation + /// may wish to log an appropriate warning message. + /// + /// NOTE: on Wayland, use `WindowDataErased::wayland_clipboard` instead. + /// This split API probably can't be resolved until Winit integrates + /// clipboard support. + fn get_clipboard(&mut self) -> Option; + + /// Attempt to set clipboard contents + /// + /// NOTE: on Wayland, use `WindowDataErased::wayland_clipboard` instead. + /// This split API probably can't be resolved until Winit integrates + /// clipboard support. + fn set_clipboard(&mut self, content: String); + + /// Get contents of primary buffer + /// + /// Linux has a "primary buffer" with implicit copy on text selection and + /// paste on middle-click. This method does nothing on other platforms. + /// + /// NOTE: on Wayland, use `WindowDataErased::wayland_clipboard` instead. + /// This split API probably can't be resolved until Winit integrates + /// clipboard support. + fn get_primary(&mut self) -> Option; + + /// Set contents of primary buffer + /// + /// Linux has a "primary buffer" with implicit copy on text selection and + /// paste on middle-click. This method does nothing on other platforms. + /// + /// NOTE: on Wayland, use `WindowDataErased::wayland_clipboard` instead. + /// This split API probably can't be resolved until Winit integrates + /// clipboard support. + fn set_primary(&mut self, content: String); + + /// Adjust the theme + /// + /// Note: theme adjustments apply to all windows, as does the [`Action`] + /// returned from the closure. + // + // TODO(opt): pass f by value, not boxed + fn adjust_theme<'s>(&'s mut self, f: Box Action + 's>); + + /// Access the [`DrawShared`] object + fn draw_shared(&mut self) -> &mut dyn DrawShared; + + /// Access a Waker + fn waker(&self) -> &std::task::Waker; +} + +impl> ShellSharedErased + for ShellShared +{ + fn add_popup(&mut self, parent_id: WindowId, popup: kas::PopupDescriptor) -> WindowId { + let id = self.next_window_id(); + self.pending + .push_back(Pending::AddPopup(parent_id, id, popup)); + id + } + + unsafe fn add_window(&mut self, window: kas::Window<()>, data_type_id: TypeId) -> WindowId { + // Safety: the window should be `Window`. We cast to that. + if data_type_id != TypeId::of::() { + // If this fails it is not safe to add the window (though we could just return). + panic!("add_window: window has wrong Data type!"); + } + let window: kas::Window = std::mem::transmute(window); + + // By far the simplest way to implement this is to let our call + // anscestor, event::Loop::handle, do the work. + // + // In theory we could pass the EventLoopWindowTarget for *each* event + // handled to create the winit window here or use statics to generate + // errors now, but user code can't do much with this error anyway. + let id = self.next_window_id(); + let window = Box::new(super::Window::new(&self, id, window)); + self.pending.push_back(Pending::AddWindow(id, window)); + id + } + + fn close_window(&mut self, id: WindowId) { + self.pending.push_back(Pending::CloseWindow(id)); + } + + fn get_clipboard(&mut self) -> Option { #[cfg(feature = "clipboard")] { if let Some(cb) = self.clipboard.as_mut() { @@ -130,8 +250,7 @@ impl ShellShared { None } - #[inline] - pub fn set_clipboard(&mut self, _content: String) { + fn set_clipboard<'c>(&mut self, _content: String) { #[cfg(feature = "clipboard")] if let Some(cb) = self.clipboard.as_mut() { match cb.set_text(_content) { @@ -141,8 +260,7 @@ impl ShellShared { } } - #[inline] - pub fn get_primary(&mut self) -> Option { + fn get_primary(&mut self) -> Option { #[cfg(all( unix, not(any(target_os = "macos", target_os = "android", target_os = "emscripten")), @@ -161,8 +279,7 @@ impl ShellShared { None } - #[inline] - pub fn set_primary(&mut self, _content: String) { + fn set_primary(&mut self, _content: String) { #[cfg(all( unix, not(any(target_os = "macos", target_os = "android", target_os = "emscripten")), @@ -180,4 +297,18 @@ impl ShellShared { } } } + + fn adjust_theme<'s>(&'s mut self, f: Box Action + 's>) { + let action = f(&mut self.theme); + self.pending.push_back(Pending::Action(action)); + } + + fn draw_shared(&mut self) -> &mut dyn DrawShared { + &mut self.draw + } + + #[inline] + fn waker(&self) -> &std::task::Waker { + &self.waker + } } diff --git a/crates/kas-core/src/shell/shell.rs b/crates/kas-core/src/shell/shell.rs index 74cac5b03..51fcb9f19 100644 --- a/crates/kas-core/src/shell/shell.rs +++ b/crates/kas-core/src/shell/shell.rs @@ -27,7 +27,7 @@ use winit::event_loop::{EventLoop, EventLoopBuilder, EventLoopProxy}; /// been initialised first. pub struct Shell> { el: EventLoop, - windows: Vec>, + windows: Vec>>, shared: SharedState, } @@ -156,7 +156,7 @@ where #[inline] pub fn add(&mut self, window: Window) -> WindowId { let id = self.shared.shell.next_window_id(); - let win = super::Window::new(&self.shared, id, window); + let win = Box::new(super::Window::new(&self.shared.shell, id, window)); self.windows.push(win); id } @@ -177,8 +177,7 @@ where #[inline] pub fn run(self) -> Result<()> { let mut el = super::EventLoop::new(self.windows, self.shared); - self.el - .run(move |event, elwt, control_flow| el.handle(event, elwt, control_flow))?; + self.el.run(move |event, elwt| el.handle(event, elwt))?; Ok(()) } } diff --git a/crates/kas-core/src/shell/window.rs b/crates/kas-core/src/shell/window.rs index 330a96c4f..37cfdceb4 100644 --- a/crates/kas-core/src/shell/window.rs +++ b/crates/kas-core/src/shell/window.rs @@ -5,21 +5,19 @@ //! Window types -use super::{PendingAction, Platform, ProxyAction}; -use super::{SharedState, ShellShared, ShellWindow, WindowSurface}; +use super::common::WindowSurface; +use super::shared::{SharedState, ShellShared}; +use super::ProxyAction; use kas::cast::Cast; -use kas::draw::{color::Rgba, AnimationState, DrawShared}; +use kas::draw::{color::Rgba, AnimationState}; use kas::event::{config::WindowConfig, ConfigCx, CursorIcon, EventState}; use kas::geom::{Coord, Rect, Size}; use kas::layout::SolveCache; -use kas::theme::{DrawCx, SizeCx, ThemeControl, ThemeSize}; +use kas::theme::{DrawCx, SizeCx, ThemeSize}; use kas::theme::{Theme, Window as _}; -#[cfg(all(wayland_platform, feature = "clipboard"))] -use kas::util::warn_about_error; -use kas::{Action, AppData, ErasedStack, Layout, LayoutExt, Widget, WindowId}; -use std::any::TypeId; +use kas::{autoimpl, Action, AppData, ErasedStack, Layout, LayoutExt, Widget, WindowId}; use std::mem::take; -use std::time::Instant; +use std::time::{Duration, Instant}; use winit::event::WindowEvent; use winit::event_loop::EventLoopWindowTarget; use winit::window::WindowBuilder; @@ -31,6 +29,8 @@ struct WindowData> { #[cfg(all(wayland_platform, feature = "clipboard"))] wayland_clipboard: Option, surface: S, + /// Frame rate counter + frame_count: (Instant, u32), // NOTE: cached components could be here or in Window window_id: WindowId, @@ -41,9 +41,10 @@ struct WindowData> { } /// Per-window data +#[autoimpl(Debug ignore self._data, self.widget, self.ev_state, self.window)] pub struct Window> { _data: std::marker::PhantomData, - widget: kas::Window, + pub(super) widget: kas::Window, pub(super) window_id: WindowId, ev_state: EventState, window: Option>, @@ -53,7 +54,7 @@ pub struct Window> { impl> Window { /// Construct window state (widget) pub(super) fn new( - shared: &SharedState, + shared: &ShellShared, window_id: WindowId, widget: kas::Window, ) -> Self { @@ -62,7 +63,7 @@ impl> Window { _data: std::marker::PhantomData, widget, window_id, - ev_state: EventState::new(config), + ev_state: EventState::new(config, shared.platform), window: None, } } @@ -90,7 +91,6 @@ impl> Window { self.ev_state.update_config(scale_factor, dpem); self.ev_state.full_configure( theme_window.size(), - &mut shared.shell.draw, self.window_id, &mut self.widget, &shared.data, @@ -169,6 +169,7 @@ impl> Window { #[cfg(all(wayland_platform, feature = "clipboard"))] wayland_clipboard, surface, + frame_count: (Instant::now(), 0), window_id: self.window_id, solve_cache, @@ -190,12 +191,18 @@ impl> Window { } /// Handle an event - pub(super) fn handle_event(&mut self, shared: &mut SharedState, event: WindowEvent) { + /// + /// Returns `true` to force polling temporarily. + pub(super) fn handle_event( + &mut self, + shared: &mut SharedState, + event: WindowEvent, + ) -> bool { let Some(ref mut window) = self.window else { - return; + return false; }; match event { - WindowEvent::Destroyed => (), + WindowEvent::Moved(_) | WindowEvent::Destroyed => false, WindowEvent::Resized(size) => { // TODO: maybe enqueue to allow skipping of obsolete resizes if window @@ -204,6 +211,7 @@ impl> Window { { self.apply_size(shared, false); } + false } WindowEvent::ScaleFactorChanged { scale_factor, .. } => { // Note: API allows us to set new window size here. @@ -216,13 +224,15 @@ impl> Window { let dpem = window.theme_window.size().dpem(); self.ev_state.update_config(scale_factor, dpem); window.solve_cache.invalidate_rule_cache(); + false } + WindowEvent::RedrawRequested => self.do_draw(shared).is_err(), event => { - let mut tkw = TkWindow::new(&mut shared.shell, window); let mut messages = ErasedStack::new(); - self.ev_state.with(&mut tkw, &mut messages, |cx| { - cx.handle_winit(&shared.data, &mut self.widget, event); - }); + self.ev_state + .with(&mut shared.shell, window, &mut messages, |cx| { + cx.handle_winit(&shared.data, &mut self.widget, event); + }); shared.handle_messages(&mut messages); if self.ev_state.action.contains(Action::RECONFIGURE) { @@ -230,6 +240,7 @@ impl> Window { self.reconfigure(shared); self.ev_state.action.remove(Action::RECONFIGURE); } + false } } } @@ -242,11 +253,14 @@ impl> Window { let Some(ref window) = self.window else { return (Action::empty(), None); }; - let mut tkw = TkWindow::new(&mut shared.shell, window); let mut messages = ErasedStack::new(); - let action = - self.ev_state - .flush_pending(&mut tkw, &mut messages, &mut self.widget, &shared.data); + let action = self.ev_state.flush_pending( + &mut shared.shell, + window, + &mut messages, + &mut self.widget, + &shared.data, + ); shared.handle_messages(&mut messages); if action.contains(Action::CLOSE | Action::EXIT) { @@ -301,11 +315,6 @@ impl> Window { } else if action.contains(Action::SET_RECT) { self.apply_size(shared, false); } - /*if action.contains(Action::Popup) { - let widget = &mut self.widget; - self.ev_state.with(&mut tkw, |cx| widget.resize_popups(cx)); - self.ev_state.region_moved(&mut *self.widget); - } else*/ if action.contains(Action::REGION_MOVED) { self.ev_state.region_moved(&mut self.widget, &shared.data); } @@ -320,11 +329,12 @@ impl> Window { let Some(ref window) = self.window else { return None; }; - let mut tkw = TkWindow::new(&mut shared.shell, window); let widget = self.widget.as_node(&shared.data); let mut messages = ErasedStack::new(); self.ev_state - .with(&mut tkw, &mut messages, |cx| cx.update_timer(widget)); + .with(&mut shared.shell, window, &mut messages, |cx| { + cx.update_timer(widget) + }); shared.handle_messages(&mut messages); self.next_resume() } @@ -338,11 +348,11 @@ impl> Window { let Some(ref window) = self.window else { return; }; - let mut tkw = TkWindow::new(&mut shared.shell, window); let mut messages = ErasedStack::new(); - self.ev_state.with(&mut tkw, &mut messages, |cx| { - self.widget.add_popup(cx, &shared.data, id, popup) - }); + self.ev_state + .with(&mut shared.shell, window, &mut messages, |cx| { + self.widget.add_popup(cx, &shared.data, id, popup) + }); shared.handle_messages(&mut messages); } @@ -354,11 +364,12 @@ impl> Window { if id == self.window_id { self.ev_state.send_action(Action::CLOSE); } else if let Some(window) = self.window.as_ref() { - let mut tkw = TkWindow::new(&mut shared.shell, window); let widget = &mut self.widget; let mut messages = ErasedStack::new(); self.ev_state - .with(&mut tkw, &mut messages, |cx| widget.remove_popup(cx, id)); + .with(&mut shared.shell, window, &mut messages, |cx| { + widget.remove_popup(cx, id) + }); shared.handle_messages(&mut messages); } } @@ -374,7 +385,6 @@ impl> Window { self.ev_state.full_configure( window.theme_window.size(), - &mut shared.shell.draw, self.window_id, &mut self.widget, &shared.data, @@ -390,7 +400,7 @@ impl> Window { }; let size = window.theme_window.size(); - let mut cx = ConfigCx::new(&size, &mut shared.shell.draw, &mut self.ev_state); + let mut cx = ConfigCx::new(&size, &mut self.ev_state); cx.update(self.widget.as_node(&shared.data)); log::trace!(target: "kas_perf::wgpu::window", "update: {}µs", time.elapsed().as_micros()); @@ -405,11 +415,7 @@ impl> Window { log::debug!("apply_size: rect={rect:?}"); let solve_cache = &mut window.solve_cache; - let mut cx = ConfigCx::new( - window.theme_window.size(), - &mut shared.shell.draw, - &mut self.ev_state, - ); + let mut cx = ConfigCx::new(window.theme_window.size(), &mut self.ev_state); solve_cache.apply_rect(self.widget.as_node(&shared.data), &mut cx, rect, true); if first { solve_cache.print_widget_heirarchy(self.widget.as_layout()); @@ -489,6 +495,20 @@ impl> Window { text_dur_micros.as_micros(), (end - time2).as_micros() ); + + const SECOND: Duration = Duration::from_secs(1); + window.frame_count.1 += 1; + let now = Instant::now(); + if window.frame_count.0 + SECOND <= now { + log::debug!( + "Window {:?}: {} frames in last second", + window.window_id, + window.frame_count.1 + ); + window.frame_count.0 = now; + window.frame_count.1 = 0; + } + Ok(()) } @@ -504,135 +524,49 @@ impl> Window { } } -struct TkWindow<'a, A: AppData, S: WindowSurface, T: Theme> { - shared: &'a mut ShellShared, - window: &'a WindowData, -} - -impl<'a, A: AppData, S: WindowSurface, T: Theme> TkWindow<'a, A, S, T> { - fn new(shared: &'a mut ShellShared, window: &'a WindowData) -> Self { - TkWindow { shared, window } - } -} - -impl<'a, A: AppData, S: WindowSurface, T: Theme> ShellWindow for TkWindow<'a, A, S, T> { - fn add_popup(&mut self, popup: kas::PopupDescriptor) -> WindowId { - let parent_id = self.window.window_id; - let id = self.shared.next_window_id(); - self.shared - .pending - .push(PendingAction::AddPopup(parent_id, id, popup)); - id - } - - unsafe fn add_window(&mut self, window: kas::Window<()>, data_type_id: TypeId) -> WindowId { - // Safety: the window should be `Window`. We cast to that. - if data_type_id != TypeId::of::() { - // If this fails it is not safe to add the window (though we could just return). - panic!("add_window: window has wrong Data type!"); - } - let window: kas::Window = std::mem::transmute(window); - - // By far the simplest way to implement this is to let our call - // anscestor, event::Loop::handle, do the work. - // - // In theory we could pass the EventLoopWindowTarget for *each* event - // handled to create the winit window here or use statics to generate - // errors now, but user code can't do much with this error anyway. - let id = self.shared.next_window_id(); - self.shared - .pending - .push(PendingAction::AddWindow(id, window)); - id - } - - fn close_window(&mut self, id: WindowId) { - self.shared.pending.push(PendingAction::CloseWindow(id)); - } +pub(crate) trait WindowDataErased { + /// Get the window identifier + fn window_id(&self) -> WindowId; - #[inline] - fn get_clipboard(&mut self) -> Option { - #[cfg(all(wayland_platform, feature = "clipboard"))] - if let Some(cb) = self.window.wayland_clipboard.as_ref() { - return match cb.load() { - Ok(s) => Some(s), - Err(e) => { - warn_about_error("Failed to get clipboard contents", &e); - None - } - }; - } - - self.shared.get_clipboard() - } - - #[inline] - fn set_clipboard<'c>(&mut self, content: String) { - #[cfg(all(wayland_platform, feature = "clipboard"))] - if let Some(cb) = self.window.wayland_clipboard.as_ref() { - cb.store(content); - return; - } - - self.shared.set_clipboard(content); - } + /// Access the wayland clipboard object, if available + #[cfg(all(wayland_platform, feature = "clipboard"))] + fn wayland_clipboard(&self) -> Option<&smithay_clipboard::Clipboard>; - #[inline] - fn get_primary(&mut self) -> Option { - #[cfg(all(wayland_platform, feature = "clipboard"))] - if let Some(cb) = self.window.wayland_clipboard.as_ref() { - return match cb.load_primary() { - Ok(s) => Some(s), - Err(e) => { - warn_about_error("Failed to get clipboard contents", &e); - None - } - }; - } + /// Access the [`ThemeSize`] object + fn theme_size(&self) -> &dyn ThemeSize; - self.shared.get_primary() - } + /// Set the mouse cursor + fn set_cursor_icon(&self, icon: CursorIcon); - #[inline] - fn set_primary<'c>(&mut self, content: String) { - #[cfg(all(wayland_platform, feature = "clipboard"))] - if let Some(cb) = self.window.wayland_clipboard.as_ref() { - cb.store_primary(content); - return; - } + /// Directly access Winit Window + /// + /// This is a temporary API, allowing e.g. to minimize the window. + #[cfg(winit)] + fn winit_window(&self) -> Option<&winit::window::Window>; +} - self.shared.set_primary(content); +impl> WindowDataErased for WindowData { + fn window_id(&self) -> WindowId { + self.window_id } - fn adjust_theme<'s>(&'s mut self, f: Box Action + 's>) { - let action = f(&mut self.shared.theme); - self.shared.pending.push(PendingAction::Action(action)); + #[cfg(all(wayland_platform, feature = "clipboard"))] + fn wayland_clipboard(&self) -> Option<&smithay_clipboard::Clipboard> { + self.wayland_clipboard.as_ref() } - fn size_and_draw_shared<'s>( - &'s mut self, - f: Box, - ) { - f(self.window.theme_window.size(), &mut self.shared.draw); + fn theme_size(&self) -> &dyn ThemeSize { + self.theme_window.size() } #[inline] - fn set_cursor_icon(&mut self, icon: CursorIcon) { + fn set_cursor_icon(&self, icon: CursorIcon) { self.window.set_cursor_icon(icon); } - fn platform(&self) -> Platform { - self.shared.platform - } - #[cfg(winit)] #[inline] fn winit_window(&self) -> Option<&winit::window::Window> { - Some(self.window) - } - - #[inline] - fn waker(&self) -> &std::task::Waker { - &self.shared.waker + Some(&self.window) } } diff --git a/crates/kas-core/src/theme/draw.rs b/crates/kas-core/src/theme/draw.rs index 74a764ef5..b4bf1d208 100644 --- a/crates/kas-core/src/theme/draw.rs +++ b/crates/kas-core/src/theme/draw.rs @@ -99,8 +99,8 @@ impl<'a> DrawCx<'a> { /// Access a [`ConfigCx`] pub fn config_cx T, T>(&mut self, f: F) -> T { - let (sh, draw, ev) = self.h.components(); - let mut cx = ConfigCx::new(sh, draw.shared(), ev); + let (sh, _, ev) = self.h.components(); + let mut cx = ConfigCx::new(sh, ev); f(&mut cx) } diff --git a/crates/kas-macros/src/lib.rs b/crates/kas-macros/src/lib.rs index 308ce6e37..00ed9c146 100644 --- a/crates/kas-macros/src/lib.rs +++ b/crates/kas-macros/src/lib.rs @@ -187,7 +187,7 @@ pub fn impl_scope(input: TokenStream) -> TokenStream { /// - hover_highlight = bool — if true, generate /// `Events::handle_hover` to request a redraw on focus gained/lost /// - cursor_icon = expr — if used, generate -/// `Event::handle_hover`, calling `cx.set_cursor_icon(expr)` +/// `Event::handle_hover`, calling `cx.set_hover_cursor(expr)` /// - layout = layout — defines widget layout via an /// expression; [see below for documentation](#layout) /// diff --git a/crates/kas-macros/src/widget.rs b/crates/kas-macros/src/widget.rs index 57ffc07e8..72f9f6af5 100644 --- a/crates/kas-macros/src/widget.rs +++ b/crates/kas-macros/src/widget.rs @@ -778,7 +778,7 @@ pub fn widget(attr_span: Span, mut args: WidgetArgs, scope: &mut Scope) -> Resul #[inline] fn handle_hover(&mut self, cx: &mut EventCx, state: bool) -> ::kas::event::IsUsed { if state { - cx.set_cursor_icon(#icon_expr); + cx.set_hover_cursor(#icon_expr); } ::kas::event::Used } @@ -788,7 +788,7 @@ pub fn widget(attr_span: Span, mut args: WidgetArgs, scope: &mut Scope) -> Resul fn handle_hover(&mut self, cx: &mut EventCx, state: bool) -> ::kas::event::IsUsed { cx.redraw(self.id()); if state { - cx.set_cursor_icon(#icon_expr); + cx.set_hover_cursor(#icon_expr); } ::kas::event::Used } diff --git a/crates/kas-resvg/src/canvas.rs b/crates/kas-resvg/src/canvas.rs index 4fbdd0cf6..1b0a54cf3 100644 --- a/crates/kas-resvg/src/canvas.rs +++ b/crates/kas-resvg/src/canvas.rs @@ -191,24 +191,23 @@ impl_scope! { if let Some((program, mut pixmap)) = cx.try_pop::<(P, Pixmap)>() { debug_assert!(matches!(self.inner, State::Rendering)); let size = (pixmap.width(), pixmap.height()); + let ds = cx.draw_shared(); - cx.draw_shared(|ds| { - if let Some(im_size) = self.image.as_ref().and_then(|h| ds.image_size(h)) { - if im_size != Size::conv(size) { - if let Some(handle) = self.image.take() { - ds.image_free(handle); - } + if let Some(im_size) = self.image.as_ref().and_then(|h| ds.image_size(h)) { + if im_size != Size::conv(size) { + if let Some(handle) = self.image.take() { + ds.image_free(handle); } } + } - if self.image.is_none() { - self.image = ds.image_alloc(size).ok(); - } + if self.image.is_none() { + self.image = ds.image_alloc(size).ok(); + } - if let Some(handle) = self.image.as_ref() { - ds.image_upload(handle, pixmap.data(), ImageFormat::Rgba8); - } - }); + if let Some(handle) = self.image.as_ref() { + ds.image_upload(handle, pixmap.data(), ImageFormat::Rgba8); + } cx.redraw(self.id()); diff --git a/crates/kas-resvg/src/svg.rs b/crates/kas-resvg/src/svg.rs index 7f063c918..3f8e1c36b 100644 --- a/crates/kas-resvg/src/svg.rs +++ b/crates/kas-resvg/src/svg.rs @@ -259,23 +259,23 @@ impl_scope! { fn handle_messages(&mut self, cx: &mut EventCx, _: &Self::Data) { if let Some(pixmap) = cx.try_pop::() { let size = (pixmap.width(), pixmap.height()); - cx.draw_shared(|ds| { - if let Some(im_size) = self.image.as_ref().and_then(|h| ds.image_size(h)) { - if im_size != Size::conv(size) { - if let Some(handle) = self.image.take() { - ds.image_free(handle); - } + let ds = cx.draw_shared(); + + if let Some(im_size) = self.image.as_ref().and_then(|h| ds.image_size(h)) { + if im_size != Size::conv(size) { + if let Some(handle) = self.image.take() { + ds.image_free(handle); } } + } - if self.image.is_none() { - self.image = ds.image_alloc(size).ok(); - } + if self.image.is_none() { + self.image = ds.image_alloc(size).ok(); + } - if let Some(handle) = self.image.as_ref() { - ds.image_upload(handle, pixmap.data(), ImageFormat::Rgba8); - } - }); + if let Some(handle) = self.image.as_ref() { + ds.image_upload(handle, pixmap.data(), ImageFormat::Rgba8); + } cx.redraw(self.id()); let inner = std::mem::replace(&mut self.inner, State::None); diff --git a/crates/kas-view/src/filter/filter_list.rs b/crates/kas-view/src/filter/filter_list.rs index aa356895c..b215aa326 100644 --- a/crates/kas-view/src/filter/filter_list.rs +++ b/crates/kas-view/src/filter/filter_list.rs @@ -91,7 +91,7 @@ impl_scope! { impl Events for Self { fn handle_messages(&mut self, cx: &mut EventCx, data: &A) { if let Some(SetFilter(value)) = cx.try_pop() { - cx.config_cx(|cx| self.list.set_filter(cx, data, value)); + self.list.set_filter(&mut cx.config_cx(), data, value); } } } @@ -163,7 +163,7 @@ impl_scope! { fn handle_messages(&mut self, cx: &mut EventCx, data: &A) { if let Some(SetFilter(value)) = cx.try_pop() { - cx.config_cx(|cx| self.set_filter(cx, data, value)); + self.set_filter(&mut cx.config_cx(), data, value); } } } diff --git a/crates/kas-view/src/list_view.rs b/crates/kas-view/src/list_view.rs index 68cbe6ec9..2e6a93d03 100644 --- a/crates/kas-view/src/list_view.rs +++ b/crates/kas-view/src/list_view.rs @@ -659,7 +659,7 @@ impl_scope! { return if let Some(i_data) = data_index { // Set nav focus to i_data and update scroll position if self.scroll.focus_rect(cx, solver.rect(i_data), self.core.rect) { - cx.config_cx(|cx| self.update_widgets(cx, data)); + self.update_widgets(&mut cx.config_cx(), data); } let index = i_data % usize::conv(self.cur_len); cx.next_nav_focus(self.widgets[index].widget.id(), false, FocusSource::Key); @@ -704,7 +704,7 @@ impl_scope! { .scroll .scroll_by_event(cx, event, self.id(), self.core.rect); if moved { - cx.config_cx(|cx| self.update_widgets(cx, data)); + self.update_widgets(&mut cx.config_cx(), data); } is_used | used_by_sber } @@ -751,7 +751,7 @@ impl_scope! { fn handle_scroll(&mut self, cx: &mut EventCx, data: &A, scroll: Scroll) { self.scroll.scroll(cx, self.rect(), scroll); - cx.config_cx(|cx| self.update_widgets(cx, data)); + self.update_widgets(&mut cx.config_cx(), data); } } @@ -852,7 +852,7 @@ impl_scope! { }; if self.scroll.focus_rect(cx, solver.rect(data_index), self.core.rect) { - cx.config_cx(|cx| self.update_widgets(cx, data)); + self.update_widgets(&mut cx.config_cx(), data); } let index = data_index % usize::conv(self.cur_len); diff --git a/crates/kas-view/src/matrix_view.rs b/crates/kas-view/src/matrix_view.rs index 427c9d7a1..2dcfab15d 100644 --- a/crates/kas-view/src/matrix_view.rs +++ b/crates/kas-view/src/matrix_view.rs @@ -608,7 +608,7 @@ impl_scope! { return if let Some((ci, ri)) = data_index { // Set nav focus and update scroll position if self.scroll.focus_rect(cx, solver.rect(ci, ri), self.core.rect) { - solver = cx.config_cx(|cx| self.update_widgets(cx, data)); + solver = self.update_widgets(&mut cx.config_cx(), data); } let index = solver.data_to_child(ci, ri); @@ -671,7 +671,7 @@ impl_scope! { .scroll .scroll_by_event(cx, event, self.id(), self.core.rect); if moved { - cx.config_cx(|cx| self.update_widgets(cx, data)); + self.update_widgets(&mut cx.config_cx(), data); } is_used | used_by_sber } @@ -718,7 +718,7 @@ impl_scope! { fn handle_scroll(&mut self, cx: &mut EventCx, data: &A, scroll: Scroll) { self.scroll.scroll(cx, self.rect(), scroll); - cx.config_cx(|cx| self.update_widgets(cx, data)); + self.update_widgets(&mut cx.config_cx(), data); } } @@ -829,7 +829,7 @@ impl_scope! { }; if self.scroll.focus_rect(cx, solver.rect(ci, ri), self.core.rect) { - solver = cx.config_cx(|cx| self.update_widgets(cx, data)); + solver = self.update_widgets(&mut cx.config_cx(), data); } let index = solver.data_to_child(ci, ri); diff --git a/crates/kas-widgets/src/spinner.rs b/crates/kas-widgets/src/spinner.rs index 8206eb9b3..948eb9a83 100644 --- a/crates/kas-widgets/src/spinner.rs +++ b/crates/kas-widgets/src/spinner.rs @@ -112,7 +112,7 @@ impl SpinnerGuard { /// Returns new value if different fn handle_btn(&self, cx: &mut EventCx, data: &A, btn: SpinBtn) -> Option { - let old_value = cx.config_cx(|cx| (self.state_fn)(cx, data)); + let old_value = (self.state_fn)(&cx.config_cx(), data); let value = match btn { SpinBtn::Down => old_value.sub_step(self.step, self.start), SpinBtn::Up => old_value.add_step(self.step, self.end), @@ -134,7 +134,7 @@ impl EditGuard for SpinnerGuard { if let Some(value) = edit.guard.parsed.take() { cx.push(ValueMsg(value)); } else { - let value = cx.config_cx(|cx| (edit.guard.state_fn)(cx, data)); + let value = (edit.guard.state_fn)(&cx.config_cx(), data); *cx |= edit.set_string(value.to_string()); } } diff --git a/crates/kas-widgets/src/splitter.rs b/crates/kas-widgets/src/splitter.rs index 900d116c0..120f253c6 100644 --- a/crates/kas-widgets/src/splitter.rs +++ b/crates/kas-widgets/src/splitter.rs @@ -260,7 +260,7 @@ impl_scope! { let n = index >> 1; assert!(n < self.handles.len()); *cx |= self.handles[n].set_offset(offset).1; - cx.config_cx(|cx| self.adjust_size(cx, n)); + self.adjust_size(&mut cx.config_cx(), n); } } } diff --git a/crates/kas-widgets/src/tab_stack.rs b/crates/kas-widgets/src/tab_stack.rs index 25dfceb3e..f63164d27 100644 --- a/crates/kas-widgets/src/tab_stack.rs +++ b/crates/kas-widgets/src/tab_stack.rs @@ -193,7 +193,7 @@ impl_scope! { fn handle_messages(&mut self, cx: &mut EventCx, data: &W::Data) { if let Some(MsgSelectIndex(index)) = cx.try_pop() { - cx.config_cx(|cx| self.set_active(cx, data, index)); + self.set_active(&mut cx.config_cx(), data, index); if let Some(ref f) = self.on_change { let title = self.tabs[index].get_str(); f(cx, data, index, title);