Merge branch 'master' of https://github.com/iced-rs/iced into iced-rs-master

This commit is contained in:
gigas002 2024-05-08 19:16:06 +09:00
commit 477887b387
120 changed files with 4104 additions and 2129 deletions

View file

@ -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,

View file

@ -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 {

View file

@ -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

View file

@ -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 scale = Vector::new(
adjusted_fit.width / rotated_size.width,
adjusted_fit.height / rotated_size.height,
);
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| {
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
};
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)

View file

@ -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)

View file

@ -224,7 +224,7 @@ where
);
if state.keys != self.keys {
state.keys = self.keys.clone();
state.keys.clone_from(&self.keys);
}
}

View file

@ -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;

View file

@ -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

View file

@ -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();
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())
};
// 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 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;
}
shell.publish(on_scroll(viewport));
state.last_notified = Some(viewport);
}
if let Some(on_scroll) = on_scroll {
shell.publish(on_scroll(viewport));
}
state.last_notified = Some(viewport);
true
}
#[derive(Debug, Clone, Copy)]

333
widget/src/stack.rs Normal file
View file

@ -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)
}
}

View file

@ -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 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 drawing_bounds = Rectangle::new(position, final_size);
let is_mouse_over = cursor.is_over(bounds);
let status = if is_mouse_over {
Status::Hovered
} else {
Status::Idle
};
let style = theme.style(&self.class, status);
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 status = if is_mouse_over {
Status::Hovered
} else {
Status::Idle
};
let style = theme.style(&self.class, status);
renderer.draw_svg(
self.handle.clone(),
style.color,
drawing_bounds + offset,
drawing_bounds,
self.rotation.radians(),
self.opacity,
);
};

View file

@ -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()
},