summaryrefslogtreecommitdiffstats
path: root/widget
diff options
context:
space:
mode:
authorLibravatar gigas002 <gigas002@pm.me>2024-05-08 19:16:20 +0900
committerLibravatar gigas002 <gigas002@pm.me>2024-05-08 19:16:20 +0900
commitff26fb7df43bd241253af85e0621e2cc030e9cec (patch)
tree2b3272bb178f8757169511966589fe6a106733f4 /widget
parent0ebe0629cef37aee5c48b9409fc36618a3a3e60d (diff)
parent477887b3870aa5fbdab96c3a06f3b930462d7842 (diff)
downloadiced-ff26fb7df43bd241253af85e0621e2cc030e9cec.tar.gz
iced-ff26fb7df43bd241253af85e0621e2cc030e9cec.tar.bz2
iced-ff26fb7df43bd241253af85e0621e2cc030e9cec.zip
Merge branch 'iced-rs-master' into viewer_content_fit
Diffstat (limited to 'widget')
-rw-r--r--widget/src/canvas.rs1
-rw-r--r--widget/src/container.rs62
-rw-r--r--widget/src/helpers.rs460
-rw-r--r--widget/src/image.rs87
-rw-r--r--widget/src/image/viewer.rs14
-rw-r--r--widget/src/keyed/column.rs2
-rw-r--r--widget/src/lib.rs3
-rw-r--r--widget/src/mouse_area.rs2
-rw-r--r--widget/src/scrollable.rs378
-rw-r--r--widget/src/stack.rs333
-rw-r--r--widget/src/svg.rs80
-rw-r--r--widget/src/text_editor.rs41
12 files changed, 1205 insertions, 258 deletions
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,
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<Pixels>) -> 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 61789c19..fd8614f5 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};
@@ -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
@@ -65,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<A>(x: A) -> Container<'static, ()> { unreachable!() }
+/// let centered = container("Centered!").center();
+/// ```
+///
+/// [`Container`]: crate::Container
+pub fn center<'a, Message, Theme, Renderer>(
+ content: impl Into<Element<'a, Message, Theme, Renderer>>,
+) -> 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<Item = Element<'a, Message, Theme, Renderer>>,
@@ -98,6 +132,428 @@ 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<Item = Element<'a, Message, Theme, Renderer>>,
+) -> Stack<'a, Message, Theme, Renderer>
+where
+ Renderer: core::Renderer,
+{
+ Stack::with_children(children)
+}
+
+/// Wraps the given widget and captures any mouse button presses inside the bounds of
+/// 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.
+///
+/// [`Stack`]: crate::Stack
+pub fn opaque<'a, Message, Theme, Renderer>(
+ content: impl Into<Element<'a, Message, Theme, Renderer>>,
+) -> 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<Message, Theme, Renderer>
+ 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<Tree> {
+ self.content.as_widget().children()
+ }
+
+ fn diff(&self, tree: &mut Tree) {
+ self.content.as_widget().diff(tree);
+ }
+
+ fn size(&self) -> Size<Length> {
+ self.content.as_widget().size()
+ }
+
+ fn size_hint(&self) -> Size<Length> {
+ 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<Message>,
+ ) {
+ 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<core::overlay::Element<'b, Message, Theme, Renderer>>
+ {
+ self.content.as_widget_mut().overlay(
+ state,
+ layout,
+ renderer,
+ translation,
+ )
+ }
+ }
+
+ Element::new(Opaque {
+ content: content.into(),
+ })
+}
+
+/// 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.
+///
+/// [`stack`]: stack()
+pub fn hover<'a, Message, Theme, Renderer>(
+ base: impl Into<Element<'a, Message, Theme, Renderer>>,
+ top: impl Into<Element<'a, Message, Theme, Renderer>>,
+) -> 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_top_overlay_active: bool,
+ }
+
+ impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
+ for Hover<'a, Message, Theme, Renderer>
+ where
+ Renderer: core::Renderer,
+ {
+ fn tag(&self) -> tree::Tag {
+ struct Tag;
+ tree::Tag::of::<Tag>()
+ }
+
+ fn children(&self) -> Vec<Tree> {
+ 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<Length> {
+ self.base.as_widget().size()
+ }
+
+ fn size_hint(&self) -> Size<Length> {
+ 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,
+ ) {
+ 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();
+
+ renderer.with_layer(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<Message>,
+ ) {
+ 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 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(
+ 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<core::overlay::Element<'b, Message, Theme, Renderer>>
+ {
+ let mut overlays = [&mut self.base, &mut self.top]
+ .into_iter()
+ .zip(layout.children().zip(tree.children.iter_mut()))
+ .map(|(child, (layout, tree))| {
+ child.as_widget_mut().overlay(
+ tree,
+ layout,
+ renderer,
+ translation,
+ )
+ });
+
+ 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_top_overlay_active: false,
+ })
+}
+
/// Creates a new [`Scrollable`] with the provided content.
///
/// [`Scrollable`]: crate::Scrollable
diff --git a/widget/src/image.rs b/widget/src/image.rs
index f673c7b3..80e17263 100644
--- a/widget/src/image.rs
+++ b/widget/src/image.rs
@@ -8,11 +8,10 @@ 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, Point, Rectangle, Rotation, Size,
+ Vector, Widget,
};
-use std::hash::Hash;
-
pub use image::{FilterMethod, Handle};
/// Creates a new [`Viewer`] with the given image `Handle`.
@@ -38,6 +37,8 @@ pub struct Image<Handle> {
height: Length,
content_fit: ContentFit,
filter_method: FilterMethod,
+ rotation: Rotation,
+ opacity: f32,
}
impl<Handle> Image<Handle> {
@@ -47,8 +48,10 @@ impl<Handle> Image<Handle> {
handle: handle.into(),
width: Length::Shrink,
height: Length::Shrink,
- content_fit: ContentFit::Contain,
+ content_fit: ContentFit::default(),
filter_method: FilterMethod::default(),
+ rotation: Rotation::default(),
+ opacity: 1.0,
}
}
@@ -77,6 +80,21 @@ impl<Handle> Image<Handle> {
self.filter_method = filter_method;
self
}
+
+ /// Applies the given [`Rotation`] to the [`Image`].
+ pub fn rotation(mut self, rotation: impl Into<Rotation>) -> Self {
+ 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<f32>) -> Self {
+ self.opacity = opacity.into();
+ self
+ }
}
/// Computes the layout of an [`Image`].
@@ -87,22 +105,24 @@ pub fn layout<Renderer, Handle>(
width: Length,
height: Length,
content_fit: ContentFit,
+ rotation: Rotation,
) -> layout::Node
where
Renderer: image::Renderer<Handle = Handle>,
{
// 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.apply(image_size);
// 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 {
@@ -126,32 +146,46 @@ pub fn draw<Renderer, Handle>(
handle: &Handle,
content_fit: ContentFit,
filter_method: FilterMethod,
+ rotation: Rotation,
+ opacity: f32,
) where
Renderer: image::Renderer<Handle = Handle>,
- Handle: Clone + Hash,
+ 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.apply(image_size);
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 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 scale = Vector::new(
+ adjusted_fit.width / rotated_size.width,
+ adjusted_fit.height / rotated_size.height,
+ );
- let drawing_bounds = Rectangle {
- width: adjusted_fit.width,
- height: adjusted_fit.height,
- ..bounds
- };
+ 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, final_size);
+
+ let render = |renderer: &mut Renderer| {
renderer.draw_image(
handle.clone(),
filter_method,
- drawing_bounds + offset,
+ drawing_bounds,
+ rotation.radians(),
+ opacity,
);
};
@@ -167,7 +201,7 @@ impl<Message, Theme, Renderer, Handle> Widget<Message, Theme, Renderer>
for Image<Handle>
where
Renderer: image::Renderer<Handle = Handle>,
- Handle: Clone + Hash,
+ Handle: Clone,
{
fn size(&self) -> Size<Length> {
Size {
@@ -189,6 +223,7 @@ where
self.width,
self.height,
self.content_fit,
+ self.rotation,
)
}
@@ -208,6 +243,8 @@ where
&self.handle,
self.content_fit,
self.filter_method,
+ self.rotation,
+ self.opacity,
);
}
}
@@ -216,7 +253,7 @@ impl<'a, Message, Theme, Renderer, Handle> From<Image<Handle>>
for Element<'a, Message, Theme, Renderer>
where
Renderer: image::Renderer<Handle = Handle>,
- Handle: Clone + Hash + 'a,
+ Handle: Clone + 'a,
{
fn from(image: Image<Handle>) -> Element<'a, Message, Theme, Renderer> {
Element::new(image)
diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs
index e57857c1..03a9d895 100644
--- a/widget/src/image/viewer.rs
+++ b/widget/src/image/viewer.rs
@@ -6,12 +6,10 @@ use crate::core::mouse;
use crate::core::renderer;
use crate::core::widget::tree::{self, Tree};
use crate::core::{
- Clipboard, ContentFit, Element, Layout, Length, Pixels, Point, Rectangle,
+ Clipboard, ContentFit, Element, Layout, Length, Pixels, Point, Radians, Rectangle,
Shell, Size, 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<Handle> {
@@ -102,7 +100,7 @@ impl<Message, Theme, Renderer, Handle> Widget<Message, Theme, Renderer>
for Viewer<Handle>
where
Renderer: image::Renderer<Handle = Handle>,
- Handle: Clone + Hash,
+ Handle: Clone,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<State>()
@@ -222,7 +220,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;
};
@@ -308,7 +306,7 @@ where
} else if is_mouse_over {
mouse::Interaction::Grab
} else {
- mouse::Interaction::Idle
+ mouse::Interaction::None
}
}
@@ -354,6 +352,8 @@ where
self.handle.clone(),
self.filter_method,
drawing_bounds,
+ Radians(0.0),
+ 1.0,
);
});
};
@@ -414,7 +414,7 @@ impl<'a, Message, Theme, Renderer, Handle> From<Viewer<Handle>>
where
Renderer: 'a + image::Renderer<Handle = Handle>,
Message: 'a,
- Handle: Clone + Hash + 'a,
+ Handle: Clone + 'a,
{
fn from(viewer: Viewer<Handle>) -> Element<'a, Message, Theme, Renderer> {
Element::new(viewer)
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);
}
}
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/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 668c5372..6fc00f87 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)
@@ -422,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)
@@ -435,15 +579,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 +627,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,
@@ -498,148 +645,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,
- );
-
- 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);
-
- 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,
- );
-
- 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);
-
- notify_on_scroll(
- state,
- &self.on_scroll,
- bounds,
- content_bounds,
- shell,
- );
-
- event_status = event::Status::Captured;
- }
- }
- _ => {}
- }
- }
-
event_status
}
@@ -659,6 +664,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 +713,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 +776,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 {
@@ -850,7 +859,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);
@@ -961,51 +970,54 @@ pub fn scroll_to<Message: 'static>(
Command::widget(operation::scrollable::scroll_to(id.0, offset))
}
+/// Returns [`true`] if the viewport actually changed.
fn notify_on_scroll<Message>(
state: &mut State,
on_scroll: &Option<Box<dyn Fn(Viewport) -> 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)]
diff --git a/widget/src/stack.rs b/widget/src/stack.rs
new file mode 100644
index 00000000..5035541b
--- /dev/null
+++ b/widget/src/stack.rs
@@ -0,0 +1,333 @@
+//! Display content on top of other content.
+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<Element<'a, Message, Theme, Renderer>>,
+}
+
+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<Item = Element<'a, Message, Theme, Renderer>>,
+ ) -> 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<Element<'a, Message, Theme, Renderer>>,
+ ) -> Self {
+ Self {
+ width: Length::Shrink,
+ height: Length::Shrink,
+ children,
+ }
+ }
+
+ /// Sets the width of the [`Stack`].
+ pub fn width(mut self, width: impl Into<Length>) -> Self {
+ self.width = width.into();
+ self
+ }
+
+ /// Sets the height of the [`Stack`].
+ pub fn height(mut self, height: impl Into<Length>) -> Self {
+ self.height = height.into();
+ self
+ }
+
+ /// Adds an element to the [`Stack`].
+ pub fn push(
+ mut self,
+ child: impl Into<Element<'a, Message, Theme, Renderer>>,
+ ) -> 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<impl Into<Element<'a, Message, Theme, Renderer>>>,
+ ) -> 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<Item = Element<'a, Message, Theme, Renderer>>,
+ ) -> 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<Message, Theme, Renderer>
+ for Stack<'a, Message, Theme, Renderer>
+where
+ Renderer: crate::core::Renderer,
+{
+ fn children(&self) -> Vec<Tree> {
+ self.children.iter().map(Tree::new).collect()
+ }
+
+ fn diff(&self, tree: &mut Tree) {
+ tree.diff_children(&self.children);
+ }
+
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: self.width,
+ height: self.height,
+ }
+ }
+
+ fn layout(
+ &self,
+ tree: &mut Tree,
+ 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,
+ self.height,
+ Size::ZERO,
+ ));
+ }
+
+ let base = self.children[0].as_widget().layout(
+ &mut tree.children[0],
+ renderer,
+ &limits,
+ );
+
+ let size = limits.resolve(self.width, self.height, 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<Message>,
+ ) {
+ 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::None)
+ .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::Element<'b, Message, Theme, Renderer>> {
+ overlay::from_children(
+ &mut self.children,
+ tree,
+ layout,
+ renderer,
+ translation,
+ )
+ }
+}
+
+impl<'a, Message, Theme, Renderer> From<Stack<'a, Message, Theme, Renderer>>
+ 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)
+ }
+}
diff --git a/widget/src/svg.rs b/widget/src/svg.rs
index eb142189..4551bcad 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, Rotation,
+ Size, Theme, Vector, Widget,
};
use std::path::PathBuf;
@@ -29,6 +29,8 @@ where
height: Length,
content_fit: ContentFit,
class: Theme::Class<'a>,
+ rotation: Rotation,
+ opacity: f32,
}
impl<'a, Theme> Svg<'a, Theme>
@@ -43,6 +45,8 @@ where
height: Length::Shrink,
content_fit: ContentFit::Contain,
class: Theme::default(),
+ rotation: Rotation::default(),
+ opacity: 1.0,
}
}
@@ -95,6 +99,21 @@ where
self.class = class.into();
self
}
+
+ /// Applies the given [`Rotation`] to the [`Svg`].
+ pub fn rotation(mut self, rotation: impl Into<Rotation>) -> Self {
+ 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<f32>) -> Self {
+ self.opacity = opacity.into();
+ self
+ }
}
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
@@ -120,11 +139,14 @@ 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.apply(image_size);
+
// 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,35 +175,47 @@ 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.apply(image_size);
let bounds = layout.bounds();
- let adjusted_fit = self.content_fit.fit(image_size, bounds.size());
- let is_mouse_over = cursor.is_over(bounds);
+ let adjusted_fit = self.content_fit.fit(rotated_size, bounds.size());
+ 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 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::new(position, final_size);
- let drawing_bounds = Rectangle {
- width: adjusted_fit.width,
- height: adjusted_fit.height,
- ..bounds
- };
+ let is_mouse_over = cursor.is_over(bounds);
- let status = if is_mouse_over {
- Status::Hovered
- } else {
- Status::Idle
- };
+ let status = if is_mouse_over {
+ Status::Hovered
+ } else {
+ Status::Idle
+ };
- let style = theme.style(&self.class, status);
+ let style = theme.style(&self.class, status);
+ let render = |renderer: &mut Renderer| {
renderer.draw_svg(
self.handle.clone(),
style.color,
- drawing_bounds + offset,
+ drawing_bounds,
+ self.rotation.radians(),
+ self.opacity,
);
};
diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs
index 92cdb251..7c0b98ea 100644
--- a/widget/src/text_editor.rs
+++ b/widget/src/text_editor.rs
@@ -319,7 +319,9 @@ where
}
}
-struct State<Highlighter: text::Highlighter> {
+/// The state of a [`TextEditor`].
+#[derive(Debug)]
+pub struct State<Highlighter: text::Highlighter> {
is_focused: bool,
last_click: Option<mouse::Click>,
drag_click: Option<mouse::click::Kind>,
@@ -329,6 +331,13 @@ struct State<Highlighter: text::Highlighter> {
highlighter_format_address: usize,
}
+impl<Highlighter: text::Highlighter> State<Highlighter> {
+ /// 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<Message, Theme, Renderer>
for TextEditor<'a, Highlighter, Message, Theme, Renderer>
where
@@ -560,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()
},