Create iced_widget subcrate and re-organize the whole codebase
This commit is contained in:
parent
c54409d171
commit
3a0d34c024
209 changed files with 1959 additions and 2183 deletions
37
widget/Cargo.toml
Normal file
37
widget/Cargo.toml
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
[package]
|
||||
name = "iced_widget"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[features]
|
||||
lazy = ["ouroboros"]
|
||||
image = ["iced_renderer/image"]
|
||||
svg = ["iced_renderer/svg"]
|
||||
canvas = ["iced_renderer/geometry"]
|
||||
qr_code = ["canvas", "qrcode"]
|
||||
|
||||
[dependencies]
|
||||
unicode-segmentation = "1.6"
|
||||
num-traits = "0.2"
|
||||
thiserror = "1"
|
||||
|
||||
[dependencies.iced_native]
|
||||
version = "0.9"
|
||||
path = "../native"
|
||||
|
||||
[dependencies.iced_renderer]
|
||||
version = "0.1"
|
||||
path = "../renderer"
|
||||
|
||||
[dependencies.iced_style]
|
||||
version = "0.7"
|
||||
path = "../style"
|
||||
|
||||
[dependencies.ouroboros]
|
||||
version = "0.13"
|
||||
optional = true
|
||||
|
||||
[dependencies.qrcode]
|
||||
version = "0.12"
|
||||
optional = true
|
||||
default-features = false
|
||||
455
widget/src/button.rs
Normal file
455
widget/src/button.rs
Normal file
|
|
@ -0,0 +1,455 @@
|
|||
//! Allow your users to perform actions by pressing a button.
|
||||
//!
|
||||
//! A [`Button`] has some local [`State`].
|
||||
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::touch;
|
||||
use crate::core::widget::tree::{self, Tree};
|
||||
use crate::core::widget::Operation;
|
||||
use crate::core::{
|
||||
Background, Clipboard, Color, Element, Layout, Length, Padding, Point,
|
||||
Rectangle, Shell, Vector, Widget,
|
||||
};
|
||||
|
||||
pub use iced_style::button::{Appearance, StyleSheet};
|
||||
|
||||
/// A generic widget that produces a message when pressed.
|
||||
///
|
||||
/// ```
|
||||
/// # type Button<'a, Message> =
|
||||
/// # iced_widget::Button<'a, Message, iced_widget::renderer::Renderer<iced_widget::style::Theme>>;
|
||||
/// #
|
||||
/// #[derive(Clone)]
|
||||
/// enum Message {
|
||||
/// ButtonPressed,
|
||||
/// }
|
||||
///
|
||||
/// let button = Button::new("Press me!").on_press(Message::ButtonPressed);
|
||||
/// ```
|
||||
///
|
||||
/// If a [`Button::on_press`] handler is not set, the resulting [`Button`] will
|
||||
/// be disabled:
|
||||
///
|
||||
/// ```
|
||||
/// # type Button<'a, Message> =
|
||||
/// # iced_widget::Button<'a, Message, iced_widget::renderer::Renderer<iced_widget::style::Theme>>;
|
||||
/// #
|
||||
/// #[derive(Clone)]
|
||||
/// enum Message {
|
||||
/// ButtonPressed,
|
||||
/// }
|
||||
///
|
||||
/// fn disabled_button<'a>() -> Button<'a, Message> {
|
||||
/// Button::new("I'm disabled!")
|
||||
/// }
|
||||
///
|
||||
/// fn enabled_button<'a>() -> Button<'a, Message> {
|
||||
/// disabled_button().on_press(Message::ButtonPressed)
|
||||
/// }
|
||||
/// ```
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Button<'a, Message, Renderer = crate::Renderer>
|
||||
where
|
||||
Renderer: crate::core::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
content: Element<'a, Message, Renderer>,
|
||||
on_press: Option<Message>,
|
||||
width: Length,
|
||||
height: Length,
|
||||
padding: Padding,
|
||||
style: <Renderer::Theme as StyleSheet>::Style,
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Button<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: crate::core::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
/// Creates a new [`Button`] with the given content.
|
||||
pub fn new(content: impl Into<Element<'a, Message, Renderer>>) -> Self {
|
||||
Button {
|
||||
content: content.into(),
|
||||
on_press: None,
|
||||
width: Length::Shrink,
|
||||
height: Length::Shrink,
|
||||
padding: Padding::new(5.0),
|
||||
style: <Renderer::Theme as StyleSheet>::Style::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the width of the [`Button`].
|
||||
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||
self.width = width.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the height of the [`Button`].
|
||||
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
||||
self.height = height.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`Padding`] of the [`Button`].
|
||||
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
|
||||
self.padding = padding.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the message that will be produced when the [`Button`] is pressed.
|
||||
///
|
||||
/// Unless `on_press` is called, the [`Button`] will be disabled.
|
||||
pub fn on_press(mut self, msg: Message) -> Self {
|
||||
self.on_press = Some(msg);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style variant of this [`Button`].
|
||||
pub fn style(
|
||||
mut self,
|
||||
style: <Renderer::Theme as StyleSheet>::Style,
|
||||
) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Widget<Message, Renderer>
|
||||
for Button<'a, Message, Renderer>
|
||||
where
|
||||
Message: 'a + Clone,
|
||||
Renderer: 'a + crate::core::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
fn tag(&self) -> tree::Tag {
|
||||
tree::Tag::of::<State>()
|
||||
}
|
||||
|
||||
fn state(&self) -> tree::State {
|
||||
tree::State::new(State::new())
|
||||
}
|
||||
|
||||
fn children(&self) -> Vec<Tree> {
|
||||
vec![Tree::new(&self.content)]
|
||||
}
|
||||
|
||||
fn diff(&self, tree: &mut Tree) {
|
||||
tree.diff_children(std::slice::from_ref(&self.content))
|
||||
}
|
||||
|
||||
fn width(&self) -> Length {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
layout(
|
||||
renderer,
|
||||
limits,
|
||||
self.width,
|
||||
self.height,
|
||||
self.padding,
|
||||
|renderer, limits| {
|
||||
self.content.as_widget().layout(renderer, limits)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn operate(
|
||||
&self,
|
||||
tree: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn Operation<Message>,
|
||||
) {
|
||||
operation.container(None, &mut |operation| {
|
||||
self.content.as_widget().operate(
|
||||
&mut tree.children[0],
|
||||
layout.children().next().unwrap(),
|
||||
renderer,
|
||||
operation,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
if let event::Status::Captured = self.content.as_widget_mut().on_event(
|
||||
&mut tree.children[0],
|
||||
event.clone(),
|
||||
layout.children().next().unwrap(),
|
||||
cursor_position,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
) {
|
||||
return event::Status::Captured;
|
||||
}
|
||||
|
||||
update(
|
||||
event,
|
||||
layout,
|
||||
cursor_position,
|
||||
shell,
|
||||
&self.on_press,
|
||||
|| tree.state.downcast_mut::<State>(),
|
||||
)
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
_style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
) {
|
||||
let bounds = layout.bounds();
|
||||
let content_layout = layout.children().next().unwrap();
|
||||
|
||||
let styling = draw(
|
||||
renderer,
|
||||
bounds,
|
||||
cursor_position,
|
||||
self.on_press.is_some(),
|
||||
theme,
|
||||
&self.style,
|
||||
|| tree.state.downcast_ref::<State>(),
|
||||
);
|
||||
|
||||
self.content.as_widget().draw(
|
||||
&tree.children[0],
|
||||
renderer,
|
||||
theme,
|
||||
&renderer::Style {
|
||||
text_color: styling.text_color,
|
||||
},
|
||||
content_layout,
|
||||
cursor_position,
|
||||
&bounds,
|
||||
);
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
_tree: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
_renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
mouse_interaction(layout, cursor_position, self.on_press.is_some())
|
||||
}
|
||||
|
||||
fn overlay<'b>(
|
||||
&'b mut self,
|
||||
tree: &'b mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
) -> Option<overlay::Element<'b, Message, Renderer>> {
|
||||
self.content.as_widget_mut().overlay(
|
||||
&mut tree.children[0],
|
||||
layout.children().next().unwrap(),
|
||||
renderer,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> From<Button<'a, Message, Renderer>>
|
||||
for Element<'a, Message, Renderer>
|
||||
where
|
||||
Message: Clone + 'a,
|
||||
Renderer: crate::core::Renderer + 'a,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
fn from(button: Button<'a, Message, Renderer>) -> Self {
|
||||
Self::new(button)
|
||||
}
|
||||
}
|
||||
|
||||
/// The local state of a [`Button`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct State {
|
||||
is_pressed: bool,
|
||||
}
|
||||
|
||||
impl State {
|
||||
/// Creates a new [`State`].
|
||||
pub fn new() -> State {
|
||||
State::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Processes the given [`Event`] and updates the [`State`] of a [`Button`]
|
||||
/// accordingly.
|
||||
pub fn update<'a, Message: Clone>(
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
on_press: &Option<Message>,
|
||||
state: impl FnOnce() -> &'a mut State,
|
||||
) -> event::Status {
|
||||
match event {
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
|
||||
| Event::Touch(touch::Event::FingerPressed { .. }) => {
|
||||
if on_press.is_some() {
|
||||
let bounds = layout.bounds();
|
||||
|
||||
if bounds.contains(cursor_position) {
|
||||
let state = state();
|
||||
|
||||
state.is_pressed = true;
|
||||
|
||||
return event::Status::Captured;
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
|
||||
| Event::Touch(touch::Event::FingerLifted { .. }) => {
|
||||
if let Some(on_press) = on_press.clone() {
|
||||
let state = state();
|
||||
|
||||
if state.is_pressed {
|
||||
state.is_pressed = false;
|
||||
|
||||
let bounds = layout.bounds();
|
||||
|
||||
if bounds.contains(cursor_position) {
|
||||
shell.publish(on_press);
|
||||
}
|
||||
|
||||
return event::Status::Captured;
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::Touch(touch::Event::FingerLost { .. }) => {
|
||||
let state = state();
|
||||
|
||||
state.is_pressed = false;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
event::Status::Ignored
|
||||
}
|
||||
|
||||
/// Draws a [`Button`].
|
||||
pub fn draw<'a, Renderer: crate::core::Renderer>(
|
||||
renderer: &mut Renderer,
|
||||
bounds: Rectangle,
|
||||
cursor_position: Point,
|
||||
is_enabled: bool,
|
||||
style_sheet: &dyn StyleSheet<
|
||||
Style = <Renderer::Theme as StyleSheet>::Style,
|
||||
>,
|
||||
style: &<Renderer::Theme as StyleSheet>::Style,
|
||||
state: impl FnOnce() -> &'a State,
|
||||
) -> Appearance
|
||||
where
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
let is_mouse_over = bounds.contains(cursor_position);
|
||||
|
||||
let styling = if !is_enabled {
|
||||
style_sheet.disabled(style)
|
||||
} else if is_mouse_over {
|
||||
let state = state();
|
||||
|
||||
if state.is_pressed {
|
||||
style_sheet.pressed(style)
|
||||
} else {
|
||||
style_sheet.hovered(style)
|
||||
}
|
||||
} else {
|
||||
style_sheet.active(style)
|
||||
};
|
||||
|
||||
if styling.background.is_some() || styling.border_width > 0.0 {
|
||||
if styling.shadow_offset != Vector::default() {
|
||||
// TODO: Implement proper shadow support
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: Rectangle {
|
||||
x: bounds.x + styling.shadow_offset.x,
|
||||
y: bounds.y + styling.shadow_offset.y,
|
||||
..bounds
|
||||
},
|
||||
border_radius: styling.border_radius.into(),
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
},
|
||||
Background::Color([0.0, 0.0, 0.0, 0.5].into()),
|
||||
);
|
||||
}
|
||||
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds,
|
||||
border_radius: styling.border_radius.into(),
|
||||
border_width: styling.border_width,
|
||||
border_color: styling.border_color,
|
||||
},
|
||||
styling
|
||||
.background
|
||||
.unwrap_or(Background::Color(Color::TRANSPARENT)),
|
||||
);
|
||||
}
|
||||
|
||||
styling
|
||||
}
|
||||
|
||||
/// Computes the layout of a [`Button`].
|
||||
pub fn layout<Renderer>(
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
width: Length,
|
||||
height: Length,
|
||||
padding: Padding,
|
||||
layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node,
|
||||
) -> layout::Node {
|
||||
let limits = limits.width(width).height(height);
|
||||
|
||||
let mut content = layout_content(renderer, &limits.pad(padding));
|
||||
let padding = padding.fit(content.size(), limits.max());
|
||||
let size = limits.pad(padding).resolve(content.size()).pad(padding);
|
||||
|
||||
content.move_to(Point::new(padding.left, padding.top));
|
||||
|
||||
layout::Node::with_children(size, vec![content])
|
||||
}
|
||||
|
||||
/// Returns the [`mouse::Interaction`] of a [`Button`].
|
||||
pub fn mouse_interaction(
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
is_enabled: bool,
|
||||
) -> mouse::Interaction {
|
||||
let is_mouse_over = layout.bounds().contains(cursor_position);
|
||||
|
||||
if is_mouse_over && is_enabled {
|
||||
mouse::Interaction::Pointer
|
||||
} else {
|
||||
mouse::Interaction::default()
|
||||
}
|
||||
}
|
||||
238
widget/src/canvas.rs
Normal file
238
widget/src/canvas.rs
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
//! Draw 2D graphics for your users.
|
||||
pub mod event;
|
||||
|
||||
mod cursor;
|
||||
mod program;
|
||||
|
||||
pub use cursor::Cursor;
|
||||
pub use event::Event;
|
||||
pub use program::Program;
|
||||
|
||||
pub use crate::graphics::geometry::*;
|
||||
pub use crate::renderer::geometry::*;
|
||||
|
||||
use crate::core;
|
||||
use crate::core::layout::{self, Layout};
|
||||
use crate::core::mouse;
|
||||
use crate::core::renderer;
|
||||
use crate::core::widget::tree::{self, Tree};
|
||||
use crate::core::{Clipboard, Element, Shell, Widget};
|
||||
use crate::core::{Length, Point, Rectangle, Size, Vector};
|
||||
use crate::graphics::geometry;
|
||||
|
||||
use std::marker::PhantomData;
|
||||
|
||||
/// A widget capable of drawing 2D graphics.
|
||||
///
|
||||
/// ## Drawing a simple circle
|
||||
/// If you want to get a quick overview, here's how we can draw a simple circle:
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use iced_widget::canvas::{self, Canvas, Cursor, Fill, Frame, Geometry, Path, Program};
|
||||
/// # use iced_widget::core::{Color, Rectangle};
|
||||
/// # use iced_widget::style::Theme;
|
||||
/// #
|
||||
/// # pub type Renderer = iced_widget::renderer::Renderer<Theme>;
|
||||
/// // First, we define the data we need for drawing
|
||||
/// #[derive(Debug)]
|
||||
/// struct Circle {
|
||||
/// radius: f32,
|
||||
/// }
|
||||
///
|
||||
/// // Then, we implement the `Program` trait
|
||||
/// impl Program<()> for Circle {
|
||||
/// type State = ();
|
||||
///
|
||||
/// fn draw(&self, _state: &(), renderer: &Renderer, _theme: &Theme, bounds: Rectangle, _cursor: Cursor) -> Vec<Geometry>{
|
||||
/// // We prepare a new `Frame`
|
||||
/// let mut frame = Frame::new(renderer, bounds.size());
|
||||
///
|
||||
/// // We create a `Path` representing a simple circle
|
||||
/// let circle = Path::circle(frame.center(), self.radius);
|
||||
///
|
||||
/// // And fill it with some color
|
||||
/// frame.fill(&circle, Color::BLACK);
|
||||
///
|
||||
/// // Finally, we produce the geometry
|
||||
/// vec![frame.into_geometry()]
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// // Finally, we simply use our `Circle` to create the `Canvas`!
|
||||
/// let canvas = Canvas::new(Circle { radius: 50.0 });
|
||||
/// ```
|
||||
#[derive(Debug)]
|
||||
pub struct Canvas<P, Message, Renderer = crate::Renderer>
|
||||
where
|
||||
Renderer: geometry::Renderer,
|
||||
P: Program<Message, Renderer>,
|
||||
{
|
||||
width: Length,
|
||||
height: Length,
|
||||
program: P,
|
||||
message_: PhantomData<Message>,
|
||||
theme_: PhantomData<Renderer>,
|
||||
}
|
||||
|
||||
impl<P, Message, Renderer> Canvas<P, Message, Renderer>
|
||||
where
|
||||
Renderer: geometry::Renderer,
|
||||
P: Program<Message, Renderer>,
|
||||
{
|
||||
const DEFAULT_SIZE: f32 = 100.0;
|
||||
|
||||
/// Creates a new [`Canvas`].
|
||||
pub fn new(program: P) -> Self {
|
||||
Canvas {
|
||||
width: Length::Fixed(Self::DEFAULT_SIZE),
|
||||
height: Length::Fixed(Self::DEFAULT_SIZE),
|
||||
program,
|
||||
message_: PhantomData,
|
||||
theme_: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the width of the [`Canvas`].
|
||||
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||
self.width = width.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the height of the [`Canvas`].
|
||||
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
||||
self.height = height.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<P, Message, Renderer> Widget<Message, Renderer>
|
||||
for Canvas<P, Message, Renderer>
|
||||
where
|
||||
Renderer: geometry::Renderer,
|
||||
P: Program<Message, Renderer>,
|
||||
{
|
||||
fn tag(&self) -> tree::Tag {
|
||||
struct Tag<T>(T);
|
||||
tree::Tag::of::<Tag<P::State>>()
|
||||
}
|
||||
|
||||
fn state(&self) -> tree::State {
|
||||
tree::State::new(P::State::default())
|
||||
}
|
||||
|
||||
fn width(&self) -> Length {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
_renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
let limits = limits.width(self.width).height(self.height);
|
||||
let size = limits.resolve(Size::ZERO);
|
||||
|
||||
layout::Node::new(size)
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: core::Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
_renderer: &Renderer,
|
||||
_clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
let bounds = layout.bounds();
|
||||
|
||||
let canvas_event = match event {
|
||||
core::Event::Mouse(mouse_event) => Some(Event::Mouse(mouse_event)),
|
||||
core::Event::Touch(touch_event) => Some(Event::Touch(touch_event)),
|
||||
core::Event::Keyboard(keyboard_event) => {
|
||||
Some(Event::Keyboard(keyboard_event))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let cursor = Cursor::from_window_position(cursor_position);
|
||||
|
||||
if let Some(canvas_event) = canvas_event {
|
||||
let state = tree.state.downcast_mut::<P::State>();
|
||||
|
||||
let (event_status, message) =
|
||||
self.program.update(state, canvas_event, bounds, cursor);
|
||||
|
||||
if let Some(message) = message {
|
||||
shell.publish(message);
|
||||
}
|
||||
|
||||
return event_status;
|
||||
}
|
||||
|
||||
event::Status::Ignored
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
_renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
let bounds = layout.bounds();
|
||||
let cursor = Cursor::from_window_position(cursor_position);
|
||||
let state = tree.state.downcast_ref::<P::State>();
|
||||
|
||||
self.program.mouse_interaction(state, bounds, cursor)
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
_style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
) {
|
||||
let bounds = layout.bounds();
|
||||
|
||||
if bounds.width < 1.0 || bounds.height < 1.0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let cursor = Cursor::from_window_position(cursor_position);
|
||||
let state = tree.state.downcast_ref::<P::State>();
|
||||
|
||||
renderer.with_translation(
|
||||
Vector::new(bounds.x, bounds.y),
|
||||
|renderer| {
|
||||
renderer.draw(
|
||||
self.program.draw(state, renderer, theme, bounds, cursor),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, P, Message, Renderer> From<Canvas<P, Message, Renderer>>
|
||||
for Element<'a, Message, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Renderer: 'a + geometry::Renderer,
|
||||
P: Program<Message, Renderer> + 'a,
|
||||
{
|
||||
fn from(
|
||||
canvas: Canvas<P, Message, Renderer>,
|
||||
) -> Element<'a, Message, Renderer> {
|
||||
Element::new(canvas)
|
||||
}
|
||||
}
|
||||
64
widget/src/canvas/cursor.rs
Normal file
64
widget/src/canvas/cursor.rs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
use crate::core::{Point, Rectangle};
|
||||
|
||||
/// The mouse cursor state.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum Cursor {
|
||||
/// The cursor has a defined position.
|
||||
Available(Point),
|
||||
|
||||
/// The cursor is currently unavailable (i.e. out of bounds or busy).
|
||||
Unavailable,
|
||||
}
|
||||
|
||||
impl Cursor {
|
||||
// TODO: Remove this once this type is used in `iced_native` to encode
|
||||
// proper cursor availability
|
||||
pub(crate) fn from_window_position(position: Point) -> Self {
|
||||
if position.x < 0.0 || position.y < 0.0 {
|
||||
Cursor::Unavailable
|
||||
} else {
|
||||
Cursor::Available(position)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the absolute position of the [`Cursor`], if available.
|
||||
pub fn position(&self) -> Option<Point> {
|
||||
match self {
|
||||
Cursor::Available(position) => Some(*position),
|
||||
Cursor::Unavailable => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the relative position of the [`Cursor`] inside the given bounds,
|
||||
/// if available.
|
||||
///
|
||||
/// If the [`Cursor`] is not over the provided bounds, this method will
|
||||
/// return `None`.
|
||||
pub fn position_in(&self, bounds: &Rectangle) -> Option<Point> {
|
||||
if self.is_over(bounds) {
|
||||
self.position_from(bounds.position())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the relative position of the [`Cursor`] from the given origin,
|
||||
/// if available.
|
||||
pub fn position_from(&self, origin: Point) -> Option<Point> {
|
||||
match self {
|
||||
Cursor::Available(position) => {
|
||||
Some(Point::new(position.x - origin.x, position.y - origin.y))
|
||||
}
|
||||
Cursor::Unavailable => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether the [`Cursor`] is currently over the provided bounds
|
||||
/// or not.
|
||||
pub fn is_over(&self, bounds: &Rectangle) -> bool {
|
||||
match self {
|
||||
Cursor::Available(position) => bounds.contains(*position),
|
||||
Cursor::Unavailable => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
21
widget/src/canvas/event.rs
Normal file
21
widget/src/canvas/event.rs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
//! Handle events of a canvas.
|
||||
use crate::core::keyboard;
|
||||
use crate::core::mouse;
|
||||
use crate::core::touch;
|
||||
|
||||
pub use crate::core::event::Status;
|
||||
|
||||
/// A [`Canvas`] event.
|
||||
///
|
||||
/// [`Canvas`]: crate::widget::Canvas
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum Event {
|
||||
/// A mouse event.
|
||||
Mouse(mouse::Event),
|
||||
|
||||
/// A touch event.
|
||||
Touch(touch::Event),
|
||||
|
||||
/// A keyboard event.
|
||||
Keyboard(keyboard::Event),
|
||||
}
|
||||
109
widget/src/canvas/program.rs
Normal file
109
widget/src/canvas/program.rs
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
use crate::canvas::event::{self, Event};
|
||||
use crate::canvas::mouse;
|
||||
use crate::canvas::Cursor;
|
||||
use crate::core::Rectangle;
|
||||
use crate::graphics::geometry;
|
||||
|
||||
/// The state and logic of a [`Canvas`].
|
||||
///
|
||||
/// A [`Program`] can mutate internal state and produce messages for an
|
||||
/// application.
|
||||
///
|
||||
/// [`Canvas`]: crate::widget::Canvas
|
||||
pub trait Program<Message, Renderer = crate::Renderer>
|
||||
where
|
||||
Renderer: geometry::Renderer,
|
||||
{
|
||||
/// The internal state mutated by the [`Program`].
|
||||
type State: Default + 'static;
|
||||
|
||||
/// Updates the [`State`](Self::State) of the [`Program`].
|
||||
///
|
||||
/// When a [`Program`] is used in a [`Canvas`], the runtime will call this
|
||||
/// method for each [`Event`].
|
||||
///
|
||||
/// This method can optionally return a `Message` to notify an application
|
||||
/// of any meaningful interactions.
|
||||
///
|
||||
/// By default, this method does and returns nothing.
|
||||
///
|
||||
/// [`Canvas`]: crate::widget::Canvas
|
||||
fn update(
|
||||
&self,
|
||||
_state: &mut Self::State,
|
||||
_event: Event,
|
||||
_bounds: Rectangle,
|
||||
_cursor: Cursor,
|
||||
) -> (event::Status, Option<Message>) {
|
||||
(event::Status::Ignored, None)
|
||||
}
|
||||
|
||||
/// Draws the state of the [`Program`], producing a bunch of [`Geometry`].
|
||||
///
|
||||
/// [`Geometry`] can be easily generated with a [`Frame`] or stored in a
|
||||
/// [`Cache`].
|
||||
///
|
||||
/// [`Frame`]: crate::widget::canvas::Frame
|
||||
/// [`Cache`]: crate::widget::canvas::Cache
|
||||
fn draw(
|
||||
&self,
|
||||
state: &Self::State,
|
||||
renderer: &Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
bounds: Rectangle,
|
||||
cursor: Cursor,
|
||||
) -> Vec<Renderer::Geometry>;
|
||||
|
||||
/// Returns the current mouse interaction of the [`Program`].
|
||||
///
|
||||
/// The interaction returned will be in effect even if the cursor position
|
||||
/// is out of bounds of the program's [`Canvas`].
|
||||
///
|
||||
/// [`Canvas`]: crate::widget::Canvas
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
_state: &Self::State,
|
||||
_bounds: Rectangle,
|
||||
_cursor: Cursor,
|
||||
) -> mouse::Interaction {
|
||||
mouse::Interaction::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Message, Renderer, T> Program<Message, Renderer> for &T
|
||||
where
|
||||
Renderer: geometry::Renderer,
|
||||
T: Program<Message, Renderer>,
|
||||
{
|
||||
type State = T::State;
|
||||
|
||||
fn update(
|
||||
&self,
|
||||
state: &mut Self::State,
|
||||
event: Event,
|
||||
bounds: Rectangle,
|
||||
cursor: Cursor,
|
||||
) -> (event::Status, Option<Message>) {
|
||||
T::update(self, state, event, bounds, cursor)
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
state: &Self::State,
|
||||
renderer: &Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
bounds: Rectangle,
|
||||
cursor: Cursor,
|
||||
) -> Vec<Renderer::Geometry> {
|
||||
T::draw(self, state, renderer, theme, bounds, cursor)
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
state: &Self::State,
|
||||
bounds: Rectangle,
|
||||
cursor: Cursor,
|
||||
) -> mouse::Interaction {
|
||||
T::mouse_interaction(self, state, bounds, cursor)
|
||||
}
|
||||
}
|
||||
323
widget/src/checkbox.rs
Normal file
323
widget/src/checkbox.rs
Normal file
|
|
@ -0,0 +1,323 @@
|
|||
//! Show toggle controls using checkboxes.
|
||||
use crate::core::alignment;
|
||||
use crate::core::event::{self, Event};
|
||||
use crate::core::layout;
|
||||
use crate::core::mouse;
|
||||
use crate::core::renderer;
|
||||
use crate::core::text;
|
||||
use crate::core::touch;
|
||||
use crate::core::widget::Tree;
|
||||
use crate::core::{
|
||||
Alignment, Clipboard, Element, Layout, Length, Pixels, Point, Rectangle,
|
||||
Shell, Widget,
|
||||
};
|
||||
use crate::{Row, Text};
|
||||
|
||||
pub use iced_style::checkbox::{Appearance, StyleSheet};
|
||||
|
||||
/// The icon in a [`Checkbox`].
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Icon<Font> {
|
||||
/// Font that will be used to display the `code_point`,
|
||||
pub font: Font,
|
||||
/// The unicode code point that will be used as the icon.
|
||||
pub code_point: char,
|
||||
/// Font size of the content.
|
||||
pub size: Option<f32>,
|
||||
}
|
||||
|
||||
/// A box that can be checked.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # type Checkbox<'a, Message> =
|
||||
/// # iced_widget::Checkbox<'a, Message, iced_widget::renderer::Renderer<iced_widget::style::Theme>>;
|
||||
/// #
|
||||
/// pub enum Message {
|
||||
/// CheckboxToggled(bool),
|
||||
/// }
|
||||
///
|
||||
/// let is_checked = true;
|
||||
///
|
||||
/// Checkbox::new("Toggle me!", is_checked, Message::CheckboxToggled);
|
||||
/// ```
|
||||
///
|
||||
/// 
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Checkbox<'a, Message, Renderer = crate::Renderer>
|
||||
where
|
||||
Renderer: text::Renderer,
|
||||
Renderer::Theme: StyleSheet + crate::text::StyleSheet,
|
||||
{
|
||||
is_checked: bool,
|
||||
on_toggle: Box<dyn Fn(bool) -> Message + 'a>,
|
||||
label: String,
|
||||
width: Length,
|
||||
size: f32,
|
||||
spacing: f32,
|
||||
text_size: Option<f32>,
|
||||
font: Option<Renderer::Font>,
|
||||
icon: Icon<Renderer::Font>,
|
||||
style: <Renderer::Theme as StyleSheet>::Style,
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Checkbox<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: text::Renderer,
|
||||
Renderer::Theme: StyleSheet + crate::text::StyleSheet,
|
||||
{
|
||||
/// The default size of a [`Checkbox`].
|
||||
const DEFAULT_SIZE: f32 = 20.0;
|
||||
|
||||
/// The default spacing of a [`Checkbox`].
|
||||
const DEFAULT_SPACING: f32 = 15.0;
|
||||
|
||||
/// Creates a new [`Checkbox`].
|
||||
///
|
||||
/// It expects:
|
||||
/// * a boolean describing whether the [`Checkbox`] is checked or not
|
||||
/// * the label of the [`Checkbox`]
|
||||
/// * a function that will be called when the [`Checkbox`] is toggled. It
|
||||
/// will receive the new state of the [`Checkbox`] and must produce a
|
||||
/// `Message`.
|
||||
pub fn new<F>(label: impl Into<String>, is_checked: bool, f: F) -> Self
|
||||
where
|
||||
F: 'a + Fn(bool) -> Message,
|
||||
{
|
||||
Checkbox {
|
||||
is_checked,
|
||||
on_toggle: Box::new(f),
|
||||
label: label.into(),
|
||||
width: Length::Shrink,
|
||||
size: Self::DEFAULT_SIZE,
|
||||
spacing: Self::DEFAULT_SPACING,
|
||||
text_size: None,
|
||||
font: None,
|
||||
icon: Icon {
|
||||
font: Renderer::ICON_FONT,
|
||||
code_point: Renderer::CHECKMARK_ICON,
|
||||
size: None,
|
||||
},
|
||||
style: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the size of the [`Checkbox`].
|
||||
pub fn size(mut self, size: impl Into<Pixels>) -> Self {
|
||||
self.size = size.into().0;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the width of the [`Checkbox`].
|
||||
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||
self.width = width.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the spacing between the [`Checkbox`] and the text.
|
||||
pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
|
||||
self.spacing = spacing.into().0;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the text size of the [`Checkbox`].
|
||||
pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self {
|
||||
self.text_size = Some(text_size.into().0);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`Font`] of the text of the [`Checkbox`].
|
||||
///
|
||||
/// [`Font`]: crate::text::Renderer::Font
|
||||
pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
|
||||
self.font = Some(font.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`Icon`] of the [`Checkbox`].
|
||||
pub fn icon(mut self, icon: Icon<Renderer::Font>) -> Self {
|
||||
self.icon = icon;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the [`Checkbox`].
|
||||
pub fn style(
|
||||
mut self,
|
||||
style: impl Into<<Renderer::Theme as StyleSheet>::Style>,
|
||||
) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Widget<Message, Renderer>
|
||||
for Checkbox<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: text::Renderer,
|
||||
Renderer::Theme: StyleSheet + crate::text::StyleSheet,
|
||||
{
|
||||
fn width(&self) -> Length {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
Length::Shrink
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
Row::<(), Renderer>::new()
|
||||
.width(self.width)
|
||||
.spacing(self.spacing)
|
||||
.align_items(Alignment::Center)
|
||||
.push(Row::new().width(self.size).height(self.size))
|
||||
.push(
|
||||
Text::new(&self.label)
|
||||
.font(self.font.unwrap_or_else(|| renderer.default_font()))
|
||||
.width(self.width)
|
||||
.size(
|
||||
self.text_size
|
||||
.unwrap_or_else(|| renderer.default_size()),
|
||||
),
|
||||
)
|
||||
.layout(renderer, limits)
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
_tree: &mut Tree,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
_renderer: &Renderer,
|
||||
_clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
match event {
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
|
||||
| Event::Touch(touch::Event::FingerPressed { .. }) => {
|
||||
let mouse_over = layout.bounds().contains(cursor_position);
|
||||
|
||||
if mouse_over {
|
||||
shell.publish((self.on_toggle)(!self.is_checked));
|
||||
|
||||
return event::Status::Captured;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
event::Status::Ignored
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
_tree: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
_renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
if layout.bounds().contains(cursor_position) {
|
||||
mouse::Interaction::Pointer
|
||||
} else {
|
||||
mouse::Interaction::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
_tree: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
) {
|
||||
let bounds = layout.bounds();
|
||||
let is_mouse_over = bounds.contains(cursor_position);
|
||||
|
||||
let mut children = layout.children();
|
||||
|
||||
let custom_style = if is_mouse_over {
|
||||
theme.hovered(&self.style, self.is_checked)
|
||||
} else {
|
||||
theme.active(&self.style, self.is_checked)
|
||||
};
|
||||
|
||||
{
|
||||
let layout = children.next().unwrap();
|
||||
let bounds = layout.bounds();
|
||||
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds,
|
||||
border_radius: custom_style.border_radius.into(),
|
||||
border_width: custom_style.border_width,
|
||||
border_color: custom_style.border_color,
|
||||
},
|
||||
custom_style.background,
|
||||
);
|
||||
|
||||
let Icon {
|
||||
font,
|
||||
code_point,
|
||||
size,
|
||||
} = &self.icon;
|
||||
let size = size.unwrap_or(bounds.height * 0.7);
|
||||
|
||||
if self.is_checked {
|
||||
renderer.fill_text(text::Text {
|
||||
content: &code_point.to_string(),
|
||||
font: *font,
|
||||
size,
|
||||
bounds: Rectangle {
|
||||
x: bounds.center_x(),
|
||||
y: bounds.center_y(),
|
||||
..bounds
|
||||
},
|
||||
color: custom_style.icon_color,
|
||||
horizontal_alignment: alignment::Horizontal::Center,
|
||||
vertical_alignment: alignment::Vertical::Center,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let label_layout = children.next().unwrap();
|
||||
|
||||
crate::text::draw(
|
||||
renderer,
|
||||
style,
|
||||
label_layout,
|
||||
&self.label,
|
||||
self.text_size,
|
||||
self.font,
|
||||
crate::text::Appearance {
|
||||
color: custom_style.text_color,
|
||||
},
|
||||
alignment::Horizontal::Left,
|
||||
alignment::Vertical::Center,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> From<Checkbox<'a, Message, Renderer>>
|
||||
for Element<'a, Message, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Renderer: 'a + text::Renderer,
|
||||
Renderer::Theme: StyleSheet + crate::text::StyleSheet,
|
||||
{
|
||||
fn from(
|
||||
checkbox: Checkbox<'a, Message, Renderer>,
|
||||
) -> Element<'a, Message, Renderer> {
|
||||
Element::new(checkbox)
|
||||
}
|
||||
}
|
||||
264
widget/src/column.rs
Normal file
264
widget/src/column.rs
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
//! Distribute content vertically.
|
||||
use crate::core::event::{self, Event};
|
||||
use crate::core::layout;
|
||||
use crate::core::mouse;
|
||||
use crate::core::overlay;
|
||||
use crate::core::renderer;
|
||||
use crate::core::widget::{Operation, Tree};
|
||||
use crate::core::{
|
||||
Alignment, Clipboard, Element, Layout, Length, Padding, Pixels, Point,
|
||||
Rectangle, Shell, Widget,
|
||||
};
|
||||
|
||||
/// A container that distributes its contents vertically.
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Column<'a, Message, Renderer = crate::Renderer> {
|
||||
spacing: f32,
|
||||
padding: Padding,
|
||||
width: Length,
|
||||
height: Length,
|
||||
max_width: f32,
|
||||
align_items: Alignment,
|
||||
children: Vec<Element<'a, Message, Renderer>>,
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Column<'a, Message, Renderer> {
|
||||
/// Creates an empty [`Column`].
|
||||
pub fn new() -> Self {
|
||||
Self::with_children(Vec::new())
|
||||
}
|
||||
|
||||
/// Creates a [`Column`] with the given elements.
|
||||
pub fn with_children(
|
||||
children: Vec<Element<'a, Message, Renderer>>,
|
||||
) -> Self {
|
||||
Column {
|
||||
spacing: 0.0,
|
||||
padding: Padding::ZERO,
|
||||
width: Length::Shrink,
|
||||
height: Length::Shrink,
|
||||
max_width: f32::INFINITY,
|
||||
align_items: Alignment::Start,
|
||||
children,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the vertical spacing _between_ elements.
|
||||
///
|
||||
/// Custom margins per element do not exist in iced. You should use this
|
||||
/// method instead! While less flexible, it helps you keep spacing between
|
||||
/// elements consistent.
|
||||
pub fn spacing(mut self, amount: impl Into<Pixels>) -> Self {
|
||||
self.spacing = amount.into().0;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`Padding`] of the [`Column`].
|
||||
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
|
||||
self.padding = padding.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the width of the [`Column`].
|
||||
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||
self.width = width.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the height of the [`Column`].
|
||||
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
||||
self.height = height.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the maximum width of the [`Column`].
|
||||
pub fn max_width(mut self, max_width: impl Into<Pixels>) -> Self {
|
||||
self.max_width = max_width.into().0;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the horizontal alignment of the contents of the [`Column`] .
|
||||
pub fn align_items(mut self, align: Alignment) -> Self {
|
||||
self.align_items = align;
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds an element to the [`Column`].
|
||||
pub fn push(
|
||||
mut self,
|
||||
child: impl Into<Element<'a, Message, Renderer>>,
|
||||
) -> Self {
|
||||
self.children.push(child.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Default for Column<'a, Message, Renderer> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Widget<Message, Renderer>
|
||||
for Column<'a, Message, 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 width(&self) -> Length {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
let limits = limits
|
||||
.max_width(self.max_width)
|
||||
.width(self.width)
|
||||
.height(self.height);
|
||||
|
||||
layout::flex::resolve(
|
||||
layout::flex::Axis::Vertical,
|
||||
renderer,
|
||||
&limits,
|
||||
self.padding,
|
||||
self.spacing,
|
||||
self.align_items,
|
||||
&self.children,
|
||||
)
|
||||
}
|
||||
|
||||
fn operate(
|
||||
&self,
|
||||
tree: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn Operation<Message>,
|
||||
) {
|
||||
operation.container(None, &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_position: Point,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
self.children
|
||||
.iter_mut()
|
||||
.zip(&mut tree.children)
|
||||
.zip(layout.children())
|
||||
.map(|((child, state), layout)| {
|
||||
child.as_widget_mut().on_event(
|
||||
state,
|
||||
event.clone(),
|
||||
layout,
|
||||
cursor_position,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
)
|
||||
})
|
||||
.fold(event::Status::Ignored, event::Status::merge)
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
self.children
|
||||
.iter()
|
||||
.zip(&tree.children)
|
||||
.zip(layout.children())
|
||||
.map(|((child, state), layout)| {
|
||||
child.as_widget().mouse_interaction(
|
||||
state,
|
||||
layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
renderer,
|
||||
)
|
||||
})
|
||||
.max()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
for ((child, state), layout) in self
|
||||
.children
|
||||
.iter()
|
||||
.zip(&tree.children)
|
||||
.zip(layout.children())
|
||||
{
|
||||
child.as_widget().draw(
|
||||
state,
|
||||
renderer,
|
||||
theme,
|
||||
style,
|
||||
layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn overlay<'b>(
|
||||
&'b mut self,
|
||||
tree: &'b mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
) -> Option<overlay::Element<'b, Message, Renderer>> {
|
||||
overlay::from_children(&mut self.children, tree, layout, renderer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> From<Column<'a, Message, Renderer>>
|
||||
for Element<'a, Message, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Renderer: crate::core::Renderer + 'a,
|
||||
{
|
||||
fn from(column: Column<'a, Message, Renderer>) -> Self {
|
||||
Self::new(column)
|
||||
}
|
||||
}
|
||||
368
widget/src/container.rs
Normal file
368
widget/src/container.rs
Normal file
|
|
@ -0,0 +1,368 @@
|
|||
//! Decorate content and apply alignment.
|
||||
use crate::core::alignment::{self, Alignment};
|
||||
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::{self, Operation, Tree};
|
||||
use crate::core::{
|
||||
Background, Clipboard, Color, Element, Layout, Length, Padding, Pixels,
|
||||
Point, Rectangle, Shell, Widget,
|
||||
};
|
||||
|
||||
pub use iced_style::container::{Appearance, StyleSheet};
|
||||
|
||||
/// An element decorating some content.
|
||||
///
|
||||
/// It is normally used for alignment purposes.
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Container<'a, Message, Renderer = crate::Renderer>
|
||||
where
|
||||
Renderer: crate::core::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
id: Option<Id>,
|
||||
padding: Padding,
|
||||
width: Length,
|
||||
height: Length,
|
||||
max_width: f32,
|
||||
max_height: f32,
|
||||
horizontal_alignment: alignment::Horizontal,
|
||||
vertical_alignment: alignment::Vertical,
|
||||
style: <Renderer::Theme as StyleSheet>::Style,
|
||||
content: Element<'a, Message, Renderer>,
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Container<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: crate::core::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
/// Creates an empty [`Container`].
|
||||
pub fn new<T>(content: T) -> Self
|
||||
where
|
||||
T: Into<Element<'a, Message, Renderer>>,
|
||||
{
|
||||
Container {
|
||||
id: None,
|
||||
padding: Padding::ZERO,
|
||||
width: Length::Shrink,
|
||||
height: Length::Shrink,
|
||||
max_width: f32::INFINITY,
|
||||
max_height: f32::INFINITY,
|
||||
horizontal_alignment: alignment::Horizontal::Left,
|
||||
vertical_alignment: alignment::Vertical::Top,
|
||||
style: Default::default(),
|
||||
content: content.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the [`Id`] of the [`Container`].
|
||||
pub fn id(mut self, id: Id) -> Self {
|
||||
self.id = Some(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`Padding`] of the [`Container`].
|
||||
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
|
||||
self.padding = padding.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the width of the [`Container`].
|
||||
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||
self.width = width.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the height of the [`Container`].
|
||||
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
||||
self.height = height.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// 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;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the maximum height of the [`Container`].
|
||||
pub fn max_height(mut self, max_height: impl Into<Pixels>) -> Self {
|
||||
self.max_height = max_height.into().0;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the content alignment for the horizontal axis of the [`Container`].
|
||||
pub fn align_x(mut self, alignment: alignment::Horizontal) -> Self {
|
||||
self.horizontal_alignment = alignment;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the content alignment for the vertical axis of the [`Container`].
|
||||
pub fn align_y(mut self, alignment: alignment::Vertical) -> Self {
|
||||
self.vertical_alignment = alignment;
|
||||
self
|
||||
}
|
||||
|
||||
/// Centers the contents in the horizontal axis of the [`Container`].
|
||||
pub fn center_x(mut self) -> Self {
|
||||
self.horizontal_alignment = alignment::Horizontal::Center;
|
||||
self
|
||||
}
|
||||
|
||||
/// Centers the contents in the vertical axis of the [`Container`].
|
||||
pub fn center_y(mut self) -> Self {
|
||||
self.vertical_alignment = alignment::Vertical::Center;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the [`Container`].
|
||||
pub fn style(
|
||||
mut self,
|
||||
style: impl Into<<Renderer::Theme as StyleSheet>::Style>,
|
||||
) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Widget<Message, Renderer>
|
||||
for Container<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: crate::core::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
fn children(&self) -> Vec<Tree> {
|
||||
vec![Tree::new(&self.content)]
|
||||
}
|
||||
|
||||
fn diff(&self, tree: &mut Tree) {
|
||||
tree.diff_children(std::slice::from_ref(&self.content))
|
||||
}
|
||||
|
||||
fn width(&self) -> Length {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
layout(
|
||||
renderer,
|
||||
limits,
|
||||
self.width,
|
||||
self.height,
|
||||
self.max_width,
|
||||
self.max_height,
|
||||
self.padding,
|
||||
self.horizontal_alignment,
|
||||
self.vertical_alignment,
|
||||
|renderer, limits| {
|
||||
self.content.as_widget().layout(renderer, limits)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn operate(
|
||||
&self,
|
||||
tree: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn Operation<Message>,
|
||||
) {
|
||||
operation.container(
|
||||
self.id.as_ref().map(|id| &id.0),
|
||||
&mut |operation| {
|
||||
self.content.as_widget().operate(
|
||||
&mut tree.children[0],
|
||||
layout.children().next().unwrap(),
|
||||
renderer,
|
||||
operation,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
self.content.as_widget_mut().on_event(
|
||||
&mut tree.children[0],
|
||||
event,
|
||||
layout.children().next().unwrap(),
|
||||
cursor_position,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
)
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
self.content.as_widget().mouse_interaction(
|
||||
&tree.children[0],
|
||||
layout.children().next().unwrap(),
|
||||
cursor_position,
|
||||
viewport,
|
||||
renderer,
|
||||
)
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
renderer_style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
let style = theme.appearance(&self.style);
|
||||
|
||||
draw_background(renderer, &style, layout.bounds());
|
||||
|
||||
self.content.as_widget().draw(
|
||||
&tree.children[0],
|
||||
renderer,
|
||||
theme,
|
||||
&renderer::Style {
|
||||
text_color: style
|
||||
.text_color
|
||||
.unwrap_or(renderer_style.text_color),
|
||||
},
|
||||
layout.children().next().unwrap(),
|
||||
cursor_position,
|
||||
viewport,
|
||||
);
|
||||
}
|
||||
|
||||
fn overlay<'b>(
|
||||
&'b mut self,
|
||||
tree: &'b mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
) -> Option<overlay::Element<'b, Message, Renderer>> {
|
||||
self.content.as_widget_mut().overlay(
|
||||
&mut tree.children[0],
|
||||
layout.children().next().unwrap(),
|
||||
renderer,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> From<Container<'a, Message, Renderer>>
|
||||
for Element<'a, Message, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Renderer: 'a + crate::core::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
fn from(
|
||||
column: Container<'a, Message, Renderer>,
|
||||
) -> Element<'a, Message, Renderer> {
|
||||
Element::new(column)
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes the layout of a [`Container`].
|
||||
pub fn layout<Renderer>(
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
width: Length,
|
||||
height: Length,
|
||||
max_width: f32,
|
||||
max_height: f32,
|
||||
padding: Padding,
|
||||
horizontal_alignment: alignment::Horizontal,
|
||||
vertical_alignment: alignment::Vertical,
|
||||
layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node,
|
||||
) -> layout::Node {
|
||||
let limits = limits
|
||||
.loose()
|
||||
.max_width(max_width)
|
||||
.max_height(max_height)
|
||||
.width(width)
|
||||
.height(height);
|
||||
|
||||
let mut content = layout_content(renderer, &limits.pad(padding).loose());
|
||||
let padding = padding.fit(content.size(), limits.max());
|
||||
let size = limits.pad(padding).resolve(content.size());
|
||||
|
||||
content.move_to(Point::new(padding.left, padding.top));
|
||||
content.align(
|
||||
Alignment::from(horizontal_alignment),
|
||||
Alignment::from(vertical_alignment),
|
||||
size,
|
||||
);
|
||||
|
||||
layout::Node::with_children(size.pad(padding), vec![content])
|
||||
}
|
||||
|
||||
/// Draws the background of a [`Container`] given its [`Appearance`] and its `bounds`.
|
||||
pub fn draw_background<Renderer>(
|
||||
renderer: &mut Renderer,
|
||||
appearance: &Appearance,
|
||||
bounds: Rectangle,
|
||||
) where
|
||||
Renderer: crate::core::Renderer,
|
||||
{
|
||||
if appearance.background.is_some() || appearance.border_width > 0.0 {
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds,
|
||||
border_radius: appearance.border_radius.into(),
|
||||
border_width: appearance.border_width,
|
||||
border_color: appearance.border_color,
|
||||
},
|
||||
appearance
|
||||
.background
|
||||
.unwrap_or(Background::Color(Color::TRANSPARENT)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The identifier of a [`Container`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct Id(widget::Id);
|
||||
|
||||
impl Id {
|
||||
/// Creates a custom [`Id`].
|
||||
pub fn new(id: impl Into<std::borrow::Cow<'static, str>>) -> Self {
|
||||
Self(widget::Id::new(id))
|
||||
}
|
||||
|
||||
/// Creates a unique [`Id`].
|
||||
///
|
||||
/// This function produces a different [`Id`] every time it is called.
|
||||
pub fn unique() -> Self {
|
||||
Self(widget::Id::unique())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Id> for widget::Id {
|
||||
fn from(id: Id) -> Self {
|
||||
id.0
|
||||
}
|
||||
}
|
||||
362
widget/src/helpers.rs
Normal file
362
widget/src/helpers.rs
Normal file
|
|
@ -0,0 +1,362 @@
|
|||
//! Helper functions to create pure widgets.
|
||||
use crate::button::{self, Button};
|
||||
use crate::checkbox::{self, Checkbox};
|
||||
use crate::container::{self, Container};
|
||||
use crate::core;
|
||||
use crate::core::widget::operation;
|
||||
use crate::core::{Element, Length, Pixels};
|
||||
use crate::native::Command;
|
||||
use crate::overlay;
|
||||
use crate::pick_list::{self, PickList};
|
||||
use crate::progress_bar::{self, ProgressBar};
|
||||
use crate::radio::{self, Radio};
|
||||
use crate::rule::{self, Rule};
|
||||
use crate::scrollable::{self, Scrollable};
|
||||
use crate::slider::{self, Slider};
|
||||
use crate::text::{self, Text};
|
||||
use crate::text_input::{self, TextInput};
|
||||
use crate::toggler::{self, Toggler};
|
||||
use crate::tooltip::{self, Tooltip};
|
||||
use crate::{Column, Row, Space, VerticalSlider};
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
/// Creates a [`Column`] with the given children.
|
||||
///
|
||||
/// [`Column`]: widget::Column
|
||||
#[macro_export]
|
||||
macro_rules! column {
|
||||
() => (
|
||||
$crate::Column::new()
|
||||
);
|
||||
($($x:expr),+ $(,)?) => (
|
||||
$crate::Column::with_children(vec![$($crate::core::Element::from($x)),+])
|
||||
);
|
||||
}
|
||||
|
||||
/// Creates a [`Row`] with the given children.
|
||||
///
|
||||
/// [`Row`]: widget::Row
|
||||
#[macro_export]
|
||||
macro_rules! row {
|
||||
() => (
|
||||
$crate::Row::new()
|
||||
);
|
||||
($($x:expr),+ $(,)?) => (
|
||||
$crate::Row::with_children(vec![$($crate::core::Element::from($x)),+])
|
||||
);
|
||||
}
|
||||
|
||||
/// Creates a new [`Container`] with the provided content.
|
||||
///
|
||||
/// [`Container`]: widget::Container
|
||||
pub fn container<'a, Message, Renderer>(
|
||||
content: impl Into<Element<'a, Message, Renderer>>,
|
||||
) -> Container<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: core::Renderer,
|
||||
Renderer::Theme: container::StyleSheet,
|
||||
{
|
||||
Container::new(content)
|
||||
}
|
||||
|
||||
/// Creates a new [`Column`] with the given children.
|
||||
///
|
||||
/// [`Column`]: widget::Column
|
||||
pub fn column<Message, Renderer>(
|
||||
children: Vec<Element<'_, Message, Renderer>>,
|
||||
) -> Column<'_, Message, Renderer> {
|
||||
Column::with_children(children)
|
||||
}
|
||||
|
||||
/// Creates a new [`Row`] with the given children.
|
||||
///
|
||||
/// [`Row`]: widget::Row
|
||||
pub fn row<Message, Renderer>(
|
||||
children: Vec<Element<'_, Message, Renderer>>,
|
||||
) -> Row<'_, Message, Renderer> {
|
||||
Row::with_children(children)
|
||||
}
|
||||
|
||||
/// Creates a new [`Scrollable`] with the provided content.
|
||||
///
|
||||
/// [`Scrollable`]: widget::Scrollable
|
||||
pub fn scrollable<'a, Message, Renderer>(
|
||||
content: impl Into<Element<'a, Message, Renderer>>,
|
||||
) -> Scrollable<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: core::Renderer,
|
||||
Renderer::Theme: scrollable::StyleSheet,
|
||||
{
|
||||
Scrollable::new(content)
|
||||
}
|
||||
|
||||
/// Creates a new [`Button`] with the provided content.
|
||||
///
|
||||
/// [`Button`]: widget::Button
|
||||
pub fn button<'a, Message, Renderer>(
|
||||
content: impl Into<Element<'a, Message, Renderer>>,
|
||||
) -> Button<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: core::Renderer,
|
||||
Renderer::Theme: button::StyleSheet,
|
||||
<Renderer::Theme as button::StyleSheet>::Style: Default,
|
||||
{
|
||||
Button::new(content)
|
||||
}
|
||||
|
||||
/// Creates a new [`Tooltip`] with the provided content, tooltip text, and [`tooltip::Position`].
|
||||
///
|
||||
/// [`Tooltip`]: widget::Tooltip
|
||||
/// [`tooltip::Position`]: widget::tooltip::Position
|
||||
pub fn tooltip<'a, Message, Renderer>(
|
||||
content: impl Into<Element<'a, Message, Renderer>>,
|
||||
tooltip: impl ToString,
|
||||
position: tooltip::Position,
|
||||
) -> crate::Tooltip<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: core::text::Renderer,
|
||||
Renderer::Theme: container::StyleSheet + text::StyleSheet,
|
||||
{
|
||||
Tooltip::new(content, tooltip.to_string(), position)
|
||||
}
|
||||
|
||||
/// Creates a new [`Text`] widget with the provided content.
|
||||
///
|
||||
/// [`Text`]: widget::Text
|
||||
pub fn text<'a, Renderer>(text: impl ToString) -> Text<'a, Renderer>
|
||||
where
|
||||
Renderer: core::text::Renderer,
|
||||
Renderer::Theme: text::StyleSheet,
|
||||
{
|
||||
Text::new(text.to_string())
|
||||
}
|
||||
|
||||
/// Creates a new [`Checkbox`].
|
||||
///
|
||||
/// [`Checkbox`]: widget::Checkbox
|
||||
pub fn checkbox<'a, Message, Renderer>(
|
||||
label: impl Into<String>,
|
||||
is_checked: bool,
|
||||
f: impl Fn(bool) -> Message + 'a,
|
||||
) -> Checkbox<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: core::text::Renderer,
|
||||
Renderer::Theme: checkbox::StyleSheet + text::StyleSheet,
|
||||
{
|
||||
Checkbox::new(label, is_checked, f)
|
||||
}
|
||||
|
||||
/// Creates a new [`Radio`].
|
||||
///
|
||||
/// [`Radio`]: widget::Radio
|
||||
pub fn radio<Message, Renderer, V>(
|
||||
label: impl Into<String>,
|
||||
value: V,
|
||||
selected: Option<V>,
|
||||
on_click: impl FnOnce(V) -> Message,
|
||||
) -> Radio<Message, Renderer>
|
||||
where
|
||||
Message: Clone,
|
||||
Renderer: core::text::Renderer,
|
||||
Renderer::Theme: radio::StyleSheet,
|
||||
V: Copy + Eq,
|
||||
{
|
||||
Radio::new(value, label, selected, on_click)
|
||||
}
|
||||
|
||||
/// Creates a new [`Toggler`].
|
||||
///
|
||||
/// [`Toggler`]: widget::Toggler
|
||||
pub fn toggler<'a, Message, Renderer>(
|
||||
label: impl Into<Option<String>>,
|
||||
is_checked: bool,
|
||||
f: impl Fn(bool) -> Message + 'a,
|
||||
) -> Toggler<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: core::text::Renderer,
|
||||
Renderer::Theme: toggler::StyleSheet,
|
||||
{
|
||||
Toggler::new(label, is_checked, f)
|
||||
}
|
||||
|
||||
/// Creates a new [`TextInput`].
|
||||
///
|
||||
/// [`TextInput`]: widget::TextInput
|
||||
pub fn text_input<'a, Message, Renderer>(
|
||||
placeholder: &str,
|
||||
value: &str,
|
||||
on_change: impl Fn(String) -> Message + 'a,
|
||||
) -> TextInput<'a, Message, Renderer>
|
||||
where
|
||||
Message: Clone,
|
||||
Renderer: core::text::Renderer,
|
||||
Renderer::Theme: text_input::StyleSheet,
|
||||
{
|
||||
TextInput::new(placeholder, value, on_change)
|
||||
}
|
||||
|
||||
/// Creates a new [`Slider`].
|
||||
///
|
||||
/// [`Slider`]: widget::Slider
|
||||
pub fn slider<'a, T, Message, Renderer>(
|
||||
range: std::ops::RangeInclusive<T>,
|
||||
value: T,
|
||||
on_change: impl Fn(T) -> Message + 'a,
|
||||
) -> Slider<'a, T, Message, Renderer>
|
||||
where
|
||||
T: Copy + From<u8> + std::cmp::PartialOrd,
|
||||
Message: Clone,
|
||||
Renderer: core::Renderer,
|
||||
Renderer::Theme: slider::StyleSheet,
|
||||
{
|
||||
Slider::new(range, value, on_change)
|
||||
}
|
||||
|
||||
/// Creates a new [`VerticalSlider`].
|
||||
///
|
||||
/// [`VerticalSlider`]: widget::VerticalSlider
|
||||
pub fn vertical_slider<'a, T, Message, Renderer>(
|
||||
range: std::ops::RangeInclusive<T>,
|
||||
value: T,
|
||||
on_change: impl Fn(T) -> Message + 'a,
|
||||
) -> VerticalSlider<'a, T, Message, Renderer>
|
||||
where
|
||||
T: Copy + From<u8> + std::cmp::PartialOrd,
|
||||
Message: Clone,
|
||||
Renderer: core::Renderer,
|
||||
Renderer::Theme: slider::StyleSheet,
|
||||
{
|
||||
VerticalSlider::new(range, value, on_change)
|
||||
}
|
||||
|
||||
/// Creates a new [`PickList`].
|
||||
///
|
||||
/// [`PickList`]: widget::PickList
|
||||
pub fn pick_list<'a, Message, Renderer, T>(
|
||||
options: impl Into<Cow<'a, [T]>>,
|
||||
selected: Option<T>,
|
||||
on_selected: impl Fn(T) -> Message + 'a,
|
||||
) -> PickList<'a, T, Message, Renderer>
|
||||
where
|
||||
T: ToString + Eq + 'static,
|
||||
[T]: ToOwned<Owned = Vec<T>>,
|
||||
Renderer: core::text::Renderer,
|
||||
Renderer::Theme: pick_list::StyleSheet
|
||||
+ scrollable::StyleSheet
|
||||
+ overlay::menu::StyleSheet
|
||||
+ container::StyleSheet,
|
||||
<Renderer::Theme as overlay::menu::StyleSheet>::Style:
|
||||
From<<Renderer::Theme as pick_list::StyleSheet>::Style>,
|
||||
{
|
||||
PickList::new(options, selected, on_selected)
|
||||
}
|
||||
|
||||
/// Creates a new horizontal [`Space`] with the given [`Length`].
|
||||
///
|
||||
/// [`Space`]: widget::Space
|
||||
pub fn horizontal_space(width: impl Into<Length>) -> Space {
|
||||
Space::with_width(width)
|
||||
}
|
||||
|
||||
/// Creates a new vertical [`Space`] with the given [`Length`].
|
||||
///
|
||||
/// [`Space`]: widget::Space
|
||||
pub fn vertical_space(height: impl Into<Length>) -> Space {
|
||||
Space::with_height(height)
|
||||
}
|
||||
|
||||
/// Creates a horizontal [`Rule`] with the given height.
|
||||
///
|
||||
/// [`Rule`]: widget::Rule
|
||||
pub fn horizontal_rule<Renderer>(height: impl Into<Pixels>) -> Rule<Renderer>
|
||||
where
|
||||
Renderer: core::Renderer,
|
||||
Renderer::Theme: rule::StyleSheet,
|
||||
{
|
||||
Rule::horizontal(height)
|
||||
}
|
||||
|
||||
/// Creates a vertical [`Rule`] with the given width.
|
||||
///
|
||||
/// [`Rule`]: widget::Rule
|
||||
pub fn vertical_rule<Renderer>(width: impl Into<Pixels>) -> Rule<Renderer>
|
||||
where
|
||||
Renderer: core::Renderer,
|
||||
Renderer::Theme: rule::StyleSheet,
|
||||
{
|
||||
Rule::vertical(width)
|
||||
}
|
||||
|
||||
/// Creates a new [`ProgressBar`].
|
||||
///
|
||||
/// It expects:
|
||||
/// * an inclusive range of possible values, and
|
||||
/// * the current value of the [`ProgressBar`].
|
||||
///
|
||||
/// [`ProgressBar`]: widget::ProgressBar
|
||||
pub fn progress_bar<Renderer>(
|
||||
range: RangeInclusive<f32>,
|
||||
value: f32,
|
||||
) -> ProgressBar<Renderer>
|
||||
where
|
||||
Renderer: core::Renderer,
|
||||
Renderer::Theme: progress_bar::StyleSheet,
|
||||
{
|
||||
ProgressBar::new(range, value)
|
||||
}
|
||||
|
||||
/// Creates a new [`Image`].
|
||||
///
|
||||
/// [`Image`]: widget::Image
|
||||
#[cfg(feature = "image")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "image")))]
|
||||
pub fn image<Handle>(handle: impl Into<Handle>) -> crate::Image<Handle> {
|
||||
crate::Image::new(handle.into())
|
||||
}
|
||||
|
||||
/// Creates a new [`Svg`] widget from the given [`Handle`].
|
||||
///
|
||||
/// [`Svg`]: widget::Svg
|
||||
/// [`Handle`]: widget::svg::Handle
|
||||
#[cfg(feature = "svg")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "svg")))]
|
||||
pub fn svg<Renderer>(
|
||||
handle: impl Into<core::svg::Handle>,
|
||||
) -> crate::Svg<Renderer>
|
||||
where
|
||||
Renderer: core::svg::Renderer,
|
||||
Renderer::Theme: crate::svg::StyleSheet,
|
||||
{
|
||||
crate::Svg::new(handle)
|
||||
}
|
||||
|
||||
/// Creates a new [`Canvas`].
|
||||
#[cfg(feature = "canvas")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "canvas")))]
|
||||
pub fn canvas<P, Message, Renderer>(
|
||||
program: P,
|
||||
) -> crate::Canvas<P, Message, Renderer>
|
||||
where
|
||||
Renderer: crate::graphics::geometry::Renderer,
|
||||
P: crate::canvas::Program<Message, Renderer>,
|
||||
{
|
||||
crate::Canvas::new(program)
|
||||
}
|
||||
|
||||
/// Focuses the previous focusable widget.
|
||||
pub fn focus_previous<Message>() -> Command<Message>
|
||||
where
|
||||
Message: 'static,
|
||||
{
|
||||
Command::widget(operation::focusable::focus_previous())
|
||||
}
|
||||
|
||||
/// Focuses the next focusable widget.
|
||||
pub fn focus_next<Message>() -> Command<Message>
|
||||
where
|
||||
Message: 'static,
|
||||
{
|
||||
Command::widget(operation::focusable::focus_next())
|
||||
}
|
||||
205
widget/src/image.rs
Normal file
205
widget/src/image.rs
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
//! Display images in your user interface.
|
||||
pub mod viewer;
|
||||
pub use viewer::Viewer;
|
||||
|
||||
use crate::core::image;
|
||||
use crate::core::layout;
|
||||
use crate::core::renderer;
|
||||
use crate::core::widget::Tree;
|
||||
use crate::core::{
|
||||
ContentFit, Element, Layout, Length, Point, Rectangle, Size, Vector, Widget,
|
||||
};
|
||||
|
||||
use std::hash::Hash;
|
||||
|
||||
pub use image::Handle;
|
||||
|
||||
/// Creates a new [`Viewer`] with the given image `Handle`.
|
||||
pub fn viewer<Handle>(handle: Handle) -> Viewer<Handle> {
|
||||
Viewer::new(handle)
|
||||
}
|
||||
|
||||
/// A frame that displays an image while keeping aspect ratio.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use iced_widget::image::{self, Image};
|
||||
/// #
|
||||
/// let image = Image::<image::Handle>::new("resources/ferris.png");
|
||||
/// ```
|
||||
///
|
||||
/// <img src="https://github.com/iced-rs/iced/blob/9712b319bb7a32848001b96bd84977430f14b623/examples/resources/ferris.png?raw=true" width="300">
|
||||
#[derive(Debug)]
|
||||
pub struct Image<Handle> {
|
||||
handle: Handle,
|
||||
width: Length,
|
||||
height: Length,
|
||||
content_fit: ContentFit,
|
||||
}
|
||||
|
||||
impl<Handle> Image<Handle> {
|
||||
/// Creates a new [`Image`] with the given path.
|
||||
pub fn new<T: Into<Handle>>(handle: T) -> Self {
|
||||
Image {
|
||||
handle: handle.into(),
|
||||
width: Length::Shrink,
|
||||
height: Length::Shrink,
|
||||
content_fit: ContentFit::Contain,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the width of the [`Image`] boundaries.
|
||||
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||
self.width = width.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the height of the [`Image`] boundaries.
|
||||
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
||||
self.height = height.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`ContentFit`] of the [`Image`].
|
||||
///
|
||||
/// Defaults to [`ContentFit::Contain`]
|
||||
pub fn content_fit(self, content_fit: ContentFit) -> Self {
|
||||
Self {
|
||||
content_fit,
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes the layout of an [`Image`].
|
||||
pub fn layout<Renderer, Handle>(
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
handle: &Handle,
|
||||
width: Length,
|
||||
height: Length,
|
||||
content_fit: ContentFit,
|
||||
) -> layout::Node
|
||||
where
|
||||
Renderer: image::Renderer<Handle = Handle>,
|
||||
{
|
||||
// The raw w/h of the underlying image
|
||||
let image_size = {
|
||||
let Size { width, height } = renderer.dimensions(handle);
|
||||
|
||||
Size::new(width as f32, height as f32)
|
||||
};
|
||||
|
||||
// The size to be available to the widget prior to `Shrink`ing
|
||||
let raw_size = limits.width(width).height(height).resolve(image_size);
|
||||
|
||||
// The uncropped size of the image when fit to the bounds above
|
||||
let full_size = content_fit.fit(image_size, raw_size);
|
||||
|
||||
// Shrink the widget to fit the resized image, if requested
|
||||
let final_size = Size {
|
||||
width: match width {
|
||||
Length::Shrink => f32::min(raw_size.width, full_size.width),
|
||||
_ => raw_size.width,
|
||||
},
|
||||
height: match height {
|
||||
Length::Shrink => f32::min(raw_size.height, full_size.height),
|
||||
_ => raw_size.height,
|
||||
},
|
||||
};
|
||||
|
||||
layout::Node::new(final_size)
|
||||
}
|
||||
|
||||
/// Draws an [`Image`]
|
||||
pub fn draw<Renderer, Handle>(
|
||||
renderer: &mut Renderer,
|
||||
layout: Layout<'_>,
|
||||
handle: &Handle,
|
||||
content_fit: ContentFit,
|
||||
) where
|
||||
Renderer: image::Renderer<Handle = Handle>,
|
||||
Handle: Clone + Hash,
|
||||
{
|
||||
let Size { width, height } = renderer.dimensions(handle);
|
||||
let image_size = Size::new(width as f32, height as f32);
|
||||
|
||||
let bounds = layout.bounds();
|
||||
let adjusted_fit = content_fit.fit(image_size, bounds.size());
|
||||
|
||||
let render = |renderer: &mut Renderer| {
|
||||
let offset = Vector::new(
|
||||
(bounds.width - adjusted_fit.width).max(0.0) / 2.0,
|
||||
(bounds.height - adjusted_fit.height).max(0.0) / 2.0,
|
||||
);
|
||||
|
||||
let drawing_bounds = Rectangle {
|
||||
width: adjusted_fit.width,
|
||||
height: adjusted_fit.height,
|
||||
..bounds
|
||||
};
|
||||
|
||||
renderer.draw(handle.clone(), drawing_bounds + offset)
|
||||
};
|
||||
|
||||
if adjusted_fit.width > bounds.width || adjusted_fit.height > bounds.height
|
||||
{
|
||||
renderer.with_layer(bounds, render);
|
||||
} else {
|
||||
render(renderer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Message, Renderer, Handle> Widget<Message, Renderer> for Image<Handle>
|
||||
where
|
||||
Renderer: image::Renderer<Handle = Handle>,
|
||||
Handle: Clone + Hash,
|
||||
{
|
||||
fn width(&self) -> Length {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
layout(
|
||||
renderer,
|
||||
limits,
|
||||
&self.handle,
|
||||
self.width,
|
||||
self.height,
|
||||
self.content_fit,
|
||||
)
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
_state: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
_theme: &Renderer::Theme,
|
||||
_style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
_cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
) {
|
||||
draw(renderer, layout, &self.handle, self.content_fit)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer, Handle> From<Image<Handle>>
|
||||
for Element<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: image::Renderer<Handle = Handle>,
|
||||
Handle: Clone + Hash + 'a,
|
||||
{
|
||||
fn from(image: Image<Handle>) -> Element<'a, Message, Renderer> {
|
||||
Element::new(image)
|
||||
}
|
||||
}
|
||||
428
widget/src/image/viewer.rs
Normal file
428
widget/src/image/viewer.rs
Normal file
|
|
@ -0,0 +1,428 @@
|
|||
//! Zoom and pan on an image.
|
||||
use crate::core::event::{self, Event};
|
||||
use crate::core::image;
|
||||
use crate::core::layout;
|
||||
use crate::core::mouse;
|
||||
use crate::core::renderer;
|
||||
use crate::core::widget::tree::{self, Tree};
|
||||
use crate::core::{
|
||||
Clipboard, Element, Layout, Length, Pixels, Point, Rectangle, Shell, Size,
|
||||
Vector, Widget,
|
||||
};
|
||||
|
||||
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> {
|
||||
padding: f32,
|
||||
width: Length,
|
||||
height: Length,
|
||||
min_scale: f32,
|
||||
max_scale: f32,
|
||||
scale_step: f32,
|
||||
handle: Handle,
|
||||
}
|
||||
|
||||
impl<Handle> Viewer<Handle> {
|
||||
/// Creates a new [`Viewer`] with the given [`State`].
|
||||
pub fn new(handle: Handle) -> Self {
|
||||
Viewer {
|
||||
padding: 0.0,
|
||||
width: Length::Shrink,
|
||||
height: Length::Shrink,
|
||||
min_scale: 0.25,
|
||||
max_scale: 10.0,
|
||||
scale_step: 0.10,
|
||||
handle,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the padding of the [`Viewer`].
|
||||
pub fn padding(mut self, padding: impl Into<Pixels>) -> Self {
|
||||
self.padding = padding.into().0;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the width of the [`Viewer`].
|
||||
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||
self.width = width.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the height of the [`Viewer`].
|
||||
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
||||
self.height = height.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the max scale applied to the image of the [`Viewer`].
|
||||
///
|
||||
/// Default is `10.0`
|
||||
pub fn max_scale(mut self, max_scale: f32) -> Self {
|
||||
self.max_scale = max_scale;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the min scale applied to the image of the [`Viewer`].
|
||||
///
|
||||
/// Default is `0.25`
|
||||
pub fn min_scale(mut self, min_scale: f32) -> Self {
|
||||
self.min_scale = min_scale;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the percentage the image of the [`Viewer`] will be scaled by
|
||||
/// when zoomed in / out.
|
||||
///
|
||||
/// Default is `0.10`
|
||||
pub fn scale_step(mut self, scale_step: f32) -> Self {
|
||||
self.scale_step = scale_step;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<Message, Renderer, Handle> Widget<Message, Renderer> for Viewer<Handle>
|
||||
where
|
||||
Renderer: image::Renderer<Handle = Handle>,
|
||||
Handle: Clone + Hash,
|
||||
{
|
||||
fn tag(&self) -> tree::Tag {
|
||||
tree::Tag::of::<State>()
|
||||
}
|
||||
|
||||
fn state(&self) -> tree::State {
|
||||
tree::State::new(State::new())
|
||||
}
|
||||
|
||||
fn width(&self) -> Length {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
let Size { width, height } = renderer.dimensions(&self.handle);
|
||||
|
||||
let mut size = limits
|
||||
.width(self.width)
|
||||
.height(self.height)
|
||||
.resolve(Size::new(width as f32, height as f32));
|
||||
|
||||
let expansion_size = if height > width {
|
||||
self.width
|
||||
} else {
|
||||
self.height
|
||||
};
|
||||
|
||||
// Only calculate viewport sizes if the images are constrained to a limited space.
|
||||
// If they are Fill|Portion let them expand within their alotted space.
|
||||
match expansion_size {
|
||||
Length::Shrink | Length::Fixed(_) => {
|
||||
let aspect_ratio = width as f32 / height as f32;
|
||||
let viewport_aspect_ratio = size.width / size.height;
|
||||
if viewport_aspect_ratio > aspect_ratio {
|
||||
size.width = width as f32 * size.height / height as f32;
|
||||
} else {
|
||||
size.height = height as f32 * size.width / width as f32;
|
||||
}
|
||||
}
|
||||
Length::Fill | Length::FillPortion(_) => {}
|
||||
}
|
||||
|
||||
layout::Node::new(size)
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
renderer: &Renderer,
|
||||
_clipboard: &mut dyn Clipboard,
|
||||
_shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
let bounds = layout.bounds();
|
||||
let is_mouse_over = bounds.contains(cursor_position);
|
||||
|
||||
match event {
|
||||
Event::Mouse(mouse::Event::WheelScrolled { delta })
|
||||
if is_mouse_over =>
|
||||
{
|
||||
match delta {
|
||||
mouse::ScrollDelta::Lines { y, .. }
|
||||
| mouse::ScrollDelta::Pixels { y, .. } => {
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
let previous_scale = state.scale;
|
||||
|
||||
if y < 0.0 && previous_scale > self.min_scale
|
||||
|| y > 0.0 && previous_scale < self.max_scale
|
||||
{
|
||||
state.scale = (if y > 0.0 {
|
||||
state.scale * (1.0 + self.scale_step)
|
||||
} else {
|
||||
state.scale / (1.0 + self.scale_step)
|
||||
})
|
||||
.clamp(self.min_scale, self.max_scale);
|
||||
|
||||
let image_size = image_size(
|
||||
renderer,
|
||||
&self.handle,
|
||||
state,
|
||||
bounds.size(),
|
||||
);
|
||||
|
||||
let factor = state.scale / previous_scale - 1.0;
|
||||
|
||||
let cursor_to_center =
|
||||
cursor_position - bounds.center();
|
||||
|
||||
let adjustment = cursor_to_center * factor
|
||||
+ state.current_offset * factor;
|
||||
|
||||
state.current_offset = Vector::new(
|
||||
if image_size.width > bounds.width {
|
||||
state.current_offset.x + adjustment.x
|
||||
} else {
|
||||
0.0
|
||||
},
|
||||
if image_size.height > bounds.height {
|
||||
state.current_offset.y + adjustment.y
|
||||
} else {
|
||||
0.0
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
event::Status::Captured
|
||||
}
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
|
||||
if is_mouse_over =>
|
||||
{
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
|
||||
state.cursor_grabbed_at = Some(cursor_position);
|
||||
state.starting_offset = state.current_offset;
|
||||
|
||||
event::Status::Captured
|
||||
}
|
||||
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
|
||||
if state.cursor_grabbed_at.is_some() {
|
||||
state.cursor_grabbed_at = None;
|
||||
|
||||
event::Status::Captured
|
||||
} else {
|
||||
event::Status::Ignored
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse::Event::CursorMoved { position }) => {
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
|
||||
if let Some(origin) = state.cursor_grabbed_at {
|
||||
let image_size = image_size(
|
||||
renderer,
|
||||
&self.handle,
|
||||
state,
|
||||
bounds.size(),
|
||||
);
|
||||
|
||||
let hidden_width = (image_size.width - bounds.width / 2.0)
|
||||
.max(0.0)
|
||||
.round();
|
||||
|
||||
let hidden_height = (image_size.height
|
||||
- bounds.height / 2.0)
|
||||
.max(0.0)
|
||||
.round();
|
||||
|
||||
let delta = position - origin;
|
||||
|
||||
let x = if bounds.width < image_size.width {
|
||||
(state.starting_offset.x - delta.x)
|
||||
.clamp(-hidden_width, hidden_width)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let y = if bounds.height < image_size.height {
|
||||
(state.starting_offset.y - delta.y)
|
||||
.clamp(-hidden_height, hidden_height)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
state.current_offset = Vector::new(x, y);
|
||||
|
||||
event::Status::Captured
|
||||
} else {
|
||||
event::Status::Ignored
|
||||
}
|
||||
}
|
||||
_ => event::Status::Ignored,
|
||||
}
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
_renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
let state = tree.state.downcast_ref::<State>();
|
||||
let bounds = layout.bounds();
|
||||
let is_mouse_over = bounds.contains(cursor_position);
|
||||
|
||||
if state.is_cursor_grabbed() {
|
||||
mouse::Interaction::Grabbing
|
||||
} else if is_mouse_over {
|
||||
mouse::Interaction::Grab
|
||||
} else {
|
||||
mouse::Interaction::Idle
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
_theme: &Renderer::Theme,
|
||||
_style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
_cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
) {
|
||||
let state = tree.state.downcast_ref::<State>();
|
||||
let bounds = layout.bounds();
|
||||
|
||||
let image_size =
|
||||
image_size(renderer, &self.handle, state, bounds.size());
|
||||
|
||||
let translation = {
|
||||
let image_top_left = Vector::new(
|
||||
bounds.width / 2.0 - image_size.width / 2.0,
|
||||
bounds.height / 2.0 - image_size.height / 2.0,
|
||||
);
|
||||
|
||||
image_top_left - state.offset(bounds, image_size)
|
||||
};
|
||||
|
||||
renderer.with_layer(bounds, |renderer| {
|
||||
renderer.with_translation(translation, |renderer| {
|
||||
image::Renderer::draw(
|
||||
renderer,
|
||||
self.handle.clone(),
|
||||
Rectangle {
|
||||
x: bounds.x,
|
||||
y: bounds.y,
|
||||
..Rectangle::with_size(image_size)
|
||||
},
|
||||
)
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// The local state of a [`Viewer`].
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct State {
|
||||
scale: f32,
|
||||
starting_offset: Vector,
|
||||
current_offset: Vector,
|
||||
cursor_grabbed_at: Option<Point>,
|
||||
}
|
||||
|
||||
impl Default for State {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
scale: 1.0,
|
||||
starting_offset: Vector::default(),
|
||||
current_offset: Vector::default(),
|
||||
cursor_grabbed_at: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl State {
|
||||
/// Creates a new [`State`].
|
||||
pub fn new() -> Self {
|
||||
State::default()
|
||||
}
|
||||
|
||||
/// Returns the current offset of the [`State`], given the bounds
|
||||
/// of the [`Viewer`] and its image.
|
||||
fn offset(&self, bounds: Rectangle, image_size: Size) -> Vector {
|
||||
let hidden_width =
|
||||
(image_size.width - bounds.width / 2.0).max(0.0).round();
|
||||
|
||||
let hidden_height =
|
||||
(image_size.height - bounds.height / 2.0).max(0.0).round();
|
||||
|
||||
Vector::new(
|
||||
self.current_offset.x.clamp(-hidden_width, hidden_width),
|
||||
self.current_offset.y.clamp(-hidden_height, hidden_height),
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns if the cursor is currently grabbed by the [`Viewer`].
|
||||
pub fn is_cursor_grabbed(&self) -> bool {
|
||||
self.cursor_grabbed_at.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer, Handle> From<Viewer<Handle>>
|
||||
for Element<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: 'a + image::Renderer<Handle = Handle>,
|
||||
Message: 'a,
|
||||
Handle: Clone + Hash + 'a,
|
||||
{
|
||||
fn from(viewer: Viewer<Handle>) -> Element<'a, Message, Renderer> {
|
||||
Element::new(viewer)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the bounds of the underlying image, given the bounds of
|
||||
/// the [`Viewer`]. Scaling will be applied and original aspect ratio
|
||||
/// will be respected.
|
||||
pub fn image_size<Renderer>(
|
||||
renderer: &Renderer,
|
||||
handle: &<Renderer as image::Renderer>::Handle,
|
||||
state: &State,
|
||||
bounds: Size,
|
||||
) -> Size
|
||||
where
|
||||
Renderer: image::Renderer,
|
||||
{
|
||||
let Size { width, height } = renderer.dimensions(handle);
|
||||
|
||||
let (width, height) = {
|
||||
let dimensions = (width as f32, height as f32);
|
||||
|
||||
let width_ratio = bounds.width / dimensions.0;
|
||||
let height_ratio = bounds.height / dimensions.1;
|
||||
|
||||
let ratio = width_ratio.min(height_ratio);
|
||||
let scale = state.scale;
|
||||
|
||||
if ratio < 1.0 {
|
||||
(dimensions.0 * ratio * scale, dimensions.1 * ratio * scale)
|
||||
} else {
|
||||
(dimensions.0 * scale, dimensions.1 * scale)
|
||||
}
|
||||
};
|
||||
|
||||
Size::new(width, height)
|
||||
}
|
||||
409
widget/src/lazy.rs
Normal file
409
widget/src/lazy.rs
Normal file
|
|
@ -0,0 +1,409 @@
|
|||
#![allow(clippy::await_holding_refcell_ref, clippy::type_complexity)]
|
||||
pub(crate) mod helpers;
|
||||
|
||||
pub mod component;
|
||||
pub mod responsive;
|
||||
|
||||
pub use component::Component;
|
||||
pub use responsive::Responsive;
|
||||
|
||||
mod cache;
|
||||
|
||||
use crate::core::event::{self, Event};
|
||||
use crate::core::layout::{self, Layout};
|
||||
use crate::core::mouse;
|
||||
use crate::core::overlay;
|
||||
use crate::core::renderer;
|
||||
use crate::core::widget::tree::{self, Tree};
|
||||
use crate::core::widget::{self, Widget};
|
||||
use crate::core::Element;
|
||||
use crate::core::{
|
||||
self, Clipboard, Hasher, Length, Point, Rectangle, Shell, Size,
|
||||
};
|
||||
|
||||
use ouroboros::self_referencing;
|
||||
use std::cell::RefCell;
|
||||
use std::hash::{Hash, Hasher as H};
|
||||
use std::rc::Rc;
|
||||
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Lazy<'a, Message, Renderer, Dependency, View> {
|
||||
dependency: Dependency,
|
||||
view: Box<dyn Fn(&Dependency) -> View + 'a>,
|
||||
element: RefCell<
|
||||
Option<Rc<RefCell<Option<Element<'static, Message, Renderer>>>>>,
|
||||
>,
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer, Dependency, View>
|
||||
Lazy<'a, Message, Renderer, Dependency, View>
|
||||
where
|
||||
Dependency: Hash + 'a,
|
||||
View: Into<Element<'static, Message, Renderer>>,
|
||||
{
|
||||
pub fn new(
|
||||
dependency: Dependency,
|
||||
view: impl Fn(&Dependency) -> View + 'a,
|
||||
) -> Self {
|
||||
Self {
|
||||
dependency,
|
||||
view: Box::new(view),
|
||||
element: RefCell::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn with_element<T>(
|
||||
&self,
|
||||
f: impl FnOnce(&Element<'_, Message, Renderer>) -> T,
|
||||
) -> T {
|
||||
f(self
|
||||
.element
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.unwrap())
|
||||
}
|
||||
|
||||
fn with_element_mut<T>(
|
||||
&self,
|
||||
f: impl FnOnce(&mut Element<'_, Message, Renderer>) -> T,
|
||||
) -> T {
|
||||
f(self
|
||||
.element
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.borrow_mut()
|
||||
.as_mut()
|
||||
.unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
struct Internal<Message, Renderer> {
|
||||
element: Rc<RefCell<Option<Element<'static, Message, Renderer>>>>,
|
||||
hash: u64,
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer, Dependency, View> Widget<Message, Renderer>
|
||||
for Lazy<'a, Message, Renderer, Dependency, View>
|
||||
where
|
||||
View: Into<Element<'static, Message, Renderer>> + 'static,
|
||||
Dependency: Hash + 'a,
|
||||
Message: 'static,
|
||||
Renderer: core::Renderer + 'static,
|
||||
{
|
||||
fn tag(&self) -> tree::Tag {
|
||||
struct Tag<T>(T);
|
||||
tree::Tag::of::<Tag<View>>()
|
||||
}
|
||||
|
||||
fn state(&self) -> tree::State {
|
||||
let mut hasher = Hasher::default();
|
||||
self.dependency.hash(&mut hasher);
|
||||
let hash = hasher.finish();
|
||||
|
||||
let element =
|
||||
Rc::new(RefCell::new(Some((self.view)(&self.dependency).into())));
|
||||
|
||||
(*self.element.borrow_mut()) = Some(element.clone());
|
||||
|
||||
tree::State::new(Internal { element, hash })
|
||||
}
|
||||
|
||||
fn children(&self) -> Vec<Tree> {
|
||||
self.with_element(|element| vec![Tree::new(element.as_widget())])
|
||||
}
|
||||
|
||||
fn diff(&self, tree: &mut Tree) {
|
||||
let current = tree.state.downcast_mut::<Internal<Message, Renderer>>();
|
||||
|
||||
let mut hasher = Hasher::default();
|
||||
self.dependency.hash(&mut hasher);
|
||||
let new_hash = hasher.finish();
|
||||
|
||||
if current.hash != new_hash {
|
||||
current.hash = new_hash;
|
||||
|
||||
let element = (self.view)(&self.dependency).into();
|
||||
current.element = Rc::new(RefCell::new(Some(element)));
|
||||
|
||||
(*self.element.borrow_mut()) = Some(current.element.clone());
|
||||
self.with_element(|element| {
|
||||
tree.diff_children(std::slice::from_ref(&element.as_widget()))
|
||||
});
|
||||
} else {
|
||||
(*self.element.borrow_mut()) = Some(current.element.clone());
|
||||
}
|
||||
}
|
||||
|
||||
fn width(&self) -> Length {
|
||||
self.with_element(|element| element.as_widget().width())
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
self.with_element(|element| element.as_widget().height())
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
self.with_element(|element| {
|
||||
element.as_widget().layout(renderer, limits)
|
||||
})
|
||||
}
|
||||
|
||||
fn operate(
|
||||
&self,
|
||||
tree: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn widget::Operation<Message>,
|
||||
) {
|
||||
self.with_element(|element| {
|
||||
element.as_widget().operate(
|
||||
&mut tree.children[0],
|
||||
layout,
|
||||
renderer,
|
||||
operation,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
self.with_element_mut(|element| {
|
||||
element.as_widget_mut().on_event(
|
||||
&mut tree.children[0],
|
||||
event,
|
||||
layout,
|
||||
cursor_position,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
self.with_element(|element| {
|
||||
element.as_widget().mouse_interaction(
|
||||
&tree.children[0],
|
||||
layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
renderer,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
self.with_element(|element| {
|
||||
element.as_widget().draw(
|
||||
&tree.children[0],
|
||||
renderer,
|
||||
theme,
|
||||
style,
|
||||
layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn overlay<'b>(
|
||||
&'b mut self,
|
||||
tree: &'b mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
) -> Option<overlay::Element<'_, Message, Renderer>> {
|
||||
let overlay = Overlay(Some(
|
||||
InnerBuilder {
|
||||
cell: self.element.borrow().as_ref().unwrap().clone(),
|
||||
element: self
|
||||
.element
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.borrow_mut()
|
||||
.take()
|
||||
.unwrap(),
|
||||
tree: &mut tree.children[0],
|
||||
overlay_builder: |element, tree| {
|
||||
element.as_widget_mut().overlay(tree, layout, renderer)
|
||||
},
|
||||
}
|
||||
.build(),
|
||||
));
|
||||
|
||||
let has_overlay = overlay
|
||||
.with_overlay_maybe(|overlay| overlay::Element::position(overlay));
|
||||
|
||||
has_overlay
|
||||
.map(|position| overlay::Element::new(position, Box::new(overlay)))
|
||||
}
|
||||
}
|
||||
|
||||
#[self_referencing]
|
||||
struct Inner<'a, Message, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Renderer: 'a,
|
||||
{
|
||||
cell: Rc<RefCell<Option<Element<'static, Message, Renderer>>>>,
|
||||
element: Element<'static, Message, Renderer>,
|
||||
tree: &'a mut Tree,
|
||||
|
||||
#[borrows(mut element, mut tree)]
|
||||
#[covariant]
|
||||
overlay: Option<overlay::Element<'this, Message, Renderer>>,
|
||||
}
|
||||
|
||||
struct Overlay<'a, Message, Renderer>(Option<Inner<'a, Message, Renderer>>);
|
||||
|
||||
impl<'a, Message, Renderer> Drop for Overlay<'a, Message, Renderer> {
|
||||
fn drop(&mut self) {
|
||||
let heads = self.0.take().unwrap().into_heads();
|
||||
(*heads.cell.borrow_mut()) = Some(heads.element);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Overlay<'a, Message, Renderer> {
|
||||
fn with_overlay_maybe<T>(
|
||||
&self,
|
||||
f: impl FnOnce(&overlay::Element<'_, Message, Renderer>) -> T,
|
||||
) -> Option<T> {
|
||||
self.0.as_ref().unwrap().borrow_overlay().as_ref().map(f)
|
||||
}
|
||||
|
||||
fn with_overlay_mut_maybe<T>(
|
||||
&mut self,
|
||||
f: impl FnOnce(&mut overlay::Element<'_, Message, Renderer>) -> T,
|
||||
) -> Option<T> {
|
||||
self.0
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.with_overlay_mut(|overlay| overlay.as_mut().map(f))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> overlay::Overlay<Message, Renderer>
|
||||
for Overlay<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: core::Renderer,
|
||||
{
|
||||
fn layout(
|
||||
&self,
|
||||
renderer: &Renderer,
|
||||
bounds: Size,
|
||||
position: Point,
|
||||
) -> layout::Node {
|
||||
self.with_overlay_maybe(|overlay| {
|
||||
let translation = position - overlay.position();
|
||||
|
||||
overlay.layout(renderer, bounds, translation)
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
) {
|
||||
let _ = self.with_overlay_maybe(|overlay| {
|
||||
overlay.draw(renderer, theme, style, layout, cursor_position);
|
||||
});
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
self.with_overlay_maybe(|overlay| {
|
||||
overlay.mouse_interaction(
|
||||
layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
renderer,
|
||||
)
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
self.with_overlay_mut_maybe(|overlay| {
|
||||
overlay.on_event(
|
||||
event,
|
||||
layout,
|
||||
cursor_position,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
)
|
||||
})
|
||||
.unwrap_or(event::Status::Ignored)
|
||||
}
|
||||
|
||||
fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool {
|
||||
self.with_overlay_maybe(|overlay| {
|
||||
overlay.is_over(layout, cursor_position)
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer, Dependency, View>
|
||||
From<Lazy<'a, Message, Renderer, Dependency, View>>
|
||||
for Element<'a, Message, Renderer>
|
||||
where
|
||||
View: Into<Element<'static, Message, Renderer>> + 'static,
|
||||
Renderer: core::Renderer + 'static,
|
||||
Message: 'static,
|
||||
Dependency: Hash + 'a,
|
||||
{
|
||||
fn from(lazy: Lazy<'a, Message, Renderer, Dependency, View>) -> Self {
|
||||
Self::new(lazy)
|
||||
}
|
||||
}
|
||||
13
widget/src/lazy/cache.rs
Normal file
13
widget/src/lazy/cache.rs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
use crate::core::overlay;
|
||||
use crate::core::Element;
|
||||
|
||||
use ouroboros::self_referencing;
|
||||
|
||||
#[self_referencing(pub_extras)]
|
||||
pub struct Cache<'a, Message: 'a, Renderer: 'a> {
|
||||
pub element: Element<'a, Message, Renderer>,
|
||||
|
||||
#[borrows(mut element)]
|
||||
#[covariant]
|
||||
overlay: Option<overlay::Element<'this, Message, Renderer>>,
|
||||
}
|
||||
575
widget/src/lazy/component.rs
Normal file
575
widget/src/lazy/component.rs
Normal file
|
|
@ -0,0 +1,575 @@
|
|||
//! Build and reuse custom widgets using The Elm Architecture.
|
||||
use crate::core::event;
|
||||
use crate::core::layout::{self, Layout};
|
||||
use crate::core::mouse;
|
||||
use crate::core::overlay;
|
||||
use crate::core::renderer;
|
||||
use crate::core::widget;
|
||||
use crate::core::widget::tree::{self, Tree};
|
||||
use crate::core::{
|
||||
self, Clipboard, Element, Length, Point, Rectangle, Shell, Size, Widget,
|
||||
};
|
||||
|
||||
use ouroboros::self_referencing;
|
||||
use std::cell::RefCell;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
/// A reusable, custom widget that uses The Elm Architecture.
|
||||
///
|
||||
/// A [`Component`] allows you to implement custom widgets as if they were
|
||||
/// `iced` applications with encapsulated state.
|
||||
///
|
||||
/// In other words, a [`Component`] allows you to turn `iced` applications into
|
||||
/// custom widgets and embed them without cumbersome wiring.
|
||||
///
|
||||
/// A [`Component`] produces widgets that may fire an [`Event`](Component::Event)
|
||||
/// and update the internal state of the [`Component`].
|
||||
///
|
||||
/// Additionally, a [`Component`] is capable of producing a `Message` to notify
|
||||
/// the parent application of any relevant interactions.
|
||||
pub trait Component<Message, Renderer> {
|
||||
/// The internal state of this [`Component`].
|
||||
type State: Default;
|
||||
|
||||
/// The type of event this [`Component`] handles internally.
|
||||
type Event;
|
||||
|
||||
/// Processes an [`Event`](Component::Event) and updates the [`Component`] state accordingly.
|
||||
///
|
||||
/// It can produce a `Message` for the parent application.
|
||||
fn update(
|
||||
&mut self,
|
||||
state: &mut Self::State,
|
||||
event: Self::Event,
|
||||
) -> Option<Message>;
|
||||
|
||||
/// Produces the widgets of the [`Component`], which may trigger an [`Event`](Component::Event)
|
||||
/// on user interaction.
|
||||
fn view(&self, state: &Self::State) -> Element<'_, Self::Event, Renderer>;
|
||||
|
||||
/// Update the [`Component`] state based on the provided [`Operation`](widget::Operation)
|
||||
///
|
||||
/// By default, it does nothing.
|
||||
fn operate(
|
||||
&self,
|
||||
_state: &mut Self::State,
|
||||
_operation: &mut dyn widget::Operation<Message>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Turns an implementor of [`Component`] into an [`Element`] that can be
|
||||
/// embedded in any application.
|
||||
pub fn view<'a, C, Message, Renderer>(
|
||||
component: C,
|
||||
) -> Element<'a, Message, Renderer>
|
||||
where
|
||||
C: Component<Message, Renderer> + 'a,
|
||||
C::State: 'static,
|
||||
Message: 'a,
|
||||
Renderer: core::Renderer + 'a,
|
||||
{
|
||||
Element::new(Instance {
|
||||
state: RefCell::new(Some(
|
||||
StateBuilder {
|
||||
component: Box::new(component),
|
||||
message: PhantomData,
|
||||
state: PhantomData,
|
||||
element_builder: |_| None,
|
||||
}
|
||||
.build(),
|
||||
)),
|
||||
})
|
||||
}
|
||||
|
||||
struct Instance<'a, Message, Renderer, Event, S> {
|
||||
state: RefCell<Option<State<'a, Message, Renderer, Event, S>>>,
|
||||
}
|
||||
|
||||
#[self_referencing]
|
||||
struct State<'a, Message: 'a, Renderer: 'a, Event: 'a, S: 'a> {
|
||||
component:
|
||||
Box<dyn Component<Message, Renderer, Event = Event, State = S> + 'a>,
|
||||
message: PhantomData<Message>,
|
||||
state: PhantomData<S>,
|
||||
|
||||
#[borrows(component)]
|
||||
#[covariant]
|
||||
element: Option<Element<'this, Event, Renderer>>,
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer, Event, S> Instance<'a, Message, Renderer, Event, S>
|
||||
where
|
||||
S: Default,
|
||||
{
|
||||
fn rebuild_element(&self, state: &S) {
|
||||
let heads = self.state.borrow_mut().take().unwrap().into_heads();
|
||||
|
||||
*self.state.borrow_mut() = Some(
|
||||
StateBuilder {
|
||||
component: heads.component,
|
||||
message: PhantomData,
|
||||
state: PhantomData,
|
||||
element_builder: |component| Some(component.view(state)),
|
||||
}
|
||||
.build(),
|
||||
);
|
||||
}
|
||||
|
||||
fn rebuild_element_with_operation(
|
||||
&self,
|
||||
state: &mut S,
|
||||
operation: &mut dyn widget::Operation<Message>,
|
||||
) {
|
||||
let heads = self.state.borrow_mut().take().unwrap().into_heads();
|
||||
|
||||
heads.component.operate(state, operation);
|
||||
|
||||
*self.state.borrow_mut() = Some(
|
||||
StateBuilder {
|
||||
component: heads.component,
|
||||
message: PhantomData,
|
||||
state: PhantomData,
|
||||
element_builder: |component| Some(component.view(state)),
|
||||
}
|
||||
.build(),
|
||||
);
|
||||
}
|
||||
|
||||
fn with_element<T>(
|
||||
&self,
|
||||
f: impl FnOnce(&Element<'_, Event, Renderer>) -> T,
|
||||
) -> T {
|
||||
self.with_element_mut(|element| f(element))
|
||||
}
|
||||
|
||||
fn with_element_mut<T>(
|
||||
&self,
|
||||
f: impl FnOnce(&mut Element<'_, Event, Renderer>) -> T,
|
||||
) -> T {
|
||||
self.state
|
||||
.borrow_mut()
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.with_element_mut(|element| f(element.as_mut().unwrap()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer, Event, S> Widget<Message, Renderer>
|
||||
for Instance<'a, Message, Renderer, Event, S>
|
||||
where
|
||||
S: 'static + Default,
|
||||
Renderer: core::Renderer,
|
||||
{
|
||||
fn tag(&self) -> tree::Tag {
|
||||
struct Tag<T>(T);
|
||||
tree::Tag::of::<Tag<S>>()
|
||||
}
|
||||
|
||||
fn state(&self) -> tree::State {
|
||||
tree::State::new(S::default())
|
||||
}
|
||||
|
||||
fn children(&self) -> Vec<Tree> {
|
||||
self.rebuild_element(&S::default());
|
||||
self.with_element(|element| vec![Tree::new(element)])
|
||||
}
|
||||
|
||||
fn diff(&self, tree: &mut Tree) {
|
||||
self.rebuild_element(tree.state.downcast_ref());
|
||||
self.with_element(|element| {
|
||||
tree.diff_children(std::slice::from_ref(&element))
|
||||
})
|
||||
}
|
||||
|
||||
fn width(&self) -> Length {
|
||||
self.with_element(|element| element.as_widget().width())
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
self.with_element(|element| element.as_widget().height())
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
self.with_element(|element| {
|
||||
element.as_widget().layout(renderer, limits)
|
||||
})
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: core::Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
let mut local_messages = Vec::new();
|
||||
let mut local_shell = Shell::new(&mut local_messages);
|
||||
|
||||
let event_status = self.with_element_mut(|element| {
|
||||
element.as_widget_mut().on_event(
|
||||
&mut tree.children[0],
|
||||
event,
|
||||
layout,
|
||||
cursor_position,
|
||||
renderer,
|
||||
clipboard,
|
||||
&mut local_shell,
|
||||
)
|
||||
});
|
||||
|
||||
local_shell.revalidate_layout(|| shell.invalidate_layout());
|
||||
|
||||
if let Some(redraw_request) = local_shell.redraw_request() {
|
||||
shell.request_redraw(redraw_request);
|
||||
}
|
||||
|
||||
if !local_messages.is_empty() {
|
||||
let mut heads = self.state.take().unwrap().into_heads();
|
||||
|
||||
for message in local_messages.into_iter().filter_map(|message| {
|
||||
heads
|
||||
.component
|
||||
.update(tree.state.downcast_mut::<S>(), message)
|
||||
}) {
|
||||
shell.publish(message);
|
||||
}
|
||||
|
||||
self.state = RefCell::new(Some(
|
||||
StateBuilder {
|
||||
component: heads.component,
|
||||
message: PhantomData,
|
||||
state: PhantomData,
|
||||
element_builder: |state| {
|
||||
Some(state.view(tree.state.downcast_ref::<S>()))
|
||||
},
|
||||
}
|
||||
.build(),
|
||||
));
|
||||
|
||||
self.with_element(|element| {
|
||||
tree.diff_children(std::slice::from_ref(&element))
|
||||
});
|
||||
|
||||
shell.invalidate_layout();
|
||||
}
|
||||
|
||||
event_status
|
||||
}
|
||||
|
||||
fn operate(
|
||||
&self,
|
||||
tree: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn widget::Operation<Message>,
|
||||
) {
|
||||
self.rebuild_element_with_operation(
|
||||
tree.state.downcast_mut(),
|
||||
operation,
|
||||
);
|
||||
|
||||
struct MapOperation<'a, B> {
|
||||
operation: &'a mut dyn widget::Operation<B>,
|
||||
}
|
||||
|
||||
impl<'a, T, B> widget::Operation<T> for MapOperation<'a, B> {
|
||||
fn container(
|
||||
&mut self,
|
||||
id: Option<&widget::Id>,
|
||||
operate_on_children: &mut dyn FnMut(
|
||||
&mut dyn widget::Operation<T>,
|
||||
),
|
||||
) {
|
||||
self.operation.container(id, &mut |operation| {
|
||||
operate_on_children(&mut MapOperation { operation });
|
||||
});
|
||||
}
|
||||
|
||||
fn focusable(
|
||||
&mut self,
|
||||
state: &mut dyn widget::operation::Focusable,
|
||||
id: Option<&widget::Id>,
|
||||
) {
|
||||
self.operation.focusable(state, id);
|
||||
}
|
||||
|
||||
fn text_input(
|
||||
&mut self,
|
||||
state: &mut dyn widget::operation::TextInput,
|
||||
id: Option<&widget::Id>,
|
||||
) {
|
||||
self.operation.text_input(state, id);
|
||||
}
|
||||
}
|
||||
|
||||
self.with_element(|element| {
|
||||
tree.diff_children(std::slice::from_ref(&element));
|
||||
|
||||
element.as_widget().operate(
|
||||
&mut tree.children[0],
|
||||
layout,
|
||||
renderer,
|
||||
&mut MapOperation { operation },
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
self.with_element(|element| {
|
||||
element.as_widget().draw(
|
||||
&tree.children[0],
|
||||
renderer,
|
||||
theme,
|
||||
style,
|
||||
layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
self.with_element(|element| {
|
||||
element.as_widget().mouse_interaction(
|
||||
&tree.children[0],
|
||||
layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
renderer,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn overlay<'b>(
|
||||
&'b mut self,
|
||||
tree: &'b mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
) -> Option<overlay::Element<'b, Message, Renderer>> {
|
||||
let overlay = OverlayBuilder {
|
||||
instance: self,
|
||||
tree,
|
||||
types: PhantomData,
|
||||
overlay_builder: |instance, tree| {
|
||||
instance.state.get_mut().as_mut().unwrap().with_element_mut(
|
||||
move |element| {
|
||||
element.as_mut().unwrap().as_widget_mut().overlay(
|
||||
&mut tree.children[0],
|
||||
layout,
|
||||
renderer,
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
}
|
||||
.build();
|
||||
|
||||
let has_overlay = overlay.with_overlay(|overlay| {
|
||||
overlay.as_ref().map(overlay::Element::position)
|
||||
});
|
||||
|
||||
has_overlay.map(|position| {
|
||||
overlay::Element::new(
|
||||
position,
|
||||
Box::new(OverlayInstance {
|
||||
overlay: Some(overlay),
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[self_referencing]
|
||||
struct Overlay<'a, 'b, Message, Renderer, Event, S> {
|
||||
instance: &'a mut Instance<'b, Message, Renderer, Event, S>,
|
||||
tree: &'a mut Tree,
|
||||
types: PhantomData<(Message, Event, S)>,
|
||||
|
||||
#[borrows(mut instance, mut tree)]
|
||||
#[covariant]
|
||||
overlay: Option<overlay::Element<'this, Event, Renderer>>,
|
||||
}
|
||||
|
||||
struct OverlayInstance<'a, 'b, Message, Renderer, Event, S> {
|
||||
overlay: Option<Overlay<'a, 'b, Message, Renderer, Event, S>>,
|
||||
}
|
||||
|
||||
impl<'a, 'b, Message, Renderer, Event, S>
|
||||
OverlayInstance<'a, 'b, Message, Renderer, Event, S>
|
||||
{
|
||||
fn with_overlay_maybe<T>(
|
||||
&self,
|
||||
f: impl FnOnce(&overlay::Element<'_, Event, Renderer>) -> T,
|
||||
) -> Option<T> {
|
||||
self.overlay
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.borrow_overlay()
|
||||
.as_ref()
|
||||
.map(f)
|
||||
}
|
||||
|
||||
fn with_overlay_mut_maybe<T>(
|
||||
&mut self,
|
||||
f: impl FnOnce(&mut overlay::Element<'_, Event, Renderer>) -> T,
|
||||
) -> Option<T> {
|
||||
self.overlay
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.with_overlay_mut(|overlay| overlay.as_mut().map(f))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b, Message, Renderer, Event, S> overlay::Overlay<Message, Renderer>
|
||||
for OverlayInstance<'a, 'b, Message, Renderer, Event, S>
|
||||
where
|
||||
Renderer: core::Renderer,
|
||||
S: 'static + Default,
|
||||
{
|
||||
fn layout(
|
||||
&self,
|
||||
renderer: &Renderer,
|
||||
bounds: Size,
|
||||
position: Point,
|
||||
) -> layout::Node {
|
||||
self.with_overlay_maybe(|overlay| {
|
||||
let translation = position - overlay.position();
|
||||
|
||||
overlay.layout(renderer, bounds, translation)
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
) {
|
||||
let _ = self.with_overlay_maybe(|overlay| {
|
||||
overlay.draw(renderer, theme, style, layout, cursor_position);
|
||||
});
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
self.with_overlay_maybe(|overlay| {
|
||||
overlay.mouse_interaction(
|
||||
layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
renderer,
|
||||
)
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
event: core::Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
let mut local_messages = Vec::new();
|
||||
let mut local_shell = Shell::new(&mut local_messages);
|
||||
|
||||
let event_status = self
|
||||
.with_overlay_mut_maybe(|overlay| {
|
||||
overlay.on_event(
|
||||
event,
|
||||
layout,
|
||||
cursor_position,
|
||||
renderer,
|
||||
clipboard,
|
||||
&mut local_shell,
|
||||
)
|
||||
})
|
||||
.unwrap_or(event::Status::Ignored);
|
||||
|
||||
local_shell.revalidate_layout(|| shell.invalidate_layout());
|
||||
|
||||
if !local_messages.is_empty() {
|
||||
let overlay = self.overlay.take().unwrap().into_heads();
|
||||
let mut heads = overlay.instance.state.take().unwrap().into_heads();
|
||||
|
||||
for message in local_messages.into_iter().filter_map(|message| {
|
||||
heads
|
||||
.component
|
||||
.update(overlay.tree.state.downcast_mut::<S>(), message)
|
||||
}) {
|
||||
shell.publish(message);
|
||||
}
|
||||
|
||||
*overlay.instance.state.borrow_mut() = Some(
|
||||
StateBuilder {
|
||||
component: heads.component,
|
||||
message: PhantomData,
|
||||
state: PhantomData,
|
||||
element_builder: |state| {
|
||||
Some(state.view(overlay.tree.state.downcast_ref::<S>()))
|
||||
},
|
||||
}
|
||||
.build(),
|
||||
);
|
||||
|
||||
overlay.instance.with_element(|element| {
|
||||
overlay.tree.diff_children(std::slice::from_ref(&element))
|
||||
});
|
||||
|
||||
self.overlay = Some(
|
||||
OverlayBuilder {
|
||||
instance: overlay.instance,
|
||||
tree: overlay.tree,
|
||||
types: PhantomData,
|
||||
overlay_builder: |_, _| None,
|
||||
}
|
||||
.build(),
|
||||
);
|
||||
|
||||
shell.invalidate_layout();
|
||||
}
|
||||
|
||||
event_status
|
||||
}
|
||||
|
||||
fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool {
|
||||
self.with_overlay_maybe(|overlay| {
|
||||
overlay.is_over(layout, cursor_position)
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
39
widget/src/lazy/helpers.rs
Normal file
39
widget/src/lazy/helpers.rs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
use crate::core::{self, Element, Size};
|
||||
use crate::lazy::component::{self, Component};
|
||||
use crate::lazy::{Lazy, Responsive};
|
||||
|
||||
use std::hash::Hash;
|
||||
|
||||
pub fn lazy<'a, Message, Renderer, Dependency, View>(
|
||||
dependency: Dependency,
|
||||
view: impl Fn(&Dependency) -> View + 'a,
|
||||
) -> Lazy<'a, Message, Renderer, Dependency, View>
|
||||
where
|
||||
Dependency: Hash + 'a,
|
||||
View: Into<Element<'static, Message, Renderer>>,
|
||||
{
|
||||
Lazy::new(dependency, view)
|
||||
}
|
||||
|
||||
/// Turns an implementor of [`Component`] into an [`Element`] that can be
|
||||
/// embedded in any application.
|
||||
pub fn component<'a, C, Message, Renderer>(
|
||||
component: C,
|
||||
) -> Element<'a, Message, Renderer>
|
||||
where
|
||||
C: Component<Message, Renderer> + 'a,
|
||||
C::State: 'static,
|
||||
Message: 'a,
|
||||
Renderer: core::Renderer + 'a,
|
||||
{
|
||||
component::view(component)
|
||||
}
|
||||
|
||||
pub fn responsive<'a, Message, Renderer>(
|
||||
f: impl Fn(Size) -> Element<'a, Message, Renderer> + 'a,
|
||||
) -> Responsive<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: core::Renderer,
|
||||
{
|
||||
Responsive::new(f)
|
||||
}
|
||||
427
widget/src/lazy/responsive.rs
Normal file
427
widget/src/lazy/responsive.rs
Normal file
|
|
@ -0,0 +1,427 @@
|
|||
use crate::core::event::{self, Event};
|
||||
use crate::core::layout::{self, Layout};
|
||||
use crate::core::mouse;
|
||||
use crate::core::overlay;
|
||||
use crate::core::renderer;
|
||||
use crate::core::widget;
|
||||
use crate::core::widget::tree::{self, Tree};
|
||||
use crate::core::{
|
||||
self, Clipboard, Element, Length, Point, Rectangle, Shell, Size, Widget,
|
||||
};
|
||||
use crate::horizontal_space;
|
||||
|
||||
use ouroboros::self_referencing;
|
||||
use std::cell::{RefCell, RefMut};
|
||||
use std::marker::PhantomData;
|
||||
use std::ops::Deref;
|
||||
|
||||
/// A widget that is aware of its dimensions.
|
||||
///
|
||||
/// A [`Responsive`] widget will always try to fill all the available space of
|
||||
/// its parent.
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Responsive<'a, Message, Renderer = crate::Renderer> {
|
||||
view: Box<dyn Fn(Size) -> Element<'a, Message, Renderer> + 'a>,
|
||||
content: RefCell<Content<'a, Message, Renderer>>,
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Responsive<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: core::Renderer,
|
||||
{
|
||||
/// Creates a new [`Responsive`] widget with a closure that produces its
|
||||
/// contents.
|
||||
///
|
||||
/// The `view` closure will be provided with the current [`Size`] of
|
||||
/// the [`Responsive`] widget and, therefore, can be used to build the
|
||||
/// contents of the widget in a responsive way.
|
||||
pub fn new(
|
||||
view: impl Fn(Size) -> Element<'a, Message, Renderer> + 'a,
|
||||
) -> Self {
|
||||
Self {
|
||||
view: Box::new(view),
|
||||
content: RefCell::new(Content {
|
||||
size: Size::ZERO,
|
||||
layout: layout::Node::new(Size::ZERO),
|
||||
element: Element::new(horizontal_space(0)),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Content<'a, Message, Renderer> {
|
||||
size: Size,
|
||||
layout: layout::Node,
|
||||
element: Element<'a, Message, Renderer>,
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Content<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: core::Renderer,
|
||||
{
|
||||
fn update(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
renderer: &Renderer,
|
||||
new_size: Size,
|
||||
view: &dyn Fn(Size) -> Element<'a, Message, Renderer>,
|
||||
) {
|
||||
if self.size == new_size {
|
||||
return;
|
||||
}
|
||||
|
||||
self.element = view(new_size);
|
||||
self.size = new_size;
|
||||
|
||||
tree.diff(&self.element);
|
||||
|
||||
self.layout = self
|
||||
.element
|
||||
.as_widget()
|
||||
.layout(renderer, &layout::Limits::new(Size::ZERO, self.size));
|
||||
}
|
||||
|
||||
fn resolve<R, T>(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
renderer: R,
|
||||
layout: Layout<'_>,
|
||||
view: &dyn Fn(Size) -> Element<'a, Message, Renderer>,
|
||||
f: impl FnOnce(
|
||||
&mut Tree,
|
||||
R,
|
||||
Layout<'_>,
|
||||
&mut Element<'a, Message, Renderer>,
|
||||
) -> T,
|
||||
) -> T
|
||||
where
|
||||
R: Deref<Target = Renderer>,
|
||||
{
|
||||
self.update(tree, renderer.deref(), layout.bounds().size(), view);
|
||||
|
||||
let content_layout = Layout::with_offset(
|
||||
layout.position() - Point::ORIGIN,
|
||||
&self.layout,
|
||||
);
|
||||
|
||||
f(tree, renderer, content_layout, &mut self.element)
|
||||
}
|
||||
}
|
||||
|
||||
struct State {
|
||||
tree: RefCell<Tree>,
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Widget<Message, Renderer>
|
||||
for Responsive<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: core::Renderer,
|
||||
{
|
||||
fn tag(&self) -> tree::Tag {
|
||||
tree::Tag::of::<State>()
|
||||
}
|
||||
|
||||
fn state(&self) -> tree::State {
|
||||
tree::State::new(State {
|
||||
tree: RefCell::new(Tree::empty()),
|
||||
})
|
||||
}
|
||||
|
||||
fn width(&self) -> Length {
|
||||
Length::Fill
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
Length::Fill
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
_renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
layout::Node::new(limits.max())
|
||||
}
|
||||
|
||||
fn operate(
|
||||
&self,
|
||||
tree: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn widget::Operation<Message>,
|
||||
) {
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
let mut content = self.content.borrow_mut();
|
||||
|
||||
content.resolve(
|
||||
&mut state.tree.borrow_mut(),
|
||||
renderer,
|
||||
layout,
|
||||
&self.view,
|
||||
|tree, renderer, layout, element| {
|
||||
element
|
||||
.as_widget()
|
||||
.operate(tree, layout, renderer, operation);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
let mut content = self.content.borrow_mut();
|
||||
|
||||
content.resolve(
|
||||
&mut state.tree.borrow_mut(),
|
||||
renderer,
|
||||
layout,
|
||||
&self.view,
|
||||
|tree, renderer, layout, element| {
|
||||
element.as_widget_mut().on_event(
|
||||
tree,
|
||||
event,
|
||||
layout,
|
||||
cursor_position,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
let state = tree.state.downcast_ref::<State>();
|
||||
let mut content = self.content.borrow_mut();
|
||||
|
||||
content.resolve(
|
||||
&mut state.tree.borrow_mut(),
|
||||
renderer,
|
||||
layout,
|
||||
&self.view,
|
||||
|tree, renderer, layout, element| {
|
||||
element.as_widget().draw(
|
||||
tree,
|
||||
renderer,
|
||||
theme,
|
||||
style,
|
||||
layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
let state = tree.state.downcast_ref::<State>();
|
||||
let mut content = self.content.borrow_mut();
|
||||
|
||||
content.resolve(
|
||||
&mut state.tree.borrow_mut(),
|
||||
renderer,
|
||||
layout,
|
||||
&self.view,
|
||||
|tree, renderer, layout, element| {
|
||||
element.as_widget().mouse_interaction(
|
||||
tree,
|
||||
layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
renderer,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn overlay<'b>(
|
||||
&'b mut self,
|
||||
tree: &'b mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
) -> Option<overlay::Element<'b, Message, Renderer>> {
|
||||
use std::ops::DerefMut;
|
||||
|
||||
let state = tree.state.downcast_ref::<State>();
|
||||
|
||||
let overlay = OverlayBuilder {
|
||||
content: self.content.borrow_mut(),
|
||||
tree: state.tree.borrow_mut(),
|
||||
types: PhantomData,
|
||||
overlay_builder: |content: &mut RefMut<'_, Content<'_, _, _>>,
|
||||
tree| {
|
||||
content.update(
|
||||
tree,
|
||||
renderer,
|
||||
layout.bounds().size(),
|
||||
&self.view,
|
||||
);
|
||||
|
||||
let Content {
|
||||
element,
|
||||
layout: content_layout,
|
||||
..
|
||||
} = content.deref_mut();
|
||||
|
||||
let content_layout = Layout::with_offset(
|
||||
layout.bounds().position() - Point::ORIGIN,
|
||||
content_layout,
|
||||
);
|
||||
|
||||
element
|
||||
.as_widget_mut()
|
||||
.overlay(tree, content_layout, renderer)
|
||||
},
|
||||
}
|
||||
.build();
|
||||
|
||||
let has_overlay = overlay.with_overlay(|overlay| {
|
||||
overlay.as_ref().map(overlay::Element::position)
|
||||
});
|
||||
|
||||
has_overlay
|
||||
.map(|position| overlay::Element::new(position, Box::new(overlay)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> From<Responsive<'a, Message, Renderer>>
|
||||
for Element<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: core::Renderer + 'a,
|
||||
Message: 'a,
|
||||
{
|
||||
fn from(responsive: Responsive<'a, Message, Renderer>) -> Self {
|
||||
Self::new(responsive)
|
||||
}
|
||||
}
|
||||
|
||||
#[self_referencing]
|
||||
struct Overlay<'a, 'b, Message, Renderer> {
|
||||
content: RefMut<'a, Content<'b, Message, Renderer>>,
|
||||
tree: RefMut<'a, Tree>,
|
||||
types: PhantomData<Message>,
|
||||
|
||||
#[borrows(mut content, mut tree)]
|
||||
#[covariant]
|
||||
overlay: Option<overlay::Element<'this, Message, Renderer>>,
|
||||
}
|
||||
|
||||
impl<'a, 'b, Message, Renderer> Overlay<'a, 'b, Message, Renderer> {
|
||||
fn with_overlay_maybe<T>(
|
||||
&self,
|
||||
f: impl FnOnce(&overlay::Element<'_, Message, Renderer>) -> T,
|
||||
) -> Option<T> {
|
||||
self.borrow_overlay().as_ref().map(f)
|
||||
}
|
||||
|
||||
fn with_overlay_mut_maybe<T>(
|
||||
&mut self,
|
||||
f: impl FnOnce(&mut overlay::Element<'_, Message, Renderer>) -> T,
|
||||
) -> Option<T> {
|
||||
self.with_overlay_mut(|overlay| overlay.as_mut().map(f))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b, Message, Renderer> overlay::Overlay<Message, Renderer>
|
||||
for Overlay<'a, 'b, Message, Renderer>
|
||||
where
|
||||
Renderer: core::Renderer,
|
||||
{
|
||||
fn layout(
|
||||
&self,
|
||||
renderer: &Renderer,
|
||||
bounds: Size,
|
||||
position: Point,
|
||||
) -> layout::Node {
|
||||
self.with_overlay_maybe(|overlay| {
|
||||
let translation = position - overlay.position();
|
||||
|
||||
overlay.layout(renderer, bounds, translation)
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
) {
|
||||
let _ = self.with_overlay_maybe(|overlay| {
|
||||
overlay.draw(renderer, theme, style, layout, cursor_position);
|
||||
});
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
self.with_overlay_maybe(|overlay| {
|
||||
overlay.mouse_interaction(
|
||||
layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
renderer,
|
||||
)
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
self.with_overlay_mut_maybe(|overlay| {
|
||||
overlay.on_event(
|
||||
event,
|
||||
layout,
|
||||
cursor_position,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
)
|
||||
})
|
||||
.unwrap_or(event::Status::Ignored)
|
||||
}
|
||||
|
||||
fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool {
|
||||
self.with_overlay_maybe(|overlay| {
|
||||
overlay.is_over(layout, cursor_position)
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
122
widget/src/lib.rs
Normal file
122
widget/src/lib.rs
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
//! Use the built-in widgets or create your own.
|
||||
#![doc(
|
||||
html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg"
|
||||
)]
|
||||
#![deny(
|
||||
missing_debug_implementations,
|
||||
//missing_docs,
|
||||
unused_results,
|
||||
clippy::extra_unused_lifetimes,
|
||||
clippy::from_over_into,
|
||||
clippy::needless_borrow,
|
||||
clippy::new_without_default,
|
||||
clippy::useless_conversion
|
||||
)]
|
||||
#![forbid(unsafe_code, rust_2018_idioms)]
|
||||
#![allow(clippy::inherent_to_string, clippy::type_complexity)]
|
||||
pub use iced_native as native;
|
||||
pub use iced_native::core;
|
||||
pub use iced_renderer as renderer;
|
||||
pub use iced_renderer::graphics;
|
||||
pub use iced_style as style;
|
||||
|
||||
mod column;
|
||||
mod row;
|
||||
|
||||
pub mod button;
|
||||
pub mod checkbox;
|
||||
pub mod container;
|
||||
pub mod overlay;
|
||||
pub mod pane_grid;
|
||||
pub mod pick_list;
|
||||
pub mod progress_bar;
|
||||
pub mod radio;
|
||||
pub mod rule;
|
||||
pub mod scrollable;
|
||||
pub mod slider;
|
||||
pub mod space;
|
||||
pub mod text;
|
||||
pub mod text_input;
|
||||
pub mod toggler;
|
||||
pub mod tooltip;
|
||||
pub mod vertical_slider;
|
||||
|
||||
mod helpers;
|
||||
|
||||
pub use helpers::*;
|
||||
|
||||
#[cfg(feature = "lazy")]
|
||||
mod lazy;
|
||||
|
||||
#[cfg(feature = "lazy")]
|
||||
pub use crate::lazy::{Component, Lazy, Responsive};
|
||||
|
||||
#[cfg(feature = "lazy")]
|
||||
pub use crate::lazy::helpers::*;
|
||||
|
||||
#[doc(no_inline)]
|
||||
pub use button::Button;
|
||||
#[doc(no_inline)]
|
||||
pub use checkbox::Checkbox;
|
||||
#[doc(no_inline)]
|
||||
pub use column::Column;
|
||||
#[doc(no_inline)]
|
||||
pub use container::Container;
|
||||
#[doc(no_inline)]
|
||||
pub use pane_grid::PaneGrid;
|
||||
#[doc(no_inline)]
|
||||
pub use pick_list::PickList;
|
||||
#[doc(no_inline)]
|
||||
pub use progress_bar::ProgressBar;
|
||||
#[doc(no_inline)]
|
||||
pub use radio::Radio;
|
||||
#[doc(no_inline)]
|
||||
pub use row::Row;
|
||||
#[doc(no_inline)]
|
||||
pub use rule::Rule;
|
||||
#[doc(no_inline)]
|
||||
pub use scrollable::Scrollable;
|
||||
#[doc(no_inline)]
|
||||
pub use slider::Slider;
|
||||
#[doc(no_inline)]
|
||||
pub use space::Space;
|
||||
#[doc(no_inline)]
|
||||
pub use text::Text;
|
||||
#[doc(no_inline)]
|
||||
pub use text_input::TextInput;
|
||||
#[doc(no_inline)]
|
||||
pub use toggler::Toggler;
|
||||
#[doc(no_inline)]
|
||||
pub use tooltip::Tooltip;
|
||||
#[doc(no_inline)]
|
||||
pub use vertical_slider::VerticalSlider;
|
||||
|
||||
#[cfg(feature = "svg")]
|
||||
pub mod svg;
|
||||
|
||||
#[cfg(feature = "svg")]
|
||||
#[doc(no_inline)]
|
||||
pub use svg::Svg;
|
||||
|
||||
#[cfg(feature = "image")]
|
||||
pub mod image;
|
||||
|
||||
#[cfg(feature = "image")]
|
||||
#[doc(no_inline)]
|
||||
pub use image::Image;
|
||||
|
||||
#[cfg(feature = "canvas")]
|
||||
pub mod canvas;
|
||||
|
||||
#[cfg(feature = "canvas")]
|
||||
#[doc(no_inline)]
|
||||
pub use canvas::Canvas;
|
||||
|
||||
#[cfg(feature = "qr_code")]
|
||||
pub mod qr_code;
|
||||
|
||||
#[cfg(feature = "qr_code")]
|
||||
#[doc(no_inline)]
|
||||
pub use qr_code::QRCode;
|
||||
|
||||
type Renderer<Theme = style::Theme> = renderer::Renderer<Theme>;
|
||||
1
widget/src/overlay.rs
Normal file
1
widget/src/overlay.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
pub mod menu;
|
||||
519
widget/src/overlay/menu.rs
Normal file
519
widget/src/overlay/menu.rs
Normal file
|
|
@ -0,0 +1,519 @@
|
|||
//! Build and show dropdown menus.
|
||||
use crate::container::{self, Container};
|
||||
use crate::core::alignment;
|
||||
use crate::core::event::{self, Event};
|
||||
use crate::core::layout::{self, Layout};
|
||||
use crate::core::mouse;
|
||||
use crate::core::overlay;
|
||||
use crate::core::renderer;
|
||||
use crate::core::text::{self, Text};
|
||||
use crate::core::touch;
|
||||
use crate::core::widget::Tree;
|
||||
use crate::core::{
|
||||
Clipboard, Color, Length, Padding, Pixels, Point, Rectangle, Size, Vector,
|
||||
};
|
||||
use crate::core::{Element, Shell, Widget};
|
||||
use crate::scrollable::{self, Scrollable};
|
||||
|
||||
pub use iced_style::menu::{Appearance, StyleSheet};
|
||||
|
||||
/// A list of selectable options.
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Menu<'a, T, Renderer = crate::Renderer>
|
||||
where
|
||||
Renderer: text::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
state: &'a mut State,
|
||||
options: &'a [T],
|
||||
hovered_option: &'a mut Option<usize>,
|
||||
last_selection: &'a mut Option<T>,
|
||||
width: f32,
|
||||
padding: Padding,
|
||||
text_size: Option<f32>,
|
||||
font: Option<Renderer::Font>,
|
||||
style: <Renderer::Theme as StyleSheet>::Style,
|
||||
}
|
||||
|
||||
impl<'a, T, Renderer> Menu<'a, T, Renderer>
|
||||
where
|
||||
T: ToString + Clone,
|
||||
Renderer: text::Renderer + 'a,
|
||||
Renderer::Theme:
|
||||
StyleSheet + container::StyleSheet + scrollable::StyleSheet,
|
||||
{
|
||||
/// Creates a new [`Menu`] with the given [`State`], a list of options, and
|
||||
/// the message to produced when an option is selected.
|
||||
pub fn new(
|
||||
state: &'a mut State,
|
||||
options: &'a [T],
|
||||
hovered_option: &'a mut Option<usize>,
|
||||
last_selection: &'a mut Option<T>,
|
||||
) -> Self {
|
||||
Menu {
|
||||
state,
|
||||
options,
|
||||
hovered_option,
|
||||
last_selection,
|
||||
width: 0.0,
|
||||
padding: Padding::ZERO,
|
||||
text_size: None,
|
||||
font: None,
|
||||
style: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the width of the [`Menu`].
|
||||
pub fn width(mut self, width: f32) -> Self {
|
||||
self.width = width;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`Padding`] of the [`Menu`].
|
||||
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
|
||||
self.padding = padding.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the text size of the [`Menu`].
|
||||
pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self {
|
||||
self.text_size = Some(text_size.into().0);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the font of the [`Menu`].
|
||||
pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
|
||||
self.font = Some(font.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the [`Menu`].
|
||||
pub fn style(
|
||||
mut self,
|
||||
style: impl Into<<Renderer::Theme as StyleSheet>::Style>,
|
||||
) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Turns the [`Menu`] into an overlay [`Element`] at the given target
|
||||
/// position.
|
||||
///
|
||||
/// The `target_height` will be used to display the menu either on top
|
||||
/// of the target or under it, depending on the screen position and the
|
||||
/// dimensions of the [`Menu`].
|
||||
pub fn overlay<Message: 'a>(
|
||||
self,
|
||||
position: Point,
|
||||
target_height: f32,
|
||||
) -> overlay::Element<'a, Message, Renderer> {
|
||||
overlay::Element::new(
|
||||
position,
|
||||
Box::new(Overlay::new(self, target_height)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// The local state of a [`Menu`].
|
||||
#[derive(Debug)]
|
||||
pub struct State {
|
||||
tree: Tree,
|
||||
}
|
||||
|
||||
impl State {
|
||||
/// Creates a new [`State`] for a [`Menu`].
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
tree: Tree::empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for State {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
struct Overlay<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: crate::core::Renderer,
|
||||
Renderer::Theme: StyleSheet + container::StyleSheet,
|
||||
{
|
||||
state: &'a mut Tree,
|
||||
container: Container<'a, Message, Renderer>,
|
||||
width: f32,
|
||||
target_height: f32,
|
||||
style: <Renderer::Theme as StyleSheet>::Style,
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Overlay<'a, Message, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Renderer: 'a,
|
||||
Renderer: text::Renderer,
|
||||
Renderer::Theme:
|
||||
StyleSheet + container::StyleSheet + scrollable::StyleSheet,
|
||||
{
|
||||
pub fn new<T>(menu: Menu<'a, T, Renderer>, target_height: f32) -> Self
|
||||
where
|
||||
T: Clone + ToString,
|
||||
{
|
||||
let Menu {
|
||||
state,
|
||||
options,
|
||||
hovered_option,
|
||||
last_selection,
|
||||
width,
|
||||
padding,
|
||||
font,
|
||||
text_size,
|
||||
style,
|
||||
} = menu;
|
||||
|
||||
let container = Container::new(Scrollable::new(List {
|
||||
options,
|
||||
hovered_option,
|
||||
last_selection,
|
||||
font,
|
||||
text_size,
|
||||
padding,
|
||||
style: style.clone(),
|
||||
}));
|
||||
|
||||
state.tree.diff(&container as &dyn Widget<_, _>);
|
||||
|
||||
Self {
|
||||
state: &mut state.tree,
|
||||
container,
|
||||
width,
|
||||
target_height,
|
||||
style,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> crate::core::Overlay<Message, Renderer>
|
||||
for Overlay<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: text::Renderer,
|
||||
Renderer::Theme: StyleSheet + container::StyleSheet,
|
||||
{
|
||||
fn layout(
|
||||
&self,
|
||||
renderer: &Renderer,
|
||||
bounds: Size,
|
||||
position: Point,
|
||||
) -> layout::Node {
|
||||
let space_below = bounds.height - (position.y + self.target_height);
|
||||
let space_above = position.y;
|
||||
|
||||
let limits = layout::Limits::new(
|
||||
Size::ZERO,
|
||||
Size::new(
|
||||
bounds.width - position.x,
|
||||
if space_below > space_above {
|
||||
space_below
|
||||
} else {
|
||||
space_above
|
||||
},
|
||||
),
|
||||
)
|
||||
.width(self.width);
|
||||
|
||||
let mut node = self.container.layout(renderer, &limits);
|
||||
|
||||
node.move_to(if space_below > space_above {
|
||||
position + Vector::new(0.0, self.target_height)
|
||||
} else {
|
||||
position - Vector::new(0.0, node.size().height)
|
||||
});
|
||||
|
||||
node
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
self.container.on_event(
|
||||
self.state,
|
||||
event,
|
||||
layout,
|
||||
cursor_position,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
)
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
self.container.mouse_interaction(
|
||||
self.state,
|
||||
layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
renderer,
|
||||
)
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
) {
|
||||
let appearance = theme.appearance(&self.style);
|
||||
let bounds = layout.bounds();
|
||||
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds,
|
||||
border_color: appearance.border_color,
|
||||
border_width: appearance.border_width,
|
||||
border_radius: appearance.border_radius.into(),
|
||||
},
|
||||
appearance.background,
|
||||
);
|
||||
|
||||
self.container.draw(
|
||||
self.state,
|
||||
renderer,
|
||||
theme,
|
||||
style,
|
||||
layout,
|
||||
cursor_position,
|
||||
&bounds,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
struct List<'a, T, Renderer>
|
||||
where
|
||||
Renderer: text::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
options: &'a [T],
|
||||
hovered_option: &'a mut Option<usize>,
|
||||
last_selection: &'a mut Option<T>,
|
||||
padding: Padding,
|
||||
text_size: Option<f32>,
|
||||
font: Option<Renderer::Font>,
|
||||
style: <Renderer::Theme as StyleSheet>::Style,
|
||||
}
|
||||
|
||||
impl<'a, T, Message, Renderer> Widget<Message, Renderer>
|
||||
for List<'a, T, Renderer>
|
||||
where
|
||||
T: Clone + ToString,
|
||||
Renderer: text::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
fn width(&self) -> Length {
|
||||
Length::Fill
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
Length::Shrink
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
use std::f32;
|
||||
|
||||
let limits = limits.width(Length::Fill).height(Length::Shrink);
|
||||
let text_size =
|
||||
self.text_size.unwrap_or_else(|| renderer.default_size());
|
||||
|
||||
let size = {
|
||||
let intrinsic = Size::new(
|
||||
0.0,
|
||||
(text_size * 1.2 + self.padding.vertical())
|
||||
* self.options.len() as f32,
|
||||
);
|
||||
|
||||
limits.resolve(intrinsic)
|
||||
};
|
||||
|
||||
layout::Node::new(size)
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
_state: &mut Tree,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
renderer: &Renderer,
|
||||
_clipboard: &mut dyn Clipboard,
|
||||
_shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
match event {
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
|
||||
let bounds = layout.bounds();
|
||||
|
||||
if bounds.contains(cursor_position) {
|
||||
if let Some(index) = *self.hovered_option {
|
||||
if let Some(option) = self.options.get(index) {
|
||||
*self.last_selection = Some(option.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse::Event::CursorMoved { .. }) => {
|
||||
let bounds = layout.bounds();
|
||||
|
||||
if bounds.contains(cursor_position) {
|
||||
let text_size = self
|
||||
.text_size
|
||||
.unwrap_or_else(|| renderer.default_size());
|
||||
|
||||
*self.hovered_option = Some(
|
||||
((cursor_position.y - bounds.y)
|
||||
/ (text_size * 1.2 + self.padding.vertical()))
|
||||
as usize,
|
||||
);
|
||||
}
|
||||
}
|
||||
Event::Touch(touch::Event::FingerPressed { .. }) => {
|
||||
let bounds = layout.bounds();
|
||||
|
||||
if bounds.contains(cursor_position) {
|
||||
let text_size = self
|
||||
.text_size
|
||||
.unwrap_or_else(|| renderer.default_size());
|
||||
|
||||
*self.hovered_option = Some(
|
||||
((cursor_position.y - bounds.y)
|
||||
/ (text_size * 1.2 + self.padding.vertical()))
|
||||
as usize,
|
||||
);
|
||||
|
||||
if let Some(index) = *self.hovered_option {
|
||||
if let Some(option) = self.options.get(index) {
|
||||
*self.last_selection = Some(option.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
event::Status::Ignored
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
_state: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
_renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
let is_mouse_over = layout.bounds().contains(cursor_position);
|
||||
|
||||
if is_mouse_over {
|
||||
mouse::Interaction::Pointer
|
||||
} else {
|
||||
mouse::Interaction::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
_state: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
_style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
_cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
let appearance = theme.appearance(&self.style);
|
||||
let bounds = layout.bounds();
|
||||
|
||||
let text_size =
|
||||
self.text_size.unwrap_or_else(|| renderer.default_size());
|
||||
let option_height =
|
||||
(text_size * 1.2 + self.padding.vertical()) as usize;
|
||||
|
||||
let offset = viewport.y - bounds.y;
|
||||
let start = (offset / option_height as f32) as usize;
|
||||
let end =
|
||||
((offset + viewport.height) / option_height as f32).ceil() as usize;
|
||||
|
||||
let visible_options = &self.options[start..end.min(self.options.len())];
|
||||
|
||||
for (i, option) in visible_options.iter().enumerate() {
|
||||
let i = start + i;
|
||||
let is_selected = *self.hovered_option == Some(i);
|
||||
|
||||
let bounds = Rectangle {
|
||||
x: bounds.x,
|
||||
y: bounds.y + (option_height * i) as f32,
|
||||
width: bounds.width,
|
||||
height: text_size * 1.2 + self.padding.vertical(),
|
||||
};
|
||||
|
||||
if is_selected {
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds,
|
||||
border_color: Color::TRANSPARENT,
|
||||
border_width: 0.0,
|
||||
border_radius: appearance.border_radius.into(),
|
||||
},
|
||||
appearance.selected_background,
|
||||
);
|
||||
}
|
||||
|
||||
renderer.fill_text(Text {
|
||||
content: &option.to_string(),
|
||||
bounds: Rectangle {
|
||||
x: bounds.x + self.padding.left,
|
||||
y: bounds.center_y(),
|
||||
width: f32::INFINITY,
|
||||
..bounds
|
||||
},
|
||||
size: text_size,
|
||||
font: self.font.unwrap_or_else(|| renderer.default_font()),
|
||||
color: if is_selected {
|
||||
appearance.selected_text_color
|
||||
} else {
|
||||
appearance.text_color
|
||||
},
|
||||
horizontal_alignment: alignment::Horizontal::Left,
|
||||
vertical_alignment: alignment::Vertical::Center,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T, Message, Renderer> From<List<'a, T, Renderer>>
|
||||
for Element<'a, Message, Renderer>
|
||||
where
|
||||
T: ToString + Clone,
|
||||
Message: 'a,
|
||||
Renderer: 'a + text::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
fn from(list: List<'a, T, Renderer>) -> Self {
|
||||
Element::new(list)
|
||||
}
|
||||
}
|
||||
991
widget/src/pane_grid.rs
Normal file
991
widget/src/pane_grid.rs
Normal file
|
|
@ -0,0 +1,991 @@
|
|||
//! Let your users split regions of your application and organize layout dynamically.
|
||||
//!
|
||||
//! [](https://gfycat.com/mixedflatjellyfish)
|
||||
//!
|
||||
//! # Example
|
||||
//! The [`pane_grid` example] showcases how to use a [`PaneGrid`] with resizing,
|
||||
//! drag and drop, and hotkey support.
|
||||
//!
|
||||
//! [`pane_grid` example]: https://github.com/iced-rs/iced/tree/0.8/examples/pane_grid
|
||||
mod axis;
|
||||
mod configuration;
|
||||
mod content;
|
||||
mod direction;
|
||||
mod draggable;
|
||||
mod node;
|
||||
mod pane;
|
||||
mod split;
|
||||
mod title_bar;
|
||||
|
||||
pub mod state;
|
||||
|
||||
pub use axis::Axis;
|
||||
pub use configuration::Configuration;
|
||||
pub use content::Content;
|
||||
pub use direction::Direction;
|
||||
pub use draggable::Draggable;
|
||||
pub use node::Node;
|
||||
pub use pane::Pane;
|
||||
pub use split::Split;
|
||||
pub use state::State;
|
||||
pub use title_bar::TitleBar;
|
||||
|
||||
pub use crate::style::pane_grid::{Line, StyleSheet};
|
||||
|
||||
use crate::container;
|
||||
use crate::core::event::{self, Event};
|
||||
use crate::core::layout;
|
||||
use crate::core::mouse;
|
||||
use crate::core::overlay::{self, Group};
|
||||
use crate::core::renderer;
|
||||
use crate::core::touch;
|
||||
use crate::core::widget;
|
||||
use crate::core::widget::tree::{self, Tree};
|
||||
use crate::core::{
|
||||
Clipboard, Color, Element, Layout, Length, Pixels, Point, Rectangle, Shell,
|
||||
Size, Vector, Widget,
|
||||
};
|
||||
|
||||
/// A collection of panes distributed using either vertical or horizontal splits
|
||||
/// to completely fill the space available.
|
||||
///
|
||||
/// [](https://gfycat.com/frailfreshairedaleterrier)
|
||||
///
|
||||
/// This distribution of space is common in tiling window managers (like
|
||||
/// [`awesome`](https://awesomewm.org/), [`i3`](https://i3wm.org/), or even
|
||||
/// [`tmux`](https://github.com/tmux/tmux)).
|
||||
///
|
||||
/// A [`PaneGrid`] supports:
|
||||
///
|
||||
/// * Vertical and horizontal splits
|
||||
/// * Tracking of the last active pane
|
||||
/// * Mouse-based resizing
|
||||
/// * Drag and drop to reorganize panes
|
||||
/// * Hotkey support
|
||||
/// * Configurable modifier keys
|
||||
/// * [`State`] API to perform actions programmatically (`split`, `swap`, `resize`, etc.)
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```
|
||||
/// # use iced_widget::{pane_grid, text};
|
||||
/// #
|
||||
/// # type PaneGrid<'a, Message> =
|
||||
/// # iced_widget::PaneGrid<'a, Message, iced_widget::renderer::Renderer<iced_widget::style::Theme>>;
|
||||
/// #
|
||||
/// enum PaneState {
|
||||
/// SomePane,
|
||||
/// AnotherKindOfPane,
|
||||
/// }
|
||||
///
|
||||
/// enum Message {
|
||||
/// PaneDragged(pane_grid::DragEvent),
|
||||
/// PaneResized(pane_grid::ResizeEvent),
|
||||
/// }
|
||||
///
|
||||
/// let (mut state, _) = pane_grid::State::new(PaneState::SomePane);
|
||||
///
|
||||
/// let pane_grid =
|
||||
/// PaneGrid::new(&state, |pane, state, is_maximized| {
|
||||
/// pane_grid::Content::new(match state {
|
||||
/// PaneState::SomePane => text("This is some pane"),
|
||||
/// PaneState::AnotherKindOfPane => text("This is another kind of pane"),
|
||||
/// })
|
||||
/// })
|
||||
/// .on_drag(Message::PaneDragged)
|
||||
/// .on_resize(10, Message::PaneResized);
|
||||
/// ```
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct PaneGrid<'a, Message, Renderer = crate::Renderer>
|
||||
where
|
||||
Renderer: crate::core::Renderer,
|
||||
Renderer::Theme: StyleSheet + container::StyleSheet,
|
||||
{
|
||||
contents: Contents<'a, Content<'a, Message, Renderer>>,
|
||||
width: Length,
|
||||
height: Length,
|
||||
spacing: f32,
|
||||
on_click: Option<Box<dyn Fn(Pane) -> Message + 'a>>,
|
||||
on_drag: Option<Box<dyn Fn(DragEvent) -> Message + 'a>>,
|
||||
on_resize: Option<(f32, Box<dyn Fn(ResizeEvent) -> Message + 'a>)>,
|
||||
style: <Renderer::Theme as StyleSheet>::Style,
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> PaneGrid<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: crate::core::Renderer,
|
||||
Renderer::Theme: StyleSheet + container::StyleSheet,
|
||||
{
|
||||
/// Creates a [`PaneGrid`] with the given [`State`] and view function.
|
||||
///
|
||||
/// The view function will be called to display each [`Pane`] present in the
|
||||
/// [`State`]. [`bool`] is set if the pane is maximized.
|
||||
pub fn new<T>(
|
||||
state: &'a State<T>,
|
||||
view: impl Fn(Pane, &'a T, bool) -> Content<'a, Message, Renderer>,
|
||||
) -> Self {
|
||||
let contents = if let Some((pane, pane_state)) =
|
||||
state.maximized.and_then(|pane| {
|
||||
state.panes.get(&pane).map(|pane_state| (pane, pane_state))
|
||||
}) {
|
||||
Contents::Maximized(
|
||||
pane,
|
||||
view(pane, pane_state, true),
|
||||
Node::Pane(pane),
|
||||
)
|
||||
} else {
|
||||
Contents::All(
|
||||
state
|
||||
.panes
|
||||
.iter()
|
||||
.map(|(pane, pane_state)| {
|
||||
(*pane, view(*pane, pane_state, false))
|
||||
})
|
||||
.collect(),
|
||||
&state.internal,
|
||||
)
|
||||
};
|
||||
|
||||
Self {
|
||||
contents,
|
||||
width: Length::Fill,
|
||||
height: Length::Fill,
|
||||
spacing: 0.0,
|
||||
on_click: None,
|
||||
on_drag: None,
|
||||
on_resize: None,
|
||||
style: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the width of the [`PaneGrid`].
|
||||
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||
self.width = width.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the height of the [`PaneGrid`].
|
||||
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
||||
self.height = height.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the spacing _between_ the panes of the [`PaneGrid`].
|
||||
pub fn spacing(mut self, amount: impl Into<Pixels>) -> Self {
|
||||
self.spacing = amount.into().0;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the message that will be produced when a [`Pane`] of the
|
||||
/// [`PaneGrid`] is clicked.
|
||||
pub fn on_click<F>(mut self, f: F) -> Self
|
||||
where
|
||||
F: 'a + Fn(Pane) -> Message,
|
||||
{
|
||||
self.on_click = Some(Box::new(f));
|
||||
self
|
||||
}
|
||||
|
||||
/// Enables the drag and drop interactions of the [`PaneGrid`], which will
|
||||
/// use the provided function to produce messages.
|
||||
pub fn on_drag<F>(mut self, f: F) -> Self
|
||||
where
|
||||
F: 'a + Fn(DragEvent) -> Message,
|
||||
{
|
||||
self.on_drag = Some(Box::new(f));
|
||||
self
|
||||
}
|
||||
|
||||
/// Enables the resize interactions of the [`PaneGrid`], which will
|
||||
/// use the provided function to produce messages.
|
||||
///
|
||||
/// The `leeway` describes the amount of space around a split that can be
|
||||
/// used to grab it.
|
||||
///
|
||||
/// The grabbable area of a split will have a length of `spacing + leeway`,
|
||||
/// properly centered. In other words, a length of
|
||||
/// `(spacing + leeway) / 2.0` on either side of the split line.
|
||||
pub fn on_resize<F>(mut self, leeway: impl Into<Pixels>, f: F) -> Self
|
||||
where
|
||||
F: 'a + Fn(ResizeEvent) -> Message,
|
||||
{
|
||||
self.on_resize = Some((leeway.into().0, Box::new(f)));
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the [`PaneGrid`].
|
||||
pub fn style(
|
||||
mut self,
|
||||
style: impl Into<<Renderer::Theme as StyleSheet>::Style>,
|
||||
) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
fn drag_enabled(&self) -> bool {
|
||||
(!self.contents.is_maximized())
|
||||
.then(|| self.on_drag.is_some())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Widget<Message, Renderer>
|
||||
for PaneGrid<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: crate::core::Renderer,
|
||||
Renderer::Theme: StyleSheet + container::StyleSheet,
|
||||
{
|
||||
fn tag(&self) -> tree::Tag {
|
||||
tree::Tag::of::<state::Action>()
|
||||
}
|
||||
|
||||
fn state(&self) -> tree::State {
|
||||
tree::State::new(state::Action::Idle)
|
||||
}
|
||||
|
||||
fn children(&self) -> Vec<Tree> {
|
||||
self.contents
|
||||
.iter()
|
||||
.map(|(_, content)| content.state())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn diff(&self, tree: &mut Tree) {
|
||||
match &self.contents {
|
||||
Contents::All(contents, _) => tree.diff_children_custom(
|
||||
contents,
|
||||
|state, (_, content)| content.diff(state),
|
||||
|(_, content)| content.state(),
|
||||
),
|
||||
Contents::Maximized(_, content, _) => tree.diff_children_custom(
|
||||
&[content],
|
||||
|state, content| content.diff(state),
|
||||
|content| content.state(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn width(&self) -> Length {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
layout(
|
||||
renderer,
|
||||
limits,
|
||||
self.contents.layout(),
|
||||
self.width,
|
||||
self.height,
|
||||
self.spacing,
|
||||
self.contents.iter(),
|
||||
|content, renderer, limits| content.layout(renderer, limits),
|
||||
)
|
||||
}
|
||||
|
||||
fn operate(
|
||||
&self,
|
||||
tree: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn widget::Operation<Message>,
|
||||
) {
|
||||
operation.container(None, &mut |operation| {
|
||||
self.contents
|
||||
.iter()
|
||||
.zip(&mut tree.children)
|
||||
.zip(layout.children())
|
||||
.for_each(|(((_pane, content), state), layout)| {
|
||||
content.operate(state, layout, renderer, operation);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
let action = tree.state.downcast_mut::<state::Action>();
|
||||
|
||||
let on_drag = if self.drag_enabled() {
|
||||
&self.on_drag
|
||||
} else {
|
||||
&None
|
||||
};
|
||||
|
||||
let event_status = update(
|
||||
action,
|
||||
self.contents.layout(),
|
||||
&event,
|
||||
layout,
|
||||
cursor_position,
|
||||
shell,
|
||||
self.spacing,
|
||||
self.contents.iter(),
|
||||
&self.on_click,
|
||||
on_drag,
|
||||
&self.on_resize,
|
||||
);
|
||||
|
||||
let picked_pane = action.picked_pane().map(|(pane, _)| pane);
|
||||
|
||||
self.contents
|
||||
.iter_mut()
|
||||
.zip(&mut tree.children)
|
||||
.zip(layout.children())
|
||||
.map(|(((pane, content), tree), layout)| {
|
||||
let is_picked = picked_pane == Some(pane);
|
||||
|
||||
content.on_event(
|
||||
tree,
|
||||
event.clone(),
|
||||
layout,
|
||||
cursor_position,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
is_picked,
|
||||
)
|
||||
})
|
||||
.fold(event_status, event::Status::merge)
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
mouse_interaction(
|
||||
tree.state.downcast_ref(),
|
||||
self.contents.layout(),
|
||||
layout,
|
||||
cursor_position,
|
||||
self.spacing,
|
||||
self.on_resize.as_ref().map(|(leeway, _)| *leeway),
|
||||
)
|
||||
.unwrap_or_else(|| {
|
||||
self.contents
|
||||
.iter()
|
||||
.zip(&tree.children)
|
||||
.zip(layout.children())
|
||||
.map(|(((_pane, content), tree), layout)| {
|
||||
content.mouse_interaction(
|
||||
tree,
|
||||
layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
renderer,
|
||||
self.drag_enabled(),
|
||||
)
|
||||
})
|
||||
.max()
|
||||
.unwrap_or_default()
|
||||
})
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
draw(
|
||||
tree.state.downcast_ref(),
|
||||
self.contents.layout(),
|
||||
layout,
|
||||
cursor_position,
|
||||
renderer,
|
||||
theme,
|
||||
style,
|
||||
viewport,
|
||||
self.spacing,
|
||||
self.on_resize.as_ref().map(|(leeway, _)| *leeway),
|
||||
&self.style,
|
||||
self.contents
|
||||
.iter()
|
||||
.zip(&tree.children)
|
||||
.map(|((pane, content), tree)| (pane, (content, tree))),
|
||||
|(content, tree),
|
||||
renderer,
|
||||
style,
|
||||
layout,
|
||||
cursor_position,
|
||||
rectangle| {
|
||||
content.draw(
|
||||
tree,
|
||||
renderer,
|
||||
theme,
|
||||
style,
|
||||
layout,
|
||||
cursor_position,
|
||||
rectangle,
|
||||
);
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn overlay<'b>(
|
||||
&'b mut self,
|
||||
tree: &'b mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
) -> Option<overlay::Element<'_, Message, Renderer>> {
|
||||
let children = self
|
||||
.contents
|
||||
.iter_mut()
|
||||
.zip(&mut tree.children)
|
||||
.zip(layout.children())
|
||||
.filter_map(|(((_, content), state), layout)| {
|
||||
content.overlay(state, layout, renderer)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
(!children.is_empty()).then(|| Group::with_children(children).overlay())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> From<PaneGrid<'a, Message, Renderer>>
|
||||
for Element<'a, Message, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Renderer: 'a + crate::core::Renderer,
|
||||
Renderer::Theme: StyleSheet + container::StyleSheet,
|
||||
{
|
||||
fn from(
|
||||
pane_grid: PaneGrid<'a, Message, Renderer>,
|
||||
) -> Element<'a, Message, Renderer> {
|
||||
Element::new(pane_grid)
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculates the [`Layout`] of a [`PaneGrid`].
|
||||
pub fn layout<Renderer, T>(
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
node: &Node,
|
||||
width: Length,
|
||||
height: Length,
|
||||
spacing: f32,
|
||||
contents: impl Iterator<Item = (Pane, T)>,
|
||||
layout_content: impl Fn(T, &Renderer, &layout::Limits) -> layout::Node,
|
||||
) -> layout::Node {
|
||||
let limits = limits.width(width).height(height);
|
||||
let size = limits.resolve(Size::ZERO);
|
||||
|
||||
let regions = node.pane_regions(spacing, size);
|
||||
let children = contents
|
||||
.filter_map(|(pane, content)| {
|
||||
let region = regions.get(&pane)?;
|
||||
let size = Size::new(region.width, region.height);
|
||||
|
||||
let mut node = layout_content(
|
||||
content,
|
||||
renderer,
|
||||
&layout::Limits::new(size, size),
|
||||
);
|
||||
|
||||
node.move_to(Point::new(region.x, region.y));
|
||||
|
||||
Some(node)
|
||||
})
|
||||
.collect();
|
||||
|
||||
layout::Node::with_children(size, children)
|
||||
}
|
||||
|
||||
/// Processes an [`Event`] and updates the [`state`] of a [`PaneGrid`]
|
||||
/// accordingly.
|
||||
pub fn update<'a, Message, T: Draggable>(
|
||||
action: &mut state::Action,
|
||||
node: &Node,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
spacing: f32,
|
||||
contents: impl Iterator<Item = (Pane, T)>,
|
||||
on_click: &Option<Box<dyn Fn(Pane) -> Message + 'a>>,
|
||||
on_drag: &Option<Box<dyn Fn(DragEvent) -> Message + 'a>>,
|
||||
on_resize: &Option<(f32, Box<dyn Fn(ResizeEvent) -> Message + 'a>)>,
|
||||
) -> event::Status {
|
||||
let mut event_status = event::Status::Ignored;
|
||||
|
||||
match event {
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
|
||||
| Event::Touch(touch::Event::FingerPressed { .. }) => {
|
||||
let bounds = layout.bounds();
|
||||
|
||||
if bounds.contains(cursor_position) {
|
||||
event_status = event::Status::Captured;
|
||||
|
||||
match on_resize {
|
||||
Some((leeway, _)) => {
|
||||
let relative_cursor = Point::new(
|
||||
cursor_position.x - bounds.x,
|
||||
cursor_position.y - bounds.y,
|
||||
);
|
||||
|
||||
let splits = node.split_regions(
|
||||
spacing,
|
||||
Size::new(bounds.width, bounds.height),
|
||||
);
|
||||
|
||||
let clicked_split = hovered_split(
|
||||
splits.iter(),
|
||||
spacing + leeway,
|
||||
relative_cursor,
|
||||
);
|
||||
|
||||
if let Some((split, axis, _)) = clicked_split {
|
||||
if action.picked_pane().is_none() {
|
||||
*action =
|
||||
state::Action::Resizing { split, axis };
|
||||
}
|
||||
} else {
|
||||
click_pane(
|
||||
action,
|
||||
layout,
|
||||
cursor_position,
|
||||
shell,
|
||||
contents,
|
||||
on_click,
|
||||
on_drag,
|
||||
);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
click_pane(
|
||||
action,
|
||||
layout,
|
||||
cursor_position,
|
||||
shell,
|
||||
contents,
|
||||
on_click,
|
||||
on_drag,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
|
||||
| Event::Touch(touch::Event::FingerLifted { .. })
|
||||
| Event::Touch(touch::Event::FingerLost { .. }) => {
|
||||
if let Some((pane, _)) = action.picked_pane() {
|
||||
if let Some(on_drag) = on_drag {
|
||||
let mut dropped_region = contents
|
||||
.zip(layout.children())
|
||||
.filter(|(_, layout)| {
|
||||
layout.bounds().contains(cursor_position)
|
||||
});
|
||||
|
||||
let event = match dropped_region.next() {
|
||||
Some(((target, _), _)) if pane != target => {
|
||||
DragEvent::Dropped { pane, target }
|
||||
}
|
||||
_ => DragEvent::Canceled { pane },
|
||||
};
|
||||
|
||||
shell.publish(on_drag(event));
|
||||
}
|
||||
|
||||
*action = state::Action::Idle;
|
||||
|
||||
event_status = event::Status::Captured;
|
||||
} else if action.picked_split().is_some() {
|
||||
*action = state::Action::Idle;
|
||||
|
||||
event_status = event::Status::Captured;
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse::Event::CursorMoved { .. })
|
||||
| Event::Touch(touch::Event::FingerMoved { .. }) => {
|
||||
if let Some((_, on_resize)) = on_resize {
|
||||
if let Some((split, _)) = action.picked_split() {
|
||||
let bounds = layout.bounds();
|
||||
|
||||
let splits = node.split_regions(
|
||||
spacing,
|
||||
Size::new(bounds.width, bounds.height),
|
||||
);
|
||||
|
||||
if let Some((axis, rectangle, _)) = splits.get(&split) {
|
||||
let ratio = match axis {
|
||||
Axis::Horizontal => {
|
||||
let position =
|
||||
cursor_position.y - bounds.y - rectangle.y;
|
||||
|
||||
(position / rectangle.height).clamp(0.1, 0.9)
|
||||
}
|
||||
Axis::Vertical => {
|
||||
let position =
|
||||
cursor_position.x - bounds.x - rectangle.x;
|
||||
|
||||
(position / rectangle.width).clamp(0.1, 0.9)
|
||||
}
|
||||
};
|
||||
|
||||
shell.publish(on_resize(ResizeEvent { split, ratio }));
|
||||
|
||||
event_status = event::Status::Captured;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
event_status
|
||||
}
|
||||
|
||||
fn click_pane<'a, Message, T>(
|
||||
action: &mut state::Action,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
contents: impl Iterator<Item = (Pane, T)>,
|
||||
on_click: &Option<Box<dyn Fn(Pane) -> Message + 'a>>,
|
||||
on_drag: &Option<Box<dyn Fn(DragEvent) -> Message + 'a>>,
|
||||
) where
|
||||
T: Draggable,
|
||||
{
|
||||
let mut clicked_region = contents
|
||||
.zip(layout.children())
|
||||
.filter(|(_, layout)| layout.bounds().contains(cursor_position));
|
||||
|
||||
if let Some(((pane, content), layout)) = clicked_region.next() {
|
||||
if let Some(on_click) = &on_click {
|
||||
shell.publish(on_click(pane));
|
||||
}
|
||||
|
||||
if let Some(on_drag) = &on_drag {
|
||||
if content.can_be_dragged_at(layout, cursor_position) {
|
||||
let pane_position = layout.position();
|
||||
|
||||
let origin = cursor_position
|
||||
- Vector::new(pane_position.x, pane_position.y);
|
||||
|
||||
*action = state::Action::Dragging { pane, origin };
|
||||
|
||||
shell.publish(on_drag(DragEvent::Picked { pane }));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the current [`mouse::Interaction`] of a [`PaneGrid`].
|
||||
pub fn mouse_interaction(
|
||||
action: &state::Action,
|
||||
node: &Node,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
spacing: f32,
|
||||
resize_leeway: Option<f32>,
|
||||
) -> Option<mouse::Interaction> {
|
||||
if action.picked_pane().is_some() {
|
||||
return Some(mouse::Interaction::Grabbing);
|
||||
}
|
||||
|
||||
let resize_axis =
|
||||
action.picked_split().map(|(_, axis)| axis).or_else(|| {
|
||||
resize_leeway.and_then(|leeway| {
|
||||
let bounds = layout.bounds();
|
||||
|
||||
let splits = node.split_regions(spacing, bounds.size());
|
||||
|
||||
let relative_cursor = Point::new(
|
||||
cursor_position.x - bounds.x,
|
||||
cursor_position.y - bounds.y,
|
||||
);
|
||||
|
||||
hovered_split(splits.iter(), spacing + leeway, relative_cursor)
|
||||
.map(|(_, axis, _)| axis)
|
||||
})
|
||||
});
|
||||
|
||||
if let Some(resize_axis) = resize_axis {
|
||||
return Some(match resize_axis {
|
||||
Axis::Horizontal => mouse::Interaction::ResizingVertically,
|
||||
Axis::Vertical => mouse::Interaction::ResizingHorizontally,
|
||||
});
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Draws a [`PaneGrid`].
|
||||
pub fn draw<Renderer, T>(
|
||||
action: &state::Action,
|
||||
node: &Node,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
default_style: &renderer::Style,
|
||||
viewport: &Rectangle,
|
||||
spacing: f32,
|
||||
resize_leeway: Option<f32>,
|
||||
style: &<Renderer::Theme as StyleSheet>::Style,
|
||||
contents: impl Iterator<Item = (Pane, T)>,
|
||||
draw_pane: impl Fn(
|
||||
T,
|
||||
&mut Renderer,
|
||||
&renderer::Style,
|
||||
Layout<'_>,
|
||||
Point,
|
||||
&Rectangle,
|
||||
),
|
||||
) where
|
||||
Renderer: crate::core::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
let picked_pane = action.picked_pane();
|
||||
|
||||
let picked_split = action
|
||||
.picked_split()
|
||||
.and_then(|(split, axis)| {
|
||||
let bounds = layout.bounds();
|
||||
|
||||
let splits = node.split_regions(spacing, bounds.size());
|
||||
|
||||
let (_axis, region, ratio) = splits.get(&split)?;
|
||||
|
||||
let region = axis.split_line_bounds(*region, *ratio, spacing);
|
||||
|
||||
Some((axis, region + Vector::new(bounds.x, bounds.y), true))
|
||||
})
|
||||
.or_else(|| match resize_leeway {
|
||||
Some(leeway) => {
|
||||
let bounds = layout.bounds();
|
||||
|
||||
let relative_cursor = Point::new(
|
||||
cursor_position.x - bounds.x,
|
||||
cursor_position.y - bounds.y,
|
||||
);
|
||||
|
||||
let splits = node.split_regions(spacing, bounds.size());
|
||||
|
||||
let (_split, axis, region) = hovered_split(
|
||||
splits.iter(),
|
||||
spacing + leeway,
|
||||
relative_cursor,
|
||||
)?;
|
||||
|
||||
Some((axis, region + Vector::new(bounds.x, bounds.y), false))
|
||||
}
|
||||
None => None,
|
||||
});
|
||||
|
||||
let pane_cursor_position = if picked_pane.is_some() {
|
||||
// TODO: Remove once cursor availability is encoded in the type
|
||||
// system
|
||||
Point::new(-1.0, -1.0)
|
||||
} else {
|
||||
cursor_position
|
||||
};
|
||||
|
||||
let mut render_picked_pane = None;
|
||||
|
||||
for ((id, pane), layout) in contents.zip(layout.children()) {
|
||||
match picked_pane {
|
||||
Some((dragging, origin)) if id == dragging => {
|
||||
render_picked_pane = Some((pane, origin, layout));
|
||||
}
|
||||
_ => {
|
||||
draw_pane(
|
||||
pane,
|
||||
renderer,
|
||||
default_style,
|
||||
layout,
|
||||
pane_cursor_position,
|
||||
viewport,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Render picked pane last
|
||||
if let Some((pane, origin, layout)) = render_picked_pane {
|
||||
let bounds = layout.bounds();
|
||||
|
||||
renderer.with_translation(
|
||||
cursor_position
|
||||
- Point::new(bounds.x + origin.x, bounds.y + origin.y),
|
||||
|renderer| {
|
||||
renderer.with_layer(bounds, |renderer| {
|
||||
draw_pane(
|
||||
pane,
|
||||
renderer,
|
||||
default_style,
|
||||
layout,
|
||||
pane_cursor_position,
|
||||
viewport,
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
if let Some((axis, split_region, is_picked)) = picked_split {
|
||||
let highlight = if is_picked {
|
||||
theme.picked_split(style)
|
||||
} else {
|
||||
theme.hovered_split(style)
|
||||
};
|
||||
|
||||
if let Some(highlight) = highlight {
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: match axis {
|
||||
Axis::Horizontal => Rectangle {
|
||||
x: split_region.x,
|
||||
y: (split_region.y
|
||||
+ (split_region.height - highlight.width)
|
||||
/ 2.0)
|
||||
.round(),
|
||||
width: split_region.width,
|
||||
height: highlight.width,
|
||||
},
|
||||
Axis::Vertical => Rectangle {
|
||||
x: (split_region.x
|
||||
+ (split_region.width - highlight.width) / 2.0)
|
||||
.round(),
|
||||
y: split_region.y,
|
||||
width: highlight.width,
|
||||
height: split_region.height,
|
||||
},
|
||||
},
|
||||
border_radius: 0.0.into(),
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
},
|
||||
highlight.color,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An event produced during a drag and drop interaction of a [`PaneGrid`].
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum DragEvent {
|
||||
/// A [`Pane`] was picked for dragging.
|
||||
Picked {
|
||||
/// The picked [`Pane`].
|
||||
pane: Pane,
|
||||
},
|
||||
|
||||
/// A [`Pane`] was dropped on top of another [`Pane`].
|
||||
Dropped {
|
||||
/// The picked [`Pane`].
|
||||
pane: Pane,
|
||||
|
||||
/// The [`Pane`] where the picked one was dropped on.
|
||||
target: Pane,
|
||||
},
|
||||
|
||||
/// A [`Pane`] was picked and then dropped outside of other [`Pane`]
|
||||
/// boundaries.
|
||||
Canceled {
|
||||
/// The picked [`Pane`].
|
||||
pane: Pane,
|
||||
},
|
||||
}
|
||||
|
||||
/// An event produced during a resize interaction of a [`PaneGrid`].
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct ResizeEvent {
|
||||
/// The [`Split`] that is being dragged for resizing.
|
||||
pub split: Split,
|
||||
|
||||
/// The new ratio of the [`Split`].
|
||||
///
|
||||
/// The ratio is a value in [0, 1], representing the exact position of a
|
||||
/// [`Split`] between two panes.
|
||||
pub ratio: f32,
|
||||
}
|
||||
|
||||
/*
|
||||
* Helpers
|
||||
*/
|
||||
fn hovered_split<'a>(
|
||||
splits: impl Iterator<Item = (&'a Split, &'a (Axis, Rectangle, f32))>,
|
||||
spacing: f32,
|
||||
cursor_position: Point,
|
||||
) -> Option<(Split, Axis, Rectangle)> {
|
||||
splits
|
||||
.filter_map(|(split, (axis, region, ratio))| {
|
||||
let bounds = axis.split_line_bounds(*region, *ratio, spacing);
|
||||
|
||||
if bounds.contains(cursor_position) {
|
||||
Some((*split, *axis, bounds))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.next()
|
||||
}
|
||||
|
||||
/// The visible contents of the [`PaneGrid`]
|
||||
#[derive(Debug)]
|
||||
pub enum Contents<'a, T> {
|
||||
/// All panes are visible
|
||||
All(Vec<(Pane, T)>, &'a state::Internal),
|
||||
/// A maximized pane is visible
|
||||
Maximized(Pane, T, Node),
|
||||
}
|
||||
|
||||
impl<'a, T> Contents<'a, T> {
|
||||
/// Returns the layout [`Node`] of the [`Contents`]
|
||||
pub fn layout(&self) -> &Node {
|
||||
match self {
|
||||
Contents::All(_, state) => state.layout(),
|
||||
Contents::Maximized(_, _, layout) => layout,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an iterator over the values of the [`Contents`]
|
||||
pub fn iter(&self) -> Box<dyn Iterator<Item = (Pane, &T)> + '_> {
|
||||
match self {
|
||||
Contents::All(contents, _) => Box::new(
|
||||
contents.iter().map(|(pane, content)| (*pane, content)),
|
||||
),
|
||||
Contents::Maximized(pane, content, _) => {
|
||||
Box::new(std::iter::once((*pane, content)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn iter_mut(&mut self) -> Box<dyn Iterator<Item = (Pane, &mut T)> + '_> {
|
||||
match self {
|
||||
Contents::All(contents, _) => Box::new(
|
||||
contents.iter_mut().map(|(pane, content)| (*pane, content)),
|
||||
),
|
||||
Contents::Maximized(pane, content, _) => {
|
||||
Box::new(std::iter::once((*pane, content)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_maximized(&self) -> bool {
|
||||
matches!(self, Self::Maximized(..))
|
||||
}
|
||||
}
|
||||
241
widget/src/pane_grid/axis.rs
Normal file
241
widget/src/pane_grid/axis.rs
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
use crate::core::Rectangle;
|
||||
|
||||
/// A fixed reference line for the measurement of coordinates.
|
||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
||||
pub enum Axis {
|
||||
/// The horizontal axis: —
|
||||
Horizontal,
|
||||
/// The vertical axis: |
|
||||
Vertical,
|
||||
}
|
||||
|
||||
impl Axis {
|
||||
/// Splits the provided [`Rectangle`] on the current [`Axis`] with the
|
||||
/// given `ratio` and `spacing`.
|
||||
pub fn split(
|
||||
&self,
|
||||
rectangle: &Rectangle,
|
||||
ratio: f32,
|
||||
spacing: f32,
|
||||
) -> (Rectangle, Rectangle) {
|
||||
match self {
|
||||
Axis::Horizontal => {
|
||||
let height_top =
|
||||
(rectangle.height * ratio - spacing / 2.0).round();
|
||||
let height_bottom = rectangle.height - height_top - spacing;
|
||||
|
||||
(
|
||||
Rectangle {
|
||||
height: height_top,
|
||||
..*rectangle
|
||||
},
|
||||
Rectangle {
|
||||
y: rectangle.y + height_top + spacing,
|
||||
height: height_bottom,
|
||||
..*rectangle
|
||||
},
|
||||
)
|
||||
}
|
||||
Axis::Vertical => {
|
||||
let width_left =
|
||||
(rectangle.width * ratio - spacing / 2.0).round();
|
||||
let width_right = rectangle.width - width_left - spacing;
|
||||
|
||||
(
|
||||
Rectangle {
|
||||
width: width_left,
|
||||
..*rectangle
|
||||
},
|
||||
Rectangle {
|
||||
x: rectangle.x + width_left + spacing,
|
||||
width: width_right,
|
||||
..*rectangle
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculates the bounds of the split line in a [`Rectangle`] region.
|
||||
pub fn split_line_bounds(
|
||||
&self,
|
||||
rectangle: Rectangle,
|
||||
ratio: f32,
|
||||
spacing: f32,
|
||||
) -> Rectangle {
|
||||
match self {
|
||||
Axis::Horizontal => Rectangle {
|
||||
x: rectangle.x,
|
||||
y: (rectangle.y + rectangle.height * ratio - spacing / 2.0)
|
||||
.round(),
|
||||
width: rectangle.width,
|
||||
height: spacing,
|
||||
},
|
||||
Axis::Vertical => Rectangle {
|
||||
x: (rectangle.x + rectangle.width * ratio - spacing / 2.0)
|
||||
.round(),
|
||||
y: rectangle.y,
|
||||
width: spacing,
|
||||
height: rectangle.height,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
enum Case {
|
||||
Horizontal {
|
||||
overall_height: f32,
|
||||
spacing: f32,
|
||||
top_height: f32,
|
||||
bottom_y: f32,
|
||||
bottom_height: f32,
|
||||
},
|
||||
Vertical {
|
||||
overall_width: f32,
|
||||
spacing: f32,
|
||||
left_width: f32,
|
||||
right_x: f32,
|
||||
right_width: f32,
|
||||
},
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split() {
|
||||
let cases = vec![
|
||||
// Even height, even spacing
|
||||
Case::Horizontal {
|
||||
overall_height: 10.0,
|
||||
spacing: 2.0,
|
||||
top_height: 4.0,
|
||||
bottom_y: 6.0,
|
||||
bottom_height: 4.0,
|
||||
},
|
||||
// Odd height, even spacing
|
||||
Case::Horizontal {
|
||||
overall_height: 9.0,
|
||||
spacing: 2.0,
|
||||
top_height: 4.0,
|
||||
bottom_y: 6.0,
|
||||
bottom_height: 3.0,
|
||||
},
|
||||
// Even height, odd spacing
|
||||
Case::Horizontal {
|
||||
overall_height: 10.0,
|
||||
spacing: 1.0,
|
||||
top_height: 5.0,
|
||||
bottom_y: 6.0,
|
||||
bottom_height: 4.0,
|
||||
},
|
||||
// Odd height, odd spacing
|
||||
Case::Horizontal {
|
||||
overall_height: 9.0,
|
||||
spacing: 1.0,
|
||||
top_height: 4.0,
|
||||
bottom_y: 5.0,
|
||||
bottom_height: 4.0,
|
||||
},
|
||||
// Even width, even spacing
|
||||
Case::Vertical {
|
||||
overall_width: 10.0,
|
||||
spacing: 2.0,
|
||||
left_width: 4.0,
|
||||
right_x: 6.0,
|
||||
right_width: 4.0,
|
||||
},
|
||||
// Odd width, even spacing
|
||||
Case::Vertical {
|
||||
overall_width: 9.0,
|
||||
spacing: 2.0,
|
||||
left_width: 4.0,
|
||||
right_x: 6.0,
|
||||
right_width: 3.0,
|
||||
},
|
||||
// Even width, odd spacing
|
||||
Case::Vertical {
|
||||
overall_width: 10.0,
|
||||
spacing: 1.0,
|
||||
left_width: 5.0,
|
||||
right_x: 6.0,
|
||||
right_width: 4.0,
|
||||
},
|
||||
// Odd width, odd spacing
|
||||
Case::Vertical {
|
||||
overall_width: 9.0,
|
||||
spacing: 1.0,
|
||||
left_width: 4.0,
|
||||
right_x: 5.0,
|
||||
right_width: 4.0,
|
||||
},
|
||||
];
|
||||
for case in cases {
|
||||
match case {
|
||||
Case::Horizontal {
|
||||
overall_height,
|
||||
spacing,
|
||||
top_height,
|
||||
bottom_y,
|
||||
bottom_height,
|
||||
} => {
|
||||
let a = Axis::Horizontal;
|
||||
let r = Rectangle {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
width: 10.0,
|
||||
height: overall_height,
|
||||
};
|
||||
let (top, bottom) = a.split(&r, 0.5, spacing);
|
||||
assert_eq!(
|
||||
top,
|
||||
Rectangle {
|
||||
height: top_height,
|
||||
..r
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
bottom,
|
||||
Rectangle {
|
||||
y: bottom_y,
|
||||
height: bottom_height,
|
||||
..r
|
||||
}
|
||||
);
|
||||
}
|
||||
Case::Vertical {
|
||||
overall_width,
|
||||
spacing,
|
||||
left_width,
|
||||
right_x,
|
||||
right_width,
|
||||
} => {
|
||||
let a = Axis::Vertical;
|
||||
let r = Rectangle {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
width: overall_width,
|
||||
height: 10.0,
|
||||
};
|
||||
let (left, right) = a.split(&r, 0.5, spacing);
|
||||
assert_eq!(
|
||||
left,
|
||||
Rectangle {
|
||||
width: left_width,
|
||||
..r
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
right,
|
||||
Rectangle {
|
||||
x: right_x,
|
||||
width: right_width,
|
||||
..r
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
26
widget/src/pane_grid/configuration.rs
Normal file
26
widget/src/pane_grid/configuration.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
use crate::pane_grid::Axis;
|
||||
|
||||
/// The arrangement of a [`PaneGrid`].
|
||||
///
|
||||
/// [`PaneGrid`]: crate::widget::PaneGrid
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Configuration<T> {
|
||||
/// A split of the available space.
|
||||
Split {
|
||||
/// The direction of the split.
|
||||
axis: Axis,
|
||||
|
||||
/// The ratio of the split in [0.0, 1.0].
|
||||
ratio: f32,
|
||||
|
||||
/// The left/top [`Configuration`] of the split.
|
||||
a: Box<Configuration<T>>,
|
||||
|
||||
/// The right/bottom [`Configuration`] of the split.
|
||||
b: Box<Configuration<T>>,
|
||||
},
|
||||
/// A [`Pane`].
|
||||
///
|
||||
/// [`Pane`]: crate::widget::pane_grid::Pane
|
||||
Pane(T),
|
||||
}
|
||||
373
widget/src/pane_grid/content.rs
Normal file
373
widget/src/pane_grid/content.rs
Normal file
|
|
@ -0,0 +1,373 @@
|
|||
use crate::container;
|
||||
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::{self, Tree};
|
||||
use crate::core::{Clipboard, Element, Layout, Point, Rectangle, Shell, Size};
|
||||
use crate::pane_grid::{Draggable, TitleBar};
|
||||
|
||||
/// The content of a [`Pane`].
|
||||
///
|
||||
/// [`Pane`]: crate::widget::pane_grid::Pane
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Content<'a, Message, Renderer = crate::Renderer>
|
||||
where
|
||||
Renderer: crate::core::Renderer,
|
||||
Renderer::Theme: container::StyleSheet,
|
||||
{
|
||||
title_bar: Option<TitleBar<'a, Message, Renderer>>,
|
||||
body: Element<'a, Message, Renderer>,
|
||||
style: <Renderer::Theme as container::StyleSheet>::Style,
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Content<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: crate::core::Renderer,
|
||||
Renderer::Theme: container::StyleSheet,
|
||||
{
|
||||
/// Creates a new [`Content`] with the provided body.
|
||||
pub fn new(body: impl Into<Element<'a, Message, Renderer>>) -> Self {
|
||||
Self {
|
||||
title_bar: None,
|
||||
body: body.into(),
|
||||
style: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the [`TitleBar`] of this [`Content`].
|
||||
pub fn title_bar(
|
||||
mut self,
|
||||
title_bar: TitleBar<'a, Message, Renderer>,
|
||||
) -> Self {
|
||||
self.title_bar = Some(title_bar);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the [`Content`].
|
||||
pub fn style(
|
||||
mut self,
|
||||
style: impl Into<<Renderer::Theme as container::StyleSheet>::Style>,
|
||||
) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Content<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: crate::core::Renderer,
|
||||
Renderer::Theme: container::StyleSheet,
|
||||
{
|
||||
pub(super) fn state(&self) -> Tree {
|
||||
let children = if let Some(title_bar) = self.title_bar.as_ref() {
|
||||
vec![Tree::new(&self.body), title_bar.state()]
|
||||
} else {
|
||||
vec![Tree::new(&self.body), Tree::empty()]
|
||||
};
|
||||
|
||||
Tree {
|
||||
children,
|
||||
..Tree::empty()
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn diff(&self, tree: &mut Tree) {
|
||||
if tree.children.len() == 2 {
|
||||
if let Some(title_bar) = self.title_bar.as_ref() {
|
||||
title_bar.diff(&mut tree.children[1]);
|
||||
}
|
||||
|
||||
tree.children[0].diff(&self.body);
|
||||
} else {
|
||||
*tree = self.state();
|
||||
}
|
||||
}
|
||||
|
||||
/// Draws the [`Content`] with the provided [`Renderer`] and [`Layout`].
|
||||
///
|
||||
/// [`Renderer`]: crate::Renderer
|
||||
pub fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
use container::StyleSheet;
|
||||
|
||||
let bounds = layout.bounds();
|
||||
|
||||
{
|
||||
let style = theme.appearance(&self.style);
|
||||
|
||||
container::draw_background(renderer, &style, bounds);
|
||||
}
|
||||
|
||||
if let Some(title_bar) = &self.title_bar {
|
||||
let mut children = layout.children();
|
||||
let title_bar_layout = children.next().unwrap();
|
||||
let body_layout = children.next().unwrap();
|
||||
|
||||
let show_controls = bounds.contains(cursor_position);
|
||||
|
||||
self.body.as_widget().draw(
|
||||
&tree.children[0],
|
||||
renderer,
|
||||
theme,
|
||||
style,
|
||||
body_layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
);
|
||||
|
||||
title_bar.draw(
|
||||
&tree.children[1],
|
||||
renderer,
|
||||
theme,
|
||||
style,
|
||||
title_bar_layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
show_controls,
|
||||
);
|
||||
} else {
|
||||
self.body.as_widget().draw(
|
||||
&tree.children[0],
|
||||
renderer,
|
||||
theme,
|
||||
style,
|
||||
layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn layout(
|
||||
&self,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
if let Some(title_bar) = &self.title_bar {
|
||||
let max_size = limits.max();
|
||||
|
||||
let title_bar_layout = title_bar
|
||||
.layout(renderer, &layout::Limits::new(Size::ZERO, max_size));
|
||||
|
||||
let title_bar_size = title_bar_layout.size();
|
||||
|
||||
let mut body_layout = self.body.as_widget().layout(
|
||||
renderer,
|
||||
&layout::Limits::new(
|
||||
Size::ZERO,
|
||||
Size::new(
|
||||
max_size.width,
|
||||
max_size.height - title_bar_size.height,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
body_layout.move_to(Point::new(0.0, title_bar_size.height));
|
||||
|
||||
layout::Node::with_children(
|
||||
max_size,
|
||||
vec![title_bar_layout, body_layout],
|
||||
)
|
||||
} else {
|
||||
self.body.as_widget().layout(renderer, limits)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn operate(
|
||||
&self,
|
||||
tree: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn widget::Operation<Message>,
|
||||
) {
|
||||
let body_layout = if let Some(title_bar) = &self.title_bar {
|
||||
let mut children = layout.children();
|
||||
|
||||
title_bar.operate(
|
||||
&mut tree.children[1],
|
||||
children.next().unwrap(),
|
||||
renderer,
|
||||
operation,
|
||||
);
|
||||
|
||||
children.next().unwrap()
|
||||
} else {
|
||||
layout
|
||||
};
|
||||
|
||||
self.body.as_widget().operate(
|
||||
&mut tree.children[0],
|
||||
body_layout,
|
||||
renderer,
|
||||
operation,
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn on_event(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
is_picked: bool,
|
||||
) -> event::Status {
|
||||
let mut event_status = event::Status::Ignored;
|
||||
|
||||
let body_layout = if let Some(title_bar) = &mut self.title_bar {
|
||||
let mut children = layout.children();
|
||||
|
||||
event_status = title_bar.on_event(
|
||||
&mut tree.children[1],
|
||||
event.clone(),
|
||||
children.next().unwrap(),
|
||||
cursor_position,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
);
|
||||
|
||||
children.next().unwrap()
|
||||
} else {
|
||||
layout
|
||||
};
|
||||
|
||||
let body_status = if is_picked {
|
||||
event::Status::Ignored
|
||||
} else {
|
||||
self.body.as_widget_mut().on_event(
|
||||
&mut tree.children[0],
|
||||
event,
|
||||
body_layout,
|
||||
cursor_position,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
)
|
||||
};
|
||||
|
||||
event_status.merge(body_status)
|
||||
}
|
||||
|
||||
pub(crate) fn mouse_interaction(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
renderer: &Renderer,
|
||||
drag_enabled: bool,
|
||||
) -> mouse::Interaction {
|
||||
let (body_layout, title_bar_interaction) =
|
||||
if let Some(title_bar) = &self.title_bar {
|
||||
let mut children = layout.children();
|
||||
let title_bar_layout = children.next().unwrap();
|
||||
|
||||
let is_over_pick_area = title_bar
|
||||
.is_over_pick_area(title_bar_layout, cursor_position);
|
||||
|
||||
if is_over_pick_area && drag_enabled {
|
||||
return mouse::Interaction::Grab;
|
||||
}
|
||||
|
||||
let mouse_interaction = title_bar.mouse_interaction(
|
||||
&tree.children[1],
|
||||
title_bar_layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
renderer,
|
||||
);
|
||||
|
||||
(children.next().unwrap(), mouse_interaction)
|
||||
} else {
|
||||
(layout, mouse::Interaction::default())
|
||||
};
|
||||
|
||||
self.body
|
||||
.as_widget()
|
||||
.mouse_interaction(
|
||||
&tree.children[0],
|
||||
body_layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
renderer,
|
||||
)
|
||||
.max(title_bar_interaction)
|
||||
}
|
||||
|
||||
pub(crate) fn overlay<'b>(
|
||||
&'b mut self,
|
||||
tree: &'b mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
) -> Option<overlay::Element<'b, Message, Renderer>> {
|
||||
if let Some(title_bar) = self.title_bar.as_mut() {
|
||||
let mut children = layout.children();
|
||||
let title_bar_layout = children.next()?;
|
||||
|
||||
let mut states = tree.children.iter_mut();
|
||||
let body_state = states.next().unwrap();
|
||||
let title_bar_state = states.next().unwrap();
|
||||
|
||||
match title_bar.overlay(title_bar_state, title_bar_layout, renderer)
|
||||
{
|
||||
Some(overlay) => Some(overlay),
|
||||
None => self.body.as_widget_mut().overlay(
|
||||
body_state,
|
||||
children.next()?,
|
||||
renderer,
|
||||
),
|
||||
}
|
||||
} else {
|
||||
self.body.as_widget_mut().overlay(
|
||||
&mut tree.children[0],
|
||||
layout,
|
||||
renderer,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Draggable for &Content<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: crate::core::Renderer,
|
||||
Renderer::Theme: container::StyleSheet,
|
||||
{
|
||||
fn can_be_dragged_at(
|
||||
&self,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
) -> bool {
|
||||
if let Some(title_bar) = &self.title_bar {
|
||||
let mut children = layout.children();
|
||||
let title_bar_layout = children.next().unwrap();
|
||||
|
||||
title_bar.is_over_pick_area(title_bar_layout, cursor_position)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T, Message, Renderer> From<T> for Content<'a, Message, Renderer>
|
||||
where
|
||||
T: Into<Element<'a, Message, Renderer>>,
|
||||
Renderer: crate::core::Renderer,
|
||||
Renderer::Theme: container::StyleSheet,
|
||||
{
|
||||
fn from(element: T) -> Self {
|
||||
Self::new(element)
|
||||
}
|
||||
}
|
||||
12
widget/src/pane_grid/direction.rs
Normal file
12
widget/src/pane_grid/direction.rs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
/// A four cardinal direction.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Direction {
|
||||
/// ↑
|
||||
Up,
|
||||
/// ↓
|
||||
Down,
|
||||
/// ←
|
||||
Left,
|
||||
/// →
|
||||
Right,
|
||||
}
|
||||
12
widget/src/pane_grid/draggable.rs
Normal file
12
widget/src/pane_grid/draggable.rs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
use crate::core::{Layout, Point};
|
||||
|
||||
/// A pane that can be dragged.
|
||||
pub trait Draggable {
|
||||
/// Returns whether the [`Draggable`] with the given [`Layout`] can be picked
|
||||
/// at the provided cursor position.
|
||||
fn can_be_dragged_at(
|
||||
&self,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
) -> bool;
|
||||
}
|
||||
250
widget/src/pane_grid/node.rs
Normal file
250
widget/src/pane_grid/node.rs
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
use crate::core::{Rectangle, Size};
|
||||
use crate::pane_grid::{Axis, Pane, Split};
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
/// A layout node of a [`PaneGrid`].
|
||||
///
|
||||
/// [`PaneGrid`]: crate::widget::PaneGrid
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Node {
|
||||
/// The region of this [`Node`] is split into two.
|
||||
Split {
|
||||
/// The [`Split`] of this [`Node`].
|
||||
id: Split,
|
||||
|
||||
/// The direction of the split.
|
||||
axis: Axis,
|
||||
|
||||
/// The ratio of the split in [0.0, 1.0].
|
||||
ratio: f32,
|
||||
|
||||
/// The left/top [`Node`] of the split.
|
||||
a: Box<Node>,
|
||||
|
||||
/// The right/bottom [`Node`] of the split.
|
||||
b: Box<Node>,
|
||||
},
|
||||
/// The region of this [`Node`] is taken by a [`Pane`].
|
||||
Pane(Pane),
|
||||
}
|
||||
|
||||
impl Node {
|
||||
/// Returns an iterator over each [`Split`] in this [`Node`].
|
||||
pub fn splits(&self) -> impl Iterator<Item = &Split> {
|
||||
let mut unvisited_nodes = vec![self];
|
||||
|
||||
std::iter::from_fn(move || {
|
||||
while let Some(node) = unvisited_nodes.pop() {
|
||||
if let Node::Split { id, a, b, .. } = node {
|
||||
unvisited_nodes.push(a);
|
||||
unvisited_nodes.push(b);
|
||||
|
||||
return Some(id);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the rectangular region for each [`Pane`] in the [`Node`] given
|
||||
/// the spacing between panes and the total available space.
|
||||
pub fn pane_regions(
|
||||
&self,
|
||||
spacing: f32,
|
||||
size: Size,
|
||||
) -> BTreeMap<Pane, Rectangle> {
|
||||
let mut regions = BTreeMap::new();
|
||||
|
||||
self.compute_regions(
|
||||
spacing,
|
||||
&Rectangle {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
},
|
||||
&mut regions,
|
||||
);
|
||||
|
||||
regions
|
||||
}
|
||||
|
||||
/// Returns the axis, rectangular region, and ratio for each [`Split`] in
|
||||
/// the [`Node`] given the spacing between panes and the total available
|
||||
/// space.
|
||||
pub fn split_regions(
|
||||
&self,
|
||||
spacing: f32,
|
||||
size: Size,
|
||||
) -> BTreeMap<Split, (Axis, Rectangle, f32)> {
|
||||
let mut splits = BTreeMap::new();
|
||||
|
||||
self.compute_splits(
|
||||
spacing,
|
||||
&Rectangle {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
},
|
||||
&mut splits,
|
||||
);
|
||||
|
||||
splits
|
||||
}
|
||||
|
||||
pub(crate) fn find(&mut self, pane: &Pane) -> Option<&mut Node> {
|
||||
match self {
|
||||
Node::Split { a, b, .. } => {
|
||||
a.find(pane).or_else(move || b.find(pane))
|
||||
}
|
||||
Node::Pane(p) => {
|
||||
if p == pane {
|
||||
Some(self)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn split(&mut self, id: Split, axis: Axis, new_pane: Pane) {
|
||||
*self = Node::Split {
|
||||
id,
|
||||
axis,
|
||||
ratio: 0.5,
|
||||
a: Box::new(self.clone()),
|
||||
b: Box::new(Node::Pane(new_pane)),
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn update(&mut self, f: &impl Fn(&mut Node)) {
|
||||
if let Node::Split { a, b, .. } = self {
|
||||
a.update(f);
|
||||
b.update(f);
|
||||
}
|
||||
|
||||
f(self);
|
||||
}
|
||||
|
||||
pub(crate) fn resize(&mut self, split: &Split, percentage: f32) -> bool {
|
||||
match self {
|
||||
Node::Split {
|
||||
id, ratio, a, b, ..
|
||||
} => {
|
||||
if id == split {
|
||||
*ratio = percentage;
|
||||
|
||||
true
|
||||
} else if a.resize(split, percentage) {
|
||||
true
|
||||
} else {
|
||||
b.resize(split, percentage)
|
||||
}
|
||||
}
|
||||
Node::Pane(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn remove(&mut self, pane: &Pane) -> Option<Pane> {
|
||||
match self {
|
||||
Node::Split { a, b, .. } => {
|
||||
if a.pane() == Some(*pane) {
|
||||
*self = *b.clone();
|
||||
Some(self.first_pane())
|
||||
} else if b.pane() == Some(*pane) {
|
||||
*self = *a.clone();
|
||||
Some(self.first_pane())
|
||||
} else {
|
||||
a.remove(pane).or_else(|| b.remove(pane))
|
||||
}
|
||||
}
|
||||
Node::Pane(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn pane(&self) -> Option<Pane> {
|
||||
match self {
|
||||
Node::Split { .. } => None,
|
||||
Node::Pane(pane) => Some(*pane),
|
||||
}
|
||||
}
|
||||
|
||||
fn first_pane(&self) -> Pane {
|
||||
match self {
|
||||
Node::Split { a, .. } => a.first_pane(),
|
||||
Node::Pane(pane) => *pane,
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_regions(
|
||||
&self,
|
||||
spacing: f32,
|
||||
current: &Rectangle,
|
||||
regions: &mut BTreeMap<Pane, Rectangle>,
|
||||
) {
|
||||
match self {
|
||||
Node::Split {
|
||||
axis, ratio, a, b, ..
|
||||
} => {
|
||||
let (region_a, region_b) = axis.split(current, *ratio, spacing);
|
||||
|
||||
a.compute_regions(spacing, ®ion_a, regions);
|
||||
b.compute_regions(spacing, ®ion_b, regions);
|
||||
}
|
||||
Node::Pane(pane) => {
|
||||
let _ = regions.insert(*pane, *current);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_splits(
|
||||
&self,
|
||||
spacing: f32,
|
||||
current: &Rectangle,
|
||||
splits: &mut BTreeMap<Split, (Axis, Rectangle, f32)>,
|
||||
) {
|
||||
match self {
|
||||
Node::Split {
|
||||
axis,
|
||||
ratio,
|
||||
a,
|
||||
b,
|
||||
id,
|
||||
} => {
|
||||
let (region_a, region_b) = axis.split(current, *ratio, spacing);
|
||||
|
||||
let _ = splits.insert(*id, (*axis, *current, *ratio));
|
||||
|
||||
a.compute_splits(spacing, ®ion_a, splits);
|
||||
b.compute_splits(spacing, ®ion_b, splits);
|
||||
}
|
||||
Node::Pane(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::hash::Hash for Node {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
match self {
|
||||
Node::Split {
|
||||
id,
|
||||
axis,
|
||||
ratio,
|
||||
a,
|
||||
b,
|
||||
} => {
|
||||
id.hash(state);
|
||||
axis.hash(state);
|
||||
((ratio * 100_000.0) as u32).hash(state);
|
||||
a.hash(state);
|
||||
b.hash(state);
|
||||
}
|
||||
Node::Pane(pane) => {
|
||||
pane.hash(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
5
widget/src/pane_grid/pane.rs
Normal file
5
widget/src/pane_grid/pane.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
/// A rectangular region in a [`PaneGrid`] used to display widgets.
|
||||
///
|
||||
/// [`PaneGrid`]: crate::widget::PaneGrid
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct Pane(pub(super) usize);
|
||||
5
widget/src/pane_grid/split.rs
Normal file
5
widget/src/pane_grid/split.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
/// A divider that splits a region in a [`PaneGrid`] into two different panes.
|
||||
///
|
||||
/// [`PaneGrid`]: crate::widget::PaneGrid
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct Split(pub(super) usize);
|
||||
348
widget/src/pane_grid/state.rs
Normal file
348
widget/src/pane_grid/state.rs
Normal file
|
|
@ -0,0 +1,348 @@
|
|||
//! The state of a [`PaneGrid`].
|
||||
//!
|
||||
//! [`PaneGrid`]: crate::widget::PaneGrid
|
||||
use crate::core::{Point, Size};
|
||||
use crate::pane_grid::{Axis, Configuration, Direction, Node, Pane, Split};
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// The state of a [`PaneGrid`].
|
||||
///
|
||||
/// It keeps track of the state of each [`Pane`] and the position of each
|
||||
/// [`Split`].
|
||||
///
|
||||
/// The [`State`] needs to own any mutable contents a [`Pane`] may need. This is
|
||||
/// why this struct is generic over the type `T`. Values of this type are
|
||||
/// provided to the view function of [`PaneGrid::new`] for displaying each
|
||||
/// [`Pane`].
|
||||
///
|
||||
/// [`PaneGrid`]: crate::widget::PaneGrid
|
||||
/// [`PaneGrid::new`]: crate::widget::PaneGrid::new
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct State<T> {
|
||||
/// The panes of the [`PaneGrid`].
|
||||
///
|
||||
/// [`PaneGrid`]: crate::widget::PaneGrid
|
||||
pub panes: HashMap<Pane, T>,
|
||||
|
||||
/// The internal state of the [`PaneGrid`].
|
||||
///
|
||||
/// [`PaneGrid`]: crate::widget::PaneGrid
|
||||
pub internal: Internal,
|
||||
|
||||
/// The maximized [`Pane`] of the [`PaneGrid`].
|
||||
///
|
||||
/// [`PaneGrid`]: crate::widget::PaneGrid
|
||||
pub(super) maximized: Option<Pane>,
|
||||
}
|
||||
|
||||
impl<T> State<T> {
|
||||
/// Creates a new [`State`], initializing the first pane with the provided
|
||||
/// state.
|
||||
///
|
||||
/// Alongside the [`State`], it returns the first [`Pane`] identifier.
|
||||
pub fn new(first_pane_state: T) -> (Self, Pane) {
|
||||
(
|
||||
Self::with_configuration(Configuration::Pane(first_pane_state)),
|
||||
Pane(0),
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a new [`State`] with the given [`Configuration`].
|
||||
pub fn with_configuration(config: impl Into<Configuration<T>>) -> Self {
|
||||
let mut panes = HashMap::new();
|
||||
|
||||
let internal =
|
||||
Internal::from_configuration(&mut panes, config.into(), 0);
|
||||
|
||||
State {
|
||||
panes,
|
||||
internal,
|
||||
maximized: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the total amount of panes in the [`State`].
|
||||
pub fn len(&self) -> usize {
|
||||
self.panes.len()
|
||||
}
|
||||
|
||||
/// Returns `true` if the amount of panes in the [`State`] is 0.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
|
||||
/// Returns the internal state of the given [`Pane`], if it exists.
|
||||
pub fn get(&self, pane: &Pane) -> Option<&T> {
|
||||
self.panes.get(pane)
|
||||
}
|
||||
|
||||
/// Returns the internal state of the given [`Pane`] with mutability, if it
|
||||
/// exists.
|
||||
pub fn get_mut(&mut self, pane: &Pane) -> Option<&mut T> {
|
||||
self.panes.get_mut(pane)
|
||||
}
|
||||
|
||||
/// Returns an iterator over all the panes of the [`State`], alongside its
|
||||
/// internal state.
|
||||
pub fn iter(&self) -> impl Iterator<Item = (&Pane, &T)> {
|
||||
self.panes.iter()
|
||||
}
|
||||
|
||||
/// Returns a mutable iterator over all the panes of the [`State`],
|
||||
/// alongside its internal state.
|
||||
pub fn iter_mut(&mut self) -> impl Iterator<Item = (&Pane, &mut T)> {
|
||||
self.panes.iter_mut()
|
||||
}
|
||||
|
||||
/// Returns the layout of the [`State`].
|
||||
pub fn layout(&self) -> &Node {
|
||||
&self.internal.layout
|
||||
}
|
||||
|
||||
/// Returns the adjacent [`Pane`] of another [`Pane`] in the given
|
||||
/// direction, if there is one.
|
||||
pub fn adjacent(&self, pane: &Pane, direction: Direction) -> Option<Pane> {
|
||||
let regions = self
|
||||
.internal
|
||||
.layout
|
||||
.pane_regions(0.0, Size::new(4096.0, 4096.0));
|
||||
|
||||
let current_region = regions.get(pane)?;
|
||||
|
||||
let target = match direction {
|
||||
Direction::Left => {
|
||||
Point::new(current_region.x - 1.0, current_region.y + 1.0)
|
||||
}
|
||||
Direction::Right => Point::new(
|
||||
current_region.x + current_region.width + 1.0,
|
||||
current_region.y + 1.0,
|
||||
),
|
||||
Direction::Up => {
|
||||
Point::new(current_region.x + 1.0, current_region.y - 1.0)
|
||||
}
|
||||
Direction::Down => Point::new(
|
||||
current_region.x + 1.0,
|
||||
current_region.y + current_region.height + 1.0,
|
||||
),
|
||||
};
|
||||
|
||||
let mut colliding_regions =
|
||||
regions.iter().filter(|(_, region)| region.contains(target));
|
||||
|
||||
let (pane, _) = colliding_regions.next()?;
|
||||
|
||||
Some(*pane)
|
||||
}
|
||||
|
||||
/// Splits the given [`Pane`] into two in the given [`Axis`] and
|
||||
/// initializing the new [`Pane`] with the provided internal state.
|
||||
pub fn split(
|
||||
&mut self,
|
||||
axis: Axis,
|
||||
pane: &Pane,
|
||||
state: T,
|
||||
) -> Option<(Pane, Split)> {
|
||||
let node = self.internal.layout.find(pane)?;
|
||||
|
||||
let new_pane = {
|
||||
self.internal.last_id = self.internal.last_id.checked_add(1)?;
|
||||
|
||||
Pane(self.internal.last_id)
|
||||
};
|
||||
|
||||
let new_split = {
|
||||
self.internal.last_id = self.internal.last_id.checked_add(1)?;
|
||||
|
||||
Split(self.internal.last_id)
|
||||
};
|
||||
|
||||
node.split(new_split, axis, new_pane);
|
||||
|
||||
let _ = self.panes.insert(new_pane, state);
|
||||
let _ = self.maximized.take();
|
||||
|
||||
Some((new_pane, new_split))
|
||||
}
|
||||
|
||||
/// Swaps the position of the provided panes in the [`State`].
|
||||
///
|
||||
/// If you want to swap panes on drag and drop in your [`PaneGrid`], you
|
||||
/// will need to call this method when handling a [`DragEvent`].
|
||||
///
|
||||
/// [`PaneGrid`]: crate::widget::PaneGrid
|
||||
/// [`DragEvent`]: crate::widget::pane_grid::DragEvent
|
||||
pub fn swap(&mut self, a: &Pane, b: &Pane) {
|
||||
self.internal.layout.update(&|node| match node {
|
||||
Node::Split { .. } => {}
|
||||
Node::Pane(pane) => {
|
||||
if pane == a {
|
||||
*node = Node::Pane(*b);
|
||||
} else if pane == b {
|
||||
*node = Node::Pane(*a);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Resizes two panes by setting the position of the provided [`Split`].
|
||||
///
|
||||
/// The ratio is a value in [0, 1], representing the exact position of a
|
||||
/// [`Split`] between two panes.
|
||||
///
|
||||
/// If you want to enable resize interactions in your [`PaneGrid`], you will
|
||||
/// need to call this method when handling a [`ResizeEvent`].
|
||||
///
|
||||
/// [`PaneGrid`]: crate::widget::PaneGrid
|
||||
/// [`ResizeEvent`]: crate::widget::pane_grid::ResizeEvent
|
||||
pub fn resize(&mut self, split: &Split, ratio: f32) {
|
||||
let _ = self.internal.layout.resize(split, ratio);
|
||||
}
|
||||
|
||||
/// Closes the given [`Pane`] and returns its internal state and its closest
|
||||
/// sibling, if it exists.
|
||||
pub fn close(&mut self, pane: &Pane) -> Option<(T, Pane)> {
|
||||
if self.maximized == Some(*pane) {
|
||||
let _ = self.maximized.take();
|
||||
}
|
||||
|
||||
if let Some(sibling) = self.internal.layout.remove(pane) {
|
||||
self.panes.remove(pane).map(|state| (state, sibling))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Maximize the given [`Pane`]. Only this pane will be rendered by the
|
||||
/// [`PaneGrid`] until [`Self::restore()`] is called.
|
||||
///
|
||||
/// [`PaneGrid`]: crate::widget::PaneGrid
|
||||
pub fn maximize(&mut self, pane: &Pane) {
|
||||
self.maximized = Some(*pane);
|
||||
}
|
||||
|
||||
/// Restore the currently maximized [`Pane`] to it's normal size. All panes
|
||||
/// will be rendered by the [`PaneGrid`].
|
||||
///
|
||||
/// [`PaneGrid`]: crate::widget::PaneGrid
|
||||
pub fn restore(&mut self) {
|
||||
let _ = self.maximized.take();
|
||||
}
|
||||
|
||||
/// Returns the maximized [`Pane`] of the [`PaneGrid`].
|
||||
///
|
||||
/// [`PaneGrid`]: crate::widget::PaneGrid
|
||||
pub fn maximized(&self) -> Option<Pane> {
|
||||
self.maximized
|
||||
}
|
||||
}
|
||||
|
||||
/// The internal state of a [`PaneGrid`].
|
||||
///
|
||||
/// [`PaneGrid`]: crate::widget::PaneGrid
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Internal {
|
||||
layout: Node,
|
||||
last_id: usize,
|
||||
}
|
||||
|
||||
impl Internal {
|
||||
/// Initializes the [`Internal`] state of a [`PaneGrid`] from a
|
||||
/// [`Configuration`].
|
||||
///
|
||||
/// [`PaneGrid`]: crate::widget::PaneGrid
|
||||
pub fn from_configuration<T>(
|
||||
panes: &mut HashMap<Pane, T>,
|
||||
content: Configuration<T>,
|
||||
next_id: usize,
|
||||
) -> Self {
|
||||
let (layout, last_id) = match content {
|
||||
Configuration::Split { axis, ratio, a, b } => {
|
||||
let Internal {
|
||||
layout: a,
|
||||
last_id: next_id,
|
||||
..
|
||||
} = Self::from_configuration(panes, *a, next_id);
|
||||
|
||||
let Internal {
|
||||
layout: b,
|
||||
last_id: next_id,
|
||||
..
|
||||
} = Self::from_configuration(panes, *b, next_id);
|
||||
|
||||
(
|
||||
Node::Split {
|
||||
id: Split(next_id),
|
||||
axis,
|
||||
ratio,
|
||||
a: Box::new(a),
|
||||
b: Box::new(b),
|
||||
},
|
||||
next_id + 1,
|
||||
)
|
||||
}
|
||||
Configuration::Pane(state) => {
|
||||
let id = Pane(next_id);
|
||||
let _ = panes.insert(id, state);
|
||||
|
||||
(Node::Pane(id), next_id + 1)
|
||||
}
|
||||
};
|
||||
|
||||
Self { layout, last_id }
|
||||
}
|
||||
}
|
||||
|
||||
/// The current action of a [`PaneGrid`].
|
||||
///
|
||||
/// [`PaneGrid`]: crate::widget::PaneGrid
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum Action {
|
||||
/// The [`PaneGrid`] is idle.
|
||||
///
|
||||
/// [`PaneGrid`]: crate::widget::PaneGrid
|
||||
Idle,
|
||||
/// A [`Pane`] in the [`PaneGrid`] is being dragged.
|
||||
///
|
||||
/// [`PaneGrid`]: crate::widget::PaneGrid
|
||||
Dragging {
|
||||
/// The [`Pane`] being dragged.
|
||||
pane: Pane,
|
||||
/// The starting [`Point`] of the drag interaction.
|
||||
origin: Point,
|
||||
},
|
||||
/// A [`Split`] in the [`PaneGrid`] is being dragged.
|
||||
///
|
||||
/// [`PaneGrid`]: crate::widget::PaneGrid
|
||||
Resizing {
|
||||
/// The [`Split`] being dragged.
|
||||
split: Split,
|
||||
/// The [`Axis`] of the [`Split`].
|
||||
axis: Axis,
|
||||
},
|
||||
}
|
||||
|
||||
impl Action {
|
||||
/// Returns the current [`Pane`] that is being dragged, if any.
|
||||
pub fn picked_pane(&self) -> Option<(Pane, Point)> {
|
||||
match *self {
|
||||
Action::Dragging { pane, origin, .. } => Some((pane, origin)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the current [`Split`] that is being dragged, if any.
|
||||
pub fn picked_split(&self) -> Option<(Split, Axis)> {
|
||||
match *self {
|
||||
Action::Resizing { split, axis, .. } => Some((split, axis)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Internal {
|
||||
/// The layout [`Node`] of the [`Internal`] state
|
||||
pub fn layout(&self) -> &Node {
|
||||
&self.layout
|
||||
}
|
||||
}
|
||||
432
widget/src/pane_grid/title_bar.rs
Normal file
432
widget/src/pane_grid/title_bar.rs
Normal file
|
|
@ -0,0 +1,432 @@
|
|||
use crate::container;
|
||||
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::{self, Tree};
|
||||
use crate::core::{
|
||||
Clipboard, Element, Layout, Padding, Point, Rectangle, Shell, Size,
|
||||
};
|
||||
|
||||
/// The title bar of a [`Pane`].
|
||||
///
|
||||
/// [`Pane`]: crate::widget::pane_grid::Pane
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct TitleBar<'a, Message, Renderer = crate::Renderer>
|
||||
where
|
||||
Renderer: crate::core::Renderer,
|
||||
Renderer::Theme: container::StyleSheet,
|
||||
{
|
||||
content: Element<'a, Message, Renderer>,
|
||||
controls: Option<Element<'a, Message, Renderer>>,
|
||||
padding: Padding,
|
||||
always_show_controls: bool,
|
||||
style: <Renderer::Theme as container::StyleSheet>::Style,
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> TitleBar<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: crate::core::Renderer,
|
||||
Renderer::Theme: container::StyleSheet,
|
||||
{
|
||||
/// Creates a new [`TitleBar`] with the given content.
|
||||
pub fn new<E>(content: E) -> Self
|
||||
where
|
||||
E: Into<Element<'a, Message, Renderer>>,
|
||||
{
|
||||
Self {
|
||||
content: content.into(),
|
||||
controls: None,
|
||||
padding: Padding::ZERO,
|
||||
always_show_controls: false,
|
||||
style: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the controls of the [`TitleBar`].
|
||||
pub fn controls(
|
||||
mut self,
|
||||
controls: impl Into<Element<'a, Message, Renderer>>,
|
||||
) -> Self {
|
||||
self.controls = Some(controls.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`Padding`] of the [`TitleBar`].
|
||||
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
|
||||
self.padding = padding.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the [`TitleBar`].
|
||||
pub fn style(
|
||||
mut self,
|
||||
style: impl Into<<Renderer::Theme as container::StyleSheet>::Style>,
|
||||
) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets whether or not the [`controls`] attached to this [`TitleBar`] are
|
||||
/// always visible.
|
||||
///
|
||||
/// By default, the controls are only visible when the [`Pane`] of this
|
||||
/// [`TitleBar`] is hovered.
|
||||
///
|
||||
/// [`controls`]: Self::controls
|
||||
/// [`Pane`]: crate::widget::pane_grid::Pane
|
||||
pub fn always_show_controls(mut self) -> Self {
|
||||
self.always_show_controls = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> TitleBar<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: crate::core::Renderer,
|
||||
Renderer::Theme: container::StyleSheet,
|
||||
{
|
||||
pub(super) fn state(&self) -> Tree {
|
||||
let children = if let Some(controls) = self.controls.as_ref() {
|
||||
vec![Tree::new(&self.content), Tree::new(controls)]
|
||||
} else {
|
||||
vec![Tree::new(&self.content), Tree::empty()]
|
||||
};
|
||||
|
||||
Tree {
|
||||
children,
|
||||
..Tree::empty()
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn diff(&self, tree: &mut Tree) {
|
||||
if tree.children.len() == 2 {
|
||||
if let Some(controls) = self.controls.as_ref() {
|
||||
tree.children[1].diff(controls);
|
||||
}
|
||||
|
||||
tree.children[0].diff(&self.content);
|
||||
} else {
|
||||
*tree = self.state();
|
||||
}
|
||||
}
|
||||
|
||||
/// Draws the [`TitleBar`] with the provided [`Renderer`] and [`Layout`].
|
||||
///
|
||||
/// [`Renderer`]: crate::Renderer
|
||||
pub fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
inherited_style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
show_controls: bool,
|
||||
) {
|
||||
use container::StyleSheet;
|
||||
|
||||
let bounds = layout.bounds();
|
||||
let style = theme.appearance(&self.style);
|
||||
let inherited_style = renderer::Style {
|
||||
text_color: style.text_color.unwrap_or(inherited_style.text_color),
|
||||
};
|
||||
|
||||
container::draw_background(renderer, &style, bounds);
|
||||
|
||||
let mut children = layout.children();
|
||||
let padded = children.next().unwrap();
|
||||
|
||||
let mut children = padded.children();
|
||||
let title_layout = children.next().unwrap();
|
||||
let mut show_title = true;
|
||||
|
||||
if let Some(controls) = &self.controls {
|
||||
if show_controls || self.always_show_controls {
|
||||
let controls_layout = children.next().unwrap();
|
||||
if title_layout.bounds().width + controls_layout.bounds().width
|
||||
> padded.bounds().width
|
||||
{
|
||||
show_title = false;
|
||||
}
|
||||
|
||||
controls.as_widget().draw(
|
||||
&tree.children[1],
|
||||
renderer,
|
||||
theme,
|
||||
&inherited_style,
|
||||
controls_layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if show_title {
|
||||
self.content.as_widget().draw(
|
||||
&tree.children[0],
|
||||
renderer,
|
||||
theme,
|
||||
&inherited_style,
|
||||
title_layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether the mouse cursor is over the pick area of the
|
||||
/// [`TitleBar`] or not.
|
||||
///
|
||||
/// The whole [`TitleBar`] is a pick area, except its controls.
|
||||
pub fn is_over_pick_area(
|
||||
&self,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
) -> bool {
|
||||
if layout.bounds().contains(cursor_position) {
|
||||
let mut children = layout.children();
|
||||
let padded = children.next().unwrap();
|
||||
let mut children = padded.children();
|
||||
let title_layout = children.next().unwrap();
|
||||
|
||||
if self.controls.is_some() {
|
||||
let controls_layout = children.next().unwrap();
|
||||
|
||||
if title_layout.bounds().width + controls_layout.bounds().width
|
||||
> padded.bounds().width
|
||||
{
|
||||
!controls_layout.bounds().contains(cursor_position)
|
||||
} else {
|
||||
!controls_layout.bounds().contains(cursor_position)
|
||||
&& !title_layout.bounds().contains(cursor_position)
|
||||
}
|
||||
} else {
|
||||
!title_layout.bounds().contains(cursor_position)
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn layout(
|
||||
&self,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
let limits = limits.pad(self.padding);
|
||||
let max_size = limits.max();
|
||||
|
||||
let title_layout = self
|
||||
.content
|
||||
.as_widget()
|
||||
.layout(renderer, &layout::Limits::new(Size::ZERO, max_size));
|
||||
|
||||
let title_size = title_layout.size();
|
||||
|
||||
let mut node = if let Some(controls) = &self.controls {
|
||||
let mut controls_layout = controls
|
||||
.as_widget()
|
||||
.layout(renderer, &layout::Limits::new(Size::ZERO, max_size));
|
||||
|
||||
let controls_size = controls_layout.size();
|
||||
let space_before_controls = max_size.width - controls_size.width;
|
||||
|
||||
let height = title_size.height.max(controls_size.height);
|
||||
|
||||
controls_layout.move_to(Point::new(space_before_controls, 0.0));
|
||||
|
||||
layout::Node::with_children(
|
||||
Size::new(max_size.width, height),
|
||||
vec![title_layout, controls_layout],
|
||||
)
|
||||
} else {
|
||||
layout::Node::with_children(
|
||||
Size::new(max_size.width, title_size.height),
|
||||
vec![title_layout],
|
||||
)
|
||||
};
|
||||
|
||||
node.move_to(Point::new(self.padding.left, self.padding.top));
|
||||
|
||||
layout::Node::with_children(node.size().pad(self.padding), vec![node])
|
||||
}
|
||||
|
||||
pub(crate) fn operate(
|
||||
&self,
|
||||
tree: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn widget::Operation<Message>,
|
||||
) {
|
||||
let mut children = layout.children();
|
||||
let padded = children.next().unwrap();
|
||||
|
||||
let mut children = padded.children();
|
||||
let title_layout = children.next().unwrap();
|
||||
let mut show_title = true;
|
||||
|
||||
if let Some(controls) = &self.controls {
|
||||
let controls_layout = children.next().unwrap();
|
||||
|
||||
if title_layout.bounds().width + controls_layout.bounds().width
|
||||
> padded.bounds().width
|
||||
{
|
||||
show_title = false;
|
||||
}
|
||||
|
||||
controls.as_widget().operate(
|
||||
&mut tree.children[1],
|
||||
controls_layout,
|
||||
renderer,
|
||||
operation,
|
||||
)
|
||||
};
|
||||
|
||||
if show_title {
|
||||
self.content.as_widget().operate(
|
||||
&mut tree.children[0],
|
||||
title_layout,
|
||||
renderer,
|
||||
operation,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn on_event(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
let mut children = layout.children();
|
||||
let padded = children.next().unwrap();
|
||||
|
||||
let mut children = padded.children();
|
||||
let title_layout = children.next().unwrap();
|
||||
let mut show_title = true;
|
||||
|
||||
let control_status = if let Some(controls) = &mut self.controls {
|
||||
let controls_layout = children.next().unwrap();
|
||||
if title_layout.bounds().width + controls_layout.bounds().width
|
||||
> padded.bounds().width
|
||||
{
|
||||
show_title = false;
|
||||
}
|
||||
|
||||
controls.as_widget_mut().on_event(
|
||||
&mut tree.children[1],
|
||||
event.clone(),
|
||||
controls_layout,
|
||||
cursor_position,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
)
|
||||
} else {
|
||||
event::Status::Ignored
|
||||
};
|
||||
|
||||
let title_status = if show_title {
|
||||
self.content.as_widget_mut().on_event(
|
||||
&mut tree.children[0],
|
||||
event,
|
||||
title_layout,
|
||||
cursor_position,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
)
|
||||
} else {
|
||||
event::Status::Ignored
|
||||
};
|
||||
|
||||
control_status.merge(title_status)
|
||||
}
|
||||
|
||||
pub(crate) fn mouse_interaction(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
let mut children = layout.children();
|
||||
let padded = children.next().unwrap();
|
||||
|
||||
let mut children = padded.children();
|
||||
let title_layout = children.next().unwrap();
|
||||
|
||||
let title_interaction = self.content.as_widget().mouse_interaction(
|
||||
&tree.children[0],
|
||||
title_layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
renderer,
|
||||
);
|
||||
|
||||
if let Some(controls) = &self.controls {
|
||||
let controls_layout = children.next().unwrap();
|
||||
let controls_interaction = controls.as_widget().mouse_interaction(
|
||||
&tree.children[1],
|
||||
controls_layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
renderer,
|
||||
);
|
||||
|
||||
if title_layout.bounds().width + controls_layout.bounds().width
|
||||
> padded.bounds().width
|
||||
{
|
||||
controls_interaction
|
||||
} else {
|
||||
controls_interaction.max(title_interaction)
|
||||
}
|
||||
} else {
|
||||
title_interaction
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn overlay<'b>(
|
||||
&'b mut self,
|
||||
tree: &'b mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
) -> Option<overlay::Element<'b, Message, Renderer>> {
|
||||
let mut children = layout.children();
|
||||
let padded = children.next()?;
|
||||
|
||||
let mut children = padded.children();
|
||||
let title_layout = children.next()?;
|
||||
|
||||
let Self {
|
||||
content, controls, ..
|
||||
} = self;
|
||||
|
||||
let mut states = tree.children.iter_mut();
|
||||
let title_state = states.next().unwrap();
|
||||
let controls_state = states.next().unwrap();
|
||||
|
||||
content
|
||||
.as_widget_mut()
|
||||
.overlay(title_state, title_layout, renderer)
|
||||
.or_else(move || {
|
||||
controls.as_mut().and_then(|controls| {
|
||||
let controls_layout = children.next()?;
|
||||
|
||||
controls.as_widget_mut().overlay(
|
||||
controls_state,
|
||||
controls_layout,
|
||||
renderer,
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
658
widget/src/pick_list.rs
Normal file
658
widget/src/pick_list.rs
Normal file
|
|
@ -0,0 +1,658 @@
|
|||
//! Display a dropdown list of selectable values.
|
||||
use crate::container;
|
||||
use crate::core::alignment;
|
||||
use crate::core::event::{self, Event};
|
||||
use crate::core::keyboard;
|
||||
use crate::core::layout;
|
||||
use crate::core::mouse;
|
||||
use crate::core::overlay;
|
||||
use crate::core::renderer;
|
||||
use crate::core::text::{self, Text};
|
||||
use crate::core::touch;
|
||||
use crate::core::widget::tree::{self, Tree};
|
||||
use crate::core::{
|
||||
Clipboard, Element, Layout, Length, Padding, Pixels, Point, Rectangle,
|
||||
Shell, Size, Widget,
|
||||
};
|
||||
use crate::overlay::menu::{self, Menu};
|
||||
use crate::scrollable;
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
pub use crate::style::pick_list::{Appearance, StyleSheet};
|
||||
|
||||
/// A widget for selecting a single value from a list of options.
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct PickList<'a, T, Message, Renderer = crate::Renderer>
|
||||
where
|
||||
[T]: ToOwned<Owned = Vec<T>>,
|
||||
Renderer: text::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
on_selected: Box<dyn Fn(T) -> Message + 'a>,
|
||||
options: Cow<'a, [T]>,
|
||||
placeholder: Option<String>,
|
||||
selected: Option<T>,
|
||||
width: Length,
|
||||
padding: Padding,
|
||||
text_size: Option<f32>,
|
||||
font: Option<Renderer::Font>,
|
||||
handle: Handle<Renderer::Font>,
|
||||
style: <Renderer::Theme as StyleSheet>::Style,
|
||||
}
|
||||
|
||||
impl<'a, T: 'a, Message, Renderer> PickList<'a, T, Message, Renderer>
|
||||
where
|
||||
T: ToString + Eq,
|
||||
[T]: ToOwned<Owned = Vec<T>>,
|
||||
Renderer: text::Renderer,
|
||||
Renderer::Theme: StyleSheet
|
||||
+ scrollable::StyleSheet
|
||||
+ menu::StyleSheet
|
||||
+ container::StyleSheet,
|
||||
<Renderer::Theme as menu::StyleSheet>::Style:
|
||||
From<<Renderer::Theme as StyleSheet>::Style>,
|
||||
{
|
||||
/// The default padding of a [`PickList`].
|
||||
pub const DEFAULT_PADDING: Padding = Padding::new(5.0);
|
||||
|
||||
/// Creates a new [`PickList`] with the given list of options, the current
|
||||
/// selected value, and the message to produce when an option is selected.
|
||||
pub fn new(
|
||||
options: impl Into<Cow<'a, [T]>>,
|
||||
selected: Option<T>,
|
||||
on_selected: impl Fn(T) -> Message + 'a,
|
||||
) -> Self {
|
||||
Self {
|
||||
on_selected: Box::new(on_selected),
|
||||
options: options.into(),
|
||||
placeholder: None,
|
||||
selected,
|
||||
width: Length::Shrink,
|
||||
padding: Self::DEFAULT_PADDING,
|
||||
text_size: None,
|
||||
font: None,
|
||||
handle: Default::default(),
|
||||
style: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the placeholder of the [`PickList`].
|
||||
pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
|
||||
self.placeholder = Some(placeholder.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the width of the [`PickList`].
|
||||
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||
self.width = width.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`Padding`] of the [`PickList`].
|
||||
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
|
||||
self.padding = padding.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the text size of the [`PickList`].
|
||||
pub fn text_size(mut self, size: impl Into<Pixels>) -> Self {
|
||||
self.text_size = Some(size.into().0);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the font of the [`PickList`].
|
||||
pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
|
||||
self.font = Some(font.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`Handle`] of the [`PickList`].
|
||||
pub fn handle(mut self, handle: Handle<Renderer::Font>) -> Self {
|
||||
self.handle = handle;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the [`PickList`].
|
||||
pub fn style(
|
||||
mut self,
|
||||
style: impl Into<<Renderer::Theme as StyleSheet>::Style>,
|
||||
) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: 'a, Message, Renderer> Widget<Message, Renderer>
|
||||
for PickList<'a, T, Message, Renderer>
|
||||
where
|
||||
T: Clone + ToString + Eq + 'static,
|
||||
[T]: ToOwned<Owned = Vec<T>>,
|
||||
Message: 'a,
|
||||
Renderer: text::Renderer + 'a,
|
||||
Renderer::Theme: StyleSheet
|
||||
+ scrollable::StyleSheet
|
||||
+ menu::StyleSheet
|
||||
+ container::StyleSheet,
|
||||
<Renderer::Theme as menu::StyleSheet>::Style:
|
||||
From<<Renderer::Theme as StyleSheet>::Style>,
|
||||
{
|
||||
fn tag(&self) -> tree::Tag {
|
||||
tree::Tag::of::<State<T>>()
|
||||
}
|
||||
|
||||
fn state(&self) -> tree::State {
|
||||
tree::State::new(State::<T>::new())
|
||||
}
|
||||
|
||||
fn width(&self) -> Length {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
Length::Shrink
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
layout(
|
||||
renderer,
|
||||
limits,
|
||||
self.width,
|
||||
self.padding,
|
||||
self.text_size,
|
||||
self.font,
|
||||
self.placeholder.as_deref(),
|
||||
&self.options,
|
||||
)
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
_renderer: &Renderer,
|
||||
_clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
update(
|
||||
event,
|
||||
layout,
|
||||
cursor_position,
|
||||
shell,
|
||||
self.on_selected.as_ref(),
|
||||
self.selected.as_ref(),
|
||||
&self.options,
|
||||
|| tree.state.downcast_mut::<State<T>>(),
|
||||
)
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
_tree: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
_renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
mouse_interaction(layout, cursor_position)
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
_style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
) {
|
||||
let font = self.font.unwrap_or_else(|| renderer.default_font());
|
||||
draw(
|
||||
renderer,
|
||||
theme,
|
||||
layout,
|
||||
cursor_position,
|
||||
self.padding,
|
||||
self.text_size,
|
||||
font,
|
||||
self.placeholder.as_deref(),
|
||||
self.selected.as_ref(),
|
||||
&self.handle,
|
||||
&self.style,
|
||||
|| tree.state.downcast_ref::<State<T>>(),
|
||||
)
|
||||
}
|
||||
|
||||
fn overlay<'b>(
|
||||
&'b mut self,
|
||||
tree: &'b mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
) -> Option<overlay::Element<'b, Message, Renderer>> {
|
||||
let state = tree.state.downcast_mut::<State<T>>();
|
||||
|
||||
overlay(
|
||||
layout,
|
||||
state,
|
||||
self.padding,
|
||||
self.text_size,
|
||||
self.font.unwrap_or_else(|| renderer.default_font()),
|
||||
&self.options,
|
||||
self.style.clone(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: 'a, Message, Renderer> From<PickList<'a, T, Message, Renderer>>
|
||||
for Element<'a, Message, Renderer>
|
||||
where
|
||||
T: Clone + ToString + Eq + 'static,
|
||||
[T]: ToOwned<Owned = Vec<T>>,
|
||||
Message: 'a,
|
||||
Renderer: text::Renderer + 'a,
|
||||
Renderer::Theme: StyleSheet
|
||||
+ scrollable::StyleSheet
|
||||
+ menu::StyleSheet
|
||||
+ container::StyleSheet,
|
||||
<Renderer::Theme as menu::StyleSheet>::Style:
|
||||
From<<Renderer::Theme as StyleSheet>::Style>,
|
||||
{
|
||||
fn from(pick_list: PickList<'a, T, Message, Renderer>) -> Self {
|
||||
Self::new(pick_list)
|
||||
}
|
||||
}
|
||||
|
||||
/// The local state of a [`PickList`].
|
||||
#[derive(Debug)]
|
||||
pub struct State<T> {
|
||||
menu: menu::State,
|
||||
keyboard_modifiers: keyboard::Modifiers,
|
||||
is_open: bool,
|
||||
hovered_option: Option<usize>,
|
||||
last_selection: Option<T>,
|
||||
}
|
||||
|
||||
impl<T> State<T> {
|
||||
/// Creates a new [`State`] for a [`PickList`].
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
menu: menu::State::default(),
|
||||
keyboard_modifiers: keyboard::Modifiers::default(),
|
||||
is_open: bool::default(),
|
||||
hovered_option: Option::default(),
|
||||
last_selection: Option::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Default for State<T> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// The handle to the right side of the [`PickList`].
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Handle<Font> {
|
||||
/// Displays an arrow icon (▼).
|
||||
///
|
||||
/// This is the default.
|
||||
Arrow {
|
||||
/// Font size of the content.
|
||||
size: Option<f32>,
|
||||
},
|
||||
/// A custom static handle.
|
||||
Static(Icon<Font>),
|
||||
/// A custom dynamic handle.
|
||||
Dynamic {
|
||||
/// The [`Icon`] used when [`PickList`] is closed.
|
||||
closed: Icon<Font>,
|
||||
/// The [`Icon`] used when [`PickList`] is open.
|
||||
open: Icon<Font>,
|
||||
},
|
||||
/// No handle will be shown.
|
||||
None,
|
||||
}
|
||||
|
||||
impl<Font> Default for Handle<Font> {
|
||||
fn default() -> Self {
|
||||
Self::Arrow { size: None }
|
||||
}
|
||||
}
|
||||
|
||||
/// The icon of a [`Handle`].
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Icon<Font> {
|
||||
/// Font that will be used to display the `code_point`,
|
||||
pub font: Font,
|
||||
/// The unicode code point that will be used as the icon.
|
||||
pub code_point: char,
|
||||
/// Font size of the content.
|
||||
pub size: Option<f32>,
|
||||
}
|
||||
|
||||
/// Computes the layout of a [`PickList`].
|
||||
pub fn layout<Renderer, T>(
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
width: Length,
|
||||
padding: Padding,
|
||||
text_size: Option<f32>,
|
||||
font: Option<Renderer::Font>,
|
||||
placeholder: Option<&str>,
|
||||
options: &[T],
|
||||
) -> layout::Node
|
||||
where
|
||||
Renderer: text::Renderer,
|
||||
T: ToString,
|
||||
{
|
||||
use std::f32;
|
||||
|
||||
let limits = limits.width(width).height(Length::Shrink).pad(padding);
|
||||
let text_size = text_size.unwrap_or_else(|| renderer.default_size());
|
||||
|
||||
let max_width = match width {
|
||||
Length::Shrink => {
|
||||
let measure = |label: &str| -> f32 {
|
||||
let (width, _) = renderer.measure(
|
||||
label,
|
||||
text_size,
|
||||
font.unwrap_or_else(|| renderer.default_font()),
|
||||
Size::new(f32::INFINITY, f32::INFINITY),
|
||||
);
|
||||
|
||||
width.round()
|
||||
};
|
||||
|
||||
let labels = options.iter().map(ToString::to_string);
|
||||
|
||||
let labels_width = labels
|
||||
.map(|label| measure(&label))
|
||||
.fold(100.0, |candidate, current| current.max(candidate));
|
||||
|
||||
let placeholder_width = placeholder.map(measure).unwrap_or(100.0);
|
||||
|
||||
labels_width.max(placeholder_width)
|
||||
}
|
||||
_ => 0.0,
|
||||
};
|
||||
|
||||
let size = {
|
||||
let intrinsic =
|
||||
Size::new(max_width + text_size + padding.left, text_size * 1.2);
|
||||
|
||||
limits.resolve(intrinsic).pad(padding)
|
||||
};
|
||||
|
||||
layout::Node::new(size)
|
||||
}
|
||||
|
||||
/// Processes an [`Event`] and updates the [`State`] of a [`PickList`]
|
||||
/// accordingly.
|
||||
pub fn update<'a, T, Message>(
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
on_selected: &dyn Fn(T) -> Message,
|
||||
selected: Option<&T>,
|
||||
options: &[T],
|
||||
state: impl FnOnce() -> &'a mut State<T>,
|
||||
) -> event::Status
|
||||
where
|
||||
T: PartialEq + Clone + 'a,
|
||||
{
|
||||
match event {
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
|
||||
| Event::Touch(touch::Event::FingerPressed { .. }) => {
|
||||
let state = state();
|
||||
|
||||
let event_status = if state.is_open {
|
||||
// Event wasn't processed by overlay, so cursor was clicked either outside it's
|
||||
// bounds or on the drop-down, either way we close the overlay.
|
||||
state.is_open = false;
|
||||
|
||||
event::Status::Captured
|
||||
} else if layout.bounds().contains(cursor_position) {
|
||||
state.is_open = true;
|
||||
state.hovered_option =
|
||||
options.iter().position(|option| Some(option) == selected);
|
||||
|
||||
event::Status::Captured
|
||||
} else {
|
||||
event::Status::Ignored
|
||||
};
|
||||
|
||||
if let Some(last_selection) = state.last_selection.take() {
|
||||
shell.publish((on_selected)(last_selection));
|
||||
|
||||
state.is_open = false;
|
||||
|
||||
event::Status::Captured
|
||||
} else {
|
||||
event_status
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse::Event::WheelScrolled {
|
||||
delta: mouse::ScrollDelta::Lines { y, .. },
|
||||
}) => {
|
||||
let state = state();
|
||||
|
||||
if state.keyboard_modifiers.command()
|
||||
&& layout.bounds().contains(cursor_position)
|
||||
&& !state.is_open
|
||||
{
|
||||
fn find_next<'a, T: PartialEq>(
|
||||
selected: &'a T,
|
||||
mut options: impl Iterator<Item = &'a T>,
|
||||
) -> Option<&'a T> {
|
||||
let _ = options.find(|&option| option == selected);
|
||||
|
||||
options.next()
|
||||
}
|
||||
|
||||
let next_option = if y < 0.0 {
|
||||
if let Some(selected) = selected {
|
||||
find_next(selected, options.iter())
|
||||
} else {
|
||||
options.first()
|
||||
}
|
||||
} else if y > 0.0 {
|
||||
if let Some(selected) = selected {
|
||||
find_next(selected, options.iter().rev())
|
||||
} else {
|
||||
options.last()
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(next_option) = next_option {
|
||||
shell.publish((on_selected)(next_option.clone()));
|
||||
}
|
||||
|
||||
event::Status::Captured
|
||||
} else {
|
||||
event::Status::Ignored
|
||||
}
|
||||
}
|
||||
Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
|
||||
let state = state();
|
||||
|
||||
state.keyboard_modifiers = modifiers;
|
||||
|
||||
event::Status::Ignored
|
||||
}
|
||||
_ => event::Status::Ignored,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the current [`mouse::Interaction`] of a [`PickList`].
|
||||
pub fn mouse_interaction(
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
) -> mouse::Interaction {
|
||||
let bounds = layout.bounds();
|
||||
let is_mouse_over = bounds.contains(cursor_position);
|
||||
|
||||
if is_mouse_over {
|
||||
mouse::Interaction::Pointer
|
||||
} else {
|
||||
mouse::Interaction::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the current overlay of a [`PickList`].
|
||||
pub fn overlay<'a, T, Message, Renderer>(
|
||||
layout: Layout<'_>,
|
||||
state: &'a mut State<T>,
|
||||
padding: Padding,
|
||||
text_size: Option<f32>,
|
||||
font: Renderer::Font,
|
||||
options: &'a [T],
|
||||
style: <Renderer::Theme as StyleSheet>::Style,
|
||||
) -> Option<overlay::Element<'a, Message, Renderer>>
|
||||
where
|
||||
T: Clone + ToString,
|
||||
Message: 'a,
|
||||
Renderer: text::Renderer + 'a,
|
||||
Renderer::Theme: StyleSheet
|
||||
+ scrollable::StyleSheet
|
||||
+ menu::StyleSheet
|
||||
+ container::StyleSheet,
|
||||
<Renderer::Theme as menu::StyleSheet>::Style:
|
||||
From<<Renderer::Theme as StyleSheet>::Style>,
|
||||
{
|
||||
if state.is_open {
|
||||
let bounds = layout.bounds();
|
||||
|
||||
let mut menu = Menu::new(
|
||||
&mut state.menu,
|
||||
options,
|
||||
&mut state.hovered_option,
|
||||
&mut state.last_selection,
|
||||
)
|
||||
.width(bounds.width)
|
||||
.padding(padding)
|
||||
.font(font)
|
||||
.style(style);
|
||||
|
||||
if let Some(text_size) = text_size {
|
||||
menu = menu.text_size(text_size);
|
||||
}
|
||||
|
||||
Some(menu.overlay(layout.position(), bounds.height))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Draws a [`PickList`].
|
||||
pub fn draw<'a, T, Renderer>(
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
padding: Padding,
|
||||
text_size: Option<f32>,
|
||||
font: Renderer::Font,
|
||||
placeholder: Option<&str>,
|
||||
selected: Option<&T>,
|
||||
handle: &Handle<Renderer::Font>,
|
||||
style: &<Renderer::Theme as StyleSheet>::Style,
|
||||
state: impl FnOnce() -> &'a State<T>,
|
||||
) where
|
||||
Renderer: text::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
T: ToString + 'a,
|
||||
{
|
||||
let bounds = layout.bounds();
|
||||
let is_mouse_over = bounds.contains(cursor_position);
|
||||
let is_selected = selected.is_some();
|
||||
|
||||
let style = if is_mouse_over {
|
||||
theme.hovered(style)
|
||||
} else {
|
||||
theme.active(style)
|
||||
};
|
||||
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds,
|
||||
border_color: style.border_color,
|
||||
border_width: style.border_width,
|
||||
border_radius: style.border_radius.into(),
|
||||
},
|
||||
style.background,
|
||||
);
|
||||
|
||||
let handle = match handle {
|
||||
Handle::Arrow { size } => {
|
||||
Some((Renderer::ICON_FONT, Renderer::ARROW_DOWN_ICON, *size))
|
||||
}
|
||||
Handle::Static(Icon {
|
||||
font,
|
||||
code_point,
|
||||
size,
|
||||
}) => Some((*font, *code_point, *size)),
|
||||
Handle::Dynamic { open, closed } => {
|
||||
if state().is_open {
|
||||
Some((open.font, open.code_point, open.size))
|
||||
} else {
|
||||
Some((closed.font, closed.code_point, closed.size))
|
||||
}
|
||||
}
|
||||
Handle::None => None,
|
||||
};
|
||||
|
||||
if let Some((font, code_point, size)) = handle {
|
||||
let size = size.unwrap_or_else(|| renderer.default_size());
|
||||
|
||||
renderer.fill_text(Text {
|
||||
content: &code_point.to_string(),
|
||||
size,
|
||||
font,
|
||||
color: style.handle_color,
|
||||
bounds: Rectangle {
|
||||
x: bounds.x + bounds.width - padding.horizontal(),
|
||||
y: bounds.center_y(),
|
||||
height: size * 1.2,
|
||||
..bounds
|
||||
},
|
||||
horizontal_alignment: alignment::Horizontal::Right,
|
||||
vertical_alignment: alignment::Vertical::Center,
|
||||
});
|
||||
}
|
||||
|
||||
let label = selected.map(ToString::to_string);
|
||||
|
||||
if let Some(label) = label.as_deref().or(placeholder) {
|
||||
let text_size = text_size.unwrap_or_else(|| renderer.default_size());
|
||||
|
||||
renderer.fill_text(Text {
|
||||
content: label,
|
||||
size: text_size,
|
||||
font,
|
||||
color: if is_selected {
|
||||
style.text_color
|
||||
} else {
|
||||
style.placeholder_color
|
||||
},
|
||||
bounds: Rectangle {
|
||||
x: bounds.x + padding.left,
|
||||
y: bounds.center_y(),
|
||||
width: bounds.width - padding.horizontal(),
|
||||
height: text_size * 1.2,
|
||||
},
|
||||
horizontal_alignment: alignment::Horizontal::Left,
|
||||
vertical_alignment: alignment::Vertical::Center,
|
||||
});
|
||||
}
|
||||
}
|
||||
172
widget/src/progress_bar.rs
Normal file
172
widget/src/progress_bar.rs
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
//! Provide progress feedback to your users.
|
||||
use crate::core::layout;
|
||||
use crate::core::renderer;
|
||||
use crate::core::widget::Tree;
|
||||
use crate::core::{
|
||||
Color, Element, Layout, Length, Point, Rectangle, Size, Widget,
|
||||
};
|
||||
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
pub use iced_style::progress_bar::{Appearance, StyleSheet};
|
||||
|
||||
/// A bar that displays progress.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # type ProgressBar =
|
||||
/// # iced_widget::ProgressBar<iced_widget::renderer::Renderer<iced_widget::style::Theme>>;
|
||||
/// #
|
||||
/// let value = 50.0;
|
||||
///
|
||||
/// ProgressBar::new(0.0..=100.0, value);
|
||||
/// ```
|
||||
///
|
||||
/// 
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct ProgressBar<Renderer = crate::Renderer>
|
||||
where
|
||||
Renderer: crate::core::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
range: RangeInclusive<f32>,
|
||||
value: f32,
|
||||
width: Length,
|
||||
height: Option<Length>,
|
||||
style: <Renderer::Theme as StyleSheet>::Style,
|
||||
}
|
||||
|
||||
impl<Renderer> ProgressBar<Renderer>
|
||||
where
|
||||
Renderer: crate::core::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
/// The default height of a [`ProgressBar`].
|
||||
pub const DEFAULT_HEIGHT: f32 = 30.0;
|
||||
|
||||
/// Creates a new [`ProgressBar`].
|
||||
///
|
||||
/// It expects:
|
||||
/// * an inclusive range of possible values
|
||||
/// * the current value of the [`ProgressBar`]
|
||||
pub fn new(range: RangeInclusive<f32>, value: f32) -> Self {
|
||||
ProgressBar {
|
||||
value: value.clamp(*range.start(), *range.end()),
|
||||
range,
|
||||
width: Length::Fill,
|
||||
height: None,
|
||||
style: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the width of the [`ProgressBar`].
|
||||
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||
self.width = width.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the height of the [`ProgressBar`].
|
||||
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
||||
self.height = Some(height.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the [`ProgressBar`].
|
||||
pub fn style(
|
||||
mut self,
|
||||
style: impl Into<<Renderer::Theme as StyleSheet>::Style>,
|
||||
) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<Message, Renderer> Widget<Message, Renderer> for ProgressBar<Renderer>
|
||||
where
|
||||
Renderer: crate::core::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
fn width(&self) -> Length {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
self.height.unwrap_or(Length::Fixed(Self::DEFAULT_HEIGHT))
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
_renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
let limits = limits
|
||||
.width(self.width)
|
||||
.height(self.height.unwrap_or(Length::Fixed(Self::DEFAULT_HEIGHT)));
|
||||
|
||||
let size = limits.resolve(Size::ZERO);
|
||||
|
||||
layout::Node::new(size)
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
_state: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
_style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
_cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
) {
|
||||
let bounds = layout.bounds();
|
||||
let (range_start, range_end) = self.range.clone().into_inner();
|
||||
|
||||
let active_progress_width = if range_start >= range_end {
|
||||
0.0
|
||||
} else {
|
||||
bounds.width * (self.value - range_start)
|
||||
/ (range_end - range_start)
|
||||
};
|
||||
|
||||
let style = theme.appearance(&self.style);
|
||||
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: Rectangle { ..bounds },
|
||||
border_radius: style.border_radius.into(),
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
},
|
||||
style.background,
|
||||
);
|
||||
|
||||
if active_progress_width > 0.0 {
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: Rectangle {
|
||||
width: active_progress_width,
|
||||
..bounds
|
||||
},
|
||||
border_radius: style.border_radius.into(),
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
},
|
||||
style.bar,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> From<ProgressBar<Renderer>>
|
||||
for Element<'a, Message, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Renderer: 'a + crate::core::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
fn from(
|
||||
progress_bar: ProgressBar<Renderer>,
|
||||
) -> Element<'a, Message, Renderer> {
|
||||
Element::new(progress_bar)
|
||||
}
|
||||
}
|
||||
297
widget/src/qr_code.rs
Normal file
297
widget/src/qr_code.rs
Normal file
|
|
@ -0,0 +1,297 @@
|
|||
//! Encode and display information in a QR code.
|
||||
use crate::canvas;
|
||||
use crate::core::layout;
|
||||
use crate::core::renderer::{self, Renderer as _};
|
||||
use crate::core::widget::Tree;
|
||||
use crate::core::{
|
||||
Color, Element, Layout, Length, Point, Rectangle, Size, Vector, Widget,
|
||||
};
|
||||
use crate::Renderer;
|
||||
use thiserror::Error;
|
||||
|
||||
const DEFAULT_CELL_SIZE: u16 = 4;
|
||||
const QUIET_ZONE: usize = 2;
|
||||
|
||||
/// A type of matrix barcode consisting of squares arranged in a grid which
|
||||
/// can be read by an imaging device, such as a camera.
|
||||
#[derive(Debug)]
|
||||
pub struct QRCode<'a> {
|
||||
state: &'a State,
|
||||
dark: Color,
|
||||
light: Color,
|
||||
cell_size: u16,
|
||||
}
|
||||
|
||||
impl<'a> QRCode<'a> {
|
||||
/// Creates a new [`QRCode`] with the provided [`State`].
|
||||
pub fn new(state: &'a State) -> Self {
|
||||
Self {
|
||||
cell_size: DEFAULT_CELL_SIZE,
|
||||
dark: Color::BLACK,
|
||||
light: Color::WHITE,
|
||||
state,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets both the dark and light [`Color`]s of the [`QRCode`].
|
||||
pub fn color(mut self, dark: Color, light: Color) -> Self {
|
||||
self.dark = dark;
|
||||
self.light = light;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the size of the squares of the grid cell of the [`QRCode`].
|
||||
pub fn cell_size(mut self, cell_size: u16) -> Self {
|
||||
self.cell_size = cell_size;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme> Widget<Message, Renderer<Theme>> for QRCode<'a> {
|
||||
fn width(&self) -> Length {
|
||||
Length::Shrink
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
Length::Shrink
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
_renderer: &Renderer<Theme>,
|
||||
_limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
let side_length = (self.state.width + 2 * QUIET_ZONE) as f32
|
||||
* f32::from(self.cell_size);
|
||||
|
||||
layout::Node::new(Size::new(side_length, side_length))
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
_state: &Tree,
|
||||
renderer: &mut Renderer<Theme>,
|
||||
_theme: &Theme,
|
||||
_style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
_cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
) {
|
||||
let bounds = layout.bounds();
|
||||
let side_length = self.state.width + 2 * QUIET_ZONE;
|
||||
|
||||
// Reuse cache if possible
|
||||
let geometry =
|
||||
self.state.cache.draw(renderer, bounds.size(), |frame| {
|
||||
// Scale units to cell size
|
||||
frame.scale(f32::from(self.cell_size));
|
||||
|
||||
// Draw background
|
||||
frame.fill_rectangle(
|
||||
Point::ORIGIN,
|
||||
Size::new(side_length as f32, side_length as f32),
|
||||
self.light,
|
||||
);
|
||||
|
||||
// Avoid drawing on the quiet zone
|
||||
frame.translate(Vector::new(
|
||||
QUIET_ZONE as f32,
|
||||
QUIET_ZONE as f32,
|
||||
));
|
||||
|
||||
// Draw contents
|
||||
self.state
|
||||
.contents
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, value)| **value == qrcode::Color::Dark)
|
||||
.for_each(|(index, _)| {
|
||||
let row = index / self.state.width;
|
||||
let column = index % self.state.width;
|
||||
|
||||
frame.fill_rectangle(
|
||||
Point::new(column as f32, row as f32),
|
||||
Size::UNIT,
|
||||
self.dark,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
let translation = Vector::new(bounds.x, bounds.y);
|
||||
|
||||
renderer.with_translation(translation, |renderer| {
|
||||
renderer.draw_primitive(geometry.0);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme> From<QRCode<'a>>
|
||||
for Element<'a, Message, Renderer<Theme>>
|
||||
{
|
||||
fn from(qr_code: QRCode<'a>) -> Self {
|
||||
Self::new(qr_code)
|
||||
}
|
||||
}
|
||||
|
||||
/// The state of a [`QRCode`].
|
||||
///
|
||||
/// It stores the data that will be displayed.
|
||||
#[derive(Debug)]
|
||||
pub struct State {
|
||||
contents: Vec<qrcode::Color>,
|
||||
width: usize,
|
||||
cache: canvas::Cache,
|
||||
}
|
||||
|
||||
impl State {
|
||||
/// Creates a new [`State`] with the provided data.
|
||||
///
|
||||
/// This method uses an [`ErrorCorrection::Medium`] and chooses the smallest
|
||||
/// size to display the data.
|
||||
pub fn new(data: impl AsRef<[u8]>) -> Result<Self, Error> {
|
||||
let encoded = qrcode::QrCode::new(data)?;
|
||||
|
||||
Ok(Self::build(encoded))
|
||||
}
|
||||
|
||||
/// Creates a new [`State`] with the provided [`ErrorCorrection`].
|
||||
pub fn with_error_correction(
|
||||
data: impl AsRef<[u8]>,
|
||||
error_correction: ErrorCorrection,
|
||||
) -> Result<Self, Error> {
|
||||
let encoded = qrcode::QrCode::with_error_correction_level(
|
||||
data,
|
||||
error_correction.into(),
|
||||
)?;
|
||||
|
||||
Ok(Self::build(encoded))
|
||||
}
|
||||
|
||||
/// Creates a new [`State`] with the provided [`Version`] and
|
||||
/// [`ErrorCorrection`].
|
||||
pub fn with_version(
|
||||
data: impl AsRef<[u8]>,
|
||||
version: Version,
|
||||
error_correction: ErrorCorrection,
|
||||
) -> Result<Self, Error> {
|
||||
let encoded = qrcode::QrCode::with_version(
|
||||
data,
|
||||
version.into(),
|
||||
error_correction.into(),
|
||||
)?;
|
||||
|
||||
Ok(Self::build(encoded))
|
||||
}
|
||||
|
||||
fn build(encoded: qrcode::QrCode) -> Self {
|
||||
let width = encoded.width();
|
||||
let contents = encoded.into_colors();
|
||||
|
||||
Self {
|
||||
contents,
|
||||
width,
|
||||
cache: canvas::Cache::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
/// The size of a [`QRCode`].
|
||||
///
|
||||
/// The higher the version the larger the grid of cells, and therefore the more
|
||||
/// information the [`QRCode`] can carry.
|
||||
pub enum Version {
|
||||
/// A normal QR code version. It should be between 1 and 40.
|
||||
Normal(u8),
|
||||
|
||||
/// A micro QR code version. It should be between 1 and 4.
|
||||
Micro(u8),
|
||||
}
|
||||
|
||||
impl From<Version> for qrcode::Version {
|
||||
fn from(version: Version) -> Self {
|
||||
match version {
|
||||
Version::Normal(v) => qrcode::Version::Normal(i16::from(v)),
|
||||
Version::Micro(v) => qrcode::Version::Micro(i16::from(v)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The error correction level.
|
||||
///
|
||||
/// It controls the amount of data that can be damaged while still being able
|
||||
/// to recover the original information.
|
||||
///
|
||||
/// A higher error correction level allows for more corrupted data.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ErrorCorrection {
|
||||
/// Low error correction. 7% of the data can be restored.
|
||||
Low,
|
||||
/// Medium error correction. 15% of the data can be restored.
|
||||
Medium,
|
||||
/// Quartile error correction. 25% of the data can be restored.
|
||||
Quartile,
|
||||
/// High error correction. 30% of the data can be restored.
|
||||
High,
|
||||
}
|
||||
|
||||
impl From<ErrorCorrection> for qrcode::EcLevel {
|
||||
fn from(ec_level: ErrorCorrection) -> Self {
|
||||
match ec_level {
|
||||
ErrorCorrection::Low => qrcode::EcLevel::L,
|
||||
ErrorCorrection::Medium => qrcode::EcLevel::M,
|
||||
ErrorCorrection::Quartile => qrcode::EcLevel::Q,
|
||||
ErrorCorrection::High => qrcode::EcLevel::H,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An error that occurred when building a [`State`] for a [`QRCode`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
|
||||
pub enum Error {
|
||||
/// The data is too long to encode in a QR code for the chosen [`Version`].
|
||||
#[error(
|
||||
"The data is too long to encode in a QR code for the chosen version"
|
||||
)]
|
||||
DataTooLong,
|
||||
|
||||
/// The chosen [`Version`] and [`ErrorCorrection`] combination is invalid.
|
||||
#[error(
|
||||
"The chosen version and error correction level combination is invalid."
|
||||
)]
|
||||
InvalidVersion,
|
||||
|
||||
/// One or more characters in the provided data are not supported by the
|
||||
/// chosen [`Version`].
|
||||
#[error(
|
||||
"One or more characters in the provided data are not supported by the \
|
||||
chosen version"
|
||||
)]
|
||||
UnsupportedCharacterSet,
|
||||
|
||||
/// The chosen ECI designator is invalid. A valid designator should be
|
||||
/// between 0 and 999999.
|
||||
#[error(
|
||||
"The chosen ECI designator is invalid. A valid designator should be \
|
||||
between 0 and 999999."
|
||||
)]
|
||||
InvalidEciDesignator,
|
||||
|
||||
/// A character that does not belong to the character set was found.
|
||||
#[error("A character that does not belong to the character set was found")]
|
||||
InvalidCharacter,
|
||||
}
|
||||
|
||||
impl From<qrcode::types::QrError> for Error {
|
||||
fn from(error: qrcode::types::QrError) -> Self {
|
||||
use qrcode::types::QrError;
|
||||
|
||||
match error {
|
||||
QrError::DataTooLong => Error::DataTooLong,
|
||||
QrError::InvalidVersion => Error::InvalidVersion,
|
||||
QrError::UnsupportedCharacterSet => Error::UnsupportedCharacterSet,
|
||||
QrError::InvalidEciDesignator => Error::InvalidEciDesignator,
|
||||
QrError::InvalidCharacter => Error::InvalidCharacter,
|
||||
}
|
||||
}
|
||||
}
|
||||
300
widget/src/radio.rs
Normal file
300
widget/src/radio.rs
Normal file
|
|
@ -0,0 +1,300 @@
|
|||
//! Create choices using radio buttons.
|
||||
use crate::core::alignment;
|
||||
use crate::core::event::{self, Event};
|
||||
use crate::core::layout;
|
||||
use crate::core::mouse;
|
||||
use crate::core::renderer;
|
||||
use crate::core::text;
|
||||
use crate::core::touch;
|
||||
use crate::core::widget::Tree;
|
||||
use crate::core::{
|
||||
Alignment, Clipboard, Color, Element, Layout, Length, Pixels, Point,
|
||||
Rectangle, Shell, Widget,
|
||||
};
|
||||
use crate::{Row, Text};
|
||||
|
||||
pub use iced_style::radio::{Appearance, StyleSheet};
|
||||
|
||||
/// A circular button representing a choice.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # type Radio<Message> =
|
||||
/// # iced_widget::Radio<Message, iced_widget::renderer::Renderer<iced_widget::style::Theme>>;
|
||||
/// #
|
||||
/// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
/// pub enum Choice {
|
||||
/// A,
|
||||
/// B,
|
||||
/// }
|
||||
///
|
||||
/// #[derive(Debug, Clone, Copy)]
|
||||
/// pub enum Message {
|
||||
/// RadioSelected(Choice),
|
||||
/// }
|
||||
///
|
||||
/// let selected_choice = Some(Choice::A);
|
||||
///
|
||||
/// Radio::new(Choice::A, "This is A", selected_choice, Message::RadioSelected);
|
||||
///
|
||||
/// Radio::new(Choice::B, "This is B", selected_choice, Message::RadioSelected);
|
||||
/// ```
|
||||
///
|
||||
/// 
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Radio<Message, Renderer = crate::Renderer>
|
||||
where
|
||||
Renderer: text::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
is_selected: bool,
|
||||
on_click: Message,
|
||||
label: String,
|
||||
width: Length,
|
||||
size: f32,
|
||||
spacing: f32,
|
||||
text_size: Option<f32>,
|
||||
font: Option<Renderer::Font>,
|
||||
style: <Renderer::Theme as StyleSheet>::Style,
|
||||
}
|
||||
|
||||
impl<Message, Renderer> Radio<Message, Renderer>
|
||||
where
|
||||
Message: Clone,
|
||||
Renderer: text::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
/// The default size of a [`Radio`] button.
|
||||
pub const DEFAULT_SIZE: f32 = 28.0;
|
||||
|
||||
/// The default spacing of a [`Radio`] button.
|
||||
pub const DEFAULT_SPACING: f32 = 15.0;
|
||||
|
||||
/// Creates a new [`Radio`] button.
|
||||
///
|
||||
/// It expects:
|
||||
/// * the value related to the [`Radio`] button
|
||||
/// * the label of the [`Radio`] button
|
||||
/// * the current selected value
|
||||
/// * a function that will be called when the [`Radio`] is selected. It
|
||||
/// receives the value of the radio and must produce a `Message`.
|
||||
pub fn new<F, V>(
|
||||
value: V,
|
||||
label: impl Into<String>,
|
||||
selected: Option<V>,
|
||||
f: F,
|
||||
) -> Self
|
||||
where
|
||||
V: Eq + Copy,
|
||||
F: FnOnce(V) -> Message,
|
||||
{
|
||||
Radio {
|
||||
is_selected: Some(value) == selected,
|
||||
on_click: f(value),
|
||||
label: label.into(),
|
||||
width: Length::Shrink,
|
||||
size: Self::DEFAULT_SIZE,
|
||||
spacing: Self::DEFAULT_SPACING, //15
|
||||
text_size: None,
|
||||
font: None,
|
||||
style: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the size of the [`Radio`] button.
|
||||
pub fn size(mut self, size: impl Into<Pixels>) -> Self {
|
||||
self.size = size.into().0;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the width of the [`Radio`] button.
|
||||
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||
self.width = width.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the spacing between the [`Radio`] button and the text.
|
||||
pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
|
||||
self.spacing = spacing.into().0;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the text size of the [`Radio`] button.
|
||||
pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self {
|
||||
self.text_size = Some(text_size.into().0);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the text font of the [`Radio`] button.
|
||||
pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
|
||||
self.font = Some(font.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the [`Radio`] button.
|
||||
pub fn style(
|
||||
mut self,
|
||||
style: impl Into<<Renderer::Theme as StyleSheet>::Style>,
|
||||
) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<Message, Renderer> Widget<Message, Renderer> for Radio<Message, Renderer>
|
||||
where
|
||||
Message: Clone,
|
||||
Renderer: text::Renderer,
|
||||
Renderer::Theme: StyleSheet + crate::text::StyleSheet,
|
||||
{
|
||||
fn width(&self) -> Length {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
Length::Shrink
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
Row::<(), Renderer>::new()
|
||||
.width(self.width)
|
||||
.spacing(self.spacing)
|
||||
.align_items(Alignment::Center)
|
||||
.push(Row::new().width(self.size).height(self.size))
|
||||
.push(Text::new(&self.label).width(self.width).size(
|
||||
self.text_size.unwrap_or_else(|| renderer.default_size()),
|
||||
))
|
||||
.layout(renderer, limits)
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
_state: &mut Tree,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
_renderer: &Renderer,
|
||||
_clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
match event {
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
|
||||
| Event::Touch(touch::Event::FingerPressed { .. }) => {
|
||||
if layout.bounds().contains(cursor_position) {
|
||||
shell.publish(self.on_click.clone());
|
||||
|
||||
return event::Status::Captured;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
event::Status::Ignored
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
_state: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
_renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
if layout.bounds().contains(cursor_position) {
|
||||
mouse::Interaction::Pointer
|
||||
} else {
|
||||
mouse::Interaction::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
_state: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
) {
|
||||
let bounds = layout.bounds();
|
||||
let is_mouse_over = bounds.contains(cursor_position);
|
||||
|
||||
let mut children = layout.children();
|
||||
|
||||
let custom_style = if is_mouse_over {
|
||||
theme.hovered(&self.style, self.is_selected)
|
||||
} else {
|
||||
theme.active(&self.style, self.is_selected)
|
||||
};
|
||||
|
||||
{
|
||||
let layout = children.next().unwrap();
|
||||
let bounds = layout.bounds();
|
||||
|
||||
let size = bounds.width;
|
||||
let dot_size = size / 2.0;
|
||||
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds,
|
||||
border_radius: (size / 2.0).into(),
|
||||
border_width: custom_style.border_width,
|
||||
border_color: custom_style.border_color,
|
||||
},
|
||||
custom_style.background,
|
||||
);
|
||||
|
||||
if self.is_selected {
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: Rectangle {
|
||||
x: bounds.x + dot_size / 2.0,
|
||||
y: bounds.y + dot_size / 2.0,
|
||||
width: bounds.width - dot_size,
|
||||
height: bounds.height - dot_size,
|
||||
},
|
||||
border_radius: (dot_size / 2.0).into(),
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
},
|
||||
custom_style.dot_color,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let label_layout = children.next().unwrap();
|
||||
|
||||
crate::text::draw(
|
||||
renderer,
|
||||
style,
|
||||
label_layout,
|
||||
&self.label,
|
||||
self.text_size,
|
||||
self.font,
|
||||
crate::text::Appearance {
|
||||
color: custom_style.text_color,
|
||||
},
|
||||
alignment::Horizontal::Left,
|
||||
alignment::Vertical::Center,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> From<Radio<Message, Renderer>>
|
||||
for Element<'a, Message, Renderer>
|
||||
where
|
||||
Message: 'a + Clone,
|
||||
Renderer: 'a + text::Renderer,
|
||||
Renderer::Theme: StyleSheet + crate::text::StyleSheet,
|
||||
{
|
||||
fn from(radio: Radio<Message, Renderer>) -> Element<'a, Message, Renderer> {
|
||||
Element::new(radio)
|
||||
}
|
||||
}
|
||||
253
widget/src/row.rs
Normal file
253
widget/src/row.rs
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
//! Distribute content horizontally.
|
||||
use crate::core::event::{self, Event};
|
||||
use crate::core::layout::{self, Layout};
|
||||
use crate::core::mouse;
|
||||
use crate::core::overlay;
|
||||
use crate::core::renderer;
|
||||
use crate::core::widget::{Operation, Tree};
|
||||
use crate::core::{
|
||||
Alignment, Clipboard, Element, Length, Padding, Pixels, Point, Rectangle,
|
||||
Shell, Widget,
|
||||
};
|
||||
|
||||
/// A container that distributes its contents horizontally.
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Row<'a, Message, Renderer = crate::Renderer> {
|
||||
spacing: f32,
|
||||
padding: Padding,
|
||||
width: Length,
|
||||
height: Length,
|
||||
align_items: Alignment,
|
||||
children: Vec<Element<'a, Message, Renderer>>,
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Row<'a, Message, Renderer> {
|
||||
/// Creates an empty [`Row`].
|
||||
pub fn new() -> Self {
|
||||
Self::with_children(Vec::new())
|
||||
}
|
||||
|
||||
/// Creates a [`Row`] with the given elements.
|
||||
pub fn with_children(
|
||||
children: Vec<Element<'a, Message, Renderer>>,
|
||||
) -> Self {
|
||||
Row {
|
||||
spacing: 0.0,
|
||||
padding: Padding::ZERO,
|
||||
width: Length::Shrink,
|
||||
height: Length::Shrink,
|
||||
align_items: Alignment::Start,
|
||||
children,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the horizontal spacing _between_ elements.
|
||||
///
|
||||
/// Custom margins per element do not exist in iced. You should use this
|
||||
/// method instead! While less flexible, it helps you keep spacing between
|
||||
/// elements consistent.
|
||||
pub fn spacing(mut self, amount: impl Into<Pixels>) -> Self {
|
||||
self.spacing = amount.into().0;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`Padding`] of the [`Row`].
|
||||
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
|
||||
self.padding = padding.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the width of the [`Row`].
|
||||
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||
self.width = width.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the height of the [`Row`].
|
||||
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
||||
self.height = height.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the vertical alignment of the contents of the [`Row`] .
|
||||
pub fn align_items(mut self, align: Alignment) -> Self {
|
||||
self.align_items = align;
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds an [`Element`] to the [`Row`].
|
||||
pub fn push(
|
||||
mut self,
|
||||
child: impl Into<Element<'a, Message, Renderer>>,
|
||||
) -> Self {
|
||||
self.children.push(child.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Default for Row<'a, Message, Renderer> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Widget<Message, Renderer>
|
||||
for Row<'a, Message, 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 width(&self) -> Length {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
let limits = limits.width(self.width).height(self.height);
|
||||
|
||||
layout::flex::resolve(
|
||||
layout::flex::Axis::Horizontal,
|
||||
renderer,
|
||||
&limits,
|
||||
self.padding,
|
||||
self.spacing,
|
||||
self.align_items,
|
||||
&self.children,
|
||||
)
|
||||
}
|
||||
|
||||
fn operate(
|
||||
&self,
|
||||
tree: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn Operation<Message>,
|
||||
) {
|
||||
operation.container(None, &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_position: Point,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
self.children
|
||||
.iter_mut()
|
||||
.zip(&mut tree.children)
|
||||
.zip(layout.children())
|
||||
.map(|((child, state), layout)| {
|
||||
child.as_widget_mut().on_event(
|
||||
state,
|
||||
event.clone(),
|
||||
layout,
|
||||
cursor_position,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
)
|
||||
})
|
||||
.fold(event::Status::Ignored, event::Status::merge)
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
self.children
|
||||
.iter()
|
||||
.zip(&tree.children)
|
||||
.zip(layout.children())
|
||||
.map(|((child, state), layout)| {
|
||||
child.as_widget().mouse_interaction(
|
||||
state,
|
||||
layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
renderer,
|
||||
)
|
||||
})
|
||||
.max()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
for ((child, state), layout) in self
|
||||
.children
|
||||
.iter()
|
||||
.zip(&tree.children)
|
||||
.zip(layout.children())
|
||||
{
|
||||
child.as_widget().draw(
|
||||
state,
|
||||
renderer,
|
||||
theme,
|
||||
style,
|
||||
layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn overlay<'b>(
|
||||
&'b mut self,
|
||||
tree: &'b mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
) -> Option<overlay::Element<'b, Message, Renderer>> {
|
||||
overlay::from_children(&mut self.children, tree, layout, renderer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> From<Row<'a, Message, Renderer>>
|
||||
for Element<'a, Message, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Renderer: crate::core::Renderer + 'a,
|
||||
{
|
||||
fn from(row: Row<'a, Message, Renderer>) -> Self {
|
||||
Self::new(row)
|
||||
}
|
||||
}
|
||||
147
widget/src/rule.rs
Normal file
147
widget/src/rule.rs
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
//! Display a horizontal or vertical rule for dividing content.
|
||||
use crate::core::layout;
|
||||
use crate::core::renderer;
|
||||
use crate::core::widget::Tree;
|
||||
use crate::core::{
|
||||
Color, Element, Layout, Length, Pixels, Point, Rectangle, Size, Widget,
|
||||
};
|
||||
|
||||
pub use crate::style::rule::{Appearance, FillMode, StyleSheet};
|
||||
|
||||
/// Display a horizontal or vertical rule for dividing content.
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Rule<Renderer = crate::Renderer>
|
||||
where
|
||||
Renderer: crate::core::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
width: Length,
|
||||
height: Length,
|
||||
is_horizontal: bool,
|
||||
style: <Renderer::Theme as StyleSheet>::Style,
|
||||
}
|
||||
|
||||
impl<Renderer> Rule<Renderer>
|
||||
where
|
||||
Renderer: crate::core::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
/// Creates a horizontal [`Rule`] with the given height.
|
||||
pub fn horizontal(height: impl Into<Pixels>) -> Self {
|
||||
Rule {
|
||||
width: Length::Fill,
|
||||
height: Length::Fixed(height.into().0),
|
||||
is_horizontal: true,
|
||||
style: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a vertical [`Rule`] with the given width.
|
||||
pub fn vertical(width: impl Into<Pixels>) -> Self {
|
||||
Rule {
|
||||
width: Length::Fixed(width.into().0),
|
||||
height: Length::Fill,
|
||||
is_horizontal: false,
|
||||
style: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the style of the [`Rule`].
|
||||
pub fn style(
|
||||
mut self,
|
||||
style: impl Into<<Renderer::Theme as StyleSheet>::Style>,
|
||||
) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<Message, Renderer> Widget<Message, Renderer> for Rule<Renderer>
|
||||
where
|
||||
Renderer: crate::core::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
fn width(&self) -> Length {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
_renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
let limits = limits.width(self.width).height(self.height);
|
||||
|
||||
layout::Node::new(limits.resolve(Size::ZERO))
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
_state: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
_style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
_cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
) {
|
||||
let bounds = layout.bounds();
|
||||
let style = theme.appearance(&self.style);
|
||||
|
||||
let bounds = if self.is_horizontal {
|
||||
let line_y = (bounds.y + (bounds.height / 2.0)
|
||||
- (style.width as f32 / 2.0))
|
||||
.round();
|
||||
|
||||
let (offset, line_width) = style.fill_mode.fill(bounds.width);
|
||||
let line_x = bounds.x + offset;
|
||||
|
||||
Rectangle {
|
||||
x: line_x,
|
||||
y: line_y,
|
||||
width: line_width,
|
||||
height: style.width as f32,
|
||||
}
|
||||
} else {
|
||||
let line_x = (bounds.x + (bounds.width / 2.0)
|
||||
- (style.width as f32 / 2.0))
|
||||
.round();
|
||||
|
||||
let (offset, line_height) = style.fill_mode.fill(bounds.height);
|
||||
let line_y = bounds.y + offset;
|
||||
|
||||
Rectangle {
|
||||
x: line_x,
|
||||
y: line_y,
|
||||
width: style.width as f32,
|
||||
height: line_height,
|
||||
}
|
||||
};
|
||||
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds,
|
||||
border_radius: style.radius.into(),
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
},
|
||||
style.color,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> From<Rule<Renderer>>
|
||||
for Element<'a, Message, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Renderer: 'a + crate::core::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
fn from(rule: Rule<Renderer>) -> Element<'a, Message, Renderer> {
|
||||
Element::new(rule)
|
||||
}
|
||||
}
|
||||
1325
widget/src/scrollable.rs
Normal file
1325
widget/src/scrollable.rs
Normal file
File diff suppressed because it is too large
Load diff
471
widget/src/slider.rs
Normal file
471
widget/src/slider.rs
Normal file
|
|
@ -0,0 +1,471 @@
|
|||
//! Display an interactive selector of a single value from a range of values.
|
||||
//!
|
||||
//! A [`Slider`] has some local [`State`].
|
||||
use crate::core::event::{self, Event};
|
||||
use crate::core::layout;
|
||||
use crate::core::mouse;
|
||||
use crate::core::renderer;
|
||||
use crate::core::touch;
|
||||
use crate::core::widget::tree::{self, Tree};
|
||||
use crate::core::{
|
||||
Background, Clipboard, Color, Element, Layout, Length, Pixels, Point,
|
||||
Rectangle, Shell, Size, Widget,
|
||||
};
|
||||
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
pub use iced_style::slider::{Appearance, Handle, HandleShape, StyleSheet};
|
||||
|
||||
/// An horizontal bar and a handle that selects a single value from a range of
|
||||
/// values.
|
||||
///
|
||||
/// A [`Slider`] will try to fill the horizontal space of its container.
|
||||
///
|
||||
/// The [`Slider`] range of numeric values is generic and its step size defaults
|
||||
/// to 1 unit.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # type Slider<'a, T, Message> =
|
||||
/// # iced_widget::Slider<'a, Message, T, iced_widget::renderer::Renderer<iced_widget::style::Theme>>;
|
||||
/// #
|
||||
/// #[derive(Clone)]
|
||||
/// pub enum Message {
|
||||
/// SliderChanged(f32),
|
||||
/// }
|
||||
///
|
||||
/// let value = 50.0;
|
||||
///
|
||||
/// Slider::new(0.0..=100.0, value, Message::SliderChanged);
|
||||
/// ```
|
||||
///
|
||||
/// 
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Slider<'a, T, Message, Renderer = crate::Renderer>
|
||||
where
|
||||
Renderer: crate::core::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
range: RangeInclusive<T>,
|
||||
step: T,
|
||||
value: T,
|
||||
on_change: Box<dyn Fn(T) -> Message + 'a>,
|
||||
on_release: Option<Message>,
|
||||
width: Length,
|
||||
height: f32,
|
||||
style: <Renderer::Theme as StyleSheet>::Style,
|
||||
}
|
||||
|
||||
impl<'a, T, Message, Renderer> Slider<'a, T, Message, Renderer>
|
||||
where
|
||||
T: Copy + From<u8> + std::cmp::PartialOrd,
|
||||
Message: Clone,
|
||||
Renderer: crate::core::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
/// The default height of a [`Slider`].
|
||||
pub const DEFAULT_HEIGHT: f32 = 22.0;
|
||||
|
||||
/// Creates a new [`Slider`].
|
||||
///
|
||||
/// It expects:
|
||||
/// * an inclusive range of possible values
|
||||
/// * the current value of the [`Slider`]
|
||||
/// * a function that will be called when the [`Slider`] is dragged.
|
||||
/// It receives the new value of the [`Slider`] and must produce a
|
||||
/// `Message`.
|
||||
pub fn new<F>(range: RangeInclusive<T>, value: T, on_change: F) -> Self
|
||||
where
|
||||
F: 'a + Fn(T) -> Message,
|
||||
{
|
||||
let value = if value >= *range.start() {
|
||||
value
|
||||
} else {
|
||||
*range.start()
|
||||
};
|
||||
|
||||
let value = if value <= *range.end() {
|
||||
value
|
||||
} else {
|
||||
*range.end()
|
||||
};
|
||||
|
||||
Slider {
|
||||
value,
|
||||
range,
|
||||
step: T::from(1),
|
||||
on_change: Box::new(on_change),
|
||||
on_release: None,
|
||||
width: Length::Fill,
|
||||
height: Self::DEFAULT_HEIGHT,
|
||||
style: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the release message of the [`Slider`].
|
||||
/// This is called when the mouse is released from the slider.
|
||||
///
|
||||
/// Typically, the user's interaction with the slider is finished when this message is produced.
|
||||
/// This is useful if you need to spawn a long-running task from the slider's result, where
|
||||
/// the default on_change message could create too many events.
|
||||
pub fn on_release(mut self, on_release: Message) -> Self {
|
||||
self.on_release = Some(on_release);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the width of the [`Slider`].
|
||||
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||
self.width = width.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the height of the [`Slider`].
|
||||
pub fn height(mut self, height: impl Into<Pixels>) -> Self {
|
||||
self.height = height.into().0;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the [`Slider`].
|
||||
pub fn style(
|
||||
mut self,
|
||||
style: impl Into<<Renderer::Theme as StyleSheet>::Style>,
|
||||
) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the step size of the [`Slider`].
|
||||
pub fn step(mut self, step: T) -> Self {
|
||||
self.step = step;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T, Message, Renderer> Widget<Message, Renderer>
|
||||
for Slider<'a, T, Message, Renderer>
|
||||
where
|
||||
T: Copy + Into<f64> + num_traits::FromPrimitive,
|
||||
Message: Clone,
|
||||
Renderer: crate::core::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
fn tag(&self) -> tree::Tag {
|
||||
tree::Tag::of::<State>()
|
||||
}
|
||||
|
||||
fn state(&self) -> tree::State {
|
||||
tree::State::new(State::new())
|
||||
}
|
||||
|
||||
fn width(&self) -> Length {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
Length::Shrink
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
_renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
let limits = limits.width(self.width).height(self.height);
|
||||
let size = limits.resolve(Size::ZERO);
|
||||
|
||||
layout::Node::new(size)
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
_renderer: &Renderer,
|
||||
_clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
update(
|
||||
event,
|
||||
layout,
|
||||
cursor_position,
|
||||
shell,
|
||||
tree.state.downcast_mut::<State>(),
|
||||
&mut self.value,
|
||||
&self.range,
|
||||
self.step,
|
||||
self.on_change.as_ref(),
|
||||
&self.on_release,
|
||||
)
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
_style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
) {
|
||||
draw(
|
||||
renderer,
|
||||
layout,
|
||||
cursor_position,
|
||||
tree.state.downcast_ref::<State>(),
|
||||
self.value,
|
||||
&self.range,
|
||||
theme,
|
||||
&self.style,
|
||||
)
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
_renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
mouse_interaction(
|
||||
layout,
|
||||
cursor_position,
|
||||
tree.state.downcast_ref::<State>(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T, Message, Renderer> From<Slider<'a, T, Message, Renderer>>
|
||||
for Element<'a, Message, Renderer>
|
||||
where
|
||||
T: 'a + Copy + Into<f64> + num_traits::FromPrimitive,
|
||||
Message: 'a + Clone,
|
||||
Renderer: 'a + crate::core::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
fn from(
|
||||
slider: Slider<'a, T, Message, Renderer>,
|
||||
) -> Element<'a, Message, Renderer> {
|
||||
Element::new(slider)
|
||||
}
|
||||
}
|
||||
|
||||
/// Processes an [`Event`] and updates the [`State`] of a [`Slider`]
|
||||
/// accordingly.
|
||||
pub fn update<Message, T>(
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
state: &mut State,
|
||||
value: &mut T,
|
||||
range: &RangeInclusive<T>,
|
||||
step: T,
|
||||
on_change: &dyn Fn(T) -> Message,
|
||||
on_release: &Option<Message>,
|
||||
) -> event::Status
|
||||
where
|
||||
T: Copy + Into<f64> + num_traits::FromPrimitive,
|
||||
Message: Clone,
|
||||
{
|
||||
let is_dragging = state.is_dragging;
|
||||
|
||||
let mut change = || {
|
||||
let bounds = layout.bounds();
|
||||
let new_value = if cursor_position.x <= bounds.x {
|
||||
*range.start()
|
||||
} else if cursor_position.x >= bounds.x + bounds.width {
|
||||
*range.end()
|
||||
} else {
|
||||
let step = step.into();
|
||||
let start = (*range.start()).into();
|
||||
let end = (*range.end()).into();
|
||||
|
||||
let percent = f64::from(cursor_position.x - bounds.x)
|
||||
/ f64::from(bounds.width);
|
||||
|
||||
let steps = (percent * (end - start) / step).round();
|
||||
let value = steps * step + start;
|
||||
|
||||
if let Some(value) = T::from_f64(value) {
|
||||
value
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if ((*value).into() - new_value.into()).abs() > f64::EPSILON {
|
||||
shell.publish((on_change)(new_value));
|
||||
|
||||
*value = new_value;
|
||||
}
|
||||
};
|
||||
|
||||
match event {
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
|
||||
| Event::Touch(touch::Event::FingerPressed { .. }) => {
|
||||
if layout.bounds().contains(cursor_position) {
|
||||
change();
|
||||
state.is_dragging = true;
|
||||
|
||||
return event::Status::Captured;
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
|
||||
| Event::Touch(touch::Event::FingerLifted { .. })
|
||||
| Event::Touch(touch::Event::FingerLost { .. }) => {
|
||||
if is_dragging {
|
||||
if let Some(on_release) = on_release.clone() {
|
||||
shell.publish(on_release);
|
||||
}
|
||||
state.is_dragging = false;
|
||||
|
||||
return event::Status::Captured;
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse::Event::CursorMoved { .. })
|
||||
| Event::Touch(touch::Event::FingerMoved { .. }) => {
|
||||
if is_dragging {
|
||||
change();
|
||||
|
||||
return event::Status::Captured;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
event::Status::Ignored
|
||||
}
|
||||
|
||||
/// Draws a [`Slider`].
|
||||
pub fn draw<T, R>(
|
||||
renderer: &mut R,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
state: &State,
|
||||
value: T,
|
||||
range: &RangeInclusive<T>,
|
||||
style_sheet: &dyn StyleSheet<Style = <R::Theme as StyleSheet>::Style>,
|
||||
style: &<R::Theme as StyleSheet>::Style,
|
||||
) where
|
||||
T: Into<f64> + Copy,
|
||||
R: crate::core::Renderer,
|
||||
R::Theme: StyleSheet,
|
||||
{
|
||||
let bounds = layout.bounds();
|
||||
let is_mouse_over = bounds.contains(cursor_position);
|
||||
|
||||
let style = if state.is_dragging {
|
||||
style_sheet.dragging(style)
|
||||
} else if is_mouse_over {
|
||||
style_sheet.hovered(style)
|
||||
} else {
|
||||
style_sheet.active(style)
|
||||
};
|
||||
|
||||
let rail_y = bounds.y + (bounds.height / 2.0).round();
|
||||
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: Rectangle {
|
||||
x: bounds.x,
|
||||
y: rail_y - 1.0,
|
||||
width: bounds.width,
|
||||
height: 2.0,
|
||||
},
|
||||
border_radius: 0.0.into(),
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
},
|
||||
style.rail_colors.0,
|
||||
);
|
||||
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: Rectangle {
|
||||
x: bounds.x,
|
||||
y: rail_y + 1.0,
|
||||
width: bounds.width,
|
||||
height: 2.0,
|
||||
},
|
||||
border_radius: 0.0.into(),
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
},
|
||||
Background::Color(style.rail_colors.1),
|
||||
);
|
||||
|
||||
let (handle_width, handle_height, handle_border_radius) = match style
|
||||
.handle
|
||||
.shape
|
||||
{
|
||||
HandleShape::Circle { radius } => (radius * 2.0, radius * 2.0, radius),
|
||||
HandleShape::Rectangle {
|
||||
width,
|
||||
border_radius,
|
||||
} => (f32::from(width), bounds.height, border_radius),
|
||||
};
|
||||
|
||||
let value = value.into() as f32;
|
||||
let (range_start, range_end) = {
|
||||
let (start, end) = range.clone().into_inner();
|
||||
|
||||
(start.into() as f32, end.into() as f32)
|
||||
};
|
||||
|
||||
let handle_offset = if range_start >= range_end {
|
||||
0.0
|
||||
} else {
|
||||
(bounds.width - handle_width) * (value - range_start)
|
||||
/ (range_end - range_start)
|
||||
};
|
||||
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: Rectangle {
|
||||
x: bounds.x + handle_offset.round(),
|
||||
y: rail_y - handle_height / 2.0,
|
||||
width: handle_width,
|
||||
height: handle_height,
|
||||
},
|
||||
border_radius: handle_border_radius.into(),
|
||||
border_width: style.handle.border_width,
|
||||
border_color: style.handle.border_color,
|
||||
},
|
||||
style.handle.color,
|
||||
);
|
||||
}
|
||||
|
||||
/// Computes the current [`mouse::Interaction`] of a [`Slider`].
|
||||
pub fn mouse_interaction(
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
state: &State,
|
||||
) -> mouse::Interaction {
|
||||
let bounds = layout.bounds();
|
||||
let is_mouse_over = bounds.contains(cursor_position);
|
||||
|
||||
if state.is_dragging {
|
||||
mouse::Interaction::Grabbing
|
||||
} else if is_mouse_over {
|
||||
mouse::Interaction::Grab
|
||||
} else {
|
||||
mouse::Interaction::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// The local state of a [`Slider`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct State {
|
||||
is_dragging: bool,
|
||||
}
|
||||
|
||||
impl State {
|
||||
/// Creates a new [`State`].
|
||||
pub fn new() -> State {
|
||||
State::default()
|
||||
}
|
||||
}
|
||||
86
widget/src/space.rs
Normal file
86
widget/src/space.rs
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
//! Distribute content vertically.
|
||||
use crate::core;
|
||||
use crate::core::layout;
|
||||
use crate::core::renderer;
|
||||
use crate::core::widget::Tree;
|
||||
use crate::core::{Element, Layout, Length, Point, Rectangle, Size, Widget};
|
||||
|
||||
/// An amount of empty space.
|
||||
///
|
||||
/// It can be useful if you want to fill some space with nothing.
|
||||
#[derive(Debug)]
|
||||
pub struct Space {
|
||||
width: Length,
|
||||
height: Length,
|
||||
}
|
||||
|
||||
impl Space {
|
||||
/// Creates an amount of empty [`Space`] with the given width and height.
|
||||
pub fn new(width: impl Into<Length>, height: impl Into<Length>) -> Self {
|
||||
Space {
|
||||
width: width.into(),
|
||||
height: height.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an amount of horizontal [`Space`].
|
||||
pub fn with_width(width: impl Into<Length>) -> Self {
|
||||
Space {
|
||||
width: width.into(),
|
||||
height: Length::Shrink,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an amount of vertical [`Space`].
|
||||
pub fn with_height(height: impl Into<Length>) -> Self {
|
||||
Space {
|
||||
width: Length::Shrink,
|
||||
height: height.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Message, Renderer> Widget<Message, Renderer> for Space
|
||||
where
|
||||
Renderer: core::Renderer,
|
||||
{
|
||||
fn width(&self) -> Length {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
_renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
let limits = limits.width(self.width).height(self.height);
|
||||
|
||||
layout::Node::new(limits.resolve(Size::ZERO))
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
_state: &Tree,
|
||||
_renderer: &mut Renderer,
|
||||
_theme: &Renderer::Theme,
|
||||
_style: &renderer::Style,
|
||||
_layout: Layout<'_>,
|
||||
_cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> From<Space> for Element<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: core::Renderer,
|
||||
Message: 'a,
|
||||
{
|
||||
fn from(space: Space) -> Element<'a, Message, Renderer> {
|
||||
Element::new(space)
|
||||
}
|
||||
}
|
||||
195
widget/src/svg.rs
Normal file
195
widget/src/svg.rs
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
//! Display vector graphics in your application.
|
||||
use crate::core::layout;
|
||||
use crate::core::renderer;
|
||||
use crate::core::svg;
|
||||
use crate::core::widget::Tree;
|
||||
use crate::core::{
|
||||
ContentFit, Element, Layout, Length, Point, Rectangle, Size, Vector, Widget,
|
||||
};
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub use crate::style::svg::{Appearance, StyleSheet};
|
||||
pub use svg::Handle;
|
||||
|
||||
/// A vector graphics image.
|
||||
///
|
||||
/// An [`Svg`] image resizes smoothly without losing any quality.
|
||||
///
|
||||
/// [`Svg`] images can have a considerable rendering cost when resized,
|
||||
/// specially when they are complex.
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Svg<Renderer = crate::Renderer>
|
||||
where
|
||||
Renderer: svg::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
handle: Handle,
|
||||
width: Length,
|
||||
height: Length,
|
||||
content_fit: ContentFit,
|
||||
style: <Renderer::Theme as StyleSheet>::Style,
|
||||
}
|
||||
|
||||
impl<Renderer> Svg<Renderer>
|
||||
where
|
||||
Renderer: svg::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
/// Creates a new [`Svg`] from the given [`Handle`].
|
||||
pub fn new(handle: impl Into<Handle>) -> Self {
|
||||
Svg {
|
||||
handle: handle.into(),
|
||||
width: Length::Fill,
|
||||
height: Length::Shrink,
|
||||
content_fit: ContentFit::Contain,
|
||||
style: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new [`Svg`] that will display the contents of the file at the
|
||||
/// provided path.
|
||||
#[must_use]
|
||||
pub fn from_path(path: impl Into<PathBuf>) -> Self {
|
||||
Self::new(Handle::from_path(path))
|
||||
}
|
||||
|
||||
/// Sets the width of the [`Svg`].
|
||||
#[must_use]
|
||||
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||
self.width = width.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the height of the [`Svg`].
|
||||
#[must_use]
|
||||
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
||||
self.height = height.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`ContentFit`] of the [`Svg`].
|
||||
///
|
||||
/// Defaults to [`ContentFit::Contain`]
|
||||
#[must_use]
|
||||
pub fn content_fit(self, content_fit: ContentFit) -> Self {
|
||||
Self {
|
||||
content_fit,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the style variant of this [`Svg`].
|
||||
#[must_use]
|
||||
pub fn style(
|
||||
mut self,
|
||||
style: <Renderer::Theme as StyleSheet>::Style,
|
||||
) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<Message, Renderer> Widget<Message, Renderer> for Svg<Renderer>
|
||||
where
|
||||
Renderer: svg::Renderer,
|
||||
Renderer::Theme: iced_style::svg::StyleSheet,
|
||||
{
|
||||
fn width(&self) -> Length {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
// The raw w/h of the underlying image
|
||||
let Size { width, height } = renderer.dimensions(&self.handle);
|
||||
let image_size = Size::new(width as f32, height as f32);
|
||||
|
||||
// The size to be available to the widget prior to `Shrink`ing
|
||||
let raw_size = limits
|
||||
.width(self.width)
|
||||
.height(self.height)
|
||||
.resolve(image_size);
|
||||
|
||||
// The uncropped size of the image when fit to the bounds above
|
||||
let full_size = self.content_fit.fit(image_size, raw_size);
|
||||
|
||||
// Shrink the widget to fit the resized image, if requested
|
||||
let final_size = Size {
|
||||
width: match self.width {
|
||||
Length::Shrink => f32::min(raw_size.width, full_size.width),
|
||||
_ => raw_size.width,
|
||||
},
|
||||
height: match self.height {
|
||||
Length::Shrink => f32::min(raw_size.height, full_size.height),
|
||||
_ => raw_size.height,
|
||||
},
|
||||
};
|
||||
|
||||
layout::Node::new(final_size)
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
_state: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
_style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
_cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
) {
|
||||
let Size { width, height } = renderer.dimensions(&self.handle);
|
||||
let image_size = Size::new(width as f32, height as f32);
|
||||
|
||||
let bounds = layout.bounds();
|
||||
let adjusted_fit = self.content_fit.fit(image_size, bounds.size());
|
||||
|
||||
let render = |renderer: &mut Renderer| {
|
||||
let offset = Vector::new(
|
||||
(bounds.width - adjusted_fit.width).max(0.0) / 2.0,
|
||||
(bounds.height - adjusted_fit.height).max(0.0) / 2.0,
|
||||
);
|
||||
|
||||
let drawing_bounds = Rectangle {
|
||||
width: adjusted_fit.width,
|
||||
height: adjusted_fit.height,
|
||||
..bounds
|
||||
};
|
||||
|
||||
let appearance = theme.appearance(&self.style);
|
||||
|
||||
renderer.draw(
|
||||
self.handle.clone(),
|
||||
appearance.color,
|
||||
drawing_bounds + offset,
|
||||
);
|
||||
};
|
||||
|
||||
if adjusted_fit.width > bounds.width
|
||||
|| adjusted_fit.height > bounds.height
|
||||
{
|
||||
renderer.with_layer(bounds, render);
|
||||
} else {
|
||||
render(renderer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> From<Svg<Renderer>>
|
||||
for Element<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: svg::Renderer + 'a,
|
||||
Renderer::Theme: iced_style::svg::StyleSheet,
|
||||
{
|
||||
fn from(icon: Svg<Renderer>) -> Element<'a, Message, Renderer> {
|
||||
Element::new(icon)
|
||||
}
|
||||
}
|
||||
4
widget/src/text.rs
Normal file
4
widget/src/text.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
pub use crate::core::widget::text::*;
|
||||
|
||||
pub type Text<'a, Renderer = crate::Renderer> =
|
||||
crate::core::widget::Text<'a, Renderer>;
|
||||
1221
widget/src/text_input.rs
Normal file
1221
widget/src/text_input.rs
Normal file
File diff suppressed because it is too large
Load diff
189
widget/src/text_input/cursor.rs
Normal file
189
widget/src/text_input/cursor.rs
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
//! Track the cursor of a text input.
|
||||
use crate::text_input::Value;
|
||||
|
||||
/// The cursor of a text input.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct Cursor {
|
||||
state: State,
|
||||
}
|
||||
|
||||
/// The state of a [`Cursor`].
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum State {
|
||||
/// Cursor without a selection
|
||||
Index(usize),
|
||||
|
||||
/// Cursor selecting a range of text
|
||||
Selection {
|
||||
/// The start of the selection
|
||||
start: usize,
|
||||
/// The end of the selection
|
||||
end: usize,
|
||||
},
|
||||
}
|
||||
|
||||
impl Default for Cursor {
|
||||
fn default() -> Self {
|
||||
Cursor {
|
||||
state: State::Index(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Cursor {
|
||||
/// Returns the [`State`] of the [`Cursor`].
|
||||
pub fn state(&self, value: &Value) -> State {
|
||||
match self.state {
|
||||
State::Index(index) => State::Index(index.min(value.len())),
|
||||
State::Selection { start, end } => {
|
||||
let start = start.min(value.len());
|
||||
let end = end.min(value.len());
|
||||
|
||||
if start == end {
|
||||
State::Index(start)
|
||||
} else {
|
||||
State::Selection { start, end }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the current selection of the [`Cursor`] for the given [`Value`].
|
||||
///
|
||||
/// `start` is guaranteed to be <= than `end`.
|
||||
pub fn selection(&self, value: &Value) -> Option<(usize, usize)> {
|
||||
match self.state(value) {
|
||||
State::Selection { start, end } => {
|
||||
Some((start.min(end), start.max(end)))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn move_to(&mut self, position: usize) {
|
||||
self.state = State::Index(position);
|
||||
}
|
||||
|
||||
pub(crate) fn move_right(&mut self, value: &Value) {
|
||||
self.move_right_by_amount(value, 1)
|
||||
}
|
||||
|
||||
pub(crate) fn move_right_by_words(&mut self, value: &Value) {
|
||||
self.move_to(value.next_end_of_word(self.right(value)))
|
||||
}
|
||||
|
||||
pub(crate) fn move_right_by_amount(
|
||||
&mut self,
|
||||
value: &Value,
|
||||
amount: usize,
|
||||
) {
|
||||
match self.state(value) {
|
||||
State::Index(index) => {
|
||||
self.move_to(index.saturating_add(amount).min(value.len()))
|
||||
}
|
||||
State::Selection { start, end } => self.move_to(end.max(start)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn move_left(&mut self, value: &Value) {
|
||||
match self.state(value) {
|
||||
State::Index(index) if index > 0 => self.move_to(index - 1),
|
||||
State::Selection { start, end } => self.move_to(start.min(end)),
|
||||
_ => self.move_to(0),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn move_left_by_words(&mut self, value: &Value) {
|
||||
self.move_to(value.previous_start_of_word(self.left(value)));
|
||||
}
|
||||
|
||||
pub(crate) fn select_range(&mut self, start: usize, end: usize) {
|
||||
if start == end {
|
||||
self.state = State::Index(start);
|
||||
} else {
|
||||
self.state = State::Selection { start, end };
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn select_left(&mut self, value: &Value) {
|
||||
match self.state(value) {
|
||||
State::Index(index) if index > 0 => {
|
||||
self.select_range(index, index - 1)
|
||||
}
|
||||
State::Selection { start, end } if end > 0 => {
|
||||
self.select_range(start, end - 1)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn select_right(&mut self, value: &Value) {
|
||||
match self.state(value) {
|
||||
State::Index(index) if index < value.len() => {
|
||||
self.select_range(index, index + 1)
|
||||
}
|
||||
State::Selection { start, end } if end < value.len() => {
|
||||
self.select_range(start, end + 1)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn select_left_by_words(&mut self, value: &Value) {
|
||||
match self.state(value) {
|
||||
State::Index(index) => {
|
||||
self.select_range(index, value.previous_start_of_word(index))
|
||||
}
|
||||
State::Selection { start, end } => {
|
||||
self.select_range(start, value.previous_start_of_word(end))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn select_right_by_words(&mut self, value: &Value) {
|
||||
match self.state(value) {
|
||||
State::Index(index) => {
|
||||
self.select_range(index, value.next_end_of_word(index))
|
||||
}
|
||||
State::Selection { start, end } => {
|
||||
self.select_range(start, value.next_end_of_word(end))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn select_all(&mut self, value: &Value) {
|
||||
self.select_range(0, value.len());
|
||||
}
|
||||
|
||||
pub(crate) fn start(&self, value: &Value) -> usize {
|
||||
let start = match self.state {
|
||||
State::Index(index) => index,
|
||||
State::Selection { start, .. } => start,
|
||||
};
|
||||
|
||||
start.min(value.len())
|
||||
}
|
||||
|
||||
pub(crate) fn end(&self, value: &Value) -> usize {
|
||||
let end = match self.state {
|
||||
State::Index(index) => index,
|
||||
State::Selection { end, .. } => end,
|
||||
};
|
||||
|
||||
end.min(value.len())
|
||||
}
|
||||
|
||||
fn left(&self, value: &Value) -> usize {
|
||||
match self.state(value) {
|
||||
State::Index(index) => index,
|
||||
State::Selection { start, end } => start.min(end),
|
||||
}
|
||||
}
|
||||
|
||||
fn right(&self, value: &Value) -> usize {
|
||||
match self.state(value) {
|
||||
State::Index(index) => index,
|
||||
State::Selection { start, end } => start.max(end),
|
||||
}
|
||||
}
|
||||
}
|
||||
70
widget/src/text_input/editor.rs
Normal file
70
widget/src/text_input/editor.rs
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
use crate::text_input::{Cursor, Value};
|
||||
|
||||
pub struct Editor<'a> {
|
||||
value: &'a mut Value,
|
||||
cursor: &'a mut Cursor,
|
||||
}
|
||||
|
||||
impl<'a> Editor<'a> {
|
||||
pub fn new(value: &'a mut Value, cursor: &'a mut Cursor) -> Editor<'a> {
|
||||
Editor { value, cursor }
|
||||
}
|
||||
|
||||
pub fn contents(&self) -> String {
|
||||
self.value.to_string()
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, character: char) {
|
||||
if let Some((left, right)) = self.cursor.selection(self.value) {
|
||||
self.cursor.move_left(self.value);
|
||||
self.value.remove_many(left, right);
|
||||
}
|
||||
|
||||
self.value.insert(self.cursor.end(self.value), character);
|
||||
self.cursor.move_right(self.value);
|
||||
}
|
||||
|
||||
pub fn paste(&mut self, content: Value) {
|
||||
let length = content.len();
|
||||
if let Some((left, right)) = self.cursor.selection(self.value) {
|
||||
self.cursor.move_left(self.value);
|
||||
self.value.remove_many(left, right);
|
||||
}
|
||||
|
||||
self.value.insert_many(self.cursor.end(self.value), content);
|
||||
|
||||
self.cursor.move_right_by_amount(self.value, length);
|
||||
}
|
||||
|
||||
pub fn backspace(&mut self) {
|
||||
match self.cursor.selection(self.value) {
|
||||
Some((start, end)) => {
|
||||
self.cursor.move_left(self.value);
|
||||
self.value.remove_many(start, end);
|
||||
}
|
||||
None => {
|
||||
let start = self.cursor.start(self.value);
|
||||
|
||||
if start > 0 {
|
||||
self.cursor.move_left(self.value);
|
||||
self.value.remove(start - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete(&mut self) {
|
||||
match self.cursor.selection(self.value) {
|
||||
Some(_) => {
|
||||
self.backspace();
|
||||
}
|
||||
None => {
|
||||
let end = self.cursor.end(self.value);
|
||||
|
||||
if end < self.value.len() {
|
||||
self.value.remove(end);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
133
widget/src/text_input/value.rs
Normal file
133
widget/src/text_input/value.rs
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
/// The value of a [`TextInput`].
|
||||
///
|
||||
/// [`TextInput`]: crate::widget::TextInput
|
||||
// TODO: Reduce allocations, cache results (?)
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Value {
|
||||
graphemes: Vec<String>,
|
||||
}
|
||||
|
||||
impl Value {
|
||||
/// Creates a new [`Value`] from a string slice.
|
||||
pub fn new(string: &str) -> Self {
|
||||
let graphemes = UnicodeSegmentation::graphemes(string, true)
|
||||
.map(String::from)
|
||||
.collect();
|
||||
|
||||
Self { graphemes }
|
||||
}
|
||||
|
||||
/// Returns whether the [`Value`] is empty or not.
|
||||
///
|
||||
/// A [`Value`] is empty when it contains no graphemes.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
|
||||
/// Returns the total amount of graphemes in the [`Value`].
|
||||
pub fn len(&self) -> usize {
|
||||
self.graphemes.len()
|
||||
}
|
||||
|
||||
/// Returns the position of the previous start of a word from the given
|
||||
/// grapheme `index`.
|
||||
pub fn previous_start_of_word(&self, index: usize) -> usize {
|
||||
let previous_string =
|
||||
&self.graphemes[..index.min(self.graphemes.len())].concat();
|
||||
|
||||
UnicodeSegmentation::split_word_bound_indices(previous_string as &str)
|
||||
.filter(|(_, word)| !word.trim_start().is_empty())
|
||||
.next_back()
|
||||
.map(|(i, previous_word)| {
|
||||
index
|
||||
- UnicodeSegmentation::graphemes(previous_word, true)
|
||||
.count()
|
||||
- UnicodeSegmentation::graphemes(
|
||||
&previous_string[i + previous_word.len()..] as &str,
|
||||
true,
|
||||
)
|
||||
.count()
|
||||
})
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Returns the position of the next end of a word from the given grapheme
|
||||
/// `index`.
|
||||
pub fn next_end_of_word(&self, index: usize) -> usize {
|
||||
let next_string = &self.graphemes[index..].concat();
|
||||
|
||||
UnicodeSegmentation::split_word_bound_indices(next_string as &str)
|
||||
.find(|(_, word)| !word.trim_start().is_empty())
|
||||
.map(|(i, next_word)| {
|
||||
index
|
||||
+ UnicodeSegmentation::graphemes(next_word, true).count()
|
||||
+ UnicodeSegmentation::graphemes(
|
||||
&next_string[..i] as &str,
|
||||
true,
|
||||
)
|
||||
.count()
|
||||
})
|
||||
.unwrap_or(self.len())
|
||||
}
|
||||
|
||||
/// Returns a new [`Value`] containing the graphemes from `start` until the
|
||||
/// given `end`.
|
||||
pub fn select(&self, start: usize, end: usize) -> Self {
|
||||
let graphemes =
|
||||
self.graphemes[start.min(self.len())..end.min(self.len())].to_vec();
|
||||
|
||||
Self { graphemes }
|
||||
}
|
||||
|
||||
/// Returns a new [`Value`] containing the graphemes until the given
|
||||
/// `index`.
|
||||
pub fn until(&self, index: usize) -> Self {
|
||||
let graphemes = self.graphemes[..index.min(self.len())].to_vec();
|
||||
|
||||
Self { graphemes }
|
||||
}
|
||||
|
||||
/// Converts the [`Value`] into a `String`.
|
||||
pub fn to_string(&self) -> String {
|
||||
self.graphemes.concat()
|
||||
}
|
||||
|
||||
/// Inserts a new `char` at the given grapheme `index`.
|
||||
pub fn insert(&mut self, index: usize, c: char) {
|
||||
self.graphemes.insert(index, c.to_string());
|
||||
|
||||
self.graphemes =
|
||||
UnicodeSegmentation::graphemes(&self.to_string() as &str, true)
|
||||
.map(String::from)
|
||||
.collect();
|
||||
}
|
||||
|
||||
/// Inserts a bunch of graphemes at the given grapheme `index`.
|
||||
pub fn insert_many(&mut self, index: usize, mut value: Value) {
|
||||
let _ = self
|
||||
.graphemes
|
||||
.splice(index..index, value.graphemes.drain(..));
|
||||
}
|
||||
|
||||
/// Removes the grapheme at the given `index`.
|
||||
pub fn remove(&mut self, index: usize) {
|
||||
let _ = self.graphemes.remove(index);
|
||||
}
|
||||
|
||||
/// Removes the graphemes from `start` to `end`.
|
||||
pub fn remove_many(&mut self, start: usize, end: usize) {
|
||||
let _ = self.graphemes.splice(start..end, std::iter::empty());
|
||||
}
|
||||
|
||||
/// Returns a new [`Value`] with all its graphemes replaced with the
|
||||
/// dot ('•') character.
|
||||
pub fn secure(&self) -> Self {
|
||||
Self {
|
||||
graphemes: std::iter::repeat(String::from("•"))
|
||||
.take(self.graphemes.len())
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
326
widget/src/toggler.rs
Normal file
326
widget/src/toggler.rs
Normal file
|
|
@ -0,0 +1,326 @@
|
|||
//! Show toggle controls using togglers.
|
||||
use crate::core::alignment;
|
||||
use crate::core::event;
|
||||
use crate::core::layout;
|
||||
use crate::core::mouse;
|
||||
use crate::core::renderer;
|
||||
use crate::core::text;
|
||||
use crate::core::widget::Tree;
|
||||
use crate::core::{
|
||||
Alignment, Clipboard, Element, Event, Layout, Length, Pixels, Point,
|
||||
Rectangle, Shell, Widget,
|
||||
};
|
||||
use crate::{Row, Text};
|
||||
|
||||
pub use crate::style::toggler::{Appearance, StyleSheet};
|
||||
|
||||
/// A toggler widget.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # type Toggler<'a, Message> =
|
||||
/// # iced_widget::Toggler<'a, Message, iced_widget::renderer::Renderer<iced_widget::style::Theme>>;
|
||||
/// #
|
||||
/// pub enum Message {
|
||||
/// TogglerToggled(bool),
|
||||
/// }
|
||||
///
|
||||
/// let is_toggled = true;
|
||||
///
|
||||
/// Toggler::new(String::from("Toggle me!"), is_toggled, |b| Message::TogglerToggled(b));
|
||||
/// ```
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Toggler<'a, Message, Renderer = crate::Renderer>
|
||||
where
|
||||
Renderer: text::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
is_toggled: bool,
|
||||
on_toggle: Box<dyn Fn(bool) -> Message + 'a>,
|
||||
label: Option<String>,
|
||||
width: Length,
|
||||
size: f32,
|
||||
text_size: Option<f32>,
|
||||
text_alignment: alignment::Horizontal,
|
||||
spacing: f32,
|
||||
font: Option<Renderer::Font>,
|
||||
style: <Renderer::Theme as StyleSheet>::Style,
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Toggler<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: text::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
/// The default size of a [`Toggler`].
|
||||
pub const DEFAULT_SIZE: f32 = 20.0;
|
||||
|
||||
/// Creates a new [`Toggler`].
|
||||
///
|
||||
/// It expects:
|
||||
/// * a boolean describing whether the [`Toggler`] is checked or not
|
||||
/// * An optional label for the [`Toggler`]
|
||||
/// * a function that will be called when the [`Toggler`] is toggled. It
|
||||
/// will receive the new state of the [`Toggler`] and must produce a
|
||||
/// `Message`.
|
||||
pub fn new<F>(
|
||||
label: impl Into<Option<String>>,
|
||||
is_toggled: bool,
|
||||
f: F,
|
||||
) -> Self
|
||||
where
|
||||
F: 'a + Fn(bool) -> Message,
|
||||
{
|
||||
Toggler {
|
||||
is_toggled,
|
||||
on_toggle: Box::new(f),
|
||||
label: label.into(),
|
||||
width: Length::Fill,
|
||||
size: Self::DEFAULT_SIZE,
|
||||
text_size: None,
|
||||
text_alignment: alignment::Horizontal::Left,
|
||||
spacing: 0.0,
|
||||
font: None,
|
||||
style: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the size of the [`Toggler`].
|
||||
pub fn size(mut self, size: impl Into<Pixels>) -> Self {
|
||||
self.size = size.into().0;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the width of the [`Toggler`].
|
||||
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||
self.width = width.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the text size o the [`Toggler`].
|
||||
pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self {
|
||||
self.text_size = Some(text_size.into().0);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the horizontal alignment of the text of the [`Toggler`]
|
||||
pub fn text_alignment(mut self, alignment: alignment::Horizontal) -> Self {
|
||||
self.text_alignment = alignment;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the spacing between the [`Toggler`] and the text.
|
||||
pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
|
||||
self.spacing = spacing.into().0;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`Font`] of the text of the [`Toggler`]
|
||||
///
|
||||
/// [`Font`]: crate::text::Renderer::Font
|
||||
pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
|
||||
self.font = Some(font.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the [`Toggler`].
|
||||
pub fn style(
|
||||
mut self,
|
||||
style: impl Into<<Renderer::Theme as StyleSheet>::Style>,
|
||||
) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Widget<Message, Renderer>
|
||||
for Toggler<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: text::Renderer,
|
||||
Renderer::Theme: StyleSheet + crate::text::StyleSheet,
|
||||
{
|
||||
fn width(&self) -> Length {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
Length::Shrink
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
let mut row = Row::<(), Renderer>::new()
|
||||
.width(self.width)
|
||||
.spacing(self.spacing)
|
||||
.align_items(Alignment::Center);
|
||||
|
||||
if let Some(label) = &self.label {
|
||||
row = row.push(
|
||||
Text::new(label)
|
||||
.horizontal_alignment(self.text_alignment)
|
||||
.font(self.font.unwrap_or_else(|| renderer.default_font()))
|
||||
.width(self.width)
|
||||
.size(
|
||||
self.text_size
|
||||
.unwrap_or_else(|| renderer.default_size()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
row = row.push(Row::new().width(2.0 * self.size).height(self.size));
|
||||
|
||||
row.layout(renderer, limits)
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
_state: &mut Tree,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
_renderer: &Renderer,
|
||||
_clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
match event {
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
|
||||
let mouse_over = layout.bounds().contains(cursor_position);
|
||||
|
||||
if mouse_over {
|
||||
shell.publish((self.on_toggle)(!self.is_toggled));
|
||||
|
||||
event::Status::Captured
|
||||
} else {
|
||||
event::Status::Ignored
|
||||
}
|
||||
}
|
||||
_ => event::Status::Ignored,
|
||||
}
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
_state: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
_renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
if layout.bounds().contains(cursor_position) {
|
||||
mouse::Interaction::Pointer
|
||||
} else {
|
||||
mouse::Interaction::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
_state: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
) {
|
||||
/// Makes sure that the border radius of the toggler looks good at every size.
|
||||
const BORDER_RADIUS_RATIO: f32 = 32.0 / 13.0;
|
||||
|
||||
/// The space ratio between the background Quad and the Toggler bounds, and
|
||||
/// between the background Quad and foreground Quad.
|
||||
const SPACE_RATIO: f32 = 0.05;
|
||||
|
||||
let mut children = layout.children();
|
||||
|
||||
if let Some(label) = &self.label {
|
||||
let label_layout = children.next().unwrap();
|
||||
|
||||
crate::text::draw(
|
||||
renderer,
|
||||
style,
|
||||
label_layout,
|
||||
label,
|
||||
self.text_size,
|
||||
self.font,
|
||||
Default::default(),
|
||||
self.text_alignment,
|
||||
alignment::Vertical::Center,
|
||||
);
|
||||
}
|
||||
|
||||
let toggler_layout = children.next().unwrap();
|
||||
let bounds = toggler_layout.bounds();
|
||||
|
||||
let is_mouse_over = bounds.contains(cursor_position);
|
||||
|
||||
let style = if is_mouse_over {
|
||||
theme.hovered(&self.style, self.is_toggled)
|
||||
} else {
|
||||
theme.active(&self.style, self.is_toggled)
|
||||
};
|
||||
|
||||
let border_radius = bounds.height / BORDER_RADIUS_RATIO;
|
||||
let space = SPACE_RATIO * bounds.height;
|
||||
|
||||
let toggler_background_bounds = Rectangle {
|
||||
x: bounds.x + space,
|
||||
y: bounds.y + space,
|
||||
width: bounds.width - (2.0 * space),
|
||||
height: bounds.height - (2.0 * space),
|
||||
};
|
||||
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: toggler_background_bounds,
|
||||
border_radius: border_radius.into(),
|
||||
border_width: 1.0,
|
||||
border_color: style
|
||||
.background_border
|
||||
.unwrap_or(style.background),
|
||||
},
|
||||
style.background,
|
||||
);
|
||||
|
||||
let toggler_foreground_bounds = Rectangle {
|
||||
x: bounds.x
|
||||
+ if self.is_toggled {
|
||||
bounds.width - 2.0 * space - (bounds.height - (4.0 * space))
|
||||
} else {
|
||||
2.0 * space
|
||||
},
|
||||
y: bounds.y + (2.0 * space),
|
||||
width: bounds.height - (4.0 * space),
|
||||
height: bounds.height - (4.0 * space),
|
||||
};
|
||||
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: toggler_foreground_bounds,
|
||||
border_radius: border_radius.into(),
|
||||
border_width: 1.0,
|
||||
border_color: style
|
||||
.foreground_border
|
||||
.unwrap_or(style.foreground),
|
||||
},
|
||||
style.foreground,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> From<Toggler<'a, Message, Renderer>>
|
||||
for Element<'a, Message, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Renderer: 'a + text::Renderer,
|
||||
Renderer::Theme: StyleSheet + crate::text::StyleSheet,
|
||||
{
|
||||
fn from(
|
||||
toggler: Toggler<'a, Message, Renderer>,
|
||||
) -> Element<'a, Message, Renderer> {
|
||||
Element::new(toggler)
|
||||
}
|
||||
}
|
||||
388
widget/src/tooltip.rs
Normal file
388
widget/src/tooltip.rs
Normal file
|
|
@ -0,0 +1,388 @@
|
|||
//! Display a widget over another.
|
||||
use crate::container;
|
||||
use crate::core;
|
||||
use crate::core::event::{self, Event};
|
||||
use crate::core::layout::{self, Layout};
|
||||
use crate::core::mouse;
|
||||
use crate::core::overlay;
|
||||
use crate::core::renderer;
|
||||
use crate::core::text;
|
||||
use crate::core::widget::Tree;
|
||||
use crate::core::{
|
||||
Clipboard, Element, Length, Padding, Pixels, Point, Rectangle, Shell, Size,
|
||||
Vector, Widget,
|
||||
};
|
||||
use crate::Text;
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// An element to display a widget over another.
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Tooltip<'a, Message, Renderer = crate::Renderer>
|
||||
where
|
||||
Renderer: text::Renderer,
|
||||
Renderer::Theme: container::StyleSheet + crate::text::StyleSheet,
|
||||
{
|
||||
content: Element<'a, Message, Renderer>,
|
||||
tooltip: Text<'a, Renderer>,
|
||||
position: Position,
|
||||
gap: f32,
|
||||
padding: f32,
|
||||
snap_within_viewport: bool,
|
||||
style: <Renderer::Theme as container::StyleSheet>::Style,
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Tooltip<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: text::Renderer,
|
||||
Renderer::Theme: container::StyleSheet + crate::text::StyleSheet,
|
||||
{
|
||||
/// The default padding of a [`Tooltip`] drawn by this renderer.
|
||||
const DEFAULT_PADDING: f32 = 5.0;
|
||||
|
||||
/// Creates a new [`Tooltip`].
|
||||
///
|
||||
/// [`Tooltip`]: struct.Tooltip.html
|
||||
pub fn new(
|
||||
content: impl Into<Element<'a, Message, Renderer>>,
|
||||
tooltip: impl Into<Cow<'a, str>>,
|
||||
position: Position,
|
||||
) -> Self {
|
||||
Tooltip {
|
||||
content: content.into(),
|
||||
tooltip: Text::new(tooltip),
|
||||
position,
|
||||
gap: 0.0,
|
||||
padding: Self::DEFAULT_PADDING,
|
||||
snap_within_viewport: true,
|
||||
style: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the size of the text of the [`Tooltip`].
|
||||
pub fn size(mut self, size: impl Into<Pixels>) -> Self {
|
||||
self.tooltip = self.tooltip.size(size);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the font of the [`Tooltip`].
|
||||
///
|
||||
/// [`Font`]: Renderer::Font
|
||||
pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
|
||||
self.tooltip = self.tooltip.font(font);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the gap between the content and its [`Tooltip`].
|
||||
pub fn gap(mut self, gap: impl Into<Pixels>) -> Self {
|
||||
self.gap = gap.into().0;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the padding of the [`Tooltip`].
|
||||
pub fn padding(mut self, padding: impl Into<Pixels>) -> Self {
|
||||
self.padding = padding.into().0;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets whether the [`Tooltip`] is snapped within the viewport.
|
||||
pub fn snap_within_viewport(mut self, snap: bool) -> Self {
|
||||
self.snap_within_viewport = snap;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the [`Tooltip`].
|
||||
pub fn style(
|
||||
mut self,
|
||||
style: impl Into<<Renderer::Theme as container::StyleSheet>::Style>,
|
||||
) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Widget<Message, Renderer>
|
||||
for Tooltip<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: text::Renderer,
|
||||
Renderer::Theme: container::StyleSheet + crate::text::StyleSheet,
|
||||
{
|
||||
fn children(&self) -> Vec<Tree> {
|
||||
vec![Tree::new(&self.content)]
|
||||
}
|
||||
|
||||
fn diff(&self, tree: &mut Tree) {
|
||||
tree.diff_children(std::slice::from_ref(&self.content))
|
||||
}
|
||||
|
||||
fn width(&self) -> Length {
|
||||
self.content.as_widget().width()
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
self.content.as_widget().height()
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
self.content.as_widget().layout(renderer, limits)
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
self.content.as_widget_mut().on_event(
|
||||
&mut tree.children[0],
|
||||
event,
|
||||
layout,
|
||||
cursor_position,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
)
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
self.content.as_widget().mouse_interaction(
|
||||
&tree.children[0],
|
||||
layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
renderer,
|
||||
)
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
inherited_style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
self.content.as_widget().draw(
|
||||
&tree.children[0],
|
||||
renderer,
|
||||
theme,
|
||||
inherited_style,
|
||||
layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
);
|
||||
|
||||
let tooltip = &self.tooltip;
|
||||
|
||||
draw(
|
||||
renderer,
|
||||
theme,
|
||||
inherited_style,
|
||||
layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
self.position,
|
||||
self.gap,
|
||||
self.padding,
|
||||
self.snap_within_viewport,
|
||||
&self.style,
|
||||
|renderer, limits| {
|
||||
Widget::<(), Renderer>::layout(tooltip, renderer, limits)
|
||||
},
|
||||
|renderer, defaults, layout, cursor_position, viewport| {
|
||||
Widget::<(), Renderer>::draw(
|
||||
tooltip,
|
||||
&Tree::empty(),
|
||||
renderer,
|
||||
theme,
|
||||
defaults,
|
||||
layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn overlay<'b>(
|
||||
&'b mut self,
|
||||
tree: &'b mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
) -> Option<overlay::Element<'b, Message, Renderer>> {
|
||||
self.content.as_widget_mut().overlay(
|
||||
&mut tree.children[0],
|
||||
layout,
|
||||
renderer,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> From<Tooltip<'a, Message, Renderer>>
|
||||
for Element<'a, Message, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Renderer: 'a + text::Renderer,
|
||||
Renderer::Theme: container::StyleSheet + crate::text::StyleSheet,
|
||||
{
|
||||
fn from(
|
||||
tooltip: Tooltip<'a, Message, Renderer>,
|
||||
) -> Element<'a, Message, Renderer> {
|
||||
Element::new(tooltip)
|
||||
}
|
||||
}
|
||||
|
||||
/// The position of the tooltip. Defaults to following the cursor.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Position {
|
||||
/// The tooltip will follow the cursor.
|
||||
FollowCursor,
|
||||
/// The tooltip will appear on the top of the widget.
|
||||
Top,
|
||||
/// The tooltip will appear on the bottom of the widget.
|
||||
Bottom,
|
||||
/// The tooltip will appear on the left of the widget.
|
||||
Left,
|
||||
/// The tooltip will appear on the right of the widget.
|
||||
Right,
|
||||
}
|
||||
|
||||
/// Draws a [`Tooltip`].
|
||||
pub fn draw<Renderer>(
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
inherited_style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
position: Position,
|
||||
gap: f32,
|
||||
padding: f32,
|
||||
snap_within_viewport: bool,
|
||||
style: &<Renderer::Theme as container::StyleSheet>::Style,
|
||||
layout_text: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node,
|
||||
draw_text: impl FnOnce(
|
||||
&mut Renderer,
|
||||
&renderer::Style,
|
||||
Layout<'_>,
|
||||
Point,
|
||||
&Rectangle,
|
||||
),
|
||||
) where
|
||||
Renderer: core::Renderer,
|
||||
Renderer::Theme: container::StyleSheet,
|
||||
{
|
||||
use container::StyleSheet;
|
||||
|
||||
let bounds = layout.bounds();
|
||||
|
||||
if bounds.contains(cursor_position) {
|
||||
let style = theme.appearance(style);
|
||||
|
||||
let defaults = renderer::Style {
|
||||
text_color: style.text_color.unwrap_or(inherited_style.text_color),
|
||||
};
|
||||
|
||||
let text_layout = layout_text(
|
||||
renderer,
|
||||
&layout::Limits::new(
|
||||
Size::ZERO,
|
||||
snap_within_viewport
|
||||
.then(|| viewport.size())
|
||||
.unwrap_or(Size::INFINITY),
|
||||
)
|
||||
.pad(Padding::new(padding)),
|
||||
);
|
||||
|
||||
let text_bounds = text_layout.bounds();
|
||||
let x_center = bounds.x + (bounds.width - text_bounds.width) / 2.0;
|
||||
let y_center = bounds.y + (bounds.height - text_bounds.height) / 2.0;
|
||||
|
||||
let mut tooltip_bounds = {
|
||||
let offset = match position {
|
||||
Position::Top => Vector::new(
|
||||
x_center,
|
||||
bounds.y - text_bounds.height - gap - padding,
|
||||
),
|
||||
Position::Bottom => Vector::new(
|
||||
x_center,
|
||||
bounds.y + bounds.height + gap + padding,
|
||||
),
|
||||
Position::Left => Vector::new(
|
||||
bounds.x - text_bounds.width - gap - padding,
|
||||
y_center,
|
||||
),
|
||||
Position::Right => Vector::new(
|
||||
bounds.x + bounds.width + gap + padding,
|
||||
y_center,
|
||||
),
|
||||
Position::FollowCursor => Vector::new(
|
||||
cursor_position.x,
|
||||
cursor_position.y - text_bounds.height,
|
||||
),
|
||||
};
|
||||
|
||||
Rectangle {
|
||||
x: offset.x - padding,
|
||||
y: offset.y - padding,
|
||||
width: text_bounds.width + padding * 2.0,
|
||||
height: text_bounds.height + padding * 2.0,
|
||||
}
|
||||
};
|
||||
|
||||
if snap_within_viewport {
|
||||
if tooltip_bounds.x < viewport.x {
|
||||
tooltip_bounds.x = viewport.x;
|
||||
} else if viewport.x + viewport.width
|
||||
< tooltip_bounds.x + tooltip_bounds.width
|
||||
{
|
||||
tooltip_bounds.x =
|
||||
viewport.x + viewport.width - tooltip_bounds.width;
|
||||
}
|
||||
|
||||
if tooltip_bounds.y < viewport.y {
|
||||
tooltip_bounds.y = viewport.y;
|
||||
} else if viewport.y + viewport.height
|
||||
< tooltip_bounds.y + tooltip_bounds.height
|
||||
{
|
||||
tooltip_bounds.y =
|
||||
viewport.y + viewport.height - tooltip_bounds.height;
|
||||
}
|
||||
}
|
||||
|
||||
renderer.with_layer(Rectangle::with_size(Size::INFINITY), |renderer| {
|
||||
container::draw_background(renderer, &style, tooltip_bounds);
|
||||
|
||||
draw_text(
|
||||
renderer,
|
||||
&defaults,
|
||||
Layout::with_offset(
|
||||
Vector::new(
|
||||
tooltip_bounds.x + padding,
|
||||
tooltip_bounds.y + padding,
|
||||
),
|
||||
&text_layout,
|
||||
),
|
||||
cursor_position,
|
||||
viewport,
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
471
widget/src/vertical_slider.rs
Normal file
471
widget/src/vertical_slider.rs
Normal file
|
|
@ -0,0 +1,471 @@
|
|||
//! Display an interactive selector of a single value from a range of values.
|
||||
//!
|
||||
//! A [`VerticalSlider`] has some local [`State`].
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
pub use crate::style::slider::{Appearance, Handle, HandleShape, StyleSheet};
|
||||
|
||||
use crate::core;
|
||||
use crate::core::event::{self, Event};
|
||||
use crate::core::layout::{self, Layout};
|
||||
use crate::core::mouse;
|
||||
use crate::core::renderer;
|
||||
use crate::core::touch;
|
||||
use crate::core::widget::tree::{self, Tree};
|
||||
use crate::core::{
|
||||
Background, Clipboard, Color, Element, Length, Pixels, Point, Rectangle,
|
||||
Shell, Size, Widget,
|
||||
};
|
||||
|
||||
/// An vertical bar and a handle that selects a single value from a range of
|
||||
/// values.
|
||||
///
|
||||
/// A [`VerticalSlider`] will try to fill the vertical space of its container.
|
||||
///
|
||||
/// The [`VerticalSlider`] range of numeric values is generic and its step size defaults
|
||||
/// to 1 unit.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # type VerticalSlider<'a, T, Message> =
|
||||
/// # iced_widget::VerticalSlider<'a, T, Message, iced_widget::renderer::Renderer<iced_widget::style::Theme>>;
|
||||
/// #
|
||||
/// #[derive(Clone)]
|
||||
/// pub enum Message {
|
||||
/// SliderChanged(f32),
|
||||
/// }
|
||||
///
|
||||
/// let value = 50.0;
|
||||
///
|
||||
/// VerticalSlider::new(0.0..=100.0, value, Message::SliderChanged);
|
||||
/// ```
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct VerticalSlider<'a, T, Message, Renderer = crate::Renderer>
|
||||
where
|
||||
Renderer: core::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
range: RangeInclusive<T>,
|
||||
step: T,
|
||||
value: T,
|
||||
on_change: Box<dyn Fn(T) -> Message + 'a>,
|
||||
on_release: Option<Message>,
|
||||
width: f32,
|
||||
height: Length,
|
||||
style: <Renderer::Theme as StyleSheet>::Style,
|
||||
}
|
||||
|
||||
impl<'a, T, Message, Renderer> VerticalSlider<'a, T, Message, Renderer>
|
||||
where
|
||||
T: Copy + From<u8> + std::cmp::PartialOrd,
|
||||
Message: Clone,
|
||||
Renderer: core::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
/// The default width of a [`VerticalSlider`].
|
||||
pub const DEFAULT_WIDTH: f32 = 22.0;
|
||||
|
||||
/// Creates a new [`VerticalSlider`].
|
||||
///
|
||||
/// It expects:
|
||||
/// * an inclusive range of possible values
|
||||
/// * the current value of the [`VerticalSlider`]
|
||||
/// * a function that will be called when the [`VerticalSlider`] is dragged.
|
||||
/// It receives the new value of the [`VerticalSlider`] and must produce a
|
||||
/// `Message`.
|
||||
pub fn new<F>(range: RangeInclusive<T>, value: T, on_change: F) -> Self
|
||||
where
|
||||
F: 'a + Fn(T) -> Message,
|
||||
{
|
||||
let value = if value >= *range.start() {
|
||||
value
|
||||
} else {
|
||||
*range.start()
|
||||
};
|
||||
|
||||
let value = if value <= *range.end() {
|
||||
value
|
||||
} else {
|
||||
*range.end()
|
||||
};
|
||||
|
||||
VerticalSlider {
|
||||
value,
|
||||
range,
|
||||
step: T::from(1),
|
||||
on_change: Box::new(on_change),
|
||||
on_release: None,
|
||||
width: Self::DEFAULT_WIDTH,
|
||||
height: Length::Fill,
|
||||
style: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the release message of the [`VerticalSlider`].
|
||||
/// This is called when the mouse is released from the slider.
|
||||
///
|
||||
/// Typically, the user's interaction with the slider is finished when this message is produced.
|
||||
/// This is useful if you need to spawn a long-running task from the slider's result, where
|
||||
/// the default on_change message could create too many events.
|
||||
pub fn on_release(mut self, on_release: Message) -> Self {
|
||||
self.on_release = Some(on_release);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the width of the [`VerticalSlider`].
|
||||
pub fn width(mut self, width: impl Into<Pixels>) -> Self {
|
||||
self.width = width.into().0;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the height of the [`VerticalSlider`].
|
||||
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
||||
self.height = height.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the [`VerticalSlider`].
|
||||
pub fn style(
|
||||
mut self,
|
||||
style: impl Into<<Renderer::Theme as StyleSheet>::Style>,
|
||||
) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the step size of the [`VerticalSlider`].
|
||||
pub fn step(mut self, step: T) -> Self {
|
||||
self.step = step;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T, Message, Renderer> Widget<Message, Renderer>
|
||||
for VerticalSlider<'a, T, Message, Renderer>
|
||||
where
|
||||
T: Copy + Into<f64> + num_traits::FromPrimitive,
|
||||
Message: Clone,
|
||||
Renderer: core::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
fn tag(&self) -> tree::Tag {
|
||||
tree::Tag::of::<State>()
|
||||
}
|
||||
|
||||
fn state(&self) -> tree::State {
|
||||
tree::State::new(State::new())
|
||||
}
|
||||
|
||||
fn width(&self) -> Length {
|
||||
Length::Shrink
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
_renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
let limits = limits.width(self.width).height(self.height);
|
||||
let size = limits.resolve(Size::ZERO);
|
||||
|
||||
layout::Node::new(size)
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
_renderer: &Renderer,
|
||||
_clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
update(
|
||||
event,
|
||||
layout,
|
||||
cursor_position,
|
||||
shell,
|
||||
tree.state.downcast_mut::<State>(),
|
||||
&mut self.value,
|
||||
&self.range,
|
||||
self.step,
|
||||
self.on_change.as_ref(),
|
||||
&self.on_release,
|
||||
)
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
_style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
) {
|
||||
draw(
|
||||
renderer,
|
||||
layout,
|
||||
cursor_position,
|
||||
tree.state.downcast_ref::<State>(),
|
||||
self.value,
|
||||
&self.range,
|
||||
theme,
|
||||
&self.style,
|
||||
)
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
_renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
mouse_interaction(
|
||||
layout,
|
||||
cursor_position,
|
||||
tree.state.downcast_ref::<State>(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T, Message, Renderer> From<VerticalSlider<'a, T, Message, Renderer>>
|
||||
for Element<'a, Message, Renderer>
|
||||
where
|
||||
T: 'a + Copy + Into<f64> + num_traits::FromPrimitive,
|
||||
Message: 'a + Clone,
|
||||
Renderer: 'a + core::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
fn from(
|
||||
slider: VerticalSlider<'a, T, Message, Renderer>,
|
||||
) -> Element<'a, Message, Renderer> {
|
||||
Element::new(slider)
|
||||
}
|
||||
}
|
||||
|
||||
/// Processes an [`Event`] and updates the [`State`] of a [`VerticalSlider`]
|
||||
/// accordingly.
|
||||
pub fn update<Message, T>(
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
state: &mut State,
|
||||
value: &mut T,
|
||||
range: &RangeInclusive<T>,
|
||||
step: T,
|
||||
on_change: &dyn Fn(T) -> Message,
|
||||
on_release: &Option<Message>,
|
||||
) -> event::Status
|
||||
where
|
||||
T: Copy + Into<f64> + num_traits::FromPrimitive,
|
||||
Message: Clone,
|
||||
{
|
||||
let is_dragging = state.is_dragging;
|
||||
|
||||
let mut change = || {
|
||||
let bounds = layout.bounds();
|
||||
let new_value = if cursor_position.y >= bounds.y + bounds.height {
|
||||
*range.start()
|
||||
} else if cursor_position.y <= bounds.y {
|
||||
*range.end()
|
||||
} else {
|
||||
let step = step.into();
|
||||
let start = (*range.start()).into();
|
||||
let end = (*range.end()).into();
|
||||
|
||||
let percent = 1.0
|
||||
- f64::from(cursor_position.y - bounds.y)
|
||||
/ f64::from(bounds.height);
|
||||
|
||||
let steps = (percent * (end - start) / step).round();
|
||||
let value = steps * step + start;
|
||||
|
||||
if let Some(value) = T::from_f64(value) {
|
||||
value
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if ((*value).into() - new_value.into()).abs() > f64::EPSILON {
|
||||
shell.publish((on_change)(new_value));
|
||||
|
||||
*value = new_value;
|
||||
}
|
||||
};
|
||||
|
||||
match event {
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
|
||||
| Event::Touch(touch::Event::FingerPressed { .. }) => {
|
||||
if layout.bounds().contains(cursor_position) {
|
||||
change();
|
||||
state.is_dragging = true;
|
||||
|
||||
return event::Status::Captured;
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
|
||||
| Event::Touch(touch::Event::FingerLifted { .. })
|
||||
| Event::Touch(touch::Event::FingerLost { .. }) => {
|
||||
if is_dragging {
|
||||
if let Some(on_release) = on_release.clone() {
|
||||
shell.publish(on_release);
|
||||
}
|
||||
state.is_dragging = false;
|
||||
|
||||
return event::Status::Captured;
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse::Event::CursorMoved { .. })
|
||||
| Event::Touch(touch::Event::FingerMoved { .. }) => {
|
||||
if is_dragging {
|
||||
change();
|
||||
|
||||
return event::Status::Captured;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
event::Status::Ignored
|
||||
}
|
||||
|
||||
/// Draws a [`VerticalSlider`].
|
||||
pub fn draw<T, R>(
|
||||
renderer: &mut R,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
state: &State,
|
||||
value: T,
|
||||
range: &RangeInclusive<T>,
|
||||
style_sheet: &dyn StyleSheet<Style = <R::Theme as StyleSheet>::Style>,
|
||||
style: &<R::Theme as StyleSheet>::Style,
|
||||
) where
|
||||
T: Into<f64> + Copy,
|
||||
R: core::Renderer,
|
||||
R::Theme: StyleSheet,
|
||||
{
|
||||
let bounds = layout.bounds();
|
||||
let is_mouse_over = bounds.contains(cursor_position);
|
||||
|
||||
let style = if state.is_dragging {
|
||||
style_sheet.dragging(style)
|
||||
} else if is_mouse_over {
|
||||
style_sheet.hovered(style)
|
||||
} else {
|
||||
style_sheet.active(style)
|
||||
};
|
||||
|
||||
let rail_x = bounds.x + (bounds.width / 2.0).round();
|
||||
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: Rectangle {
|
||||
x: rail_x - 1.0,
|
||||
y: bounds.y,
|
||||
width: 2.0,
|
||||
height: bounds.height,
|
||||
},
|
||||
border_radius: 0.0.into(),
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
},
|
||||
style.rail_colors.0,
|
||||
);
|
||||
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: Rectangle {
|
||||
x: rail_x + 1.0,
|
||||
y: bounds.y,
|
||||
width: 2.0,
|
||||
height: bounds.height,
|
||||
},
|
||||
border_radius: 0.0.into(),
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
},
|
||||
Background::Color(style.rail_colors.1),
|
||||
);
|
||||
|
||||
let (handle_width, handle_height, handle_border_radius) = match style
|
||||
.handle
|
||||
.shape
|
||||
{
|
||||
HandleShape::Circle { radius } => (radius * 2.0, radius * 2.0, radius),
|
||||
HandleShape::Rectangle {
|
||||
width,
|
||||
border_radius,
|
||||
} => (f32::from(width), bounds.width, border_radius),
|
||||
};
|
||||
|
||||
let value = value.into() as f32;
|
||||
let (range_start, range_end) = {
|
||||
let (start, end) = range.clone().into_inner();
|
||||
|
||||
(start.into() as f32, end.into() as f32)
|
||||
};
|
||||
|
||||
let handle_offset = if range_start >= range_end {
|
||||
0.0
|
||||
} else {
|
||||
(bounds.height - handle_width) * (value - range_end)
|
||||
/ (range_start - range_end)
|
||||
};
|
||||
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: Rectangle {
|
||||
x: rail_x - (handle_height / 2.0),
|
||||
y: bounds.y + handle_offset.round(),
|
||||
width: handle_height,
|
||||
height: handle_width,
|
||||
},
|
||||
border_radius: handle_border_radius.into(),
|
||||
border_width: style.handle.border_width,
|
||||
border_color: style.handle.border_color,
|
||||
},
|
||||
style.handle.color,
|
||||
);
|
||||
}
|
||||
|
||||
/// Computes the current [`mouse::Interaction`] of a [`VerticalSlider`].
|
||||
pub fn mouse_interaction(
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
state: &State,
|
||||
) -> mouse::Interaction {
|
||||
let bounds = layout.bounds();
|
||||
let is_mouse_over = bounds.contains(cursor_position);
|
||||
|
||||
if state.is_dragging {
|
||||
mouse::Interaction::Grabbing
|
||||
} else if is_mouse_over {
|
||||
mouse::Interaction::Grab
|
||||
} else {
|
||||
mouse::Interaction::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// The local state of a [`VerticalSlider`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct State {
|
||||
is_dragging: bool,
|
||||
}
|
||||
|
||||
impl State {
|
||||
/// Creates a new [`State`].
|
||||
pub fn new() -> State {
|
||||
State::default()
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue