From 3af29a19d8e1a9a2b3cb39d40505f47ead038865 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Mon, 2 Mar 2026 12:17:25 +0000 Subject: [PATCH 01/10] Update impl-tools-lib: add #[split_impl] --- Cargo.toml | 4 +++ crates/kas-core/src/lib.rs | 2 +- crates/kas-macros/src/lib.rs | 58 ++++++++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 952cb3a68..42ffc9adc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -173,3 +173,7 @@ resolver = "2" [patch.crates-io.kas-text] git = "https://github.com/kas-gui/kas-text.git" rev = "a087ae3c083fb8e7f73282514b556ecc9083c95a" + +[patch.crates-io.impl-tools-lib] +git = "https://github.com/kas-gui/impl-tools.git" +rev = "e0b80cf7f2b868b1ace4ec06601a5ed4cf098686" diff --git a/crates/kas-core/src/lib.rs b/crates/kas-core/src/lib.rs index dd1e913d5..4f969404e 100644 --- a/crates/kas-core/src/lib.rs +++ b/crates/kas-core/src/lib.rs @@ -31,7 +31,7 @@ pub use crate::core::*; pub(crate) use action::{ActionClose, WindowActions}; pub use action::{ActionMoved, ActionRedraw, ActionResize, ConfigAction}; pub use kas_macros::{autoimpl, extends, impl_default}; -pub use kas_macros::{cell_collection, collection, impl_anon, impl_scope, impl_self}; +pub use kas_macros::{cell_collection, collection, impl_anon, impl_scope, impl_self, split_impl}; pub use kas_macros::{layout, widget, widget_index}; // public implementations: diff --git a/crates/kas-macros/src/lib.rs b/crates/kas-macros/src/lib.rs index 385e6dd42..9e43f86fc 100644 --- a/crates/kas-macros/src/lib.rs +++ b/crates/kas-macros/src/lib.rs @@ -95,6 +95,64 @@ pub fn autoimpl(attr: TokenStream, item: TokenStream) -> TokenStream { toks } +/// Support simultaneous definition of a trait and its implementation +/// +/// Sometimes a trait is used only (or primarily) to provide an interface over a +/// single object. In such cases, writing out the method prototypes twice (in +/// both the trait and its implementation) should be unnecessary. +/// +/// This attribute splits an implementation for a specified target off from a +/// trait definition. +/// +/// # Details +/// +/// This attribute must be applied to a trait definition. This trait definition +/// is retained with only modifications being that method *implementations* and +/// `#[inline]` attributes are removed. An implied limitation of this attribute +/// is that methods of the trait cannot have default implementations. +/// +/// Meanwhile, an implementation of the trait is generated for the given target. +/// This implementation uses all method implementations from the trait. +/// +/// Generics may be introduced for the implementation only using syntax like +/// `#[split_impl(for<'a, T> Target<'a, T>)]`. Implementation generics are +/// combined with trait generics. +/// +/// Specific attributes of the trait are copied to the implementation: `cfg`, +/// `allow`, `warn`, `deny`, `forbid`. All trait method attributes except `doc` +/// are copied to the implementation. +/// +/// # Example +/// +/// ``` +/// # use kas_macros::split_impl; +/// #[split_impl(for str)] +/// trait Greet { +/// /// Introduce yourself +/// fn greet(&self) { +/// println!("Hello world, I am {self}!"); +/// } +/// } +/// +/// fn main() { +/// "Ferris".greet(); +/// } +/// ``` +#[proc_macro_attribute] +#[proc_macro_error] +pub fn split_impl(attr: TokenStream, item: TokenStream) -> TokenStream { + match syn::parse::(attr) { + Ok(attr) => match syn::parse::(item) { + Ok(trait_) => attr.process(trait_).into(), + Err(err) => err.to_compile_error().into(), + }, + Err(err) => { + emit_call_site_error!(err); + item + } + } +} + const IMPL_SCOPE_RULES: [&dyn scope::ScopeAttr; 3] = [ &scope::AttrImplDefault, &widget_args::AttrImplWidget, From 02462b33e7cdf0cff882cee5f500f1b7fcc35404 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Mon, 2 Mar 2026 12:17:53 +0000 Subject: [PATCH 02/10] Use #[split_impl] on NodeT --- crates/kas-core/src/core/node.rs | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/crates/kas-core/src/core/node.rs b/crates/kas-core/src/core/node.rs index 7bbd51462..486232228 100644 --- a/crates/kas-core/src/core/node.rs +++ b/crates/kas-core/src/core/node.rs @@ -14,27 +14,8 @@ use crate::util::IdentifyWidget; use crate::{Id, Tile}; #[cfg(not(feature = "unsafe_node"))] +#[crate::split_impl(for<'a, T> (&'a mut dyn Widget, &'a T))] trait NodeT { - fn id_ref(&self) -> &Id; - fn rect(&self) -> Rect; - - fn clone_node(&mut self) -> Node<'_>; - fn as_tile(&self) -> &dyn Tile; - - fn find_child_index(&self, id: &Id) -> Option; - fn child_node(&mut self, index: usize) -> Option>; - - fn size_rules(&mut self, cx: &mut SizeCx, axis: AxisInfo) -> SizeRules; - fn set_rect(&mut self, cx: &mut SizeCx, rect: Rect, hints: AlignHints); - - fn _configure(&mut self, cx: &mut ConfigCx, id: Id); - fn _update(&mut self, cx: &mut ConfigCx); - - fn _send(&mut self, cx: &mut EventCx, id: Id, event: Event) -> IsUsed; - fn _replay(&mut self, cx: &mut EventCx, id: Id); -} -#[cfg(not(feature = "unsafe_node"))] -impl<'a, T> NodeT for (&'a mut dyn Widget, &'a T) { fn id_ref(&self) -> &Id { self.0.id_ref() } @@ -52,7 +33,6 @@ impl<'a, T> NodeT for (&'a mut dyn Widget, &'a T) { fn find_child_index(&self, id: &Id) -> Option { self.0.find_child_index(id) } - fn child_node(&mut self, index: usize) -> Option> { self.0.child_node(self.1, index) } From 662eaa097c43c29e10a45edeeae164cc87e6ebc8 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Mon, 2 Mar 2026 12:23:01 +0000 Subject: [PATCH 03/10] Use #[split_impl] on kas::draw::Draw --- crates/kas-core/src/draw/draw.rs | 134 +++++++++++-------------------- 1 file changed, 48 insertions(+), 86 deletions(-) diff --git a/crates/kas-core/src/draw/draw.rs b/crates/kas-core/src/draw/draw.rs index ba6704891..97d72f4f9 100644 --- a/crates/kas-core/src/draw/draw.rs +++ b/crates/kas-core/src/draw/draw.rs @@ -153,29 +153,40 @@ impl<'a, DS: DrawSharedImpl> DrawIface<'a, DS> { /// on the graphics backend. Since Rust does not (yet) support trait-object-downcast, /// accessing these requires reconstruction of the implementing type via /// [`DrawIface::downcast_from`]. +#[crate::split_impl(for<'a, DS: DrawSharedImpl> DrawIface<'a, DS>)] pub trait Draw { /// Access shared draw state - fn shared(&mut self) -> &mut dyn DrawShared; + fn shared(&mut self) -> &mut dyn DrawShared { + self.shared + } /// Request redraw at the next frame time /// /// Animations should call this each frame until complete. - fn animate(&mut self); + fn animate(&mut self) { + self.draw.animate(); + } /// Request a redraw at a specific time /// /// This may be used for animations with delays, e.g. flashing. Calling this /// method only ensures that the *next* draw happens *no later* than `time`, /// thus the method should be called again in each following frame. - fn animate_at(&mut self, time: Instant); + fn animate_at(&mut self, time: Instant) { + self.draw.animate_at(time); + } /// Get the current draw pass - fn get_pass(&self) -> PassId; + fn get_pass(&self) -> PassId { + self.pass + } /// Cast fields to [`Any`] references #[cfg_attr(not(feature = "internal_doc"), doc(hidden))] #[cfg_attr(docsrs, doc(cfg(internal_doc)))] - fn get_fields_as_any_mut(&mut self) -> (&mut dyn Any, &mut dyn Any); + fn get_fields_as_any_mut(&mut self) -> (&mut dyn Any, &mut dyn Any) { + (self.draw, self.shared) + } /// Add a draw pass /// @@ -197,7 +208,9 @@ pub trait Draw { rect: Rect, offset: Offset, class: PassType, - ) -> Box; + ) -> Box { + Box::new(self.new_pass(rect, offset, class)) + } /// Get drawable rect for a draw `pass` /// @@ -206,19 +219,25 @@ pub trait Draw { /// /// (This is not guaranteed to equal the rect passed to /// [`DrawIface::new_pass`].) - fn get_clip_rect(&self) -> Rect; + fn get_clip_rect(&self) -> Rect { + self.draw.get_clip_rect(self.pass) + } /// Draw a rectangle of uniform colour /// /// Note: where the implementation batches and/or re-orders draw calls, /// this should be one of the first items drawn such that almost anything /// else will draw "in front of" a rect. - fn rect(&mut self, rect: Quad, col: Rgba); + fn rect(&mut self, rect: Quad, col: Rgba) { + self.draw.rect(self.pass, rect, col); + } /// Draw a frame of uniform colour /// /// The frame is defined by the area inside `outer` and not inside `inner`. - fn frame(&mut self, outer: Quad, inner: Quad, col: Rgba); + fn frame(&mut self, outer: Quad, inner: Quad, col: Rgba) { + self.draw.frame(self.pass, outer, inner, col); + } /// Draw a line with uniform colour /// @@ -230,10 +249,14 @@ pub trait Draw { /// /// Note that for rectangular, axis-aligned lines, [`DrawImpl::rect`] should be /// preferred. - fn line(&mut self, p1: Vec2, p2: Vec2, width: f32, col: Rgba); + fn line(&mut self, p1: Vec2, p2: Vec2, width: f32, col: Rgba) { + self.draw.line(self.pass, p1, p2, width, col); + } /// Draw the image in the given `rect` - fn image(&mut self, id: ImageId, rect: Quad); + fn image(&mut self, id: ImageId, rect: Quad) { + self.shared.draw.draw_image(self.draw, self.pass, id, rect); + } /// Draw text with a list of color effects /// @@ -248,7 +271,11 @@ pub trait Draw { text: &TextDisplay, theme: &ColorsLinear, tokens: &[(u32, format::Colors)], - ); + ) { + self.shared + .draw + .draw_text(self.draw, self.pass, pos, bounding_box, text, theme, tokens); + } /// Draw text decorations (e.g. underlines) /// @@ -261,81 +288,16 @@ pub trait Draw { text: &TextDisplay, theme: &ColorsLinear, decorations: &[(u32, format::Decoration)], - ); -} - -impl<'a, DS: DrawSharedImpl> Draw for DrawIface<'a, DS> { - fn shared(&mut self) -> &mut dyn DrawShared { - self.shared - } - - fn animate(&mut self) { - self.draw.animate(); - } - - fn animate_at(&mut self, time: Instant) { - self.draw.animate_at(time); - } - - fn get_pass(&self) -> PassId { - self.pass - } - - fn get_fields_as_any_mut(&mut self) -> (&mut dyn Any, &mut dyn Any) { - (self.draw, self.shared) - } - - fn new_dyn_pass<'b>( - &'b mut self, - rect: Rect, - offset: Offset, - class: PassType, - ) -> Box { - Box::new(self.new_pass(rect, offset, class)) - } - - fn get_clip_rect(&self) -> Rect { - self.draw.get_clip_rect(self.pass) - } - - fn rect(&mut self, rect: Quad, col: Rgba) { - self.draw.rect(self.pass, rect, col); - } - fn frame(&mut self, outer: Quad, inner: Quad, col: Rgba) { - self.draw.frame(self.pass, outer, inner, col); - } - fn line(&mut self, p1: Vec2, p2: Vec2, width: f32, col: Rgba) { - self.draw.line(self.pass, p1, p2, width, col); - } - - fn image(&mut self, id: ImageId, rect: Quad) { - self.shared.draw.draw_image(self.draw, self.pass, id, rect); - } - - fn text( - &mut self, - pos: Vec2, - bb: Quad, - text: &TextDisplay, - theme: &ColorsLinear, - tokens: &[(u32, format::Colors)], ) { - self.shared - .draw - .draw_text(self.draw, self.pass, pos, bb, text, theme, tokens); - } - - fn decorate_text( - &mut self, - pos: Vec2, - bb: Quad, - text: &TextDisplay, - theme: &ColorsLinear, - decorations: &[(u32, format::Decoration)], - ) { - self.shared - .draw - .decorate_text(self.draw, self.pass, pos, bb, text, theme, decorations); + self.shared.draw.decorate_text( + self.draw, + self.pass, + pos, + bounding_box, + text, + theme, + decorations, + ); } } From 802d2adf1749c65ffc3589a5caecfb1d06913512 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Mon, 2 Mar 2026 12:31:10 +0000 Subject: [PATCH 04/10] Use #[split_impl] on DrawRounded --- crates/kas-core/src/draw/draw_rounded.rs | 43 ++++++++---------------- 1 file changed, 14 insertions(+), 29 deletions(-) diff --git a/crates/kas-core/src/draw/draw_rounded.rs b/crates/kas-core/src/draw/draw_rounded.rs index cb83281ad..31f86406f 100644 --- a/crates/kas-core/src/draw/draw_rounded.rs +++ b/crates/kas-core/src/draw/draw_rounded.rs @@ -12,6 +12,7 @@ use crate::geom::{Quad, Vec2}; /// Extended draw interface for [`DrawIface`] providing rounded drawing /// /// All methods draw some feature. +#[crate::split_impl(for<'a, DS: DrawSharedImpl> DrawIface<'a, DS> where DS::Draw: DrawRoundedImpl)] pub trait DrawRounded: Draw { /// Draw a line with rounded ends and uniform colour /// @@ -21,7 +22,9 @@ pub trait DrawRounded: Draw { /// /// Note that for rectangular, axis-aligned lines, [`DrawImpl::rect`] should be /// preferred. - fn rounded_line(&mut self, p1: Vec2, p2: Vec2, radius: f32, col: Rgba); + fn rounded_line(&mut self, p1: Vec2, p2: Vec2, radius: f32, col: Rgba) { + self.draw.rounded_line(self.pass, p1, p2, radius, col); + } /// Draw a circle or oval of uniform colour /// @@ -30,7 +33,9 @@ pub trait DrawRounded: Draw { /// The `inner_radius` parameter gives the inner radius relative to the /// outer radius: a value of `0.0` will result in the whole shape being /// painted, while `1.0` will result in a zero-width line on the outer edge. - fn circle(&mut self, rect: Quad, inner_radius: f32, col: Rgba); + fn circle(&mut self, rect: Quad, inner_radius: f32, col: Rgba) { + self.draw.circle(self.pass, rect, inner_radius, col); + } /// Draw a circle or oval with two colours /// @@ -41,7 +46,9 @@ pub trait DrawRounded: Draw { /// /// Note: this is drawn *before* other drawables, allowing it to be used /// for shadows without masking. - fn circle_2col(&mut self, rect: Quad, col1: Rgba, col2: Rgba); + fn circle_2col(&mut self, rect: Quad, col1: Rgba, col2: Rgba) { + self.draw.circle_2col(self.pass, rect, col1, col2); + } /// Draw a frame with rounded corners and uniform colour /// @@ -54,7 +61,10 @@ pub trait DrawRounded: Draw { /// painted, while `1.0` will result in a zero-width line on the outer edge. /// When `inner_radius > 0`, the frame will be visually thinner than the /// allocated area. - fn rounded_frame(&mut self, outer: Quad, inner: Quad, inner_radius: f32, col: Rgba); + fn rounded_frame(&mut self, outer: Quad, inner: Quad, inner_radius: f32, col: Rgba) { + self.draw + .rounded_frame(self.pass, outer, inner, inner_radius, col); + } /// Draw a frame with rounded corners with two colours /// @@ -63,31 +73,6 @@ pub trait DrawRounded: Draw { /// /// Note: this is drawn *before* other drawables, allowing it to be used /// for shadows without masking. - fn rounded_frame_2col(&mut self, outer: Quad, inner: Quad, c1: Rgba, c2: Rgba); -} - -impl<'a, DS: DrawSharedImpl> DrawRounded for DrawIface<'a, DS> -where - DS::Draw: DrawRoundedImpl, -{ - #[inline] - fn rounded_line(&mut self, p1: Vec2, p2: Vec2, radius: f32, col: Rgba) { - self.draw.rounded_line(self.pass, p1, p2, radius, col); - } - #[inline] - fn circle(&mut self, rect: Quad, inner_radius: f32, col: Rgba) { - self.draw.circle(self.pass, rect, inner_radius, col); - } - #[inline] - fn circle_2col(&mut self, rect: Quad, col1: Rgba, col2: Rgba) { - self.draw.circle_2col(self.pass, rect, col1, col2); - } - #[inline] - fn rounded_frame(&mut self, outer: Quad, inner: Quad, inner_radius: f32, col: Rgba) { - self.draw - .rounded_frame(self.pass, outer, inner, inner_radius, col); - } - #[inline] fn rounded_frame_2col(&mut self, outer: Quad, inner: Quad, c1: Rgba, c2: Rgba) { self.draw .rounded_frame_2col(self.pass, outer, inner, c1, c2); From a013ebe3b5b5c358dd9ffa9d953bfeba7fad1d19 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Mon, 2 Mar 2026 12:31:56 +0000 Subject: [PATCH 05/10] Use #[split_impl] on DrawShared --- crates/kas-core/src/draw/draw_shared.rs | 37 ++++++++----------------- 1 file changed, 12 insertions(+), 25 deletions(-) diff --git a/crates/kas-core/src/draw/draw_shared.rs b/crates/kas-core/src/draw/draw_shared.rs index ea34542cb..c0fccdca5 100644 --- a/crates/kas-core/src/draw/draw_shared.rs +++ b/crates/kas-core/src/draw/draw_shared.rs @@ -106,11 +106,17 @@ impl SharedState { /// Interface over [`SharedState`] /// /// All methods concern management of resources for drawing. +#[crate::split_impl(for SharedState)] pub trait DrawShared { /// Allocate an image /// /// Use [`SharedState::image_upload`] to set contents of the new image. - fn image_alloc(&mut self, format: ImageFormat, size: Size) -> Result; + #[inline] + fn image_alloc(&mut self, format: ImageFormat, size: Size) -> Result { + self.draw + .image_alloc(format, size) + .map(|id| ImageHandle(id, Arc::new(()))) + } /// Upload an image to the GPU /// @@ -123,30 +129,6 @@ pub trait DrawShared { /// /// On success, this returns an [`ActionRedraw`] to indicate that any /// widgets using this image will require a redraw. - fn image_upload( - &mut self, - handle: &ImageHandle, - data: &[u8], - ) -> Result; - - /// Potentially free an image - /// - /// The input `handle` is consumed. If this reduces its reference count to - /// zero, then the image is freed. - fn image_free(&mut self, handle: ImageHandle); - - /// Get the size of an image - fn image_size(&self, handle: &ImageHandle) -> Option; -} - -impl DrawShared for SharedState { - #[inline] - fn image_alloc(&mut self, format: ImageFormat, size: Size) -> Result { - self.draw - .image_alloc(format, size) - .map(|id| ImageHandle(id, Arc::new(()))) - } - #[inline] fn image_upload( &mut self, @@ -156,6 +138,10 @@ impl DrawShared for SharedState { self.draw.image_upload(handle.0, data).map(|_| ActionRedraw) } + /// Potentially free an image + /// + /// The input `handle` is consumed. If this reduces its reference count to + /// zero, then the image is freed. #[inline] fn image_free(&mut self, handle: ImageHandle) { if let Ok(()) = Arc::try_unwrap(handle.1) { @@ -163,6 +149,7 @@ impl DrawShared for SharedState { } } + /// Get the size of an image #[inline] fn image_size(&self, handle: &ImageHandle) -> Option { self.draw.image_size(handle.0) From ff76f44bac08b70344687c174316f02106868342 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Mon, 2 Mar 2026 12:34:18 +0000 Subject: [PATCH 06/10] Use #[split_impl] on ReadMessage --- crates/kas-core/src/runner/mod.rs | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/crates/kas-core/src/runner/mod.rs b/crates/kas-core/src/runner/mod.rs index c4fa84e71..bcc19eb4b 100644 --- a/crates/kas-core/src/runner/mod.rs +++ b/crates/kas-core/src/runner/mod.rs @@ -112,27 +112,16 @@ impl MessageStack { } /// This trait allows peeking and popping messages from the stack +#[crate::split_impl(for MessageStack)] pub trait ReadMessage { /// Pop a type-erased message from the stack, if non-empty - fn pop_erased(&mut self) -> Option; - - /// Try popping the last message from the stack with the given type - fn try_pop(&mut self) -> Option; - - /// Try observing the last message on the stack without popping - fn try_peek(&self) -> Option<&M>; - - /// Debug the last message on the stack, if any - fn peek_debug(&self) -> Option<&dyn Debug>; -} - -impl ReadMessage for MessageStack { #[inline] fn pop_erased(&mut self) -> Option { self.count = self.count.wrapping_add(1); self.stack.pop() } + /// Try popping the last message from the stack with the given type fn try_pop(&mut self) -> Option { if self.has_any() && self.stack.last().map(|m| m.is::()).unwrap_or(false) { self.count = self.count.wrapping_add(1); @@ -142,6 +131,7 @@ impl ReadMessage for MessageStack { } } + /// Try observing the last message on the stack without popping fn try_peek(&self) -> Option<&M> { if self.has_any() { self.stack.last().and_then(|m| m.downcast_ref::()) @@ -150,6 +140,7 @@ impl ReadMessage for MessageStack { } } + /// Debug the last message on the stack, if any fn peek_debug(&self) -> Option<&dyn Debug> { self.stack.last().map(Erased::debug) } From 5265adb20fd7c41d8462278fdc4772ff739002e9 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Mon, 2 Mar 2026 12:40:45 +0000 Subject: [PATCH 07/10] Use #[split_impl] on RunnerT --- crates/kas-core/src/runner/shared.rs | 154 +++++++++++---------------- 1 file changed, 64 insertions(+), 90 deletions(-) diff --git a/crates/kas-core/src/runner/shared.rs b/crates/kas-core/src/runner/shared.rs index 829ed3e81..636e9325b 100644 --- a/crates/kas-core/src/runner/shared.rs +++ b/crates/kas-core/src/runner/shared.rs @@ -162,9 +162,12 @@ where /// Runner shared-state type-erased interface /// /// A `dyn RunnerT` object is used by [`crate::event::EventCx`]. +#[crate::split_impl(for> Shared)] pub(crate) trait RunnerT { /// Require configuration updates - fn config_update(&mut self, action: ConfigAction); + fn config_update(&mut self, action: ConfigAction) { + self.pending.push_back(Pending::ConfigUpdate(action)); + } /// Add a pop-up /// @@ -176,91 +179,6 @@ pub(crate) trait RunnerT { /// /// 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: PopupDescriptor) -> WindowId; - - /// Resize and reposition an existing pop-up - fn reposition_popup(&mut self, id: WindowId, popup: PopupDescriptor); - - /// Add a window to the UI at run-time. - fn add_dataless_window(&mut self, window: WindowWidget<()>) -> WindowId; - - /// Add a window to the UI at run-time. - /// - /// Safety: this method *should* require generic parameter `Data` (data type - /// passed to the `Runner`). 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: WindowWidget<()>, data_type_id: TypeId) -> WindowId; - - /// Close a window - fn close_window(&mut self, id: WindowId); - - /// Exit the application - fn exit(&mut self); - - /// Access the message stack (read-only) - fn message_stack(&self) -> &MessageStack; - - /// Access the message stack (mutable) - fn message_stack_mut(&mut self) -> &mut MessageStack; - - /// Send a message to another window - fn send_erased(&mut self, id: Id, msg: Erased); - - /// Set send targets - fn set_send_targets(&mut self, targets: &mut Vec<(TypeId, Id)>); - - /// 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); - - /// Access the [`DrawShared`] object - fn draw_shared(&mut self) -> &mut dyn DrawShared; - - /// Access a Waker - fn waker(&self) -> &std::task::Waker; -} - -impl> RunnerT for Shared { - fn config_update(&mut self, action: ConfigAction) { - self.pending.push_back(Pending::ConfigUpdate(action)); - } - fn add_popup(&mut self, parent_id: WindowId, popup: PopupDescriptor) -> WindowId { let id = self.window_id_factory.make_next(); self.pending @@ -268,10 +186,12 @@ impl> RunnerT for Shared id } + /// Resize and reposition an existing pop-up fn reposition_popup(&mut self, id: WindowId, popup: PopupDescriptor) { self.pending.push_back(Pending::RepositionPopup(id, popup)); } + /// Add a window to the UI at run-time. fn add_dataless_window(&mut self, window: WindowWidget<()>) -> WindowId { let id = self.window_id_factory.make_next(); self.pending @@ -279,6 +199,14 @@ impl> RunnerT for Shared id } + /// Add a window to the UI at run-time. + /// + /// Safety: this method *should* require generic parameter `Data` (data type + /// passed to the `Runner`). 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: WindowWidget<()>, data_type_id: TypeId) -> WindowId { // Safety: the window should be `Window`. We cast to that. if data_type_id != TypeId::of::() { @@ -299,22 +227,27 @@ impl> RunnerT for Shared id } + /// Close a window fn close_window(&mut self, id: WindowId) { self.pending.push_back(Pending::CloseWindow(id)); } + /// Exit the application fn exit(&mut self) { self.pending.push_back(Pending::Exit); } + /// Access the message stack (read-only) fn message_stack(&self) -> &MessageStack { &self.messages } + /// Access the message stack (mutable) fn message_stack_mut(&mut self) -> &mut MessageStack { &mut self.messages } + /// Send a message to another window fn send_erased(&mut self, id: Id, msg: Erased) { self.send_queue.push_back((id, msg)); } @@ -326,6 +259,14 @@ impl> RunnerT for Shared } } + /// 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 { #[cfg(feature = "clipboard")] { @@ -340,16 +281,32 @@ impl> RunnerT for Shared None } - fn set_clipboard<'c>(&mut self, _content: String) { + /// 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) { + #[cfg(not(feature = "clipboard"))] + let _ = content; + #[cfg(feature = "clipboard")] if let Some(cb) = self.clipboard.as_mut() { - match cb.set_text(_content) { + match cb.set_text(content) { Ok(()) => (), Err(e) => warn_about_error("Failed to set clipboard contents", &e), } } } + /// 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 { #[cfg(all( unix, @@ -369,7 +326,22 @@ impl> RunnerT for Shared None } - fn set_primary(&mut self, _content: String) { + /// 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) { + #[cfg(any( + not(unix), + any(target_os = "macos", target_os = "android", target_os = "emscripten"), + not(feature = "clipboard"), + ))] + let _ = content; + #[cfg(all( unix, not(any(target_os = "macos", target_os = "android", target_os = "emscripten")), @@ -380,7 +352,7 @@ impl> RunnerT for Shared match cb .set() .clipboard(LinuxClipboardKind::Primary) - .text(_content) + .text(content) { Ok(()) => (), Err(e) => warn_about_error("Failed to set clipboard contents", &e), @@ -388,11 +360,13 @@ impl> RunnerT for Shared } } + /// Access the [`DrawShared`] object fn draw_shared(&mut self) -> &mut dyn DrawShared { // We can expect draw to be initialized from any context where this trait is used self.draw.as_mut().unwrap() } + /// Access a Waker #[inline] fn waker(&self) -> &std::task::Waker { &self.waker From 7e6c55a99aa15a4a8cd00cfbe4008225db729d9a Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Mon, 2 Mar 2026 12:44:36 +0000 Subject: [PATCH 08/10] Use #[split_impl] on WindowDataErased --- crates/kas-core/src/runner/window.rs | 26 +++++++------------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/crates/kas-core/src/runner/window.rs b/crates/kas-core/src/runner/window.rs index 60ebd0057..69b7718a3 100644 --- a/crates/kas-core/src/runner/window.rs +++ b/crates/kas-core/src/runner/window.rs @@ -706,45 +706,33 @@ impl> Window { } } +#[crate::split_impl(for WindowData)] pub(crate) trait WindowDataErased { /// Get the window identifier - fn window_id(&self) -> WindowId; - - /// Access the wayland clipboard object, if available - #[cfg(all(wayland_platform, feature = "clipboard"))] - fn wayland_clipboard(&self) -> Option<&smithay_clipboard::Clipboard>; - - /// Set the mouse pointer icon - fn set_pointer_icon(&self, icon: CursorIcon); - - /// Enable / update / disable the Input Method Editor - fn ime_request(&self, request: ImeRequest) -> Result<(), ImeRequestError>; - - /// Directly access Winit Window - /// - /// This is a temporary API, allowing e.g. to minimize the window. - fn winit_window(&self) -> Option<&dyn winit::window::Window>; -} - -impl WindowDataErased for WindowData { fn window_id(&self) -> WindowId { self.window_id } + /// Access the wayland clipboard object, if available #[cfg(all(wayland_platform, feature = "clipboard"))] fn wayland_clipboard(&self) -> Option<&smithay_clipboard::Clipboard> { self.wayland_clipboard.as_ref() } + /// Set the mouse pointer icon #[inline] fn set_pointer_icon(&self, icon: CursorIcon) { self.window.set_cursor(icon.into()); } + /// Enable / update / disable the Input Method Editor fn ime_request(&self, request: ImeRequest) -> Result<(), ImeRequestError> { self.window.request_ime_update(request) } + /// Directly access Winit Window + /// + /// This is a temporary API, allowing e.g. to minimize the window. #[inline] fn winit_window(&self) -> Option<&dyn winit::window::Window> { Some(&**self.window) From 9dcbc42cb166374a49a9f5f835cdb0382ae693b5 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Mon, 2 Mar 2026 12:46:20 +0000 Subject: [PATCH 09/10] Use #[split_impl] on ThemeDst --- crates/kas-core/src/theme/theme_dst.rs | 48 +++++++++----------------- 1 file changed, 16 insertions(+), 32 deletions(-) diff --git a/crates/kas-core/src/theme/theme_dst.rs b/crates/kas-core/src/theme/theme_dst.rs index a6a92e1b9..35ecff6da 100644 --- a/crates/kas-core/src/theme/theme_dst.rs +++ b/crates/kas-core/src/theme/theme_dst.rs @@ -18,62 +18,46 @@ use crate::theme::ThemeDraw; /// This trait is implemented automatically for all implementations of /// [`Theme`]. It is intended only for use where a less parameterised /// trait is required. +#[crate::split_impl(for> T)] pub trait ThemeDst { /// Theme initialisation /// /// See also [`Theme::init`]. - fn init(&mut self, config: &RefCell); + fn init(&mut self, config: &RefCell) { + self.init(config); + } /// Construct per-window storage /// /// See also [`Theme::new_window`]. - fn new_window(&mut self, config: &WindowConfig) -> Box; + fn new_window(&mut self, config: &WindowConfig) -> Box { + let window = >::new_window(self, config); + Box::new(window) + } /// Update a window created by [`Theme::new_window`] /// /// See also [`Theme::update_window`]. /// /// Returns `true` when a resize is required based on changes to the scale factor or font size. - fn update_window(&mut self, window: &mut dyn Window, config: &WindowConfig) -> bool; + fn update_window(&mut self, window: &mut dyn Window, config: &WindowConfig) -> bool { + let window = window.as_any_mut().downcast_mut().unwrap(); + self.update_window(window, config) + } fn draw<'a>( &'a self, draw: DrawIface<'a, DS>, ev: &'a mut EventState, window: &'a mut dyn Window, - ) -> Box; - - /// Background colour - /// - /// See also [`Theme::clear_color`]. - fn clear_color(&self) -> color::Rgba; -} - -impl> ThemeDst for T { - fn init(&mut self, config: &RefCell) { - self.init(config); - } - - fn new_window(&mut self, config: &WindowConfig) -> Box { - let window = >::new_window(self, config); - Box::new(window) - } - - fn update_window(&mut self, window: &mut dyn Window, config: &WindowConfig) -> bool { - let window = window.as_any_mut().downcast_mut().unwrap(); - self.update_window(window, config) - } - - fn draw<'b>( - &'b self, - draw: DrawIface<'b, DS>, - ev: &'b mut EventState, - window: &'b mut dyn Window, - ) -> Box { + ) -> Box { let window = window.as_any_mut().downcast_mut().unwrap(); Box::new(>::draw(self, draw, ev, window)) } + /// Background colour + /// + /// See also [`Theme::clear_color`]. fn clear_color(&self) -> color::Rgba { self.clear_color() } From 4d4055fe6b5ff993b0fb84ef9c987244e01b2468 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Mon, 2 Mar 2026 12:54:45 +0000 Subject: [PATCH 10/10] Use #[split_impl] on DrawShaded --- crates/kas-wgpu/src/draw_shaded.rs | 31 ++++++------------------------ 1 file changed, 6 insertions(+), 25 deletions(-) diff --git a/crates/kas-wgpu/src/draw_shaded.rs b/crates/kas-wgpu/src/draw_shaded.rs index 2078883f9..0b2695cf6 100644 --- a/crates/kas-wgpu/src/draw_shaded.rs +++ b/crates/kas-wgpu/src/draw_shaded.rs @@ -19,43 +19,23 @@ use kas::geom::Quad; /// from the closed range `[-1, 1]`, where -1 points towards the inside of the /// feature, 1 points away from the feature, and 0 is perpendicular to the /// screen towards the viewer. +#[kas::split_impl(for<'a, DS: DrawSharedImpl> DrawIface<'a, DS> where DS::Draw: DrawShadedImpl)] pub trait DrawShaded { /// Add a shaded square to the draw buffer /// /// For shading purposes, the mid-point is considered the inner edge. - fn shaded_square(&mut self, rect: Quad, norm: (f32, f32), col: Rgba); - - /// Add a shaded circle to the draw buffer - /// - /// For shading purposes, the mid-point is considered the inner edge. - fn shaded_circle(&mut self, rect: Quad, norm: (f32, f32), col: Rgba); - - /// Add a shaded frame with square corners to the draw buffer - fn shaded_square_frame( - &mut self, - outer: Quad, - inner: Quad, - norm: (f32, f32), - outer_col: Rgba, - inner_col: Rgba, - ); - - /// Add a shaded frame with rounded corners to the draw buffer - fn shaded_round_frame(&mut self, outer: Quad, inner: Quad, norm: (f32, f32), col: Rgba); -} - -impl<'a, DS: DrawSharedImpl> DrawShaded for DrawIface<'a, DS> -where - DS::Draw: DrawShadedImpl, -{ fn shaded_square(&mut self, rect: Quad, norm: (f32, f32), col: Rgba) { self.draw.shaded_square(self.pass, rect, norm, col); } + /// Add a shaded circle to the draw buffer + /// + /// For shading purposes, the mid-point is considered the inner edge. fn shaded_circle(&mut self, rect: Quad, norm: (f32, f32), col: Rgba) { self.draw.shaded_circle(self.pass, rect, norm, col); } + /// Add a shaded frame with square corners to the draw buffer fn shaded_square_frame( &mut self, outer: Quad, @@ -68,6 +48,7 @@ where .shaded_square_frame(self.pass, outer, inner, norm, outer_col, inner_col); } + /// Add a shaded frame with rounded corners to the draw buffer fn shaded_round_frame(&mut self, outer: Quad, inner: Quad, norm: (f32, f32), col: Rgba) { self.draw .shaded_round_frame(self.pass, outer, inner, norm, col);