Merge branch 'master' of https://github.com/iced-rs/iced into iced-rs-master
This commit is contained in:
commit
477887b387
120 changed files with 4104 additions and 2129 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -224,7 +224,7 @@ where
|
|||
);
|
||||
|
||||
if state.keys != self.keys {
|
||||
state.keys = self.keys.clone();
|
||||
state.keys.clone_from(&self.keys);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
333
widget/src/stack.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue