From 8b1014a7547a6f12e732278b3f9fce77ab8dc2fa Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 14 Dec 2022 16:51:22 -0500 Subject: [PATCH] feat: rectangle tracker container --- examples/cosmic-sctk/src/window.rs | 64 ++-- src/widget/mod.rs | 7 +- src/widget/popup.rs | 314 ------------------- src/widget/rectangle_tracker/mod.rs | 244 ++++++++++++++ src/widget/rectangle_tracker/subscription.rs | 75 +++++ 5 files changed, 363 insertions(+), 341 deletions(-) delete mode 100644 src/widget/popup.rs create mode 100644 src/widget/rectangle_tracker/mod.rs create mode 100644 src/widget/rectangle_tracker/subscription.rs diff --git a/examples/cosmic-sctk/src/window.rs b/examples/cosmic-sctk/src/window.rs index ca7a8871..13b0addc 100644 --- a/examples/cosmic-sctk/src/window.rs +++ b/examples/cosmic-sctk/src/window.rs @@ -5,12 +5,14 @@ use cosmic::{ iced::widget::{ column, container, horizontal_space, pick_list, progress_bar, radio, row, slider, }, - iced::{self, Alignment, Application, Command, Length, wayland::SurfaceIdWrapper}, + iced::{self, wayland::SurfaceIdWrapper, Alignment, Application, Command, Length}, iced_lazy::responsive, theme::{self, Theme}, - widget::{button, nav_button, nav_bar, nav_bar_page, nav_bar_section, header_bar, settings, scrollable, toggler}, - Element, - ElementExt, + widget::{ + button, header_bar, nav_bar, nav_bar_page, nav_bar_section, nav_button, scrollable, + settings, toggler, rectangle_tracker::{RectangleTracker, rectangle_tracker_subscription, RectangleUpdate}, + }, + Element, ElementExt }; use std::{collections::BTreeMap, vec}; use theme::Button as ButtonTheme; @@ -29,6 +31,7 @@ pub struct Window { show_minimize: bool, show_maximize: bool, exit: bool, + rectangle_tracker: Option>, } impl Window { @@ -49,7 +52,7 @@ impl Window { } #[allow(dead_code)] -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Debug)] pub enum Message { Page(u8), Debug(bool), @@ -66,6 +69,7 @@ pub enum Message { Minimize, Maximize, InputChanged, + Rectangle(RectangleUpdate) } impl Application for Window { @@ -99,7 +103,7 @@ impl Application for Window { Message::SliderChanged(value) => self.slider_value = value, Message::CheckboxToggled(value) => { self.checkbox_value = value; - }, + } Message::TogglerToggled(value) => self.toggler_value = value, Message::PickListSelected(value) => self.pick_list_selected = Some(value), Message::Close => self.exit = true, @@ -108,8 +112,11 @@ impl Application for Window { Message::Minimize => todo!(), Message::Maximize => todo!(), Message::RowSelected(row) => println!("Selected row {row}"), - Message::InputChanged => {}, - + Message::InputChanged => {} + Message::Rectangle(r) => match r { + RectangleUpdate::Rectangle(r) => {dbg!(r);}, + RectangleUpdate::Init(t) => {self.rectangle_tracker.replace(t);}, + }, } Command::none() @@ -124,7 +131,7 @@ impl Application for Window { nav_button("Settings") .on_sidebar_toggled(Message::ToggleSidebar) .sidebar_active(self.sidebar_toggled) - .into() + .into(), ); if self.show_maximize { @@ -226,13 +233,21 @@ impl Application for Window { )) }, ); + let secondary = button(ButtonTheme::Secondary) + .text("Secondary") + .on_press(Message::ButtonPressed); + let secondary = if let Some(tracker) = self.rectangle_tracker.as_ref() { + tracker.container(0, secondary).into() + } else { + secondary.into() + }; let content: Element<_> = settings::view_column(vec![ settings::view_section("Debug") .add(settings::item("Debug theme", choose_theme)) .add(settings::item( "Debug layout", - toggler(String::from("Debug layout"), self.debug, Message::Debug) + toggler(String::from("Debug layout"), self.debug, Message::Debug), )) .into(), settings::view_section("Buttons") @@ -241,10 +256,7 @@ impl Application for Window { .text("Primary") .on_press(Message::ButtonPressed) .into(), - button(ButtonTheme::Secondary) - .text("Secondary") - .on_press(Message::ButtonPressed) - .into(), + secondary, button(ButtonTheme::Positive) .text("Positive") .on_press(Message::ButtonPressed) @@ -256,7 +268,7 @@ impl Application for Window { button(ButtonTheme::Text) .text("Text") .on_press(Message::ButtonPressed) - .into() + .into(), ])) .add(settings::item_row(vec![ button(ButtonTheme::Primary).text("Primary").into(), @@ -267,28 +279,31 @@ impl Application for Window { ])) .into(), settings::view_section("Controls") - .add(settings::item("Toggler", toggler(None, self.toggler_value, Message::TogglerToggled))) + .add(settings::item( + "Toggler", + toggler(None, self.toggler_value, Message::TogglerToggled), + )) .add(settings::item( "Pick List (TODO)", pick_list( - vec!["Option 1", "Option 2", "Option 3", "Option 4",], + vec!["Option 1", "Option 2", "Option 3", "Option 4"], self.pick_list_selected, - Message::PickListSelected + Message::PickListSelected, ) - .padding([8, 0, 8, 16]) + .padding([8, 0, 8, 16]), )) .add(settings::item( "Slider", slider(0.0..=100.0, self.slider_value, Message::SliderChanged) - .width(Length::Units(250)) + .width(Length::Units(250)), )) .add(settings::item( "Progress", progress_bar(0.0..=100.0, self.slider_value) .width(Length::Units(250)) - .height(Length::Units(4)) + .height(Length::Units(4)), )) - .into() + .into(), ]) .into(); @@ -323,8 +338,11 @@ impl Application for Window { fn theme(&self) -> Theme { self.theme } - + fn close_requested(&self, id: SurfaceIdWrapper) -> Self::Message { Message::Close } + fn subscription(&self) -> iced::Subscription { + rectangle_tracker_subscription(0).map(|(i, e)| Message::Rectangle(e)) + } } diff --git a/src/widget/mod.rs b/src/widget/mod.rs index 6645dcbc..8fba069d 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -19,9 +19,6 @@ pub use self::nav_button::{NavButton, nav_button}; pub mod navigation; pub use navigation::*; -pub mod popup; -pub use popup::*; - mod toggler; pub use toggler::toggler; @@ -34,4 +31,6 @@ pub mod separator; pub use separator::{horizontal_rule, vertical_rule}; pub mod spin_button; -pub use spin_button::{SpinButton, spin_button}; \ No newline at end of file +pub use spin_button::{SpinButton, spin_button}; + +pub mod rectangle_tracker; diff --git a/src/widget/popup.rs b/src/widget/popup.rs deleted file mode 100644 index 5035a8a9..00000000 --- a/src/widget/popup.rs +++ /dev/null @@ -1,314 +0,0 @@ -// Copyright 2022 System76 -// SPDX-License-Identifier: MPL-2.0 - -use iced::futures::channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender}; -use iced::futures::SinkExt; -use iced::{ - futures::StreamExt, - widget::{container, Container}, - Rectangle, -}; -use iced_native::alignment::{self, Alignment}; -use iced_native::command::platform_specific::wayland::popup::{SctkPopupSettings, SctkPositioner}; -use iced_native::event::{self, Event}; -use iced_native::layout; -use iced_native::mouse; -use iced_native::overlay; -use iced_native::renderer; -use iced_native::widget::{Operation, Tree}; -use iced_native::{ - window, Background, Clipboard, Color, Element, Layout, Length, Padding, Point, Shell, Widget, -}; -use std::u32; - -pub use iced_style::container::{Appearance, StyleSheet}; -pub struct SizeTrackingContainer<'a, Message, Renderer> -where - Renderer: iced_native::Renderer, - Renderer::Theme: StyleSheet, -{ - container: Container<'a, Message, Renderer>, - tx: UnboundedSender>, -} - -impl<'a, Message, Renderer> SizeTrackingContainer<'a, Message, Renderer> -where - Renderer: iced_native::Renderer, - Renderer::Theme: StyleSheet, -{ - /// Creates an empty [`Container`]. - pub fn new(content: T, tx: UnboundedSender>) -> Self - where - T: Into>, - { - SizeTrackingContainer { - container: container(content), - tx, - } - } - - /// Sets the [`Padding`] of the [`Container`]. - pub fn padding>(mut self, padding: P) -> Self { - self.container = self.container.padding(padding); - self - } - - /// Sets the width of the [`Container`]. - pub fn width(mut self, width: Length) -> Self { - self.container = self.container.width(width); - self - } - - /// Sets the height of the [`Container`]. - pub fn height(mut self, height: Length) -> Self { - self.container = self.container.height(height); - self - } - - /// Sets the maximum width of the [`Container`]. - pub fn max_width(mut self, max_width: u32) -> Self { - self.container = self.container.max_width(max_width); - self - } - - /// Sets the maximum height of the [`Container`] in pixels. - pub fn max_height(mut self, max_height: u32) -> Self { - self.container = self.container.max_height(max_height); - self - } - - /// Sets the content alignment for the horizontal axis of the [`Container`]. - pub fn align_x(mut self, alignment: alignment::Horizontal) -> Self { - self.container = self.container.align_x(alignment); - self - } - - /// Sets the content alignment for the vertical axis of the [`Container`]. - pub fn align_y(mut self, alignment: alignment::Vertical) -> Self { - self.container = self.container.align_y(alignment); - self - } - - /// Centers the contents in the horizontal axis of the [`Container`]. - pub fn center_x(mut self) -> Self { - self.container = self.container.center_x(); - self - } - - /// Centers the contents in the vertical axis of the [`Container`]. - pub fn center_y(mut self) -> Self { - self.container = self.container.center_y(); - self - } - - /// Sets the style of the [`Container`]. - pub fn style(mut self, style: impl Into<::Style>) -> Self { - self.container = self.container.style(style); - self - } -} - -impl<'a, Message, Renderer> Widget - for SizeTrackingContainer<'a, Message, Renderer> -where - Renderer: iced_native::Renderer, - Renderer::Theme: container::StyleSheet, -{ - fn children(&self) -> Vec { - self.container.children() - } - - fn diff(&self, tree: &mut Tree) { - self.container.diff(tree) - } - - fn width(&self) -> Length { - Widget::width(&self.container) - } - - fn height(&self) -> Length { - Widget::height(&self.container) - } - - fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node { - self.container.layout(renderer, limits) - } - - fn on_event( - &mut self, - tree: &mut Tree, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - self.container.on_event( - &mut tree.children[0], - event, - layout, - cursor_position, - renderer, - clipboard, - shell, - ) - } - - fn mouse_interaction( - &self, - tree: &Tree, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - self.container.mouse_interaction( - &tree.children[0], - layout, - cursor_position, - viewport, - renderer, - ) - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - inherited_style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - ) { - let Rectangle { - x, - y, - width, - height, - } = layout.bounds(); - let _ = self.tx.unbounded_send(Rectangle { - x: x as i32, - y: y as i32, - width: width as i32, - height: height as i32, - }); - self.container.draw( - &tree.children[0], - renderer, - theme, - inherited_style, - layout, - cursor_position, - viewport, - ); - } - - fn overlay<'b>( - &'b self, - tree: &'b mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option> { - self.container - .overlay(&mut tree.children[0], layout, renderer) - } -} - -pub struct PopupParentSubscription { - id: window::Id, - settings: SctkPopupSettings, -} - -impl PopupParentSubscription { - pub fn new(id: window::Id, settings: SctkPopupSettings) -> Self { - Self { id, settings } - } - - pub fn get_popup_container<'a, T, Message, Renderer>( - &self, - content: T, - tx: UnboundedSender>, - ) -> SizeTrackingContainer<'a, Message, Renderer> - where - T: Into>, - Renderer: iced_native::Renderer, - Renderer::Theme: StyleSheet, - { - SizeTrackingContainer::new(content, tx.clone()) - } - - pub fn subscription(&self) -> iced::Subscription<(window::Id, PositionerUpdate)> { - popup_resize(self.id, self.settings.clone()) - } -} - -pub fn popup_resize( - id: window::Id, - settings: SctkPopupSettings, -) -> iced::Subscription<(window::Id, PositionerUpdate)> { - iced_native::subscription::unfold( - id, - State::Init(settings.positioner.anchor_rect.clone()), - move |state| rectangle_size(id, state), - ) - .with(settings) - .map(|(settings, (id, update))| match update { - RectangleUpdate::Update(rect) => { - let mut new_pos = settings.positioner.clone(); - new_pos.anchor_rect = rect; - (id, PositionerUpdate::Update(new_pos)) - } - RectangleUpdate::Finished => (id, PositionerUpdate::Finished), - RectangleUpdate::Sender(sender) => (id, PositionerUpdate::Sender(sender)), - }) -} - -#[derive(Debug, Clone)] -pub enum PositionerUpdate { - Sender(UnboundedSender>), - Update(SctkPositioner), - Finished, -} - -#[derive(Debug, Clone)] -pub enum RectangleUpdate { - Sender(UnboundedSender>), - Update(Rectangle), - Finished, -} - -pub enum State { - Init(Rectangle), - WaitForUpdate(Rectangle, UnboundedReceiver>), - Finished, -} - -async fn rectangle_size(id: I, state: State) -> (Option<(I, RectangleUpdate)>, State) { - match state { - State::Init(rectangle) => { - let (tx, rx) = unbounded(); - ( - Some((id, RectangleUpdate::Sender(tx))), - State::WaitForUpdate(rectangle, rx), - ) - } - State::WaitForUpdate(old_rectangle, mut rx) => { - let response = rx.next().await; - - match response { - Some(new_rectangle) => { - let new_update = if new_rectangle == old_rectangle { - None - } else { - Some((id, RectangleUpdate::Update(new_rectangle))) - }; - (new_update, State::WaitForUpdate(new_rectangle, rx)) - } - None => (Some((id, RectangleUpdate::Finished)), State::Finished), - } - } - State::Finished => iced::futures::future::pending().await, - } -} diff --git a/src/widget/rectangle_tracker/mod.rs b/src/widget/rectangle_tracker/mod.rs new file mode 100644 index 00000000..bc7f68ce --- /dev/null +++ b/src/widget/rectangle_tracker/mod.rs @@ -0,0 +1,244 @@ +mod subscription; + +use iced::futures::channel::mpsc::UnboundedSender; +use iced::widget::Container; +pub use subscription::*; + +use iced_native::alignment; +use iced_native::event::{self, Event}; +use iced_native::layout; +use iced_native::mouse; +use iced_native::overlay; +use iced_native::renderer; +use iced_native::widget::{Operation, Tree}; +use iced_native::{Clipboard, Element, Layout, Length, Padding, Point, Rectangle, Shell, Widget}; +use std::{fmt::Debug, hash::Hash}; + +pub use iced_style::container::{Appearance, StyleSheet}; + +#[derive(Clone, Debug)] +pub struct RectangleTracker { + tx: UnboundedSender<(I, Rectangle)>, +} + +impl RectangleTracker +where + I: Hash + Copy + Send + Sync + Debug, +{ + pub fn container<'a, Message: 'static, T>( + &self, + id: I, + content: T, + ) -> RectangleTrackingContainer<'a, Message, crate::Renderer, I> + where + I: 'a, + T: Into>, + { + RectangleTrackingContainer::new(content, id, self.tx.clone()) + } +} + +/// An element decorating some content. +/// +/// It is normally used for alignment purposes. +#[allow(missing_debug_implementations)] +pub struct RectangleTrackingContainer<'a, Message, Renderer, I> +where + Renderer: iced_native::Renderer, + Renderer::Theme: StyleSheet, +{ + tx: UnboundedSender<(I, Rectangle)>, + id: I, + container: Container<'a, Message, Renderer>, +} + +impl<'a, Message, Renderer, I> RectangleTrackingContainer<'a, Message, Renderer, I> +where + Renderer: iced_native::Renderer, + Renderer::Theme: StyleSheet, + I: 'a + Hash + Copy + Send + Sync + Debug, +{ + /// Creates an empty [`Container`]. + pub(crate) fn new(content: T, id: I, tx: UnboundedSender<(I, Rectangle)>) -> Self + where + T: Into>, + { + RectangleTrackingContainer { + id, + tx, + container: Container::new(content), + } + } + + /// Sets the [`Padding`] of the [`Container`]. + pub fn padding>(mut self, padding: P) -> Self { + self.container = self.container.padding(padding); + self + } + + /// Sets the width of the [`self.`]. + pub fn width(mut self, width: Length) -> Self { + self.container = self.container.width(width); + self + } + + /// Sets the height of the [`Container`]. + pub fn height(mut self, height: Length) -> Self { + self.container = self.container.height(height); + self + } + + /// Sets the maximum width of the [`Container`]. + pub fn max_width(mut self, max_width: u32) -> Self { + self.container = self.container.max_width(max_width); + self + } + + /// Sets the maximum height of the [`Container`] in pixels. + pub fn max_height(mut self, max_height: u32) -> Self { + self.container = self.container.max_height(max_height); + self + } + + /// Sets the content alignment for the horizontal axis of the [`Container`]. + pub fn align_x(mut self, alignment: alignment::Horizontal) -> Self { + self.container = self.container.align_x(alignment); + self + } + + /// Sets the content alignment for the vertical axis of the [`Container`]. + pub fn align_y(mut self, alignment: alignment::Vertical) -> Self { + self.container = self.container.align_y(alignment); + self + } + + /// Centers the contents in the horizontal axis of the [`Container`]. + pub fn center_x(mut self) -> Self { + self.container = self.container.center_x(); + self + } + + /// Centers the contents in the vertical axis of the [`Container`]. + pub fn center_y(mut self) -> Self { + self.container = self.container.center_y(); + self + } + + /// Sets the style of the [`Container`]. + pub fn style(mut self, style: impl Into<::Style>) -> Self { + self.container = self.container.style(style); + self + } +} + +impl<'a, Message, Renderer, I> Widget + for RectangleTrackingContainer<'a, Message, Renderer, I> +where + Renderer: iced_native::Renderer, + Renderer::Theme: StyleSheet, + I: 'a + Hash + Copy + Send + Sync + Debug, +{ + fn children(&self) -> Vec { + self.container.children() + } + + fn diff(&self, tree: &mut Tree) { + self.container.diff(tree); + } + + fn width(&self) -> Length { + Widget::width(&self.container) + } + + fn height(&self) -> Length { + Widget::height(&self.container) + } + + fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node { + self.container.layout(renderer, limits) + } + + fn operate(&self, tree: &mut Tree, layout: Layout<'_>, operation: &mut dyn Operation) { + self.container.operate(tree, layout, operation) + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + self.container.on_event( + tree, + event, + layout, + cursor_position, + renderer, + clipboard, + shell, + ) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.container + .mouse_interaction(tree, layout, cursor_position, viewport, renderer) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + renderer_style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + let _ = self.tx.unbounded_send((self.id, layout.bounds())); + + self.container.draw( + tree, + renderer, + theme, + renderer_style, + layout, + cursor_position, + viewport, + ) + } + + fn overlay<'b>( + &'b self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option> { + self.container.overlay(tree, layout, renderer) + } +} + +impl<'a, Message, Renderer, I> From> + for Element<'a, Message, Renderer> +where + Message: 'a, + Renderer: 'a + iced_native::Renderer, + Renderer::Theme: StyleSheet, + I: 'a + Hash + Copy + Send + Sync + Debug, +{ + fn from( + column: RectangleTrackingContainer<'a, Message, Renderer, I>, + ) -> Element<'a, Message, Renderer> { + Element::new(column) + } +} diff --git a/src/widget/rectangle_tracker/subscription.rs b/src/widget/rectangle_tracker/subscription.rs new file mode 100644 index 00000000..468f8ab2 --- /dev/null +++ b/src/widget/rectangle_tracker/subscription.rs @@ -0,0 +1,75 @@ +use iced::{ + futures::{ + channel::mpsc::{unbounded, UnboundedReceiver}, + StreamExt, + }, + subscription, Rectangle, +}; +use std::{fmt::Debug, hash::Hash, collections::HashMap}; + +use super::RectangleTracker; + +pub fn rectangle_tracker_subscription< + I: 'static + Hash + Copy + Send + Sync + Debug, + R: 'static + Hash + Copy + Send + Sync + Debug + Eq, +>( + id: I, +) -> iced::Subscription<(I, RectangleUpdate)> { + subscription::unfold(id, State::Ready, move |state| start_listening(id, state)) +} + +pub enum State { + Ready, + Waiting(UnboundedReceiver<(I, Rectangle)>, HashMap), + Finished, +} + +async fn start_listening( + id: I, + state: State, +) -> (Option<(I, RectangleUpdate)>, State) { + match state { + State::Ready => { + let (tx, rx) = unbounded(); + + return ( + Some((id, RectangleUpdate::Init(RectangleTracker { tx }))), + State::Waiting(rx, HashMap::new()), + ); + } + State::Waiting(mut rx, mut map) => match rx.next().await { + Some(u) => + { + if let Some(prev) = map.get(&u.0) { + let new = u.1; + if prev.width != new.width || prev.height != new.height || prev.x != new.x || prev.y != new.y { + map.insert(u.0, new); + return ( + Some((id, RectangleUpdate::Rectangle(u))), + State::Waiting(rx, map), + ); + } + } else { + map.insert(u.0, u.1); + return ( + Some((id, RectangleUpdate::Rectangle(u))), + State::Waiting(rx, map), + ); + } + return (None, State::Waiting(rx, map)) + + }, + None => (None, State::Finished), + }, + State::Finished => iced::futures::future::pending().await, + } +} + +#[derive(Clone, Debug)] +pub enum RectangleUpdate +where + I: 'static + Hash + Copy + Send + Sync + Debug, +{ + Rectangle((I, Rectangle)), + Init(RectangleTracker), +}