From de65c1dc7285b15c87862198b98b435345715d7d Mon Sep 17 00:00:00 2001 From: alex-ds13 <145657253+alex-ds13@users.noreply.github.com> Date: Thu, 4 Dec 2025 02:40:12 +0000 Subject: feat(selectable): draft implementation of selectable widget --- src/selectable.rs | 243 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 243 insertions(+) create mode 100644 src/selectable.rs (limited to 'src/selectable.rs') diff --git a/src/selectable.rs b/src/selectable.rs new file mode 100644 index 0000000..217f913 --- /dev/null +++ b/src/selectable.rs @@ -0,0 +1,243 @@ +//! A [`Selectable`] widget that wraps some elements (preferrably all your apps elements) and makes +//! all its children instances of [`Text`] and [`Rich`] became globally selectable instead of +//! independently selectable. This widget also handles the copy shortcut ('ctrl + c') by checking +//! for all selections contained inside it and writing to the [`Standard`] clipboard when there is +//! some selection. +//! +//! +//! [`Text`]: crate::Text +//! [`Rich`]: crate::text::Rich +//! [`Standard`]: crate::core::clipboard::Kind::Standard + +use crate::core::{ + self, Element, Event, Length, Rectangle, Shell, Size, Vector, Widget, + clipboard::{self, Clipboard}, + keyboard, + layout::{Layout, Limits, Node}, + mouse, overlay, + renderer::Style, + widget::{Tree, operation::Operation, tree}, +}; + +/// A [`Selectable`] widget that wraps some elements (preferrably all your apps elements) and makes +/// all its children instances of [`Text`] and [`Rich`] became globally selectable instead of +/// independently selectable. This widget also handles the copy shortcut ('ctrl + c') by checking +/// for all selections contained inside it and writing to the [`Standard`] clipboard when there is +/// some selection. +/// +/// +/// [`Text`]: crate::Text +/// [`Rich`]: crate::text::Rich +/// [`Standard`]: crate::core::clipboard::Kind::Standard +pub struct Selectable<'a, Message, Theme, Renderer> { + content: Element<'a, Message, Theme, Renderer>, +} + +impl<'a, Message, Theme, Renderer> Selectable<'a, Message, Theme, Renderer> { + /// Creates a new [`Selectable`] with the given content. + pub fn new( + content: impl Into>, + ) -> Self { + Self { + content: content.into(), + } + } +} + +/// The internal state of a [`Selectable`] widget. +#[derive(Debug, Default, Clone)] +struct State { + keyboard_modifiers: keyboard::Modifiers, +} + +impl<'a, Message, Theme, Renderer> Widget + for Selectable<'a, Message, Theme, Renderer> +where + Renderer: core::Renderer, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + + fn state(&self) -> tree::State { + tree::State::new(State::default()) + } + + fn children(&self) -> Vec { + vec![Tree::new(&self.content)] + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children(std::slice::from_ref(&self.content)); + } + + fn size(&self) -> Size { + self.content.as_widget().size() + } + + fn layout( + &mut self, + tree: &mut Tree, + renderer: &Renderer, + limits: &Limits, + ) -> Node { + let layout = self.content.as_widget_mut().layout( + &mut tree.children[0], + renderer, + limits, + ); + self.content.as_widget_mut().operate( + &mut tree.children[0], + Layout::new(&layout), + renderer, + &mut crate::operation::global_selection(), + ); + + layout + } + + fn operate( + &mut self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn Operation, + ) { + operation.traverse(&mut |operation| { + self.content.as_widget_mut().operate( + &mut tree.children[0], + layout, + renderer, + operation, + ); + }); + } + + fn update( + &mut self, + tree: &mut Tree, + event: &Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) { + self.content.as_widget_mut().update( + &mut tree.children[0], + event, + layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ); + + if shell.is_event_captured() { + return; + } + + let state = tree.state.downcast_mut::(); + + if let Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) = event + && let keyboard::Key::Character("c") = key.as_ref() + && state.keyboard_modifiers.command() + { + let mut selected = crate::operation::selected(); + + selected.traverse(&mut |operation| { + self.content.as_widget_mut().operate( + &mut tree.children[0], + layout, + renderer, + &mut core::widget::operation::black_box(operation), + ); + }); + + if let core::widget::operation::Outcome::Some(selection) = + selected.finish() + { + clipboard.write(clipboard::Kind::Standard, selection); + + shell.capture_event(); + } + } else if let Event::Keyboard(keyboard::Event::ModifiersChanged( + modifiers, + )) = event + { + state.keyboard_modifiers = *modifiers; + } + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.content.as_widget().mouse_interaction( + &tree.children[0], + layout, + cursor, + viewport, + renderer, + ) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Theme, + style: &Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + style, + layout, + cursor, + viewport, + ); + } + + fn overlay<'b>( + &'b mut self, + state: &'b mut Tree, + layout: Layout<'b>, + renderer: &Renderer, + viewport: &Rectangle, + translation: Vector, + ) -> Option> { + self.content.as_widget_mut().overlay( + &mut state.children[0], + layout, + renderer, + viewport, + translation, + ) + } +} + +impl<'a, Message, Theme, Renderer> + From> + for Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Theme: 'a, + Renderer: core::Renderer + 'a, +{ + fn from( + selectable: Selectable<'a, Message, Theme, Renderer>, + ) -> Element<'a, Message, Theme, Renderer> { + Element::new(selectable) + } +} -- cgit v1.2.3