From f962c6ce0671b4b0b0e683148353fc3a3d1fc3c3 Mon Sep 17 00:00:00 2001 From: mtkennerly Date: Mon, 15 Apr 2024 08:41:29 -0400 Subject: Allow checking whether a TextEditor is focused --- widget/src/text_editor.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) (limited to 'widget') diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 92cdb251..5b0b1b79 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -319,7 +319,9 @@ where } } -struct State { +/// The state of a [`TextEditor`]. +#[derive(Debug)] +pub struct State { is_focused: bool, last_click: Option, drag_click: Option, @@ -329,6 +331,13 @@ struct State { highlighter_format_address: usize, } +impl State { + /// Returns whether the [`TextEditor`] is currently focused or not. + pub fn is_focused(&self) -> bool { + self.is_focused + } +} + impl<'a, Highlighter, Message, Theme, Renderer> Widget for TextEditor<'a, Highlighter, Message, Theme, Renderer> where -- cgit From 67e181ce7bad516d2ff8fc34c989af2435fdc7b4 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 23 Apr 2024 02:29:04 +0200 Subject: Fix clip bounds with nested `scrollable` widgets --- widget/src/scrollable.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) (limited to 'widget') diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 668c5372..626cd07f 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -659,6 +659,10 @@ where let content_layout = layout.children().next().unwrap(); let content_bounds = content_layout.bounds(); + let Some(visible_bounds) = bounds.intersection(viewport) else { + return; + }; + let scrollbars = Scrollbars::new(state, self.direction, bounds, content_bounds); @@ -704,7 +708,7 @@ where // Draw inner content if scrollbars.active() { - renderer.with_layer(bounds, |renderer| { + renderer.with_layer(visible_bounds, |renderer| { renderer.with_translation( Vector::new(-translation.x, -translation.y), |renderer| { @@ -767,9 +771,9 @@ where renderer.with_layer( Rectangle { - width: (bounds.width + 2.0).min(viewport.width), - height: (bounds.height + 2.0).min(viewport.height), - ..bounds + width: (visible_bounds.width + 2.0).min(viewport.width), + height: (visible_bounds.height + 2.0).min(viewport.height), + ..visible_bounds }, |renderer| { if let Some(scrollbar) = scrollbars.y { -- cgit From fdcec0319757d2f87c82787eab34c6bef8c5a799 Mon Sep 17 00:00:00 2001 From: Daniel <101683475+Koranir@users.noreply.github.com> Date: Tue, 23 Apr 2024 10:45:44 +1000 Subject: Don't consume unused scroll events (#2397) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Initial Commit * Update scrollable.rs * Use `let _ = ` instead of `_ =` for consistency --------- Co-authored-by: Héctor Ramón Jiménez --- widget/src/scrollable.rs | 84 ++++++++++++++++++++++++++---------------------- 1 file changed, 45 insertions(+), 39 deletions(-) (limited to 'widget') diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 626cd07f..cdc143a2 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -435,15 +435,17 @@ where state.scroll(delta, self.direction, bounds, content_bounds); - notify_on_scroll( + event_status = if notify_on_scroll( state, &self.on_scroll, bounds, content_bounds, shell, - ); - - event_status = event::Status::Captured; + ) { + event::Status::Captured + } else { + event::Status::Ignored + }; } Event::Touch(event) if state.scroll_area_touched_at.is_some() @@ -481,7 +483,8 @@ where state.scroll_area_touched_at = Some(cursor_position); - notify_on_scroll( + // TODO: bubble up touch movements if not consumed. + let _ = notify_on_scroll( state, &self.on_scroll, bounds, @@ -516,7 +519,7 @@ where content_bounds, ); - notify_on_scroll( + let _ = notify_on_scroll( state, &self.on_scroll, bounds, @@ -554,7 +557,7 @@ where state.y_scroller_grabbed_at = Some(scroller_grabbed_at); - notify_on_scroll( + let _ = notify_on_scroll( state, &self.on_scroll, bounds, @@ -587,7 +590,7 @@ where content_bounds, ); - notify_on_scroll( + let _ = notify_on_scroll( state, &self.on_scroll, bounds, @@ -625,7 +628,7 @@ where state.x_scroller_grabbed_at = Some(scroller_grabbed_at); - notify_on_scroll( + let _ = notify_on_scroll( state, &self.on_scroll, bounds, @@ -965,51 +968,54 @@ pub fn scroll_to( Command::widget(operation::scrollable::scroll_to(id.0, offset)) } +/// Returns [`true`] if the viewport actually changed. fn notify_on_scroll( state: &mut State, on_scroll: &Option Message + '_>>, bounds: Rectangle, content_bounds: Rectangle, shell: &mut Shell<'_, Message>, -) { - if let Some(on_scroll) = on_scroll { - if content_bounds.width <= bounds.width - && content_bounds.height <= bounds.height - { - return; - } +) -> bool { + if content_bounds.width <= bounds.width + && content_bounds.height <= bounds.height + { + return false; + } - let viewport = Viewport { - offset_x: state.offset_x, - offset_y: state.offset_y, - bounds, - content_bounds, - }; + let viewport = Viewport { + offset_x: state.offset_x, + offset_y: state.offset_y, + bounds, + content_bounds, + }; - // Don't publish redundant viewports to shell - if let Some(last_notified) = state.last_notified { - let last_relative_offset = last_notified.relative_offset(); - let current_relative_offset = viewport.relative_offset(); + // Don't publish redundant viewports to shell + if let Some(last_notified) = state.last_notified { + let last_relative_offset = last_notified.relative_offset(); + let current_relative_offset = viewport.relative_offset(); - let last_absolute_offset = last_notified.absolute_offset(); - let current_absolute_offset = viewport.absolute_offset(); + let last_absolute_offset = last_notified.absolute_offset(); + let current_absolute_offset = viewport.absolute_offset(); - let unchanged = |a: f32, b: f32| { - (a - b).abs() <= f32::EPSILON || (a.is_nan() && b.is_nan()) - }; + let unchanged = |a: f32, b: f32| { + (a - b).abs() <= f32::EPSILON || (a.is_nan() && b.is_nan()) + }; - if unchanged(last_relative_offset.x, current_relative_offset.x) - && unchanged(last_relative_offset.y, current_relative_offset.y) - && unchanged(last_absolute_offset.x, current_absolute_offset.x) - && unchanged(last_absolute_offset.y, current_absolute_offset.y) - { - return; - } + if unchanged(last_relative_offset.x, current_relative_offset.x) + && unchanged(last_relative_offset.y, current_relative_offset.y) + && unchanged(last_absolute_offset.x, current_absolute_offset.x) + && unchanged(last_absolute_offset.y, current_absolute_offset.y) + { + return false; } + } + if let Some(on_scroll) = on_scroll { shell.publish(on_scroll(viewport)); - state.last_notified = Some(viewport); } + state.last_notified = Some(viewport); + + true } #[derive(Debug, Clone, Copy)] -- cgit From 0c74d2645649a88799a894ed684a728d135043fa Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 25 Apr 2024 01:39:34 +0200 Subject: Implement `Stack` widget It can be used to stack elements on top of each other! --- widget/src/helpers.rs | 27 ++++- widget/src/lib.rs | 3 + widget/src/stack.rs | 327 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 356 insertions(+), 1 deletion(-) create mode 100644 widget/src/stack.rs (limited to 'widget') diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 61789c19..2afed3e6 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -21,7 +21,7 @@ use crate::text_input::{self, TextInput}; use crate::toggler::{self, Toggler}; use crate::tooltip::{self, Tooltip}; use crate::vertical_slider::{self, VerticalSlider}; -use crate::{Column, MouseArea, Row, Space, Themer}; +use crate::{Column, MouseArea, Row, Space, Stack, Themer}; use std::borrow::Borrow; use std::ops::RangeInclusive; @@ -52,6 +52,19 @@ macro_rules! row { ); } +/// Creates a [`Stack`] with the given children. +/// +/// [`Stack`]: crate::Stack +#[macro_export] +macro_rules! stack { + () => ( + $crate::Stack::new() + ); + ($($x:expr),+ $(,)?) => ( + $crate::Stack::with_children([$($crate::core::Element::from($x)),+]) + ); +} + /// Creates a new [`Container`] with the provided content. /// /// [`Container`]: crate::Container @@ -98,6 +111,18 @@ where Row::with_children(children) } +/// Creates a new [`Stack`] with the given children. +/// +/// [`Stack`]: crate::Stack +pub fn stack<'a, Message, Theme, Renderer>( + children: impl IntoIterator>, +) -> Stack<'a, Message, Theme, Renderer> +where + Renderer: core::Renderer, +{ + Stack::with_children(children) +} + /// Creates a new [`Scrollable`] with the provided content. /// /// [`Scrollable`]: crate::Scrollable diff --git a/widget/src/lib.rs b/widget/src/lib.rs index 1eeacbae..00e9aaa4 100644 --- a/widget/src/lib.rs +++ b/widget/src/lib.rs @@ -12,6 +12,7 @@ mod column; mod mouse_area; mod row; mod space; +mod stack; mod themer; pub mod button; @@ -78,6 +79,8 @@ pub use slider::Slider; #[doc(no_inline)] pub use space::Space; #[doc(no_inline)] +pub use stack::Stack; +#[doc(no_inline)] pub use text::Text; #[doc(no_inline)] pub use text_editor::TextEditor; diff --git a/widget/src/stack.rs b/widget/src/stack.rs new file mode 100644 index 00000000..84f1f7ff --- /dev/null +++ b/widget/src/stack.rs @@ -0,0 +1,327 @@ +//! Distribute content vertically. +use crate::core::event::{self, Event}; +use crate::core::layout; +use crate::core::mouse; +use crate::core::overlay; +use crate::core::renderer; +use crate::core::widget::{Operation, Tree}; +use crate::core::{ + Clipboard, Element, Layout, Length, Rectangle, Shell, Size, Vector, Widget, +}; + +/// A container that displays children on top of each other. +/// +/// The first [`Element`] dictates the intrinsic [`Size`] of a [`Stack`] and +/// will be displayed as the base layer. Every consecutive [`Element`] will be +/// renderer on top; on its own layer. +/// +/// Keep in mind that too much layering will normally produce bad UX as well as +/// introduce certain rendering overhead. Use this widget sparingly! +#[allow(missing_debug_implementations)] +pub struct Stack<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> +{ + width: Length, + height: Length, + children: Vec>, +} + +impl<'a, Message, Theme, Renderer> Stack<'a, Message, Theme, Renderer> +where + Renderer: crate::core::Renderer, +{ + /// Creates an empty [`Stack`]. + pub fn new() -> Self { + Self::from_vec(Vec::new()) + } + + /// Creates a [`Stack`] with the given capacity. + pub fn with_capacity(capacity: usize) -> Self { + Self::from_vec(Vec::with_capacity(capacity)) + } + + /// Creates a [`Stack`] with the given elements. + pub fn with_children( + children: impl IntoIterator>, + ) -> Self { + let iterator = children.into_iter(); + + Self::with_capacity(iterator.size_hint().0).extend(iterator) + } + + /// Creates a [`Stack`] from an already allocated [`Vec`]. + /// + /// Keep in mind that the [`Stack`] will not inspect the [`Vec`], which means + /// it won't automatically adapt to the sizing strategy of its contents. + /// + /// If any of the children have a [`Length::Fill`] strategy, you will need to + /// call [`Stack::width`] or [`Stack::height`] accordingly. + pub fn from_vec( + children: Vec>, + ) -> Self { + Self { + width: Length::Shrink, + height: Length::Shrink, + children, + } + } + + /// Sets the width of the [`Stack`]. + pub fn width(mut self, width: impl Into) -> Self { + self.width = width.into(); + self + } + + /// Sets the height of the [`Stack`]. + pub fn height(mut self, height: impl Into) -> Self { + self.height = height.into(); + self + } + + /// Adds an element to the [`Stack`]. + pub fn push( + mut self, + child: impl Into>, + ) -> Self { + let child = child.into(); + + if self.children.is_empty() { + let child_size = child.as_widget().size_hint(); + + self.width = self.width.enclose(child_size.width); + self.height = self.height.enclose(child_size.height); + } + + self.children.push(child); + self + } + + /// Adds an element to the [`Stack`], if `Some`. + pub fn push_maybe( + self, + child: Option>>, + ) -> Self { + if let Some(child) = child { + self.push(child) + } else { + self + } + } + + /// Extends the [`Stack`] with the given children. + pub fn extend( + self, + children: impl IntoIterator>, + ) -> Self { + children.into_iter().fold(self, Self::push) + } +} + +impl<'a, Message, Renderer> Default for Stack<'a, Message, Renderer> +where + Renderer: crate::core::Renderer, +{ + fn default() -> Self { + Self::new() + } +} + +impl<'a, Message, Theme, Renderer> Widget + for Stack<'a, Message, Theme, Renderer> +where + Renderer: crate::core::Renderer, +{ + fn children(&self) -> Vec { + self.children.iter().map(Tree::new).collect() + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children(&self.children); + } + + fn size(&self) -> Size { + Size { + width: self.width, + height: self.height, + } + } + + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + if self.children.is_empty() { + return layout::Node::new(Size::ZERO); + } + + let base = self.children[0].as_widget().layout( + &mut tree.children[0], + renderer, + limits, + ); + + let size = base.size(); + let limits = layout::Limits::new(Size::ZERO, size); + + let nodes = std::iter::once(base) + .chain(self.children[1..].iter().zip(&mut tree.children[1..]).map( + |(layer, tree)| { + let node = + layer.as_widget().layout(tree, renderer, &limits); + + node + }, + )) + .collect(); + + layout::Node::with_children(size, nodes) + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn Operation, + ) { + operation.container(None, layout.bounds(), &mut |operation| { + self.children + .iter() + .zip(&mut tree.children) + .zip(layout.children()) + .for_each(|((child, state), layout)| { + child + .as_widget() + .operate(state, layout, renderer, operation); + }); + }); + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) -> event::Status { + self.children + .iter_mut() + .rev() + .zip(tree.children.iter_mut().rev()) + .zip(layout.children().rev()) + .map(|((child, state), layout)| { + child.as_widget_mut().on_event( + state, + event.clone(), + layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ) + }) + .find(|&status| status == event::Status::Captured) + .unwrap_or(event::Status::Ignored) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.children + .iter() + .rev() + .zip(tree.children.iter().rev()) + .zip(layout.children().rev()) + .map(|((child, state), layout)| { + child.as_widget().mouse_interaction( + state, layout, cursor, viewport, renderer, + ) + }) + .find(|&interaction| interaction != mouse::Interaction::Idle) + .unwrap_or_default() + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + if let Some(clipped_viewport) = layout.bounds().intersection(viewport) { + for (i, ((layer, state), layout)) in self + .children + .iter() + .zip(&tree.children) + .zip(layout.children()) + .enumerate() + { + if i > 0 { + renderer.with_layer(clipped_viewport, |renderer| { + layer.as_widget().draw( + state, + renderer, + theme, + style, + layout, + cursor, + &clipped_viewport, + ); + }); + } else { + layer.as_widget().draw( + state, + renderer, + theme, + style, + layout, + cursor, + &clipped_viewport, + ); + } + } + } + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + translation: Vector, + ) -> Option> { + overlay::from_children( + &mut self.children, + tree, + layout, + renderer, + translation, + ) + } +} + +impl<'a, Message, Theme, Renderer> From> + for Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Theme: 'a, + Renderer: crate::core::Renderer + 'a, +{ + fn from(stack: Stack<'a, Message, Theme, Renderer>) -> Self { + Self::new(stack) + } +} -- cgit From 99434b3ecf7874d2382e7f739bc69a55768fd60b Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 25 Apr 2024 01:47:07 +0200 Subject: Fix documentation of `stack` module --- widget/src/stack.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'widget') diff --git a/widget/src/stack.rs b/widget/src/stack.rs index 84f1f7ff..d6a1538e 100644 --- a/widget/src/stack.rs +++ b/widget/src/stack.rs @@ -1,4 +1,4 @@ -//! Distribute content vertically. +//! Display content on top of other content. use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; -- cgit From 9492da11d90893b396e8dfdae47cc54c6ab42411 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 25 Apr 2024 02:25:36 +0200 Subject: Use `Limits::resolve` in `Stack` widget --- widget/src/stack.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'widget') diff --git a/widget/src/stack.rs b/widget/src/stack.rs index d6a1538e..6e5dacd2 100644 --- a/widget/src/stack.rs +++ b/widget/src/stack.rs @@ -155,13 +155,14 @@ where return layout::Node::new(Size::ZERO); } + let limits = limits.width(self.width).height(self.height); let base = self.children[0].as_widget().layout( &mut tree.children[0], renderer, - limits, + &limits, ); - let size = base.size(); + let size = limits.resolve(self.width, self.height, base.size()); let limits = layout::Limits::new(Size::ZERO, size); let nodes = std::iter::once(base) -- cgit From 4cd45643d7d2aa83212162f17a9b28ddae4a9340 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 25 Apr 2024 06:05:00 +0200 Subject: Introduce `opaque` widget helper --- widget/src/helpers.rs | 169 ++++++++++++++++++++++++++++++++++++++++++++- widget/src/image/viewer.rs | 2 +- widget/src/mouse_area.rs | 2 +- widget/src/scrollable.rs | 2 +- widget/src/stack.rs | 2 +- 5 files changed, 172 insertions(+), 5 deletions(-) (limited to 'widget') diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 2afed3e6..fd345251 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -5,7 +5,7 @@ use crate::combo_box::{self, ComboBox}; use crate::container::{self, Container}; use crate::core; use crate::core::widget::operation; -use crate::core::{Element, Length, Pixels}; +use crate::core::{Element, Length, Pixels, Widget}; use crate::keyed; use crate::overlay; use crate::pick_list::{self, PickList}; @@ -123,6 +123,173 @@ where Stack::with_children(children) } +/// Wraps the given widget and captures any mouse button presses inside the bounds of +/// the widget—therefore making it _opaque_. +/// +/// This helper is meant to be used to mark elements in a [`Stack`] to avoid mouse +/// events from passing through layers. +/// +/// [`Stack`]: crate::Stack +pub fn opaque<'a, Message, Theme, Renderer>( + content: impl Into>, +) -> Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Theme: 'a, + Renderer: core::Renderer + 'a, +{ + use crate::core::event::{self, Event}; + use crate::core::layout::{self, Layout}; + use crate::core::mouse; + use crate::core::renderer; + use crate::core::widget::tree::{self, Tree}; + use crate::core::{Rectangle, Shell, Size}; + + struct Opaque<'a, Message, Theme, Renderer> { + content: Element<'a, Message, Theme, Renderer>, + } + + impl<'a, Message, Theme, Renderer> Widget + for Opaque<'a, Message, Theme, Renderer> + where + Renderer: core::Renderer, + { + fn tag(&self) -> tree::Tag { + self.content.as_widget().tag() + } + + fn state(&self) -> tree::State { + self.content.as_widget().state() + } + + fn children(&self) -> Vec { + self.content.as_widget().children() + } + + fn diff(&self, tree: &mut Tree) { + self.content.as_widget().diff(tree); + } + + fn size(&self) -> Size { + self.content.as_widget().size() + } + + fn size_hint(&self) -> Size { + self.content.as_widget().size_hint() + } + + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + self.content.as_widget().layout(tree, renderer, limits) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + self.content + .as_widget() + .draw(tree, renderer, theme, style, layout, cursor, viewport); + } + + fn operate( + &self, + state: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn operation::Operation, + ) { + self.content + .as_widget() + .operate(state, layout, renderer, operation); + } + + fn on_event( + &mut self, + state: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn core::Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) -> event::Status { + let is_mouse_press = matches!( + event, + core::Event::Mouse(mouse::Event::ButtonPressed(_)) + ); + + if let core::event::Status::Captured = + self.content.as_widget_mut().on_event( + state, event, layout, cursor, renderer, clipboard, shell, + viewport, + ) + { + return event::Status::Captured; + } + + if is_mouse_press && cursor.is_over(layout.bounds()) { + event::Status::Captured + } else { + event::Status::Ignored + } + } + + fn mouse_interaction( + &self, + state: &core::widget::Tree, + layout: core::Layout<'_>, + cursor: core::mouse::Cursor, + viewport: &core::Rectangle, + renderer: &Renderer, + ) -> core::mouse::Interaction { + let interaction = self + .content + .as_widget() + .mouse_interaction(state, layout, cursor, viewport, renderer); + + if interaction == mouse::Interaction::None + && cursor.is_over(layout.bounds()) + { + mouse::Interaction::Idle + } else { + interaction + } + } + + fn overlay<'b>( + &'b mut self, + state: &'b mut core::widget::Tree, + layout: core::Layout<'_>, + renderer: &Renderer, + translation: core::Vector, + ) -> Option> + { + self.content.as_widget_mut().overlay( + state, + layout, + renderer, + translation, + ) + } + } + + Element::new(Opaque { + content: content.into(), + }) +} + /// Creates a new [`Scrollable`] with the provided content. /// /// [`Scrollable`]: crate::Scrollable diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs index 5f7bb345..75d73b19 100644 --- a/widget/src/image/viewer.rs +++ b/widget/src/image/viewer.rs @@ -304,7 +304,7 @@ where } else if is_mouse_over { mouse::Interaction::Grab } else { - mouse::Interaction::Idle + mouse::Interaction::None } } diff --git a/widget/src/mouse_area.rs b/widget/src/mouse_area.rs index 9634e477..d7235cf6 100644 --- a/widget/src/mouse_area.rs +++ b/widget/src/mouse_area.rs @@ -232,7 +232,7 @@ where ); match (self.interaction, content_interaction) { - (Some(interaction), mouse::Interaction::Idle) + (Some(interaction), mouse::Interaction::None) if cursor.is_over(layout.bounds()) => { interaction diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index cdc143a2..10e81cee 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -857,7 +857,7 @@ where if (mouse_over_x_scrollbar || mouse_over_y_scrollbar) || state.scrollers_grabbed() { - mouse::Interaction::Idle + mouse::Interaction::None } else { let translation = state.translation(self.direction, bounds, content_bounds); diff --git a/widget/src/stack.rs b/widget/src/stack.rs index 6e5dacd2..8a0ea2eb 100644 --- a/widget/src/stack.rs +++ b/widget/src/stack.rs @@ -249,7 +249,7 @@ where state, layout, cursor, viewport, renderer, ) }) - .find(|&interaction| interaction != mouse::Interaction::Idle) + .find(|&interaction| interaction != mouse::Interaction::None) .unwrap_or_default() } -- cgit From eb49567791015be7f329d7d57d5f8c388abecd96 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 26 Apr 2024 01:19:49 +0200 Subject: Capture scrollbar events in a `scrollable` before content events --- widget/src/scrollable.rs | 284 +++++++++++++++++++++++------------------------ 1 file changed, 142 insertions(+), 142 deletions(-) (limited to 'widget') diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 10e81cee..f0a042cd 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -350,6 +350,148 @@ where let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = scrollbars.is_mouse_over(cursor); + if let Some(scroller_grabbed_at) = state.y_scroller_grabbed_at { + match event { + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { + if let Some(scrollbar) = scrollbars.y { + let Some(cursor_position) = cursor.position() else { + return event::Status::Ignored; + }; + + state.scroll_y_to( + scrollbar.scroll_percentage_y( + scroller_grabbed_at, + cursor_position, + ), + bounds, + content_bounds, + ); + + let _ = notify_on_scroll( + state, + &self.on_scroll, + bounds, + content_bounds, + shell, + ); + + return event::Status::Captured; + } + } + _ => {} + } + } else if mouse_over_y_scrollbar { + match event { + Event::Mouse(mouse::Event::ButtonPressed( + mouse::Button::Left, + )) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + let Some(cursor_position) = cursor.position() else { + return event::Status::Ignored; + }; + + if let (Some(scroller_grabbed_at), Some(scrollbar)) = ( + scrollbars.grab_y_scroller(cursor_position), + scrollbars.y, + ) { + state.scroll_y_to( + scrollbar.scroll_percentage_y( + scroller_grabbed_at, + cursor_position, + ), + bounds, + content_bounds, + ); + + state.y_scroller_grabbed_at = Some(scroller_grabbed_at); + + let _ = notify_on_scroll( + state, + &self.on_scroll, + bounds, + content_bounds, + shell, + ); + } + + return event::Status::Captured; + } + _ => {} + } + } + + if let Some(scroller_grabbed_at) = state.x_scroller_grabbed_at { + match event { + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { + let Some(cursor_position) = cursor.position() else { + return event::Status::Ignored; + }; + + if let Some(scrollbar) = scrollbars.x { + state.scroll_x_to( + scrollbar.scroll_percentage_x( + scroller_grabbed_at, + cursor_position, + ), + bounds, + content_bounds, + ); + + let _ = notify_on_scroll( + state, + &self.on_scroll, + bounds, + content_bounds, + shell, + ); + } + + return event::Status::Captured; + } + _ => {} + } + } else if mouse_over_x_scrollbar { + match event { + Event::Mouse(mouse::Event::ButtonPressed( + mouse::Button::Left, + )) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + let Some(cursor_position) = cursor.position() else { + return event::Status::Ignored; + }; + + if let (Some(scroller_grabbed_at), Some(scrollbar)) = ( + scrollbars.grab_x_scroller(cursor_position), + scrollbars.x, + ) { + state.scroll_x_to( + scrollbar.scroll_percentage_x( + scroller_grabbed_at, + cursor_position, + ), + bounds, + content_bounds, + ); + + state.x_scroller_grabbed_at = Some(scroller_grabbed_at); + + let _ = notify_on_scroll( + state, + &self.on_scroll, + bounds, + content_bounds, + shell, + ); + + return event::Status::Captured; + } + } + _ => {} + } + } + let mut event_status = { let cursor = match cursor_over_scrollable { Some(cursor_position) @@ -501,148 +643,6 @@ where _ => {} } - if let Some(scroller_grabbed_at) = state.y_scroller_grabbed_at { - match event { - Event::Mouse(mouse::Event::CursorMoved { .. }) - | Event::Touch(touch::Event::FingerMoved { .. }) => { - if let Some(scrollbar) = scrollbars.y { - let Some(cursor_position) = cursor.position() else { - return event::Status::Ignored; - }; - - state.scroll_y_to( - scrollbar.scroll_percentage_y( - scroller_grabbed_at, - cursor_position, - ), - bounds, - content_bounds, - ); - - let _ = notify_on_scroll( - state, - &self.on_scroll, - bounds, - content_bounds, - shell, - ); - - event_status = event::Status::Captured; - } - } - _ => {} - } - } else if mouse_over_y_scrollbar { - match event { - Event::Mouse(mouse::Event::ButtonPressed( - mouse::Button::Left, - )) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - let Some(cursor_position) = cursor.position() else { - return event::Status::Ignored; - }; - - if let (Some(scroller_grabbed_at), Some(scrollbar)) = ( - scrollbars.grab_y_scroller(cursor_position), - scrollbars.y, - ) { - state.scroll_y_to( - scrollbar.scroll_percentage_y( - scroller_grabbed_at, - cursor_position, - ), - bounds, - content_bounds, - ); - - state.y_scroller_grabbed_at = Some(scroller_grabbed_at); - - let _ = notify_on_scroll( - state, - &self.on_scroll, - bounds, - content_bounds, - shell, - ); - } - - event_status = event::Status::Captured; - } - _ => {} - } - } - - if let Some(scroller_grabbed_at) = state.x_scroller_grabbed_at { - match event { - Event::Mouse(mouse::Event::CursorMoved { .. }) - | Event::Touch(touch::Event::FingerMoved { .. }) => { - let Some(cursor_position) = cursor.position() else { - return event::Status::Ignored; - }; - - if let Some(scrollbar) = scrollbars.x { - state.scroll_x_to( - scrollbar.scroll_percentage_x( - scroller_grabbed_at, - cursor_position, - ), - bounds, - content_bounds, - ); - - let _ = notify_on_scroll( - state, - &self.on_scroll, - bounds, - content_bounds, - shell, - ); - } - - event_status = event::Status::Captured; - } - _ => {} - } - } else if mouse_over_x_scrollbar { - match event { - Event::Mouse(mouse::Event::ButtonPressed( - mouse::Button::Left, - )) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - let Some(cursor_position) = cursor.position() else { - return event::Status::Ignored; - }; - - if let (Some(scroller_grabbed_at), Some(scrollbar)) = ( - scrollbars.grab_x_scroller(cursor_position), - scrollbars.x, - ) { - state.scroll_x_to( - scrollbar.scroll_percentage_x( - scroller_grabbed_at, - cursor_position, - ), - bounds, - content_bounds, - ); - - state.x_scroller_grabbed_at = Some(scroller_grabbed_at); - - let _ = notify_on_scroll( - state, - &self.on_scroll, - bounds, - content_bounds, - shell, - ); - - event_status = event::Status::Captured; - } - } - _ => {} - } - } - event_status } -- cgit From 6d4155a5488da5b7fa3fd314f98c8f28c7202bd4 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 26 Apr 2024 01:44:03 +0200 Subject: Fix `Shift` scrolling for `scrollable` on macOS Apparently, macOS inverts the scrolling axes automatically now. Was this a thing before, or did an update just break user space? --- widget/src/scrollable.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'widget') diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index f0a042cd..6fc00f87 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -564,7 +564,9 @@ where let delta = match delta { mouse::ScrollDelta::Lines { x, y } => { // TODO: Configurable speed/friction (?) - let movement = if state.keyboard_modifiers.shift() { + let movement = if !cfg!(target_os = "macos") // macOS automatically inverts the axes when Shift is pressed + && state.keyboard_modifiers.shift() + { Vector::new(y, x) } else { Vector::new(x, y) -- cgit From 73088a6fc1567f8bd557fdf479e16b395032b019 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 26 Apr 2024 15:17:35 +0200 Subject: Fix out of bounds caret in `TextEditor` in some circumstances --- widget/src/text_editor.rs | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) (limited to 'widget') diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 5b0b1b79..7c0b98ea 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -569,23 +569,27 @@ where if state.is_focused { match internal.editor.cursor() { Cursor::Caret(position) => { - let position = position + translation; + let cursor = + Rectangle::new( + position + translation, + Size::new( + 1.0, + self.line_height + .to_absolute(self.text_size.unwrap_or_else( + || renderer.default_size(), + )) + .into(), + ), + ); - if bounds.contains(position) { + if let Some(clipped_cursor) = bounds.intersection(&cursor) { renderer.fill_quad( renderer::Quad { bounds: Rectangle { - x: position.x.floor(), - y: position.y, - width: 1.0, - height: self - .line_height - .to_absolute( - self.text_size.unwrap_or_else( - || renderer.default_size(), - ), - ) - .into(), + x: clipped_cursor.x.floor(), + y: clipped_cursor.y, + width: clipped_cursor.width, + height: clipped_cursor.height, }, ..renderer::Quad::default() }, -- cgit From 23ef6547ad0f0cc2944664ace4be7cdacc0a23cf Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 27 Apr 2024 06:06:13 +0200 Subject: Introduce `hover` widget --- widget/src/helpers.rs | 230 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 229 insertions(+), 1 deletion(-) (limited to 'widget') diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index fd345251..c86223bc 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -124,7 +124,7 @@ where } /// Wraps the given widget and captures any mouse button presses inside the bounds of -/// the widget—therefore making it _opaque_. +/// the widget—effectively making it _opaque_. /// /// This helper is meant to be used to mark elements in a [`Stack`] to avoid mouse /// events from passing through layers. @@ -290,6 +290,234 @@ where }) } +/// Displays a widget on top of another one, only when the base widget is hovered. +/// +/// This works analogously to a [`stack`], but it will only display the layer on top +/// when the cursor is over the base. It can be useful for removing visual clutter. +pub fn hover<'a, Message, Theme, Renderer>( + base: impl Into>, + top: impl Into>, +) -> Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Theme: 'a, + Renderer: core::Renderer + 'a, +{ + use crate::core::event::{self, Event}; + use crate::core::layout::{self, Layout}; + use crate::core::mouse; + use crate::core::renderer; + use crate::core::widget::tree::{self, Tree}; + use crate::core::{Rectangle, Shell, Size}; + + struct Hover<'a, Message, Theme, Renderer> { + base: Element<'a, Message, Theme, Renderer>, + top: Element<'a, Message, Theme, Renderer>, + is_overlay_active: bool, + } + + impl<'a, Message, Theme, Renderer> Widget + for Hover<'a, Message, Theme, Renderer> + where + Renderer: core::Renderer, + { + fn tag(&self) -> tree::Tag { + struct Tag; + tree::Tag::of::() + } + + fn children(&self) -> Vec { + vec![Tree::new(&self.base), Tree::new(&self.top)] + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children(&[&self.base, &self.top]); + } + + fn size(&self) -> Size { + self.base.as_widget().size() + } + + fn size_hint(&self) -> Size { + self.base.as_widget().size_hint() + } + + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let base = self.base.as_widget().layout( + &mut tree.children[0], + renderer, + limits, + ); + + let top = self.top.as_widget().layout( + &mut tree.children[1], + renderer, + &layout::Limits::new(Size::ZERO, base.size()), + ); + + layout::Node::with_children(base.size(), vec![base, top]) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + let mut children = layout.children().zip(&tree.children); + + let (base_layout, base_tree) = children.next().unwrap(); + + self.base.as_widget().draw( + base_tree, + renderer, + theme, + style, + base_layout, + cursor, + viewport, + ); + + if cursor.is_over(layout.bounds()) || self.is_overlay_active { + let (top_layout, top_tree) = children.next().unwrap(); + + renderer.with_layer(layout.bounds(), |renderer| { + self.top.as_widget().draw( + top_tree, renderer, theme, style, top_layout, cursor, + viewport, + ); + }); + } + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn operation::Operation, + ) { + let children = [&self.base, &self.top] + .into_iter() + .zip(layout.children().zip(&mut tree.children)); + + for (child, (layout, tree)) in children { + child.as_widget().operate(tree, layout, renderer, operation); + } + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn core::Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) -> event::Status { + let mut children = layout.children().zip(&mut tree.children); + let (base_layout, base_tree) = children.next().unwrap(); + + let top_status = if cursor.is_over(layout.bounds()) { + let (top_layout, top_tree) = children.next().unwrap(); + + self.top.as_widget_mut().on_event( + top_tree, + event.clone(), + top_layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ) + } else { + event::Status::Ignored + }; + + if top_status == event::Status::Captured { + return top_status; + } + + self.base.as_widget_mut().on_event( + base_tree, + event.clone(), + base_layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + [&self.base, &self.top] + .into_iter() + .rev() + .zip(layout.children().rev().zip(tree.children.iter().rev())) + .map(|(child, (layout, tree))| { + child.as_widget().mouse_interaction( + tree, layout, cursor, viewport, renderer, + ) + }) + .find(|&interaction| interaction != mouse::Interaction::None) + .unwrap_or_default() + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut core::widget::Tree, + layout: core::Layout<'_>, + renderer: &Renderer, + translation: core::Vector, + ) -> Option> + { + let overlay = [&mut self.base, &mut self.top] + .into_iter() + .rev() + .zip( + layout.children().rev().zip(tree.children.iter_mut().rev()), + ) + .find_map(|(child, (layout, tree))| { + child.as_widget_mut().overlay( + tree, + layout, + renderer, + translation, + ) + }); + + self.is_overlay_active = overlay.is_some(); + overlay + } + } + + Element::new(Hover { + base: base.into(), + top: top.into(), + is_overlay_active: false, + }) +} + /// Creates a new [`Scrollable`] with the provided content. /// /// [`Scrollable`]: crate::Scrollable -- cgit From bb9244107c8fee78f9b77f5ab0e4c2c31456d6c4 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 27 Apr 2024 06:08:30 +0200 Subject: Respect `width` and `height` properties when `Stack` is empty --- widget/src/stack.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'widget') diff --git a/widget/src/stack.rs b/widget/src/stack.rs index 8a0ea2eb..2774d035 100644 --- a/widget/src/stack.rs +++ b/widget/src/stack.rs @@ -152,7 +152,11 @@ where limits: &layout::Limits, ) -> layout::Node { if self.children.is_empty() { - return layout::Node::new(Size::ZERO); + return layout::Node::new(limits.resolve( + self.width, + self.height, + Size::ZERO, + )); } let limits = limits.width(self.width).height(self.height); -- cgit From 40dff6b23d4666d69e27addcfb90d9557f59ed6c Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 27 Apr 2024 06:15:58 +0200 Subject: Fix `overlay` behavior in `hover` widget --- widget/src/helpers.rs | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) (limited to 'widget') diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index c86223bc..0b5adde7 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -313,7 +313,7 @@ where struct Hover<'a, Message, Theme, Renderer> { base: Element<'a, Message, Theme, Renderer>, top: Element<'a, Message, Theme, Renderer>, - is_overlay_active: bool, + is_top_overlay_active: bool, } impl<'a, Message, Theme, Renderer> Widget @@ -387,7 +387,7 @@ where viewport, ); - if cursor.is_over(layout.bounds()) || self.is_overlay_active { + if cursor.is_over(layout.bounds()) || self.is_top_overlay_active { let (top_layout, top_tree) = children.next().unwrap(); renderer.with_layer(layout.bounds(), |renderer| { @@ -491,13 +491,10 @@ where translation: core::Vector, ) -> Option> { - let overlay = [&mut self.base, &mut self.top] + let mut overlays = [&mut self.base, &mut self.top] .into_iter() - .rev() - .zip( - layout.children().rev().zip(tree.children.iter_mut().rev()), - ) - .find_map(|(child, (layout, tree))| { + .zip(layout.children().zip(tree.children.iter_mut())) + .map(|(child, (layout, tree))| { child.as_widget_mut().overlay( tree, layout, @@ -506,15 +503,21 @@ where ) }); - self.is_overlay_active = overlay.is_some(); - overlay + if let Some(base_overlay) = overlays.next()? { + return Some(base_overlay); + } + + let top_overlay = overlays.next()?; + self.is_top_overlay_active = top_overlay.is_some(); + + top_overlay } } Element::new(Hover { base: base.into(), top: top.into(), - is_overlay_active: false, + is_top_overlay_active: false, }) } -- cgit From 95ac45e33d8490f0cc5c3cdea80be78f338d44e5 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 27 Apr 2024 06:19:25 +0200 Subject: Fix ambiguous link in documentation of `hover` helper --- widget/src/helpers.rs | 2 ++ 1 file changed, 2 insertions(+) (limited to 'widget') diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 0b5adde7..5f27b680 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -294,6 +294,8 @@ where /// /// This works analogously to a [`stack`], but it will only display the layer on top /// when the cursor is over the base. It can be useful for removing visual clutter. +/// +/// [`stack`]: stack() pub fn hover<'a, Message, Theme, Renderer>( base: impl Into>, top: impl Into>, -- cgit From c58155a971987529515570c0d137230e9bd8f4b3 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 27 Apr 2024 06:20:41 +0200 Subject: Set proper size boundaries for `limits` in `Stack::layout` --- widget/src/stack.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'widget') diff --git a/widget/src/stack.rs b/widget/src/stack.rs index 2774d035..5035541b 100644 --- a/widget/src/stack.rs +++ b/widget/src/stack.rs @@ -151,6 +151,8 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { + let limits = limits.width(self.width).height(self.height); + if self.children.is_empty() { return layout::Node::new(limits.resolve( self.width, @@ -159,7 +161,6 @@ where )); } - let limits = limits.width(self.width).height(self.height); let base = self.children[0].as_widget().layout( &mut tree.children[0], renderer, -- cgit From 9c0f2dc9a5ea172afd27b13d55dead40098eb7e3 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 27 Apr 2024 11:21:18 +0200 Subject: Fix top layer clipping in `hover` widget --- widget/src/helpers.rs | 45 ++++++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 21 deletions(-) (limited to 'widget') diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 5f27b680..3940e389 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -375,29 +375,32 @@ where cursor: mouse::Cursor, viewport: &Rectangle, ) { - let mut children = layout.children().zip(&tree.children); + if let Some(bounds) = layout.bounds().intersection(viewport) { + let mut children = layout.children().zip(&tree.children); - let (base_layout, base_tree) = children.next().unwrap(); - - self.base.as_widget().draw( - base_tree, - renderer, - theme, - style, - base_layout, - cursor, - viewport, - ); - - if cursor.is_over(layout.bounds()) || self.is_top_overlay_active { - let (top_layout, top_tree) = children.next().unwrap(); + let (base_layout, base_tree) = children.next().unwrap(); - renderer.with_layer(layout.bounds(), |renderer| { - self.top.as_widget().draw( - top_tree, renderer, theme, style, top_layout, cursor, - viewport, - ); - }); + self.base.as_widget().draw( + base_tree, + renderer, + theme, + style, + base_layout, + cursor, + viewport, + ); + + if cursor.is_over(layout.bounds()) || self.is_top_overlay_active + { + let (top_layout, top_tree) = children.next().unwrap(); + + renderer.with_layer(bounds, |renderer| { + self.top.as_widget().draw( + top_tree, renderer, theme, style, top_layout, + cursor, viewport, + ); + }); + } } } -- cgit From 05c90775816a97e44d499aaff5e9de57b6144e8b Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 27 Apr 2024 11:28:43 +0200 Subject: Propagate mouse movement and button releases unconditionally in `hover` --- widget/src/helpers.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) (limited to 'widget') diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 3940e389..48c5dde4 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -434,7 +434,14 @@ where let mut children = layout.children().zip(&mut tree.children); let (base_layout, base_tree) = children.next().unwrap(); - let top_status = if cursor.is_over(layout.bounds()) { + let top_status = if matches!( + event, + Event::Mouse( + mouse::Event::CursorMoved { .. } + | mouse::Event::ButtonReleased(_) + ) + ) || cursor.is_over(layout.bounds()) + { let (top_layout, top_tree) = children.next().unwrap(); self.top.as_widget_mut().on_event( -- cgit From b5b78d505e22cafccb4ecbf57dc61f536ca558ca Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 30 Apr 2024 07:57:54 +0200 Subject: Introduce `canvas::Cache` grouping Caches with the same `Group` will share their text atlas! --- widget/src/canvas.rs | 1 + 1 file changed, 1 insertion(+) (limited to 'widget') diff --git a/widget/src/canvas.rs b/widget/src/canvas.rs index 42f92de0..be09f163 100644 --- a/widget/src/canvas.rs +++ b/widget/src/canvas.rs @@ -6,6 +6,7 @@ mod program; pub use event::Event; pub use program::Program; +pub use crate::graphics::cache::Group; pub use crate::graphics::geometry::{ fill, gradient, path, stroke, Fill, Gradient, LineCap, LineDash, LineJoin, Path, Stroke, Style, Text, -- cgit From b52c7bb610f593fffc624d461dca17ac50c81626 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 1 May 2024 01:39:43 +0200 Subject: Use an opaque `Id` type for `image::Handle` Hashing pointers is a terrible idea. --- widget/src/image.rs | 8 +++----- widget/src/image/viewer.rs | 6 ++---- 2 files changed, 5 insertions(+), 9 deletions(-) (limited to 'widget') diff --git a/widget/src/image.rs b/widget/src/image.rs index f673c7b3..21d371b7 100644 --- a/widget/src/image.rs +++ b/widget/src/image.rs @@ -11,8 +11,6 @@ use crate::core::{ ContentFit, Element, Layout, Length, Rectangle, Size, Vector, Widget, }; -use std::hash::Hash; - pub use image::{FilterMethod, Handle}; /// Creates a new [`Viewer`] with the given image `Handle`. @@ -128,7 +126,7 @@ pub fn draw( filter_method: FilterMethod, ) where Renderer: image::Renderer, - Handle: Clone + Hash, + Handle: Clone, { let Size { width, height } = renderer.measure_image(handle); let image_size = Size::new(width as f32, height as f32); @@ -167,7 +165,7 @@ impl Widget for Image where Renderer: image::Renderer, - Handle: Clone + Hash, + Handle: Clone, { fn size(&self) -> Size { Size { @@ -216,7 +214,7 @@ impl<'a, Message, Theme, Renderer, Handle> From> for Element<'a, Message, Theme, Renderer> where Renderer: image::Renderer, - Handle: Clone + Hash + 'a, + Handle: Clone + 'a, { fn from(image: Image) -> Element<'a, Message, Theme, Renderer> { Element::new(image) diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs index 75d73b19..214cb996 100644 --- a/widget/src/image/viewer.rs +++ b/widget/src/image/viewer.rs @@ -10,8 +10,6 @@ use crate::core::{ Vector, Widget, }; -use std::hash::Hash; - /// A frame that displays an image with the ability to zoom in/out and pan. #[allow(missing_debug_implementations)] pub struct Viewer { @@ -94,7 +92,7 @@ impl Widget for Viewer where Renderer: image::Renderer, - Handle: Clone + Hash, + Handle: Clone, { fn tag(&self) -> tree::Tag { tree::Tag::of::() @@ -401,7 +399,7 @@ impl<'a, Message, Theme, Renderer, Handle> From> where Renderer: 'a + image::Renderer, Message: 'a, - Handle: Clone + Hash + 'a, + Handle: Clone + 'a, { fn from(viewer: Viewer) -> Element<'a, Message, Theme, Renderer> { Element::new(viewer) -- cgit From aae8e4f5cfabfc3725ac938023fa29a6737a380c Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 2 May 2024 17:23:32 +0200 Subject: Fix `clippy` lints for new `1.78` stable toolchain --- widget/src/keyed/column.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'widget') diff --git a/widget/src/keyed/column.rs b/widget/src/keyed/column.rs index a34ce9e6..fdaadefa 100644 --- a/widget/src/keyed/column.rs +++ b/widget/src/keyed/column.rs @@ -224,7 +224,7 @@ where ); if state.keys != self.keys { - state.keys = self.keys.clone(); + state.keys.clone_from(&self.keys); } } -- cgit From 09a6bcfffc24f5abdc8709403bab7ae1e01563f1 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 2 May 2024 13:15:17 +0200 Subject: Add `Image` rotation support Co-authored-by: DKolter <68352124+DKolter@users.noreply.github.com> --- widget/src/image.rs | 73 ++++++++++++++++++++++++++++++++++------------ widget/src/image/viewer.rs | 2 ++ widget/src/svg.rs | 65 +++++++++++++++++++++++++++++++---------- 3 files changed, 107 insertions(+), 33 deletions(-) (limited to 'widget') diff --git a/widget/src/image.rs b/widget/src/image.rs index 21d371b7..3a0a5e53 100644 --- a/widget/src/image.rs +++ b/widget/src/image.rs @@ -1,5 +1,6 @@ //! Display images in your user interface. pub mod viewer; +use iced_renderer::core::{Point, RotationLayout}; pub use viewer::Viewer; use crate::core::image; @@ -8,7 +9,7 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::widget::Tree; use crate::core::{ - ContentFit, Element, Layout, Length, Rectangle, Size, Vector, Widget, + ContentFit, Element, Layout, Length, Rectangle, Size, Widget, }; pub use image::{FilterMethod, Handle}; @@ -36,6 +37,8 @@ pub struct Image { height: Length, content_fit: ContentFit, filter_method: FilterMethod, + rotation: f32, + rotation_layout: RotationLayout, } impl Image { @@ -47,6 +50,8 @@ impl Image { height: Length::Shrink, content_fit: ContentFit::Contain, filter_method: FilterMethod::default(), + rotation: 0.0, + rotation_layout: RotationLayout::Change, } } @@ -75,6 +80,18 @@ impl Image { self.filter_method = filter_method; self } + + /// Rotates the [`Image`] by the given angle in radians. + pub fn rotation(mut self, degrees: f32) -> Self { + self.rotation = degrees; + self + } + + /// Sets the [`RotationLayout`] of the [`Image`]. + pub fn rotation_layout(mut self, rotation_layout: RotationLayout) -> Self { + self.rotation_layout = rotation_layout; + self + } } /// Computes the layout of an [`Image`]. @@ -85,22 +102,25 @@ pub fn layout( width: Length, height: Length, content_fit: ContentFit, + rotation: f32, + rotation_layout: RotationLayout, ) -> layout::Node where Renderer: image::Renderer, { // The raw w/h of the underlying image - let image_size = { - let Size { width, height } = renderer.measure_image(handle); + let image_size = renderer.measure_image(handle); + let image_size = + Size::new(image_size.width as f32, image_size.height as f32); - Size::new(width as f32, height as f32) - }; + // The rotated size of the image + let rotated_size = rotation_layout.apply_to_size(image_size, rotation); // The size to be available to the widget prior to `Shrink`ing - let raw_size = limits.resolve(width, height, image_size); + let raw_size = limits.resolve(width, height, rotated_size); // The uncropped size of the image when fit to the bounds above - let full_size = content_fit.fit(image_size, raw_size); + let full_size = content_fit.fit(rotated_size, raw_size); // Shrink the widget to fit the resized image, if requested let final_size = Size { @@ -124,32 +144,45 @@ pub fn draw( handle: &Handle, content_fit: ContentFit, filter_method: FilterMethod, + rotation: f32, + rotation_layout: RotationLayout, ) where Renderer: image::Renderer, Handle: Clone, { let Size { width, height } = renderer.measure_image(handle); let image_size = Size::new(width as f32, height as f32); + let rotated_size = rotation_layout.apply_to_size(image_size, rotation); let bounds = layout.bounds(); - let adjusted_fit = content_fit.fit(image_size, bounds.size()); + let adjusted_fit = content_fit.fit(rotated_size, bounds.size()); + let scale = Size::new( + adjusted_fit.width / rotated_size.width, + adjusted_fit.height / rotated_size.height, + ); let render = |renderer: &mut Renderer| { - let offset = Vector::new( - (bounds.width - adjusted_fit.width).max(0.0) / 2.0, - (bounds.height - adjusted_fit.height).max(0.0) / 2.0, - ); - - let drawing_bounds = Rectangle { - width: adjusted_fit.width, - height: adjusted_fit.height, - ..bounds + let position = match content_fit { + ContentFit::None => Point::new( + bounds.position().x + + (rotated_size.width - image_size.width) / 2.0, + bounds.position().y + + (rotated_size.height - image_size.height) / 2.0, + ), + _ => Point::new( + bounds.center_x() - image_size.width / 2.0, + bounds.center_y() - image_size.height / 2.0, + ), }; + let drawing_bounds = Rectangle::new(position, image_size); + renderer.draw_image( handle.clone(), filter_method, - drawing_bounds + offset, + drawing_bounds, + rotation, + scale, ); }; @@ -187,6 +220,8 @@ where self.width, self.height, self.content_fit, + self.rotation, + self.rotation_layout, ) } @@ -206,6 +241,8 @@ where &self.handle, self.content_fit, self.filter_method, + self.rotation, + self.rotation_layout, ); } } diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs index 214cb996..ccdfdebb 100644 --- a/widget/src/image/viewer.rs +++ b/widget/src/image/viewer.rs @@ -341,6 +341,8 @@ where y: bounds.y, ..Rectangle::with_size(image_size) }, + 0.0, + Size::UNIT, ); }); }); diff --git a/widget/src/svg.rs b/widget/src/svg.rs index eb142189..21946af8 100644 --- a/widget/src/svg.rs +++ b/widget/src/svg.rs @@ -5,8 +5,8 @@ use crate::core::renderer; use crate::core::svg; use crate::core::widget::Tree; use crate::core::{ - Color, ContentFit, Element, Layout, Length, Rectangle, Size, Theme, Vector, - Widget, + Color, ContentFit, Element, Layout, Length, Point, Rectangle, + RotationLayout, Size, Theme, Widget, }; use std::path::PathBuf; @@ -29,6 +29,8 @@ where height: Length, content_fit: ContentFit, class: Theme::Class<'a>, + rotation: f32, + rotation_layout: RotationLayout, } impl<'a, Theme> Svg<'a, Theme> @@ -43,6 +45,8 @@ where height: Length::Shrink, content_fit: ContentFit::Contain, class: Theme::default(), + rotation: 0.0, + rotation_layout: RotationLayout::Change, } } @@ -95,6 +99,18 @@ where self.class = class.into(); self } + + /// Rotates the [`Svg`] by the given angle in radians. + pub fn rotation(mut self, degrees: f32) -> Self { + self.rotation = degrees; + self + } + + /// Sets the [`RotationLayout`] of the [`Svg`]. + pub fn rotation_layout(mut self, rotation_layout: RotationLayout) -> Self { + self.rotation_layout = rotation_layout; + self + } } impl<'a, Message, Theme, Renderer> Widget @@ -120,11 +136,16 @@ where let Size { width, height } = renderer.measure_svg(&self.handle); let image_size = Size::new(width as f32, height as f32); + // The rotated size of the svg + let rotated_size = self + .rotation_layout + .apply_to_size(image_size, self.rotation); + // The size to be available to the widget prior to `Shrink`ing - let raw_size = limits.resolve(self.width, self.height, image_size); + let raw_size = limits.resolve(self.width, self.height, rotated_size); // The uncropped size of the image when fit to the bounds above - let full_size = self.content_fit.fit(image_size, raw_size); + let full_size = self.content_fit.fit(rotated_size, raw_size); // Shrink the widget to fit the resized image, if requested let final_size = Size { @@ -153,23 +174,35 @@ where ) { let Size { width, height } = renderer.measure_svg(&self.handle); let image_size = Size::new(width as f32, height as f32); + let rotated_size = self + .rotation_layout + .apply_to_size(image_size, self.rotation); let bounds = layout.bounds(); - let adjusted_fit = self.content_fit.fit(image_size, bounds.size()); + let adjusted_fit = self.content_fit.fit(rotated_size, bounds.size()); + let scale = Size::new( + adjusted_fit.width / rotated_size.width, + adjusted_fit.height / rotated_size.height, + ); + let is_mouse_over = cursor.is_over(bounds); let render = |renderer: &mut Renderer| { - let offset = Vector::new( - (bounds.width - adjusted_fit.width).max(0.0) / 2.0, - (bounds.height - adjusted_fit.height).max(0.0) / 2.0, - ); - - let drawing_bounds = Rectangle { - width: adjusted_fit.width, - height: adjusted_fit.height, - ..bounds + let position = match self.content_fit { + ContentFit::None => Point::new( + bounds.position().x + + (rotated_size.width - image_size.width) / 2.0, + bounds.position().y + + (rotated_size.height - image_size.height) / 2.0, + ), + _ => Point::new( + bounds.center_x() - image_size.width / 2.0, + bounds.center_y() - image_size.height / 2.0, + ), }; + let drawing_bounds = Rectangle::new(position, image_size); + let status = if is_mouse_over { Status::Hovered } else { @@ -181,7 +214,9 @@ where renderer.draw_svg( self.handle.clone(), style.color, - drawing_bounds + offset, + drawing_bounds, + self.rotation, + scale, ); }; -- cgit From a57313b23ecb9843856ca0ea08635b6121fcb2cb Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 2 May 2024 15:21:22 +0200 Subject: Simplify image rotation API and its internals --- widget/src/image.rs | 70 +++++++++++++++++---------------------- widget/src/image/viewer.rs | 7 ++-- widget/src/svg.rs | 81 +++++++++++++++++++--------------------------- 3 files changed, 66 insertions(+), 92 deletions(-) (limited to 'widget') diff --git a/widget/src/image.rs b/widget/src/image.rs index 3a0a5e53..45209a91 100644 --- a/widget/src/image.rs +++ b/widget/src/image.rs @@ -1,6 +1,5 @@ //! Display images in your user interface. pub mod viewer; -use iced_renderer::core::{Point, RotationLayout}; pub use viewer::Viewer; use crate::core::image; @@ -9,7 +8,8 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::widget::Tree; use crate::core::{ - ContentFit, Element, Layout, Length, Rectangle, Size, Widget, + ContentFit, Element, Layout, Length, Point, Rectangle, Rotation, Size, + Vector, Widget, }; pub use image::{FilterMethod, Handle}; @@ -37,8 +37,7 @@ pub struct Image { height: Length, content_fit: ContentFit, filter_method: FilterMethod, - rotation: f32, - rotation_layout: RotationLayout, + rotation: Rotation, } impl Image { @@ -48,10 +47,9 @@ impl Image { handle: handle.into(), width: Length::Shrink, height: Length::Shrink, - content_fit: ContentFit::Contain, + content_fit: ContentFit::default(), filter_method: FilterMethod::default(), - rotation: 0.0, - rotation_layout: RotationLayout::Change, + rotation: Rotation::default(), } } @@ -81,15 +79,9 @@ impl Image { self } - /// Rotates the [`Image`] by the given angle in radians. - pub fn rotation(mut self, degrees: f32) -> Self { - self.rotation = degrees; - self - } - - /// Sets the [`RotationLayout`] of the [`Image`]. - pub fn rotation_layout(mut self, rotation_layout: RotationLayout) -> Self { - self.rotation_layout = rotation_layout; + /// Applies the given [`Rotation`] to the [`Image`]. + pub fn rotation(mut self, rotation: impl Into) -> Self { + self.rotation = rotation.into(); self } } @@ -102,8 +94,7 @@ pub fn layout( width: Length, height: Length, content_fit: ContentFit, - rotation: f32, - rotation_layout: RotationLayout, + rotation: Rotation, ) -> layout::Node where Renderer: image::Renderer, @@ -114,7 +105,7 @@ where Size::new(image_size.width as f32, image_size.height as f32); // The rotated size of the image - let rotated_size = rotation_layout.apply_to_size(image_size, rotation); + let rotated_size = rotation.apply(image_size); // The size to be available to the widget prior to `Shrink`ing let raw_size = limits.resolve(width, height, rotated_size); @@ -144,45 +135,44 @@ pub fn draw( handle: &Handle, content_fit: ContentFit, filter_method: FilterMethod, - rotation: f32, - rotation_layout: RotationLayout, + rotation: Rotation, ) where Renderer: image::Renderer, Handle: Clone, { let Size { width, height } = renderer.measure_image(handle); let image_size = Size::new(width as f32, height as f32); - let rotated_size = rotation_layout.apply_to_size(image_size, rotation); + let rotated_size = rotation.apply(image_size); let bounds = layout.bounds(); let adjusted_fit = content_fit.fit(rotated_size, bounds.size()); - let scale = Size::new( + + let scale = Vector::new( adjusted_fit.width / rotated_size.width, adjusted_fit.height / rotated_size.height, ); - let render = |renderer: &mut Renderer| { - let position = match content_fit { - ContentFit::None => Point::new( - bounds.position().x - + (rotated_size.width - image_size.width) / 2.0, - bounds.position().y - + (rotated_size.height - image_size.height) / 2.0, - ), - _ => Point::new( - bounds.center_x() - image_size.width / 2.0, - bounds.center_y() - image_size.height / 2.0, - ), - }; + let final_size = image_size * scale; + + let position = match content_fit { + ContentFit::None => Point::new( + bounds.x + (rotated_size.width - adjusted_fit.width) / 2.0, + bounds.y + (rotated_size.height - adjusted_fit.height) / 2.0, + ), + _ => Point::new( + bounds.center_x() - final_size.width / 2.0, + bounds.center_y() - final_size.height / 2.0, + ), + }; - let drawing_bounds = Rectangle::new(position, image_size); + let drawing_bounds = Rectangle::new(position, final_size); + let render = |renderer: &mut Renderer| { renderer.draw_image( handle.clone(), filter_method, drawing_bounds, - rotation, - scale, + rotation.radians(), ); }; @@ -221,7 +211,6 @@ where self.height, self.content_fit, self.rotation, - self.rotation_layout, ) } @@ -242,7 +231,6 @@ where self.content_fit, self.filter_method, self.rotation, - self.rotation_layout, ); } } diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs index ccdfdebb..ee4c0fba 100644 --- a/widget/src/image/viewer.rs +++ b/widget/src/image/viewer.rs @@ -6,8 +6,8 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - Clipboard, Element, Layout, Length, Pixels, Point, Rectangle, Shell, Size, - Vector, Widget, + Clipboard, Element, Layout, Length, Pixels, Point, Radians, Rectangle, + Shell, Size, Vector, Widget, }; /// A frame that displays an image with the ability to zoom in/out and pan. @@ -341,8 +341,7 @@ where y: bounds.y, ..Rectangle::with_size(image_size) }, - 0.0, - Size::UNIT, + Radians(0.0), ); }); }); diff --git a/widget/src/svg.rs b/widget/src/svg.rs index 21946af8..c1fccba1 100644 --- a/widget/src/svg.rs +++ b/widget/src/svg.rs @@ -5,8 +5,8 @@ use crate::core::renderer; use crate::core::svg; use crate::core::widget::Tree; use crate::core::{ - Color, ContentFit, Element, Layout, Length, Point, Rectangle, - RotationLayout, Size, Theme, Widget, + Color, ContentFit, Element, Layout, Length, Point, Rectangle, Rotation, + Size, Theme, Vector, Widget, }; use std::path::PathBuf; @@ -29,8 +29,7 @@ where height: Length, content_fit: ContentFit, class: Theme::Class<'a>, - rotation: f32, - rotation_layout: RotationLayout, + rotation: Rotation, } impl<'a, Theme> Svg<'a, Theme> @@ -45,8 +44,7 @@ where height: Length::Shrink, content_fit: ContentFit::Contain, class: Theme::default(), - rotation: 0.0, - rotation_layout: RotationLayout::Change, + rotation: Rotation::default(), } } @@ -100,15 +98,9 @@ where self } - /// Rotates the [`Svg`] by the given angle in radians. - pub fn rotation(mut self, degrees: f32) -> Self { - self.rotation = degrees; - self - } - - /// Sets the [`RotationLayout`] of the [`Svg`]. - pub fn rotation_layout(mut self, rotation_layout: RotationLayout) -> Self { - self.rotation_layout = rotation_layout; + /// Applies the given [`Rotation`] to the [`Svg`]. + pub fn rotation(mut self, rotation: impl Into) -> Self { + self.rotation = rotation.into(); self } } @@ -137,9 +129,7 @@ where let image_size = Size::new(width as f32, height as f32); // The rotated size of the svg - let rotated_size = self - .rotation_layout - .apply_to_size(image_size, self.rotation); + let rotated_size = self.rotation.apply(image_size); // The size to be available to the widget prior to `Shrink`ing let raw_size = limits.resolve(self.width, self.height, rotated_size); @@ -174,49 +164,46 @@ where ) { let Size { width, height } = renderer.measure_svg(&self.handle); let image_size = Size::new(width as f32, height as f32); - let rotated_size = self - .rotation_layout - .apply_to_size(image_size, self.rotation); + let rotated_size = self.rotation.apply(image_size); let bounds = layout.bounds(); let adjusted_fit = self.content_fit.fit(rotated_size, bounds.size()); - let scale = Size::new( + let scale = Vector::new( adjusted_fit.width / rotated_size.width, adjusted_fit.height / rotated_size.height, ); + let final_size = image_size * scale; + + let position = match self.content_fit { + ContentFit::None => Point::new( + bounds.x + (rotated_size.width - adjusted_fit.width) / 2.0, + bounds.y + (rotated_size.height - adjusted_fit.height) / 2.0, + ), + _ => Point::new( + bounds.center_x() - final_size.width / 2.0, + bounds.center_y() - final_size.height / 2.0, + ), + }; + + let drawing_bounds = Rectangle::new(position, final_size); + let is_mouse_over = cursor.is_over(bounds); - let render = |renderer: &mut Renderer| { - let position = match self.content_fit { - ContentFit::None => Point::new( - bounds.position().x - + (rotated_size.width - image_size.width) / 2.0, - bounds.position().y - + (rotated_size.height - image_size.height) / 2.0, - ), - _ => Point::new( - bounds.center_x() - image_size.width / 2.0, - bounds.center_y() - image_size.height / 2.0, - ), - }; - - let drawing_bounds = Rectangle::new(position, image_size); - - let status = if is_mouse_over { - Status::Hovered - } else { - Status::Idle - }; - - let style = theme.style(&self.class, status); + let status = if is_mouse_over { + Status::Hovered + } else { + Status::Idle + }; + let style = theme.style(&self.class, status); + + let render = |renderer: &mut Renderer| { renderer.draw_svg( self.handle.clone(), style.color, drawing_bounds, - self.rotation, - scale, + self.rotation.radians(), ); }; -- cgit From 8efd32c51bc04113a393dbf85c962f7e0343a127 Mon Sep 17 00:00:00 2001 From: myuujiku Date: Thu, 2 May 2024 21:18:47 +0200 Subject: Apply bounds check when grabbing `image::Viewer` --- widget/src/image/viewer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'widget') diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs index 214cb996..2e6a7528 100644 --- a/widget/src/image/viewer.rs +++ b/widget/src/image/viewer.rs @@ -216,7 +216,7 @@ where event::Status::Captured } Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { - let Some(cursor_position) = cursor.position() else { + let Some(cursor_position) = cursor.position_over(bounds) else { return event::Status::Ignored; }; -- cgit From 15057a05c118dafcb8cf90d4119e66caaa6026c5 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 3 May 2024 09:11:46 +0200 Subject: Introduce `center` widget helper ... and also make `center_x` and `center_y` set `width` and `height` to `Length::Fill`, respectively. This targets the most common use case when centering things and removes a bunch of boilerplate as a result. --- widget/src/container.rs | 62 +++++++++++++++++++++++++++++++++++++++++++++++-- widget/src/helpers.rs | 21 +++++++++++++++++ 2 files changed, 81 insertions(+), 2 deletions(-) (limited to 'widget') diff --git a/widget/src/container.rs b/widget/src/container.rs index 21405722..8b6638d4 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -92,6 +92,49 @@ where self } + /// Sets the [`Container`] to fill the available space in the horizontal axis. + /// + /// This can be useful to quickly position content when chained with + /// alignment functions—like [`center_x`]. + /// + /// Calling this method is equivalent to calling [`width`] with a + /// [`Length::Fill`]. + /// + /// [`center_x`]: Self::center_x + /// [`width`]: Self::width + pub fn fill_x(self) -> Self { + self.width(Length::Fill) + } + + /// Sets the [`Container`] to fill the available space in the vetical axis. + /// + /// This can be useful to quickly position content when chained with + /// alignment functions—like [`center_y`]. + /// + /// Calling this method is equivalent to calling [`height`] with a + /// [`Length::Fill`]. + /// + /// [`center_y`]: Self::center_x + /// [`height`]: Self::height + pub fn fill_y(self) -> Self { + self.height(Length::Fill) + } + + /// Sets the [`Container`] to fill all the available space. + /// + /// This can be useful to quickly position content when chained with + /// alignment functions—like [`center`]. + /// + /// Calling this method is equivalent to chaining [`fill_x`] and + /// [`fill_y`]. + /// + /// [`center`]: Self::center + /// [`fill_x`]: Self::fill_x + /// [`fill_y`]: Self::fill_y + pub fn fill(self) -> Self { + self.width(Length::Fill).height(Length::Fill) + } + /// Sets the maximum width of the [`Container`]. pub fn max_width(mut self, max_width: impl Into) -> Self { self.max_width = max_width.into().0; @@ -116,18 +159,33 @@ where self } - /// Centers the contents in the horizontal axis of the [`Container`]. + /// Sets the [`Container`] to fill the available space in the horizontal axis + /// and centers its contents there. pub fn center_x(mut self) -> Self { + self.width = Length::Fill; self.horizontal_alignment = alignment::Horizontal::Center; self } - /// Centers the contents in the vertical axis of the [`Container`]. + /// Sets the [`Container`] to fill the available space in the vertical axis + /// and centers its contents there. pub fn center_y(mut self) -> Self { + self.height = Length::Fill; self.vertical_alignment = alignment::Vertical::Center; self } + /// Centers the contents in both the horizontal and vertical axes of the + /// [`Container`]. + /// + /// This is equivalent to chaining [`center_x`] and [`center_y`]. + /// + /// [`center_x`]: Self::center_x + /// [`center_y`]: Self::center_y + pub fn center(self) -> Self { + self.center_x().center_y() + } + /// Sets whether the contents of the [`Container`] should be clipped on /// overflow. pub fn clip(mut self, clip: bool) -> Self { diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 48c5dde4..fd8614f5 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -78,6 +78,27 @@ where Container::new(content) } +/// Creates a new [`Container`] that fills all the available space +/// and centers its contents inside. +/// +/// This is equivalent to: +/// ```rust,no_run +/// # use iced_widget::Container; +/// # fn container(x: A) -> Container<'static, ()> { unreachable!() } +/// let centered = container("Centered!").center(); +/// ``` +/// +/// [`Container`]: crate::Container +pub fn center<'a, Message, Theme, Renderer>( + content: impl Into>, +) -> Container<'a, Message, Theme, Renderer> +where + Theme: container::Catalog + 'a, + Renderer: core::Renderer, +{ + container(content).fill().center() +} + /// Creates a new [`Column`] with the given children. pub fn column<'a, Message, Theme, Renderer>( children: impl IntoIterator>, -- cgit From fa9e1d96ea1924b51749b775ea0e67e69bc8a305 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 3 May 2024 13:25:58 +0200 Subject: Introduce dynamic `opacity` support for `Image` and `Svg` --- widget/src/image.rs | 14 ++++++++++++++ widget/src/image/viewer.rs | 1 + widget/src/svg.rs | 12 ++++++++++++ 3 files changed, 27 insertions(+) (limited to 'widget') diff --git a/widget/src/image.rs b/widget/src/image.rs index 45209a91..80e17263 100644 --- a/widget/src/image.rs +++ b/widget/src/image.rs @@ -38,6 +38,7 @@ pub struct Image { content_fit: ContentFit, filter_method: FilterMethod, rotation: Rotation, + opacity: f32, } impl Image { @@ -50,6 +51,7 @@ impl Image { content_fit: ContentFit::default(), filter_method: FilterMethod::default(), rotation: Rotation::default(), + opacity: 1.0, } } @@ -84,6 +86,15 @@ impl Image { self.rotation = rotation.into(); self } + + /// Sets the opacity of the [`Image`]. + /// + /// It should be in the [0.0, 1.0] range—`0.0` meaning completely transparent, + /// and `1.0` meaning completely opaque. + pub fn opacity(mut self, opacity: impl Into) -> Self { + self.opacity = opacity.into(); + self + } } /// Computes the layout of an [`Image`]. @@ -136,6 +147,7 @@ pub fn draw( content_fit: ContentFit, filter_method: FilterMethod, rotation: Rotation, + opacity: f32, ) where Renderer: image::Renderer, Handle: Clone, @@ -173,6 +185,7 @@ pub fn draw( filter_method, drawing_bounds, rotation.radians(), + opacity, ); }; @@ -231,6 +244,7 @@ where self.content_fit, self.filter_method, self.rotation, + self.opacity, ); } } diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs index 5eb76452..8fe6f021 100644 --- a/widget/src/image/viewer.rs +++ b/widget/src/image/viewer.rs @@ -342,6 +342,7 @@ where ..Rectangle::with_size(image_size) }, Radians(0.0), + 1.0, ); }); }); diff --git a/widget/src/svg.rs b/widget/src/svg.rs index c1fccba1..4551bcad 100644 --- a/widget/src/svg.rs +++ b/widget/src/svg.rs @@ -30,6 +30,7 @@ where content_fit: ContentFit, class: Theme::Class<'a>, rotation: Rotation, + opacity: f32, } impl<'a, Theme> Svg<'a, Theme> @@ -45,6 +46,7 @@ where content_fit: ContentFit::Contain, class: Theme::default(), rotation: Rotation::default(), + opacity: 1.0, } } @@ -103,6 +105,15 @@ where self.rotation = rotation.into(); self } + + /// Sets the opacity of the [`Svg`]. + /// + /// It should be in the [0.0, 1.0] range—`0.0` meaning completely transparent, + /// and `1.0` meaning completely opaque. + pub fn opacity(mut self, opacity: impl Into) -> Self { + self.opacity = opacity.into(); + self + } } impl<'a, Message, Theme, Renderer> Widget @@ -204,6 +215,7 @@ where style.color, drawing_bounds, self.rotation.radians(), + self.opacity, ); }; -- cgit