Merge branch 'master' into beacon
This commit is contained in:
commit
8bd5de72ea
371 changed files with 33138 additions and 12950 deletions
|
|
@ -22,8 +22,10 @@ lazy = ["ouroboros"]
|
|||
image = ["iced_renderer/image"]
|
||||
svg = ["iced_renderer/svg"]
|
||||
canvas = ["iced_renderer/geometry"]
|
||||
qr_code = ["canvas", "qrcode"]
|
||||
qr_code = ["canvas", "dep:qrcode"]
|
||||
wgpu = ["iced_renderer/wgpu"]
|
||||
markdown = ["dep:pulldown-cmark", "dep:url"]
|
||||
highlighter = ["dep:iced_highlighter"]
|
||||
advanced = []
|
||||
|
||||
[dependencies]
|
||||
|
|
@ -31,6 +33,7 @@ iced_renderer.workspace = true
|
|||
iced_runtime.workspace = true
|
||||
|
||||
num-traits.workspace = true
|
||||
log.workspace = true
|
||||
rustc-hash.workspace = true
|
||||
thiserror.workspace = true
|
||||
unicode-segmentation.workspace = true
|
||||
|
|
@ -40,3 +43,12 @@ ouroboros.optional = true
|
|||
|
||||
qrcode.workspace = true
|
||||
qrcode.optional = true
|
||||
|
||||
pulldown-cmark.workspace = true
|
||||
pulldown-cmark.optional = true
|
||||
|
||||
iced_highlighter.workspace = true
|
||||
iced_highlighter.optional = true
|
||||
|
||||
url.workspace = true
|
||||
url.optional = true
|
||||
|
|
|
|||
2
widget/assets/iced-logo.svg
Normal file
2
widget/assets/iced-logo.svg
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="140" height="140" fill="none" version="1.1" viewBox="35 31 179 171"><rect x="42" y="31.001" width="169.9" height="169.9" rx="49.815" fill="url(#paint1_linear)"/><path d="m182.62 65.747-28.136 28.606-6.13-6.0291 28.136-28.606 6.13 6.0291zm-26.344 0.218-42.204 42.909-6.13-6.029 42.204-42.909 6.13 6.0291zm-61.648 23.913c5.3254-5.3831 10.65-10.765 21.569-21.867l6.13 6.0291c-10.927 11.11-16.258 16.498-21.587 21.885-4.4007 4.4488-8.8009 8.8968-16.359 16.573l31.977 8.358 25.968-26.402 6.13 6.0292-25.968 26.402 8.907 31.908 42.138-42.087 6.076 6.083-49.109 49.05-45.837-12.628-13.394-45.646 1.7714-1.801c10.928-11.111 16.258-16.499 21.588-21.886zm28.419 70.99-8.846-31.689-31.831-8.32 9.1945 31.335 31.482 8.674zm47.734-56.517 7.122-7.1221-6.08-6.0797-7.147 7.1474-30.171 30.674 6.13 6.029 30.146-30.649z" clip-rule="evenodd" fill="url(#paint2_linear)" fill-rule="evenodd"/><defs><filter id="filter0_f" x="55" y="47.001" width="144" height="168" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur" stdDeviation="2"/></filter><linearGradient id="paint0_linear" x1="127" x2="127" y1="51.001" y2="211" gradientUnits="userSpaceOnUse"><stop offset=".052083"/><stop stop-opacity=".08" offset="1"/></linearGradient><linearGradient id="paint1_linear" x1="212" x2="57.5" y1="31.001" y2="189" gradientUnits="userSpaceOnUse"><stop stop-color="#00A3FF" offset="0"/><stop stop-color="#30f" offset="1"/></linearGradient><linearGradient id="paint2_linear" x1="86.098" x2="206.01" y1="158.28" y2="35.327" gradientUnits="userSpaceOnUse"><stop stop-color="#fff" offset="0"/><stop stop-color="#fff" offset="1"/></linearGradient></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
85
widget/src/action.rs
Normal file
85
widget/src/action.rs
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
use crate::core::event;
|
||||
use crate::core::time::Instant;
|
||||
use crate::core::window;
|
||||
|
||||
/// A runtime action that can be performed by some widgets.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Action<Message> {
|
||||
message_to_publish: Option<Message>,
|
||||
redraw_request: window::RedrawRequest,
|
||||
event_status: event::Status,
|
||||
}
|
||||
|
||||
impl<Message> Action<Message> {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
message_to_publish: None,
|
||||
redraw_request: window::RedrawRequest::Wait,
|
||||
event_status: event::Status::Ignored,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new "capturing" [`Action`]. A capturing [`Action`]
|
||||
/// will make other widgets consider it final and prevent further
|
||||
/// processing.
|
||||
///
|
||||
/// Prevents "event bubbling".
|
||||
pub fn capture() -> Self {
|
||||
Self {
|
||||
event_status: event::Status::Captured,
|
||||
..Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new [`Action`] that publishes the given `Message` for
|
||||
/// the application to handle.
|
||||
///
|
||||
/// Publishing a `Message` always produces a redraw.
|
||||
pub fn publish(message: Message) -> Self {
|
||||
Self {
|
||||
message_to_publish: Some(message),
|
||||
..Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new [`Action`] that requests a redraw to happen as
|
||||
/// soon as possible; without publishing any `Message`.
|
||||
pub fn request_redraw() -> Self {
|
||||
Self {
|
||||
redraw_request: window::RedrawRequest::NextFrame,
|
||||
..Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new [`Action`] that requests a redraw to happen at
|
||||
/// the given [`Instant`]; without publishing any `Message`.
|
||||
///
|
||||
/// This can be useful to efficiently animate content, like a
|
||||
/// blinking caret on a text input.
|
||||
pub fn request_redraw_at(at: Instant) -> Self {
|
||||
Self {
|
||||
redraw_request: window::RedrawRequest::At(at),
|
||||
..Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Marks the [`Action`] as "capturing". See [`Self::capture`].
|
||||
pub fn and_capture(mut self) -> Self {
|
||||
self.event_status = event::Status::Captured;
|
||||
self
|
||||
}
|
||||
|
||||
/// Converts the [`Action`] into its internal parts.
|
||||
///
|
||||
/// This method is meant to be used by runtimes, libraries, or internal
|
||||
/// widget implementations.
|
||||
pub fn into_inner(
|
||||
self,
|
||||
) -> (Option<Message>, window::RedrawRequest, event::Status) {
|
||||
(
|
||||
self.message_to_publish,
|
||||
self.redraw_request,
|
||||
self.event_status,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,48 +1,71 @@
|
|||
//! Allow your users to perform actions by pressing a button.
|
||||
use crate::core::event::{self, Event};
|
||||
//! Buttons allow your users to perform actions by pressing them.
|
||||
//!
|
||||
//! # Example
|
||||
//! ```no_run
|
||||
//! # mod iced { pub mod widget { pub use iced_widget::*; } }
|
||||
//! # pub type State = ();
|
||||
//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||
//! use iced::widget::button;
|
||||
//!
|
||||
//! #[derive(Clone)]
|
||||
//! enum Message {
|
||||
//! ButtonPressed,
|
||||
//! }
|
||||
//!
|
||||
//! fn view(state: &State) -> Element<'_, Message> {
|
||||
//! button("Press me!").on_press(Message::ButtonPressed).into()
|
||||
//! }
|
||||
//! ```
|
||||
use crate::core::border::{self, Border};
|
||||
use crate::core::layout;
|
||||
use crate::core::mouse;
|
||||
use crate::core::overlay;
|
||||
use crate::core::renderer;
|
||||
use crate::core::theme::palette;
|
||||
use crate::core::touch;
|
||||
use crate::core::widget::tree::{self, Tree};
|
||||
use crate::core::widget::Operation;
|
||||
use crate::core::widget::tree::{self, Tree};
|
||||
use crate::core::window;
|
||||
use crate::core::{
|
||||
Background, Border, Clipboard, Color, Element, Layout, Length, Padding,
|
||||
Background, Clipboard, Color, Element, Event, Layout, Length, Padding,
|
||||
Rectangle, Shadow, Shell, Size, Theme, Vector, Widget,
|
||||
};
|
||||
|
||||
/// A generic widget that produces a message when pressed.
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// # type Button<'a, Message> = iced_widget::Button<'a, Message>;
|
||||
/// #
|
||||
/// # mod iced { pub mod widget { pub use iced_widget::*; } }
|
||||
/// # pub type State = ();
|
||||
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||
/// use iced::widget::button;
|
||||
///
|
||||
/// #[derive(Clone)]
|
||||
/// enum Message {
|
||||
/// ButtonPressed,
|
||||
/// }
|
||||
///
|
||||
/// let button = Button::new("Press me!").on_press(Message::ButtonPressed);
|
||||
/// fn view(state: &State) -> Element<'_, Message> {
|
||||
/// button("Press me!").on_press(Message::ButtonPressed).into()
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// If a [`Button::on_press`] handler is not set, the resulting [`Button`] will
|
||||
/// be disabled:
|
||||
///
|
||||
/// ```
|
||||
/// # type Button<'a, Message> = iced_widget::Button<'a, Message>;
|
||||
/// #
|
||||
/// ```no_run
|
||||
/// # mod iced { pub mod widget { pub use iced_widget::*; } }
|
||||
/// # pub type State = ();
|
||||
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||
/// use iced::widget::button;
|
||||
///
|
||||
/// #[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)
|
||||
/// fn view(state: &State) -> Element<'_, Message> {
|
||||
/// button("I am disabled!").into()
|
||||
/// }
|
||||
/// ```
|
||||
#[allow(missing_debug_implementations)]
|
||||
|
|
@ -52,12 +75,27 @@ where
|
|||
Theme: Catalog,
|
||||
{
|
||||
content: Element<'a, Message, Theme, Renderer>,
|
||||
on_press: Option<Message>,
|
||||
on_press: Option<OnPress<'a, Message>>,
|
||||
width: Length,
|
||||
height: Length,
|
||||
padding: Padding,
|
||||
clip: bool,
|
||||
class: Theme::Class<'a>,
|
||||
status: Option<Status>,
|
||||
}
|
||||
|
||||
enum OnPress<'a, Message> {
|
||||
Direct(Message),
|
||||
Closure(Box<dyn Fn() -> Message + 'a>),
|
||||
}
|
||||
|
||||
impl<Message: Clone> OnPress<'_, Message> {
|
||||
fn get(&self) -> Message {
|
||||
match self {
|
||||
OnPress::Direct(message) => message.clone(),
|
||||
OnPress::Closure(f) => f(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> Button<'a, Message, Theme, Renderer>
|
||||
|
|
@ -80,6 +118,7 @@ where
|
|||
padding: DEFAULT_PADDING,
|
||||
clip: false,
|
||||
class: Theme::default(),
|
||||
status: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -105,7 +144,23 @@ where
|
|||
///
|
||||
/// Unless `on_press` is called, the [`Button`] will be disabled.
|
||||
pub fn on_press(mut self, on_press: Message) -> Self {
|
||||
self.on_press = Some(on_press);
|
||||
self.on_press = Some(OnPress::Direct(on_press));
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the message that will be produced when the [`Button`] is pressed.
|
||||
///
|
||||
/// This is analogous to [`Button::on_press`], but using a closure to produce
|
||||
/// the message.
|
||||
///
|
||||
/// This closure will only be called when the [`Button`] is actually pressed and,
|
||||
/// therefore, this method is useful to reduce overhead if creating the resulting
|
||||
/// message is slow.
|
||||
pub fn on_press_with(
|
||||
mut self,
|
||||
on_press: impl Fn() -> Message + 'a,
|
||||
) -> Self {
|
||||
self.on_press = Some(OnPress::Closure(Box::new(on_press)));
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -114,7 +169,7 @@ where
|
|||
///
|
||||
/// If `None`, the [`Button`] will be disabled.
|
||||
pub fn on_press_maybe(mut self, on_press: Option<Message>) -> Self {
|
||||
self.on_press = on_press;
|
||||
self.on_press = on_press.map(OnPress::Direct);
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -205,7 +260,7 @@ where
|
|||
tree: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn Operation<Message>,
|
||||
operation: &mut dyn Operation,
|
||||
) {
|
||||
operation.container(None, layout.bounds(), &mut |operation| {
|
||||
self.content.as_widget().operate(
|
||||
|
|
@ -217,28 +272,30 @@ where
|
|||
});
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
fn update(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: Event,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
viewport: &Rectangle,
|
||||
) -> event::Status {
|
||||
if let event::Status::Captured = self.content.as_widget_mut().on_event(
|
||||
) {
|
||||
self.content.as_widget_mut().update(
|
||||
&mut tree.children[0],
|
||||
event.clone(),
|
||||
event,
|
||||
layout.children().next().unwrap(),
|
||||
cursor,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
viewport,
|
||||
) {
|
||||
return event::Status::Captured;
|
||||
);
|
||||
|
||||
if shell.is_event_captured() {
|
||||
return;
|
||||
}
|
||||
|
||||
match event {
|
||||
|
|
@ -252,13 +309,13 @@ where
|
|||
|
||||
state.is_pressed = true;
|
||||
|
||||
return event::Status::Captured;
|
||||
shell.capture_event();
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
|
||||
| Event::Touch(touch::Event::FingerLifted { .. }) => {
|
||||
if let Some(on_press) = self.on_press.clone() {
|
||||
if let Some(on_press) = &self.on_press {
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
|
||||
if state.is_pressed {
|
||||
|
|
@ -267,10 +324,10 @@ where
|
|||
let bounds = layout.bounds();
|
||||
|
||||
if cursor.is_over(bounds) {
|
||||
shell.publish(on_press);
|
||||
shell.publish(on_press.get());
|
||||
}
|
||||
|
||||
return event::Status::Captured;
|
||||
shell.capture_event();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -282,7 +339,25 @@ where
|
|||
_ => {}
|
||||
}
|
||||
|
||||
event::Status::Ignored
|
||||
let current_status = if self.on_press.is_none() {
|
||||
Status::Disabled
|
||||
} else if cursor.is_over(layout.bounds()) {
|
||||
let state = tree.state.downcast_ref::<State>();
|
||||
|
||||
if state.is_pressed {
|
||||
Status::Pressed
|
||||
} else {
|
||||
Status::Hovered
|
||||
}
|
||||
} else {
|
||||
Status::Active
|
||||
};
|
||||
|
||||
if let Event::Window(window::Event::RedrawRequested(_now)) = event {
|
||||
self.status = Some(current_status);
|
||||
} else if self.status.is_some_and(|status| status != current_status) {
|
||||
shell.request_redraw();
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(
|
||||
|
|
@ -297,23 +372,8 @@ where
|
|||
) {
|
||||
let bounds = layout.bounds();
|
||||
let content_layout = layout.children().next().unwrap();
|
||||
let is_mouse_over = cursor.is_over(bounds);
|
||||
|
||||
let status = if self.on_press.is_none() {
|
||||
Status::Disabled
|
||||
} else if is_mouse_over {
|
||||
let state = tree.state.downcast_ref::<State>();
|
||||
|
||||
if state.is_pressed {
|
||||
Status::Pressed
|
||||
} else {
|
||||
Status::Hovered
|
||||
}
|
||||
} else {
|
||||
Status::Active
|
||||
};
|
||||
|
||||
let style = theme.style(&self.class, status);
|
||||
let style =
|
||||
theme.style(&self.class, self.status.unwrap_or(Status::Disabled));
|
||||
|
||||
if style.background.is_some()
|
||||
|| style.border.width > 0.0
|
||||
|
|
@ -417,15 +477,18 @@ pub enum Status {
|
|||
}
|
||||
|
||||
/// The style of a button.
|
||||
///
|
||||
/// If not specified with [`Button::style`]
|
||||
/// the theme will provide the style.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Style {
|
||||
/// The [`Background`] of the button.
|
||||
pub background: Option<Background>,
|
||||
/// The text [`Color`] of the button.
|
||||
pub text_color: Color,
|
||||
/// The [`Border`] of the buton.
|
||||
/// The [`Border`] of the button.
|
||||
pub border: Border,
|
||||
/// The [`Shadow`] of the butoon.
|
||||
/// The [`Shadow`] of the button.
|
||||
pub shadow: Shadow,
|
||||
}
|
||||
|
||||
|
|
@ -451,6 +514,54 @@ impl Default for Style {
|
|||
}
|
||||
|
||||
/// The theme catalog of a [`Button`].
|
||||
///
|
||||
/// All themes that can be used with [`Button`]
|
||||
/// must implement this trait.
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// # use iced_widget::core::{Color, Background};
|
||||
/// # use iced_widget::button::{Catalog, Status, Style};
|
||||
/// # struct MyTheme;
|
||||
/// #[derive(Debug, Default)]
|
||||
/// pub enum ButtonClass {
|
||||
/// #[default]
|
||||
/// Primary,
|
||||
/// Secondary,
|
||||
/// Danger
|
||||
/// }
|
||||
///
|
||||
/// impl Catalog for MyTheme {
|
||||
/// type Class<'a> = ButtonClass;
|
||||
///
|
||||
/// fn default<'a>() -> Self::Class<'a> {
|
||||
/// ButtonClass::default()
|
||||
/// }
|
||||
///
|
||||
///
|
||||
/// fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
|
||||
/// let mut style = Style::default();
|
||||
///
|
||||
/// match class {
|
||||
/// ButtonClass::Primary => {
|
||||
/// style.background = Some(Background::Color(Color::from_rgb(0.529, 0.808, 0.921)));
|
||||
/// },
|
||||
/// ButtonClass::Secondary => {
|
||||
/// style.background = Some(Background::Color(Color::WHITE));
|
||||
/// },
|
||||
/// ButtonClass::Danger => {
|
||||
/// style.background = Some(Background::Color(Color::from_rgb(0.941, 0.502, 0.502)));
|
||||
/// },
|
||||
/// }
|
||||
///
|
||||
/// style
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Although, in order to use [`Button::style`]
|
||||
/// with `MyTheme`, [`Catalog::Class`] must implement
|
||||
/// `From<StyleFn<'_, MyTheme>>`.
|
||||
pub trait Catalog {
|
||||
/// The item class of the [`Catalog`].
|
||||
type Class<'a>;
|
||||
|
|
@ -480,12 +591,12 @@ impl Catalog for Theme {
|
|||
/// A primary button; denoting a main action.
|
||||
pub fn primary(theme: &Theme, status: Status) -> Style {
|
||||
let palette = theme.extended_palette();
|
||||
let base = styled(palette.primary.strong);
|
||||
let base = styled(palette.primary.base);
|
||||
|
||||
match status {
|
||||
Status::Active | Status::Pressed => base,
|
||||
Status::Hovered => Style {
|
||||
background: Some(Background::Color(palette.primary.base.color)),
|
||||
background: Some(Background::Color(palette.primary.strong.color)),
|
||||
..base
|
||||
},
|
||||
Status::Disabled => disabled(base),
|
||||
|
|
@ -522,6 +633,21 @@ pub fn success(theme: &Theme, status: Status) -> Style {
|
|||
}
|
||||
}
|
||||
|
||||
/// A warning button; denoting a risky action.
|
||||
pub fn warning(theme: &Theme, status: Status) -> Style {
|
||||
let palette = theme.extended_palette();
|
||||
let base = styled(palette.warning.base);
|
||||
|
||||
match status {
|
||||
Status::Active | Status::Pressed => base,
|
||||
Status::Hovered => Style {
|
||||
background: Some(Background::Color(palette.warning.strong.color)),
|
||||
..base
|
||||
},
|
||||
Status::Disabled => disabled(base),
|
||||
}
|
||||
}
|
||||
|
||||
/// A danger button; denoting a destructive action.
|
||||
pub fn danger(theme: &Theme, status: Status) -> Style {
|
||||
let palette = theme.extended_palette();
|
||||
|
|
@ -560,7 +686,7 @@ fn styled(pair: palette::Pair) -> Style {
|
|||
Style {
|
||||
background: Some(Background::Color(pair.color)),
|
||||
text_color: pair.text,
|
||||
border: Border::rounded(2),
|
||||
border: border::rounded(2),
|
||||
..Style::default()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,71 @@
|
|||
//! Draw 2D graphics for your users.
|
||||
pub mod event;
|
||||
|
||||
//! Canvases can be leveraged to draw interactive 2D graphics.
|
||||
//!
|
||||
//! # Example: Drawing a Simple Circle
|
||||
//! ```no_run
|
||||
//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
|
||||
//! # pub type State = ();
|
||||
//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||
//! #
|
||||
//! use iced::mouse;
|
||||
//! use iced::widget::canvas;
|
||||
//! use iced::{Color, Rectangle, Renderer, Theme};
|
||||
//!
|
||||
//! // First, we define the data we need for drawing
|
||||
//! #[derive(Debug)]
|
||||
//! struct Circle {
|
||||
//! radius: f32,
|
||||
//! }
|
||||
//!
|
||||
//! // Then, we implement the `Program` trait
|
||||
//! impl<Message> canvas::Program<Message> for Circle {
|
||||
//! // No internal state
|
||||
//! type State = ();
|
||||
//!
|
||||
//! fn draw(
|
||||
//! &self,
|
||||
//! _state: &(),
|
||||
//! renderer: &Renderer,
|
||||
//! _theme: &Theme,
|
||||
//! bounds: Rectangle,
|
||||
//! _cursor: mouse::Cursor
|
||||
//! ) -> Vec<canvas::Geometry> {
|
||||
//! // We prepare a new `Frame`
|
||||
//! let mut frame = canvas::Frame::new(renderer, bounds.size());
|
||||
//!
|
||||
//! // We create a `Path` representing a simple circle
|
||||
//! let circle = canvas::Path::circle(frame.center(), self.radius);
|
||||
//!
|
||||
//! // And fill it with some color
|
||||
//! frame.fill(&circle, Color::BLACK);
|
||||
//!
|
||||
//! // Then, we produce the geometry
|
||||
//! vec![frame.into_geometry()]
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! // Finally, we simply use our `Circle` to create the `Canvas`!
|
||||
//! fn view<'a, Message: 'a>(_state: &'a State) -> Element<'a, Message> {
|
||||
//! canvas(Circle { radius: 50.0 }).into()
|
||||
//! }
|
||||
//! ```
|
||||
mod program;
|
||||
|
||||
pub use event::Event;
|
||||
pub use program::Program;
|
||||
|
||||
pub use crate::Action;
|
||||
pub use crate::core::event::Event;
|
||||
pub use crate::graphics::cache::Group;
|
||||
pub use crate::graphics::geometry::{
|
||||
fill, gradient, path, stroke, Fill, Gradient, LineCap, LineDash, LineJoin,
|
||||
Path, Stroke, Style, Text,
|
||||
Fill, Gradient, Image, LineCap, LineDash, LineJoin, Path, Stroke, Style,
|
||||
Text, fill, gradient, path, stroke,
|
||||
};
|
||||
|
||||
use crate::core;
|
||||
use crate::core::event;
|
||||
use crate::core::layout::{self, Layout};
|
||||
use crate::core::mouse;
|
||||
use crate::core::renderer;
|
||||
use crate::core::widget::tree::{self, Tree};
|
||||
use crate::core::window;
|
||||
use crate::core::{
|
||||
Clipboard, Element, Length, Rectangle, Shell, Size, Vector, Widget,
|
||||
};
|
||||
|
|
@ -39,15 +88,16 @@ pub type Frame<Renderer = crate::Renderer> = geometry::Frame<Renderer>;
|
|||
|
||||
/// 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:
|
||||
///
|
||||
/// # Example: Drawing a Simple Circle
|
||||
/// ```no_run
|
||||
/// # use iced_widget::canvas::{self, Canvas, Fill, Frame, Geometry, Path, Program};
|
||||
/// # use iced_widget::core::{Color, Rectangle};
|
||||
/// # use iced_widget::core::mouse;
|
||||
/// # use iced_widget::{Renderer, Theme};
|
||||
/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
|
||||
/// # pub type State = ();
|
||||
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||
/// #
|
||||
/// use iced::mouse;
|
||||
/// use iced::widget::canvas;
|
||||
/// use iced::{Color, Rectangle, Renderer, Theme};
|
||||
///
|
||||
/// // First, we define the data we need for drawing
|
||||
/// #[derive(Debug)]
|
||||
/// struct Circle {
|
||||
|
|
@ -55,26 +105,36 @@ pub type Frame<Renderer = crate::Renderer> = geometry::Frame<Renderer>;
|
|||
/// }
|
||||
///
|
||||
/// // Then, we implement the `Program` trait
|
||||
/// impl Program<()> for Circle {
|
||||
/// impl<Message> canvas::Program<Message> for Circle {
|
||||
/// // No internal state
|
||||
/// type State = ();
|
||||
///
|
||||
/// fn draw(&self, _state: &(), renderer: &Renderer, _theme: &Theme, bounds: Rectangle, _cursor: mouse::Cursor) -> Vec<Geometry> {
|
||||
/// fn draw(
|
||||
/// &self,
|
||||
/// _state: &(),
|
||||
/// renderer: &Renderer,
|
||||
/// _theme: &Theme,
|
||||
/// bounds: Rectangle,
|
||||
/// _cursor: mouse::Cursor
|
||||
/// ) -> Vec<canvas::Geometry> {
|
||||
/// // We prepare a new `Frame`
|
||||
/// let mut frame = Frame::new(renderer, bounds.size());
|
||||
/// let mut frame = canvas::Frame::new(renderer, bounds.size());
|
||||
///
|
||||
/// // We create a `Path` representing a simple circle
|
||||
/// let circle = Path::circle(frame.center(), self.radius);
|
||||
/// let circle = canvas::Path::circle(frame.center(), self.radius);
|
||||
///
|
||||
/// // And fill it with some color
|
||||
/// frame.fill(&circle, Color::BLACK);
|
||||
///
|
||||
/// // Finally, we produce the geometry
|
||||
/// // Then, 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 });
|
||||
/// fn view<'a, Message: 'a>(_state: &'a State) -> Element<'a, Message> {
|
||||
/// canvas(Circle { radius: 50.0 }).into()
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Debug)]
|
||||
pub struct Canvas<P, Message, Theme = crate::Theme, Renderer = crate::Renderer>
|
||||
|
|
@ -88,6 +148,7 @@ where
|
|||
message_: PhantomData<Message>,
|
||||
theme_: PhantomData<Theme>,
|
||||
renderer_: PhantomData<Renderer>,
|
||||
last_mouse_interaction: Option<mouse::Interaction>,
|
||||
}
|
||||
|
||||
impl<P, Message, Theme, Renderer> Canvas<P, Message, Theme, Renderer>
|
||||
|
|
@ -106,6 +167,7 @@ where
|
|||
message_: PhantomData,
|
||||
theme_: PhantomData,
|
||||
renderer_: PhantomData,
|
||||
last_mouse_interaction: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -153,42 +215,54 @@ where
|
|||
layout::atomic(limits, self.width, self.height)
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
fn update(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: core::Event,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
_renderer: &Renderer,
|
||||
renderer: &Renderer,
|
||||
_clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
_viewport: &Rectangle,
|
||||
) -> event::Status {
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
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 state = tree.state.downcast_mut::<P::State>();
|
||||
let is_redraw_request = matches!(
|
||||
event,
|
||||
Event::Window(window::Event::RedrawRequested(_now)),
|
||||
);
|
||||
|
||||
if let Some(canvas_event) = canvas_event {
|
||||
let state = tree.state.downcast_mut::<P::State>();
|
||||
if let Some(action) = self.program.update(state, event, bounds, cursor)
|
||||
{
|
||||
let (message, redraw_request, event_status) = action.into_inner();
|
||||
|
||||
let (event_status, message) =
|
||||
self.program.update(state, canvas_event, bounds, cursor);
|
||||
shell.request_redraw_at(redraw_request);
|
||||
|
||||
if let Some(message) = message {
|
||||
shell.publish(message);
|
||||
}
|
||||
|
||||
return event_status;
|
||||
if event_status == event::Status::Captured {
|
||||
shell.capture_event();
|
||||
}
|
||||
}
|
||||
|
||||
event::Status::Ignored
|
||||
if shell.redraw_request() != window::RedrawRequest::NextFrame {
|
||||
let mouse_interaction = self
|
||||
.mouse_interaction(tree, layout, cursor, viewport, renderer);
|
||||
|
||||
if is_redraw_request {
|
||||
self.last_mouse_interaction = Some(mouse_interaction);
|
||||
} else if self.last_mouse_interaction.is_some_and(
|
||||
|last_mouse_interaction| {
|
||||
last_mouse_interaction != mouse_interaction
|
||||
},
|
||||
) {
|
||||
shell.request_redraw();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
//! 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::Canvas
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Event {
|
||||
/// A mouse event.
|
||||
Mouse(mouse::Event),
|
||||
|
||||
/// A touch event.
|
||||
Touch(touch::Event),
|
||||
|
||||
/// A keyboard event.
|
||||
Keyboard(keyboard::Event),
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
use crate::canvas::event::{self, Event};
|
||||
use crate::Action;
|
||||
use crate::canvas::mouse;
|
||||
use crate::canvas::Geometry;
|
||||
use crate::canvas::{Event, Geometry};
|
||||
use crate::core::Rectangle;
|
||||
use crate::graphics::geometry;
|
||||
|
||||
|
|
@ -22,8 +22,9 @@ where
|
|||
/// 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.
|
||||
/// This method can optionally return an [`Action`] to either notify an
|
||||
/// application of any meaningful interactions, capture the event, or
|
||||
/// request a redraw.
|
||||
///
|
||||
/// By default, this method does and returns nothing.
|
||||
///
|
||||
|
|
@ -31,11 +32,11 @@ where
|
|||
fn update(
|
||||
&self,
|
||||
_state: &mut Self::State,
|
||||
_event: Event,
|
||||
_event: &Event,
|
||||
_bounds: Rectangle,
|
||||
_cursor: mouse::Cursor,
|
||||
) -> (event::Status, Option<Message>) {
|
||||
(event::Status::Ignored, None)
|
||||
) -> Option<Action<Message>> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Draws the state of the [`Program`], producing a bunch of [`Geometry`].
|
||||
|
|
@ -81,10 +82,10 @@ where
|
|||
fn update(
|
||||
&self,
|
||||
state: &mut Self::State,
|
||||
event: Event,
|
||||
event: &Event,
|
||||
bounds: Rectangle,
|
||||
cursor: mouse::Cursor,
|
||||
) -> (event::Status, Option<Message>) {
|
||||
) -> Option<Action<Message>> {
|
||||
T::update(self, state, event, bounds, cursor)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,36 @@
|
|||
//! Show toggle controls using checkboxes.
|
||||
//! Checkboxes can be used to let users make binary choices.
|
||||
//!
|
||||
//! # Example
|
||||
//! ```no_run
|
||||
//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
|
||||
//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||
//! #
|
||||
//! use iced::widget::checkbox;
|
||||
//!
|
||||
//! struct State {
|
||||
//! is_checked: bool,
|
||||
//! }
|
||||
//!
|
||||
//! enum Message {
|
||||
//! CheckboxToggled(bool),
|
||||
//! }
|
||||
//!
|
||||
//! fn view(state: &State) -> Element<'_, Message> {
|
||||
//! checkbox("Toggle me!", state.is_checked)
|
||||
//! .on_toggle(Message::CheckboxToggled)
|
||||
//! .into()
|
||||
//! }
|
||||
//!
|
||||
//! fn update(state: &mut State, message: Message) {
|
||||
//! match message {
|
||||
//! Message::CheckboxToggled(is_checked) => {
|
||||
//! state.is_checked = is_checked;
|
||||
//! }
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
//! 
|
||||
use crate::core::alignment;
|
||||
use crate::core::event::{self, Event};
|
||||
use crate::core::layout;
|
||||
use crate::core::mouse;
|
||||
use crate::core::renderer;
|
||||
|
|
@ -9,27 +39,43 @@ use crate::core::theme::palette;
|
|||
use crate::core::touch;
|
||||
use crate::core::widget;
|
||||
use crate::core::widget::tree::{self, Tree};
|
||||
use crate::core::window;
|
||||
use crate::core::{
|
||||
Background, Border, Clipboard, Color, Element, Layout, Length, Pixels,
|
||||
Rectangle, Shell, Size, Theme, Widget,
|
||||
Background, Border, Clipboard, Color, Element, Event, Layout, Length,
|
||||
Pixels, Rectangle, Shell, Size, Theme, Widget,
|
||||
};
|
||||
|
||||
/// A box that can be checked.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// # type Checkbox<'a, Message> = iced_widget::Checkbox<'a, Message>;
|
||||
/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
|
||||
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||
/// #
|
||||
/// pub enum Message {
|
||||
/// use iced::widget::checkbox;
|
||||
///
|
||||
/// struct State {
|
||||
/// is_checked: bool,
|
||||
/// }
|
||||
///
|
||||
/// enum Message {
|
||||
/// CheckboxToggled(bool),
|
||||
/// }
|
||||
///
|
||||
/// let is_checked = true;
|
||||
/// fn view(state: &State) -> Element<'_, Message> {
|
||||
/// checkbox("Toggle me!", state.is_checked)
|
||||
/// .on_toggle(Message::CheckboxToggled)
|
||||
/// .into()
|
||||
/// }
|
||||
///
|
||||
/// Checkbox::new("Toggle me!", is_checked).on_toggle(Message::CheckboxToggled);
|
||||
/// fn update(state: &mut State, message: Message) {
|
||||
/// match message {
|
||||
/// Message::CheckboxToggled(is_checked) => {
|
||||
/// state.is_checked = is_checked;
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// 
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Checkbox<
|
||||
|
|
@ -50,9 +96,11 @@ pub struct Checkbox<
|
|||
text_size: Option<Pixels>,
|
||||
text_line_height: text::LineHeight,
|
||||
text_shaping: text::Shaping,
|
||||
text_wrapping: text::Wrapping,
|
||||
font: Option<Renderer::Font>,
|
||||
icon: Icon<Renderer::Font>,
|
||||
class: Theme::Class<'a>,
|
||||
last_status: Option<Status>,
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> Checkbox<'a, Message, Theme, Renderer>
|
||||
|
|
@ -81,7 +129,8 @@ where
|
|||
spacing: Self::DEFAULT_SPACING,
|
||||
text_size: None,
|
||||
text_line_height: text::LineHeight::default(),
|
||||
text_shaping: text::Shaping::Basic,
|
||||
text_shaping: text::Shaping::default(),
|
||||
text_wrapping: text::Wrapping::default(),
|
||||
font: None,
|
||||
icon: Icon {
|
||||
font: Renderer::ICON_FONT,
|
||||
|
|
@ -91,6 +140,7 @@ where
|
|||
shaping: text::Shaping::Basic,
|
||||
},
|
||||
class: Theme::default(),
|
||||
last_status: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -158,6 +208,12 @@ where
|
|||
self
|
||||
}
|
||||
|
||||
/// Sets the [`text::Wrapping`] strategy of the [`Checkbox`].
|
||||
pub fn text_wrapping(mut self, wrapping: text::Wrapping) -> Self {
|
||||
self.text_wrapping = wrapping;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`Renderer::Font`] of the text of the [`Checkbox`].
|
||||
///
|
||||
/// [`Renderer::Font`]: crate::core::text::Renderer
|
||||
|
|
@ -191,8 +247,8 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Checkbox<'a, Message, Theme, Renderer>
|
||||
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Checkbox<'_, Message, Theme, Renderer>
|
||||
where
|
||||
Renderer: text::Renderer,
|
||||
Theme: Catalog,
|
||||
|
|
@ -240,22 +296,23 @@ where
|
|||
alignment::Horizontal::Left,
|
||||
alignment::Vertical::Top,
|
||||
self.text_shaping,
|
||||
self.text_wrapping,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
fn update(
|
||||
&mut self,
|
||||
_tree: &mut Tree,
|
||||
event: Event,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
_renderer: &Renderer,
|
||||
_clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
_viewport: &Rectangle,
|
||||
) -> event::Status {
|
||||
) {
|
||||
match event {
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
|
||||
| Event::Touch(touch::Event::FingerPressed { .. }) => {
|
||||
|
|
@ -264,14 +321,35 @@ where
|
|||
if mouse_over {
|
||||
if let Some(on_toggle) = &self.on_toggle {
|
||||
shell.publish((on_toggle)(!self.is_checked));
|
||||
return event::Status::Captured;
|
||||
shell.capture_event();
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
event::Status::Ignored
|
||||
let current_status = {
|
||||
let is_mouse_over = cursor.is_over(layout.bounds());
|
||||
let is_disabled = self.on_toggle.is_none();
|
||||
let is_checked = self.is_checked;
|
||||
|
||||
if is_disabled {
|
||||
Status::Disabled { is_checked }
|
||||
} else if is_mouse_over {
|
||||
Status::Hovered { is_checked }
|
||||
} else {
|
||||
Status::Active { is_checked }
|
||||
}
|
||||
};
|
||||
|
||||
if let Event::Window(window::Event::RedrawRequested(_now)) = event {
|
||||
self.last_status = Some(current_status);
|
||||
} else if self
|
||||
.last_status
|
||||
.is_some_and(|status| status != current_status)
|
||||
{
|
||||
shell.request_redraw();
|
||||
}
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
|
|
@ -296,24 +374,17 @@ where
|
|||
theme: &Theme,
|
||||
defaults: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
_cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
let is_mouse_over = cursor.is_over(layout.bounds());
|
||||
let is_disabled = self.on_toggle.is_none();
|
||||
let is_checked = self.is_checked;
|
||||
|
||||
let mut children = layout.children();
|
||||
|
||||
let status = if is_disabled {
|
||||
Status::Disabled { is_checked }
|
||||
} else if is_mouse_over {
|
||||
Status::Hovered { is_checked }
|
||||
} else {
|
||||
Status::Active { is_checked }
|
||||
};
|
||||
|
||||
let style = theme.style(&self.class, status);
|
||||
let style = theme.style(
|
||||
&self.class,
|
||||
self.last_status.unwrap_or(Status::Disabled {
|
||||
is_checked: self.is_checked,
|
||||
}),
|
||||
);
|
||||
|
||||
{
|
||||
let layout = children.next().unwrap();
|
||||
|
|
@ -348,6 +419,7 @@ where
|
|||
horizontal_alignment: alignment::Horizontal::Center,
|
||||
vertical_alignment: alignment::Vertical::Center,
|
||||
shaping: *shaping,
|
||||
wrapping: text::Wrapping::default(),
|
||||
},
|
||||
bounds.center(),
|
||||
style.icon_color,
|
||||
|
|
@ -358,12 +430,14 @@ where
|
|||
|
||||
{
|
||||
let label_layout = children.next().unwrap();
|
||||
let state: &widget::text::State<Renderer::Paragraph> =
|
||||
tree.state.downcast_ref();
|
||||
|
||||
crate::text::draw(
|
||||
renderer,
|
||||
defaults,
|
||||
label_layout,
|
||||
tree.state.downcast_ref(),
|
||||
state.0.raw(),
|
||||
crate::text::Style {
|
||||
color: style.text_color,
|
||||
},
|
||||
|
|
@ -371,6 +445,16 @@ where
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn operate(
|
||||
&self,
|
||||
_state: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
_renderer: &Renderer,
|
||||
operation: &mut dyn widget::Operation,
|
||||
) {
|
||||
operation.text(None, layout.bounds(), &self.label);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> From<Checkbox<'a, Message, Theme, Renderer>>
|
||||
|
|
@ -423,13 +507,13 @@ pub enum Status {
|
|||
}
|
||||
|
||||
/// The style of a checkbox.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Style {
|
||||
/// The [`Background`] of the checkbox.
|
||||
pub background: Background,
|
||||
/// The icon [`Color`] of the checkbox.
|
||||
pub icon_color: Color,
|
||||
/// The [`Border`] of hte checkbox.
|
||||
/// The [`Border`] of the checkbox.
|
||||
pub border: Border,
|
||||
/// The text [`Color`] of the checkbox.
|
||||
pub text_color: Option<Color>,
|
||||
|
|
@ -471,18 +555,21 @@ pub fn primary(theme: &Theme, status: Status) -> Style {
|
|||
match status {
|
||||
Status::Active { is_checked } => styled(
|
||||
palette.primary.strong.text,
|
||||
palette.background.strongest.color,
|
||||
palette.background.base,
|
||||
palette.primary.strong,
|
||||
palette.primary.base,
|
||||
is_checked,
|
||||
),
|
||||
Status::Hovered { is_checked } => styled(
|
||||
palette.primary.strong.text,
|
||||
palette.background.strongest.color,
|
||||
palette.background.weak,
|
||||
palette.primary.base,
|
||||
palette.primary.strong,
|
||||
is_checked,
|
||||
),
|
||||
Status::Disabled { is_checked } => styled(
|
||||
palette.primary.strong.text,
|
||||
palette.background.weak.color,
|
||||
palette.background.weak,
|
||||
palette.background.strong,
|
||||
is_checked,
|
||||
|
|
@ -497,18 +584,21 @@ pub fn secondary(theme: &Theme, status: Status) -> Style {
|
|||
match status {
|
||||
Status::Active { is_checked } => styled(
|
||||
palette.background.base.text,
|
||||
palette.background.strongest.color,
|
||||
palette.background.base,
|
||||
palette.background.strong,
|
||||
is_checked,
|
||||
),
|
||||
Status::Hovered { is_checked } => styled(
|
||||
palette.background.base.text,
|
||||
palette.background.strongest.color,
|
||||
palette.background.weak,
|
||||
palette.background.strong,
|
||||
is_checked,
|
||||
),
|
||||
Status::Disabled { is_checked } => styled(
|
||||
palette.background.strong.color,
|
||||
palette.background.weak.color,
|
||||
palette.background.weak,
|
||||
palette.background.weak,
|
||||
is_checked,
|
||||
|
|
@ -523,18 +613,21 @@ pub fn success(theme: &Theme, status: Status) -> Style {
|
|||
match status {
|
||||
Status::Active { is_checked } => styled(
|
||||
palette.success.base.text,
|
||||
palette.background.weak.color,
|
||||
palette.background.base,
|
||||
palette.success.base,
|
||||
is_checked,
|
||||
),
|
||||
Status::Hovered { is_checked } => styled(
|
||||
palette.success.base.text,
|
||||
palette.background.strongest.color,
|
||||
palette.background.weak,
|
||||
palette.success.base,
|
||||
palette.success.strong,
|
||||
is_checked,
|
||||
),
|
||||
Status::Disabled { is_checked } => styled(
|
||||
palette.success.base.text,
|
||||
palette.background.weak.color,
|
||||
palette.background.weak,
|
||||
palette.success.weak,
|
||||
is_checked,
|
||||
|
|
@ -542,25 +635,28 @@ pub fn success(theme: &Theme, status: Status) -> Style {
|
|||
}
|
||||
}
|
||||
|
||||
/// A danger checkbox; denoting a negaive toggle.
|
||||
/// A danger checkbox; denoting a negative toggle.
|
||||
pub fn danger(theme: &Theme, status: Status) -> Style {
|
||||
let palette = theme.extended_palette();
|
||||
|
||||
match status {
|
||||
Status::Active { is_checked } => styled(
|
||||
palette.danger.base.text,
|
||||
palette.background.strongest.color,
|
||||
palette.background.base,
|
||||
palette.danger.base,
|
||||
is_checked,
|
||||
),
|
||||
Status::Hovered { is_checked } => styled(
|
||||
palette.danger.base.text,
|
||||
palette.background.strongest.color,
|
||||
palette.background.weak,
|
||||
palette.danger.base,
|
||||
palette.danger.strong,
|
||||
is_checked,
|
||||
),
|
||||
Status::Disabled { is_checked } => styled(
|
||||
palette.danger.base.text,
|
||||
palette.background.weak.color,
|
||||
palette.background.weak,
|
||||
palette.danger.weak,
|
||||
is_checked,
|
||||
|
|
@ -570,6 +666,7 @@ pub fn danger(theme: &Theme, status: Status) -> Style {
|
|||
|
||||
fn styled(
|
||||
icon_color: Color,
|
||||
border_color: Color,
|
||||
base: palette::Pair,
|
||||
accent: palette::Pair,
|
||||
is_checked: bool,
|
||||
|
|
@ -584,7 +681,11 @@ fn styled(
|
|||
border: Border {
|
||||
radius: 2.0.into(),
|
||||
width: 1.0,
|
||||
color: accent.color,
|
||||
color: if is_checked {
|
||||
accent.color
|
||||
} else {
|
||||
border_color
|
||||
},
|
||||
},
|
||||
text_color: None,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,37 @@
|
|||
//! Distribute content vertically.
|
||||
use crate::core::event::{self, Event};
|
||||
use crate::core::alignment::{self, Alignment};
|
||||
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, Rectangle,
|
||||
Clipboard, Element, Event, Layout, Length, Padding, Pixels, Rectangle,
|
||||
Shell, Size, Vector, Widget,
|
||||
};
|
||||
|
||||
/// A container that distributes its contents vertically.
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// # mod iced { pub mod widget { pub use iced_widget::*; } }
|
||||
/// # pub type State = ();
|
||||
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||
/// use iced::widget::{button, column};
|
||||
///
|
||||
/// #[derive(Debug, Clone)]
|
||||
/// enum Message {
|
||||
/// // ...
|
||||
/// }
|
||||
///
|
||||
/// fn view(state: &State) -> Element<'_, Message> {
|
||||
/// column![
|
||||
/// "I am on top!",
|
||||
/// button("I am in the center!"),
|
||||
/// "I am below.",
|
||||
/// ].into()
|
||||
/// }
|
||||
/// ```
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Column<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
|
||||
{
|
||||
|
|
@ -19,7 +40,7 @@ pub struct Column<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
|
|||
width: Length,
|
||||
height: Length,
|
||||
max_width: f32,
|
||||
align_items: Alignment,
|
||||
align: Alignment,
|
||||
clip: bool,
|
||||
children: Vec<Element<'a, Message, Theme, Renderer>>,
|
||||
}
|
||||
|
|
@ -63,7 +84,7 @@ where
|
|||
width: Length::Shrink,
|
||||
height: Length::Shrink,
|
||||
max_width: f32::INFINITY,
|
||||
align_items: Alignment::Start,
|
||||
align: Alignment::Start,
|
||||
clip: false,
|
||||
children,
|
||||
}
|
||||
|
|
@ -104,8 +125,8 @@ where
|
|||
}
|
||||
|
||||
/// Sets the horizontal alignment of the contents of the [`Column`] .
|
||||
pub fn align_items(mut self, align: Alignment) -> Self {
|
||||
self.align_items = align;
|
||||
pub fn align_x(mut self, align: impl Into<alignment::Horizontal>) -> Self {
|
||||
self.align = Alignment::from(align.into());
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -152,7 +173,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Default for Column<'a, Message, Renderer>
|
||||
impl<Message, Renderer> Default for Column<'_, Message, Renderer>
|
||||
where
|
||||
Renderer: crate::core::Renderer,
|
||||
{
|
||||
|
|
@ -161,8 +182,21 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
impl<'a, Message, Theme, Renderer: crate::core::Renderer>
|
||||
FromIterator<Element<'a, Message, Theme, Renderer>>
|
||||
for Column<'a, Message, Theme, Renderer>
|
||||
{
|
||||
fn from_iter<
|
||||
T: IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
|
||||
>(
|
||||
iter: T,
|
||||
) -> Self {
|
||||
Self::with_children(iter)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Column<'_, Message, Theme, Renderer>
|
||||
where
|
||||
Renderer: crate::core::Renderer,
|
||||
{
|
||||
|
|
@ -197,7 +231,7 @@ where
|
|||
self.height,
|
||||
self.padding,
|
||||
self.spacing,
|
||||
self.align_items,
|
||||
self.align,
|
||||
&self.children,
|
||||
&mut tree.children,
|
||||
)
|
||||
|
|
@ -208,7 +242,7 @@ where
|
|||
tree: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn Operation<Message>,
|
||||
operation: &mut dyn Operation,
|
||||
) {
|
||||
operation.container(None, layout.bounds(), &mut |operation| {
|
||||
self.children
|
||||
|
|
@ -223,34 +257,28 @@ where
|
|||
});
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
fn update(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: Event,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
viewport: &Rectangle,
|
||||
) -> event::Status {
|
||||
self.children
|
||||
) {
|
||||
for ((child, state), layout) in 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,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
viewport,
|
||||
)
|
||||
})
|
||||
.fold(event::Status::Ignored, event::Status::merge)
|
||||
{
|
||||
child.as_widget_mut().update(
|
||||
state, event, layout, cursor, renderer, clipboard, shell,
|
||||
viewport,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
|
|
@ -285,24 +313,21 @@ where
|
|||
viewport: &Rectangle,
|
||||
) {
|
||||
if let Some(clipped_viewport) = layout.bounds().intersection(viewport) {
|
||||
let viewport = if self.clip {
|
||||
&clipped_viewport
|
||||
} else {
|
||||
viewport
|
||||
};
|
||||
|
||||
for ((child, state), layout) in self
|
||||
.children
|
||||
.iter()
|
||||
.zip(&tree.children)
|
||||
.zip(layout.children())
|
||||
.filter(|(_, layout)| layout.bounds().intersects(viewport))
|
||||
{
|
||||
child.as_widget().draw(
|
||||
state,
|
||||
renderer,
|
||||
theme,
|
||||
style,
|
||||
layout,
|
||||
cursor,
|
||||
if self.clip {
|
||||
&clipped_viewport
|
||||
} else {
|
||||
viewport
|
||||
},
|
||||
state, renderer, theme, style, layout, cursor, viewport,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,59 @@
|
|||
//! Display a dropdown list of searchable and selectable options.
|
||||
use crate::core::event::{self, Event};
|
||||
//! Combo boxes display a dropdown list of searchable and selectable options.
|
||||
//!
|
||||
//! # Example
|
||||
//! ```no_run
|
||||
//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
|
||||
//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||
//! #
|
||||
//! use iced::widget::combo_box;
|
||||
//!
|
||||
//! struct State {
|
||||
//! fruits: combo_box::State<Fruit>,
|
||||
//! favorite: Option<Fruit>,
|
||||
//! }
|
||||
//!
|
||||
//! #[derive(Debug, Clone)]
|
||||
//! enum Fruit {
|
||||
//! Apple,
|
||||
//! Orange,
|
||||
//! Strawberry,
|
||||
//! Tomato,
|
||||
//! }
|
||||
//!
|
||||
//! #[derive(Debug, Clone)]
|
||||
//! enum Message {
|
||||
//! FruitSelected(Fruit),
|
||||
//! }
|
||||
//!
|
||||
//! fn view(state: &State) -> Element<'_, Message> {
|
||||
//! combo_box(
|
||||
//! &state.fruits,
|
||||
//! "Select your favorite fruit...",
|
||||
//! state.favorite.as_ref(),
|
||||
//! Message::FruitSelected
|
||||
//! )
|
||||
//! .into()
|
||||
//! }
|
||||
//!
|
||||
//! fn update(state: &mut State, message: Message) {
|
||||
//! match message {
|
||||
//! Message::FruitSelected(fruit) => {
|
||||
//! state.favorite = Some(fruit);
|
||||
//! }
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! impl std::fmt::Display for Fruit {
|
||||
//! fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
//! f.write_str(match self {
|
||||
//! Self::Apple => "Apple",
|
||||
//! Self::Orange => "Orange",
|
||||
//! Self::Strawberry => "Strawberry",
|
||||
//! Self::Tomato => "Tomato",
|
||||
//! })
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
use crate::core::keyboard;
|
||||
use crate::core::keyboard::key;
|
||||
use crate::core::layout::{self, Layout};
|
||||
|
|
@ -10,7 +64,8 @@ use crate::core::text;
|
|||
use crate::core::time::Instant;
|
||||
use crate::core::widget::{self, Widget};
|
||||
use crate::core::{
|
||||
Clipboard, Element, Length, Padding, Rectangle, Shell, Size, Theme, Vector,
|
||||
Clipboard, Element, Event, Length, Padding, Rectangle, Shell, Size, Theme,
|
||||
Vector,
|
||||
};
|
||||
use crate::overlay::menu;
|
||||
use crate::text::LineHeight;
|
||||
|
|
@ -21,9 +76,60 @@ use std::fmt::Display;
|
|||
|
||||
/// A widget for searching and selecting a single value from a list of options.
|
||||
///
|
||||
/// This widget is composed by a [`TextInput`] that can be filled with the text
|
||||
/// to search for corresponding values from the list of options that are displayed
|
||||
/// as a Menu.
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
|
||||
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||
/// #
|
||||
/// use iced::widget::combo_box;
|
||||
///
|
||||
/// struct State {
|
||||
/// fruits: combo_box::State<Fruit>,
|
||||
/// favorite: Option<Fruit>,
|
||||
/// }
|
||||
///
|
||||
/// #[derive(Debug, Clone)]
|
||||
/// enum Fruit {
|
||||
/// Apple,
|
||||
/// Orange,
|
||||
/// Strawberry,
|
||||
/// Tomato,
|
||||
/// }
|
||||
///
|
||||
/// #[derive(Debug, Clone)]
|
||||
/// enum Message {
|
||||
/// FruitSelected(Fruit),
|
||||
/// }
|
||||
///
|
||||
/// fn view(state: &State) -> Element<'_, Message> {
|
||||
/// combo_box(
|
||||
/// &state.fruits,
|
||||
/// "Select your favorite fruit...",
|
||||
/// state.favorite.as_ref(),
|
||||
/// Message::FruitSelected
|
||||
/// )
|
||||
/// .into()
|
||||
/// }
|
||||
///
|
||||
/// fn update(state: &mut State, message: Message) {
|
||||
/// match message {
|
||||
/// Message::FruitSelected(fruit) => {
|
||||
/// state.favorite = Some(fruit);
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// impl std::fmt::Display for Fruit {
|
||||
/// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
/// f.write_str(match self {
|
||||
/// Self::Apple => "Apple",
|
||||
/// Self::Orange => "Orange",
|
||||
/// Self::Strawberry => "Strawberry",
|
||||
/// Self::Tomato => "Tomato",
|
||||
/// })
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct ComboBox<
|
||||
'a,
|
||||
|
|
@ -41,6 +147,7 @@ pub struct ComboBox<
|
|||
selection: text_input::Value,
|
||||
on_selected: Box<dyn Fn(T) -> Message>,
|
||||
on_option_hovered: Option<Box<dyn Fn(T) -> Message>>,
|
||||
on_open: Option<Message>,
|
||||
on_close: Option<Message>,
|
||||
on_input: Option<Box<dyn Fn(String) -> Message>>,
|
||||
menu_class: <Theme as menu::Catalog>::Class<'a>,
|
||||
|
|
@ -77,6 +184,7 @@ where
|
|||
on_selected: Box::new(on_selected),
|
||||
on_option_hovered: None,
|
||||
on_input: None,
|
||||
on_open: None,
|
||||
on_close: None,
|
||||
menu_class: <Theme as Catalog>::default_menu(),
|
||||
padding: text_input::DEFAULT_PADDING,
|
||||
|
|
@ -104,6 +212,13 @@ where
|
|||
self
|
||||
}
|
||||
|
||||
/// Sets the message that will be produced when the [`ComboBox`] is
|
||||
/// opened.
|
||||
pub fn on_open(mut self, message: Message) -> Self {
|
||||
self.on_open = Some(message);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the message that will be produced when the outside area
|
||||
/// of the [`ComboBox`] is pressed.
|
||||
pub fn on_close(mut self, message: Message) -> Self {
|
||||
|
|
@ -208,12 +323,14 @@ where
|
|||
|
||||
/// The local state of a [`ComboBox`].
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct State<T>(RefCell<Inner<T>>);
|
||||
pub struct State<T> {
|
||||
options: Vec<T>,
|
||||
inner: RefCell<Inner<T>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Inner<T> {
|
||||
value: String,
|
||||
options: Vec<T>,
|
||||
option_matchers: Vec<String>,
|
||||
filtered_options: Filtered<T>,
|
||||
}
|
||||
|
|
@ -247,39 +364,58 @@ where
|
|||
.collect(),
|
||||
);
|
||||
|
||||
Self(RefCell::new(Inner {
|
||||
value,
|
||||
Self {
|
||||
options,
|
||||
option_matchers,
|
||||
filtered_options,
|
||||
}))
|
||||
inner: RefCell::new(Inner {
|
||||
value,
|
||||
option_matchers,
|
||||
filtered_options,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the options of the [`State`].
|
||||
///
|
||||
/// These are the options provided when the [`State`]
|
||||
/// was constructed with [`State::new`].
|
||||
pub fn options(&self) -> &[T] {
|
||||
&self.options
|
||||
}
|
||||
|
||||
fn value(&self) -> String {
|
||||
let inner = self.0.borrow();
|
||||
let inner = self.inner.borrow();
|
||||
|
||||
inner.value.clone()
|
||||
}
|
||||
|
||||
fn with_inner<O>(&self, f: impl FnOnce(&Inner<T>) -> O) -> O {
|
||||
let inner = self.0.borrow();
|
||||
let inner = self.inner.borrow();
|
||||
|
||||
f(&inner)
|
||||
}
|
||||
|
||||
fn with_inner_mut(&self, f: impl FnOnce(&mut Inner<T>)) {
|
||||
let mut inner = self.0.borrow_mut();
|
||||
let mut inner = self.inner.borrow_mut();
|
||||
|
||||
f(&mut inner);
|
||||
}
|
||||
|
||||
fn sync_filtered_options(&self, options: &mut Filtered<T>) {
|
||||
let inner = self.0.borrow();
|
||||
let inner = self.inner.borrow();
|
||||
|
||||
inner.filtered_options.sync(options);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Default for State<T>
|
||||
where
|
||||
T: Display + Clone,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self::new(Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Filtered<T>
|
||||
where
|
||||
T: Clone,
|
||||
|
|
@ -322,8 +458,8 @@ enum TextInputEvent {
|
|||
TextChanged(String),
|
||||
}
|
||||
|
||||
impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for ComboBox<'a, T, Message, Theme, Renderer>
|
||||
impl<T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for ComboBox<'_, T, Message, Theme, Renderer>
|
||||
where
|
||||
T: Display + Clone + 'static,
|
||||
Message: Clone,
|
||||
|
|
@ -373,17 +509,17 @@ where
|
|||
vec![widget::Tree::new(&self.text_input as &dyn Widget<_, _, _>)]
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
fn update(
|
||||
&mut self,
|
||||
tree: &mut widget::Tree,
|
||||
event: Event,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
viewport: &Rectangle,
|
||||
) -> event::Status {
|
||||
) {
|
||||
let menu = tree.state.downcast_mut::<Menu<T>>();
|
||||
|
||||
let started_focused = {
|
||||
|
|
@ -402,9 +538,9 @@ where
|
|||
let mut local_shell = Shell::new(&mut local_messages);
|
||||
|
||||
// Provide it to the widget
|
||||
let mut event_status = self.text_input.on_event(
|
||||
self.text_input.update(
|
||||
&mut tree.children[0],
|
||||
event.clone(),
|
||||
event,
|
||||
layout,
|
||||
cursor,
|
||||
renderer,
|
||||
|
|
@ -413,13 +549,19 @@ where
|
|||
viewport,
|
||||
);
|
||||
|
||||
if local_shell.is_event_captured() {
|
||||
shell.capture_event();
|
||||
}
|
||||
|
||||
shell.request_redraw_at(local_shell.redraw_request());
|
||||
shell.request_input_method(local_shell.input_method());
|
||||
|
||||
// Then finally react to them here
|
||||
for message in local_messages {
|
||||
let TextInputEvent::TextChanged(new_value) = message;
|
||||
|
||||
if let Some(on_input) = &self.on_input {
|
||||
shell.publish((on_input)(new_value.clone()));
|
||||
published_message_to_shell = true;
|
||||
}
|
||||
|
||||
// Couple the filtered options with the `ComboBox`
|
||||
|
|
@ -431,7 +573,7 @@ where
|
|||
|
||||
state.filtered_options.update(
|
||||
search(
|
||||
&state.options,
|
||||
&self.state.options,
|
||||
&state.option_matchers,
|
||||
&state.value,
|
||||
)
|
||||
|
|
@ -440,6 +582,7 @@ where
|
|||
);
|
||||
});
|
||||
shell.invalidate_layout();
|
||||
shell.request_redraw();
|
||||
}
|
||||
|
||||
let is_focused = {
|
||||
|
|
@ -472,8 +615,8 @@ where
|
|||
..
|
||||
}) = event
|
||||
{
|
||||
let shift_modifer = modifiers.shift();
|
||||
match (named_key, shift_modifer) {
|
||||
let shift_modifier = modifiers.shift();
|
||||
match (named_key, shift_modifier) {
|
||||
(key::Named::Enter, _) => {
|
||||
if let Some(index) = &menu.hovered_option {
|
||||
if let Some(option) =
|
||||
|
|
@ -483,9 +626,9 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
event_status = event::Status::Captured;
|
||||
shell.capture_event();
|
||||
shell.request_redraw();
|
||||
}
|
||||
|
||||
(key::Named::ArrowUp, _) | (key::Named::Tab, true) => {
|
||||
if let Some(index) = &mut menu.hovered_option {
|
||||
if *index == 0 {
|
||||
|
|
@ -520,7 +663,8 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
event_status = event::Status::Captured;
|
||||
shell.capture_event();
|
||||
shell.request_redraw();
|
||||
}
|
||||
(key::Named::ArrowDown, _)
|
||||
| (key::Named::Tab, false)
|
||||
|
|
@ -567,7 +711,8 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
event_status = event::Status::Captured;
|
||||
shell.capture_event();
|
||||
shell.request_redraw();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
|
@ -580,7 +725,7 @@ where
|
|||
if let Some(selection) = menu.new_selection.take() {
|
||||
// Clear the value and reset the options and menu
|
||||
state.value = String::new();
|
||||
state.filtered_options.update(state.options.clone());
|
||||
state.filtered_options.update(self.state.options.clone());
|
||||
menu.menu = menu::State::default();
|
||||
|
||||
// Notify the selection
|
||||
|
|
@ -588,18 +733,21 @@ where
|
|||
published_message_to_shell = true;
|
||||
|
||||
// Unfocus the input
|
||||
let _ = self.text_input.on_event(
|
||||
let mut local_messages = Vec::new();
|
||||
let mut local_shell = Shell::new(&mut local_messages);
|
||||
self.text_input.update(
|
||||
&mut tree.children[0],
|
||||
Event::Mouse(mouse::Event::ButtonPressed(
|
||||
&Event::Mouse(mouse::Event::ButtonPressed(
|
||||
mouse::Button::Left,
|
||||
)),
|
||||
layout,
|
||||
mouse::Cursor::Unavailable,
|
||||
renderer,
|
||||
clipboard,
|
||||
&mut Shell::new(&mut vec![]),
|
||||
&mut local_shell,
|
||||
viewport,
|
||||
);
|
||||
shell.request_input_method(local_shell.input_method());
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -611,18 +759,20 @@ where
|
|||
text_input_state.is_focused()
|
||||
};
|
||||
|
||||
if started_focused && !is_focused && !published_message_to_shell {
|
||||
if let Some(message) = self.on_close.take() {
|
||||
shell.publish(message);
|
||||
if started_focused != is_focused {
|
||||
// Focus changed, invalidate widget tree to force a fresh `view`
|
||||
shell.invalidate_widgets();
|
||||
|
||||
if !published_message_to_shell {
|
||||
if is_focused {
|
||||
if let Some(on_open) = self.on_open.take() {
|
||||
shell.publish(on_open);
|
||||
}
|
||||
} else if let Some(on_close) = self.on_close.take() {
|
||||
shell.publish(on_close);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Focus changed, invalidate widget tree to force a fresh `view`
|
||||
if started_focused != is_focused {
|
||||
shell.invalidate_widgets();
|
||||
}
|
||||
|
||||
event_status
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
|
|
|
|||
|
|
@ -1,23 +1,62 @@
|
|||
//! Decorate content and apply alignment.
|
||||
//! Containers let you align a widget inside their boundaries.
|
||||
//!
|
||||
//! # Example
|
||||
//! ```no_run
|
||||
//! # mod iced { pub mod widget { pub use iced_widget::*; } }
|
||||
//! # pub type State = ();
|
||||
//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||
//! use iced::widget::container;
|
||||
//!
|
||||
//! enum Message {
|
||||
//! // ...
|
||||
//! }
|
||||
//!
|
||||
//! fn view(state: &State) -> Element<'_, Message> {
|
||||
//! container("This text is centered inside a rounded box!")
|
||||
//! .padding(10)
|
||||
//! .center(800)
|
||||
//! .style(container::rounded_box)
|
||||
//! .into()
|
||||
//! }
|
||||
//! ```
|
||||
use crate::core::alignment::{self, Alignment};
|
||||
use crate::core::event::{self, Event};
|
||||
use crate::core::border::{self, Border};
|
||||
use crate::core::gradient::{self, Gradient};
|
||||
use crate::core::layout;
|
||||
use crate::core::mouse;
|
||||
use crate::core::overlay;
|
||||
use crate::core::renderer;
|
||||
use crate::core::theme;
|
||||
use crate::core::widget::tree::{self, Tree};
|
||||
use crate::core::widget::{self, Operation};
|
||||
use crate::core::{
|
||||
self, Background, Border, Clipboard, Color, Element, Layout, Length,
|
||||
self, Background, Clipboard, Color, Element, Event, Layout, Length,
|
||||
Padding, Pixels, Point, Rectangle, Shadow, Shell, Size, Theme, Vector,
|
||||
Widget,
|
||||
Widget, color,
|
||||
};
|
||||
use crate::runtime::Command;
|
||||
use crate::runtime::task::{self, Task};
|
||||
|
||||
/// An element decorating some content.
|
||||
/// A widget that aligns its contents inside of its boundaries.
|
||||
///
|
||||
/// It is normally used for alignment purposes.
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// # mod iced { pub mod widget { pub use iced_widget::*; } }
|
||||
/// # pub type State = ();
|
||||
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||
/// use iced::widget::container;
|
||||
///
|
||||
/// enum Message {
|
||||
/// // ...
|
||||
/// }
|
||||
///
|
||||
/// fn view(state: &State) -> Element<'_, Message> {
|
||||
/// container("This text is centered inside a rounded box!")
|
||||
/// .padding(10)
|
||||
/// .center(800)
|
||||
/// .style(container::rounded_box)
|
||||
/// .into()
|
||||
/// }
|
||||
/// ```
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Container<
|
||||
'a,
|
||||
|
|
@ -69,8 +108,8 @@ where
|
|||
}
|
||||
|
||||
/// Sets the [`Id`] of the [`Container`].
|
||||
pub fn id(mut self, id: Id) -> Self {
|
||||
self.id = Some(id);
|
||||
pub fn id(mut self, id: impl Into<Id>) -> Self {
|
||||
self.id = Some(id.into());
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -92,46 +131,6 @@ where
|
|||
self
|
||||
}
|
||||
|
||||
/// Sets the [`Container`] to fill the available space in the horizontal axis.
|
||||
///
|
||||
/// This can be useful to quickly position content when chained with
|
||||
/// alignment functions—like [`center_x`].
|
||||
///
|
||||
/// Calling this method is equivalent to calling [`width`] with a
|
||||
/// [`Length::Fill`].
|
||||
///
|
||||
/// [`center_x`]: Self::center_x
|
||||
/// [`width`]: Self::width
|
||||
pub fn fill_x(self) -> Self {
|
||||
self.width(Length::Fill)
|
||||
}
|
||||
|
||||
/// Sets the [`Container`] to fill the available space in the vetical axis.
|
||||
///
|
||||
/// This can be useful to quickly position content when chained with
|
||||
/// alignment functions—like [`center_y`].
|
||||
///
|
||||
/// Calling this method is equivalent to calling [`height`] with a
|
||||
/// [`Length::Fill`].
|
||||
///
|
||||
/// [`center_y`]: Self::center_x
|
||||
/// [`height`]: Self::height
|
||||
pub fn fill_y(self) -> Self {
|
||||
self.height(Length::Fill)
|
||||
}
|
||||
|
||||
/// Sets the [`Container`] to fill all the available space.
|
||||
///
|
||||
/// Calling this method is equivalent to chaining [`fill_x`] and
|
||||
/// [`fill_y`].
|
||||
///
|
||||
/// [`center`]: Self::center
|
||||
/// [`fill_x`]: Self::fill_x
|
||||
/// [`fill_y`]: Self::fill_y
|
||||
pub fn fill(self) -> Self {
|
||||
self.width(Length::Fill).height(Length::Fill)
|
||||
}
|
||||
|
||||
/// Sets the maximum width of the [`Container`].
|
||||
pub fn max_width(mut self, max_width: impl Into<Pixels>) -> Self {
|
||||
self.max_width = max_width.into().0;
|
||||
|
|
@ -144,18 +143,6 @@ where
|
|||
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
|
||||
}
|
||||
|
||||
/// Sets the width of the [`Container`] and centers its contents horizontally.
|
||||
pub fn center_x(self, width: impl Into<Length>) -> Self {
|
||||
self.width(width).align_x(alignment::Horizontal::Center)
|
||||
|
|
@ -179,6 +166,44 @@ where
|
|||
self.center_x(length).center_y(length)
|
||||
}
|
||||
|
||||
/// Aligns the contents of the [`Container`] to the left.
|
||||
pub fn align_left(self, width: impl Into<Length>) -> Self {
|
||||
self.width(width).align_x(alignment::Horizontal::Left)
|
||||
}
|
||||
|
||||
/// Aligns the contents of the [`Container`] to the right.
|
||||
pub fn align_right(self, width: impl Into<Length>) -> Self {
|
||||
self.width(width).align_x(alignment::Horizontal::Right)
|
||||
}
|
||||
|
||||
/// Aligns the contents of the [`Container`] to the top.
|
||||
pub fn align_top(self, height: impl Into<Length>) -> Self {
|
||||
self.height(height).align_y(alignment::Vertical::Top)
|
||||
}
|
||||
|
||||
/// Aligns the contents of the [`Container`] to the bottom.
|
||||
pub fn align_bottom(self, height: impl Into<Length>) -> Self {
|
||||
self.height(height).align_y(alignment::Vertical::Bottom)
|
||||
}
|
||||
|
||||
/// Sets the content alignment for the horizontal axis of the [`Container`].
|
||||
pub fn align_x(
|
||||
mut self,
|
||||
alignment: impl Into<alignment::Horizontal>,
|
||||
) -> Self {
|
||||
self.horizontal_alignment = alignment.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the content alignment for the vertical axis of the [`Container`].
|
||||
pub fn align_y(
|
||||
mut self,
|
||||
alignment: impl Into<alignment::Vertical>,
|
||||
) -> Self {
|
||||
self.vertical_alignment = alignment.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets whether the contents of the [`Container`] should be clipped on
|
||||
/// overflow.
|
||||
pub fn clip(mut self, clip: bool) -> Self {
|
||||
|
|
@ -197,7 +222,6 @@ where
|
|||
}
|
||||
|
||||
/// Sets the style class of the [`Container`].
|
||||
#[cfg(feature = "advanced")]
|
||||
#[must_use]
|
||||
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
|
||||
self.class = class.into();
|
||||
|
|
@ -205,8 +229,8 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Container<'a, Message, Theme, Renderer>
|
||||
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Container<'_, Message, Theme, Renderer>
|
||||
where
|
||||
Theme: Catalog,
|
||||
Renderer: core::Renderer,
|
||||
|
|
@ -258,7 +282,7 @@ where
|
|||
tree: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn Operation<Message>,
|
||||
operation: &mut dyn Operation,
|
||||
) {
|
||||
operation.container(
|
||||
self.id.as_ref().map(|id| &id.0),
|
||||
|
|
@ -274,18 +298,18 @@ where
|
|||
);
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
fn update(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: Event,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
viewport: &Rectangle,
|
||||
) -> event::Status {
|
||||
self.content.as_widget_mut().on_event(
|
||||
) {
|
||||
self.content.as_widget_mut().update(
|
||||
tree,
|
||||
event,
|
||||
layout.children().next().unwrap(),
|
||||
|
|
@ -294,7 +318,7 @@ where
|
|||
clipboard,
|
||||
shell,
|
||||
viewport,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
|
|
@ -457,9 +481,17 @@ impl From<Id> for widget::Id {
|
|||
}
|
||||
}
|
||||
|
||||
/// Produces a [`Command`] that queries the visible screen bounds of the
|
||||
impl From<&'static str> for Id {
|
||||
fn from(value: &'static str) -> Self {
|
||||
Id::new(value)
|
||||
}
|
||||
}
|
||||
|
||||
/// Produces a [`Task`] that queries the visible screen bounds of the
|
||||
/// [`Container`] with the given [`Id`].
|
||||
pub fn visible_bounds(id: Id) -> Command<Option<Rectangle>> {
|
||||
pub fn visible_bounds(id: impl Into<Id>) -> Task<Option<Rectangle>> {
|
||||
let id = id.into();
|
||||
|
||||
struct VisibleBounds {
|
||||
target: widget::Id,
|
||||
depth: usize,
|
||||
|
|
@ -470,10 +502,11 @@ pub fn visible_bounds(id: Id) -> Command<Option<Rectangle>> {
|
|||
impl Operation<Option<Rectangle>> for VisibleBounds {
|
||||
fn scrollable(
|
||||
&mut self,
|
||||
_state: &mut dyn widget::operation::Scrollable,
|
||||
_id: Option<&widget::Id>,
|
||||
bounds: Rectangle,
|
||||
_content_bounds: Rectangle,
|
||||
translation: Vector,
|
||||
_state: &mut dyn widget::operation::Scrollable,
|
||||
) {
|
||||
match self.scrollables.last() {
|
||||
Some((last_translation, last_viewport, _depth)) => {
|
||||
|
|
@ -538,7 +571,7 @@ pub fn visible_bounds(id: Id) -> Command<Option<Rectangle>> {
|
|||
}
|
||||
}
|
||||
|
||||
Command::widget(VisibleBounds {
|
||||
task::widget(VisibleBounds {
|
||||
target: id.into(),
|
||||
depth: 0,
|
||||
scrollables: Vec::new(),
|
||||
|
|
@ -547,7 +580,7 @@ pub fn visible_bounds(id: Id) -> Command<Option<Rectangle>> {
|
|||
}
|
||||
|
||||
/// The appearance of a container.
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||
pub struct Style {
|
||||
/// The text [`Color`] of the container.
|
||||
pub text_color: Option<Color>,
|
||||
|
|
@ -560,46 +593,54 @@ pub struct Style {
|
|||
}
|
||||
|
||||
impl Style {
|
||||
/// Updates the border of the [`Style`] with the given [`Color`] and `width`.
|
||||
pub fn with_border(
|
||||
self,
|
||||
color: impl Into<Color>,
|
||||
width: impl Into<Pixels>,
|
||||
) -> Self {
|
||||
/// Updates the text color of the [`Style`].
|
||||
pub fn color(self, color: impl Into<Color>) -> Self {
|
||||
Self {
|
||||
border: Border {
|
||||
color: color.into(),
|
||||
width: width.into().0,
|
||||
..Border::default()
|
||||
},
|
||||
text_color: Some(color.into()),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the border of the [`Style`].
|
||||
pub fn border(self, border: impl Into<Border>) -> Self {
|
||||
Self {
|
||||
border: border.into(),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the background of the [`Style`].
|
||||
pub fn with_background(self, background: impl Into<Background>) -> Self {
|
||||
pub fn background(self, background: impl Into<Background>) -> Self {
|
||||
Self {
|
||||
background: Some(background.into()),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the shadow of the [`Style`].
|
||||
pub fn shadow(self, shadow: impl Into<Shadow>) -> Self {
|
||||
Self {
|
||||
shadow: shadow.into(),
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color> for Style {
|
||||
fn from(color: Color) -> Self {
|
||||
Self::default().with_background(color)
|
||||
Self::default().background(color)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Gradient> for Style {
|
||||
fn from(gradient: Gradient) -> Self {
|
||||
Self::default().with_background(gradient)
|
||||
Self::default().background(gradient)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<gradient::Linear> for Style {
|
||||
fn from(gradient: gradient::Linear) -> Self {
|
||||
Self::default().with_background(gradient)
|
||||
Self::default().background(gradient)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -618,6 +659,12 @@ pub trait Catalog {
|
|||
/// A styling function for a [`Container`].
|
||||
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
|
||||
|
||||
impl<Theme> From<Style> for StyleFn<'_, Theme> {
|
||||
fn from(style: Style) -> Self {
|
||||
Box::new(move |_theme| style)
|
||||
}
|
||||
}
|
||||
|
||||
impl Catalog for Theme {
|
||||
type Class<'a> = StyleFn<'a, Self>;
|
||||
|
||||
|
|
@ -635,13 +682,18 @@ pub fn transparent<Theme>(_theme: &Theme) -> Style {
|
|||
Style::default()
|
||||
}
|
||||
|
||||
/// A [`Container`] with the given [`Background`].
|
||||
pub fn background(background: impl Into<Background>) -> Style {
|
||||
Style::default().background(background)
|
||||
}
|
||||
|
||||
/// A rounded [`Container`] with a background.
|
||||
pub fn rounded_box(theme: &Theme) -> Style {
|
||||
let palette = theme.extended_palette();
|
||||
|
||||
Style {
|
||||
background: Some(palette.background.weak.color.into()),
|
||||
border: Border::rounded(2),
|
||||
border: border::rounded(2),
|
||||
..Style::default()
|
||||
}
|
||||
}
|
||||
|
|
@ -651,12 +703,57 @@ pub fn bordered_box(theme: &Theme) -> Style {
|
|||
let palette = theme.extended_palette();
|
||||
|
||||
Style {
|
||||
background: Some(palette.background.weak.color.into()),
|
||||
background: Some(palette.background.weakest.color.into()),
|
||||
border: Border {
|
||||
width: 1.0,
|
||||
radius: 0.0.into(),
|
||||
radius: 5.0.into(),
|
||||
color: palette.background.strong.color,
|
||||
},
|
||||
..Style::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`Container`] with a dark background and white text.
|
||||
pub fn dark(_theme: &Theme) -> Style {
|
||||
style(theme::palette::Pair {
|
||||
color: color!(0x111111),
|
||||
text: Color::WHITE,
|
||||
})
|
||||
}
|
||||
|
||||
/// A [`Container`] with a primary background color.
|
||||
pub fn primary(theme: &Theme) -> Style {
|
||||
let palette = theme.extended_palette();
|
||||
|
||||
style(palette.primary.base)
|
||||
}
|
||||
|
||||
/// A [`Container`] with a secondary background color.
|
||||
pub fn secondary(theme: &Theme) -> Style {
|
||||
let palette = theme.extended_palette();
|
||||
|
||||
style(palette.secondary.base)
|
||||
}
|
||||
|
||||
/// A [`Container`] with a success background color.
|
||||
pub fn success(theme: &Theme) -> Style {
|
||||
let palette = theme.extended_palette();
|
||||
|
||||
style(palette.success.base)
|
||||
}
|
||||
|
||||
/// A [`Container`] with a danger background color.
|
||||
pub fn danger(theme: &Theme) -> Style {
|
||||
let palette = theme.extended_palette();
|
||||
|
||||
style(palette.danger.base)
|
||||
}
|
||||
|
||||
fn style(pair: theme::palette::Pair) -> Style {
|
||||
Style {
|
||||
background: Some(pair.color.into()),
|
||||
text_color: Some(pair.text),
|
||||
border: border::rounded(2),
|
||||
..Style::default()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,4 +1,21 @@
|
|||
//! Display images in your user interface.
|
||||
//! Images display raster graphics in different formats (PNG, JPG, etc.).
|
||||
//!
|
||||
//! # Example
|
||||
//! ```no_run
|
||||
//! # mod iced { pub mod widget { pub use iced_widget::*; } }
|
||||
//! # pub type State = ();
|
||||
//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||
//! use iced::widget::image;
|
||||
//!
|
||||
//! enum Message {
|
||||
//! // ...
|
||||
//! }
|
||||
//!
|
||||
//! fn view(state: &State) -> Element<'_, Message> {
|
||||
//! image("ferris.png").into()
|
||||
//! }
|
||||
//! ```
|
||||
//! <img src="https://github.com/iced-rs/iced/blob/9712b319bb7a32848001b96bd84977430f14b623/examples/resources/ferris.png?raw=true" width="300">
|
||||
pub mod viewer;
|
||||
pub use viewer::Viewer;
|
||||
|
||||
|
|
@ -22,16 +39,23 @@ pub fn viewer<Handle>(handle: Handle) -> Viewer<Handle> {
|
|||
/// A frame that displays an image while keeping aspect ratio.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use iced_widget::image::{self, Image};
|
||||
/// #
|
||||
/// let image = Image::<image::Handle>::new("resources/ferris.png");
|
||||
/// ```
|
||||
/// # mod iced { pub mod widget { pub use iced_widget::*; } }
|
||||
/// # pub type State = ();
|
||||
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||
/// use iced::widget::image;
|
||||
///
|
||||
/// enum Message {
|
||||
/// // ...
|
||||
/// }
|
||||
///
|
||||
/// fn view(state: &State) -> Element<'_, Message> {
|
||||
/// image("ferris.png").into()
|
||||
/// }
|
||||
/// ```
|
||||
/// <img src="https://github.com/iced-rs/iced/blob/9712b319bb7a32848001b96bd84977430f14b623/examples/resources/ferris.png?raw=true" width="300">
|
||||
#[derive(Debug)]
|
||||
pub struct Image<Handle> {
|
||||
pub struct Image<Handle = image::Handle> {
|
||||
handle: Handle,
|
||||
width: Length,
|
||||
height: Length,
|
||||
|
|
@ -39,11 +63,12 @@ pub struct Image<Handle> {
|
|||
filter_method: FilterMethod,
|
||||
rotation: Rotation,
|
||||
opacity: f32,
|
||||
scale: f32,
|
||||
}
|
||||
|
||||
impl<Handle> Image<Handle> {
|
||||
/// Creates a new [`Image`] with the given path.
|
||||
pub fn new<T: Into<Handle>>(handle: T) -> Self {
|
||||
pub fn new(handle: impl Into<Handle>) -> Self {
|
||||
Image {
|
||||
handle: handle.into(),
|
||||
width: Length::Shrink,
|
||||
|
|
@ -52,6 +77,7 @@ impl<Handle> Image<Handle> {
|
|||
filter_method: FilterMethod::default(),
|
||||
rotation: Rotation::default(),
|
||||
opacity: 1.0,
|
||||
scale: 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -95,6 +121,15 @@ impl<Handle> Image<Handle> {
|
|||
self.opacity = opacity.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the scale of the [`Image`].
|
||||
///
|
||||
/// The region of the [`Image`] drawn will be scaled from the center by the given scale factor.
|
||||
/// This can be useful to create certain effects and animations, like smooth zoom in / out.
|
||||
pub fn scale(mut self, scale: impl Into<f32>) -> Self {
|
||||
self.scale = scale.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes the layout of an [`Image`].
|
||||
|
|
@ -143,11 +178,13 @@ where
|
|||
pub fn draw<Renderer, Handle>(
|
||||
renderer: &mut Renderer,
|
||||
layout: Layout<'_>,
|
||||
viewport: &Rectangle,
|
||||
handle: &Handle,
|
||||
content_fit: ContentFit,
|
||||
filter_method: FilterMethod,
|
||||
rotation: Rotation,
|
||||
opacity: f32,
|
||||
scale: f32,
|
||||
) where
|
||||
Renderer: image::Renderer<Handle = Handle>,
|
||||
Handle: Clone,
|
||||
|
|
@ -159,12 +196,12 @@ pub fn draw<Renderer, Handle>(
|
|||
let bounds = layout.bounds();
|
||||
let adjusted_fit = content_fit.fit(rotated_size, bounds.size());
|
||||
|
||||
let scale = Vector::new(
|
||||
let fit_scale = Vector::new(
|
||||
adjusted_fit.width / rotated_size.width,
|
||||
adjusted_fit.height / rotated_size.height,
|
||||
);
|
||||
|
||||
let final_size = image_size * scale;
|
||||
let final_size = image_size * fit_scale * scale;
|
||||
|
||||
let position = match content_fit {
|
||||
ContentFit::None => Point::new(
|
||||
|
|
@ -181,17 +218,22 @@ pub fn draw<Renderer, Handle>(
|
|||
|
||||
let render = |renderer: &mut Renderer| {
|
||||
renderer.draw_image(
|
||||
handle.clone(),
|
||||
filter_method,
|
||||
image::Image {
|
||||
handle: handle.clone(),
|
||||
filter_method,
|
||||
rotation: rotation.radians(),
|
||||
opacity,
|
||||
snap: true,
|
||||
},
|
||||
drawing_bounds,
|
||||
rotation.radians(),
|
||||
opacity,
|
||||
);
|
||||
};
|
||||
|
||||
if adjusted_fit.width > bounds.width || adjusted_fit.height > bounds.height
|
||||
{
|
||||
renderer.with_layer(bounds, render);
|
||||
if let Some(bounds) = bounds.intersection(viewport) {
|
||||
renderer.with_layer(bounds, render);
|
||||
}
|
||||
} else {
|
||||
render(renderer);
|
||||
}
|
||||
|
|
@ -235,16 +277,18 @@ where
|
|||
_style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
_cursor: mouse::Cursor,
|
||||
_viewport: &Rectangle,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
draw(
|
||||
renderer,
|
||||
layout,
|
||||
viewport,
|
||||
&self.handle,
|
||||
self.content_fit,
|
||||
self.filter_method,
|
||||
self.rotation,
|
||||
self.opacity,
|
||||
self.scale,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
//! Zoom and pan on an image.
|
||||
use crate::core::event::{self, Event};
|
||||
use crate::core::image;
|
||||
use crate::core::image::{self, FilterMethod};
|
||||
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, Radians, Rectangle,
|
||||
Shell, Size, Vector, Widget,
|
||||
Clipboard, ContentFit, Element, Event, Image, Layout, Length, Pixels,
|
||||
Point, Radians, Rectangle, Shell, Size, Vector, Widget,
|
||||
};
|
||||
|
||||
/// A frame that displays an image with the ability to zoom in/out and pan.
|
||||
|
|
@ -20,30 +19,38 @@ pub struct Viewer<Handle> {
|
|||
max_scale: f32,
|
||||
scale_step: f32,
|
||||
handle: Handle,
|
||||
filter_method: image::FilterMethod,
|
||||
filter_method: FilterMethod,
|
||||
content_fit: ContentFit,
|
||||
}
|
||||
|
||||
impl<Handle> Viewer<Handle> {
|
||||
/// Creates a new [`Viewer`] with the given [`State`].
|
||||
pub fn new(handle: Handle) -> Self {
|
||||
pub fn new<T: Into<Handle>>(handle: T) -> Self {
|
||||
Viewer {
|
||||
handle,
|
||||
handle: handle.into(),
|
||||
padding: 0.0,
|
||||
width: Length::Shrink,
|
||||
height: Length::Shrink,
|
||||
min_scale: 0.25,
|
||||
max_scale: 10.0,
|
||||
scale_step: 0.10,
|
||||
filter_method: image::FilterMethod::default(),
|
||||
filter_method: FilterMethod::default(),
|
||||
content_fit: ContentFit::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the [`image::FilterMethod`] of the [`Viewer`].
|
||||
/// Sets the [`FilterMethod`] of the [`Viewer`].
|
||||
pub fn filter_method(mut self, filter_method: image::FilterMethod) -> Self {
|
||||
self.filter_method = filter_method;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`ContentFit`] of the [`Viewer`].
|
||||
pub fn content_fit(mut self, content_fit: ContentFit) -> Self {
|
||||
self.content_fit = content_fit;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the padding of the [`Viewer`].
|
||||
pub fn padding(mut self, padding: impl Into<Pixels>) -> Self {
|
||||
self.padding = padding.into().0;
|
||||
|
|
@ -115,58 +122,52 @@ where
|
|||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
let Size { width, height } = renderer.measure_image(&self.handle);
|
||||
// The raw w/h of the underlying image
|
||||
let image_size = renderer.measure_image(&self.handle);
|
||||
let image_size =
|
||||
Size::new(image_size.width as f32, image_size.height as f32);
|
||||
|
||||
let mut size = limits.resolve(
|
||||
self.width,
|
||||
self.height,
|
||||
Size::new(width as f32, height as f32),
|
||||
);
|
||||
// The size to be available to the widget prior to `Shrink`ing
|
||||
let raw_size = limits.resolve(self.width, self.height, image_size);
|
||||
|
||||
let expansion_size = if height > width {
|
||||
self.width
|
||||
} else {
|
||||
self.height
|
||||
// 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,
|
||||
},
|
||||
};
|
||||
|
||||
// Only calculate viewport sizes if the images are constrained to a limited space.
|
||||
// If they are Fill|Portion let them expand within their allotted 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)
|
||||
layout::Node::new(final_size)
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
fn update(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: Event,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &Renderer,
|
||||
_clipboard: &mut dyn Clipboard,
|
||||
_shell: &mut Shell<'_, Message>,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
_viewport: &Rectangle,
|
||||
) -> event::Status {
|
||||
) {
|
||||
let bounds = layout.bounds();
|
||||
|
||||
match event {
|
||||
Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
|
||||
let Some(cursor_position) = cursor.position_over(bounds) else {
|
||||
return event::Status::Ignored;
|
||||
return;
|
||||
};
|
||||
|
||||
match delta {
|
||||
match *delta {
|
||||
mouse::ScrollDelta::Lines { y, .. }
|
||||
| mouse::ScrollDelta::Pixels { y, .. } => {
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
|
|
@ -182,11 +183,12 @@ where
|
|||
})
|
||||
.clamp(self.min_scale, self.max_scale);
|
||||
|
||||
let image_size = image_size(
|
||||
let scaled_size = scaled_image_size(
|
||||
renderer,
|
||||
&self.handle,
|
||||
state,
|
||||
bounds.size(),
|
||||
self.content_fit,
|
||||
);
|
||||
|
||||
let factor = state.scale / previous_scale - 1.0;
|
||||
|
|
@ -198,12 +200,12 @@ where
|
|||
+ state.current_offset * factor;
|
||||
|
||||
state.current_offset = Vector::new(
|
||||
if image_size.width > bounds.width {
|
||||
if scaled_size.width > bounds.width {
|
||||
state.current_offset.x + adjustment.x
|
||||
} else {
|
||||
0.0
|
||||
},
|
||||
if image_size.height > bounds.height {
|
||||
if scaled_size.height > bounds.height {
|
||||
state.current_offset.y + adjustment.y
|
||||
} else {
|
||||
0.0
|
||||
|
|
@ -213,11 +215,12 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
event::Status::Captured
|
||||
shell.request_redraw();
|
||||
shell.capture_event();
|
||||
}
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
|
||||
let Some(cursor_position) = cursor.position_over(bounds) else {
|
||||
return event::Status::Ignored;
|
||||
return;
|
||||
};
|
||||
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
|
|
@ -225,49 +228,48 @@ where
|
|||
state.cursor_grabbed_at = Some(cursor_position);
|
||||
state.starting_offset = state.current_offset;
|
||||
|
||||
event::Status::Captured
|
||||
shell.request_redraw();
|
||||
shell.capture_event();
|
||||
}
|
||||
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
|
||||
shell.request_redraw();
|
||||
shell.capture_event();
|
||||
}
|
||||
}
|
||||
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(
|
||||
let scaled_size = scaled_image_size(
|
||||
renderer,
|
||||
&self.handle,
|
||||
state,
|
||||
bounds.size(),
|
||||
self.content_fit,
|
||||
);
|
||||
|
||||
let hidden_width = (image_size.width - bounds.width / 2.0)
|
||||
let hidden_width = (scaled_size.width - bounds.width / 2.0)
|
||||
.max(0.0)
|
||||
.round();
|
||||
|
||||
let hidden_height = (image_size.height
|
||||
let hidden_height = (scaled_size.height
|
||||
- bounds.height / 2.0)
|
||||
.max(0.0)
|
||||
.round();
|
||||
|
||||
let delta = position - origin;
|
||||
let delta = *position - origin;
|
||||
|
||||
let x = if bounds.width < image_size.width {
|
||||
let x = if bounds.width < scaled_size.width {
|
||||
(state.starting_offset.x - delta.x)
|
||||
.clamp(-hidden_width, hidden_width)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let y = if bounds.height < image_size.height {
|
||||
let y = if bounds.height < scaled_size.height {
|
||||
(state.starting_offset.y - delta.y)
|
||||
.clamp(-hidden_height, hidden_height)
|
||||
} else {
|
||||
|
|
@ -275,13 +277,11 @@ where
|
|||
};
|
||||
|
||||
state.current_offset = Vector::new(x, y);
|
||||
|
||||
event::Status::Captured
|
||||
} else {
|
||||
event::Status::Ignored
|
||||
shell.request_redraw();
|
||||
shell.capture_event();
|
||||
}
|
||||
}
|
||||
_ => event::Status::Ignored,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -319,33 +319,46 @@ where
|
|||
let state = tree.state.downcast_ref::<State>();
|
||||
let bounds = layout.bounds();
|
||||
|
||||
let image_size =
|
||||
image_size(renderer, &self.handle, state, bounds.size());
|
||||
let final_size = scaled_image_size(
|
||||
renderer,
|
||||
&self.handle,
|
||||
state,
|
||||
bounds.size(),
|
||||
self.content_fit,
|
||||
);
|
||||
|
||||
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,
|
||||
);
|
||||
let diff_w = bounds.width - final_size.width;
|
||||
let diff_h = bounds.height - final_size.height;
|
||||
|
||||
image_top_left - state.offset(bounds, image_size)
|
||||
let image_top_left = match self.content_fit {
|
||||
ContentFit::None => {
|
||||
Vector::new(diff_w.max(0.0) / 2.0, diff_h.max(0.0) / 2.0)
|
||||
}
|
||||
_ => Vector::new(diff_w / 2.0, diff_h / 2.0),
|
||||
};
|
||||
|
||||
image_top_left - state.offset(bounds, final_size)
|
||||
};
|
||||
|
||||
renderer.with_layer(bounds, |renderer| {
|
||||
let drawing_bounds = Rectangle::new(bounds.position(), final_size);
|
||||
|
||||
let render = |renderer: &mut Renderer| {
|
||||
renderer.with_translation(translation, |renderer| {
|
||||
renderer.draw_image(
|
||||
self.handle.clone(),
|
||||
self.filter_method,
|
||||
Rectangle {
|
||||
x: bounds.x,
|
||||
y: bounds.y,
|
||||
..Rectangle::with_size(image_size)
|
||||
Image {
|
||||
handle: self.handle.clone(),
|
||||
filter_method: self.filter_method,
|
||||
rotation: Radians(0.0),
|
||||
opacity: 1.0,
|
||||
snap: true,
|
||||
},
|
||||
Radians(0.0),
|
||||
1.0,
|
||||
drawing_bounds,
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
renderer.with_layer(bounds, render);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -411,32 +424,23 @@ where
|
|||
/// 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>(
|
||||
pub fn scaled_image_size<Renderer>(
|
||||
renderer: &Renderer,
|
||||
handle: &<Renderer as image::Renderer>::Handle,
|
||||
state: &State,
|
||||
bounds: Size,
|
||||
content_fit: ContentFit,
|
||||
) -> Size
|
||||
where
|
||||
Renderer: image::Renderer,
|
||||
{
|
||||
let Size { width, height } = renderer.measure_image(handle);
|
||||
let image_size = Size::new(width as f32, height as f32);
|
||||
|
||||
let (width, height) = {
|
||||
let dimensions = (width as f32, height as f32);
|
||||
let adjusted_fit = content_fit.fit(image_size, bounds);
|
||||
|
||||
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)
|
||||
Size::new(
|
||||
adjusted_fit.width * state.scale,
|
||||
adjusted_fit.height * state.scale,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
//! Use widgets that can provide hints to ensure continuity.
|
||||
//! Keyed widgets can provide hints to ensure continuity.
|
||||
//!
|
||||
//! # What is continuity?
|
||||
//! Continuity is the feeling of persistence of state.
|
||||
|
|
@ -41,13 +41,35 @@ pub mod column;
|
|||
|
||||
pub use column::Column;
|
||||
|
||||
/// Creates a [`Column`] with the given children.
|
||||
/// Creates a keyed [`Column`] with the given children.
|
||||
///
|
||||
/// Keyed columns distribute content vertically while keeping continuity.
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// # mod iced { pub mod widget { pub use iced_widget::*; } }
|
||||
/// # pub type State = ();
|
||||
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||
/// use iced::widget::keyed_column;
|
||||
///
|
||||
/// enum Message {
|
||||
/// // ...
|
||||
/// }
|
||||
///
|
||||
/// fn view(state: &State) -> Element<'_, Message> {
|
||||
/// keyed_column![
|
||||
/// (0, "Item 0"),
|
||||
/// (1, "Item 1"),
|
||||
/// (2, "Item 2"),
|
||||
/// ].into()
|
||||
/// }
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! keyed_column {
|
||||
() => (
|
||||
$crate::Column::new()
|
||||
$crate::keyed::Column::new()
|
||||
);
|
||||
($($x:expr),+ $(,)?) => (
|
||||
$crate::keyed::Column::with_children(vec![$($crate::core::Element::from($x)),+])
|
||||
($(($key:expr, $x:expr)),+ $(,)?) => (
|
||||
$crate::keyed::Column::with_children(vec![$(($key, $crate::core::Element::from($x))),+])
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,34 @@
|
|||
//! Distribute content vertically.
|
||||
use crate::core::event::{self, Event};
|
||||
//! Keyed columns distribute content vertically while keeping continuity.
|
||||
use crate::core::layout;
|
||||
use crate::core::mouse;
|
||||
use crate::core::overlay;
|
||||
use crate::core::renderer;
|
||||
use crate::core::widget::tree::{self, Tree};
|
||||
use crate::core::widget::Operation;
|
||||
use crate::core::widget::tree::{self, Tree};
|
||||
use crate::core::{
|
||||
Alignment, Clipboard, Element, Layout, Length, Padding, Pixels, Rectangle,
|
||||
Shell, Size, Vector, Widget,
|
||||
Alignment, Clipboard, Element, Event, Layout, Length, Padding, Pixels,
|
||||
Rectangle, Shell, Size, Vector, Widget,
|
||||
};
|
||||
|
||||
/// A container that distributes its contents vertically.
|
||||
/// A container that distributes its contents vertically while keeping continuity.
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// # mod iced { pub mod widget { pub use iced_widget::*; } }
|
||||
/// # pub type State = ();
|
||||
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||
/// use iced::widget::{keyed_column, text};
|
||||
///
|
||||
/// enum Message {
|
||||
/// // ...
|
||||
/// }
|
||||
///
|
||||
/// fn view(state: &State) -> Element<'_, Message> {
|
||||
/// keyed_column((0..=100).map(|i| {
|
||||
/// (i, text!("Item {i}").into())
|
||||
/// })).into()
|
||||
/// }
|
||||
/// ```
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Column<
|
||||
'a,
|
||||
|
|
@ -168,7 +185,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Key, Message, Renderer> Default for Column<'a, Key, Message, Renderer>
|
||||
impl<Key, Message, Renderer> Default for Column<'_, Key, Message, Renderer>
|
||||
where
|
||||
Key: Copy + PartialEq,
|
||||
Renderer: crate::core::Renderer,
|
||||
|
|
@ -185,8 +202,8 @@ where
|
|||
keys: Vec<Key>,
|
||||
}
|
||||
|
||||
impl<'a, Key, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Column<'a, Key, Message, Theme, Renderer>
|
||||
impl<Key, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Column<'_, Key, Message, Theme, Renderer>
|
||||
where
|
||||
Renderer: crate::core::Renderer,
|
||||
Key: Copy + PartialEq + 'static,
|
||||
|
|
@ -265,7 +282,7 @@ where
|
|||
tree: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn Operation<Message>,
|
||||
operation: &mut dyn Operation,
|
||||
) {
|
||||
operation.container(None, layout.bounds(), &mut |operation| {
|
||||
self.children
|
||||
|
|
@ -280,34 +297,28 @@ where
|
|||
});
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
fn update(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: Event,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
viewport: &Rectangle,
|
||||
) -> event::Status {
|
||||
self.children
|
||||
) {
|
||||
for ((child, state), layout) in 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,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
viewport,
|
||||
)
|
||||
})
|
||||
.fold(event::Status::Ignored, event::Status::merge)
|
||||
{
|
||||
child.as_widget_mut().update(
|
||||
state, event, layout, cursor, renderer, clipboard, shell,
|
||||
viewport,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
|
|
|
|||
|
|
@ -4,21 +4,21 @@ pub(crate) mod helpers;
|
|||
pub mod component;
|
||||
pub mod responsive;
|
||||
|
||||
#[allow(deprecated)]
|
||||
pub use component::Component;
|
||||
pub use responsive::Responsive;
|
||||
|
||||
mod cache;
|
||||
|
||||
use crate::core::event::{self, Event};
|
||||
use crate::core::Element;
|
||||
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, Length, Point, Rectangle, Shell, Size, Vector,
|
||||
self, Clipboard, Event, Length, Point, Rectangle, Shell, Size, Vector,
|
||||
};
|
||||
use crate::runtime::overlay::Nested;
|
||||
|
||||
|
|
@ -29,6 +29,7 @@ use std::hash::{Hash, Hasher as H};
|
|||
use std::rc::Rc;
|
||||
|
||||
/// A widget that only rebuilds its contents when necessary.
|
||||
#[cfg(feature = "lazy")]
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Lazy<'a, Message, Theme, Renderer, Dependency, View> {
|
||||
dependency: Dependency,
|
||||
|
|
@ -182,7 +183,7 @@ where
|
|||
tree: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn widget::Operation<Message>,
|
||||
operation: &mut dyn widget::Operation,
|
||||
) {
|
||||
self.with_element(|element| {
|
||||
element.as_widget().operate(
|
||||
|
|
@ -194,19 +195,19 @@ where
|
|||
});
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
fn update(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: Event,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
viewport: &Rectangle,
|
||||
) -> event::Status {
|
||||
) {
|
||||
self.with_element_mut(|element| {
|
||||
element.as_widget_mut().on_event(
|
||||
element.as_widget_mut().update(
|
||||
&mut tree.children[0],
|
||||
event,
|
||||
layout,
|
||||
|
|
@ -215,8 +216,8 @@ where
|
|||
clipboard,
|
||||
shell,
|
||||
viewport,
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
|
|
@ -267,30 +268,41 @@ where
|
|||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
translation: Vector,
|
||||
) -> Option<overlay::Element<'_, Message, Theme, 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, translation)
|
||||
.map(|overlay| RefCell::new(Nested::new(overlay)))
|
||||
},
|
||||
}
|
||||
.build(),
|
||||
));
|
||||
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
|
||||
let overlay = 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, translation)
|
||||
.map(|overlay| RefCell::new(Nested::new(overlay)))
|
||||
},
|
||||
}
|
||||
.build();
|
||||
|
||||
Some(overlay::Element::new(Box::new(overlay)))
|
||||
#[allow(clippy::redundant_closure_for_method_calls)]
|
||||
if overlay.with_overlay(|overlay| overlay.is_some()) {
|
||||
Some(overlay::Element::new(Box::new(Overlay(Some(overlay)))))
|
||||
} else {
|
||||
let heads = overlay.into_heads();
|
||||
|
||||
// - You may not like it, but this is what peak performance looks like
|
||||
// - TODO: Get rid of ouroboros, for good
|
||||
// - What?!
|
||||
*self.element.borrow().as_ref().unwrap().borrow_mut() =
|
||||
Some(heads.element);
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -309,16 +321,14 @@ struct Overlay<'a, Message, Theme, Renderer>(
|
|||
Option<Inner<'a, Message, Theme, Renderer>>,
|
||||
);
|
||||
|
||||
impl<'a, Message, Theme, Renderer> Drop
|
||||
for Overlay<'a, Message, Theme, Renderer>
|
||||
{
|
||||
impl<Message, Theme, Renderer> Drop for Overlay<'_, Message, Theme, Renderer> {
|
||||
fn drop(&mut self) {
|
||||
let heads = self.0.take().unwrap().into_heads();
|
||||
(*heads.cell.borrow_mut()) = Some(heads.element);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> Overlay<'a, Message, Theme, Renderer> {
|
||||
impl<Message, Theme, Renderer> Overlay<'_, Message, Theme, Renderer> {
|
||||
fn with_overlay_maybe<T>(
|
||||
&self,
|
||||
f: impl FnOnce(&mut Nested<'_, Message, Theme, Renderer>) -> T,
|
||||
|
|
@ -338,8 +348,8 @@ impl<'a, Message, Theme, Renderer> Overlay<'a, Message, Theme, Renderer> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> overlay::Overlay<Message, Theme, Renderer>
|
||||
for Overlay<'a, Message, Theme, Renderer>
|
||||
impl<Message, Theme, Renderer> overlay::Overlay<Message, Theme, Renderer>
|
||||
for Overlay<'_, Message, Theme, Renderer>
|
||||
where
|
||||
Renderer: core::Renderer,
|
||||
{
|
||||
|
|
@ -374,19 +384,18 @@ where
|
|||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
fn update(
|
||||
&mut self,
|
||||
event: Event,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
self.with_overlay_mut_maybe(|overlay| {
|
||||
overlay.on_event(event, layout, cursor, renderer, clipboard, shell)
|
||||
})
|
||||
.unwrap_or(event::Status::Ignored)
|
||||
) {
|
||||
let _ = self.with_overlay_mut_maybe(|overlay| {
|
||||
overlay.update(event, layout, cursor, renderer, clipboard, shell);
|
||||
});
|
||||
}
|
||||
|
||||
fn is_over(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
use crate::core::overlay;
|
||||
#![allow(dead_code)]
|
||||
use crate::core::Element;
|
||||
use crate::core::overlay;
|
||||
|
||||
use ouroboros::self_referencing;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//! Build and reuse custom widgets using The Elm Architecture.
|
||||
use crate::core::event;
|
||||
#![allow(deprecated)]
|
||||
use crate::core::layout::{self, Layout};
|
||||
use crate::core::mouse;
|
||||
use crate::core::overlay;
|
||||
|
|
@ -30,6 +30,25 @@ use std::rc::Rc;
|
|||
///
|
||||
/// Additionally, a [`Component`] is capable of producing a `Message` to notify
|
||||
/// the parent application of any relevant interactions.
|
||||
///
|
||||
/// # State
|
||||
/// A component can store its state in one of two ways: either as data within the
|
||||
/// implementor of the trait, or in a type [`State`][Component::State] that is managed
|
||||
/// by the runtime and provided to the trait methods. These two approaches are not
|
||||
/// mutually exclusive and have opposite pros and cons.
|
||||
///
|
||||
/// For instance, if a piece of state is needed by multiple components that reside
|
||||
/// in different branches of the tree, then it's more convenient to let a common
|
||||
/// ancestor store it and pass it down.
|
||||
///
|
||||
/// On the other hand, if a piece of state is only needed by the component itself,
|
||||
/// you can store it as part of its internal [`State`][Component::State].
|
||||
#[cfg(feature = "lazy")]
|
||||
#[deprecated(
|
||||
since = "0.13.0",
|
||||
note = "components introduce encapsulated state and hamper the use of a single source of truth. \
|
||||
Instead, leverage the Elm Architecture directly, or implement a custom widget"
|
||||
)]
|
||||
pub trait Component<Message, Theme = crate::Theme, Renderer = crate::Renderer> {
|
||||
/// The internal state of this [`Component`].
|
||||
type State: Default;
|
||||
|
|
@ -58,8 +77,9 @@ pub trait Component<Message, Theme = crate::Theme, Renderer = crate::Renderer> {
|
|||
/// By default, it does nothing.
|
||||
fn operate(
|
||||
&self,
|
||||
_bounds: Rectangle,
|
||||
_state: &mut Self::State,
|
||||
_operation: &mut dyn widget::Operation<Message>,
|
||||
_operation: &mut dyn widget::Operation,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
@ -121,8 +141,8 @@ struct State<'a, Message: 'a, Theme: 'a, Renderer: 'a, Event: 'a, S: 'a> {
|
|||
element: Option<Element<'this, Event, Theme, Renderer>>,
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer, Event, S>
|
||||
Instance<'a, Message, Theme, Renderer, Event, S>
|
||||
impl<Message, Theme, Renderer, Event, S>
|
||||
Instance<'_, Message, Theme, Renderer, Event, S>
|
||||
where
|
||||
S: Default + 'static,
|
||||
Renderer: renderer::Renderer,
|
||||
|
|
@ -172,11 +192,13 @@ where
|
|||
|
||||
fn rebuild_element_with_operation(
|
||||
&self,
|
||||
operation: &mut dyn widget::Operation<Message>,
|
||||
layout: Layout<'_>,
|
||||
operation: &mut dyn widget::Operation,
|
||||
) {
|
||||
let heads = self.state.borrow_mut().take().unwrap().into_heads();
|
||||
|
||||
heads.component.operate(
|
||||
layout.bounds(),
|
||||
self.tree
|
||||
.borrow_mut()
|
||||
.borrow_mut()
|
||||
|
|
@ -231,8 +253,8 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer, Event, S> Widget<Message, Theme, Renderer>
|
||||
for Instance<'a, Message, Theme, Renderer, Event, S>
|
||||
impl<Message, Theme, Renderer, Event, S> Widget<Message, Theme, Renderer>
|
||||
for Instance<'_, Message, Theme, Renderer, Event, S>
|
||||
where
|
||||
S: 'static + Default,
|
||||
Renderer: core::Renderer,
|
||||
|
|
@ -247,7 +269,10 @@ where
|
|||
state: tree::State::new(S::default()),
|
||||
children: vec![Tree::empty()],
|
||||
})));
|
||||
|
||||
*self.tree.borrow_mut() = state.clone();
|
||||
self.diff_self();
|
||||
|
||||
tree::State::new(state)
|
||||
}
|
||||
|
||||
|
|
@ -291,23 +316,23 @@ where
|
|||
})
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
fn update(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: core::Event,
|
||||
event: &core::Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
viewport: &Rectangle,
|
||||
) -> event::Status {
|
||||
) {
|
||||
let mut local_messages = Vec::new();
|
||||
let mut local_shell = Shell::new(&mut local_messages);
|
||||
|
||||
let t = tree.state.downcast_mut::<Rc<RefCell<Option<Tree>>>>();
|
||||
let event_status = self.with_element_mut(|element| {
|
||||
element.as_widget_mut().on_event(
|
||||
self.with_element_mut(|element| {
|
||||
element.as_widget_mut().update(
|
||||
&mut t.borrow_mut().as_mut().unwrap().children[0],
|
||||
event,
|
||||
layout,
|
||||
|
|
@ -316,15 +341,17 @@ where
|
|||
clipboard,
|
||||
&mut local_shell,
|
||||
viewport,
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
local_shell.revalidate_layout(|| shell.invalidate_layout());
|
||||
|
||||
if let Some(redraw_request) = local_shell.redraw_request() {
|
||||
shell.request_redraw(redraw_request);
|
||||
if local_shell.is_event_captured() {
|
||||
shell.capture_event();
|
||||
}
|
||||
|
||||
local_shell.revalidate_layout(|| shell.invalidate_layout());
|
||||
shell.request_redraw_at(local_shell.redraw_request());
|
||||
shell.request_input_method(local_shell.input_method());
|
||||
|
||||
if !local_messages.is_empty() {
|
||||
let mut heads = self.state.take().unwrap().into_heads();
|
||||
|
||||
|
|
@ -349,8 +376,6 @@ where
|
|||
|
||||
shell.invalidate_layout();
|
||||
}
|
||||
|
||||
event_status
|
||||
}
|
||||
|
||||
fn operate(
|
||||
|
|
@ -358,62 +383,9 @@ where
|
|||
tree: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn widget::Operation<Message>,
|
||||
operation: &mut dyn widget::Operation,
|
||||
) {
|
||||
self.rebuild_element_with_operation(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>,
|
||||
bounds: Rectangle,
|
||||
operate_on_children: &mut dyn FnMut(
|
||||
&mut dyn widget::Operation<T>,
|
||||
),
|
||||
) {
|
||||
self.operation.container(id, bounds, &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);
|
||||
}
|
||||
|
||||
fn scrollable(
|
||||
&mut self,
|
||||
state: &mut dyn widget::operation::Scrollable,
|
||||
id: Option<&widget::Id>,
|
||||
bounds: Rectangle,
|
||||
translation: Vector,
|
||||
) {
|
||||
self.operation.scrollable(state, id, bounds, translation);
|
||||
}
|
||||
|
||||
fn custom(
|
||||
&mut self,
|
||||
state: &mut dyn std::any::Any,
|
||||
id: Option<&widget::Id>,
|
||||
) {
|
||||
self.operation.custom(state, id);
|
||||
}
|
||||
}
|
||||
self.rebuild_element_with_operation(layout, operation);
|
||||
|
||||
let tree = tree.state.downcast_mut::<Rc<RefCell<Option<Tree>>>>();
|
||||
self.with_element(|element| {
|
||||
|
|
@ -421,7 +393,7 @@ where
|
|||
&mut tree.borrow_mut().as_mut().unwrap().children[0],
|
||||
layout,
|
||||
renderer,
|
||||
&mut MapOperation { operation },
|
||||
operation,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
@ -479,44 +451,48 @@ where
|
|||
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
|
||||
self.rebuild_element_if_necessary();
|
||||
|
||||
let tree = tree
|
||||
.state
|
||||
.downcast_mut::<Rc<RefCell<Option<Tree>>>>()
|
||||
.borrow_mut()
|
||||
.take()
|
||||
.unwrap();
|
||||
let state = tree.state.downcast_mut::<Rc<RefCell<Option<Tree>>>>();
|
||||
let tree = state.borrow_mut().take().unwrap();
|
||||
|
||||
let overlay = Overlay(Some(
|
||||
InnerBuilder {
|
||||
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,
|
||||
translation,
|
||||
)
|
||||
.map(|overlay| {
|
||||
RefCell::new(Nested::new(overlay))
|
||||
})
|
||||
},
|
||||
)
|
||||
},
|
||||
}
|
||||
.build(),
|
||||
));
|
||||
let overlay = InnerBuilder {
|
||||
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,
|
||||
translation,
|
||||
)
|
||||
.map(|overlay| RefCell::new(Nested::new(overlay)))
|
||||
},
|
||||
)
|
||||
},
|
||||
}
|
||||
.build();
|
||||
|
||||
Some(overlay::Element::new(Box::new(OverlayInstance {
|
||||
overlay: Some(overlay),
|
||||
})))
|
||||
#[allow(clippy::redundant_closure_for_method_calls)]
|
||||
if overlay.with_overlay(|overlay| overlay.is_some()) {
|
||||
Some(overlay::Element::new(Box::new(OverlayInstance {
|
||||
overlay: Some(Overlay(Some(overlay))), // Beautiful, I know
|
||||
})))
|
||||
} else {
|
||||
let heads = overlay.into_heads();
|
||||
|
||||
// - You may not like it, but this is what peak performance looks like
|
||||
// - TODO: Get rid of ouroboros, for good
|
||||
// - What?!
|
||||
*state.borrow_mut() = Some(heads.tree);
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -524,8 +500,8 @@ struct Overlay<'a, 'b, Message, Theme, Renderer, Event, S>(
|
|||
Option<Inner<'a, 'b, Message, Theme, Renderer, Event, S>>,
|
||||
);
|
||||
|
||||
impl<'a, 'b, Message, Theme, Renderer, Event, S> Drop
|
||||
for Overlay<'a, 'b, Message, Theme, Renderer, Event, S>
|
||||
impl<Message, Theme, Renderer, Event, S> Drop
|
||||
for Overlay<'_, '_, Message, Theme, Renderer, Event, S>
|
||||
{
|
||||
fn drop(&mut self) {
|
||||
if let Some(heads) = self.0.take().map(Inner::into_heads) {
|
||||
|
|
@ -549,8 +525,8 @@ struct OverlayInstance<'a, 'b, Message, Theme, Renderer, Event, S> {
|
|||
overlay: Option<Overlay<'a, 'b, Message, Theme, Renderer, Event, S>>,
|
||||
}
|
||||
|
||||
impl<'a, 'b, Message, Theme, Renderer, Event, S>
|
||||
OverlayInstance<'a, 'b, Message, Theme, Renderer, Event, S>
|
||||
impl<Message, Theme, Renderer, Event, S>
|
||||
OverlayInstance<'_, '_, Message, Theme, Renderer, Event, S>
|
||||
{
|
||||
fn with_overlay_maybe<T>(
|
||||
&self,
|
||||
|
|
@ -583,9 +559,9 @@ impl<'a, 'b, Message, Theme, Renderer, Event, S>
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b, Message, Theme, Renderer, Event, S>
|
||||
impl<Message, Theme, Renderer, Event, S>
|
||||
overlay::Overlay<Message, Theme, Renderer>
|
||||
for OverlayInstance<'a, 'b, Message, Theme, Renderer, Event, S>
|
||||
for OverlayInstance<'_, '_, Message, Theme, Renderer, Event, S>
|
||||
where
|
||||
Renderer: core::Renderer,
|
||||
S: 'static + Default,
|
||||
|
|
@ -621,36 +597,36 @@ where
|
|||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
fn update(
|
||||
&mut self,
|
||||
event: core::Event,
|
||||
event: &core::Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
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,
|
||||
renderer,
|
||||
clipboard,
|
||||
&mut local_shell,
|
||||
)
|
||||
})
|
||||
.unwrap_or(event::Status::Ignored);
|
||||
let _ = self.with_overlay_mut_maybe(|overlay| {
|
||||
overlay.update(
|
||||
event,
|
||||
layout,
|
||||
cursor,
|
||||
renderer,
|
||||
clipboard,
|
||||
&mut local_shell,
|
||||
);
|
||||
});
|
||||
|
||||
if local_shell.is_event_captured() {
|
||||
shell.capture_event();
|
||||
}
|
||||
|
||||
local_shell.revalidate_layout(|| shell.invalidate_layout());
|
||||
|
||||
if let Some(redraw_request) = local_shell.redraw_request() {
|
||||
shell.request_redraw(redraw_request);
|
||||
}
|
||||
shell.request_redraw_at(local_shell.redraw_request());
|
||||
shell.request_input_method(local_shell.input_method());
|
||||
|
||||
if !local_messages.is_empty() {
|
||||
let mut inner =
|
||||
|
|
@ -687,8 +663,6 @@ where
|
|||
|
||||
shell.invalidate_layout();
|
||||
}
|
||||
|
||||
event_status
|
||||
}
|
||||
|
||||
fn is_over(
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
use crate::core::{self, Element, Size};
|
||||
use crate::lazy::component::{self, Component};
|
||||
use crate::lazy::{Lazy, Responsive};
|
||||
use crate::lazy::component;
|
||||
|
||||
use std::hash::Hash;
|
||||
|
||||
#[allow(deprecated)]
|
||||
pub use crate::lazy::{Component, Lazy, Responsive};
|
||||
|
||||
/// Creates a new [`Lazy`] widget with the given data `Dependency` and a
|
||||
/// closure that can turn this data into a widget tree.
|
||||
#[cfg(feature = "lazy")]
|
||||
|
|
@ -21,6 +23,12 @@ where
|
|||
/// Turns an implementor of [`Component`] into an [`Element`] that can be
|
||||
/// embedded in any application.
|
||||
#[cfg(feature = "lazy")]
|
||||
#[deprecated(
|
||||
since = "0.13.0",
|
||||
note = "components introduce encapsulated state and hamper the use of a single source of truth. \
|
||||
Instead, leverage the Elm Architecture directly, or implement a custom widget"
|
||||
)]
|
||||
#[allow(deprecated)]
|
||||
pub fn component<'a, C, Message, Theme, Renderer>(
|
||||
component: C,
|
||||
) -> Element<'a, Message, Theme, Renderer>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
use crate::core::event::{self, Event};
|
||||
use crate::core::layout::{self, Layout};
|
||||
use crate::core::mouse;
|
||||
use crate::core::overlay;
|
||||
|
|
@ -6,8 +5,8 @@ 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, Vector,
|
||||
Widget,
|
||||
self, Clipboard, Element, Event, Length, Point, Rectangle, Shell, Size,
|
||||
Vector, Widget,
|
||||
};
|
||||
use crate::horizontal_space;
|
||||
use crate::runtime::overlay::Nested;
|
||||
|
|
@ -21,6 +20,7 @@ use std::ops::Deref;
|
|||
///
|
||||
/// A [`Responsive`] widget will always try to fill all the available space of
|
||||
/// its parent.
|
||||
#[cfg(feature = "lazy")]
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Responsive<
|
||||
'a,
|
||||
|
|
@ -82,15 +82,21 @@ where
|
|||
new_size: Size,
|
||||
view: &dyn Fn(Size) -> Element<'a, Message, Theme, Renderer>,
|
||||
) {
|
||||
if self.size == new_size {
|
||||
return;
|
||||
if self.size != new_size {
|
||||
self.element = view(new_size);
|
||||
self.size = new_size;
|
||||
self.layout = None;
|
||||
|
||||
tree.diff(&self.element);
|
||||
} else {
|
||||
let is_tree_empty =
|
||||
tree.tag == tree::Tag::stateless() && tree.children.is_empty();
|
||||
|
||||
if is_tree_empty {
|
||||
self.layout = None;
|
||||
tree.diff(&self.element);
|
||||
}
|
||||
}
|
||||
|
||||
self.element = view(new_size);
|
||||
self.size = new_size;
|
||||
self.layout = None;
|
||||
|
||||
tree.diff(&self.element);
|
||||
}
|
||||
|
||||
fn resolve<R, T>(
|
||||
|
|
@ -125,8 +131,8 @@ struct State {
|
|||
tree: RefCell<Tree>,
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Responsive<'a, Message, Theme, Renderer>
|
||||
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Responsive<'_, Message, Theme, Renderer>
|
||||
where
|
||||
Renderer: core::Renderer,
|
||||
{
|
||||
|
|
@ -161,7 +167,7 @@ where
|
|||
tree: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn widget::Operation<Message>,
|
||||
operation: &mut dyn widget::Operation,
|
||||
) {
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
let mut content = self.content.borrow_mut();
|
||||
|
|
@ -179,30 +185,30 @@ where
|
|||
);
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
fn update(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: Event,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
viewport: &Rectangle,
|
||||
) -> event::Status {
|
||||
) {
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
let mut content = self.content.borrow_mut();
|
||||
|
||||
let mut local_messages = vec![];
|
||||
let mut local_shell = Shell::new(&mut local_messages);
|
||||
|
||||
let status = content.resolve(
|
||||
content.resolve(
|
||||
&mut state.tree.borrow_mut(),
|
||||
renderer,
|
||||
layout,
|
||||
&self.view,
|
||||
|tree, renderer, layout, element| {
|
||||
element.as_widget_mut().on_event(
|
||||
element.as_widget_mut().update(
|
||||
tree,
|
||||
event,
|
||||
layout,
|
||||
|
|
@ -211,7 +217,7 @@ where
|
|||
clipboard,
|
||||
&mut local_shell,
|
||||
viewport,
|
||||
)
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -220,8 +226,6 @@ where
|
|||
}
|
||||
|
||||
shell.merge(local_shell, std::convert::identity);
|
||||
|
||||
status
|
||||
}
|
||||
|
||||
fn draw(
|
||||
|
|
@ -319,7 +323,11 @@ where
|
|||
}
|
||||
.build();
|
||||
|
||||
Some(overlay::Element::new(Box::new(overlay)))
|
||||
if overlay.with_overlay(|(overlay, _layout)| overlay.is_some()) {
|
||||
Some(overlay::Element::new(Box::new(overlay)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -350,9 +358,7 @@ struct Overlay<'a, 'b, Message, Theme, Renderer> {
|
|||
),
|
||||
}
|
||||
|
||||
impl<'a, 'b, Message, Theme, Renderer>
|
||||
Overlay<'a, 'b, Message, Theme, Renderer>
|
||||
{
|
||||
impl<Message, Theme, Renderer> Overlay<'_, '_, Message, Theme, Renderer> {
|
||||
fn with_overlay_maybe<T>(
|
||||
&self,
|
||||
f: impl FnOnce(&mut Nested<'_, Message, Theme, Renderer>) -> T,
|
||||
|
|
@ -372,9 +378,8 @@ impl<'a, 'b, Message, Theme, Renderer>
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b, Message, Theme, Renderer>
|
||||
overlay::Overlay<Message, Theme, Renderer>
|
||||
for Overlay<'a, 'b, Message, Theme, Renderer>
|
||||
impl<Message, Theme, Renderer> overlay::Overlay<Message, Theme, Renderer>
|
||||
for Overlay<'_, '_, Message, Theme, Renderer>
|
||||
where
|
||||
Renderer: core::Renderer,
|
||||
{
|
||||
|
|
@ -409,36 +414,28 @@ where
|
|||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
fn update(
|
||||
&mut self,
|
||||
event: Event,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
) {
|
||||
let mut is_layout_invalid = false;
|
||||
|
||||
let event_status = self
|
||||
.with_overlay_mut_maybe(|overlay| {
|
||||
let event_status = overlay.on_event(
|
||||
event, layout, cursor, renderer, clipboard, shell,
|
||||
);
|
||||
let _ = self.with_overlay_mut_maybe(|overlay| {
|
||||
overlay.update(event, layout, cursor, renderer, clipboard, shell);
|
||||
|
||||
is_layout_invalid = shell.is_layout_invalid();
|
||||
|
||||
event_status
|
||||
})
|
||||
.unwrap_or(event::Status::Ignored);
|
||||
is_layout_invalid = shell.is_layout_invalid();
|
||||
});
|
||||
|
||||
if is_layout_invalid {
|
||||
self.with_overlay_mut(|(_overlay, layout)| {
|
||||
**layout = None;
|
||||
});
|
||||
}
|
||||
|
||||
event_status
|
||||
}
|
||||
|
||||
fn is_over(
|
||||
|
|
@ -452,4 +449,15 @@ where
|
|||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn operate(
|
||||
&mut self,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn widget::Operation,
|
||||
) {
|
||||
let _ = self.with_overlay_mut_maybe(|overlay| {
|
||||
overlay.operate(layout, renderer, operation);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,9 +8,10 @@ pub use iced_renderer::graphics;
|
|||
pub use iced_runtime as runtime;
|
||||
pub use iced_runtime::core;
|
||||
|
||||
mod action;
|
||||
mod column;
|
||||
mod mouse_area;
|
||||
mod row;
|
||||
mod pin;
|
||||
mod space;
|
||||
mod stack;
|
||||
mod themer;
|
||||
|
|
@ -23,8 +24,10 @@ pub mod keyed;
|
|||
pub mod overlay;
|
||||
pub mod pane_grid;
|
||||
pub mod pick_list;
|
||||
pub mod pop;
|
||||
pub mod progress_bar;
|
||||
pub mod radio;
|
||||
pub mod row;
|
||||
pub mod rule;
|
||||
pub mod scrollable;
|
||||
pub mod slider;
|
||||
|
|
@ -42,9 +45,6 @@ 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::*;
|
||||
|
||||
|
|
@ -65,6 +65,10 @@ pub use pane_grid::PaneGrid;
|
|||
#[doc(no_inline)]
|
||||
pub use pick_list::PickList;
|
||||
#[doc(no_inline)]
|
||||
pub use pin::Pin;
|
||||
#[doc(no_inline)]
|
||||
pub use pop::Pop;
|
||||
#[doc(no_inline)]
|
||||
pub use progress_bar::ProgressBar;
|
||||
#[doc(no_inline)]
|
||||
pub use radio::Radio;
|
||||
|
|
@ -130,5 +134,9 @@ pub mod qr_code;
|
|||
#[doc(no_inline)]
|
||||
pub use qr_code::QRCode;
|
||||
|
||||
#[cfg(feature = "markdown")]
|
||||
pub mod markdown;
|
||||
|
||||
pub use crate::core::theme::{self, Theme};
|
||||
pub use action::Action;
|
||||
pub use renderer::Renderer;
|
||||
|
|
|
|||
1337
widget/src/markdown.rs
Normal file
1337
widget/src/markdown.rs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,16 +1,13 @@
|
|||
//! A container for capturing mouse events.
|
||||
|
||||
use iced_renderer::core::Point;
|
||||
|
||||
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, Operation, Tree};
|
||||
use crate::core::widget::{Operation, Tree, tree};
|
||||
use crate::core::{
|
||||
Clipboard, Element, Layout, Length, Rectangle, Shell, Size, Vector, Widget,
|
||||
Clipboard, Element, Event, Layout, Length, Point, Rectangle, Shell, Size,
|
||||
Vector, Widget,
|
||||
};
|
||||
|
||||
/// Emit messages on mouse events.
|
||||
|
|
@ -24,12 +21,14 @@ pub struct MouseArea<
|
|||
content: Element<'a, Message, Theme, Renderer>,
|
||||
on_press: Option<Message>,
|
||||
on_release: Option<Message>,
|
||||
on_double_click: Option<Message>,
|
||||
on_right_press: Option<Message>,
|
||||
on_right_release: Option<Message>,
|
||||
on_middle_press: Option<Message>,
|
||||
on_middle_release: Option<Message>,
|
||||
on_scroll: Option<Box<dyn Fn(mouse::ScrollDelta) -> Message + 'a>>,
|
||||
on_enter: Option<Message>,
|
||||
on_move: Option<Box<dyn Fn(Point) -> Message>>,
|
||||
on_move: Option<Box<dyn Fn(Point) -> Message + 'a>>,
|
||||
on_exit: Option<Message>,
|
||||
interaction: Option<mouse::Interaction>,
|
||||
}
|
||||
|
|
@ -49,6 +48,22 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> {
|
|||
self
|
||||
}
|
||||
|
||||
/// The message to emit on a double click.
|
||||
///
|
||||
/// If you use this with [`on_press`]/[`on_release`], those
|
||||
/// event will be emit as normal.
|
||||
///
|
||||
/// The events stream will be: on_press -> on_release -> on_press
|
||||
/// -> on_double_click -> on_release -> on_press ...
|
||||
///
|
||||
/// [`on_press`]: Self::on_press
|
||||
/// [`on_release`]: Self::on_release
|
||||
#[must_use]
|
||||
pub fn on_double_click(mut self, message: Message) -> Self {
|
||||
self.on_double_click = Some(message);
|
||||
self
|
||||
}
|
||||
|
||||
/// The message to emit on a right button press.
|
||||
#[must_use]
|
||||
pub fn on_right_press(mut self, message: Message) -> Self {
|
||||
|
|
@ -77,6 +92,16 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> {
|
|||
self
|
||||
}
|
||||
|
||||
/// The message to emit when scroll wheel is used
|
||||
#[must_use]
|
||||
pub fn on_scroll(
|
||||
mut self,
|
||||
on_scroll: impl Fn(mouse::ScrollDelta) -> Message + 'a,
|
||||
) -> Self {
|
||||
self.on_scroll = Some(Box::new(on_scroll));
|
||||
self
|
||||
}
|
||||
|
||||
/// The message to emit when the mouse enters the area.
|
||||
#[must_use]
|
||||
pub fn on_enter(mut self, message: Message) -> Self {
|
||||
|
|
@ -86,11 +111,8 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> {
|
|||
|
||||
/// The message to emit when the mouse moves in the area.
|
||||
#[must_use]
|
||||
pub fn on_move<F>(mut self, build_message: F) -> Self
|
||||
where
|
||||
F: Fn(Point) -> Message + 'static,
|
||||
{
|
||||
self.on_move = Some(Box::new(build_message));
|
||||
pub fn on_move(mut self, on_move: impl Fn(Point) -> Message + 'a) -> Self {
|
||||
self.on_move = Some(Box::new(on_move));
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -113,6 +135,9 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> {
|
|||
#[derive(Default)]
|
||||
struct State {
|
||||
is_hovered: bool,
|
||||
bounds: Rectangle,
|
||||
cursor_position: Option<Point>,
|
||||
previous_click: Option<mouse::Click>,
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> {
|
||||
|
|
@ -124,10 +149,12 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> {
|
|||
content: content.into(),
|
||||
on_press: None,
|
||||
on_release: None,
|
||||
on_double_click: None,
|
||||
on_right_press: None,
|
||||
on_right_release: None,
|
||||
on_middle_press: None,
|
||||
on_middle_release: None,
|
||||
on_scroll: None,
|
||||
on_enter: None,
|
||||
on_move: None,
|
||||
on_exit: None,
|
||||
|
|
@ -136,8 +163,8 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for MouseArea<'a, Message, Theme, Renderer>
|
||||
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for MouseArea<'_, Message, Theme, Renderer>
|
||||
where
|
||||
Renderer: renderer::Renderer,
|
||||
Message: Clone,
|
||||
|
|
@ -178,7 +205,7 @@ where
|
|||
tree: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn Operation<Message>,
|
||||
operation: &mut dyn Operation,
|
||||
) {
|
||||
self.content.as_widget().operate(
|
||||
&mut tree.children[0],
|
||||
|
|
@ -188,31 +215,33 @@ where
|
|||
);
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
fn update(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: Event,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
viewport: &Rectangle,
|
||||
) -> event::Status {
|
||||
if let event::Status::Captured = self.content.as_widget_mut().on_event(
|
||||
) {
|
||||
self.content.as_widget_mut().update(
|
||||
&mut tree.children[0],
|
||||
event.clone(),
|
||||
event,
|
||||
layout,
|
||||
cursor,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
viewport,
|
||||
) {
|
||||
return event::Status::Captured;
|
||||
);
|
||||
|
||||
if shell.is_event_captured() {
|
||||
return;
|
||||
}
|
||||
|
||||
update(self, tree, event, layout, cursor, shell)
|
||||
update(self, tree, event, layout, cursor, shell);
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
|
|
@ -297,18 +326,22 @@ where
|
|||
fn update<Message: Clone, Theme, Renderer>(
|
||||
widget: &mut MouseArea<'_, Message, Theme, Renderer>,
|
||||
tree: &mut Tree,
|
||||
event: Event,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
if let Event::Mouse(mouse::Event::CursorMoved { .. })
|
||||
| Event::Touch(touch::Event::FingerMoved { .. }) = event
|
||||
{
|
||||
let state: &mut State = tree.state.downcast_mut();
|
||||
) {
|
||||
let state: &mut State = tree.state.downcast_mut();
|
||||
|
||||
let cursor_position = cursor.position();
|
||||
let bounds = layout.bounds();
|
||||
|
||||
if state.cursor_position != cursor_position || state.bounds != bounds {
|
||||
let was_hovered = state.is_hovered;
|
||||
|
||||
state.is_hovered = cursor.is_over(layout.bounds());
|
||||
state.cursor_position = cursor_position;
|
||||
state.bounds = bounds;
|
||||
|
||||
match (
|
||||
widget.on_enter.as_ref(),
|
||||
|
|
@ -331,71 +364,71 @@ fn update<Message: Clone, Theme, Renderer>(
|
|||
}
|
||||
|
||||
if !cursor.is_over(layout.bounds()) {
|
||||
return event::Status::Ignored;
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(message) = widget.on_press.as_ref() {
|
||||
if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
|
||||
| Event::Touch(touch::Event::FingerPressed { .. }) = event
|
||||
{
|
||||
shell.publish(message.clone());
|
||||
match event {
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
|
||||
| Event::Touch(touch::Event::FingerPressed { .. }) => {
|
||||
if let Some(message) = widget.on_press.as_ref() {
|
||||
shell.publish(message.clone());
|
||||
shell.capture_event();
|
||||
}
|
||||
|
||||
return event::Status::Captured;
|
||||
if let Some(position) = cursor_position {
|
||||
if let Some(message) = widget.on_double_click.as_ref() {
|
||||
let new_click = mouse::Click::new(
|
||||
position,
|
||||
mouse::Button::Left,
|
||||
state.previous_click,
|
||||
);
|
||||
|
||||
if new_click.kind() == mouse::click::Kind::Double {
|
||||
shell.publish(message.clone());
|
||||
}
|
||||
|
||||
state.previous_click = Some(new_click);
|
||||
|
||||
// Even if this is not a double click, but the press is nevertheless
|
||||
// processed by us and should not be popup to parent widgets.
|
||||
shell.capture_event();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(message) = widget.on_release.as_ref() {
|
||||
if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
|
||||
| Event::Touch(touch::Event::FingerLifted { .. }) = event
|
||||
{
|
||||
shell.publish(message.clone());
|
||||
|
||||
return event::Status::Captured;
|
||||
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
|
||||
| Event::Touch(touch::Event::FingerLifted { .. }) => {
|
||||
if let Some(message) = widget.on_release.as_ref() {
|
||||
shell.publish(message.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(message) = widget.on_right_press.as_ref() {
|
||||
if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) =
|
||||
event
|
||||
{
|
||||
shell.publish(message.clone());
|
||||
|
||||
return event::Status::Captured;
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) => {
|
||||
if let Some(message) = widget.on_right_press.as_ref() {
|
||||
shell.publish(message.clone());
|
||||
shell.capture_event();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(message) = widget.on_right_release.as_ref() {
|
||||
if let Event::Mouse(mouse::Event::ButtonReleased(
|
||||
mouse::Button::Right,
|
||||
)) = event
|
||||
{
|
||||
shell.publish(message.clone());
|
||||
|
||||
return event::Status::Captured;
|
||||
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Right)) => {
|
||||
if let Some(message) = widget.on_right_release.as_ref() {
|
||||
shell.publish(message.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(message) = widget.on_middle_press.as_ref() {
|
||||
if let Event::Mouse(mouse::Event::ButtonPressed(
|
||||
mouse::Button::Middle,
|
||||
)) = event
|
||||
{
|
||||
shell.publish(message.clone());
|
||||
|
||||
return event::Status::Captured;
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle)) => {
|
||||
if let Some(message) = widget.on_middle_press.as_ref() {
|
||||
shell.publish(message.clone());
|
||||
shell.capture_event();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(message) = widget.on_middle_release.as_ref() {
|
||||
if let Event::Mouse(mouse::Event::ButtonReleased(
|
||||
mouse::Button::Middle,
|
||||
)) = event
|
||||
{
|
||||
shell.publish(message.clone());
|
||||
|
||||
return event::Status::Captured;
|
||||
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Middle)) => {
|
||||
if let Some(message) = widget.on_middle_release.as_ref() {
|
||||
shell.publish(message.clone());
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
|
||||
if let Some(on_scroll) = widget.on_scroll.as_ref() {
|
||||
shell.publish(on_scroll(*delta));
|
||||
shell.capture_event();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
event::Status::Ignored
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
//! Build and show dropdown menus.
|
||||
use crate::core::alignment;
|
||||
use crate::core::event::{self, Event};
|
||||
use crate::core::border::{self, Border};
|
||||
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::widget::tree::{self, Tree};
|
||||
use crate::core::window;
|
||||
use crate::core::{
|
||||
Background, Border, Clipboard, Color, Length, Padding, Pixels, Point,
|
||||
Background, Clipboard, Color, Event, Length, Padding, Pixels, Point,
|
||||
Rectangle, Size, Theme, Vector,
|
||||
};
|
||||
use crate::core::{Element, Shell, Widget};
|
||||
|
|
@ -200,21 +201,18 @@ where
|
|||
class,
|
||||
} = menu;
|
||||
|
||||
let list = Scrollable::with_direction(
|
||||
List {
|
||||
options,
|
||||
hovered_option,
|
||||
on_selected,
|
||||
on_option_hovered,
|
||||
font,
|
||||
text_size,
|
||||
text_line_height,
|
||||
text_shaping,
|
||||
padding,
|
||||
class,
|
||||
},
|
||||
scrollable::Direction::default(),
|
||||
);
|
||||
let list = Scrollable::new(List {
|
||||
options,
|
||||
hovered_option,
|
||||
on_selected,
|
||||
on_option_hovered,
|
||||
font,
|
||||
text_size,
|
||||
text_line_height,
|
||||
text_shaping,
|
||||
padding,
|
||||
class,
|
||||
});
|
||||
|
||||
state.tree.diff(&list as &dyn Widget<_, _, _>);
|
||||
|
||||
|
|
@ -229,9 +227,8 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b, Message, Theme, Renderer>
|
||||
crate::core::Overlay<Message, Theme, Renderer>
|
||||
for Overlay<'a, 'b, Message, Theme, Renderer>
|
||||
impl<Message, Theme, Renderer> crate::core::Overlay<Message, Theme, Renderer>
|
||||
for Overlay<'_, '_, Message, Theme, Renderer>
|
||||
where
|
||||
Theme: Catalog,
|
||||
Renderer: text::Renderer,
|
||||
|
|
@ -264,21 +261,21 @@ where
|
|||
})
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
fn update(
|
||||
&mut self,
|
||||
event: Event,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
) {
|
||||
let bounds = layout.bounds();
|
||||
|
||||
self.list.on_event(
|
||||
self.list.update(
|
||||
self.state, event, layout, cursor, renderer, clipboard, shell,
|
||||
&bounds,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
|
|
@ -336,13 +333,25 @@ where
|
|||
class: &'a <Theme as Catalog>::Class<'b>,
|
||||
}
|
||||
|
||||
impl<'a, 'b, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for List<'a, 'b, T, Message, Theme, Renderer>
|
||||
struct ListState {
|
||||
is_hovered: Option<bool>,
|
||||
}
|
||||
|
||||
impl<T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for List<'_, '_, T, Message, Theme, Renderer>
|
||||
where
|
||||
T: Clone + ToString,
|
||||
Theme: Catalog,
|
||||
Renderer: text::Renderer,
|
||||
{
|
||||
fn tag(&self) -> tree::Tag {
|
||||
tree::Tag::of::<Option<bool>>()
|
||||
}
|
||||
|
||||
fn state(&self) -> tree::State {
|
||||
tree::State::new(ListState { is_hovered: None })
|
||||
}
|
||||
|
||||
fn size(&self) -> Size<Length> {
|
||||
Size {
|
||||
width: Length::Fill,
|
||||
|
|
@ -376,24 +385,24 @@ where
|
|||
layout::Node::new(size)
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
fn update(
|
||||
&mut self,
|
||||
_state: &mut Tree,
|
||||
event: Event,
|
||||
tree: &mut Tree,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &Renderer,
|
||||
_clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
_viewport: &Rectangle,
|
||||
) -> event::Status {
|
||||
) {
|
||||
match event {
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
|
||||
if cursor.is_over(layout.bounds()) {
|
||||
if let Some(index) = *self.hovered_option {
|
||||
if let Some(option) = self.options.get(index) {
|
||||
shell.publish((self.on_selected)(option.clone()));
|
||||
return event::Status::Captured;
|
||||
shell.capture_event();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -413,14 +422,18 @@ where
|
|||
let new_hovered_option =
|
||||
(cursor_position.y / option_height) as usize;
|
||||
|
||||
if let Some(on_option_hovered) = self.on_option_hovered {
|
||||
if *self.hovered_option != Some(new_hovered_option) {
|
||||
if let Some(option) =
|
||||
self.options.get(new_hovered_option)
|
||||
if *self.hovered_option != Some(new_hovered_option) {
|
||||
if let Some(option) =
|
||||
self.options.get(new_hovered_option)
|
||||
{
|
||||
if let Some(on_option_hovered) =
|
||||
self.on_option_hovered
|
||||
{
|
||||
shell
|
||||
.publish(on_option_hovered(option.clone()));
|
||||
}
|
||||
|
||||
shell.request_redraw();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -445,7 +458,7 @@ where
|
|||
if let Some(index) = *self.hovered_option {
|
||||
if let Some(option) = self.options.get(index) {
|
||||
shell.publish((self.on_selected)(option.clone()));
|
||||
return event::Status::Captured;
|
||||
shell.capture_event();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -453,7 +466,15 @@ where
|
|||
_ => {}
|
||||
}
|
||||
|
||||
event::Status::Ignored
|
||||
let state = tree.state.downcast_mut::<ListState>();
|
||||
|
||||
if let Event::Window(window::Event::RedrawRequested(_now)) = event {
|
||||
state.is_hovered = Some(cursor.is_over(layout.bounds()));
|
||||
} else if state.is_hovered.is_some_and(|is_hovered| {
|
||||
is_hovered != cursor.is_over(layout.bounds())
|
||||
}) {
|
||||
shell.request_redraw();
|
||||
}
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
|
|
@ -517,7 +538,7 @@ where
|
|||
width: bounds.width - style.border.width * 2.0,
|
||||
..bounds
|
||||
},
|
||||
border: Border::rounded(style.border.radius),
|
||||
border: border::rounded(style.border.radius),
|
||||
..renderer::Quad::default()
|
||||
},
|
||||
style.selected_background,
|
||||
|
|
@ -534,6 +555,7 @@ where
|
|||
horizontal_alignment: alignment::Horizontal::Left,
|
||||
vertical_alignment: alignment::Vertical::Center,
|
||||
shaping: self.text_shaping,
|
||||
wrapping: text::Wrapping::default(),
|
||||
},
|
||||
Point::new(bounds.x + self.padding.left, bounds.center_y()),
|
||||
if is_selected {
|
||||
|
|
@ -563,7 +585,7 @@ where
|
|||
}
|
||||
|
||||
/// The appearance of a [`Menu`].
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Style {
|
||||
/// The [`Background`] of the menu.
|
||||
pub background: Background,
|
||||
|
|
|
|||
|
|
@ -1,15 +1,62 @@
|
|||
//! Let your users split regions of your application and organize layout dynamically.
|
||||
//! Pane grids let your users split regions of your application and organize layout dynamically.
|
||||
//!
|
||||
//! 
|
||||
//!
|
||||
//! 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
|
||||
//! ```no_run
|
||||
//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
|
||||
//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||
//! #
|
||||
//! use iced::widget::{pane_grid, text};
|
||||
//!
|
||||
//! struct State {
|
||||
//! panes: pane_grid::State<Pane>,
|
||||
//! }
|
||||
//!
|
||||
//! enum Pane {
|
||||
//! SomePane,
|
||||
//! AnotherKindOfPane,
|
||||
//! }
|
||||
//!
|
||||
//! enum Message {
|
||||
//! PaneDragged(pane_grid::DragEvent),
|
||||
//! PaneResized(pane_grid::ResizeEvent),
|
||||
//! }
|
||||
//!
|
||||
//! fn view(state: &State) -> Element<'_, Message> {
|
||||
//! pane_grid(&state.panes, |pane, state, is_maximized| {
|
||||
//! pane_grid::Content::new(match state {
|
||||
//! Pane::SomePane => text("This is some pane"),
|
||||
//! Pane::AnotherKindOfPane => text("This is another kind of pane"),
|
||||
//! })
|
||||
//! })
|
||||
//! .on_drag(Message::PaneDragged)
|
||||
//! .on_resize(10, Message::PaneResized)
|
||||
//! .into()
|
||||
//! }
|
||||
//! ```
|
||||
//! 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.12/examples/pane_grid
|
||||
//! [`pane_grid` example]: https://github.com/iced-rs/iced/tree/0.13/examples/pane_grid
|
||||
mod axis;
|
||||
mod configuration;
|
||||
mod content;
|
||||
mod controls;
|
||||
mod direction;
|
||||
mod draggable;
|
||||
mod node;
|
||||
|
|
@ -22,6 +69,7 @@ pub mod state;
|
|||
pub use axis::Axis;
|
||||
pub use configuration::Configuration;
|
||||
pub use content::Content;
|
||||
pub use controls::Controls;
|
||||
pub use direction::Direction;
|
||||
pub use draggable::Draggable;
|
||||
pub use node::Node;
|
||||
|
|
@ -31,7 +79,6 @@ pub use state::State;
|
|||
pub use title_bar::TitleBar;
|
||||
|
||||
use crate::container;
|
||||
use crate::core::event::{self, Event};
|
||||
use crate::core::layout;
|
||||
use crate::core::mouse;
|
||||
use crate::core::overlay::{self, Group};
|
||||
|
|
@ -39,8 +86,9 @@ use crate::core::renderer;
|
|||
use crate::core::touch;
|
||||
use crate::core::widget;
|
||||
use crate::core::widget::tree::{self, Tree};
|
||||
use crate::core::window;
|
||||
use crate::core::{
|
||||
self, Background, Border, Clipboard, Color, Element, Layout, Length,
|
||||
self, Background, Border, Clipboard, Color, Element, Event, Layout, Length,
|
||||
Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget,
|
||||
};
|
||||
|
||||
|
|
@ -66,14 +114,18 @@ const THICKNESS_RATIO: f32 = 25.0;
|
|||
/// * Configurable modifier keys
|
||||
/// * [`State`] API to perform actions programmatically (`split`, `swap`, `resize`, etc.)
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// # use iced_widget::{pane_grid, text};
|
||||
/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
|
||||
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||
/// #
|
||||
/// # type PaneGrid<'a, Message> = iced_widget::PaneGrid<'a, Message>;
|
||||
/// #
|
||||
/// enum PaneState {
|
||||
/// use iced::widget::{pane_grid, text};
|
||||
///
|
||||
/// struct State {
|
||||
/// panes: pane_grid::State<Pane>,
|
||||
/// }
|
||||
///
|
||||
/// enum Pane {
|
||||
/// SomePane,
|
||||
/// AnotherKindOfPane,
|
||||
/// }
|
||||
|
|
@ -83,17 +135,17 @@ const THICKNESS_RATIO: f32 = 25.0;
|
|||
/// PaneResized(pane_grid::ResizeEvent),
|
||||
/// }
|
||||
///
|
||||
/// let (mut state, _) = pane_grid::State::new(PaneState::SomePane);
|
||||
///
|
||||
/// let pane_grid =
|
||||
/// PaneGrid::new(&state, |pane, state, is_maximized| {
|
||||
/// fn view(state: &State) -> Element<'_, Message> {
|
||||
/// pane_grid(&state.panes, |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"),
|
||||
/// Pane::SomePane => text("This is some pane"),
|
||||
/// Pane::AnotherKindOfPane => text("This is another kind of pane"),
|
||||
/// })
|
||||
/// })
|
||||
/// .on_drag(Message::PaneDragged)
|
||||
/// .on_resize(10, Message::PaneResized);
|
||||
/// .on_resize(10, Message::PaneResized)
|
||||
/// .into()
|
||||
/// }
|
||||
/// ```
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct PaneGrid<
|
||||
|
|
@ -105,7 +157,9 @@ pub struct PaneGrid<
|
|||
Theme: Catalog,
|
||||
Renderer: core::Renderer,
|
||||
{
|
||||
contents: Contents<'a, Content<'a, Message, Theme, Renderer>>,
|
||||
internal: &'a state::Internal,
|
||||
panes: Vec<Pane>,
|
||||
contents: Vec<Content<'a, Message, Theme, Renderer>>,
|
||||
width: Length,
|
||||
height: Length,
|
||||
spacing: f32,
|
||||
|
|
@ -113,6 +167,7 @@ pub struct PaneGrid<
|
|||
on_drag: Option<Box<dyn Fn(DragEvent) -> Message + 'a>>,
|
||||
on_resize: Option<(f32, Box<dyn Fn(ResizeEvent) -> Message + 'a>)>,
|
||||
class: <Theme as Catalog>::Class<'a>,
|
||||
last_mouse_interaction: Option<mouse::Interaction>,
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> PaneGrid<'a, Message, Theme, Renderer>
|
||||
|
|
@ -128,29 +183,19 @@ where
|
|||
state: &'a State<T>,
|
||||
view: impl Fn(Pane, &'a T, bool) -> Content<'a, Message, Theme, 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,
|
||||
)
|
||||
};
|
||||
let panes = state.panes.keys().copied().collect();
|
||||
let contents = state
|
||||
.panes
|
||||
.iter()
|
||||
.map(|(pane, pane_state)| match state.maximized() {
|
||||
Some(p) if *pane == p => view(*pane, pane_state, true),
|
||||
_ => view(*pane, pane_state, false),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
internal: &state.internal,
|
||||
panes,
|
||||
contents,
|
||||
width: Length::Fill,
|
||||
height: Length::Fill,
|
||||
|
|
@ -159,6 +204,7 @@ where
|
|||
on_drag: None,
|
||||
on_resize: None,
|
||||
class: <Theme as Catalog>::default(),
|
||||
last_mouse_interaction: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -196,7 +242,9 @@ where
|
|||
where
|
||||
F: 'a + Fn(DragEvent) -> Message,
|
||||
{
|
||||
self.on_drag = Some(Box::new(f));
|
||||
if self.internal.maximized().is_none() {
|
||||
self.on_drag = Some(Box::new(f));
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -213,7 +261,9 @@ where
|
|||
where
|
||||
F: 'a + Fn(ResizeEvent) -> Message,
|
||||
{
|
||||
self.on_resize = Some((leeway.into().0, Box::new(f)));
|
||||
if self.internal.maximized().is_none() {
|
||||
self.on_resize = Some((leeway.into().0, Box::new(f)));
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -239,46 +289,114 @@ where
|
|||
}
|
||||
|
||||
fn drag_enabled(&self) -> bool {
|
||||
(!self.contents.is_maximized())
|
||||
self.internal
|
||||
.maximized()
|
||||
.is_none()
|
||||
.then(|| self.on_drag.is_some())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn grid_interaction(
|
||||
&self,
|
||||
action: &state::Action,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
) -> Option<mouse::Interaction> {
|
||||
if action.picked_pane().is_some() {
|
||||
return Some(mouse::Interaction::Grabbing);
|
||||
}
|
||||
|
||||
let resize_leeway = self.on_resize.as_ref().map(|(leeway, _)| *leeway);
|
||||
let node = self.internal.layout();
|
||||
|
||||
let resize_axis =
|
||||
action.picked_split().map(|(_, axis)| axis).or_else(|| {
|
||||
resize_leeway.and_then(|leeway| {
|
||||
let cursor_position = cursor.position()?;
|
||||
let bounds = layout.bounds();
|
||||
|
||||
let splits =
|
||||
node.split_regions(self.spacing, bounds.size());
|
||||
|
||||
let relative_cursor = Point::new(
|
||||
cursor_position.x - bounds.x,
|
||||
cursor_position.y - bounds.y,
|
||||
);
|
||||
|
||||
hovered_split(
|
||||
splits.iter(),
|
||||
self.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
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for PaneGrid<'a, Message, Theme, Renderer>
|
||||
#[derive(Default)]
|
||||
struct Memory {
|
||||
action: state::Action,
|
||||
order: Vec<Pane>,
|
||||
}
|
||||
|
||||
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for PaneGrid<'_, Message, Theme, Renderer>
|
||||
where
|
||||
Theme: Catalog,
|
||||
Renderer: core::Renderer,
|
||||
{
|
||||
fn tag(&self) -> tree::Tag {
|
||||
tree::Tag::of::<state::Action>()
|
||||
tree::Tag::of::<Memory>()
|
||||
}
|
||||
|
||||
fn state(&self) -> tree::State {
|
||||
tree::State::new(state::Action::Idle)
|
||||
tree::State::new(Memory::default())
|
||||
}
|
||||
|
||||
fn children(&self) -> Vec<Tree> {
|
||||
self.contents
|
||||
.iter()
|
||||
.map(|(_, content)| content.state())
|
||||
.collect()
|
||||
self.contents.iter().map(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(),
|
||||
),
|
||||
}
|
||||
let Memory { order, .. } = tree.state.downcast_ref();
|
||||
|
||||
// `Pane` always increments and is iterated by Ord so new
|
||||
// states are always added at the end. We can simply remove
|
||||
// states which no longer exist and `diff_children` will
|
||||
// diff the remaining values in the correct order and
|
||||
// add new states at the end
|
||||
|
||||
let mut i = 0;
|
||||
let mut j = 0;
|
||||
tree.children.retain(|_| {
|
||||
let retain = self.panes.get(i) == order.get(j);
|
||||
|
||||
if retain {
|
||||
i += 1;
|
||||
}
|
||||
j += 1;
|
||||
|
||||
retain
|
||||
});
|
||||
|
||||
tree.diff_children_custom(
|
||||
&self.contents,
|
||||
|state, content| content.diff(state),
|
||||
Content::state,
|
||||
);
|
||||
|
||||
let Memory { order, .. } = tree.state.downcast_mut();
|
||||
order.clone_from(&self.panes);
|
||||
}
|
||||
|
||||
fn size(&self) -> Size<Length> {
|
||||
|
|
@ -295,14 +413,23 @@ where
|
|||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
let size = limits.resolve(self.width, self.height, Size::ZERO);
|
||||
let node = self.contents.layout();
|
||||
let regions = node.pane_regions(self.spacing, size);
|
||||
let regions = self.internal.layout().pane_regions(self.spacing, size);
|
||||
|
||||
let children = self
|
||||
.contents
|
||||
.panes
|
||||
.iter()
|
||||
.copied()
|
||||
.zip(&self.contents)
|
||||
.zip(tree.children.iter_mut())
|
||||
.filter_map(|((pane, content), tree)| {
|
||||
if self
|
||||
.internal
|
||||
.maximized()
|
||||
.is_some_and(|maximized| maximized != pane)
|
||||
{
|
||||
return Some(layout::Node::new(Size::ZERO));
|
||||
}
|
||||
|
||||
let region = regions.get(&pane)?;
|
||||
let size = Size::new(region.width, region.height);
|
||||
|
||||
|
|
@ -324,34 +451,39 @@ where
|
|||
tree: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn widget::Operation<Message>,
|
||||
operation: &mut dyn widget::Operation,
|
||||
) {
|
||||
operation.container(None, layout.bounds(), &mut |operation| {
|
||||
self.contents
|
||||
self.panes
|
||||
.iter()
|
||||
.copied()
|
||||
.zip(&self.contents)
|
||||
.zip(&mut tree.children)
|
||||
.zip(layout.children())
|
||||
.for_each(|(((_pane, content), state), layout)| {
|
||||
.filter(|(((pane, _), _), _)| {
|
||||
self.internal
|
||||
.maximized()
|
||||
.is_none_or(|maximized| *pane == maximized)
|
||||
})
|
||||
.for_each(|(((_, content), state), layout)| {
|
||||
content.operate(state, layout, renderer, operation);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
fn update(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: Event,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
viewport: &Rectangle,
|
||||
) -> event::Status {
|
||||
let mut event_status = event::Status::Ignored;
|
||||
|
||||
let action = tree.state.downcast_mut::<state::Action>();
|
||||
let node = self.contents.layout();
|
||||
) {
|
||||
let Memory { action, .. } = tree.state.downcast_mut();
|
||||
let node = self.internal.layout();
|
||||
|
||||
let on_drag = if self.drag_enabled() {
|
||||
&self.on_drag
|
||||
|
|
@ -359,13 +491,36 @@ where
|
|||
&None
|
||||
};
|
||||
|
||||
let picked_pane = action.picked_pane().map(|(pane, _)| pane);
|
||||
|
||||
for (((pane, content), tree), layout) in self
|
||||
.panes
|
||||
.iter()
|
||||
.copied()
|
||||
.zip(&mut self.contents)
|
||||
.zip(&mut tree.children)
|
||||
.zip(layout.children())
|
||||
.filter(|(((pane, _), _), _)| {
|
||||
self.internal
|
||||
.maximized()
|
||||
.is_none_or(|maximized| *pane == maximized)
|
||||
})
|
||||
{
|
||||
let is_picked = picked_pane == Some(pane);
|
||||
|
||||
content.update(
|
||||
tree, event, layout, cursor, renderer, clipboard, shell,
|
||||
viewport, is_picked,
|
||||
);
|
||||
}
|
||||
|
||||
match event {
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
|
||||
| Event::Touch(touch::Event::FingerPressed { .. }) => {
|
||||
let bounds = layout.bounds();
|
||||
|
||||
if let Some(cursor_position) = cursor.position_over(bounds) {
|
||||
event_status = event::Status::Captured;
|
||||
shell.capture_event();
|
||||
|
||||
match &self.on_resize {
|
||||
Some((leeway, _)) => {
|
||||
|
|
@ -396,7 +551,10 @@ where
|
|||
layout,
|
||||
cursor_position,
|
||||
shell,
|
||||
self.contents.iter(),
|
||||
self.panes
|
||||
.iter()
|
||||
.copied()
|
||||
.zip(&self.contents),
|
||||
&self.on_click,
|
||||
on_drag,
|
||||
);
|
||||
|
|
@ -408,7 +566,7 @@ where
|
|||
layout,
|
||||
cursor_position,
|
||||
shell,
|
||||
self.contents.iter(),
|
||||
self.panes.iter().copied().zip(&self.contents),
|
||||
&self.on_click,
|
||||
on_drag,
|
||||
);
|
||||
|
|
@ -434,8 +592,10 @@ where
|
|||
}
|
||||
} else {
|
||||
let dropped_region = self
|
||||
.contents
|
||||
.panes
|
||||
.iter()
|
||||
.copied()
|
||||
.zip(&self.contents)
|
||||
.zip(layout.children())
|
||||
.find_map(|(target, layout)| {
|
||||
layout_region(
|
||||
|
|
@ -461,13 +621,13 @@ where
|
|||
};
|
||||
|
||||
shell.publish(on_drag(event));
|
||||
} else {
|
||||
shell.publish(on_drag(DragEvent::Canceled {
|
||||
pane,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
event_status = event::Status::Captured;
|
||||
} else if action.picked_split().is_some() {
|
||||
event_status = event::Status::Captured;
|
||||
}
|
||||
|
||||
*action = state::Action::Idle;
|
||||
|
|
@ -509,37 +669,48 @@ where
|
|||
ratio,
|
||||
}));
|
||||
|
||||
event_status = event::Status::Captured;
|
||||
shell.capture_event();
|
||||
}
|
||||
}
|
||||
} else if action.picked_pane().is_some() {
|
||||
shell.request_redraw();
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let picked_pane = action.picked_pane().map(|(pane, _)| pane);
|
||||
if shell.redraw_request() != window::RedrawRequest::NextFrame {
|
||||
let interaction = self
|
||||
.grid_interaction(action, layout, cursor)
|
||||
.or_else(|| {
|
||||
self.panes
|
||||
.iter()
|
||||
.zip(&self.contents)
|
||||
.zip(layout.children())
|
||||
.filter(|((pane, _content), _layout)| {
|
||||
self.internal
|
||||
.maximized()
|
||||
.is_none_or(|maximized| **pane == maximized)
|
||||
})
|
||||
.find_map(|((_pane, content), layout)| {
|
||||
content.grid_interaction(
|
||||
layout,
|
||||
cursor,
|
||||
on_drag.is_some(),
|
||||
)
|
||||
})
|
||||
})
|
||||
.unwrap_or(mouse::Interaction::None);
|
||||
|
||||
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,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
viewport,
|
||||
is_picked,
|
||||
)
|
||||
})
|
||||
.fold(event_status, event::Status::merge)
|
||||
if let Event::Window(window::Event::RedrawRequested(_now)) = event {
|
||||
self.last_mouse_interaction = Some(interaction);
|
||||
} else if self.last_mouse_interaction.is_some_and(
|
||||
|last_mouse_interaction| last_mouse_interaction != interaction,
|
||||
) {
|
||||
shell.request_redraw();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
|
|
@ -550,50 +721,26 @@ where
|
|||
viewport: &Rectangle,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
let action = tree.state.downcast_ref::<state::Action>();
|
||||
let Memory { action, .. } = tree.state.downcast_ref();
|
||||
|
||||
if action.picked_pane().is_some() {
|
||||
return mouse::Interaction::Grabbing;
|
||||
if let Some(grid_interaction) =
|
||||
self.grid_interaction(action, layout, cursor)
|
||||
{
|
||||
return grid_interaction;
|
||||
}
|
||||
|
||||
let resize_leeway = self.on_resize.as_ref().map(|(leeway, _)| *leeway);
|
||||
let node = self.contents.layout();
|
||||
|
||||
let resize_axis =
|
||||
action.picked_split().map(|(_, axis)| axis).or_else(|| {
|
||||
resize_leeway.and_then(|leeway| {
|
||||
let cursor_position = cursor.position()?;
|
||||
let bounds = layout.bounds();
|
||||
|
||||
let splits =
|
||||
node.split_regions(self.spacing, bounds.size());
|
||||
|
||||
let relative_cursor = Point::new(
|
||||
cursor_position.x - bounds.x,
|
||||
cursor_position.y - bounds.y,
|
||||
);
|
||||
|
||||
hovered_split(
|
||||
splits.iter(),
|
||||
self.spacing + leeway,
|
||||
relative_cursor,
|
||||
)
|
||||
.map(|(_, axis, _)| axis)
|
||||
})
|
||||
});
|
||||
|
||||
if let Some(resize_axis) = resize_axis {
|
||||
return match resize_axis {
|
||||
Axis::Horizontal => mouse::Interaction::ResizingVertically,
|
||||
Axis::Vertical => mouse::Interaction::ResizingHorizontally,
|
||||
};
|
||||
}
|
||||
|
||||
self.contents
|
||||
self.panes
|
||||
.iter()
|
||||
.copied()
|
||||
.zip(&self.contents)
|
||||
.zip(&tree.children)
|
||||
.zip(layout.children())
|
||||
.map(|(((_pane, content), tree), layout)| {
|
||||
.filter(|(((pane, _), _), _)| {
|
||||
self.internal
|
||||
.maximized()
|
||||
.is_none_or(|maximized| *pane == maximized)
|
||||
})
|
||||
.map(|(((_, content), tree), layout)| {
|
||||
content.mouse_interaction(
|
||||
tree,
|
||||
layout,
|
||||
|
|
@ -617,16 +764,10 @@ where
|
|||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
let action = tree.state.downcast_ref::<state::Action>();
|
||||
let node = self.contents.layout();
|
||||
let Memory { action, .. } = tree.state.downcast_ref();
|
||||
let node = self.internal.layout();
|
||||
let resize_leeway = self.on_resize.as_ref().map(|(leeway, _)| *leeway);
|
||||
|
||||
let contents = self
|
||||
.contents
|
||||
.iter()
|
||||
.zip(&tree.children)
|
||||
.map(|((pane, content), tree)| (pane, (content, tree)));
|
||||
|
||||
let picked_pane = action.picked_pane().filter(|(_, origin)| {
|
||||
cursor
|
||||
.position()
|
||||
|
|
@ -695,8 +836,18 @@ where
|
|||
|
||||
let style = Catalog::style(theme, &self.class);
|
||||
|
||||
for ((id, (content, tree)), pane_layout) in
|
||||
contents.zip(layout.children())
|
||||
for (((id, content), tree), pane_layout) in self
|
||||
.panes
|
||||
.iter()
|
||||
.copied()
|
||||
.zip(&self.contents)
|
||||
.zip(&tree.children)
|
||||
.zip(layout.children())
|
||||
.filter(|(((pane, _), _), _)| {
|
||||
self.internal
|
||||
.maximized()
|
||||
.is_none_or(|maximized| maximized == *pane)
|
||||
})
|
||||
{
|
||||
match picked_pane {
|
||||
Some((dragging, origin)) if id == dragging => {
|
||||
|
|
@ -829,13 +980,23 @@ where
|
|||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
translation: Vector,
|
||||
) -> Option<overlay::Element<'_, Message, Theme, Renderer>> {
|
||||
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
|
||||
let children = self
|
||||
.contents
|
||||
.iter_mut()
|
||||
.panes
|
||||
.iter()
|
||||
.copied()
|
||||
.zip(&mut self.contents)
|
||||
.zip(&mut tree.children)
|
||||
.zip(layout.children())
|
||||
.filter_map(|(((_, content), state), layout)| {
|
||||
.filter_map(|(((pane, content), state), layout)| {
|
||||
if self
|
||||
.internal
|
||||
.maximized()
|
||||
.is_some_and(|maximized| maximized != pane)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
content.overlay(state, layout, renderer, translation)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
|
@ -1084,52 +1245,6 @@ fn hovered_split<'a>(
|
|||
})
|
||||
}
|
||||
|
||||
/// 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(..))
|
||||
}
|
||||
}
|
||||
|
||||
/// The appearance of a [`PaneGrid`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Style {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
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::{
|
||||
self, Clipboard, Element, Layout, Point, Rectangle, Shell, Size, Vector,
|
||||
self, Clipboard, Element, Event, Layout, Point, Rectangle, Shell, Size,
|
||||
Vector,
|
||||
};
|
||||
use crate::pane_grid::{Draggable, TitleBar};
|
||||
|
||||
|
|
@ -73,7 +73,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> Content<'a, Message, Theme, Renderer>
|
||||
impl<Message, Theme, Renderer> Content<'_, Message, Theme, Renderer>
|
||||
where
|
||||
Theme: container::Catalog,
|
||||
Renderer: core::Renderer,
|
||||
|
|
@ -214,7 +214,7 @@ where
|
|||
tree: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn widget::Operation<Message>,
|
||||
operation: &mut dyn widget::Operation,
|
||||
) {
|
||||
let body_layout = if let Some(title_bar) = &self.title_bar {
|
||||
let mut children = layout.children();
|
||||
|
|
@ -239,10 +239,10 @@ where
|
|||
);
|
||||
}
|
||||
|
||||
pub(crate) fn on_event(
|
||||
pub(crate) fn update(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: Event,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &Renderer,
|
||||
|
|
@ -250,15 +250,13 @@ where
|
|||
shell: &mut Shell<'_, Message>,
|
||||
viewport: &Rectangle,
|
||||
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(
|
||||
title_bar.update(
|
||||
&mut tree.children[1],
|
||||
event.clone(),
|
||||
event,
|
||||
children.next().unwrap(),
|
||||
cursor,
|
||||
renderer,
|
||||
|
|
@ -272,10 +270,8 @@ where
|
|||
layout
|
||||
};
|
||||
|
||||
let body_status = if is_picked {
|
||||
event::Status::Ignored
|
||||
} else {
|
||||
self.body.as_widget_mut().on_event(
|
||||
if !is_picked {
|
||||
self.body.as_widget_mut().update(
|
||||
&mut tree.children[0],
|
||||
event,
|
||||
body_layout,
|
||||
|
|
@ -284,10 +280,33 @@ where
|
|||
clipboard,
|
||||
shell,
|
||||
viewport,
|
||||
)
|
||||
};
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
event_status.merge(body_status)
|
||||
pub(crate) fn grid_interaction(
|
||||
&self,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
drag_enabled: bool,
|
||||
) -> Option<mouse::Interaction> {
|
||||
let title_bar = self.title_bar.as_ref()?;
|
||||
|
||||
let mut children = layout.children();
|
||||
let title_bar_layout = children.next().unwrap();
|
||||
|
||||
let is_over_pick_area = cursor
|
||||
.position()
|
||||
.map(|cursor_position| {
|
||||
title_bar.is_over_pick_area(title_bar_layout, cursor_position)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
if is_over_pick_area && drag_enabled {
|
||||
return Some(mouse::Interaction::Grab);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) fn mouse_interaction(
|
||||
|
|
@ -382,8 +401,8 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> Draggable
|
||||
for &Content<'a, Message, Theme, Renderer>
|
||||
impl<Message, Theme, Renderer> Draggable
|
||||
for &Content<'_, Message, Theme, Renderer>
|
||||
where
|
||||
Theme: container::Catalog,
|
||||
Renderer: core::Renderer,
|
||||
|
|
|
|||
59
widget/src/pane_grid/controls.rs
Normal file
59
widget/src/pane_grid/controls.rs
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
use crate::container;
|
||||
use crate::core::{self, Element};
|
||||
|
||||
/// The controls of a [`Pane`].
|
||||
///
|
||||
/// [`Pane`]: super::Pane
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Controls<
|
||||
'a,
|
||||
Message,
|
||||
Theme = crate::Theme,
|
||||
Renderer = crate::Renderer,
|
||||
> where
|
||||
Theme: container::Catalog,
|
||||
Renderer: core::Renderer,
|
||||
{
|
||||
pub(super) full: Element<'a, Message, Theme, Renderer>,
|
||||
pub(super) compact: Option<Element<'a, Message, Theme, Renderer>>,
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> Controls<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Theme: container::Catalog,
|
||||
Renderer: core::Renderer,
|
||||
{
|
||||
/// Creates a new [`Controls`] with the given content.
|
||||
pub fn new(
|
||||
content: impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
full: content.into(),
|
||||
compact: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new [`Controls`] with a full and compact variant.
|
||||
/// If there is not enough room to show the full variant without overlap,
|
||||
/// then the compact variant will be shown instead.
|
||||
pub fn dynamic(
|
||||
full: impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||
compact: impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
full: full.into(),
|
||||
compact: Some(compact.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> From<Element<'a, Message, Theme, Renderer>>
|
||||
for Controls<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Theme: container::Catalog,
|
||||
Renderer: core::Renderer,
|
||||
{
|
||||
fn from(value: Element<'a, Message, Theme, Renderer>) -> Self {
|
||||
Self::new(value)
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,8 @@ use crate::pane_grid::{
|
|||
Axis, Configuration, Direction, Edge, Node, Pane, Region, Split, Target,
|
||||
};
|
||||
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::borrow::Cow;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
/// The state of a [`PaneGrid`].
|
||||
///
|
||||
|
|
@ -25,17 +26,12 @@ pub struct State<T> {
|
|||
/// The panes of the [`PaneGrid`].
|
||||
///
|
||||
/// [`PaneGrid`]: super::PaneGrid
|
||||
pub panes: FxHashMap<Pane, T>,
|
||||
pub panes: BTreeMap<Pane, T>,
|
||||
|
||||
/// The internal state of the [`PaneGrid`].
|
||||
///
|
||||
/// [`PaneGrid`]: super::PaneGrid
|
||||
pub internal: Internal,
|
||||
|
||||
/// The maximized [`Pane`] of the [`PaneGrid`].
|
||||
///
|
||||
/// [`PaneGrid`]: super::PaneGrid
|
||||
pub(super) maximized: Option<Pane>,
|
||||
}
|
||||
|
||||
impl<T> State<T> {
|
||||
|
|
@ -52,16 +48,12 @@ impl<T> State<T> {
|
|||
|
||||
/// Creates a new [`State`] with the given [`Configuration`].
|
||||
pub fn with_configuration(config: impl Into<Configuration<T>>) -> Self {
|
||||
let mut panes = FxHashMap::default();
|
||||
let mut panes = BTreeMap::default();
|
||||
|
||||
let internal =
|
||||
Internal::from_configuration(&mut panes, config.into(), 0);
|
||||
|
||||
State {
|
||||
panes,
|
||||
internal,
|
||||
maximized: None,
|
||||
}
|
||||
State { panes, internal }
|
||||
}
|
||||
|
||||
/// Returns the total amount of panes in the [`State`].
|
||||
|
|
@ -214,7 +206,7 @@ impl<T> State<T> {
|
|||
}
|
||||
|
||||
let _ = self.panes.insert(new_pane, state);
|
||||
let _ = self.maximized.take();
|
||||
let _ = self.internal.maximized.take();
|
||||
|
||||
Some((new_pane, new_split))
|
||||
}
|
||||
|
|
@ -228,8 +220,11 @@ impl<T> State<T> {
|
|||
) {
|
||||
if let Some((state, _)) = self.close(pane) {
|
||||
if let Some((new_pane, _)) = self.split(axis, target, state) {
|
||||
// Ensure new node corresponds to original closed `Pane` for state continuity
|
||||
self.relabel(new_pane, pane);
|
||||
|
||||
if swap {
|
||||
self.swap(target, new_pane);
|
||||
self.swap(target, pane);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -259,13 +254,27 @@ impl<T> State<T> {
|
|||
&mut self,
|
||||
axis: Axis,
|
||||
pane: Pane,
|
||||
swap: bool,
|
||||
inverse: bool,
|
||||
) {
|
||||
if let Some((state, _)) = self.close(pane) {
|
||||
let _ = self.split_node(axis, None, state, swap);
|
||||
if let Some((new_pane, _)) =
|
||||
self.split_node(axis, None, state, inverse)
|
||||
{
|
||||
// Ensure new node corresponds to original closed `Pane` for state continuity
|
||||
self.relabel(new_pane, pane);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn relabel(&mut self, target: Pane, label: Pane) {
|
||||
self.swap(target, label);
|
||||
|
||||
let _ = self
|
||||
.panes
|
||||
.remove(&target)
|
||||
.and_then(|state| self.panes.insert(label, state));
|
||||
}
|
||||
|
||||
/// Swaps the position of the provided panes in the [`State`].
|
||||
///
|
||||
/// If you want to swap panes on drag and drop in your [`PaneGrid`], you
|
||||
|
|
@ -303,8 +312,8 @@ impl<T> State<T> {
|
|||
/// 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 self.internal.maximized == Some(pane) {
|
||||
let _ = self.internal.maximized.take();
|
||||
}
|
||||
|
||||
if let Some(sibling) = self.internal.layout.remove(pane) {
|
||||
|
|
@ -319,7 +328,7 @@ impl<T> State<T> {
|
|||
///
|
||||
/// [`PaneGrid`]: super::PaneGrid
|
||||
pub fn maximize(&mut self, pane: Pane) {
|
||||
self.maximized = Some(pane);
|
||||
self.internal.maximized = Some(pane);
|
||||
}
|
||||
|
||||
/// Restore the currently maximized [`Pane`] to it's normal size. All panes
|
||||
|
|
@ -327,14 +336,14 @@ impl<T> State<T> {
|
|||
///
|
||||
/// [`PaneGrid`]: super::PaneGrid
|
||||
pub fn restore(&mut self) {
|
||||
let _ = self.maximized.take();
|
||||
let _ = self.internal.maximized.take();
|
||||
}
|
||||
|
||||
/// Returns the maximized [`Pane`] of the [`PaneGrid`].
|
||||
///
|
||||
/// [`PaneGrid`]: super::PaneGrid
|
||||
pub fn maximized(&self) -> Option<Pane> {
|
||||
self.maximized
|
||||
self.internal.maximized
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -345,6 +354,7 @@ impl<T> State<T> {
|
|||
pub struct Internal {
|
||||
layout: Node,
|
||||
last_id: usize,
|
||||
maximized: Option<Pane>,
|
||||
}
|
||||
|
||||
impl Internal {
|
||||
|
|
@ -353,7 +363,7 @@ impl Internal {
|
|||
///
|
||||
/// [`PaneGrid`]: super::PaneGrid
|
||||
pub fn from_configuration<T>(
|
||||
panes: &mut FxHashMap<Pane, T>,
|
||||
panes: &mut BTreeMap<Pane, T>,
|
||||
content: Configuration<T>,
|
||||
next_id: usize,
|
||||
) -> Self {
|
||||
|
|
@ -390,18 +400,34 @@ impl Internal {
|
|||
}
|
||||
};
|
||||
|
||||
Self { layout, last_id }
|
||||
Self {
|
||||
layout,
|
||||
last_id,
|
||||
maximized: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn layout(&self) -> Cow<'_, Node> {
|
||||
match self.maximized {
|
||||
Some(pane) => Cow::Owned(Node::Pane(pane)),
|
||||
None => Cow::Borrowed(&self.layout),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn maximized(&self) -> Option<Pane> {
|
||||
self.maximized
|
||||
}
|
||||
}
|
||||
|
||||
/// The current action of a [`PaneGrid`].
|
||||
///
|
||||
/// [`PaneGrid`]: super::PaneGrid
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||
pub enum Action {
|
||||
/// The [`PaneGrid`] is idle.
|
||||
///
|
||||
/// [`PaneGrid`]: super::PaneGrid
|
||||
#[default]
|
||||
Idle,
|
||||
/// A [`Pane`] in the [`PaneGrid`] is being dragged.
|
||||
///
|
||||
|
|
@ -440,10 +466,3 @@ impl Action {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Internal {
|
||||
/// The layout [`Node`] of the [`Internal`] state
|
||||
pub fn layout(&self) -> &Node {
|
||||
&self.layout
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
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::{
|
||||
self, Clipboard, Element, Layout, Padding, Point, Rectangle, Shell, Size,
|
||||
Vector,
|
||||
self, Clipboard, Element, Event, Layout, Padding, Point, Rectangle, Shell,
|
||||
Size, Vector,
|
||||
};
|
||||
use crate::pane_grid::controls::Controls;
|
||||
|
||||
/// The title bar of a [`Pane`].
|
||||
///
|
||||
|
|
@ -24,7 +24,7 @@ pub struct TitleBar<
|
|||
Renderer: core::Renderer,
|
||||
{
|
||||
content: Element<'a, Message, Theme, Renderer>,
|
||||
controls: Option<Element<'a, Message, Theme, Renderer>>,
|
||||
controls: Option<Controls<'a, Message, Theme, Renderer>>,
|
||||
padding: Padding,
|
||||
always_show_controls: bool,
|
||||
class: Theme::Class<'a>,
|
||||
|
|
@ -51,7 +51,7 @@ where
|
|||
/// Sets the controls of the [`TitleBar`].
|
||||
pub fn controls(
|
||||
mut self,
|
||||
controls: impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||
controls: impl Into<Controls<'a, Message, Theme, Renderer>>,
|
||||
) -> Self {
|
||||
self.controls = Some(controls.into());
|
||||
self
|
||||
|
|
@ -98,16 +98,28 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> TitleBar<'a, Message, Theme, Renderer>
|
||||
impl<Message, Theme, Renderer> TitleBar<'_, Message, Theme, Renderer>
|
||||
where
|
||||
Theme: container::Catalog,
|
||||
Renderer: core::Renderer,
|
||||
{
|
||||
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()]
|
||||
let children = match self.controls.as_ref() {
|
||||
Some(controls) => match controls.compact.as_ref() {
|
||||
Some(compact) => vec![
|
||||
Tree::new(&self.content),
|
||||
Tree::new(&controls.full),
|
||||
Tree::new(compact),
|
||||
],
|
||||
None => vec![
|
||||
Tree::new(&self.content),
|
||||
Tree::new(&controls.full),
|
||||
Tree::empty(),
|
||||
],
|
||||
},
|
||||
None => {
|
||||
vec![Tree::new(&self.content), Tree::empty(), Tree::empty()]
|
||||
}
|
||||
};
|
||||
|
||||
Tree {
|
||||
|
|
@ -117,9 +129,13 @@ where
|
|||
}
|
||||
|
||||
pub(super) fn diff(&self, tree: &mut Tree) {
|
||||
if tree.children.len() == 2 {
|
||||
if tree.children.len() == 3 {
|
||||
if let Some(controls) = self.controls.as_ref() {
|
||||
tree.children[1].diff(controls);
|
||||
if let Some(compact) = controls.compact.as_ref() {
|
||||
tree.children[2].diff(compact);
|
||||
}
|
||||
|
||||
tree.children[1].diff(&controls.full);
|
||||
}
|
||||
|
||||
tree.children[0].diff(&self.content);
|
||||
|
|
@ -164,18 +180,42 @@ where
|
|||
if title_layout.bounds().width + controls_layout.bounds().width
|
||||
> padded.bounds().width
|
||||
{
|
||||
show_title = false;
|
||||
}
|
||||
if let Some(compact) = controls.compact.as_ref() {
|
||||
let compact_layout = children.next().unwrap();
|
||||
|
||||
controls.as_widget().draw(
|
||||
&tree.children[1],
|
||||
renderer,
|
||||
theme,
|
||||
&inherited_style,
|
||||
controls_layout,
|
||||
cursor,
|
||||
viewport,
|
||||
);
|
||||
compact.as_widget().draw(
|
||||
&tree.children[2],
|
||||
renderer,
|
||||
theme,
|
||||
&inherited_style,
|
||||
compact_layout,
|
||||
cursor,
|
||||
viewport,
|
||||
);
|
||||
} else {
|
||||
show_title = false;
|
||||
|
||||
controls.full.as_widget().draw(
|
||||
&tree.children[1],
|
||||
renderer,
|
||||
theme,
|
||||
&inherited_style,
|
||||
controls_layout,
|
||||
cursor,
|
||||
viewport,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
controls.full.as_widget().draw(
|
||||
&tree.children[1],
|
||||
renderer,
|
||||
theme,
|
||||
&inherited_style,
|
||||
controls_layout,
|
||||
cursor,
|
||||
viewport,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -207,13 +247,20 @@ where
|
|||
let mut children = padded.children();
|
||||
let title_layout = children.next().unwrap();
|
||||
|
||||
if self.controls.is_some() {
|
||||
if let Some(controls) = self.controls.as_ref() {
|
||||
let controls_layout = children.next().unwrap();
|
||||
|
||||
if title_layout.bounds().width + controls_layout.bounds().width
|
||||
> padded.bounds().width
|
||||
{
|
||||
!controls_layout.bounds().contains(cursor_position)
|
||||
if controls.compact.is_some() {
|
||||
let compact_layout = children.next().unwrap();
|
||||
|
||||
!compact_layout.bounds().contains(cursor_position)
|
||||
&& !title_layout.bounds().contains(cursor_position)
|
||||
} else {
|
||||
!controls_layout.bounds().contains(cursor_position)
|
||||
}
|
||||
} else {
|
||||
!controls_layout.bounds().contains(cursor_position)
|
||||
&& !title_layout.bounds().contains(cursor_position)
|
||||
|
|
@ -244,25 +291,73 @@ where
|
|||
let title_size = title_layout.size();
|
||||
|
||||
let node = if let Some(controls) = &self.controls {
|
||||
let controls_layout = controls.as_widget().layout(
|
||||
let controls_layout = controls.full.as_widget().layout(
|
||||
&mut tree.children[1],
|
||||
renderer,
|
||||
&layout::Limits::new(Size::ZERO, max_size),
|
||||
);
|
||||
|
||||
let controls_size = controls_layout.size();
|
||||
let space_before_controls = max_size.width - controls_size.width;
|
||||
if title_layout.bounds().width + controls_layout.bounds().width
|
||||
> max_size.width
|
||||
{
|
||||
if let Some(compact) = controls.compact.as_ref() {
|
||||
let compact_layout = compact.as_widget().layout(
|
||||
&mut tree.children[2],
|
||||
renderer,
|
||||
&layout::Limits::new(Size::ZERO, max_size),
|
||||
);
|
||||
|
||||
let height = title_size.height.max(controls_size.height);
|
||||
let compact_size = compact_layout.size();
|
||||
let space_before_controls =
|
||||
max_size.width - compact_size.width;
|
||||
|
||||
layout::Node::with_children(
|
||||
Size::new(max_size.width, height),
|
||||
vec![
|
||||
title_layout,
|
||||
controls_layout
|
||||
.move_to(Point::new(space_before_controls, 0.0)),
|
||||
],
|
||||
)
|
||||
let height = title_size.height.max(compact_size.height);
|
||||
|
||||
layout::Node::with_children(
|
||||
Size::new(max_size.width, height),
|
||||
vec![
|
||||
title_layout,
|
||||
controls_layout,
|
||||
compact_layout.move_to(Point::new(
|
||||
space_before_controls,
|
||||
0.0,
|
||||
)),
|
||||
],
|
||||
)
|
||||
} else {
|
||||
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);
|
||||
|
||||
layout::Node::with_children(
|
||||
Size::new(max_size.width, height),
|
||||
vec![
|
||||
title_layout,
|
||||
controls_layout.move_to(Point::new(
|
||||
space_before_controls,
|
||||
0.0,
|
||||
)),
|
||||
],
|
||||
)
|
||||
}
|
||||
} else {
|
||||
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);
|
||||
|
||||
layout::Node::with_children(
|
||||
Size::new(max_size.width, height),
|
||||
vec![
|
||||
title_layout,
|
||||
controls_layout
|
||||
.move_to(Point::new(space_before_controls, 0.0)),
|
||||
],
|
||||
)
|
||||
}
|
||||
} else {
|
||||
layout::Node::with_children(
|
||||
Size::new(max_size.width, title_size.height),
|
||||
|
|
@ -278,7 +373,7 @@ where
|
|||
tree: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn widget::Operation<Message>,
|
||||
operation: &mut dyn widget::Operation,
|
||||
) {
|
||||
let mut children = layout.children();
|
||||
let padded = children.next().unwrap();
|
||||
|
|
@ -293,15 +388,33 @@ where
|
|||
if title_layout.bounds().width + controls_layout.bounds().width
|
||||
> padded.bounds().width
|
||||
{
|
||||
show_title = false;
|
||||
}
|
||||
if let Some(compact) = controls.compact.as_ref() {
|
||||
let compact_layout = children.next().unwrap();
|
||||
|
||||
controls.as_widget().operate(
|
||||
&mut tree.children[1],
|
||||
controls_layout,
|
||||
renderer,
|
||||
operation,
|
||||
);
|
||||
compact.as_widget().operate(
|
||||
&mut tree.children[2],
|
||||
compact_layout,
|
||||
renderer,
|
||||
operation,
|
||||
);
|
||||
} else {
|
||||
show_title = false;
|
||||
|
||||
controls.full.as_widget().operate(
|
||||
&mut tree.children[1],
|
||||
controls_layout,
|
||||
renderer,
|
||||
operation,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
controls.full.as_widget().operate(
|
||||
&mut tree.children[1],
|
||||
controls_layout,
|
||||
renderer,
|
||||
operation,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if show_title {
|
||||
|
|
@ -314,17 +427,17 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn on_event(
|
||||
pub(crate) fn update(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: Event,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
viewport: &Rectangle,
|
||||
) -> event::Status {
|
||||
) {
|
||||
let mut children = layout.children();
|
||||
let padded = children.next().unwrap();
|
||||
|
||||
|
|
@ -332,30 +445,55 @@ where
|
|||
let title_layout = children.next().unwrap();
|
||||
let mut show_title = true;
|
||||
|
||||
let control_status = if let Some(controls) = &mut self.controls {
|
||||
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;
|
||||
if let Some(compact) = controls.compact.as_mut() {
|
||||
let compact_layout = children.next().unwrap();
|
||||
|
||||
compact.as_widget_mut().update(
|
||||
&mut tree.children[2],
|
||||
event,
|
||||
compact_layout,
|
||||
cursor,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
viewport,
|
||||
);
|
||||
} else {
|
||||
show_title = false;
|
||||
|
||||
controls.full.as_widget_mut().update(
|
||||
&mut tree.children[1],
|
||||
event,
|
||||
controls_layout,
|
||||
cursor,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
viewport,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
controls.full.as_widget_mut().update(
|
||||
&mut tree.children[1],
|
||||
event,
|
||||
controls_layout,
|
||||
cursor,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
viewport,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
controls.as_widget_mut().on_event(
|
||||
&mut tree.children[1],
|
||||
event.clone(),
|
||||
controls_layout,
|
||||
cursor,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
viewport,
|
||||
)
|
||||
} else {
|
||||
event::Status::Ignored
|
||||
};
|
||||
|
||||
let title_status = if show_title {
|
||||
self.content.as_widget_mut().on_event(
|
||||
if show_title {
|
||||
self.content.as_widget_mut().update(
|
||||
&mut tree.children[0],
|
||||
event,
|
||||
title_layout,
|
||||
|
|
@ -364,12 +502,8 @@ where
|
|||
clipboard,
|
||||
shell,
|
||||
viewport,
|
||||
)
|
||||
} else {
|
||||
event::Status::Ignored
|
||||
};
|
||||
|
||||
control_status.merge(title_status)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn mouse_interaction(
|
||||
|
|
@ -396,18 +530,33 @@ where
|
|||
|
||||
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,
|
||||
viewport,
|
||||
renderer,
|
||||
);
|
||||
let controls_interaction =
|
||||
controls.full.as_widget().mouse_interaction(
|
||||
&tree.children[1],
|
||||
controls_layout,
|
||||
cursor,
|
||||
viewport,
|
||||
renderer,
|
||||
);
|
||||
|
||||
if title_layout.bounds().width + controls_layout.bounds().width
|
||||
> padded.bounds().width
|
||||
{
|
||||
controls_interaction
|
||||
if let Some(compact) = controls.compact.as_ref() {
|
||||
let compact_layout = children.next().unwrap();
|
||||
let compact_interaction =
|
||||
compact.as_widget().mouse_interaction(
|
||||
&tree.children[2],
|
||||
compact_layout,
|
||||
cursor,
|
||||
viewport,
|
||||
renderer,
|
||||
);
|
||||
|
||||
compact_interaction.max(title_interaction)
|
||||
} else {
|
||||
controls_interaction
|
||||
}
|
||||
} else {
|
||||
controls_interaction.max(title_interaction)
|
||||
}
|
||||
|
|
@ -444,12 +593,36 @@ where
|
|||
controls.as_mut().and_then(|controls| {
|
||||
let controls_layout = children.next()?;
|
||||
|
||||
controls.as_widget_mut().overlay(
|
||||
controls_state,
|
||||
controls_layout,
|
||||
renderer,
|
||||
translation,
|
||||
)
|
||||
if title_layout.bounds().width
|
||||
+ controls_layout.bounds().width
|
||||
> padded.bounds().width
|
||||
{
|
||||
if let Some(compact) = controls.compact.as_mut() {
|
||||
let compact_state = states.next().unwrap();
|
||||
let compact_layout = children.next()?;
|
||||
|
||||
compact.as_widget_mut().overlay(
|
||||
compact_state,
|
||||
compact_layout,
|
||||
renderer,
|
||||
translation,
|
||||
)
|
||||
} else {
|
||||
controls.full.as_widget_mut().overlay(
|
||||
controls_state,
|
||||
controls_layout,
|
||||
renderer,
|
||||
translation,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
controls.full.as_widget_mut().overlay(
|
||||
controls_state,
|
||||
controls_layout,
|
||||
renderer,
|
||||
translation,
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,79 @@
|
|||
//! Display a dropdown list of selectable values.
|
||||
//! Pick lists display a dropdown list of selectable options.
|
||||
//!
|
||||
//! # Example
|
||||
//! ```no_run
|
||||
//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
|
||||
//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||
//! #
|
||||
//! use iced::widget::pick_list;
|
||||
//!
|
||||
//! struct State {
|
||||
//! favorite: Option<Fruit>,
|
||||
//! }
|
||||
//!
|
||||
//! #[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
//! enum Fruit {
|
||||
//! Apple,
|
||||
//! Orange,
|
||||
//! Strawberry,
|
||||
//! Tomato,
|
||||
//! }
|
||||
//!
|
||||
//! #[derive(Debug, Clone)]
|
||||
//! enum Message {
|
||||
//! FruitSelected(Fruit),
|
||||
//! }
|
||||
//!
|
||||
//! fn view(state: &State) -> Element<'_, Message> {
|
||||
//! let fruits = [
|
||||
//! Fruit::Apple,
|
||||
//! Fruit::Orange,
|
||||
//! Fruit::Strawberry,
|
||||
//! Fruit::Tomato,
|
||||
//! ];
|
||||
//!
|
||||
//! pick_list(
|
||||
//! fruits,
|
||||
//! state.favorite,
|
||||
//! Message::FruitSelected,
|
||||
//! )
|
||||
//! .placeholder("Select your favorite fruit...")
|
||||
//! .into()
|
||||
//! }
|
||||
//!
|
||||
//! fn update(state: &mut State, message: Message) {
|
||||
//! match message {
|
||||
//! Message::FruitSelected(fruit) => {
|
||||
//! state.favorite = Some(fruit);
|
||||
//! }
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! impl std::fmt::Display for Fruit {
|
||||
//! fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
//! f.write_str(match self {
|
||||
//! Self::Apple => "Apple",
|
||||
//! Self::Orange => "Orange",
|
||||
//! Self::Strawberry => "Strawberry",
|
||||
//! Self::Tomato => "Tomato",
|
||||
//! })
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
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, Paragraph as _, Text};
|
||||
use crate::core::text::paragraph;
|
||||
use crate::core::text::{self, Text};
|
||||
use crate::core::touch;
|
||||
use crate::core::widget::tree::{self, Tree};
|
||||
use crate::core::window;
|
||||
use crate::core::{
|
||||
Background, Border, Clipboard, Color, Element, Layout, Length, Padding,
|
||||
Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget,
|
||||
Background, Border, Clipboard, Color, Element, Event, Layout, Length,
|
||||
Padding, Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget,
|
||||
};
|
||||
use crate::overlay::menu::{self, Menu};
|
||||
|
||||
|
|
@ -19,6 +81,67 @@ use std::borrow::Borrow;
|
|||
use std::f32;
|
||||
|
||||
/// A widget for selecting a single value from a list of options.
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
|
||||
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||
/// #
|
||||
/// use iced::widget::pick_list;
|
||||
///
|
||||
/// struct State {
|
||||
/// favorite: Option<Fruit>,
|
||||
/// }
|
||||
///
|
||||
/// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
/// enum Fruit {
|
||||
/// Apple,
|
||||
/// Orange,
|
||||
/// Strawberry,
|
||||
/// Tomato,
|
||||
/// }
|
||||
///
|
||||
/// #[derive(Debug, Clone)]
|
||||
/// enum Message {
|
||||
/// FruitSelected(Fruit),
|
||||
/// }
|
||||
///
|
||||
/// fn view(state: &State) -> Element<'_, Message> {
|
||||
/// let fruits = [
|
||||
/// Fruit::Apple,
|
||||
/// Fruit::Orange,
|
||||
/// Fruit::Strawberry,
|
||||
/// Fruit::Tomato,
|
||||
/// ];
|
||||
///
|
||||
/// pick_list(
|
||||
/// fruits,
|
||||
/// state.favorite,
|
||||
/// Message::FruitSelected,
|
||||
/// )
|
||||
/// .placeholder("Select your favorite fruit...")
|
||||
/// .into()
|
||||
/// }
|
||||
///
|
||||
/// fn update(state: &mut State, message: Message) {
|
||||
/// match message {
|
||||
/// Message::FruitSelected(fruit) => {
|
||||
/// state.favorite = Some(fruit);
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// impl std::fmt::Display for Fruit {
|
||||
/// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
/// f.write_str(match self {
|
||||
/// Self::Apple => "Apple",
|
||||
/// Self::Orange => "Orange",
|
||||
/// Self::Strawberry => "Strawberry",
|
||||
/// Self::Tomato => "Tomato",
|
||||
/// })
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct PickList<
|
||||
'a,
|
||||
|
|
@ -50,6 +173,7 @@ pub struct PickList<
|
|||
handle: Handle<Renderer::Font>,
|
||||
class: <Theme as Catalog>::Class<'a>,
|
||||
menu_class: <Theme as menu::Catalog>::Class<'a>,
|
||||
last_status: Option<Status>,
|
||||
}
|
||||
|
||||
impl<'a, T, L, V, Message, Theme, Renderer>
|
||||
|
|
@ -80,11 +204,12 @@ where
|
|||
padding: crate::button::DEFAULT_PADDING,
|
||||
text_size: None,
|
||||
text_line_height: text::LineHeight::default(),
|
||||
text_shaping: text::Shaping::Basic,
|
||||
text_shaping: text::Shaping::default(),
|
||||
font: None,
|
||||
handle: Handle::default(),
|
||||
class: <Theme as Catalog>::default(),
|
||||
menu_class: <Theme as Catalog>::default_menu(),
|
||||
last_status: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -161,6 +286,19 @@ where
|
|||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the [`Menu`].
|
||||
#[must_use]
|
||||
pub fn menu_style(
|
||||
mut self,
|
||||
style: impl Fn(&Theme) -> menu::Style + 'a,
|
||||
) -> Self
|
||||
where
|
||||
<Theme as menu::Catalog>::Class<'a>: From<menu::StyleFn<'a, Theme>>,
|
||||
{
|
||||
self.menu_class = (Box::new(style) as menu::StyleFn<'a, Theme>).into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style class of the [`PickList`].
|
||||
#[cfg(feature = "advanced")]
|
||||
#[must_use]
|
||||
|
|
@ -171,6 +309,17 @@ where
|
|||
self.class = class.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style class of the [`Menu`].
|
||||
#[cfg(feature = "advanced")]
|
||||
#[must_use]
|
||||
pub fn menu_class(
|
||||
mut self,
|
||||
class: impl Into<<Theme as menu::Catalog>::Class<'a>>,
|
||||
) -> Self {
|
||||
self.menu_class = class.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T, L, V, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
|
|
@ -225,6 +374,7 @@ where
|
|||
horizontal_alignment: alignment::Horizontal::Left,
|
||||
vertical_alignment: alignment::Vertical::Center,
|
||||
shaping: self.text_shaping,
|
||||
wrapping: text::Wrapping::default(),
|
||||
};
|
||||
|
||||
for (option, paragraph) in options.iter().zip(state.options.iter_mut())
|
||||
|
|
@ -277,23 +427,22 @@ where
|
|||
layout::Node::new(size)
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
fn update(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: Event,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
_renderer: &Renderer,
|
||||
_clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
_viewport: &Rectangle,
|
||||
) -> event::Status {
|
||||
) {
|
||||
let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
|
||||
|
||||
match event {
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
|
||||
| Event::Touch(touch::Event::FingerPressed { .. }) => {
|
||||
let state =
|
||||
tree.state.downcast_mut::<State<Renderer::Paragraph>>();
|
||||
|
||||
if state.is_open {
|
||||
// Event wasn't processed by overlay, so cursor was clicked either outside its
|
||||
// bounds or on the drop-down, either way we close the overlay.
|
||||
|
|
@ -303,7 +452,7 @@ where
|
|||
shell.publish(on_close.clone());
|
||||
}
|
||||
|
||||
event::Status::Captured
|
||||
shell.capture_event();
|
||||
} else if cursor.is_over(layout.bounds()) {
|
||||
let selected = self.selected.as_ref().map(Borrow::borrow);
|
||||
|
||||
|
|
@ -318,17 +467,12 @@ where
|
|||
shell.publish(on_open.clone());
|
||||
}
|
||||
|
||||
event::Status::Captured
|
||||
} else {
|
||||
event::Status::Ignored
|
||||
shell.capture_event();
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse::Event::WheelScrolled {
|
||||
delta: mouse::ScrollDelta::Lines { y, .. },
|
||||
}) => {
|
||||
let state =
|
||||
tree.state.downcast_mut::<State<Renderer::Paragraph>>();
|
||||
|
||||
if state.keyboard_modifiers.command()
|
||||
&& cursor.is_over(layout.bounds())
|
||||
&& !state.is_open
|
||||
|
|
@ -345,13 +489,13 @@ where
|
|||
let options = self.options.borrow();
|
||||
let selected = self.selected.as_ref().map(Borrow::borrow);
|
||||
|
||||
let next_option = if y < 0.0 {
|
||||
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 {
|
||||
} else if *y > 0.0 {
|
||||
if let Some(selected) = selected {
|
||||
find_next(selected, options.iter().rev())
|
||||
} else {
|
||||
|
|
@ -365,20 +509,34 @@ where
|
|||
shell.publish((self.on_select)(next_option.clone()));
|
||||
}
|
||||
|
||||
event::Status::Captured
|
||||
} else {
|
||||
event::Status::Ignored
|
||||
shell.capture_event();
|
||||
}
|
||||
}
|
||||
Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
|
||||
let state =
|
||||
tree.state.downcast_mut::<State<Renderer::Paragraph>>();
|
||||
|
||||
state.keyboard_modifiers = modifiers;
|
||||
|
||||
event::Status::Ignored
|
||||
state.keyboard_modifiers = *modifiers;
|
||||
}
|
||||
_ => event::Status::Ignored,
|
||||
_ => {}
|
||||
};
|
||||
|
||||
let status = {
|
||||
let is_hovered = cursor.is_over(layout.bounds());
|
||||
|
||||
if state.is_open {
|
||||
Status::Opened { is_hovered }
|
||||
} else if is_hovered {
|
||||
Status::Hovered
|
||||
} else {
|
||||
Status::Active
|
||||
}
|
||||
};
|
||||
|
||||
if let Event::Window(window::Event::RedrawRequested(_now)) = event {
|
||||
self.last_status = Some(status);
|
||||
} else if self
|
||||
.last_status
|
||||
.is_some_and(|last_status| last_status != status)
|
||||
{
|
||||
shell.request_redraw();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -407,7 +565,7 @@ where
|
|||
theme: &Theme,
|
||||
_style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
_cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
let font = self.font.unwrap_or_else(|| renderer.default_font());
|
||||
|
|
@ -415,18 +573,12 @@ where
|
|||
let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>();
|
||||
|
||||
let bounds = layout.bounds();
|
||||
let is_mouse_over = cursor.is_over(bounds);
|
||||
let is_selected = selected.is_some();
|
||||
|
||||
let status = if state.is_open {
|
||||
Status::Opened
|
||||
} else if is_mouse_over {
|
||||
Status::Hovered
|
||||
} else {
|
||||
Status::Active
|
||||
};
|
||||
|
||||
let style = Catalog::style(theme, &self.class, status);
|
||||
let style = Catalog::style(
|
||||
theme,
|
||||
&self.class,
|
||||
self.last_status.unwrap_or(Status::Active),
|
||||
);
|
||||
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
|
|
@ -490,6 +642,7 @@ where
|
|||
horizontal_alignment: alignment::Horizontal::Right,
|
||||
vertical_alignment: alignment::Vertical::Center,
|
||||
shaping,
|
||||
wrapping: text::Wrapping::default(),
|
||||
},
|
||||
Point::new(
|
||||
bounds.x + bounds.width - self.padding.right,
|
||||
|
|
@ -519,9 +672,10 @@ where
|
|||
horizontal_alignment: alignment::Horizontal::Left,
|
||||
vertical_alignment: alignment::Vertical::Center,
|
||||
shaping: self.text_shaping,
|
||||
wrapping: text::Wrapping::default(),
|
||||
},
|
||||
Point::new(bounds.x + self.padding.left, bounds.center_y()),
|
||||
if is_selected {
|
||||
if selected.is_some() {
|
||||
style.text_color
|
||||
} else {
|
||||
style.placeholder_color
|
||||
|
|
@ -598,8 +752,8 @@ struct State<P: text::Paragraph> {
|
|||
keyboard_modifiers: keyboard::Modifiers,
|
||||
is_open: bool,
|
||||
hovered_option: Option<usize>,
|
||||
options: Vec<P>,
|
||||
placeholder: P,
|
||||
options: Vec<paragraph::Plain<P>>,
|
||||
placeholder: paragraph::Plain<P>,
|
||||
}
|
||||
|
||||
impl<P: text::Paragraph> State<P> {
|
||||
|
|
@ -611,7 +765,7 @@ impl<P: text::Paragraph> State<P> {
|
|||
is_open: bool::default(),
|
||||
hovered_option: Option::default(),
|
||||
options: Vec::new(),
|
||||
placeholder: P::default(),
|
||||
placeholder: paragraph::Plain::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -674,11 +828,14 @@ pub enum Status {
|
|||
/// The [`PickList`] is being hovered.
|
||||
Hovered,
|
||||
/// The [`PickList`] is open.
|
||||
Opened,
|
||||
Opened {
|
||||
/// Whether the [`PickList`] is hovered, while open.
|
||||
is_hovered: bool,
|
||||
},
|
||||
}
|
||||
|
||||
/// The appearance of a pick list.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Style {
|
||||
/// The text [`Color`] of the pick list.
|
||||
pub text_color: Color,
|
||||
|
|
@ -748,7 +905,7 @@ pub fn default(theme: &Theme, status: Status) -> Style {
|
|||
|
||||
match status {
|
||||
Status::Active => active,
|
||||
Status::Hovered | Status::Opened => Style {
|
||||
Status::Hovered | Status::Opened { .. } => Style {
|
||||
border: Border {
|
||||
color: palette.primary.strong.color,
|
||||
..active.border
|
||||
|
|
|
|||
270
widget/src/pin.rs
Normal file
270
widget/src/pin.rs
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
//! A pin widget positions a widget at some fixed coordinates inside its boundaries.
|
||||
//!
|
||||
//! # Example
|
||||
//! ```no_run
|
||||
//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::core::Length::Fill; }
|
||||
//! # pub type State = ();
|
||||
//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||
//! use iced::widget::pin;
|
||||
//! use iced::Fill;
|
||||
//!
|
||||
//! enum Message {
|
||||
//! // ...
|
||||
//! }
|
||||
//!
|
||||
//! fn view(state: &State) -> Element<'_, Message> {
|
||||
//! pin("This text is displayed at coordinates (50, 50)!")
|
||||
//! .x(50)
|
||||
//! .y(50)
|
||||
//! .into()
|
||||
//! }
|
||||
//! ```
|
||||
use crate::core::layout;
|
||||
use crate::core::mouse;
|
||||
use crate::core::overlay;
|
||||
use crate::core::renderer;
|
||||
use crate::core::widget;
|
||||
use crate::core::{
|
||||
self, Clipboard, Element, Event, Layout, Length, Pixels, Point, Rectangle,
|
||||
Shell, Size, Vector, Widget,
|
||||
};
|
||||
|
||||
/// A widget that positions its contents at some fixed coordinates inside of its boundaries.
|
||||
///
|
||||
/// By default, a [`Pin`] widget will try to fill its parent.
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::core::Length::Fill; }
|
||||
/// # pub type State = ();
|
||||
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||
/// use iced::widget::pin;
|
||||
/// use iced::Fill;
|
||||
///
|
||||
/// enum Message {
|
||||
/// // ...
|
||||
/// }
|
||||
///
|
||||
/// fn view(state: &State) -> Element<'_, Message> {
|
||||
/// pin("This text is displayed at coordinates (50, 50)!")
|
||||
/// .x(50)
|
||||
/// .y(50)
|
||||
/// .into()
|
||||
/// }
|
||||
/// ```
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Pin<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
|
||||
where
|
||||
Renderer: core::Renderer,
|
||||
{
|
||||
content: Element<'a, Message, Theme, Renderer>,
|
||||
width: Length,
|
||||
height: Length,
|
||||
position: Point,
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> Pin<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Renderer: core::Renderer,
|
||||
{
|
||||
/// Creates a [`Pin`] widget with the given content.
|
||||
pub fn new(
|
||||
content: impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
content: content.into(),
|
||||
width: Length::Fill,
|
||||
height: Length::Fill,
|
||||
position: Point::ORIGIN,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the width of the [`Pin`].
|
||||
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||
self.width = width.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the height of the [`Pin`].
|
||||
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
||||
self.height = height.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the position of the [`Pin`]; where the pinned widget will be displayed.
|
||||
pub fn position(mut self, position: impl Into<Point>) -> Self {
|
||||
self.position = position.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the X coordinate of the [`Pin`].
|
||||
pub fn x(mut self, x: impl Into<Pixels>) -> Self {
|
||||
self.position.x = x.into().0;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the Y coordinate of the [`Pin`].
|
||||
pub fn y(mut self, y: impl Into<Pixels>) -> Self {
|
||||
self.position.y = y.into().0;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Pin<'_, Message, Theme, Renderer>
|
||||
where
|
||||
Renderer: core::Renderer,
|
||||
{
|
||||
fn tag(&self) -> widget::tree::Tag {
|
||||
self.content.as_widget().tag()
|
||||
}
|
||||
|
||||
fn state(&self) -> widget::tree::State {
|
||||
self.content.as_widget().state()
|
||||
}
|
||||
|
||||
fn children(&self) -> Vec<widget::Tree> {
|
||||
self.content.as_widget().children()
|
||||
}
|
||||
|
||||
fn diff(&self, tree: &mut widget::Tree) {
|
||||
self.content.as_widget().diff(tree);
|
||||
}
|
||||
|
||||
fn size(&self) -> Size<Length> {
|
||||
Size {
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
}
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
tree: &mut widget::Tree,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
let limits = limits.width(self.width).height(self.height);
|
||||
|
||||
let available =
|
||||
limits.max() - Size::new(self.position.x, self.position.y);
|
||||
|
||||
let node = self
|
||||
.content
|
||||
.as_widget()
|
||||
.layout(tree, renderer, &layout::Limits::new(Size::ZERO, available))
|
||||
.move_to(self.position);
|
||||
|
||||
let size = limits.resolve(self.width, self.height, node.size());
|
||||
layout::Node::with_children(size, vec![node])
|
||||
}
|
||||
|
||||
fn operate(
|
||||
&self,
|
||||
tree: &mut widget::Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn widget::Operation,
|
||||
) {
|
||||
self.content.as_widget().operate(
|
||||
tree,
|
||||
layout.children().next().unwrap(),
|
||||
renderer,
|
||||
operation,
|
||||
);
|
||||
}
|
||||
|
||||
fn update(
|
||||
&mut self,
|
||||
tree: &mut widget::Tree,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
self.content.as_widget_mut().update(
|
||||
tree,
|
||||
event,
|
||||
layout.children().next().unwrap(),
|
||||
cursor,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
viewport,
|
||||
);
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
tree: &widget::Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
self.content.as_widget().mouse_interaction(
|
||||
tree,
|
||||
layout.children().next().unwrap(),
|
||||
cursor,
|
||||
viewport,
|
||||
renderer,
|
||||
)
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &widget::Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
let bounds = layout.bounds();
|
||||
|
||||
if let Some(clipped_viewport) = bounds.intersection(viewport) {
|
||||
self.content.as_widget().draw(
|
||||
tree,
|
||||
renderer,
|
||||
theme,
|
||||
style,
|
||||
layout.children().next().unwrap(),
|
||||
cursor,
|
||||
&clipped_viewport,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn overlay<'b>(
|
||||
&'b mut self,
|
||||
tree: &'b mut widget::Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
translation: Vector,
|
||||
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
|
||||
self.content.as_widget_mut().overlay(
|
||||
tree,
|
||||
layout.children().next().unwrap(),
|
||||
renderer,
|
||||
translation,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> From<Pin<'a, Message, Theme, Renderer>>
|
||||
for Element<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Theme: 'a,
|
||||
Renderer: core::Renderer + 'a,
|
||||
{
|
||||
fn from(
|
||||
pin: Pin<'a, Message, Theme, Renderer>,
|
||||
) -> Element<'a, Message, Theme, Renderer> {
|
||||
Element::new(pin)
|
||||
}
|
||||
}
|
||||
319
widget/src/pop.rs
Normal file
319
widget/src/pop.rs
Normal file
|
|
@ -0,0 +1,319 @@
|
|||
//! Generate messages when content pops in and out of view.
|
||||
use crate::core::layout;
|
||||
use crate::core::mouse;
|
||||
use crate::core::overlay;
|
||||
use crate::core::renderer;
|
||||
use crate::core::text;
|
||||
use crate::core::time::{Duration, Instant};
|
||||
use crate::core::widget;
|
||||
use crate::core::widget::tree::{self, Tree};
|
||||
use crate::core::window;
|
||||
use crate::core::{
|
||||
self, Clipboard, Element, Event, Layout, Length, Pixels, Rectangle, Shell,
|
||||
Size, Vector, Widget,
|
||||
};
|
||||
|
||||
/// A widget that can generate messages when its content pops in and out of view.
|
||||
///
|
||||
/// It can even notify you with anticipation at a given distance!
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Pop<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> {
|
||||
content: Element<'a, Message, Theme, Renderer>,
|
||||
key: Option<text::Fragment<'a>>,
|
||||
on_show: Option<Box<dyn Fn(Size) -> Message + 'a>>,
|
||||
on_resize: Option<Box<dyn Fn(Size) -> Message + 'a>>,
|
||||
on_hide: Option<Message>,
|
||||
anticipate: Pixels,
|
||||
delay: Duration,
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> Pop<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Renderer: core::Renderer,
|
||||
Message: Clone,
|
||||
{
|
||||
/// Creates a new [`Pop`] widget with the given content.
|
||||
pub fn new(
|
||||
content: impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
content: content.into(),
|
||||
key: None,
|
||||
on_show: None,
|
||||
on_resize: None,
|
||||
on_hide: None,
|
||||
anticipate: Pixels::ZERO,
|
||||
delay: Duration::ZERO,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the message to be produced when the content pops into view.
|
||||
///
|
||||
/// The closure will receive the [`Size`] of the content in that moment.
|
||||
pub fn on_show(mut self, on_show: impl Fn(Size) -> Message + 'a) -> Self {
|
||||
self.on_show = Some(Box::new(on_show));
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the message to be produced when the content changes [`Size`] once its in view.
|
||||
///
|
||||
/// The closure will receive the new [`Size`] of the content.
|
||||
pub fn on_resize(
|
||||
mut self,
|
||||
on_resize: impl Fn(Size) -> Message + 'a,
|
||||
) -> Self {
|
||||
self.on_resize = Some(Box::new(on_resize));
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the message to be produced when the content pops out of view.
|
||||
pub fn on_hide(mut self, on_hide: Message) -> Self {
|
||||
self.on_hide = Some(on_hide);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the key of the [`Pop`] widget, for continuity.
|
||||
///
|
||||
/// If the key changes, the [`Pop`] widget will trigger again.
|
||||
pub fn key(mut self, key: impl text::IntoFragment<'a>) -> Self {
|
||||
self.key = Some(key.into_fragment());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the distance in [`Pixels`] to use in anticipation of the
|
||||
/// content popping into view.
|
||||
///
|
||||
/// This can be quite useful to lazily load items in a long scrollable
|
||||
/// behind the scenes before the user can notice it!
|
||||
pub fn anticipate(mut self, distance: impl Into<Pixels>) -> Self {
|
||||
self.anticipate = distance.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the amount of time to wait before firing an [`on_show`] or
|
||||
/// [`on_hide`] event; after the content is shown or hidden.
|
||||
///
|
||||
/// When combined with [`key`], this can be useful to debounce key changes.
|
||||
///
|
||||
/// [`on_show`]: Self::on_show
|
||||
/// [`on_hide`]: Self::on_hide
|
||||
/// [`key`]: Self::key
|
||||
pub fn delay(mut self, delay: impl Into<Duration>) -> Self {
|
||||
self.delay = delay.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
struct State {
|
||||
has_popped_in: bool,
|
||||
should_notify_at: Option<(bool, Instant)>,
|
||||
last_size: Option<Size>,
|
||||
last_key: Option<String>,
|
||||
}
|
||||
|
||||
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Pop<'_, Message, Theme, Renderer>
|
||||
where
|
||||
Message: Clone,
|
||||
Renderer: core::Renderer,
|
||||
{
|
||||
fn tag(&self) -> tree::Tag {
|
||||
tree::Tag::of::<State>()
|
||||
}
|
||||
|
||||
fn state(&self) -> tree::State {
|
||||
tree::State::new(State::default())
|
||||
}
|
||||
|
||||
fn children(&self) -> Vec<Tree> {
|
||||
vec![Tree::new(&self.content)]
|
||||
}
|
||||
|
||||
fn diff(&self, tree: &mut Tree) {
|
||||
tree.diff_children(&[&self.content]);
|
||||
}
|
||||
|
||||
fn update(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
if let Event::Window(window::Event::RedrawRequested(now)) = &event {
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
|
||||
if state.has_popped_in
|
||||
&& state.last_key.as_deref() != self.key.as_deref()
|
||||
{
|
||||
state.has_popped_in = false;
|
||||
state.should_notify_at = None;
|
||||
state.last_key =
|
||||
self.key.as_ref().cloned().map(text::Fragment::into_owned);
|
||||
}
|
||||
|
||||
let bounds = layout.bounds();
|
||||
let top_left_distance = viewport.distance(bounds.position());
|
||||
|
||||
let bottom_right_distance = viewport
|
||||
.distance(bounds.position() + Vector::from(bounds.size()));
|
||||
|
||||
let distance = top_left_distance.min(bottom_right_distance);
|
||||
|
||||
if state.has_popped_in {
|
||||
if distance <= self.anticipate.0 {
|
||||
if let Some(on_resize) = &self.on_resize {
|
||||
let size = bounds.size();
|
||||
|
||||
if Some(size) != state.last_size {
|
||||
state.last_size = Some(size);
|
||||
shell.publish(on_resize(size));
|
||||
}
|
||||
}
|
||||
} else if self.on_hide.is_some() {
|
||||
state.has_popped_in = false;
|
||||
state.should_notify_at = Some((false, *now + self.delay));
|
||||
}
|
||||
} else if self.on_show.is_some() && distance <= self.anticipate.0 {
|
||||
let size = bounds.size();
|
||||
|
||||
state.has_popped_in = true;
|
||||
state.should_notify_at = Some((true, *now + self.delay));
|
||||
state.last_size = Some(size);
|
||||
}
|
||||
|
||||
match &state.should_notify_at {
|
||||
Some((has_popped_in, at)) if at <= now => {
|
||||
if *has_popped_in {
|
||||
if let Some(on_show) = &self.on_show {
|
||||
shell.publish(on_show(layout.bounds().size()));
|
||||
}
|
||||
} else if let Some(on_hide) = &self.on_hide {
|
||||
shell.publish(on_hide.clone());
|
||||
}
|
||||
|
||||
state.should_notify_at = None;
|
||||
}
|
||||
Some((_, at)) => {
|
||||
shell.request_redraw_at(*at);
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
||||
self.content.as_widget_mut().update(
|
||||
&mut tree.children[0],
|
||||
event,
|
||||
layout,
|
||||
cursor,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
viewport,
|
||||
);
|
||||
}
|
||||
|
||||
fn size(&self) -> Size<Length> {
|
||||
self.content.as_widget().size()
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> Size<Length> {
|
||||
self.content.as_widget().size_hint()
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
tree: &mut Tree,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
self.content
|
||||
.as_widget()
|
||||
.layout(&mut tree.children[0], renderer, limits)
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Theme,
|
||||
style: &renderer::Style,
|
||||
layout: layout::Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
self.content.as_widget().draw(
|
||||
&tree.children[0],
|
||||
renderer,
|
||||
theme,
|
||||
style,
|
||||
layout,
|
||||
cursor,
|
||||
viewport,
|
||||
);
|
||||
}
|
||||
|
||||
fn operate(
|
||||
&self,
|
||||
tree: &mut Tree,
|
||||
layout: core::Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn widget::Operation,
|
||||
) {
|
||||
self.content.as_widget().operate(
|
||||
&mut tree.children[0],
|
||||
layout,
|
||||
renderer,
|
||||
operation,
|
||||
);
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
layout: core::Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
self.content.as_widget().mouse_interaction(
|
||||
&tree.children[0],
|
||||
layout,
|
||||
cursor,
|
||||
viewport,
|
||||
renderer,
|
||||
)
|
||||
}
|
||||
|
||||
fn overlay<'b>(
|
||||
&'b mut self,
|
||||
tree: &'b mut Tree,
|
||||
layout: core::Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
translation: core::Vector,
|
||||
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
|
||||
self.content.as_widget_mut().overlay(
|
||||
&mut tree.children[0],
|
||||
layout,
|
||||
renderer,
|
||||
translation,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> From<Pop<'a, Message, Theme, Renderer>>
|
||||
for Element<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Renderer: core::Renderer + 'a,
|
||||
Theme: 'a,
|
||||
Message: Clone + 'a,
|
||||
{
|
||||
fn from(pop: Pop<'a, Message, Theme, Renderer>) -> Self {
|
||||
Element::new(pop)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,31 @@
|
|||
//! Provide progress feedback to your users.
|
||||
//! Progress bars visualize the progression of an extended computer operation, such as a download, file transfer, or installation.
|
||||
//!
|
||||
//! # Example
|
||||
//! ```no_run
|
||||
//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
|
||||
//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||
//! #
|
||||
//! use iced::widget::progress_bar;
|
||||
//!
|
||||
//! struct State {
|
||||
//! progress: f32,
|
||||
//! }
|
||||
//!
|
||||
//! enum Message {
|
||||
//! // ...
|
||||
//! }
|
||||
//!
|
||||
//! fn view(state: &State) -> Element<'_, Message> {
|
||||
//! progress_bar(0.0..=100.0, state.progress).into()
|
||||
//! }
|
||||
//! ```
|
||||
use crate::core::border::{self, Border};
|
||||
use crate::core::layout;
|
||||
use crate::core::mouse;
|
||||
use crate::core::renderer;
|
||||
use crate::core::widget::Tree;
|
||||
use crate::core::{
|
||||
self, Background, Border, Element, Layout, Length, Rectangle, Size, Theme,
|
||||
self, Background, Color, Element, Layout, Length, Rectangle, Size, Theme,
|
||||
Widget,
|
||||
};
|
||||
|
||||
|
|
@ -14,14 +35,23 @@ use std::ops::RangeInclusive;
|
|||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// # type ProgressBar<'a> = iced_widget::ProgressBar<'a>;
|
||||
/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
|
||||
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||
/// #
|
||||
/// let value = 50.0;
|
||||
/// use iced::widget::progress_bar;
|
||||
///
|
||||
/// ProgressBar::new(0.0..=100.0, value);
|
||||
/// struct State {
|
||||
/// progress: f32,
|
||||
/// }
|
||||
///
|
||||
/// enum Message {
|
||||
/// // ...
|
||||
/// }
|
||||
///
|
||||
/// fn view(state: &State) -> Element<'_, Message> {
|
||||
/// progress_bar(0.0..=100.0, state.progress).into()
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// 
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct ProgressBar<'a, Theme = crate::Theme>
|
||||
where
|
||||
|
|
@ -29,8 +59,9 @@ where
|
|||
{
|
||||
range: RangeInclusive<f32>,
|
||||
value: f32,
|
||||
width: Length,
|
||||
height: Option<Length>,
|
||||
length: Length,
|
||||
girth: Length,
|
||||
is_vertical: bool,
|
||||
class: Theme::Class<'a>,
|
||||
}
|
||||
|
||||
|
|
@ -38,8 +69,8 @@ impl<'a, Theme> ProgressBar<'a, Theme>
|
|||
where
|
||||
Theme: Catalog,
|
||||
{
|
||||
/// The default height of a [`ProgressBar`].
|
||||
pub const DEFAULT_HEIGHT: f32 = 30.0;
|
||||
/// The default girth of a [`ProgressBar`].
|
||||
pub const DEFAULT_GIRTH: f32 = 30.0;
|
||||
|
||||
/// Creates a new [`ProgressBar`].
|
||||
///
|
||||
|
|
@ -50,21 +81,30 @@ where
|
|||
ProgressBar {
|
||||
value: value.clamp(*range.start(), *range.end()),
|
||||
range,
|
||||
width: Length::Fill,
|
||||
height: None,
|
||||
length: Length::Fill,
|
||||
girth: Length::from(Self::DEFAULT_GIRTH),
|
||||
is_vertical: false,
|
||||
class: Theme::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the width of the [`ProgressBar`].
|
||||
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||
self.width = width.into();
|
||||
pub fn length(mut self, length: impl Into<Length>) -> Self {
|
||||
self.length = length.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the height of the [`ProgressBar`].
|
||||
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
||||
self.height = Some(height.into());
|
||||
pub fn girth(mut self, girth: impl Into<Length>) -> Self {
|
||||
self.girth = girth.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Turns the [`ProgressBar`] into a vertical [`ProgressBar`].
|
||||
///
|
||||
/// By default, a [`ProgressBar`] is horizontal.
|
||||
pub fn vertical(mut self) -> Self {
|
||||
self.is_vertical = true;
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -85,18 +125,34 @@ where
|
|||
self.class = class.into();
|
||||
self
|
||||
}
|
||||
|
||||
fn width(&self) -> Length {
|
||||
if self.is_vertical {
|
||||
self.girth
|
||||
} else {
|
||||
self.length
|
||||
}
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
if self.is_vertical {
|
||||
self.length
|
||||
} else {
|
||||
self.girth
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for ProgressBar<'a, Theme>
|
||||
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for ProgressBar<'_, Theme>
|
||||
where
|
||||
Theme: Catalog,
|
||||
Renderer: core::Renderer,
|
||||
{
|
||||
fn size(&self) -> Size<Length> {
|
||||
Size {
|
||||
width: self.width,
|
||||
height: self.height.unwrap_or(Length::Fixed(Self::DEFAULT_HEIGHT)),
|
||||
width: self.width(),
|
||||
height: self.height(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -106,11 +162,7 @@ where
|
|||
_renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
layout::atomic(
|
||||
limits,
|
||||
self.width,
|
||||
self.height.unwrap_or(Length::Fixed(Self::DEFAULT_HEIGHT)),
|
||||
)
|
||||
layout::atomic(limits, self.width(), self.height())
|
||||
}
|
||||
|
||||
fn draw(
|
||||
|
|
@ -126,11 +178,16 @@ where
|
|||
let bounds = layout.bounds();
|
||||
let (range_start, range_end) = self.range.clone().into_inner();
|
||||
|
||||
let active_progress_width = if range_start >= range_end {
|
||||
let length = if self.is_vertical {
|
||||
bounds.height
|
||||
} else {
|
||||
bounds.width
|
||||
};
|
||||
|
||||
let active_progress_length = if range_start >= range_end {
|
||||
0.0
|
||||
} else {
|
||||
bounds.width * (self.value - range_start)
|
||||
/ (range_end - range_start)
|
||||
length * (self.value - range_start) / (range_end - range_start)
|
||||
};
|
||||
|
||||
let style = theme.style(&self.class);
|
||||
|
|
@ -144,14 +201,27 @@ where
|
|||
style.background,
|
||||
);
|
||||
|
||||
if active_progress_width > 0.0 {
|
||||
if active_progress_length > 0.0 {
|
||||
let bounds = if self.is_vertical {
|
||||
Rectangle {
|
||||
y: bounds.y + bounds.height - active_progress_length,
|
||||
height: active_progress_length,
|
||||
..bounds
|
||||
}
|
||||
} else {
|
||||
Rectangle {
|
||||
width: active_progress_length,
|
||||
..bounds
|
||||
}
|
||||
};
|
||||
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: Rectangle {
|
||||
width: active_progress_width,
|
||||
..bounds
|
||||
bounds,
|
||||
border: Border {
|
||||
color: Color::TRANSPARENT,
|
||||
..style.border
|
||||
},
|
||||
border: Border::rounded(style.border.radius),
|
||||
..renderer::Quad::default()
|
||||
},
|
||||
style.bar,
|
||||
|
|
@ -175,7 +245,7 @@ where
|
|||
}
|
||||
|
||||
/// The appearance of a progress bar.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Style {
|
||||
/// The [`Background`] of the progress bar.
|
||||
pub background: Background,
|
||||
|
|
@ -218,10 +288,7 @@ impl Catalog for Theme {
|
|||
pub fn primary(theme: &Theme) -> Style {
|
||||
let palette = theme.extended_palette();
|
||||
|
||||
styled(
|
||||
palette.background.strong.color,
|
||||
palette.primary.strong.color,
|
||||
)
|
||||
styled(palette.background.strong.color, palette.primary.base.color)
|
||||
}
|
||||
|
||||
/// The secondary style of a [`ProgressBar`].
|
||||
|
|
@ -241,6 +308,13 @@ pub fn success(theme: &Theme) -> Style {
|
|||
styled(palette.background.strong.color, palette.success.base.color)
|
||||
}
|
||||
|
||||
/// The warning style of a [`ProgressBar`].
|
||||
pub fn warning(theme: &Theme) -> Style {
|
||||
let palette = theme.extended_palette();
|
||||
|
||||
styled(palette.background.strong.color, palette.warning.base.color)
|
||||
}
|
||||
|
||||
/// The danger style of a [`ProgressBar`].
|
||||
pub fn danger(theme: &Theme) -> Style {
|
||||
let palette = theme.extended_palette();
|
||||
|
|
@ -255,6 +329,6 @@ fn styled(
|
|||
Style {
|
||||
background: background.into(),
|
||||
bar: bar.into(),
|
||||
border: Border::rounded(2),
|
||||
border: border::rounded(2),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,30 +1,72 @@
|
|||
//! Encode and display information in a QR code.
|
||||
//! QR codes display information in a type of two-dimensional matrix barcode.
|
||||
//!
|
||||
//! # Example
|
||||
//! ```no_run
|
||||
//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
|
||||
//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||
//! #
|
||||
//! use iced::widget::qr_code;
|
||||
//!
|
||||
//! struct State {
|
||||
//! data: qr_code::Data,
|
||||
//! }
|
||||
//!
|
||||
//! #[derive(Debug, Clone)]
|
||||
//! enum Message {
|
||||
//! // ...
|
||||
//! }
|
||||
//!
|
||||
//! fn view(state: &State) -> Element<'_, Message> {
|
||||
//! qr_code(&state.data).into()
|
||||
//! }
|
||||
//! ```
|
||||
use crate::Renderer;
|
||||
use crate::canvas;
|
||||
use crate::core::layout;
|
||||
use crate::core::mouse;
|
||||
use crate::core::renderer::{self, Renderer as _};
|
||||
use crate::core::widget::tree::{self, Tree};
|
||||
use crate::core::{
|
||||
Color, Element, Layout, Length, Point, Rectangle, Size, Theme, Vector,
|
||||
Widget,
|
||||
Color, Element, Layout, Length, Pixels, Point, Rectangle, Size, Theme,
|
||||
Vector, Widget,
|
||||
};
|
||||
use crate::Renderer;
|
||||
|
||||
use std::cell::RefCell;
|
||||
use thiserror::Error;
|
||||
|
||||
const DEFAULT_CELL_SIZE: u16 = 4;
|
||||
const DEFAULT_CELL_SIZE: f32 = 4.0;
|
||||
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.
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
|
||||
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||
/// #
|
||||
/// use iced::widget::qr_code;
|
||||
///
|
||||
/// struct State {
|
||||
/// data: qr_code::Data,
|
||||
/// }
|
||||
///
|
||||
/// #[derive(Debug, Clone)]
|
||||
/// enum Message {
|
||||
/// // ...
|
||||
/// }
|
||||
///
|
||||
/// fn view(state: &State) -> Element<'_, Message> {
|
||||
/// qr_code(&state.data).into()
|
||||
/// }
|
||||
/// ```
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct QRCode<'a, Theme = crate::Theme>
|
||||
where
|
||||
Theme: Catalog,
|
||||
{
|
||||
data: &'a Data,
|
||||
cell_size: u16,
|
||||
cell_size: f32,
|
||||
class: Theme::Class<'a>,
|
||||
}
|
||||
|
||||
|
|
@ -42,8 +84,16 @@ where
|
|||
}
|
||||
|
||||
/// 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;
|
||||
pub fn cell_size(mut self, cell_size: impl Into<Pixels>) -> Self {
|
||||
self.cell_size = cell_size.into().0;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the size of the entire [`QRCode`].
|
||||
pub fn total_size(mut self, total_size: impl Into<Pixels>) -> Self {
|
||||
self.cell_size =
|
||||
total_size.into().0 / (self.data.width + 2 * QUIET_ZONE) as f32;
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -66,7 +116,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme> Widget<Message, Theme, Renderer> for QRCode<'a, Theme>
|
||||
impl<Message, Theme> Widget<Message, Theme, Renderer> for QRCode<'_, Theme>
|
||||
where
|
||||
Theme: Catalog,
|
||||
{
|
||||
|
|
@ -91,8 +141,8 @@ where
|
|||
_renderer: &Renderer,
|
||||
_limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
let side_length = (self.data.width + 2 * QUIET_ZONE) as f32
|
||||
* f32::from(self.cell_size);
|
||||
let side_length =
|
||||
(self.data.width + 2 * QUIET_ZONE) as f32 * self.cell_size;
|
||||
|
||||
layout::Node::new(Size::new(side_length, side_length))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,63 @@
|
|||
//! Create choices using radio buttons.
|
||||
//! Radio buttons let users choose a single option from a bunch of options.
|
||||
//!
|
||||
//! # Example
|
||||
//! ```no_run
|
||||
//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
|
||||
//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||
//! #
|
||||
//! use iced::widget::{column, radio};
|
||||
//!
|
||||
//! struct State {
|
||||
//! selection: Option<Choice>,
|
||||
//! }
|
||||
//!
|
||||
//! #[derive(Debug, Clone, Copy)]
|
||||
//! enum Message {
|
||||
//! RadioSelected(Choice),
|
||||
//! }
|
||||
//!
|
||||
//! #[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
//! enum Choice {
|
||||
//! A,
|
||||
//! B,
|
||||
//! C,
|
||||
//! All,
|
||||
//! }
|
||||
//!
|
||||
//! fn view(state: &State) -> Element<'_, Message> {
|
||||
//! let a = radio(
|
||||
//! "A",
|
||||
//! Choice::A,
|
||||
//! state.selection,
|
||||
//! Message::RadioSelected,
|
||||
//! );
|
||||
//!
|
||||
//! let b = radio(
|
||||
//! "B",
|
||||
//! Choice::B,
|
||||
//! state.selection,
|
||||
//! Message::RadioSelected,
|
||||
//! );
|
||||
//!
|
||||
//! let c = radio(
|
||||
//! "C",
|
||||
//! Choice::C,
|
||||
//! state.selection,
|
||||
//! Message::RadioSelected,
|
||||
//! );
|
||||
//!
|
||||
//! let all = radio(
|
||||
//! "All of the above",
|
||||
//! Choice::All,
|
||||
//! state.selection,
|
||||
//! Message::RadioSelected
|
||||
//! );
|
||||
//!
|
||||
//! column![a, b, c, all].into()
|
||||
//! }
|
||||
//! ```
|
||||
use crate::core::alignment;
|
||||
use crate::core::event::{self, Event};
|
||||
use crate::core::border::{self, Border};
|
||||
use crate::core::layout;
|
||||
use crate::core::mouse;
|
||||
use crate::core::renderer;
|
||||
|
|
@ -8,8 +65,9 @@ use crate::core::text;
|
|||
use crate::core::touch;
|
||||
use crate::core::widget;
|
||||
use crate::core::widget::tree::{self, Tree};
|
||||
use crate::core::window;
|
||||
use crate::core::{
|
||||
Background, Border, Clipboard, Color, Element, Layout, Length, Pixels,
|
||||
Background, Clipboard, Color, Element, Event, Layout, Length, Pixels,
|
||||
Rectangle, Shell, Size, Theme, Widget,
|
||||
};
|
||||
|
||||
|
|
@ -17,54 +75,59 @@ use crate::core::{
|
|||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// # type Radio<'a, Message> =
|
||||
/// # iced_widget::Radio<'a, Message, iced_widget::Theme, iced_widget::renderer::Renderer>;
|
||||
/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
|
||||
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||
/// #
|
||||
/// # use iced_widget::column;
|
||||
/// use iced::widget::{column, radio};
|
||||
///
|
||||
/// struct State {
|
||||
/// selection: Option<Choice>,
|
||||
/// }
|
||||
///
|
||||
/// #[derive(Debug, Clone, Copy)]
|
||||
/// enum Message {
|
||||
/// RadioSelected(Choice),
|
||||
/// }
|
||||
///
|
||||
/// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
/// pub enum Choice {
|
||||
/// enum Choice {
|
||||
/// A,
|
||||
/// B,
|
||||
/// C,
|
||||
/// All,
|
||||
/// }
|
||||
///
|
||||
/// #[derive(Debug, Clone, Copy)]
|
||||
/// pub enum Message {
|
||||
/// RadioSelected(Choice),
|
||||
/// fn view(state: &State) -> Element<'_, Message> {
|
||||
/// let a = radio(
|
||||
/// "A",
|
||||
/// Choice::A,
|
||||
/// state.selection,
|
||||
/// Message::RadioSelected,
|
||||
/// );
|
||||
///
|
||||
/// let b = radio(
|
||||
/// "B",
|
||||
/// Choice::B,
|
||||
/// state.selection,
|
||||
/// Message::RadioSelected,
|
||||
/// );
|
||||
///
|
||||
/// let c = radio(
|
||||
/// "C",
|
||||
/// Choice::C,
|
||||
/// state.selection,
|
||||
/// Message::RadioSelected,
|
||||
/// );
|
||||
///
|
||||
/// let all = radio(
|
||||
/// "All of the above",
|
||||
/// Choice::All,
|
||||
/// state.selection,
|
||||
/// Message::RadioSelected
|
||||
/// );
|
||||
///
|
||||
/// column![a, b, c, all].into()
|
||||
/// }
|
||||
///
|
||||
/// let selected_choice = Some(Choice::A);
|
||||
///
|
||||
/// let a = Radio::new(
|
||||
/// "A",
|
||||
/// Choice::A,
|
||||
/// selected_choice,
|
||||
/// Message::RadioSelected,
|
||||
/// );
|
||||
///
|
||||
/// let b = Radio::new(
|
||||
/// "B",
|
||||
/// Choice::B,
|
||||
/// selected_choice,
|
||||
/// Message::RadioSelected,
|
||||
/// );
|
||||
///
|
||||
/// let c = Radio::new(
|
||||
/// "C",
|
||||
/// Choice::C,
|
||||
/// selected_choice,
|
||||
/// Message::RadioSelected,
|
||||
/// );
|
||||
///
|
||||
/// let all = Radio::new(
|
||||
/// "All of the above",
|
||||
/// Choice::All,
|
||||
/// selected_choice,
|
||||
/// Message::RadioSelected
|
||||
/// );
|
||||
///
|
||||
/// let content = column![a, b, c, all];
|
||||
/// ```
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Radio<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
|
||||
|
|
@ -81,8 +144,10 @@ where
|
|||
text_size: Option<Pixels>,
|
||||
text_line_height: text::LineHeight,
|
||||
text_shaping: text::Shaping,
|
||||
text_wrapping: text::Wrapping,
|
||||
font: Option<Renderer::Font>,
|
||||
class: Theme::Class<'a>,
|
||||
last_status: Option<Status>,
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> Radio<'a, Message, Theme, Renderer>
|
||||
|
|
@ -104,7 +169,7 @@ where
|
|||
/// * 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`.
|
||||
/// receives the value of the radio and must produce a `Message`.
|
||||
pub fn new<F, V>(
|
||||
label: impl Into<String>,
|
||||
value: V,
|
||||
|
|
@ -121,12 +186,14 @@ where
|
|||
label: label.into(),
|
||||
width: Length::Shrink,
|
||||
size: Self::DEFAULT_SIZE,
|
||||
spacing: Self::DEFAULT_SPACING, //15
|
||||
spacing: Self::DEFAULT_SPACING,
|
||||
text_size: None,
|
||||
text_line_height: text::LineHeight::default(),
|
||||
text_shaping: text::Shaping::Basic,
|
||||
text_shaping: text::Shaping::default(),
|
||||
text_wrapping: text::Wrapping::default(),
|
||||
font: None,
|
||||
class: Theme::default(),
|
||||
last_status: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -169,6 +236,12 @@ where
|
|||
self
|
||||
}
|
||||
|
||||
/// Sets the [`text::Wrapping`] strategy of the [`Radio`] button.
|
||||
pub fn text_wrapping(mut self, wrapping: text::Wrapping) -> Self {
|
||||
self.text_wrapping = wrapping;
|
||||
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());
|
||||
|
|
@ -194,8 +267,8 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Radio<'a, Message, Theme, Renderer>
|
||||
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Radio<'_, Message, Theme, Renderer>
|
||||
where
|
||||
Message: Clone,
|
||||
Theme: Catalog,
|
||||
|
|
@ -244,35 +317,53 @@ where
|
|||
alignment::Horizontal::Left,
|
||||
alignment::Vertical::Top,
|
||||
self.text_shaping,
|
||||
self.text_wrapping,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
fn update(
|
||||
&mut self,
|
||||
_state: &mut Tree,
|
||||
event: Event,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
_renderer: &Renderer,
|
||||
_clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
_viewport: &Rectangle,
|
||||
) -> event::Status {
|
||||
) {
|
||||
match event {
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
|
||||
| Event::Touch(touch::Event::FingerPressed { .. }) => {
|
||||
if cursor.is_over(layout.bounds()) {
|
||||
shell.publish(self.on_click.clone());
|
||||
|
||||
return event::Status::Captured;
|
||||
shell.capture_event();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
event::Status::Ignored
|
||||
let current_status = {
|
||||
let is_mouse_over = cursor.is_over(layout.bounds());
|
||||
let is_selected = self.is_selected;
|
||||
|
||||
if is_mouse_over {
|
||||
Status::Hovered { is_selected }
|
||||
} else {
|
||||
Status::Active { is_selected }
|
||||
}
|
||||
};
|
||||
|
||||
if let Event::Window(window::Event::RedrawRequested(_now)) = event {
|
||||
self.last_status = Some(current_status);
|
||||
} else if self
|
||||
.last_status
|
||||
.is_some_and(|last_status| last_status != current_status)
|
||||
{
|
||||
shell.request_redraw();
|
||||
}
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
|
|
@ -297,21 +388,17 @@ where
|
|||
theme: &Theme,
|
||||
defaults: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
_cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
let is_mouse_over = cursor.is_over(layout.bounds());
|
||||
let is_selected = self.is_selected;
|
||||
|
||||
let mut children = layout.children();
|
||||
|
||||
let status = if is_mouse_over {
|
||||
Status::Hovered { is_selected }
|
||||
} else {
|
||||
Status::Active { is_selected }
|
||||
};
|
||||
|
||||
let style = theme.style(&self.class, status);
|
||||
let style = theme.style(
|
||||
&self.class,
|
||||
self.last_status.unwrap_or(Status::Active {
|
||||
is_selected: self.is_selected,
|
||||
}),
|
||||
);
|
||||
|
||||
{
|
||||
let layout = children.next().unwrap();
|
||||
|
|
@ -342,7 +429,7 @@ where
|
|||
width: bounds.width - dot_size,
|
||||
height: bounds.height - dot_size,
|
||||
},
|
||||
border: Border::rounded(dot_size / 2.0),
|
||||
border: border::rounded(dot_size / 2.0),
|
||||
..renderer::Quad::default()
|
||||
},
|
||||
style.dot_color,
|
||||
|
|
@ -352,12 +439,14 @@ where
|
|||
|
||||
{
|
||||
let label_layout = children.next().unwrap();
|
||||
let state: &widget::text::State<Renderer::Paragraph> =
|
||||
tree.state.downcast_ref();
|
||||
|
||||
crate::text::draw(
|
||||
renderer,
|
||||
defaults,
|
||||
label_layout,
|
||||
tree.state.downcast_ref(),
|
||||
state.0.raw(),
|
||||
crate::text::Style {
|
||||
color: style.text_color,
|
||||
},
|
||||
|
|
@ -397,7 +486,7 @@ pub enum Status {
|
|||
}
|
||||
|
||||
/// The appearance of a radio button.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Style {
|
||||
/// The [`Background`] of the radio button.
|
||||
pub background: Background,
|
||||
|
|
|
|||
|
|
@ -1,23 +1,44 @@
|
|||
//! Distribute content horizontally.
|
||||
use crate::core::event::{self, Event};
|
||||
use crate::core::alignment::{self, Alignment};
|
||||
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, Rectangle, Shell,
|
||||
Size, Vector, Widget,
|
||||
Clipboard, Element, Event, Length, Padding, Pixels, Rectangle, Shell, Size,
|
||||
Vector, Widget,
|
||||
};
|
||||
|
||||
/// A container that distributes its contents horizontally.
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// # mod iced { pub mod widget { pub use iced_widget::*; } }
|
||||
/// # pub type State = ();
|
||||
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||
/// use iced::widget::{button, row};
|
||||
///
|
||||
/// #[derive(Debug, Clone)]
|
||||
/// enum Message {
|
||||
/// // ...
|
||||
/// }
|
||||
///
|
||||
/// fn view(state: &State) -> Element<'_, Message> {
|
||||
/// row![
|
||||
/// "I am to the left!",
|
||||
/// button("I am in the middle!"),
|
||||
/// "I am to the right!",
|
||||
/// ].into()
|
||||
/// }
|
||||
/// ```
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Row<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> {
|
||||
spacing: f32,
|
||||
padding: Padding,
|
||||
width: Length,
|
||||
height: Length,
|
||||
align_items: Alignment,
|
||||
align: Alignment,
|
||||
clip: bool,
|
||||
children: Vec<Element<'a, Message, Theme, Renderer>>,
|
||||
}
|
||||
|
|
@ -60,7 +81,7 @@ where
|
|||
padding: Padding::ZERO,
|
||||
width: Length::Shrink,
|
||||
height: Length::Shrink,
|
||||
align_items: Alignment::Start,
|
||||
align: Alignment::Start,
|
||||
clip: false,
|
||||
children,
|
||||
}
|
||||
|
|
@ -95,8 +116,8 @@ where
|
|||
}
|
||||
|
||||
/// Sets the vertical alignment of the contents of the [`Row`] .
|
||||
pub fn align_items(mut self, align: Alignment) -> Self {
|
||||
self.align_items = align;
|
||||
pub fn align_y(mut self, align: impl Into<alignment::Vertical>) -> Self {
|
||||
self.align = Alignment::from(align.into());
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -141,9 +162,16 @@ where
|
|||
) -> Self {
|
||||
children.into_iter().fold(self, Self::push)
|
||||
}
|
||||
|
||||
/// Turns the [`Row`] into a [`Wrapping`] row.
|
||||
///
|
||||
/// The original alignment of the [`Row`] is preserved per row wrapped.
|
||||
pub fn wrap(self) -> Wrapping<'a, Message, Theme, Renderer> {
|
||||
Wrapping { row: self }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Default for Row<'a, Message, Renderer>
|
||||
impl<Message, Renderer> Default for Row<'_, Message, Renderer>
|
||||
where
|
||||
Renderer: crate::core::Renderer,
|
||||
{
|
||||
|
|
@ -152,8 +180,21 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
impl<'a, Message, Theme, Renderer: crate::core::Renderer>
|
||||
FromIterator<Element<'a, Message, Theme, Renderer>>
|
||||
for Row<'a, Message, Theme, Renderer>
|
||||
{
|
||||
fn from_iter<
|
||||
T: IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
|
||||
>(
|
||||
iter: T,
|
||||
) -> Self {
|
||||
Self::with_children(iter)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Row<'_, Message, Theme, Renderer>
|
||||
where
|
||||
Renderer: crate::core::Renderer,
|
||||
{
|
||||
|
|
@ -186,7 +227,7 @@ where
|
|||
self.height,
|
||||
self.padding,
|
||||
self.spacing,
|
||||
self.align_items,
|
||||
self.align,
|
||||
&self.children,
|
||||
&mut tree.children,
|
||||
)
|
||||
|
|
@ -197,7 +238,7 @@ where
|
|||
tree: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn Operation<Message>,
|
||||
operation: &mut dyn Operation,
|
||||
) {
|
||||
operation.container(None, layout.bounds(), &mut |operation| {
|
||||
self.children
|
||||
|
|
@ -212,34 +253,28 @@ where
|
|||
});
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
fn update(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: Event,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
viewport: &Rectangle,
|
||||
) -> event::Status {
|
||||
self.children
|
||||
) {
|
||||
for ((child, state), layout) in 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,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
viewport,
|
||||
)
|
||||
})
|
||||
.fold(event::Status::Ignored, event::Status::merge)
|
||||
{
|
||||
child.as_widget_mut().update(
|
||||
state, event, layout, cursor, renderer, clipboard, shell,
|
||||
viewport,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
|
|
@ -274,24 +309,21 @@ where
|
|||
viewport: &Rectangle,
|
||||
) {
|
||||
if let Some(clipped_viewport) = layout.bounds().intersection(viewport) {
|
||||
let viewport = if self.clip {
|
||||
&clipped_viewport
|
||||
} else {
|
||||
viewport
|
||||
};
|
||||
|
||||
for ((child, state), layout) in self
|
||||
.children
|
||||
.iter()
|
||||
.zip(&tree.children)
|
||||
.zip(layout.children())
|
||||
.filter(|(_, layout)| layout.bounds().intersects(viewport))
|
||||
{
|
||||
child.as_widget().draw(
|
||||
state,
|
||||
renderer,
|
||||
theme,
|
||||
style,
|
||||
layout,
|
||||
cursor,
|
||||
if self.clip {
|
||||
&clipped_viewport
|
||||
} else {
|
||||
viewport
|
||||
},
|
||||
state, renderer, theme, style, layout, cursor, viewport,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -325,3 +357,196 @@ where
|
|||
Self::new(row)
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`Row`] that wraps its contents.
|
||||
///
|
||||
/// Create a [`Row`] first, and then call [`Row::wrap`] to
|
||||
/// obtain a [`Row`] that wraps its contents.
|
||||
///
|
||||
/// The original alignment of the [`Row`] is preserved per row wrapped.
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Wrapping<
|
||||
'a,
|
||||
Message,
|
||||
Theme = crate::Theme,
|
||||
Renderer = crate::Renderer,
|
||||
> {
|
||||
row: Row<'a, Message, Theme, Renderer>,
|
||||
}
|
||||
|
||||
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Wrapping<'_, Message, Theme, Renderer>
|
||||
where
|
||||
Renderer: crate::core::Renderer,
|
||||
{
|
||||
fn children(&self) -> Vec<Tree> {
|
||||
self.row.children()
|
||||
}
|
||||
|
||||
fn diff(&self, tree: &mut Tree) {
|
||||
self.row.diff(tree);
|
||||
}
|
||||
|
||||
fn size(&self) -> Size<Length> {
|
||||
self.row.size()
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
tree: &mut Tree,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
let limits = limits
|
||||
.width(self.row.width)
|
||||
.height(self.row.height)
|
||||
.shrink(self.row.padding);
|
||||
|
||||
let spacing = self.row.spacing;
|
||||
let max_width = limits.max().width;
|
||||
|
||||
let mut children: Vec<layout::Node> = Vec::new();
|
||||
let mut intrinsic_size = Size::ZERO;
|
||||
let mut row_start = 0;
|
||||
let mut row_height = 0.0;
|
||||
let mut x = 0.0;
|
||||
let mut y = 0.0;
|
||||
|
||||
let align_factor = match self.row.align {
|
||||
Alignment::Start => 0.0,
|
||||
Alignment::Center => 2.0,
|
||||
Alignment::End => 1.0,
|
||||
};
|
||||
|
||||
let align = |row_start: std::ops::Range<usize>,
|
||||
row_height: f32,
|
||||
children: &mut Vec<layout::Node>| {
|
||||
if align_factor != 0.0 {
|
||||
for node in &mut children[row_start] {
|
||||
let height = node.size().height;
|
||||
|
||||
node.translate_mut(Vector::new(
|
||||
0.0,
|
||||
(row_height - height) / align_factor,
|
||||
));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (i, child) in self.row.children.iter().enumerate() {
|
||||
let node = child.as_widget().layout(
|
||||
&mut tree.children[i],
|
||||
renderer,
|
||||
&limits,
|
||||
);
|
||||
|
||||
let child_size = node.size();
|
||||
|
||||
if x != 0.0 && x + child_size.width > max_width {
|
||||
intrinsic_size.width = intrinsic_size.width.max(x - spacing);
|
||||
|
||||
align(row_start..i, row_height, &mut children);
|
||||
|
||||
y += row_height + spacing;
|
||||
x = 0.0;
|
||||
row_start = i;
|
||||
row_height = 0.0;
|
||||
}
|
||||
|
||||
row_height = row_height.max(child_size.height);
|
||||
|
||||
children.push(node.move_to((
|
||||
x + self.row.padding.left,
|
||||
y + self.row.padding.top,
|
||||
)));
|
||||
|
||||
x += child_size.width + spacing;
|
||||
}
|
||||
|
||||
if x != 0.0 {
|
||||
intrinsic_size.width = intrinsic_size.width.max(x - spacing);
|
||||
}
|
||||
|
||||
intrinsic_size.height = y + row_height;
|
||||
align(row_start..children.len(), row_height, &mut children);
|
||||
|
||||
let size =
|
||||
limits.resolve(self.row.width, self.row.height, intrinsic_size);
|
||||
|
||||
layout::Node::with_children(size.expand(self.row.padding), children)
|
||||
}
|
||||
|
||||
fn operate(
|
||||
&self,
|
||||
tree: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn Operation,
|
||||
) {
|
||||
self.row.operate(tree, layout, renderer, operation);
|
||||
}
|
||||
|
||||
fn update(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
self.row.update(
|
||||
tree, event, layout, cursor, renderer, clipboard, shell, viewport,
|
||||
);
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
self.row
|
||||
.mouse_interaction(tree, layout, cursor, viewport, renderer)
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
self.row
|
||||
.draw(tree, renderer, theme, style, layout, cursor, viewport);
|
||||
}
|
||||
|
||||
fn overlay<'b>(
|
||||
&'b mut self,
|
||||
tree: &'b mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
translation: Vector,
|
||||
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
|
||||
self.row.overlay(tree, layout, renderer, translation)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> From<Wrapping<'a, Message, Theme, Renderer>>
|
||||
for Element<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Theme: 'a,
|
||||
Renderer: crate::core::Renderer + 'a,
|
||||
{
|
||||
fn from(row: Wrapping<'a, Message, Theme, Renderer>) -> Self {
|
||||
Self::new(row)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,23 @@
|
|||
//! Display a horizontal or vertical rule for dividing content.
|
||||
//! Rules divide space horizontally or vertically.
|
||||
//!
|
||||
//! # Example
|
||||
//! ```no_run
|
||||
//! # mod iced { pub mod widget { pub use iced_widget::*; } }
|
||||
//! # pub type State = ();
|
||||
//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||
//! use iced::widget::horizontal_rule;
|
||||
//!
|
||||
//! #[derive(Clone)]
|
||||
//! enum Message {
|
||||
//! // ...,
|
||||
//! }
|
||||
//!
|
||||
//! fn view(state: &State) -> Element<'_, Message> {
|
||||
//! horizontal_rule(2).into()
|
||||
//! }
|
||||
//! ```
|
||||
use crate::core;
|
||||
use crate::core::border::{self, Border};
|
||||
use crate::core::border;
|
||||
use crate::core::layout;
|
||||
use crate::core::mouse;
|
||||
use crate::core::renderer;
|
||||
|
|
@ -10,6 +27,23 @@ use crate::core::{
|
|||
};
|
||||
|
||||
/// Display a horizontal or vertical rule for dividing content.
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// # mod iced { pub mod widget { pub use iced_widget::*; } }
|
||||
/// # pub type State = ();
|
||||
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||
/// use iced::widget::horizontal_rule;
|
||||
///
|
||||
/// #[derive(Clone)]
|
||||
/// enum Message {
|
||||
/// // ...,
|
||||
/// }
|
||||
///
|
||||
/// fn view(state: &State) -> Element<'_, Message> {
|
||||
/// horizontal_rule(2).into()
|
||||
/// }
|
||||
/// ```
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Rule<'a, Theme = crate::Theme>
|
||||
where
|
||||
|
|
@ -64,8 +98,8 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Rule<'a, Theme>
|
||||
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Rule<'_, Theme>
|
||||
where
|
||||
Renderer: core::Renderer,
|
||||
Theme: Catalog,
|
||||
|
|
@ -132,7 +166,7 @@ where
|
|||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds,
|
||||
border: Border::rounded(style.radius),
|
||||
border: border::rounded(style.radius),
|
||||
..renderer::Quad::default()
|
||||
},
|
||||
style.color,
|
||||
|
|
@ -153,7 +187,7 @@ where
|
|||
}
|
||||
|
||||
/// The appearance of a rule.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Style {
|
||||
/// The color of the rule.
|
||||
pub color: Color,
|
||||
|
|
@ -166,7 +200,7 @@ pub struct Style {
|
|||
}
|
||||
|
||||
/// The fill mode of a rule.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum FillMode {
|
||||
/// Fill the whole length of the container.
|
||||
Full,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,24 +1,21 @@
|
|||
//! A custom shader widget for wgpu applications.
|
||||
mod event;
|
||||
mod program;
|
||||
|
||||
pub use event::Event;
|
||||
pub use program::Program;
|
||||
|
||||
use crate::core;
|
||||
use crate::core::event;
|
||||
use crate::core::layout::{self, Layout};
|
||||
use crate::core::mouse;
|
||||
use crate::core::renderer;
|
||||
use crate::core::widget::tree::{self, Tree};
|
||||
use crate::core::widget::{self, Widget};
|
||||
use crate::core::window;
|
||||
use crate::core::{Clipboard, Element, Length, Rectangle, Shell, Size};
|
||||
use crate::core::{Clipboard, Element, Event, Length, Rectangle, Shell, Size};
|
||||
use crate::renderer::wgpu::primitive;
|
||||
|
||||
use std::marker::PhantomData;
|
||||
|
||||
pub use crate::Action;
|
||||
pub use crate::graphics::Viewport;
|
||||
pub use crate::renderer::wgpu::wgpu;
|
||||
pub use primitive::{Primitive, Storage};
|
||||
|
||||
/// A widget which can render custom shaders with Iced's `wgpu` backend.
|
||||
|
|
@ -88,50 +85,35 @@ where
|
|||
layout::atomic(limits, self.width, self.height)
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
fn update(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: crate::core::Event,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
_renderer: &Renderer,
|
||||
_clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
_viewport: &Rectangle,
|
||||
) -> event::Status {
|
||||
) {
|
||||
let bounds = layout.bounds();
|
||||
|
||||
let custom_shader_event = match event {
|
||||
core::Event::Mouse(mouse_event) => Some(Event::Mouse(mouse_event)),
|
||||
core::Event::Keyboard(keyboard_event) => {
|
||||
Some(Event::Keyboard(keyboard_event))
|
||||
}
|
||||
core::Event::Touch(touch_event) => Some(Event::Touch(touch_event)),
|
||||
core::Event::Window(_, window::Event::RedrawRequested(instant)) => {
|
||||
Some(Event::RedrawRequested(instant))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let state = tree.state.downcast_mut::<P::State>();
|
||||
|
||||
if let Some(custom_shader_event) = custom_shader_event {
|
||||
let state = tree.state.downcast_mut::<P::State>();
|
||||
if let Some(action) = self.program.update(state, event, bounds, cursor)
|
||||
{
|
||||
let (message, redraw_request, event_status) = action.into_inner();
|
||||
|
||||
let (event_status, message) = self.program.update(
|
||||
state,
|
||||
custom_shader_event,
|
||||
bounds,
|
||||
cursor,
|
||||
shell,
|
||||
);
|
||||
shell.request_redraw_at(redraw_request);
|
||||
|
||||
if let Some(message) = message {
|
||||
shell.publish(message);
|
||||
}
|
||||
|
||||
return event_status;
|
||||
if event_status == event::Status::Captured {
|
||||
shell.capture_event();
|
||||
}
|
||||
}
|
||||
|
||||
event::Status::Ignored
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
|
|
@ -192,12 +174,11 @@ where
|
|||
fn update(
|
||||
&self,
|
||||
state: &mut Self::State,
|
||||
event: Event,
|
||||
event: &Event,
|
||||
bounds: Rectangle,
|
||||
cursor: mouse::Cursor,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) -> (event::Status, Option<Message>) {
|
||||
T::update(self, state, event, bounds, cursor, shell)
|
||||
) -> Option<Action<Message>> {
|
||||
T::update(self, state, event, bounds, cursor)
|
||||
}
|
||||
|
||||
fn draw(
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
//! Handle events of a custom shader widget.
|
||||
use crate::core::keyboard;
|
||||
use crate::core::mouse;
|
||||
use crate::core::time::Instant;
|
||||
use crate::core::touch;
|
||||
|
||||
pub use crate::core::event::Status;
|
||||
|
||||
/// A [`Shader`] event.
|
||||
///
|
||||
/// [`Shader`]: crate::Shader
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Event {
|
||||
/// A mouse event.
|
||||
Mouse(mouse::Event),
|
||||
|
||||
/// A touch event.
|
||||
Touch(touch::Event),
|
||||
|
||||
/// A keyboard event.
|
||||
Keyboard(keyboard::Event),
|
||||
|
||||
/// A window requested a redraw.
|
||||
RedrawRequested(Instant),
|
||||
}
|
||||
|
|
@ -1,8 +1,7 @@
|
|||
use crate::core::event;
|
||||
use crate::core::Rectangle;
|
||||
use crate::core::mouse;
|
||||
use crate::core::{Rectangle, Shell};
|
||||
use crate::renderer::wgpu::Primitive;
|
||||
use crate::shader;
|
||||
use crate::shader::{self, Action};
|
||||
|
||||
/// The state and logic of a [`Shader`] widget.
|
||||
///
|
||||
|
|
@ -18,21 +17,20 @@ pub trait Program<Message> {
|
|||
type Primitive: Primitive + 'static;
|
||||
|
||||
/// Update the internal [`State`] of the [`Program`]. This can be used to reflect state changes
|
||||
/// based on mouse & other events. You can use the [`Shell`] to publish messages, request a
|
||||
/// redraw for the window, etc.
|
||||
/// based on mouse & other events. You can return an [`Action`] to publish a message, request a
|
||||
/// redraw, or capture the event.
|
||||
///
|
||||
/// By default, this method does and returns nothing.
|
||||
/// By default, this method returns `None`.
|
||||
///
|
||||
/// [`State`]: Self::State
|
||||
fn update(
|
||||
&self,
|
||||
_state: &mut Self::State,
|
||||
_event: shader::Event,
|
||||
_event: &shader::Event,
|
||||
_bounds: Rectangle,
|
||||
_cursor: mouse::Cursor,
|
||||
_shell: &mut Shell<'_, Message>,
|
||||
) -> (event::Status, Option<Message>) {
|
||||
(event::Status::Ignored, None)
|
||||
) -> Option<Action<Message>> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Draws the [`Primitive`].
|
||||
|
|
|
|||
|
|
@ -1,6 +1,34 @@
|
|||
//! Display an interactive selector of a single value from a range of values.
|
||||
use crate::core::border;
|
||||
use crate::core::event::{self, Event};
|
||||
//! Sliders let users set a value by moving an indicator.
|
||||
//!
|
||||
//! # Example
|
||||
//! ```no_run
|
||||
//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
|
||||
//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||
//! #
|
||||
//! use iced::widget::slider;
|
||||
//!
|
||||
//! struct State {
|
||||
//! value: f32,
|
||||
//! }
|
||||
//!
|
||||
//! #[derive(Debug, Clone)]
|
||||
//! enum Message {
|
||||
//! ValueChanged(f32),
|
||||
//! }
|
||||
//!
|
||||
//! fn view(state: &State) -> Element<'_, Message> {
|
||||
//! slider(0.0..=100.0, state.value, Message::ValueChanged).into()
|
||||
//! }
|
||||
//!
|
||||
//! fn update(state: &mut State, message: Message) {
|
||||
//! match message {
|
||||
//! Message::ValueChanged(value) => {
|
||||
//! state.value = value;
|
||||
//! }
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
use crate::core::border::{self, Border};
|
||||
use crate::core::keyboard;
|
||||
use crate::core::keyboard::key::{self, Key};
|
||||
use crate::core::layout;
|
||||
|
|
@ -8,9 +36,10 @@ use crate::core::mouse;
|
|||
use crate::core::renderer;
|
||||
use crate::core::touch;
|
||||
use crate::core::widget::tree::{self, Tree};
|
||||
use crate::core::window;
|
||||
use crate::core::{
|
||||
self, Border, Clipboard, Color, Element, Layout, Length, Pixels, Point,
|
||||
Rectangle, Shell, Size, Theme, Widget,
|
||||
self, Background, Clipboard, Color, Element, Event, Layout, Length, Pixels,
|
||||
Point, Rectangle, Shell, Size, Theme, Widget,
|
||||
};
|
||||
|
||||
use std::ops::RangeInclusive;
|
||||
|
|
@ -25,19 +54,32 @@ use std::ops::RangeInclusive;
|
|||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// # type Slider<'a, T, Message> = iced_widget::Slider<'a, Message, T>;
|
||||
/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
|
||||
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||
/// #
|
||||
/// #[derive(Clone)]
|
||||
/// pub enum Message {
|
||||
/// SliderChanged(f32),
|
||||
/// use iced::widget::slider;
|
||||
///
|
||||
/// struct State {
|
||||
/// value: f32,
|
||||
/// }
|
||||
///
|
||||
/// let value = 50.0;
|
||||
/// #[derive(Debug, Clone)]
|
||||
/// enum Message {
|
||||
/// ValueChanged(f32),
|
||||
/// }
|
||||
///
|
||||
/// Slider::new(0.0..=100.0, value, Message::SliderChanged);
|
||||
/// fn view(state: &State) -> Element<'_, Message> {
|
||||
/// slider(0.0..=100.0, state.value, Message::ValueChanged).into()
|
||||
/// }
|
||||
///
|
||||
/// fn update(state: &mut State, message: Message) {
|
||||
/// match message {
|
||||
/// Message::ValueChanged(value) => {
|
||||
/// state.value = value;
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// 
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Slider<'a, T, Message, Theme = crate::Theme>
|
||||
where
|
||||
|
|
@ -53,6 +95,7 @@ where
|
|||
width: Length,
|
||||
height: f32,
|
||||
class: Theme::Class<'a>,
|
||||
status: Option<Status>,
|
||||
}
|
||||
|
||||
impl<'a, T, Message, Theme> Slider<'a, T, Message, Theme>
|
||||
|
|
@ -70,8 +113,8 @@ where
|
|||
/// * 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`.
|
||||
/// 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,
|
||||
|
|
@ -99,6 +142,7 @@ where
|
|||
width: Length::Fill,
|
||||
height: Self::DEFAULT_HEIGHT,
|
||||
class: Theme::default(),
|
||||
status: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -166,8 +210,8 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Slider<'a, T, Message, Theme>
|
||||
impl<T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Slider<'_, T, Message, Theme>
|
||||
where
|
||||
T: Copy + Into<f64> + num_traits::FromPrimitive,
|
||||
Message: Clone,
|
||||
|
|
@ -198,29 +242,53 @@ where
|
|||
layout::atomic(limits, self.width, self.height)
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
fn update(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: Event,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
_renderer: &Renderer,
|
||||
_clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
_viewport: &Rectangle,
|
||||
) -> event::Status {
|
||||
) {
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
|
||||
let is_dragging = state.is_dragging;
|
||||
let current_value = self.value;
|
||||
let mut update = || {
|
||||
let current_value = self.value;
|
||||
|
||||
let locate = |cursor_position: Point| -> Option<T> {
|
||||
let bounds = layout.bounds();
|
||||
let new_value = if cursor_position.x <= bounds.x {
|
||||
Some(*self.range.start())
|
||||
} else if cursor_position.x >= bounds.x + bounds.width {
|
||||
Some(*self.range.end())
|
||||
} else {
|
||||
let locate = |cursor_position: Point| -> Option<T> {
|
||||
let bounds = layout.bounds();
|
||||
|
||||
let new_value = if cursor_position.x <= bounds.x {
|
||||
Some(*self.range.start())
|
||||
} else if cursor_position.x >= bounds.x + bounds.width {
|
||||
Some(*self.range.end())
|
||||
} else {
|
||||
let step = if state.keyboard_modifiers.shift() {
|
||||
self.shift_step.unwrap_or(self.step)
|
||||
} else {
|
||||
self.step
|
||||
}
|
||||
.into();
|
||||
|
||||
let start = (*self.range.start()).into();
|
||||
let end = (*self.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;
|
||||
|
||||
T::from_f64(value.min(end))
|
||||
};
|
||||
|
||||
new_value
|
||||
};
|
||||
|
||||
let increment = |value: T| -> Option<T> {
|
||||
let step = if state.keyboard_modifiers.shift() {
|
||||
self.shift_step.unwrap_or(self.step)
|
||||
} else {
|
||||
|
|
@ -228,150 +296,158 @@ where
|
|||
}
|
||||
.into();
|
||||
|
||||
let start = (*self.range.start()).into();
|
||||
let end = (*self.range.end()).into();
|
||||
let steps = (value.into() / step).round();
|
||||
let new_value = step * (steps + 1.0);
|
||||
|
||||
let percent = f64::from(cursor_position.x - bounds.x)
|
||||
/ f64::from(bounds.width);
|
||||
if new_value > (*self.range.end()).into() {
|
||||
return Some(*self.range.end());
|
||||
}
|
||||
|
||||
let steps = (percent * (end - start) / step).round();
|
||||
let value = steps * step + start;
|
||||
|
||||
T::from_f64(value)
|
||||
T::from_f64(new_value)
|
||||
};
|
||||
|
||||
new_value
|
||||
};
|
||||
let decrement = |value: T| -> Option<T> {
|
||||
let step = if state.keyboard_modifiers.shift() {
|
||||
self.shift_step.unwrap_or(self.step)
|
||||
} else {
|
||||
self.step
|
||||
}
|
||||
.into();
|
||||
|
||||
let increment = |value: T| -> Option<T> {
|
||||
let step = if state.keyboard_modifiers.shift() {
|
||||
self.shift_step.unwrap_or(self.step)
|
||||
} else {
|
||||
self.step
|
||||
}
|
||||
.into();
|
||||
let steps = (value.into() / step).round();
|
||||
let new_value = step * (steps - 1.0);
|
||||
|
||||
let steps = (value.into() / step).round();
|
||||
let new_value = step * (steps + 1.0);
|
||||
if new_value < (*self.range.start()).into() {
|
||||
return Some(*self.range.start());
|
||||
}
|
||||
|
||||
if new_value > (*self.range.end()).into() {
|
||||
return Some(*self.range.end());
|
||||
}
|
||||
T::from_f64(new_value)
|
||||
};
|
||||
|
||||
T::from_f64(new_value)
|
||||
};
|
||||
let change = |new_value: T| {
|
||||
if (self.value.into() - new_value.into()).abs() > f64::EPSILON {
|
||||
shell.publish((self.on_change)(new_value));
|
||||
|
||||
let decrement = |value: T| -> Option<T> {
|
||||
let step = if state.keyboard_modifiers.shift() {
|
||||
self.shift_step.unwrap_or(self.step)
|
||||
} else {
|
||||
self.step
|
||||
}
|
||||
.into();
|
||||
self.value = new_value;
|
||||
}
|
||||
};
|
||||
|
||||
let steps = (value.into() / step).round();
|
||||
let new_value = step * (steps - 1.0);
|
||||
match &event {
|
||||
Event::Mouse(mouse::Event::ButtonPressed(
|
||||
mouse::Button::Left,
|
||||
))
|
||||
| Event::Touch(touch::Event::FingerPressed { .. }) => {
|
||||
if let Some(cursor_position) =
|
||||
cursor.position_over(layout.bounds())
|
||||
{
|
||||
if state.keyboard_modifiers.command() {
|
||||
let _ = self.default.map(change);
|
||||
state.is_dragging = false;
|
||||
} else {
|
||||
let _ = locate(cursor_position).map(change);
|
||||
state.is_dragging = true;
|
||||
}
|
||||
|
||||
if new_value < (*self.range.start()).into() {
|
||||
return Some(*self.range.start());
|
||||
}
|
||||
|
||||
T::from_f64(new_value)
|
||||
};
|
||||
|
||||
let change = |new_value: T| {
|
||||
if (self.value.into() - new_value.into()).abs() > f64::EPSILON {
|
||||
shell.publish((self.on_change)(new_value));
|
||||
|
||||
self.value = new_value;
|
||||
}
|
||||
};
|
||||
|
||||
match event {
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
|
||||
| Event::Touch(touch::Event::FingerPressed { .. }) => {
|
||||
if let Some(cursor_position) =
|
||||
cursor.position_over(layout.bounds())
|
||||
{
|
||||
if state.keyboard_modifiers.command() {
|
||||
let _ = self.default.map(change);
|
||||
shell.capture_event();
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse::Event::ButtonReleased(
|
||||
mouse::Button::Left,
|
||||
))
|
||||
| Event::Touch(touch::Event::FingerLifted { .. })
|
||||
| Event::Touch(touch::Event::FingerLost { .. }) => {
|
||||
if state.is_dragging {
|
||||
if let Some(on_release) = self.on_release.clone() {
|
||||
shell.publish(on_release);
|
||||
}
|
||||
state.is_dragging = false;
|
||||
} else {
|
||||
let _ = locate(cursor_position).map(change);
|
||||
state.is_dragging = true;
|
||||
|
||||
shell.capture_event();
|
||||
}
|
||||
|
||||
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) = self.on_release.clone() {
|
||||
shell.publish(on_release);
|
||||
Event::Mouse(mouse::Event::CursorMoved { .. })
|
||||
| Event::Touch(touch::Event::FingerMoved { .. }) => {
|
||||
if state.is_dragging {
|
||||
let _ = cursor.position().and_then(locate).map(change);
|
||||
|
||||
shell.capture_event();
|
||||
}
|
||||
state.is_dragging = false;
|
||||
|
||||
return event::Status::Captured;
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse::Event::CursorMoved { .. })
|
||||
| Event::Touch(touch::Event::FingerMoved { .. }) => {
|
||||
if is_dragging {
|
||||
let _ = cursor.position().and_then(locate).map(change);
|
||||
Event::Mouse(mouse::Event::WheelScrolled { delta })
|
||||
if state.keyboard_modifiers.control() =>
|
||||
{
|
||||
if cursor.is_over(layout.bounds()) {
|
||||
let delta = match delta {
|
||||
mouse::ScrollDelta::Lines { x: _, y } => y,
|
||||
mouse::ScrollDelta::Pixels { x: _, y } => y,
|
||||
};
|
||||
|
||||
return event::Status::Captured;
|
||||
}
|
||||
}
|
||||
Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => {
|
||||
if cursor.position_over(layout.bounds()).is_some() {
|
||||
match key {
|
||||
Key::Named(key::Named::ArrowUp) => {
|
||||
if *delta < 0.0 {
|
||||
let _ = decrement(current_value).map(change);
|
||||
} else {
|
||||
let _ = increment(current_value).map(change);
|
||||
}
|
||||
Key::Named(key::Named::ArrowDown) => {
|
||||
let _ = decrement(current_value).map(change);
|
||||
}
|
||||
_ => (),
|
||||
|
||||
shell.capture_event();
|
||||
}
|
||||
|
||||
return event::Status::Captured;
|
||||
}
|
||||
}
|
||||
Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
|
||||
state.keyboard_modifiers = modifiers;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Event::Keyboard(keyboard::Event::KeyPressed {
|
||||
key, ..
|
||||
}) => {
|
||||
if cursor.is_over(layout.bounds()) {
|
||||
match key {
|
||||
Key::Named(key::Named::ArrowUp) => {
|
||||
let _ = increment(current_value).map(change);
|
||||
}
|
||||
Key::Named(key::Named::ArrowDown) => {
|
||||
let _ = decrement(current_value).map(change);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
event::Status::Ignored
|
||||
shell.capture_event();
|
||||
}
|
||||
}
|
||||
Event::Keyboard(keyboard::Event::ModifiersChanged(
|
||||
modifiers,
|
||||
)) => {
|
||||
state.keyboard_modifiers = *modifiers;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
};
|
||||
|
||||
update();
|
||||
|
||||
let current_status = if state.is_dragging {
|
||||
Status::Dragged
|
||||
} else if cursor.is_over(layout.bounds()) {
|
||||
Status::Hovered
|
||||
} else {
|
||||
Status::Active
|
||||
};
|
||||
|
||||
if let Event::Window(window::Event::RedrawRequested(_now)) = event {
|
||||
self.status = Some(current_status);
|
||||
} else if self.status.is_some_and(|status| status != current_status) {
|
||||
shell.request_redraw();
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
_tree: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Theme,
|
||||
_style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
_cursor: mouse::Cursor,
|
||||
_viewport: &Rectangle,
|
||||
) {
|
||||
let state = tree.state.downcast_ref::<State>();
|
||||
let bounds = layout.bounds();
|
||||
let is_mouse_over = cursor.is_over(bounds);
|
||||
|
||||
let style = theme.style(
|
||||
&self.class,
|
||||
if state.is_dragging {
|
||||
Status::Dragged
|
||||
} else if is_mouse_over {
|
||||
Status::Hovered
|
||||
} else {
|
||||
Status::Active
|
||||
},
|
||||
);
|
||||
let style =
|
||||
theme.style(&self.class, self.status.unwrap_or(Status::Active));
|
||||
|
||||
let (handle_width, handle_height, handle_border_radius) =
|
||||
match style.handle.shape {
|
||||
|
|
@ -408,10 +484,10 @@ where
|
|||
width: offset + handle_width / 2.0,
|
||||
height: style.rail.width,
|
||||
},
|
||||
border: Border::rounded(style.rail.border_radius),
|
||||
border: style.rail.border,
|
||||
..renderer::Quad::default()
|
||||
},
|
||||
style.rail.colors.0,
|
||||
style.rail.backgrounds.0,
|
||||
);
|
||||
|
||||
renderer.fill_quad(
|
||||
|
|
@ -422,10 +498,10 @@ where
|
|||
width: bounds.width - offset - handle_width / 2.0,
|
||||
height: style.rail.width,
|
||||
},
|
||||
border: Border::rounded(style.rail.border_radius),
|
||||
border: style.rail.border,
|
||||
..renderer::Quad::default()
|
||||
},
|
||||
style.rail.colors.1,
|
||||
style.rail.backgrounds.1,
|
||||
);
|
||||
|
||||
renderer.fill_quad(
|
||||
|
|
@ -443,7 +519,7 @@ where
|
|||
},
|
||||
..renderer::Quad::default()
|
||||
},
|
||||
style.handle.color,
|
||||
style.handle.background,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -502,7 +578,7 @@ pub enum Status {
|
|||
}
|
||||
|
||||
/// The appearance of a slider.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Style {
|
||||
/// The colors of the rail of the slider.
|
||||
pub rail: Rail,
|
||||
|
|
@ -522,23 +598,23 @@ impl Style {
|
|||
}
|
||||
|
||||
/// The appearance of a slider rail
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Rail {
|
||||
/// The colors of the rail of the slider.
|
||||
pub colors: (Color, Color),
|
||||
/// The backgrounds of the rail of the slider.
|
||||
pub backgrounds: (Background, Background),
|
||||
/// The width of the stroke of a slider rail.
|
||||
pub width: f32,
|
||||
/// The border radius of the corners of the rail.
|
||||
pub border_radius: border::Radius,
|
||||
/// The border of the rail.
|
||||
pub border: Border,
|
||||
}
|
||||
|
||||
/// The appearance of the handle of a slider.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Handle {
|
||||
/// The shape of the handle.
|
||||
pub shape: HandleShape,
|
||||
/// The [`Color`] of the handle.
|
||||
pub color: Color,
|
||||
/// The [`Background`] of the handle.
|
||||
pub background: Background,
|
||||
/// The border width of the handle.
|
||||
pub border_width: f32,
|
||||
/// The border [`Color`] of the handle.
|
||||
|
|
@ -546,7 +622,7 @@ pub struct Handle {
|
|||
}
|
||||
|
||||
/// The shape of the handle of a slider.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum HandleShape {
|
||||
/// A circular handle.
|
||||
Circle {
|
||||
|
|
@ -594,20 +670,24 @@ pub fn default(theme: &Theme, status: Status) -> Style {
|
|||
let palette = theme.extended_palette();
|
||||
|
||||
let color = match status {
|
||||
Status::Active => palette.primary.strong.color,
|
||||
Status::Hovered => palette.primary.base.color,
|
||||
Status::Dragged => palette.primary.strong.color,
|
||||
Status::Active => palette.primary.base.color,
|
||||
Status::Hovered => palette.primary.strong.color,
|
||||
Status::Dragged => palette.primary.weak.color,
|
||||
};
|
||||
|
||||
Style {
|
||||
rail: Rail {
|
||||
colors: (color, palette.secondary.base.color),
|
||||
backgrounds: (color.into(), palette.background.strong.color.into()),
|
||||
width: 4.0,
|
||||
border_radius: 2.0.into(),
|
||||
border: Border {
|
||||
radius: 2.0.into(),
|
||||
width: 0.0,
|
||||
color: Color::TRANSPARENT,
|
||||
},
|
||||
},
|
||||
handle: Handle {
|
||||
shape: HandleShape::Circle { radius: 7.0 },
|
||||
color,
|
||||
background: color.into(),
|
||||
border_color: Color::TRANSPARENT,
|
||||
border_width: 0.0,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
//! Display content on top of other content.
|
||||
use crate::core::event::{self, Event};
|
||||
use crate::core::layout;
|
||||
use crate::core::mouse;
|
||||
use crate::core::overlay;
|
||||
use crate::core::renderer;
|
||||
use crate::core::widget::{Operation, Tree};
|
||||
use crate::core::{
|
||||
Clipboard, Element, Layout, Length, Rectangle, Shell, Size, Vector, Widget,
|
||||
Clipboard, Element, Event, Layout, Length, Rectangle, Shell, Size, Vector,
|
||||
Widget,
|
||||
};
|
||||
|
||||
/// A container that displays children on top of each other.
|
||||
|
|
@ -116,7 +116,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Default for Stack<'a, Message, Renderer>
|
||||
impl<Message, Renderer> Default for Stack<'_, Message, Renderer>
|
||||
where
|
||||
Renderer: crate::core::Renderer,
|
||||
{
|
||||
|
|
@ -189,7 +189,7 @@ where
|
|||
tree: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn Operation<Message>,
|
||||
operation: &mut dyn Operation,
|
||||
) {
|
||||
operation.container(None, layout.bounds(), &mut |operation| {
|
||||
self.children
|
||||
|
|
@ -204,36 +204,47 @@ where
|
|||
});
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
fn update(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: Event,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
mut cursor: mouse::Cursor,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
viewport: &Rectangle,
|
||||
) -> event::Status {
|
||||
self.children
|
||||
) {
|
||||
let is_over = cursor.is_over(layout.bounds());
|
||||
let end = self.children.len() - 1;
|
||||
|
||||
for (i, ((child, state), layout)) in self
|
||||
.children
|
||||
.iter_mut()
|
||||
.rev()
|
||||
.zip(tree.children.iter_mut().rev())
|
||||
.zip(layout.children().rev())
|
||||
.map(|((child, state), layout)| {
|
||||
child.as_widget_mut().on_event(
|
||||
state,
|
||||
event.clone(),
|
||||
layout,
|
||||
cursor,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
viewport,
|
||||
)
|
||||
})
|
||||
.find(|&status| status == event::Status::Captured)
|
||||
.unwrap_or(event::Status::Ignored)
|
||||
.enumerate()
|
||||
{
|
||||
child.as_widget_mut().update(
|
||||
state, event, layout, cursor, renderer, clipboard, shell,
|
||||
viewport,
|
||||
);
|
||||
|
||||
if shell.is_event_captured() {
|
||||
return;
|
||||
}
|
||||
|
||||
if i < end && is_over && !cursor.is_levitating() {
|
||||
let interaction = child.as_widget().mouse_interaction(
|
||||
state, layout, cursor, viewport, renderer,
|
||||
);
|
||||
|
||||
if interaction != mouse::Interaction::None {
|
||||
cursor = cursor.levitate();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
|
|
@ -269,15 +280,53 @@ where
|
|||
viewport: &Rectangle,
|
||||
) {
|
||||
if let Some(clipped_viewport) = layout.bounds().intersection(viewport) {
|
||||
for (i, ((layer, state), layout)) in self
|
||||
let layers_below = if cursor.is_over(layout.bounds()) {
|
||||
self.children
|
||||
.iter()
|
||||
.rev()
|
||||
.zip(tree.children.iter().rev())
|
||||
.zip(layout.children().rev())
|
||||
.position(|((layer, state), layout)| {
|
||||
let interaction = layer.as_widget().mouse_interaction(
|
||||
state, layout, cursor, viewport, renderer,
|
||||
);
|
||||
|
||||
interaction != mouse::Interaction::None
|
||||
})
|
||||
.map(|i| self.children.len() - i - 1)
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let mut layers = self
|
||||
.children
|
||||
.iter()
|
||||
.zip(&tree.children)
|
||||
.zip(layout.children())
|
||||
.enumerate()
|
||||
{
|
||||
if i > 0 {
|
||||
renderer.with_layer(clipped_viewport, |renderer| {
|
||||
.enumerate();
|
||||
|
||||
let layers = layers.by_ref();
|
||||
|
||||
let mut draw_layer =
|
||||
|i,
|
||||
layer: &Element<'a, Message, Theme, Renderer>,
|
||||
state,
|
||||
layout,
|
||||
cursor| {
|
||||
if i > 0 {
|
||||
renderer.with_layer(clipped_viewport, |renderer| {
|
||||
layer.as_widget().draw(
|
||||
state,
|
||||
renderer,
|
||||
theme,
|
||||
style,
|
||||
layout,
|
||||
cursor,
|
||||
&clipped_viewport,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
layer.as_widget().draw(
|
||||
state,
|
||||
renderer,
|
||||
|
|
@ -287,18 +336,15 @@ where
|
|||
cursor,
|
||||
&clipped_viewport,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
layer.as_widget().draw(
|
||||
state,
|
||||
renderer,
|
||||
theme,
|
||||
style,
|
||||
layout,
|
||||
cursor,
|
||||
&clipped_viewport,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (i, ((layer, state), layout)) in layers.take(layers_below) {
|
||||
draw_layer(i, layer, state, layout, mouse::Cursor::Unavailable);
|
||||
}
|
||||
|
||||
for (i, ((layer, state), layout)) in layers {
|
||||
draw_layer(i, layer, state, layout, cursor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,20 @@
|
|||
//! Display vector graphics in your application.
|
||||
//! Svg widgets display vector graphics in your application.
|
||||
//!
|
||||
//! # Example
|
||||
//! ```no_run
|
||||
//! # mod iced { pub mod widget { pub use iced_widget::*; } }
|
||||
//! # pub type State = ();
|
||||
//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||
//! use iced::widget::svg;
|
||||
//!
|
||||
//! enum Message {
|
||||
//! // ...
|
||||
//! }
|
||||
//!
|
||||
//! fn view(state: &State) -> Element<'_, Message> {
|
||||
//! svg("tiger.svg").into()
|
||||
//! }
|
||||
//! ```
|
||||
use crate::core::layout;
|
||||
use crate::core::mouse;
|
||||
use crate::core::renderer;
|
||||
|
|
@ -19,6 +35,22 @@ pub use crate::core::svg::Handle;
|
|||
///
|
||||
/// [`Svg`] images can have a considerable rendering cost when resized,
|
||||
/// specially when they are complex.
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// # mod iced { pub mod widget { pub use iced_widget::*; } }
|
||||
/// # pub type State = ();
|
||||
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||
/// use iced::widget::svg;
|
||||
///
|
||||
/// enum Message {
|
||||
/// // ...
|
||||
/// }
|
||||
///
|
||||
/// fn view(state: &State) -> Element<'_, Message> {
|
||||
/// svg("tiger.svg").into()
|
||||
/// }
|
||||
/// ```
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Svg<'a, Theme = crate::Theme>
|
||||
where
|
||||
|
|
@ -116,8 +148,8 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Svg<'a, Theme>
|
||||
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Svg<'_, Theme>
|
||||
where
|
||||
Renderer: svg::Renderer,
|
||||
Theme: Catalog,
|
||||
|
|
@ -211,11 +243,13 @@ where
|
|||
|
||||
let render = |renderer: &mut Renderer| {
|
||||
renderer.draw_svg(
|
||||
self.handle.clone(),
|
||||
style.color,
|
||||
svg::Svg {
|
||||
handle: self.handle.clone(),
|
||||
color: style.color,
|
||||
rotation: self.rotation.radians(),
|
||||
opacity: self.opacity,
|
||||
},
|
||||
drawing_bounds,
|
||||
self.rotation.radians(),
|
||||
self.opacity,
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -289,7 +323,7 @@ impl Catalog for Theme {
|
|||
/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`.
|
||||
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
|
||||
|
||||
impl<'a, Theme> From<Style> for StyleFn<'a, Theme> {
|
||||
impl<Theme> From<Style> for StyleFn<'_, Theme> {
|
||||
fn from(style: Style) -> Self {
|
||||
Box::new(move |_theme, _status| style)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,30 @@
|
|||
//! Draw and interact with text.
|
||||
pub use crate::core::widget::text::*;
|
||||
mod rich;
|
||||
|
||||
/// A paragraph.
|
||||
pub use crate::core::text::{Fragment, Highlighter, IntoFragment, Span};
|
||||
pub use crate::core::widget::text::*;
|
||||
pub use rich::Rich;
|
||||
|
||||
/// A bunch of text.
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
|
||||
/// # pub type State = ();
|
||||
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||
/// use iced::widget::text;
|
||||
/// use iced::color;
|
||||
///
|
||||
/// enum Message {
|
||||
/// // ...
|
||||
/// }
|
||||
///
|
||||
/// fn view(state: &State) -> Element<'_, Message> {
|
||||
/// text("Hello, this is iced!")
|
||||
/// .size(20)
|
||||
/// .color(color!(0x0000ff))
|
||||
/// .into()
|
||||
/// }
|
||||
/// ```
|
||||
pub type Text<'a, Theme = crate::Theme, Renderer = crate::Renderer> =
|
||||
crate::core::widget::Text<'a, Theme, Renderer>;
|
||||
|
|
|
|||
566
widget/src/text/rich.rs
Normal file
566
widget/src/text/rich.rs
Normal file
|
|
@ -0,0 +1,566 @@
|
|||
use crate::core::alignment;
|
||||
use crate::core::layout;
|
||||
use crate::core::mouse;
|
||||
use crate::core::renderer;
|
||||
use crate::core::text::{Paragraph, Span};
|
||||
use crate::core::widget::text::{
|
||||
self, Catalog, LineHeight, Shaping, Style, StyleFn, Wrapping,
|
||||
};
|
||||
use crate::core::widget::tree::{self, Tree};
|
||||
use crate::core::{
|
||||
self, Clipboard, Color, Element, Event, Layout, Length, Pixels, Point,
|
||||
Rectangle, Shell, Size, Vector, Widget,
|
||||
};
|
||||
|
||||
/// A bunch of [`Rich`] text.
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Rich<
|
||||
'a,
|
||||
Link,
|
||||
Message,
|
||||
Theme = crate::Theme,
|
||||
Renderer = crate::Renderer,
|
||||
> where
|
||||
Link: Clone + 'static,
|
||||
Theme: Catalog,
|
||||
Renderer: core::text::Renderer,
|
||||
{
|
||||
spans: Box<dyn AsRef<[Span<'a, Link, Renderer::Font>]> + 'a>,
|
||||
size: Option<Pixels>,
|
||||
line_height: LineHeight,
|
||||
width: Length,
|
||||
height: Length,
|
||||
font: Option<Renderer::Font>,
|
||||
align_x: alignment::Horizontal,
|
||||
align_y: alignment::Vertical,
|
||||
wrapping: Wrapping,
|
||||
class: Theme::Class<'a>,
|
||||
hovered_link: Option<usize>,
|
||||
on_link_click: Option<Box<dyn Fn(Link) -> Message + 'a>>,
|
||||
}
|
||||
|
||||
impl<'a, Link, Message, Theme, Renderer>
|
||||
Rich<'a, Link, Message, Theme, Renderer>
|
||||
where
|
||||
Link: Clone + 'static,
|
||||
Theme: Catalog,
|
||||
Renderer: core::text::Renderer,
|
||||
Renderer::Font: 'a,
|
||||
{
|
||||
/// Creates a new empty [`Rich`] text.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
spans: Box::new([]),
|
||||
size: None,
|
||||
line_height: LineHeight::default(),
|
||||
width: Length::Shrink,
|
||||
height: Length::Shrink,
|
||||
font: None,
|
||||
align_x: alignment::Horizontal::Left,
|
||||
align_y: alignment::Vertical::Top,
|
||||
wrapping: Wrapping::default(),
|
||||
class: Theme::default(),
|
||||
hovered_link: None,
|
||||
on_link_click: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new [`Rich`] text with the given text spans.
|
||||
pub fn with_spans(
|
||||
spans: impl AsRef<[Span<'a, Link, Renderer::Font>]> + 'a,
|
||||
) -> Self {
|
||||
Self {
|
||||
spans: Box::new(spans),
|
||||
..Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the default size of the [`Rich`] text.
|
||||
pub fn size(mut self, size: impl Into<Pixels>) -> Self {
|
||||
self.size = Some(size.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the default [`LineHeight`] of the [`Rich`] text.
|
||||
pub fn line_height(mut self, line_height: impl Into<LineHeight>) -> Self {
|
||||
self.line_height = line_height.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the default font of the [`Rich`] text.
|
||||
pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
|
||||
self.font = Some(font.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the width of the [`Rich`] text boundaries.
|
||||
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||
self.width = width.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the height of the [`Rich`] text boundaries.
|
||||
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
||||
self.height = height.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Centers the [`Rich`] text, both horizontally and vertically.
|
||||
pub fn center(self) -> Self {
|
||||
self.align_x(alignment::Horizontal::Center)
|
||||
.align_y(alignment::Vertical::Center)
|
||||
}
|
||||
|
||||
/// Sets the [`alignment::Horizontal`] of the [`Rich`] text.
|
||||
pub fn align_x(
|
||||
mut self,
|
||||
alignment: impl Into<alignment::Horizontal>,
|
||||
) -> Self {
|
||||
self.align_x = alignment.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`alignment::Vertical`] of the [`Rich`] text.
|
||||
pub fn align_y(
|
||||
mut self,
|
||||
alignment: impl Into<alignment::Vertical>,
|
||||
) -> Self {
|
||||
self.align_y = alignment.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`Wrapping`] strategy of the [`Rich`] text.
|
||||
pub fn wrapping(mut self, wrapping: Wrapping) -> Self {
|
||||
self.wrapping = wrapping;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the message that will be produced when a link of the [`Rich`] text
|
||||
/// is clicked.
|
||||
pub fn on_link_click(
|
||||
mut self,
|
||||
on_link_clicked: impl Fn(Link) -> Message + 'a,
|
||||
) -> Self {
|
||||
self.on_link_click = Some(Box::new(on_link_clicked));
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the default style of the [`Rich`] text.
|
||||
#[must_use]
|
||||
pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
|
||||
where
|
||||
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
|
||||
{
|
||||
self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the default [`Color`] of the [`Rich`] text.
|
||||
pub fn color(self, color: impl Into<Color>) -> Self
|
||||
where
|
||||
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
|
||||
{
|
||||
self.color_maybe(Some(color))
|
||||
}
|
||||
|
||||
/// Sets the default [`Color`] of the [`Rich`] text, if `Some`.
|
||||
pub fn color_maybe(self, color: Option<impl Into<Color>>) -> Self
|
||||
where
|
||||
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
|
||||
{
|
||||
let color = color.map(Into::into);
|
||||
|
||||
self.style(move |_theme| Style { color })
|
||||
}
|
||||
|
||||
/// Sets the default style class of the [`Rich`] text.
|
||||
#[cfg(feature = "advanced")]
|
||||
#[must_use]
|
||||
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
|
||||
self.class = class.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Link, Message, Theme, Renderer> Default
|
||||
for Rich<'a, Link, Message, Theme, Renderer>
|
||||
where
|
||||
Link: Clone + 'a,
|
||||
Theme: Catalog,
|
||||
Renderer: core::text::Renderer,
|
||||
Renderer::Font: 'a,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
struct State<Link, P: Paragraph> {
|
||||
spans: Vec<Span<'static, Link, P::Font>>,
|
||||
span_pressed: Option<usize>,
|
||||
paragraph: P,
|
||||
}
|
||||
|
||||
impl<Link, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Rich<'_, Link, Message, Theme, Renderer>
|
||||
where
|
||||
Link: Clone + 'static,
|
||||
Theme: Catalog,
|
||||
Renderer: core::text::Renderer,
|
||||
{
|
||||
fn tag(&self) -> tree::Tag {
|
||||
tree::Tag::of::<State<Link, Renderer::Paragraph>>()
|
||||
}
|
||||
|
||||
fn state(&self) -> tree::State {
|
||||
tree::State::new(State::<Link, _> {
|
||||
spans: Vec::new(),
|
||||
span_pressed: None,
|
||||
paragraph: Renderer::Paragraph::default(),
|
||||
})
|
||||
}
|
||||
|
||||
fn size(&self) -> Size<Length> {
|
||||
Size {
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
}
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
tree: &mut Tree,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
layout(
|
||||
tree.state
|
||||
.downcast_mut::<State<Link, Renderer::Paragraph>>(),
|
||||
renderer,
|
||||
limits,
|
||||
self.width,
|
||||
self.height,
|
||||
self.spans.as_ref().as_ref(),
|
||||
self.line_height,
|
||||
self.size,
|
||||
self.font,
|
||||
self.align_x,
|
||||
self.align_y,
|
||||
self.wrapping,
|
||||
)
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Theme,
|
||||
defaults: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
_cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
if !layout.bounds().intersects(viewport) {
|
||||
return;
|
||||
}
|
||||
|
||||
let state = tree
|
||||
.state
|
||||
.downcast_ref::<State<Link, Renderer::Paragraph>>();
|
||||
|
||||
let style = theme.style(&self.class);
|
||||
|
||||
for (index, span) in self.spans.as_ref().as_ref().iter().enumerate() {
|
||||
let is_hovered_link = self.on_link_click.is_some()
|
||||
&& Some(index) == self.hovered_link;
|
||||
|
||||
if span.highlight.is_some()
|
||||
|| span.underline
|
||||
|| span.strikethrough
|
||||
|| is_hovered_link
|
||||
{
|
||||
let translation = layout.position() - Point::ORIGIN;
|
||||
let regions = state.paragraph.span_bounds(index);
|
||||
|
||||
if let Some(highlight) = span.highlight {
|
||||
for bounds in ®ions {
|
||||
let bounds = Rectangle::new(
|
||||
bounds.position()
|
||||
- Vector::new(
|
||||
span.padding.left,
|
||||
span.padding.top,
|
||||
),
|
||||
bounds.size()
|
||||
+ Size::new(
|
||||
span.padding.horizontal(),
|
||||
span.padding.vertical(),
|
||||
),
|
||||
);
|
||||
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: bounds + translation,
|
||||
border: highlight.border,
|
||||
..Default::default()
|
||||
},
|
||||
highlight.background,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if span.underline || span.strikethrough || is_hovered_link {
|
||||
let size = span
|
||||
.size
|
||||
.or(self.size)
|
||||
.unwrap_or(renderer.default_size());
|
||||
|
||||
let line_height = span
|
||||
.line_height
|
||||
.unwrap_or(self.line_height)
|
||||
.to_absolute(size);
|
||||
|
||||
let color = span
|
||||
.color
|
||||
.or(style.color)
|
||||
.unwrap_or(defaults.text_color);
|
||||
|
||||
let baseline = translation
|
||||
+ Vector::new(
|
||||
0.0,
|
||||
size.0 + (line_height.0 - size.0) / 2.0,
|
||||
);
|
||||
|
||||
if span.underline || is_hovered_link {
|
||||
for bounds in ®ions {
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: Rectangle::new(
|
||||
bounds.position() + baseline
|
||||
- Vector::new(0.0, size.0 * 0.08),
|
||||
Size::new(bounds.width, 1.0),
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
color,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if span.strikethrough {
|
||||
for bounds in ®ions {
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: Rectangle::new(
|
||||
bounds.position() + baseline
|
||||
- Vector::new(0.0, size.0 / 2.0),
|
||||
Size::new(bounds.width, 1.0),
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
color,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
text::draw(
|
||||
renderer,
|
||||
defaults,
|
||||
layout,
|
||||
&state.paragraph,
|
||||
style,
|
||||
viewport,
|
||||
);
|
||||
}
|
||||
|
||||
fn update(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
_renderer: &Renderer,
|
||||
_clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
_viewport: &Rectangle,
|
||||
) {
|
||||
let Some(on_link_clicked) = &self.on_link_click else {
|
||||
return;
|
||||
};
|
||||
|
||||
let was_hovered = self.hovered_link.is_some();
|
||||
|
||||
if let Some(position) = cursor.position_in(layout.bounds()) {
|
||||
let state = tree
|
||||
.state
|
||||
.downcast_ref::<State<Link, Renderer::Paragraph>>();
|
||||
|
||||
self.hovered_link =
|
||||
state.paragraph.hit_span(position).and_then(|span| {
|
||||
if self.spans.as_ref().as_ref().get(span)?.link.is_some() {
|
||||
Some(span)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
} else {
|
||||
self.hovered_link = None;
|
||||
}
|
||||
|
||||
if was_hovered != self.hovered_link.is_some() {
|
||||
shell.request_redraw();
|
||||
}
|
||||
|
||||
match event {
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
|
||||
let state = tree
|
||||
.state
|
||||
.downcast_mut::<State<Link, Renderer::Paragraph>>();
|
||||
|
||||
if self.hovered_link.is_some() {
|
||||
state.span_pressed = self.hovered_link;
|
||||
shell.capture_event();
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
|
||||
let state = tree
|
||||
.state
|
||||
.downcast_mut::<State<Link, Renderer::Paragraph>>();
|
||||
|
||||
match state.span_pressed {
|
||||
Some(span) if Some(span) == self.hovered_link => {
|
||||
if let Some(link) = self
|
||||
.spans
|
||||
.as_ref()
|
||||
.as_ref()
|
||||
.get(span)
|
||||
.and_then(|span| span.link.clone())
|
||||
{
|
||||
shell.publish(on_link_clicked(link));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
state.span_pressed = None;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
_tree: &Tree,
|
||||
_layout: Layout<'_>,
|
||||
_cursor: mouse::Cursor,
|
||||
_viewport: &Rectangle,
|
||||
_renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
if self.hovered_link.is_some() {
|
||||
mouse::Interaction::Pointer
|
||||
} else {
|
||||
mouse::Interaction::None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn layout<Link, Renderer>(
|
||||
state: &mut State<Link, Renderer::Paragraph>,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
width: Length,
|
||||
height: Length,
|
||||
spans: &[Span<'_, Link, Renderer::Font>],
|
||||
line_height: LineHeight,
|
||||
size: Option<Pixels>,
|
||||
font: Option<Renderer::Font>,
|
||||
horizontal_alignment: alignment::Horizontal,
|
||||
vertical_alignment: alignment::Vertical,
|
||||
wrapping: Wrapping,
|
||||
) -> layout::Node
|
||||
where
|
||||
Link: Clone,
|
||||
Renderer: core::text::Renderer,
|
||||
{
|
||||
layout::sized(limits, width, height, |limits| {
|
||||
let bounds = limits.max();
|
||||
|
||||
let size = size.unwrap_or_else(|| renderer.default_size());
|
||||
let font = font.unwrap_or_else(|| renderer.default_font());
|
||||
|
||||
let text_with_spans = || core::Text {
|
||||
content: spans,
|
||||
bounds,
|
||||
size,
|
||||
line_height,
|
||||
font,
|
||||
horizontal_alignment,
|
||||
vertical_alignment,
|
||||
shaping: Shaping::Advanced,
|
||||
wrapping,
|
||||
};
|
||||
|
||||
if state.spans != spans {
|
||||
state.paragraph =
|
||||
Renderer::Paragraph::with_spans(text_with_spans());
|
||||
state.spans = spans.iter().cloned().map(Span::to_static).collect();
|
||||
} else {
|
||||
match state.paragraph.compare(core::Text {
|
||||
content: (),
|
||||
bounds,
|
||||
size,
|
||||
line_height,
|
||||
font,
|
||||
horizontal_alignment,
|
||||
vertical_alignment,
|
||||
shaping: Shaping::Advanced,
|
||||
wrapping,
|
||||
}) {
|
||||
core::text::Difference::None => {}
|
||||
core::text::Difference::Bounds => {
|
||||
state.paragraph.resize(bounds);
|
||||
}
|
||||
core::text::Difference::Shape => {
|
||||
state.paragraph =
|
||||
Renderer::Paragraph::with_spans(text_with_spans());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state.paragraph.min_bounds()
|
||||
})
|
||||
}
|
||||
|
||||
impl<'a, Link, Message, Theme, Renderer>
|
||||
FromIterator<Span<'a, Link, Renderer::Font>>
|
||||
for Rich<'a, Link, Message, Theme, Renderer>
|
||||
where
|
||||
Link: Clone + 'a,
|
||||
Theme: Catalog,
|
||||
Renderer: core::text::Renderer,
|
||||
Renderer::Font: 'a,
|
||||
{
|
||||
fn from_iter<T: IntoIterator<Item = Span<'a, Link, Renderer::Font>>>(
|
||||
spans: T,
|
||||
) -> Self {
|
||||
Self::with_spans(spans.into_iter().collect::<Vec<_>>())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Link, Message, Theme, Renderer>
|
||||
From<Rich<'a, Link, Message, Theme, Renderer>>
|
||||
for Element<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Link: Clone + 'a,
|
||||
Theme: Catalog + 'a,
|
||||
Renderer: core::text::Renderer + 'a,
|
||||
{
|
||||
fn from(
|
||||
text: Rich<'a, Link, Message, Theme, Renderer>,
|
||||
) -> Element<'a, Message, Theme, Renderer> {
|
||||
Element::new(text)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -2,13 +2,13 @@
|
|||
use crate::text_input::Value;
|
||||
|
||||
/// The cursor of a text input.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub struct Cursor {
|
||||
state: State,
|
||||
}
|
||||
|
||||
/// The state of a [`Cursor`].
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum State {
|
||||
/// Cursor without a selection
|
||||
Index(usize),
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
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::tree::{self, Tree};
|
||||
use crate::core::widget::Operation;
|
||||
use crate::core::widget::tree::{self, Tree};
|
||||
use crate::core::{
|
||||
Background, Clipboard, Color, Element, Layout, Length, Point, Rectangle,
|
||||
Shell, Size, Vector, Widget,
|
||||
Background, Clipboard, Color, Element, Event, Layout, Length, Point,
|
||||
Rectangle, Shell, Size, Vector, Widget,
|
||||
};
|
||||
|
||||
use std::marker::PhantomData;
|
||||
|
|
@ -64,8 +63,8 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, NewTheme, F, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Themer<'a, Message, Theme, NewTheme, F, Renderer>
|
||||
impl<Message, Theme, NewTheme, F, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Themer<'_, Message, Theme, NewTheme, F, Renderer>
|
||||
where
|
||||
F: Fn(&Theme) -> NewTheme,
|
||||
Renderer: crate::core::Renderer,
|
||||
|
|
@ -104,27 +103,27 @@ where
|
|||
tree: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn Operation<Message>,
|
||||
operation: &mut dyn Operation,
|
||||
) {
|
||||
self.content
|
||||
.as_widget()
|
||||
.operate(tree, layout, renderer, operation);
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
fn update(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: Event,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
viewport: &Rectangle,
|
||||
) -> event::Status {
|
||||
self.content.as_widget_mut().on_event(
|
||||
) {
|
||||
self.content.as_widget_mut().update(
|
||||
tree, event, layout, cursor, renderer, clipboard, shell, viewport,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
|
|
@ -188,9 +187,9 @@ where
|
|||
content: overlay::Element<'a, Message, NewTheme, Renderer>,
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, NewTheme, Renderer>
|
||||
impl<Message, Theme, NewTheme, Renderer>
|
||||
overlay::Overlay<Message, Theme, Renderer>
|
||||
for Overlay<'a, Message, Theme, NewTheme, Renderer>
|
||||
for Overlay<'_, Message, Theme, NewTheme, Renderer>
|
||||
where
|
||||
Renderer: crate::core::Renderer,
|
||||
{
|
||||
|
|
@ -219,24 +218,24 @@ where
|
|||
);
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
fn update(
|
||||
&mut self,
|
||||
event: Event,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
) {
|
||||
self.content
|
||||
.on_event(event, layout, cursor, renderer, clipboard, shell)
|
||||
.update(event, layout, cursor, renderer, clipboard, shell);
|
||||
}
|
||||
|
||||
fn operate(
|
||||
&mut self,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn Operation<Message>,
|
||||
operation: &mut dyn Operation,
|
||||
) {
|
||||
self.content.operate(layout, renderer, operation);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,36 @@
|
|||
//! Show toggle controls using togglers.
|
||||
//! Togglers let users make binary choices by toggling a switch.
|
||||
//!
|
||||
//! # Example
|
||||
//! ```no_run
|
||||
//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
|
||||
//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||
//! #
|
||||
//! use iced::widget::toggler;
|
||||
//!
|
||||
//! struct State {
|
||||
//! is_checked: bool,
|
||||
//! }
|
||||
//!
|
||||
//! enum Message {
|
||||
//! TogglerToggled(bool),
|
||||
//! }
|
||||
//!
|
||||
//! fn view(state: &State) -> Element<'_, Message> {
|
||||
//! toggler(state.is_checked)
|
||||
//! .label("Toggle me!")
|
||||
//! .on_toggle(Message::TogglerToggled)
|
||||
//! .into()
|
||||
//! }
|
||||
//!
|
||||
//! fn update(state: &mut State, message: Message) {
|
||||
//! match message {
|
||||
//! Message::TogglerToggled(is_checked) => {
|
||||
//! state.is_checked = is_checked;
|
||||
//! }
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
use crate::core::alignment;
|
||||
use crate::core::event;
|
||||
use crate::core::layout;
|
||||
use crate::core::mouse;
|
||||
use crate::core::renderer;
|
||||
|
|
@ -8,6 +38,7 @@ use crate::core::text;
|
|||
use crate::core::touch;
|
||||
use crate::core::widget;
|
||||
use crate::core::widget::tree::{self, Tree};
|
||||
use crate::core::window;
|
||||
use crate::core::{
|
||||
Border, Clipboard, Color, Element, Event, Layout, Length, Pixels,
|
||||
Rectangle, Shell, Size, Theme, Widget,
|
||||
|
|
@ -16,17 +47,34 @@ use crate::core::{
|
|||
/// A toggler widget.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// # type Toggler<'a, Message> = iced_widget::Toggler<'a, Message>;
|
||||
/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
|
||||
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||
/// #
|
||||
/// pub enum Message {
|
||||
/// use iced::widget::toggler;
|
||||
///
|
||||
/// struct State {
|
||||
/// is_checked: bool,
|
||||
/// }
|
||||
///
|
||||
/// enum Message {
|
||||
/// TogglerToggled(bool),
|
||||
/// }
|
||||
///
|
||||
/// let is_toggled = true;
|
||||
/// fn view(state: &State) -> Element<'_, Message> {
|
||||
/// toggler(state.is_checked)
|
||||
/// .label("Toggle me!")
|
||||
/// .on_toggle(Message::TogglerToggled)
|
||||
/// .into()
|
||||
/// }
|
||||
///
|
||||
/// Toggler::new(String::from("Toggle me!"), is_toggled, |b| Message::TogglerToggled(b));
|
||||
/// fn update(state: &mut State, message: Message) {
|
||||
/// match message {
|
||||
/// Message::TogglerToggled(is_checked) => {
|
||||
/// state.is_checked = is_checked;
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Toggler<
|
||||
|
|
@ -39,17 +87,19 @@ pub struct Toggler<
|
|||
Renderer: text::Renderer,
|
||||
{
|
||||
is_toggled: bool,
|
||||
on_toggle: Box<dyn Fn(bool) -> Message + 'a>,
|
||||
label: Option<String>,
|
||||
on_toggle: Option<Box<dyn Fn(bool) -> Message + 'a>>,
|
||||
label: Option<text::Fragment<'a>>,
|
||||
width: Length,
|
||||
size: f32,
|
||||
text_size: Option<Pixels>,
|
||||
text_line_height: text::LineHeight,
|
||||
text_alignment: alignment::Horizontal,
|
||||
text_shaping: text::Shaping,
|
||||
text_wrapping: text::Wrapping,
|
||||
spacing: f32,
|
||||
font: Option<Renderer::Font>,
|
||||
class: Theme::Class<'a>,
|
||||
last_status: Option<Status>,
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> Toggler<'a, Message, Theme, Renderer>
|
||||
|
|
@ -68,30 +118,55 @@ where
|
|||
/// * 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,
|
||||
{
|
||||
pub fn new(is_toggled: bool) -> Self {
|
||||
Toggler {
|
||||
is_toggled,
|
||||
on_toggle: Box::new(f),
|
||||
label: label.into(),
|
||||
width: Length::Fill,
|
||||
on_toggle: None,
|
||||
label: None,
|
||||
width: Length::Shrink,
|
||||
size: Self::DEFAULT_SIZE,
|
||||
text_size: None,
|
||||
text_line_height: text::LineHeight::default(),
|
||||
text_alignment: alignment::Horizontal::Left,
|
||||
text_shaping: text::Shaping::Basic,
|
||||
text_shaping: text::Shaping::default(),
|
||||
text_wrapping: text::Wrapping::default(),
|
||||
spacing: Self::DEFAULT_SIZE / 2.0,
|
||||
font: None,
|
||||
class: Theme::default(),
|
||||
last_status: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the label of the [`Toggler`].
|
||||
pub fn label(mut self, label: impl text::IntoFragment<'a>) -> Self {
|
||||
self.label = Some(label.into_fragment());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the message that should be produced when a user toggles
|
||||
/// the [`Toggler`].
|
||||
///
|
||||
/// If this method is not called, the [`Toggler`] will be disabled.
|
||||
pub fn on_toggle(
|
||||
mut self,
|
||||
on_toggle: impl Fn(bool) -> Message + 'a,
|
||||
) -> Self {
|
||||
self.on_toggle = Some(Box::new(on_toggle));
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the message that should be produced when a user toggles
|
||||
/// the [`Toggler`], if `Some`.
|
||||
///
|
||||
/// If `None`, the [`Toggler`] will be disabled.
|
||||
pub fn on_toggle_maybe(
|
||||
mut self,
|
||||
on_toggle: Option<impl Fn(bool) -> Message + 'a>,
|
||||
) -> Self {
|
||||
self.on_toggle = on_toggle.map(|on_toggle| Box::new(on_toggle) as _);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the size of the [`Toggler`].
|
||||
pub fn size(mut self, size: impl Into<Pixels>) -> Self {
|
||||
self.size = size.into().0;
|
||||
|
|
@ -131,6 +206,12 @@ where
|
|||
self
|
||||
}
|
||||
|
||||
/// Sets the [`text::Wrapping`] strategy of the [`Toggler`].
|
||||
pub fn text_wrapping(mut self, wrapping: text::Wrapping) -> Self {
|
||||
self.text_wrapping = wrapping;
|
||||
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;
|
||||
|
|
@ -164,8 +245,8 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Toggler<'a, Message, Theme, Renderer>
|
||||
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Toggler<'_, Message, Theme, Renderer>
|
||||
where
|
||||
Theme: Catalog,
|
||||
Renderer: text::Renderer,
|
||||
|
|
@ -216,6 +297,7 @@ where
|
|||
self.text_alignment,
|
||||
alignment::Vertical::Top,
|
||||
self.text_shaping,
|
||||
self.text_wrapping,
|
||||
)
|
||||
} else {
|
||||
layout::Node::new(Size::ZERO)
|
||||
|
|
@ -224,31 +306,53 @@ where
|
|||
)
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
fn update(
|
||||
&mut self,
|
||||
_state: &mut Tree,
|
||||
event: Event,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
_renderer: &Renderer,
|
||||
_clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
_viewport: &Rectangle,
|
||||
) -> event::Status {
|
||||
) {
|
||||
let Some(on_toggle) = &self.on_toggle else {
|
||||
return;
|
||||
};
|
||||
|
||||
match event {
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
|
||||
| Event::Touch(touch::Event::FingerPressed { .. }) => {
|
||||
let mouse_over = cursor.is_over(layout.bounds());
|
||||
|
||||
if mouse_over {
|
||||
shell.publish((self.on_toggle)(!self.is_toggled));
|
||||
|
||||
event::Status::Captured
|
||||
} else {
|
||||
event::Status::Ignored
|
||||
shell.publish(on_toggle(!self.is_toggled));
|
||||
shell.capture_event();
|
||||
}
|
||||
}
|
||||
_ => event::Status::Ignored,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let current_status = if self.on_toggle.is_none() {
|
||||
Status::Disabled
|
||||
} else if cursor.is_over(layout.bounds()) {
|
||||
Status::Hovered {
|
||||
is_toggled: self.is_toggled,
|
||||
}
|
||||
} else {
|
||||
Status::Active {
|
||||
is_toggled: self.is_toggled,
|
||||
}
|
||||
};
|
||||
|
||||
if let Event::Window(window::Event::RedrawRequested(_now)) = event {
|
||||
self.last_status = Some(current_status);
|
||||
} else if self
|
||||
.last_status
|
||||
.is_some_and(|status| status != current_status)
|
||||
{
|
||||
shell.request_redraw();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -261,7 +365,11 @@ where
|
|||
_renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
if cursor.is_over(layout.bounds()) {
|
||||
mouse::Interaction::Pointer
|
||||
if self.on_toggle.is_some() {
|
||||
mouse::Interaction::Pointer
|
||||
} else {
|
||||
mouse::Interaction::NotAllowed
|
||||
}
|
||||
} else {
|
||||
mouse::Interaction::default()
|
||||
}
|
||||
|
|
@ -274,7 +382,7 @@ where
|
|||
theme: &Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
_cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
/// Makes sure that the border radius of the toggler looks good at every size.
|
||||
|
|
@ -289,31 +397,22 @@ where
|
|||
|
||||
if self.label.is_some() {
|
||||
let label_layout = children.next().unwrap();
|
||||
let state: &widget::text::State<Renderer::Paragraph> =
|
||||
tree.state.downcast_ref();
|
||||
|
||||
crate::text::draw(
|
||||
renderer,
|
||||
style,
|
||||
label_layout,
|
||||
tree.state.downcast_ref(),
|
||||
state.0.raw(),
|
||||
crate::text::Style::default(),
|
||||
viewport,
|
||||
);
|
||||
}
|
||||
|
||||
let bounds = toggler_layout.bounds();
|
||||
let is_mouse_over = cursor.is_over(layout.bounds());
|
||||
|
||||
let status = if is_mouse_over {
|
||||
Status::Hovered {
|
||||
is_toggled: self.is_toggled,
|
||||
}
|
||||
} else {
|
||||
Status::Active {
|
||||
is_toggled: self.is_toggled,
|
||||
}
|
||||
};
|
||||
|
||||
let style = theme.style(&self.class, status);
|
||||
let style = theme
|
||||
.style(&self.class, self.last_status.unwrap_or(Status::Disabled));
|
||||
|
||||
let border_radius = bounds.height / BORDER_RADIUS_RATIO;
|
||||
let space = SPACE_RATIO * bounds.height;
|
||||
|
|
@ -392,10 +491,12 @@ pub enum Status {
|
|||
/// Indicates whether the [`Toggler`] is toggled.
|
||||
is_toggled: bool,
|
||||
},
|
||||
/// The [`Toggler`] is disabled.
|
||||
Disabled,
|
||||
}
|
||||
|
||||
/// The appearance of a toggler.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Style {
|
||||
/// The background [`Color`] of the toggler.
|
||||
pub background: Color,
|
||||
|
|
@ -452,6 +553,7 @@ pub fn default(theme: &Theme, status: Status) -> Style {
|
|||
palette.background.strong.color
|
||||
}
|
||||
}
|
||||
Status::Disabled => palette.background.weak.color,
|
||||
};
|
||||
|
||||
let foreground = match status {
|
||||
|
|
@ -472,6 +574,7 @@ pub fn default(theme: &Theme, status: Status) -> Style {
|
|||
palette.background.weak.color
|
||||
}
|
||||
}
|
||||
Status::Disabled => palette.background.base.color,
|
||||
};
|
||||
|
||||
Style {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,27 @@
|
|||
//! Display a widget over another.
|
||||
//! Tooltips display a hint of information over some element when hovered.
|
||||
//!
|
||||
//! # Example
|
||||
//! ```no_run
|
||||
//! # mod iced { pub mod widget { pub use iced_widget::*; } }
|
||||
//! # pub type State = ();
|
||||
//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||
//! use iced::widget::{container, tooltip};
|
||||
//!
|
||||
//! enum Message {
|
||||
//! // ...
|
||||
//! }
|
||||
//!
|
||||
//! fn view(_state: &State) -> Element<'_, Message> {
|
||||
//! tooltip(
|
||||
//! "Hover me to display the tooltip!",
|
||||
//! container("This is the tooltip contents!")
|
||||
//! .padding(10)
|
||||
//! .style(container::rounded_box),
|
||||
//! tooltip::Position::Bottom,
|
||||
//! ).into()
|
||||
//! }
|
||||
//! ```
|
||||
use crate::container;
|
||||
use crate::core::event::{self, Event};
|
||||
use crate::core::layout::{self, Layout};
|
||||
use crate::core::mouse;
|
||||
use crate::core::overlay;
|
||||
|
|
@ -8,11 +29,33 @@ use crate::core::renderer;
|
|||
use crate::core::text;
|
||||
use crate::core::widget::{self, Widget};
|
||||
use crate::core::{
|
||||
Clipboard, Element, Length, Padding, Pixels, Point, Rectangle, Shell, Size,
|
||||
Vector,
|
||||
Clipboard, Element, Event, Length, Padding, Pixels, Point, Rectangle,
|
||||
Shell, Size, Vector,
|
||||
};
|
||||
|
||||
/// An element to display a widget over another.
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// # mod iced { pub mod widget { pub use iced_widget::*; } }
|
||||
/// # pub type State = ();
|
||||
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||
/// use iced::widget::{container, tooltip};
|
||||
///
|
||||
/// enum Message {
|
||||
/// // ...
|
||||
/// }
|
||||
///
|
||||
/// fn view(_state: &State) -> Element<'_, Message> {
|
||||
/// tooltip(
|
||||
/// "Hover me to display the tooltip!",
|
||||
/// container("This is the tooltip contents!")
|
||||
/// .padding(10)
|
||||
/// .style(container::rounded_box),
|
||||
/// tooltip::Position::Bottom,
|
||||
/// ).into()
|
||||
/// }
|
||||
/// ```
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Tooltip<
|
||||
'a,
|
||||
|
|
@ -99,8 +142,8 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Tooltip<'a, Message, Theme, Renderer>
|
||||
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Tooltip<'_, Message, Theme, Renderer>
|
||||
where
|
||||
Theme: container::Catalog,
|
||||
Renderer: text::Renderer,
|
||||
|
|
@ -146,17 +189,17 @@ where
|
|||
.layout(&mut tree.children[0], renderer, limits)
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
fn update(
|
||||
&mut self,
|
||||
tree: &mut widget::Tree,
|
||||
event: Event,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
viewport: &Rectangle,
|
||||
) -> event::Status {
|
||||
) {
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
|
||||
let was_idle = *state == State::Idle;
|
||||
|
|
@ -170,9 +213,12 @@ where
|
|||
|
||||
if was_idle != is_idle {
|
||||
shell.invalidate_layout();
|
||||
shell.request_redraw();
|
||||
} else if !is_idle && self.position == Position::FollowCursor {
|
||||
shell.request_redraw();
|
||||
}
|
||||
|
||||
self.content.as_widget_mut().on_event(
|
||||
self.content.as_widget_mut().update(
|
||||
&mut tree.children[0],
|
||||
event,
|
||||
layout,
|
||||
|
|
@ -181,7 +227,7 @@ where
|
|||
clipboard,
|
||||
shell,
|
||||
viewport,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
|
|
@ -326,9 +372,8 @@ where
|
|||
class: &'b Theme::Class<'a>,
|
||||
}
|
||||
|
||||
impl<'a, 'b, Message, Theme, Renderer>
|
||||
overlay::Overlay<Message, Theme, Renderer>
|
||||
for Overlay<'a, 'b, Message, Theme, Renderer>
|
||||
impl<Message, Theme, Renderer> overlay::Overlay<Message, Theme, Renderer>
|
||||
for Overlay<'_, '_, Message, Theme, Renderer>
|
||||
where
|
||||
Theme: container::Catalog,
|
||||
Renderer: text::Renderer,
|
||||
|
|
@ -425,8 +470,10 @@ where
|
|||
|
||||
layout::Node::with_children(
|
||||
tooltip_bounds.size(),
|
||||
vec![tooltip_layout
|
||||
.translate(Vector::new(self.padding, self.padding))],
|
||||
vec![
|
||||
tooltip_layout
|
||||
.translate(Vector::new(self.padding, self.padding)),
|
||||
],
|
||||
)
|
||||
.translate(Vector::new(tooltip_bounds.x, tooltip_bounds.y))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,40 @@
|
|||
//! Display an interactive selector of a single value from a range of values.
|
||||
//! Sliders let users set a value by moving an indicator.
|
||||
//!
|
||||
//! # Example
|
||||
//! ```no_run
|
||||
//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
|
||||
//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||
//! #
|
||||
//! use iced::widget::slider;
|
||||
//!
|
||||
//! struct State {
|
||||
//! value: f32,
|
||||
//! }
|
||||
//!
|
||||
//! #[derive(Debug, Clone)]
|
||||
//! enum Message {
|
||||
//! ValueChanged(f32),
|
||||
//! }
|
||||
//!
|
||||
//! fn view(state: &State) -> Element<'_, Message> {
|
||||
//! slider(0.0..=100.0, state.value, Message::ValueChanged).into()
|
||||
//! }
|
||||
//!
|
||||
//! fn update(state: &mut State, message: Message) {
|
||||
//! match message {
|
||||
//! Message::ValueChanged(value) => {
|
||||
//! state.value = value;
|
||||
//! }
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
pub use crate::slider::{
|
||||
default, Catalog, Handle, HandleShape, Status, Style, StyleFn,
|
||||
Catalog, Handle, HandleShape, Status, Style, StyleFn, default,
|
||||
};
|
||||
|
||||
use crate::core::event::{self, Event};
|
||||
use crate::core::border::Border;
|
||||
use crate::core::keyboard;
|
||||
use crate::core::keyboard::key::{self, Key};
|
||||
use crate::core::layout::{self, Layout};
|
||||
|
|
@ -13,8 +42,9 @@ use crate::core::mouse;
|
|||
use crate::core::renderer;
|
||||
use crate::core::touch;
|
||||
use crate::core::widget::tree::{self, Tree};
|
||||
use crate::core::window;
|
||||
use crate::core::{
|
||||
self, Border, Clipboard, Element, Length, Pixels, Point, Rectangle, Shell,
|
||||
self, Clipboard, Element, Event, Length, Pixels, Point, Rectangle, Shell,
|
||||
Size, Widget,
|
||||
};
|
||||
|
||||
|
|
@ -28,16 +58,31 @@ use crate::core::{
|
|||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// # type VerticalSlider<'a, T, Message> = iced_widget::VerticalSlider<'a, T, Message>;
|
||||
/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
|
||||
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||
/// #
|
||||
/// #[derive(Clone)]
|
||||
/// pub enum Message {
|
||||
/// SliderChanged(f32),
|
||||
/// use iced::widget::vertical_slider;
|
||||
///
|
||||
/// struct State {
|
||||
/// value: f32,
|
||||
/// }
|
||||
///
|
||||
/// let value = 50.0;
|
||||
/// #[derive(Debug, Clone)]
|
||||
/// enum Message {
|
||||
/// ValueChanged(f32),
|
||||
/// }
|
||||
///
|
||||
/// VerticalSlider::new(0.0..=100.0, value, Message::SliderChanged);
|
||||
/// fn view(state: &State) -> Element<'_, Message> {
|
||||
/// vertical_slider(0.0..=100.0, state.value, Message::ValueChanged).into()
|
||||
/// }
|
||||
///
|
||||
/// fn update(state: &mut State, message: Message) {
|
||||
/// match message {
|
||||
/// Message::ValueChanged(value) => {
|
||||
/// state.value = value;
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct VerticalSlider<'a, T, Message, Theme = crate::Theme>
|
||||
|
|
@ -54,6 +99,7 @@ where
|
|||
width: f32,
|
||||
height: Length,
|
||||
class: Theme::Class<'a>,
|
||||
status: Option<Status>,
|
||||
}
|
||||
|
||||
impl<'a, T, Message, Theme> VerticalSlider<'a, T, Message, Theme>
|
||||
|
|
@ -71,8 +117,8 @@ where
|
|||
/// * 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`.
|
||||
/// 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,
|
||||
|
|
@ -100,6 +146,7 @@ where
|
|||
width: Self::DEFAULT_WIDTH,
|
||||
height: Length::Fill,
|
||||
class: Theme::default(),
|
||||
status: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -167,8 +214,8 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for VerticalSlider<'a, T, Message, Theme>
|
||||
impl<T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for VerticalSlider<'_, T, Message, Theme>
|
||||
where
|
||||
T: Copy + Into<f64> + num_traits::FromPrimitive,
|
||||
Message: Clone,
|
||||
|
|
@ -199,17 +246,17 @@ where
|
|||
layout::atomic(limits, self.width, self.height)
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
fn update(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: Event,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
_renderer: &Renderer,
|
||||
_clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
_viewport: &Rectangle,
|
||||
) -> event::Status {
|
||||
) {
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
let is_dragging = state.is_dragging;
|
||||
let current_value = self.value;
|
||||
|
|
@ -239,7 +286,7 @@ where
|
|||
let steps = (percent * (end - start) / step).round();
|
||||
let value = steps * step + start;
|
||||
|
||||
T::from_f64(value)
|
||||
T::from_f64(value.min(end))
|
||||
};
|
||||
|
||||
new_value
|
||||
|
|
@ -305,7 +352,7 @@ where
|
|||
state.is_dragging = true;
|
||||
}
|
||||
|
||||
return event::Status::Captured;
|
||||
shell.capture_event();
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
|
||||
|
|
@ -317,7 +364,7 @@ where
|
|||
}
|
||||
state.is_dragging = false;
|
||||
|
||||
return event::Status::Captured;
|
||||
shell.capture_event();
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse::Event::CursorMoved { .. })
|
||||
|
|
@ -325,11 +372,29 @@ where
|
|||
if is_dragging {
|
||||
let _ = cursor.position().and_then(locate).map(change);
|
||||
|
||||
return event::Status::Captured;
|
||||
shell.capture_event();
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse::Event::WheelScrolled { delta })
|
||||
if state.keyboard_modifiers.control() =>
|
||||
{
|
||||
if cursor.is_over(layout.bounds()) {
|
||||
let delta = match *delta {
|
||||
mouse::ScrollDelta::Lines { x: _, y } => y,
|
||||
mouse::ScrollDelta::Pixels { x: _, y } => y,
|
||||
};
|
||||
|
||||
if delta < 0.0 {
|
||||
let _ = decrement(current_value).map(change);
|
||||
} else {
|
||||
let _ = increment(current_value).map(change);
|
||||
}
|
||||
|
||||
shell.capture_event();
|
||||
}
|
||||
}
|
||||
Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => {
|
||||
if cursor.position_over(layout.bounds()).is_some() {
|
||||
if cursor.is_over(layout.bounds()) {
|
||||
match key {
|
||||
Key::Named(key::Named::ArrowUp) => {
|
||||
let _ = increment(current_value).map(change);
|
||||
|
|
@ -340,42 +405,44 @@ where
|
|||
_ => (),
|
||||
}
|
||||
|
||||
return event::Status::Captured;
|
||||
shell.capture_event();
|
||||
}
|
||||
}
|
||||
Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
|
||||
state.keyboard_modifiers = modifiers;
|
||||
state.keyboard_modifiers = *modifiers;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
event::Status::Ignored
|
||||
let current_status = if state.is_dragging {
|
||||
Status::Dragged
|
||||
} else if cursor.is_over(layout.bounds()) {
|
||||
Status::Hovered
|
||||
} else {
|
||||
Status::Active
|
||||
};
|
||||
|
||||
if let Event::Window(window::Event::RedrawRequested(_now)) = event {
|
||||
self.status = Some(current_status);
|
||||
} else if self.status.is_some_and(|status| status != current_status) {
|
||||
shell.request_redraw();
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
_tree: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Theme,
|
||||
_style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
_cursor: mouse::Cursor,
|
||||
_viewport: &Rectangle,
|
||||
) {
|
||||
let state = tree.state.downcast_ref::<State>();
|
||||
let bounds = layout.bounds();
|
||||
let is_mouse_over = cursor.is_over(bounds);
|
||||
|
||||
let style = theme.style(
|
||||
&self.class,
|
||||
if state.is_dragging {
|
||||
Status::Dragged
|
||||
} else if is_mouse_over {
|
||||
Status::Hovered
|
||||
} else {
|
||||
Status::Active
|
||||
},
|
||||
);
|
||||
let style =
|
||||
theme.style(&self.class, self.status.unwrap_or(Status::Active));
|
||||
|
||||
let (handle_width, handle_height, handle_border_radius) =
|
||||
match style.handle.shape {
|
||||
|
|
@ -412,10 +479,10 @@ where
|
|||
width: style.rail.width,
|
||||
height: offset + handle_width / 2.0,
|
||||
},
|
||||
border: Border::rounded(style.rail.border_radius),
|
||||
border: style.rail.border,
|
||||
..renderer::Quad::default()
|
||||
},
|
||||
style.rail.colors.1,
|
||||
style.rail.backgrounds.1,
|
||||
);
|
||||
|
||||
renderer.fill_quad(
|
||||
|
|
@ -426,10 +493,10 @@ where
|
|||
width: style.rail.width,
|
||||
height: bounds.height - offset - handle_width / 2.0,
|
||||
},
|
||||
border: Border::rounded(style.rail.border_radius),
|
||||
border: style.rail.border,
|
||||
..renderer::Quad::default()
|
||||
},
|
||||
style.rail.colors.0,
|
||||
style.rail.backgrounds.0,
|
||||
);
|
||||
|
||||
renderer.fill_quad(
|
||||
|
|
@ -447,7 +514,7 @@ where
|
|||
},
|
||||
..renderer::Quad::default()
|
||||
},
|
||||
style.handle.color,
|
||||
style.handle.background,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue