Create iced_widget subcrate and re-organize the whole codebase
This commit is contained in:
parent
c54409d171
commit
3a0d34c024
209 changed files with 1959 additions and 2183 deletions
|
|
@ -3,28 +3,6 @@ use iced_futures::MaybeSend;
|
|||
|
||||
use std::fmt;
|
||||
|
||||
/// A buffer for short-term storage and transfer within and between
|
||||
/// applications.
|
||||
pub trait Clipboard {
|
||||
/// Reads the current content of the [`Clipboard`] as text.
|
||||
fn read(&self) -> Option<String>;
|
||||
|
||||
/// Writes the given text contents to the [`Clipboard`].
|
||||
fn write(&mut self, contents: String);
|
||||
}
|
||||
|
||||
/// A null implementation of the [`Clipboard`] trait.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Null;
|
||||
|
||||
impl Clipboard for Null {
|
||||
fn read(&self) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
fn write(&mut self, _contents: String) {}
|
||||
}
|
||||
|
||||
/// A clipboard action to be performed by some [`Command`].
|
||||
///
|
||||
/// [`Command`]: crate::Command
|
||||
|
|
|
|||
|
|
@ -28,7 +28,9 @@ impl<T> Command<T> {
|
|||
}
|
||||
|
||||
/// Creates a [`Command`] that performs a [`widget::Operation`].
|
||||
pub fn widget(operation: impl widget::Operation<T> + 'static) -> Self {
|
||||
pub fn widget(
|
||||
operation: impl iced_core::widget::Operation<T> + 'static,
|
||||
) -> Self {
|
||||
Self(iced_futures::Command::single(Action::Widget(
|
||||
widget::Action::new(operation),
|
||||
)))
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#![allow(missing_docs)]
|
||||
use crate::time;
|
||||
use crate::core::time;
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,583 +0,0 @@
|
|||
use crate::event::{self, Event};
|
||||
use crate::layout;
|
||||
use crate::mouse;
|
||||
use crate::overlay;
|
||||
use crate::renderer;
|
||||
use crate::widget;
|
||||
use crate::widget::tree::{self, Tree};
|
||||
use crate::{
|
||||
Clipboard, Color, Layout, Length, Point, Rectangle, Shell, Widget,
|
||||
};
|
||||
|
||||
use std::any::Any;
|
||||
use std::borrow::Borrow;
|
||||
|
||||
/// A generic [`Widget`].
|
||||
///
|
||||
/// It is useful to build composable user interfaces that do not leak
|
||||
/// implementation details in their __view logic__.
|
||||
///
|
||||
/// If you have a [built-in widget], you should be able to use `Into<Element>`
|
||||
/// to turn it into an [`Element`].
|
||||
///
|
||||
/// [built-in widget]: crate::widget
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Element<'a, Message, Renderer> {
|
||||
widget: Box<dyn Widget<Message, Renderer> + 'a>,
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Element<'a, Message, Renderer> {
|
||||
/// Creates a new [`Element`] containing the given [`Widget`].
|
||||
pub fn new(widget: impl Widget<Message, Renderer> + 'a) -> Self
|
||||
where
|
||||
Renderer: crate::Renderer,
|
||||
{
|
||||
Self {
|
||||
widget: Box::new(widget),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a reference to the [`Widget`] of the [`Element`],
|
||||
pub fn as_widget(&self) -> &dyn Widget<Message, Renderer> {
|
||||
self.widget.as_ref()
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the [`Widget`] of the [`Element`],
|
||||
pub fn as_widget_mut(&mut self) -> &mut dyn Widget<Message, Renderer> {
|
||||
self.widget.as_mut()
|
||||
}
|
||||
|
||||
/// Applies a transformation to the produced message of the [`Element`].
|
||||
///
|
||||
/// This method is useful when you want to decouple different parts of your
|
||||
/// UI and make them __composable__.
|
||||
///
|
||||
/// # Example
|
||||
/// Imagine we want to use [our counter](index.html#usage). But instead of
|
||||
/// showing a single counter, we want to display many of them. We can reuse
|
||||
/// the `Counter` type as it is!
|
||||
///
|
||||
/// We use composition to model the __state__ of our new application:
|
||||
///
|
||||
/// ```
|
||||
/// # mod counter {
|
||||
/// # pub struct Counter;
|
||||
/// # }
|
||||
/// use counter::Counter;
|
||||
///
|
||||
/// struct ManyCounters {
|
||||
/// counters: Vec<Counter>,
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// We can store the state of multiple counters now. However, the
|
||||
/// __messages__ we implemented before describe the user interactions
|
||||
/// of a __single__ counter. Right now, we need to also identify which
|
||||
/// counter is receiving user interactions. Can we use composition again?
|
||||
/// Yes.
|
||||
///
|
||||
/// ```
|
||||
/// # mod counter {
|
||||
/// # #[derive(Debug, Clone, Copy)]
|
||||
/// # pub enum Message {}
|
||||
/// # }
|
||||
/// #[derive(Debug, Clone, Copy)]
|
||||
/// pub enum Message {
|
||||
/// Counter(usize, counter::Message)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// We compose the previous __messages__ with the index of the counter
|
||||
/// producing them. Let's implement our __view logic__ now:
|
||||
///
|
||||
/// ```
|
||||
/// # mod counter {
|
||||
/// # type Text<'a> = iced_native::widget::Text<'a, iced_native::renderer::Null>;
|
||||
/// #
|
||||
/// # #[derive(Debug, Clone, Copy)]
|
||||
/// # pub enum Message {}
|
||||
/// # pub struct Counter;
|
||||
/// #
|
||||
/// # impl Counter {
|
||||
/// # pub fn view(&mut self) -> Text {
|
||||
/// # Text::new("")
|
||||
/// # }
|
||||
/// # }
|
||||
/// # }
|
||||
/// #
|
||||
/// # mod iced_wgpu {
|
||||
/// # pub use iced_native::renderer::Null as Renderer;
|
||||
/// # }
|
||||
/// #
|
||||
/// # use counter::Counter;
|
||||
/// #
|
||||
/// # struct ManyCounters {
|
||||
/// # counters: Vec<Counter>,
|
||||
/// # }
|
||||
/// #
|
||||
/// # #[derive(Debug, Clone, Copy)]
|
||||
/// # pub enum Message {
|
||||
/// # Counter(usize, counter::Message)
|
||||
/// # }
|
||||
/// use iced_native::Element;
|
||||
/// use iced_native::widget::Row;
|
||||
/// use iced_wgpu::Renderer;
|
||||
///
|
||||
/// impl ManyCounters {
|
||||
/// pub fn view(&mut self) -> Row<Message, Renderer> {
|
||||
/// // We can quickly populate a `Row` by folding over our counters
|
||||
/// self.counters.iter_mut().enumerate().fold(
|
||||
/// Row::new().spacing(20),
|
||||
/// |row, (index, counter)| {
|
||||
/// // We display the counter
|
||||
/// let element: Element<counter::Message, Renderer> =
|
||||
/// counter.view().into();
|
||||
///
|
||||
/// row.push(
|
||||
/// // Here we turn our `Element<counter::Message>` into
|
||||
/// // an `Element<Message>` by combining the `index` and the
|
||||
/// // message of the `element`.
|
||||
/// element.map(move |message| Message::Counter(index, message))
|
||||
/// )
|
||||
/// }
|
||||
/// )
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Finally, our __update logic__ is pretty straightforward: simple
|
||||
/// delegation.
|
||||
///
|
||||
/// ```
|
||||
/// # mod counter {
|
||||
/// # #[derive(Debug, Clone, Copy)]
|
||||
/// # pub enum Message {}
|
||||
/// # pub struct Counter;
|
||||
/// #
|
||||
/// # impl Counter {
|
||||
/// # pub fn update(&mut self, _message: Message) {}
|
||||
/// # }
|
||||
/// # }
|
||||
/// #
|
||||
/// # use counter::Counter;
|
||||
/// #
|
||||
/// # struct ManyCounters {
|
||||
/// # counters: Vec<Counter>,
|
||||
/// # }
|
||||
/// #
|
||||
/// # #[derive(Debug, Clone, Copy)]
|
||||
/// # pub enum Message {
|
||||
/// # Counter(usize, counter::Message)
|
||||
/// # }
|
||||
/// impl ManyCounters {
|
||||
/// pub fn update(&mut self, message: Message) {
|
||||
/// match message {
|
||||
/// Message::Counter(index, counter_msg) => {
|
||||
/// if let Some(counter) = self.counters.get_mut(index) {
|
||||
/// counter.update(counter_msg);
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub fn map<B>(
|
||||
self,
|
||||
f: impl Fn(Message) -> B + 'a,
|
||||
) -> Element<'a, B, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Renderer: crate::Renderer + 'a,
|
||||
B: 'a,
|
||||
{
|
||||
Element::new(Map::new(self.widget, f))
|
||||
}
|
||||
|
||||
/// Marks the [`Element`] as _to-be-explained_.
|
||||
///
|
||||
/// The [`Renderer`] will explain the layout of the [`Element`] graphically.
|
||||
/// This can be very useful for debugging your layout!
|
||||
///
|
||||
/// [`Renderer`]: crate::Renderer
|
||||
pub fn explain<C: Into<Color>>(
|
||||
self,
|
||||
color: C,
|
||||
) -> Element<'a, Message, Renderer>
|
||||
where
|
||||
Message: 'static,
|
||||
Renderer: crate::Renderer + 'a,
|
||||
{
|
||||
Element {
|
||||
widget: Box::new(Explain::new(self, color.into())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Borrow<dyn Widget<Message, Renderer> + 'a>
|
||||
for Element<'a, Message, Renderer>
|
||||
{
|
||||
fn borrow(&self) -> &(dyn Widget<Message, Renderer> + 'a) {
|
||||
self.widget.borrow()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Borrow<dyn Widget<Message, Renderer> + 'a>
|
||||
for &Element<'a, Message, Renderer>
|
||||
{
|
||||
fn borrow(&self) -> &(dyn Widget<Message, Renderer> + 'a) {
|
||||
self.widget.borrow()
|
||||
}
|
||||
}
|
||||
|
||||
struct Map<'a, A, B, Renderer> {
|
||||
widget: Box<dyn Widget<A, Renderer> + 'a>,
|
||||
mapper: Box<dyn Fn(A) -> B + 'a>,
|
||||
}
|
||||
|
||||
impl<'a, A, B, Renderer> Map<'a, A, B, Renderer> {
|
||||
pub fn new<F>(
|
||||
widget: Box<dyn Widget<A, Renderer> + 'a>,
|
||||
mapper: F,
|
||||
) -> Map<'a, A, B, Renderer>
|
||||
where
|
||||
F: 'a + Fn(A) -> B,
|
||||
{
|
||||
Map {
|
||||
widget,
|
||||
mapper: Box::new(mapper),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, A, B, Renderer> Widget<B, Renderer> for Map<'a, A, B, Renderer>
|
||||
where
|
||||
Renderer: crate::Renderer + 'a,
|
||||
A: 'a,
|
||||
B: 'a,
|
||||
{
|
||||
fn tag(&self) -> tree::Tag {
|
||||
self.widget.tag()
|
||||
}
|
||||
|
||||
fn state(&self) -> tree::State {
|
||||
self.widget.state()
|
||||
}
|
||||
|
||||
fn children(&self) -> Vec<Tree> {
|
||||
self.widget.children()
|
||||
}
|
||||
|
||||
fn diff(&self, tree: &mut Tree) {
|
||||
self.widget.diff(tree)
|
||||
}
|
||||
|
||||
fn width(&self) -> Length {
|
||||
self.widget.width()
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
self.widget.height()
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
self.widget.layout(renderer, limits)
|
||||
}
|
||||
|
||||
fn operate(
|
||||
&self,
|
||||
tree: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn widget::Operation<B>,
|
||||
) {
|
||||
struct MapOperation<'a, B> {
|
||||
operation: &'a mut dyn widget::Operation<B>,
|
||||
}
|
||||
|
||||
impl<'a, T, B> widget::Operation<T> for MapOperation<'a, B> {
|
||||
fn container(
|
||||
&mut self,
|
||||
id: Option<&widget::Id>,
|
||||
operate_on_children: &mut dyn FnMut(
|
||||
&mut dyn widget::Operation<T>,
|
||||
),
|
||||
) {
|
||||
self.operation.container(id, &mut |operation| {
|
||||
operate_on_children(&mut MapOperation { operation });
|
||||
});
|
||||
}
|
||||
|
||||
fn focusable(
|
||||
&mut self,
|
||||
state: &mut dyn widget::operation::Focusable,
|
||||
id: Option<&widget::Id>,
|
||||
) {
|
||||
self.operation.focusable(state, id);
|
||||
}
|
||||
|
||||
fn scrollable(
|
||||
&mut self,
|
||||
state: &mut dyn widget::operation::Scrollable,
|
||||
id: Option<&widget::Id>,
|
||||
) {
|
||||
self.operation.scrollable(state, id);
|
||||
}
|
||||
|
||||
fn text_input(
|
||||
&mut self,
|
||||
state: &mut dyn widget::operation::TextInput,
|
||||
id: Option<&widget::Id>,
|
||||
) {
|
||||
self.operation.text_input(state, id);
|
||||
}
|
||||
|
||||
fn custom(&mut self, state: &mut dyn Any, id: Option<&widget::Id>) {
|
||||
self.operation.custom(state, id);
|
||||
}
|
||||
}
|
||||
|
||||
self.widget.operate(
|
||||
tree,
|
||||
layout,
|
||||
renderer,
|
||||
&mut MapOperation { operation },
|
||||
);
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, B>,
|
||||
) -> event::Status {
|
||||
let mut local_messages = Vec::new();
|
||||
let mut local_shell = Shell::new(&mut local_messages);
|
||||
|
||||
let status = self.widget.on_event(
|
||||
tree,
|
||||
event,
|
||||
layout,
|
||||
cursor_position,
|
||||
renderer,
|
||||
clipboard,
|
||||
&mut local_shell,
|
||||
);
|
||||
|
||||
shell.merge(local_shell, &self.mapper);
|
||||
|
||||
status
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
self.widget.draw(
|
||||
tree,
|
||||
renderer,
|
||||
theme,
|
||||
style,
|
||||
layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
)
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
self.widget.mouse_interaction(
|
||||
tree,
|
||||
layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
renderer,
|
||||
)
|
||||
}
|
||||
|
||||
fn overlay<'b>(
|
||||
&'b mut self,
|
||||
tree: &'b mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
) -> Option<overlay::Element<'b, B, Renderer>> {
|
||||
let mapper = &self.mapper;
|
||||
|
||||
self.widget
|
||||
.overlay(tree, layout, renderer)
|
||||
.map(move |overlay| overlay.map(mapper))
|
||||
}
|
||||
}
|
||||
|
||||
struct Explain<'a, Message, Renderer: crate::Renderer> {
|
||||
element: Element<'a, Message, Renderer>,
|
||||
color: Color,
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Explain<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: crate::Renderer,
|
||||
{
|
||||
fn new(element: Element<'a, Message, Renderer>, color: Color) -> Self {
|
||||
Explain { element, color }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Widget<Message, Renderer>
|
||||
for Explain<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: crate::Renderer,
|
||||
{
|
||||
fn width(&self) -> Length {
|
||||
self.element.widget.width()
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
self.element.widget.height()
|
||||
}
|
||||
|
||||
fn tag(&self) -> tree::Tag {
|
||||
self.element.widget.tag()
|
||||
}
|
||||
|
||||
fn state(&self) -> tree::State {
|
||||
self.element.widget.state()
|
||||
}
|
||||
|
||||
fn children(&self) -> Vec<Tree> {
|
||||
self.element.widget.children()
|
||||
}
|
||||
|
||||
fn diff(&self, tree: &mut Tree) {
|
||||
self.element.widget.diff(tree);
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
self.element.widget.layout(renderer, limits)
|
||||
}
|
||||
|
||||
fn operate(
|
||||
&self,
|
||||
state: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn widget::Operation<Message>,
|
||||
) {
|
||||
self.element
|
||||
.widget
|
||||
.operate(state, layout, renderer, operation)
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
state: &mut Tree,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
self.element.widget.on_event(
|
||||
state,
|
||||
event,
|
||||
layout,
|
||||
cursor_position,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
)
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
state: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
fn explain_layout<Renderer: crate::Renderer>(
|
||||
renderer: &mut Renderer,
|
||||
color: Color,
|
||||
layout: Layout<'_>,
|
||||
) {
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: layout.bounds(),
|
||||
border_color: color,
|
||||
border_width: 1.0,
|
||||
border_radius: 0.0.into(),
|
||||
},
|
||||
Color::TRANSPARENT,
|
||||
);
|
||||
|
||||
for child in layout.children() {
|
||||
explain_layout(renderer, color, child);
|
||||
}
|
||||
}
|
||||
|
||||
self.element.widget.draw(
|
||||
state,
|
||||
renderer,
|
||||
theme,
|
||||
style,
|
||||
layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
);
|
||||
|
||||
explain_layout(renderer, self.color, layout);
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
state: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
self.element.widget.mouse_interaction(
|
||||
state,
|
||||
layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
renderer,
|
||||
)
|
||||
}
|
||||
|
||||
fn overlay<'b>(
|
||||
&'b mut self,
|
||||
state: &'b mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
) -> Option<overlay::Element<'b, Message, Renderer>> {
|
||||
self.element.widget.overlay(state, layout, renderer)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
//! Handle events of a user interface.
|
||||
use crate::keyboard;
|
||||
use crate::mouse;
|
||||
use crate::touch;
|
||||
use crate::window;
|
||||
|
||||
/// A user interface event.
|
||||
///
|
||||
/// _**Note:** This type is largely incomplete! If you need to track
|
||||
/// additional events, feel free to [open an issue] and share your use case!_
|
||||
///
|
||||
/// [open an issue]: https://github.com/iced-rs/iced/issues
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Event {
|
||||
/// A keyboard event
|
||||
Keyboard(keyboard::Event),
|
||||
|
||||
/// A mouse event
|
||||
Mouse(mouse::Event),
|
||||
|
||||
/// A window event
|
||||
Window(window::Event),
|
||||
|
||||
/// A touch event
|
||||
Touch(touch::Event),
|
||||
|
||||
/// A platform specific event
|
||||
PlatformSpecific(PlatformSpecific),
|
||||
}
|
||||
|
||||
/// A platform specific event
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum PlatformSpecific {
|
||||
/// A MacOS specific event
|
||||
MacOS(MacOS),
|
||||
}
|
||||
|
||||
/// Describes an event specific to MacOS
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum MacOS {
|
||||
/// Triggered when the app receives an URL from the system
|
||||
///
|
||||
/// _**Note:** For this event to be triggered, the executable needs to be properly [bundled]!_
|
||||
///
|
||||
/// [bundled]: https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/BundleTypes/BundleTypes.html#//apple_ref/doc/uid/10000123i-CH101-SW19
|
||||
ReceivedUrl(String),
|
||||
}
|
||||
|
||||
/// The status of an [`Event`] after being processed.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Status {
|
||||
/// The [`Event`] was **NOT** handled by any widget.
|
||||
Ignored,
|
||||
|
||||
/// The [`Event`] was handled and processed by a widget.
|
||||
Captured,
|
||||
}
|
||||
|
||||
impl Status {
|
||||
/// Merges two [`Status`] into one.
|
||||
///
|
||||
/// `Captured` takes precedence over `Ignored`:
|
||||
///
|
||||
/// ```
|
||||
/// use iced_native::event::Status;
|
||||
///
|
||||
/// assert_eq!(Status::Ignored.merge(Status::Ignored), Status::Ignored);
|
||||
/// assert_eq!(Status::Ignored.merge(Status::Captured), Status::Captured);
|
||||
/// assert_eq!(Status::Captured.merge(Status::Ignored), Status::Captured);
|
||||
/// assert_eq!(Status::Captured.merge(Status::Captured), Status::Captured);
|
||||
/// ```
|
||||
pub fn merge(self, b: Self) -> Self {
|
||||
match self {
|
||||
Status::Ignored => b,
|
||||
Status::Captured => Status::Captured,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
/// The hasher used to compare layouts.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Hasher(twox_hash::XxHash64);
|
||||
|
||||
impl core::hash::Hasher for Hasher {
|
||||
fn write(&mut self, bytes: &[u8]) {
|
||||
self.0.write(bytes)
|
||||
}
|
||||
|
||||
fn finish(&self) -> u64 {
|
||||
self.0.finish()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,174 +0,0 @@
|
|||
//! Load and draw raster graphics.
|
||||
use crate::{Hasher, Rectangle, Size};
|
||||
|
||||
use std::hash::{Hash, Hasher as _};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// A handle of some image data.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Handle {
|
||||
id: u64,
|
||||
data: Data,
|
||||
}
|
||||
|
||||
impl Handle {
|
||||
/// Creates an image [`Handle`] pointing to the image of the given path.
|
||||
///
|
||||
/// Makes an educated guess about the image format by examining the data in the file.
|
||||
pub fn from_path<T: Into<PathBuf>>(path: T) -> Handle {
|
||||
Self::from_data(Data::Path(path.into()))
|
||||
}
|
||||
|
||||
/// Creates an image [`Handle`] containing the image pixels directly. This
|
||||
/// function expects the input data to be provided as a `Vec<u8>` of RGBA
|
||||
/// pixels.
|
||||
///
|
||||
/// This is useful if you have already decoded your image.
|
||||
pub fn from_pixels(
|
||||
width: u32,
|
||||
height: u32,
|
||||
pixels: impl AsRef<[u8]> + Send + Sync + 'static,
|
||||
) -> Handle {
|
||||
Self::from_data(Data::Rgba {
|
||||
width,
|
||||
height,
|
||||
pixels: Bytes::new(pixels),
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates an image [`Handle`] containing the image data directly.
|
||||
///
|
||||
/// Makes an educated guess about the image format by examining the given data.
|
||||
///
|
||||
/// This is useful if you already have your image loaded in-memory, maybe
|
||||
/// because you downloaded or generated it procedurally.
|
||||
pub fn from_memory(
|
||||
bytes: impl AsRef<[u8]> + Send + Sync + 'static,
|
||||
) -> Handle {
|
||||
Self::from_data(Data::Bytes(Bytes::new(bytes)))
|
||||
}
|
||||
|
||||
fn from_data(data: Data) -> Handle {
|
||||
let mut hasher = Hasher::default();
|
||||
data.hash(&mut hasher);
|
||||
|
||||
Handle {
|
||||
id: hasher.finish(),
|
||||
data,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the unique identifier of the [`Handle`].
|
||||
pub fn id(&self) -> u64 {
|
||||
self.id
|
||||
}
|
||||
|
||||
/// Returns a reference to the image [`Data`].
|
||||
pub fn data(&self) -> &Data {
|
||||
&self.data
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for Handle
|
||||
where
|
||||
T: Into<PathBuf>,
|
||||
{
|
||||
fn from(path: T) -> Handle {
|
||||
Handle::from_path(path.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for Handle {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.id.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper around raw image data.
|
||||
///
|
||||
/// It behaves like a `&[u8]`.
|
||||
#[derive(Clone)]
|
||||
pub struct Bytes(Arc<dyn AsRef<[u8]> + Send + Sync + 'static>);
|
||||
|
||||
impl Bytes {
|
||||
/// Creates new [`Bytes`] around `data`.
|
||||
pub fn new(data: impl AsRef<[u8]> + Send + Sync + 'static) -> Self {
|
||||
Self(Arc::new(data))
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Bytes {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.0.as_ref().as_ref().fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::hash::Hash for Bytes {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.0.as_ref().as_ref().hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for Bytes {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
self.0.as_ref().as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for Bytes {
|
||||
type Target = [u8];
|
||||
|
||||
fn deref(&self) -> &[u8] {
|
||||
self.0.as_ref().as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
/// The data of a raster image.
|
||||
#[derive(Clone, Hash)]
|
||||
pub enum Data {
|
||||
/// File data
|
||||
Path(PathBuf),
|
||||
|
||||
/// In-memory data
|
||||
Bytes(Bytes),
|
||||
|
||||
/// Decoded image pixels in RGBA format.
|
||||
Rgba {
|
||||
/// The width of the image.
|
||||
width: u32,
|
||||
/// The height of the image.
|
||||
height: u32,
|
||||
/// The pixels.
|
||||
pixels: Bytes,
|
||||
},
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Data {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Data::Path(path) => write!(f, "Path({path:?})"),
|
||||
Data::Bytes(_) => write!(f, "Bytes(...)"),
|
||||
Data::Rgba { width, height, .. } => {
|
||||
write!(f, "Pixels({width} * {height})")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`Renderer`] that can render raster graphics.
|
||||
///
|
||||
/// [renderer]: crate::renderer
|
||||
pub trait Renderer: crate::Renderer {
|
||||
/// The image Handle to be displayed. Iced exposes its own default implementation of a [`Handle`]
|
||||
///
|
||||
/// [`Handle`]: Self::Handle
|
||||
type Handle: Clone + Hash;
|
||||
|
||||
/// Returns the dimensions of an image for the given [`Handle`].
|
||||
fn dimensions(&self, handle: &Self::Handle) -> Size<u32>;
|
||||
|
||||
/// Draws an image with the given [`Handle`] and inside the provided
|
||||
/// `bounds`.
|
||||
fn draw(&mut self, handle: Self::Handle, bounds: Rectangle);
|
||||
}
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
//! Position your widgets properly.
|
||||
mod limits;
|
||||
mod node;
|
||||
|
||||
pub mod flex;
|
||||
|
||||
pub use limits::Limits;
|
||||
pub use node::Node;
|
||||
|
||||
use crate::{Point, Rectangle, Vector};
|
||||
|
||||
/// The bounds of a [`Node`] and its children, using absolute coordinates.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Layout<'a> {
|
||||
position: Point,
|
||||
node: &'a Node,
|
||||
}
|
||||
|
||||
impl<'a> Layout<'a> {
|
||||
/// Creates a new [`Layout`] for the given [`Node`] at the origin.
|
||||
pub fn new(node: &'a Node) -> Self {
|
||||
Self::with_offset(Vector::new(0.0, 0.0), node)
|
||||
}
|
||||
|
||||
/// Creates a new [`Layout`] for the given [`Node`] with the provided offset
|
||||
/// from the origin.
|
||||
pub fn with_offset(offset: Vector, node: &'a Node) -> Self {
|
||||
let bounds = node.bounds();
|
||||
|
||||
Self {
|
||||
position: Point::new(bounds.x, bounds.y) + offset,
|
||||
node,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the position of the [`Layout`].
|
||||
pub fn position(&self) -> Point {
|
||||
self.position
|
||||
}
|
||||
|
||||
/// Returns the bounds of the [`Layout`].
|
||||
///
|
||||
/// The returned [`Rectangle`] describes the position and size of a
|
||||
/// [`Node`].
|
||||
pub fn bounds(&self) -> Rectangle {
|
||||
let bounds = self.node.bounds();
|
||||
|
||||
Rectangle {
|
||||
x: self.position.x,
|
||||
y: self.position.y,
|
||||
width: bounds.width,
|
||||
height: bounds.height,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an iterator over the [`Layout`] of the children of a [`Node`].
|
||||
pub fn children(self) -> impl Iterator<Item = Layout<'a>> {
|
||||
self.node.children().iter().map(move |node| {
|
||||
Layout::with_offset(
|
||||
Vector::new(self.position.x, self.position.y),
|
||||
node,
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,202 +0,0 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
|
@ -1,232 +0,0 @@
|
|||
//! Distribute elements using a flex-based layout.
|
||||
// This code is heavily inspired by the [`druid`] codebase.
|
||||
//
|
||||
// [`druid`]: https://github.com/xi-editor/druid
|
||||
//
|
||||
// Copyright 2018 The xi-editor Authors, Héctor Ramón
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
use crate::Element;
|
||||
|
||||
use crate::layout::{Limits, Node};
|
||||
use crate::{Alignment, Padding, Point, Size};
|
||||
|
||||
/// The main axis of a flex layout.
|
||||
#[derive(Debug)]
|
||||
pub enum Axis {
|
||||
/// The horizontal axis
|
||||
Horizontal,
|
||||
|
||||
/// The vertical axis
|
||||
Vertical,
|
||||
}
|
||||
|
||||
impl Axis {
|
||||
fn main(&self, size: Size) -> f32 {
|
||||
match self {
|
||||
Axis::Horizontal => size.width,
|
||||
Axis::Vertical => size.height,
|
||||
}
|
||||
}
|
||||
|
||||
fn cross(&self, size: Size) -> f32 {
|
||||
match self {
|
||||
Axis::Horizontal => size.height,
|
||||
Axis::Vertical => size.width,
|
||||
}
|
||||
}
|
||||
|
||||
fn pack(&self, main: f32, cross: f32) -> (f32, f32) {
|
||||
match self {
|
||||
Axis::Horizontal => (main, cross),
|
||||
Axis::Vertical => (cross, main),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes the flex layout with the given axis and limits, applying spacing,
|
||||
/// padding and alignment to the items as needed.
|
||||
///
|
||||
/// It returns a new layout [`Node`].
|
||||
pub fn resolve<Message, Renderer>(
|
||||
axis: Axis,
|
||||
renderer: &Renderer,
|
||||
limits: &Limits,
|
||||
padding: Padding,
|
||||
spacing: f32,
|
||||
align_items: Alignment,
|
||||
items: &[Element<'_, Message, Renderer>],
|
||||
) -> Node
|
||||
where
|
||||
Renderer: crate::Renderer,
|
||||
{
|
||||
let limits = limits.pad(padding);
|
||||
let total_spacing = spacing * items.len().saturating_sub(1) as f32;
|
||||
let max_cross = axis.cross(limits.max());
|
||||
|
||||
let mut fill_sum = 0;
|
||||
let mut cross = axis.cross(limits.min()).max(axis.cross(limits.fill()));
|
||||
let mut available = axis.main(limits.max()) - total_spacing;
|
||||
|
||||
let mut nodes: Vec<Node> = Vec::with_capacity(items.len());
|
||||
nodes.resize(items.len(), Node::default());
|
||||
|
||||
if align_items == Alignment::Fill {
|
||||
let mut fill_cross = axis.cross(limits.min());
|
||||
|
||||
items.iter().for_each(|child| {
|
||||
let cross_fill_factor = match axis {
|
||||
Axis::Horizontal => child.as_widget().height(),
|
||||
Axis::Vertical => child.as_widget().width(),
|
||||
}
|
||||
.fill_factor();
|
||||
|
||||
if cross_fill_factor == 0 {
|
||||
let (max_width, max_height) = axis.pack(available, max_cross);
|
||||
|
||||
let child_limits =
|
||||
Limits::new(Size::ZERO, Size::new(max_width, max_height));
|
||||
|
||||
let layout = child.as_widget().layout(renderer, &child_limits);
|
||||
let size = layout.size();
|
||||
|
||||
fill_cross = fill_cross.max(axis.cross(size));
|
||||
}
|
||||
});
|
||||
|
||||
cross = fill_cross;
|
||||
}
|
||||
|
||||
for (i, child) in items.iter().enumerate() {
|
||||
let fill_factor = match axis {
|
||||
Axis::Horizontal => child.as_widget().width(),
|
||||
Axis::Vertical => child.as_widget().height(),
|
||||
}
|
||||
.fill_factor();
|
||||
|
||||
if fill_factor == 0 {
|
||||
let (min_width, min_height) = if align_items == Alignment::Fill {
|
||||
axis.pack(0.0, cross)
|
||||
} else {
|
||||
axis.pack(0.0, 0.0)
|
||||
};
|
||||
|
||||
let (max_width, max_height) = if align_items == Alignment::Fill {
|
||||
axis.pack(available, cross)
|
||||
} else {
|
||||
axis.pack(available, max_cross)
|
||||
};
|
||||
|
||||
let child_limits = Limits::new(
|
||||
Size::new(min_width, min_height),
|
||||
Size::new(max_width, max_height),
|
||||
);
|
||||
|
||||
let layout = child.as_widget().layout(renderer, &child_limits);
|
||||
let size = layout.size();
|
||||
|
||||
available -= axis.main(size);
|
||||
|
||||
if align_items != Alignment::Fill {
|
||||
cross = cross.max(axis.cross(size));
|
||||
}
|
||||
|
||||
nodes[i] = layout;
|
||||
} else {
|
||||
fill_sum += fill_factor;
|
||||
}
|
||||
}
|
||||
|
||||
let remaining = available.max(0.0);
|
||||
|
||||
for (i, child) in items.iter().enumerate() {
|
||||
let fill_factor = match axis {
|
||||
Axis::Horizontal => child.as_widget().width(),
|
||||
Axis::Vertical => child.as_widget().height(),
|
||||
}
|
||||
.fill_factor();
|
||||
|
||||
if fill_factor != 0 {
|
||||
let max_main = remaining * fill_factor as f32 / fill_sum as f32;
|
||||
let min_main = if max_main.is_infinite() {
|
||||
0.0
|
||||
} else {
|
||||
max_main
|
||||
};
|
||||
|
||||
let (min_width, min_height) = if align_items == Alignment::Fill {
|
||||
axis.pack(min_main, cross)
|
||||
} else {
|
||||
axis.pack(min_main, axis.cross(limits.min()))
|
||||
};
|
||||
|
||||
let (max_width, max_height) = if align_items == Alignment::Fill {
|
||||
axis.pack(max_main, cross)
|
||||
} else {
|
||||
axis.pack(max_main, max_cross)
|
||||
};
|
||||
|
||||
let child_limits = Limits::new(
|
||||
Size::new(min_width, min_height),
|
||||
Size::new(max_width, max_height),
|
||||
);
|
||||
|
||||
let layout = child.as_widget().layout(renderer, &child_limits);
|
||||
|
||||
if align_items != Alignment::Fill {
|
||||
cross = cross.max(axis.cross(layout.size()));
|
||||
}
|
||||
|
||||
nodes[i] = layout;
|
||||
}
|
||||
}
|
||||
|
||||
let pad = axis.pack(padding.left, padding.top);
|
||||
let mut main = pad.0;
|
||||
|
||||
for (i, node) in nodes.iter_mut().enumerate() {
|
||||
if i > 0 {
|
||||
main += spacing;
|
||||
}
|
||||
|
||||
let (x, y) = axis.pack(main, pad.1);
|
||||
|
||||
node.move_to(Point::new(x, y));
|
||||
|
||||
match axis {
|
||||
Axis::Horizontal => {
|
||||
node.align(
|
||||
Alignment::Start,
|
||||
align_items,
|
||||
Size::new(0.0, cross),
|
||||
);
|
||||
}
|
||||
Axis::Vertical => {
|
||||
node.align(
|
||||
align_items,
|
||||
Alignment::Start,
|
||||
Size::new(cross, 0.0),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let size = node.size();
|
||||
|
||||
main += axis.main(size);
|
||||
}
|
||||
|
||||
let (width, height) = axis.pack(main - pad.0, cross);
|
||||
let size = limits.resolve(Size::new(width, height));
|
||||
|
||||
Node::with_children(size.pad(padding), nodes)
|
||||
}
|
||||
|
|
@ -1,163 +0,0 @@
|
|||
#![allow(clippy::manual_clamp)]
|
||||
use crate::{Length, Padding, Size};
|
||||
|
||||
/// A set of size constraints for layouting.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Limits {
|
||||
min: Size,
|
||||
max: Size,
|
||||
fill: Size,
|
||||
}
|
||||
|
||||
impl Limits {
|
||||
/// No limits
|
||||
pub const NONE: Limits = Limits {
|
||||
min: Size::ZERO,
|
||||
max: Size::INFINITY,
|
||||
fill: Size::INFINITY,
|
||||
};
|
||||
|
||||
/// Creates new [`Limits`] with the given minimum and maximum [`Size`].
|
||||
pub const fn new(min: Size, max: Size) -> Limits {
|
||||
Limits {
|
||||
min,
|
||||
max,
|
||||
fill: Size::INFINITY,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the minimum [`Size`] of the [`Limits`].
|
||||
pub fn min(&self) -> Size {
|
||||
self.min
|
||||
}
|
||||
|
||||
/// Returns the maximum [`Size`] of the [`Limits`].
|
||||
pub fn max(&self) -> Size {
|
||||
self.max
|
||||
}
|
||||
|
||||
/// Returns the fill [`Size`] of the [`Limits`].
|
||||
pub fn fill(&self) -> Size {
|
||||
self.fill
|
||||
}
|
||||
|
||||
/// Applies a width constraint to the current [`Limits`].
|
||||
pub fn width(mut self, width: impl Into<Length>) -> Limits {
|
||||
match width.into() {
|
||||
Length::Shrink => {
|
||||
self.fill.width = self.min.width;
|
||||
}
|
||||
Length::Fill | Length::FillPortion(_) => {
|
||||
self.fill.width = self.fill.width.min(self.max.width);
|
||||
}
|
||||
Length::Fixed(amount) => {
|
||||
let new_width = amount.min(self.max.width).max(self.min.width);
|
||||
|
||||
self.min.width = new_width;
|
||||
self.max.width = new_width;
|
||||
self.fill.width = new_width;
|
||||
}
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Applies a height constraint to the current [`Limits`].
|
||||
pub fn height(mut self, height: impl Into<Length>) -> Limits {
|
||||
match height.into() {
|
||||
Length::Shrink => {
|
||||
self.fill.height = self.min.height;
|
||||
}
|
||||
Length::Fill | Length::FillPortion(_) => {
|
||||
self.fill.height = self.fill.height.min(self.max.height);
|
||||
}
|
||||
Length::Fixed(amount) => {
|
||||
let new_height =
|
||||
amount.min(self.max.height).max(self.min.height);
|
||||
|
||||
self.min.height = new_height;
|
||||
self.max.height = new_height;
|
||||
self.fill.height = new_height;
|
||||
}
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Applies a minimum width constraint to the current [`Limits`].
|
||||
pub fn min_width(mut self, min_width: f32) -> Limits {
|
||||
self.min.width = self.min.width.max(min_width).min(self.max.width);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Applies a maximum width constraint to the current [`Limits`].
|
||||
pub fn max_width(mut self, max_width: f32) -> Limits {
|
||||
self.max.width = self.max.width.min(max_width).max(self.min.width);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Applies a minimum height constraint to the current [`Limits`].
|
||||
pub fn min_height(mut self, min_height: f32) -> Limits {
|
||||
self.min.height = self.min.height.max(min_height).min(self.max.height);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Applies a maximum height constraint to the current [`Limits`].
|
||||
pub fn max_height(mut self, max_height: f32) -> Limits {
|
||||
self.max.height = self.max.height.min(max_height).max(self.min.height);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Shrinks the current [`Limits`] to account for the given padding.
|
||||
pub fn pad(&self, padding: Padding) -> Limits {
|
||||
self.shrink(Size::new(padding.horizontal(), padding.vertical()))
|
||||
}
|
||||
|
||||
/// Shrinks the current [`Limits`] by the given [`Size`].
|
||||
pub fn shrink(&self, size: Size) -> Limits {
|
||||
let min = Size::new(
|
||||
(self.min().width - size.width).max(0.0),
|
||||
(self.min().height - size.height).max(0.0),
|
||||
);
|
||||
|
||||
let max = Size::new(
|
||||
(self.max().width - size.width).max(0.0),
|
||||
(self.max().height - size.height).max(0.0),
|
||||
);
|
||||
|
||||
let fill = Size::new(
|
||||
(self.fill.width - size.width).max(0.0),
|
||||
(self.fill.height - size.height).max(0.0),
|
||||
);
|
||||
|
||||
Limits { min, max, fill }
|
||||
}
|
||||
|
||||
/// Removes the minimum width constraint for the current [`Limits`].
|
||||
pub fn loose(&self) -> Limits {
|
||||
Limits {
|
||||
min: Size::ZERO,
|
||||
max: self.max,
|
||||
fill: self.fill,
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes the resulting [`Size`] that fits the [`Limits`] given the
|
||||
/// intrinsic size of some content.
|
||||
pub fn resolve(&self, intrinsic_size: Size) -> Size {
|
||||
Size::new(
|
||||
intrinsic_size
|
||||
.width
|
||||
.min(self.max.width)
|
||||
.max(self.fill.width),
|
||||
intrinsic_size
|
||||
.height
|
||||
.min(self.max.height)
|
||||
.max(self.fill.height),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
use crate::{Alignment, Point, Rectangle, Size, Vector};
|
||||
|
||||
/// The bounds of an element and its children.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Node {
|
||||
bounds: Rectangle,
|
||||
children: Vec<Node>,
|
||||
}
|
||||
|
||||
impl Node {
|
||||
/// Creates a new [`Node`] with the given [`Size`].
|
||||
pub const fn new(size: Size) -> Self {
|
||||
Self::with_children(size, Vec::new())
|
||||
}
|
||||
|
||||
/// Creates a new [`Node`] with the given [`Size`] and children.
|
||||
pub const fn with_children(size: Size, children: Vec<Node>) -> Self {
|
||||
Node {
|
||||
bounds: Rectangle {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
},
|
||||
children,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the [`Size`] of the [`Node`].
|
||||
pub fn size(&self) -> Size {
|
||||
Size::new(self.bounds.width, self.bounds.height)
|
||||
}
|
||||
|
||||
/// Returns the bounds of the [`Node`].
|
||||
pub fn bounds(&self) -> Rectangle {
|
||||
self.bounds
|
||||
}
|
||||
|
||||
/// Returns the children of the [`Node`].
|
||||
pub fn children(&self) -> &[Node] {
|
||||
&self.children
|
||||
}
|
||||
|
||||
/// Aligns the [`Node`] in the given space.
|
||||
pub fn align(
|
||||
&mut self,
|
||||
horizontal_alignment: Alignment,
|
||||
vertical_alignment: Alignment,
|
||||
space: Size,
|
||||
) {
|
||||
match horizontal_alignment {
|
||||
Alignment::Start => {}
|
||||
Alignment::Center => {
|
||||
self.bounds.x += (space.width - self.bounds.width) / 2.0;
|
||||
}
|
||||
Alignment::End => {
|
||||
self.bounds.x += space.width - self.bounds.width;
|
||||
}
|
||||
Alignment::Fill => {
|
||||
self.bounds.width = space.width;
|
||||
}
|
||||
}
|
||||
|
||||
match vertical_alignment {
|
||||
Alignment::Start => {}
|
||||
Alignment::Center => {
|
||||
self.bounds.y += (space.height - self.bounds.height) / 2.0;
|
||||
}
|
||||
Alignment::End => {
|
||||
self.bounds.y += space.height - self.bounds.height;
|
||||
}
|
||||
Alignment::Fill => {
|
||||
self.bounds.height = space.height;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Moves the [`Node`] to the given position.
|
||||
pub fn move_to(&mut self, position: Point) {
|
||||
self.bounds.x = position.x;
|
||||
self.bounds.y = position.y;
|
||||
}
|
||||
|
||||
/// Translates the [`Node`] by the given translation.
|
||||
pub fn translate(self, translation: Vector) -> Self {
|
||||
Self {
|
||||
bounds: self.bounds + translation,
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -42,32 +42,19 @@
|
|||
clippy::useless_conversion
|
||||
)]
|
||||
#![forbid(unsafe_code, rust_2018_idioms)]
|
||||
#![allow(clippy::inherent_to_string, clippy::type_complexity)]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
pub mod clipboard;
|
||||
pub mod command;
|
||||
pub mod event;
|
||||
pub mod font;
|
||||
pub mod image;
|
||||
pub mod keyboard;
|
||||
pub mod layout;
|
||||
pub mod mouse;
|
||||
pub mod overlay;
|
||||
pub mod program;
|
||||
pub mod renderer;
|
||||
pub mod subscription;
|
||||
pub mod svg;
|
||||
pub mod system;
|
||||
pub mod text;
|
||||
pub mod touch;
|
||||
pub mod user_interface;
|
||||
pub mod widget;
|
||||
pub mod window;
|
||||
|
||||
mod element;
|
||||
mod hasher;
|
||||
mod runtime;
|
||||
mod shell;
|
||||
|
||||
// We disable debug capabilities on release builds unless the `debug` feature
|
||||
// is explicitly enabled.
|
||||
|
|
@ -78,35 +65,13 @@ mod debug;
|
|||
#[path = "debug/null.rs"]
|
||||
mod debug;
|
||||
|
||||
pub use iced_core::alignment;
|
||||
pub use iced_core::gradient;
|
||||
pub use iced_core::time;
|
||||
pub use iced_core::{
|
||||
color, Alignment, Background, Color, ContentFit, Length, Padding, Pixels,
|
||||
Point, Rectangle, Size, Vector,
|
||||
};
|
||||
pub use iced_futures::{executor, futures};
|
||||
pub use iced_style::application;
|
||||
pub use iced_style::theme;
|
||||
pub use iced_core as core;
|
||||
pub use iced_futures as futures;
|
||||
|
||||
#[doc(no_inline)]
|
||||
pub use executor::Executor;
|
||||
|
||||
pub use clipboard::Clipboard;
|
||||
pub use command::Command;
|
||||
pub use debug::Debug;
|
||||
pub use element::Element;
|
||||
pub use event::Event;
|
||||
pub use font::Font;
|
||||
pub use gradient::Gradient;
|
||||
pub use hasher::Hasher;
|
||||
pub use layout::Layout;
|
||||
pub use overlay::Overlay;
|
||||
pub use program::Program;
|
||||
pub use renderer::Renderer;
|
||||
pub use runtime::Runtime;
|
||||
pub use shell::Shell;
|
||||
pub use subscription::Subscription;
|
||||
pub use theme::Theme;
|
||||
pub use user_interface::UserInterface;
|
||||
pub use widget::Widget;
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
//! Track mouse events.
|
||||
|
||||
pub mod click;
|
||||
|
||||
pub use click::Click;
|
||||
pub use iced_core::mouse::*;
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
//! Track mouse clicks.
|
||||
use crate::time::Instant;
|
||||
use crate::Point;
|
||||
|
||||
/// A mouse click.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Click {
|
||||
kind: Kind,
|
||||
position: Point,
|
||||
time: Instant,
|
||||
}
|
||||
|
||||
/// The kind of mouse click.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Kind {
|
||||
/// A single click
|
||||
Single,
|
||||
|
||||
/// A double click
|
||||
Double,
|
||||
|
||||
/// A triple click
|
||||
Triple,
|
||||
}
|
||||
|
||||
impl Kind {
|
||||
fn next(&self) -> Kind {
|
||||
match self {
|
||||
Kind::Single => Kind::Double,
|
||||
Kind::Double => Kind::Triple,
|
||||
Kind::Triple => Kind::Double,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Click {
|
||||
/// Creates a new [`Click`] with the given position and previous last
|
||||
/// [`Click`].
|
||||
pub fn new(position: Point, previous: Option<Click>) -> Click {
|
||||
let time = Instant::now();
|
||||
|
||||
let kind = if let Some(previous) = previous {
|
||||
if previous.is_consecutive(position, time) {
|
||||
previous.kind.next()
|
||||
} else {
|
||||
Kind::Single
|
||||
}
|
||||
} else {
|
||||
Kind::Single
|
||||
};
|
||||
|
||||
Click {
|
||||
kind,
|
||||
position,
|
||||
time,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the [`Kind`] of [`Click`].
|
||||
pub fn kind(&self) -> Kind {
|
||||
self.kind
|
||||
}
|
||||
|
||||
fn is_consecutive(&self, new_position: Point, time: Instant) -> bool {
|
||||
let duration = if time > self.time {
|
||||
Some(time - self.time)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
self.position == new_position
|
||||
&& duration
|
||||
.map(|duration| duration.as_millis() <= 300)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,125 +0,0 @@
|
|||
//! Display interactive elements on top of other widgets.
|
||||
mod element;
|
||||
mod group;
|
||||
|
||||
pub mod menu;
|
||||
|
||||
pub use element::Element;
|
||||
pub use group::Group;
|
||||
pub use menu::Menu;
|
||||
|
||||
use crate::event::{self, Event};
|
||||
use crate::layout;
|
||||
use crate::mouse;
|
||||
use crate::renderer;
|
||||
use crate::widget;
|
||||
use crate::widget::Tree;
|
||||
use crate::{Clipboard, Layout, Point, Rectangle, Shell, Size};
|
||||
|
||||
/// An interactive component that can be displayed on top of other widgets.
|
||||
pub trait Overlay<Message, Renderer>
|
||||
where
|
||||
Renderer: crate::Renderer,
|
||||
{
|
||||
/// Returns the layout [`Node`] of the [`Overlay`].
|
||||
///
|
||||
/// This [`Node`] is used by the runtime to compute the [`Layout`] of the
|
||||
/// user interface.
|
||||
///
|
||||
/// [`Node`]: layout::Node
|
||||
fn layout(
|
||||
&self,
|
||||
renderer: &Renderer,
|
||||
bounds: Size,
|
||||
position: Point,
|
||||
) -> layout::Node;
|
||||
|
||||
/// Draws the [`Overlay`] using the associated `Renderer`.
|
||||
fn draw(
|
||||
&self,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
);
|
||||
|
||||
/// Applies a [`widget::Operation`] to the [`Overlay`].
|
||||
fn operate(
|
||||
&mut self,
|
||||
_layout: Layout<'_>,
|
||||
_renderer: &Renderer,
|
||||
_operation: &mut dyn widget::Operation<Message>,
|
||||
) {
|
||||
}
|
||||
|
||||
/// Processes a runtime [`Event`].
|
||||
///
|
||||
/// It receives:
|
||||
/// * an [`Event`] describing user interaction
|
||||
/// * the computed [`Layout`] of the [`Overlay`]
|
||||
/// * the current cursor position
|
||||
/// * a mutable `Message` list, allowing the [`Overlay`] to produce
|
||||
/// new messages based on user interaction.
|
||||
/// * the `Renderer`
|
||||
/// * a [`Clipboard`], if available
|
||||
///
|
||||
/// By default, it does nothing.
|
||||
fn on_event(
|
||||
&mut self,
|
||||
_event: Event,
|
||||
_layout: Layout<'_>,
|
||||
_cursor_position: Point,
|
||||
_renderer: &Renderer,
|
||||
_clipboard: &mut dyn Clipboard,
|
||||
_shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
event::Status::Ignored
|
||||
}
|
||||
|
||||
/// Returns the current [`mouse::Interaction`] of the [`Overlay`].
|
||||
///
|
||||
/// By default, it returns [`mouse::Interaction::Idle`].
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
_layout: Layout<'_>,
|
||||
_cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
_renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
mouse::Interaction::Idle
|
||||
}
|
||||
|
||||
/// Returns true if the cursor is over the [`Overlay`].
|
||||
///
|
||||
/// By default, it returns true if the bounds of the `layout` contain
|
||||
/// the `cursor_position`.
|
||||
fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool {
|
||||
layout.bounds().contains(cursor_position)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a [`Group`] of overlay [`Element`] children.
|
||||
///
|
||||
/// This method will generally only be used by advanced users that are
|
||||
/// implementing the [`Widget`](crate::Widget) trait.
|
||||
pub fn from_children<'a, Message, Renderer>(
|
||||
children: &'a mut [crate::Element<'_, Message, Renderer>],
|
||||
tree: &'a mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
) -> Option<Element<'a, Message, Renderer>>
|
||||
where
|
||||
Renderer: crate::Renderer,
|
||||
{
|
||||
let children = children
|
||||
.iter_mut()
|
||||
.zip(&mut tree.children)
|
||||
.zip(layout.children())
|
||||
.filter_map(|((child, state), layout)| {
|
||||
child.as_widget_mut().overlay(state, layout, renderer)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
(!children.is_empty()).then(|| Group::with_children(children).overlay())
|
||||
}
|
||||
|
|
@ -1,270 +0,0 @@
|
|||
pub use crate::Overlay;
|
||||
|
||||
use crate::event::{self, Event};
|
||||
use crate::layout;
|
||||
use crate::mouse;
|
||||
use crate::renderer;
|
||||
use crate::widget;
|
||||
use crate::{Clipboard, Layout, Point, Rectangle, Shell, Size, Vector};
|
||||
|
||||
use std::any::Any;
|
||||
|
||||
/// A generic [`Overlay`].
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Element<'a, Message, Renderer> {
|
||||
position: Point,
|
||||
overlay: Box<dyn Overlay<Message, Renderer> + 'a>,
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Element<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: crate::Renderer,
|
||||
{
|
||||
/// Creates a new [`Element`] containing the given [`Overlay`].
|
||||
pub fn new(
|
||||
position: Point,
|
||||
overlay: Box<dyn Overlay<Message, Renderer> + 'a>,
|
||||
) -> Self {
|
||||
Self { position, overlay }
|
||||
}
|
||||
|
||||
/// Returns the position of the [`Element`].
|
||||
pub fn position(&self) -> Point {
|
||||
self.position
|
||||
}
|
||||
|
||||
/// Translates the [`Element`].
|
||||
pub fn translate(mut self, translation: Vector) -> Self {
|
||||
self.position = self.position + translation;
|
||||
self
|
||||
}
|
||||
|
||||
/// Applies a transformation to the produced message of the [`Element`].
|
||||
pub fn map<B>(self, f: &'a dyn Fn(Message) -> B) -> Element<'a, B, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Renderer: 'a,
|
||||
B: 'a,
|
||||
{
|
||||
Element {
|
||||
position: self.position,
|
||||
overlay: Box::new(Map::new(self.overlay, f)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes the layout of the [`Element`] in the given bounds.
|
||||
pub fn layout(
|
||||
&self,
|
||||
renderer: &Renderer,
|
||||
bounds: Size,
|
||||
translation: Vector,
|
||||
) -> layout::Node {
|
||||
self.overlay
|
||||
.layout(renderer, bounds, self.position + translation)
|
||||
}
|
||||
|
||||
/// Processes a runtime [`Event`].
|
||||
pub fn on_event(
|
||||
&mut self,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
self.overlay.on_event(
|
||||
event,
|
||||
layout,
|
||||
cursor_position,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns the current [`mouse::Interaction`] of the [`Element`].
|
||||
pub fn mouse_interaction(
|
||||
&self,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
self.overlay.mouse_interaction(
|
||||
layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
renderer,
|
||||
)
|
||||
}
|
||||
|
||||
/// Draws the [`Element`] and its children using the given [`Layout`].
|
||||
pub fn draw(
|
||||
&self,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
) {
|
||||
self.overlay
|
||||
.draw(renderer, theme, style, layout, cursor_position)
|
||||
}
|
||||
|
||||
/// Applies a [`widget::Operation`] to the [`Element`].
|
||||
pub fn operate(
|
||||
&mut self,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn widget::Operation<Message>,
|
||||
) {
|
||||
self.overlay.operate(layout, renderer, operation);
|
||||
}
|
||||
|
||||
/// Returns true if the cursor is over the [`Element`].
|
||||
pub fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool {
|
||||
self.overlay.is_over(layout, cursor_position)
|
||||
}
|
||||
}
|
||||
|
||||
struct Map<'a, A, B, Renderer> {
|
||||
content: Box<dyn Overlay<A, Renderer> + 'a>,
|
||||
mapper: &'a dyn Fn(A) -> B,
|
||||
}
|
||||
|
||||
impl<'a, A, B, Renderer> Map<'a, A, B, Renderer> {
|
||||
pub fn new(
|
||||
content: Box<dyn Overlay<A, Renderer> + 'a>,
|
||||
mapper: &'a dyn Fn(A) -> B,
|
||||
) -> Map<'a, A, B, Renderer> {
|
||||
Map { content, mapper }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, A, B, Renderer> Overlay<B, Renderer> for Map<'a, A, B, Renderer>
|
||||
where
|
||||
Renderer: crate::Renderer,
|
||||
{
|
||||
fn layout(
|
||||
&self,
|
||||
renderer: &Renderer,
|
||||
bounds: Size,
|
||||
position: Point,
|
||||
) -> layout::Node {
|
||||
self.content.layout(renderer, bounds, position)
|
||||
}
|
||||
|
||||
fn operate(
|
||||
&mut self,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn widget::Operation<B>,
|
||||
) {
|
||||
struct MapOperation<'a, B> {
|
||||
operation: &'a mut dyn widget::Operation<B>,
|
||||
}
|
||||
|
||||
impl<'a, T, B> widget::Operation<T> for MapOperation<'a, B> {
|
||||
fn container(
|
||||
&mut self,
|
||||
id: Option<&widget::Id>,
|
||||
operate_on_children: &mut dyn FnMut(
|
||||
&mut dyn widget::Operation<T>,
|
||||
),
|
||||
) {
|
||||
self.operation.container(id, &mut |operation| {
|
||||
operate_on_children(&mut MapOperation { operation });
|
||||
});
|
||||
}
|
||||
|
||||
fn focusable(
|
||||
&mut self,
|
||||
state: &mut dyn widget::operation::Focusable,
|
||||
id: Option<&widget::Id>,
|
||||
) {
|
||||
self.operation.focusable(state, id);
|
||||
}
|
||||
|
||||
fn scrollable(
|
||||
&mut self,
|
||||
state: &mut dyn widget::operation::Scrollable,
|
||||
id: Option<&widget::Id>,
|
||||
) {
|
||||
self.operation.scrollable(state, id);
|
||||
}
|
||||
|
||||
fn text_input(
|
||||
&mut self,
|
||||
state: &mut dyn widget::operation::TextInput,
|
||||
id: Option<&widget::Id>,
|
||||
) {
|
||||
self.operation.text_input(state, id)
|
||||
}
|
||||
|
||||
fn custom(&mut self, state: &mut dyn Any, id: Option<&widget::Id>) {
|
||||
self.operation.custom(state, id);
|
||||
}
|
||||
}
|
||||
|
||||
self.content
|
||||
.operate(layout, renderer, &mut MapOperation { operation });
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, B>,
|
||||
) -> event::Status {
|
||||
let mut local_messages = Vec::new();
|
||||
let mut local_shell = Shell::new(&mut local_messages);
|
||||
|
||||
let event_status = self.content.on_event(
|
||||
event,
|
||||
layout,
|
||||
cursor_position,
|
||||
renderer,
|
||||
clipboard,
|
||||
&mut local_shell,
|
||||
);
|
||||
|
||||
shell.merge(local_shell, self.mapper);
|
||||
|
||||
event_status
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
self.content.mouse_interaction(
|
||||
layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
renderer,
|
||||
)
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
) {
|
||||
self.content
|
||||
.draw(renderer, theme, style, layout, cursor_position)
|
||||
}
|
||||
|
||||
fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool {
|
||||
self.content.is_over(layout, cursor_position)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,174 +0,0 @@
|
|||
use iced_core::{Point, Rectangle, Size};
|
||||
|
||||
use crate::event;
|
||||
use crate::layout;
|
||||
use crate::mouse;
|
||||
use crate::overlay;
|
||||
use crate::renderer;
|
||||
use crate::widget;
|
||||
use crate::{Clipboard, Event, Layout, Overlay, Shell};
|
||||
|
||||
/// An [`Overlay`] container that displays multiple overlay [`overlay::Element`]
|
||||
/// children.
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Group<'a, Message, Renderer> {
|
||||
children: Vec<overlay::Element<'a, Message, Renderer>>,
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Group<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: 'a + crate::Renderer,
|
||||
Message: 'a,
|
||||
{
|
||||
/// Creates an empty [`Group`].
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Creates a [`Group`] with the given elements.
|
||||
pub fn with_children(
|
||||
children: Vec<overlay::Element<'a, Message, Renderer>>,
|
||||
) -> Self {
|
||||
Group { children }
|
||||
}
|
||||
|
||||
/// Adds an [`overlay::Element`] to the [`Group`].
|
||||
pub fn push(
|
||||
mut self,
|
||||
child: impl Into<overlay::Element<'a, Message, Renderer>>,
|
||||
) -> Self {
|
||||
self.children.push(child.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Turns the [`Group`] into an overlay [`overlay::Element`].
|
||||
pub fn overlay(self) -> overlay::Element<'a, Message, Renderer> {
|
||||
overlay::Element::new(Point::ORIGIN, Box::new(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Default for Group<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: 'a + crate::Renderer,
|
||||
Message: 'a,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self::with_children(Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Overlay<Message, Renderer>
|
||||
for Group<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: crate::Renderer,
|
||||
{
|
||||
fn layout(
|
||||
&self,
|
||||
renderer: &Renderer,
|
||||
bounds: Size,
|
||||
position: Point,
|
||||
) -> layout::Node {
|
||||
let translation = position - Point::ORIGIN;
|
||||
|
||||
layout::Node::with_children(
|
||||
bounds,
|
||||
self.children
|
||||
.iter()
|
||||
.map(|child| child.layout(renderer, bounds, translation))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
self.children
|
||||
.iter_mut()
|
||||
.zip(layout.children())
|
||||
.map(|(child, layout)| {
|
||||
child.on_event(
|
||||
event.clone(),
|
||||
layout,
|
||||
cursor_position,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
)
|
||||
})
|
||||
.fold(event::Status::Ignored, event::Status::merge)
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
renderer: &mut Renderer,
|
||||
theme: &<Renderer as crate::Renderer>::Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
) {
|
||||
for (child, layout) in self.children.iter().zip(layout.children()) {
|
||||
child.draw(renderer, theme, style, layout, cursor_position);
|
||||
}
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
self.children
|
||||
.iter()
|
||||
.zip(layout.children())
|
||||
.map(|(child, layout)| {
|
||||
child.mouse_interaction(
|
||||
layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
renderer,
|
||||
)
|
||||
})
|
||||
.max()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn operate(
|
||||
&mut self,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn widget::Operation<Message>,
|
||||
) {
|
||||
operation.container(None, &mut |operation| {
|
||||
self.children.iter_mut().zip(layout.children()).for_each(
|
||||
|(child, layout)| {
|
||||
child.operate(layout, renderer, operation);
|
||||
},
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool {
|
||||
self.children
|
||||
.iter()
|
||||
.zip(layout.children())
|
||||
.any(|(child, layout)| child.is_over(layout, cursor_position))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> From<Group<'a, Message, Renderer>>
|
||||
for overlay::Element<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: 'a + crate::Renderer,
|
||||
Message: 'a,
|
||||
{
|
||||
fn from(group: Group<'a, Message, Renderer>) -> Self {
|
||||
group.overlay()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,519 +0,0 @@
|
|||
//! Build and show dropdown menus.
|
||||
use crate::alignment;
|
||||
use crate::event::{self, Event};
|
||||
use crate::layout;
|
||||
use crate::mouse;
|
||||
use crate::overlay;
|
||||
use crate::renderer;
|
||||
use crate::text::{self, Text};
|
||||
use crate::touch;
|
||||
use crate::widget::container::{self, Container};
|
||||
use crate::widget::scrollable::{self, Scrollable};
|
||||
use crate::widget::Tree;
|
||||
use crate::{
|
||||
Clipboard, Color, Element, Layout, Length, Padding, Pixels, Point,
|
||||
Rectangle, Shell, Size, Vector, Widget,
|
||||
};
|
||||
|
||||
pub use iced_style::menu::{Appearance, StyleSheet};
|
||||
|
||||
/// A list of selectable options.
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Menu<'a, T, Renderer>
|
||||
where
|
||||
Renderer: text::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
state: &'a mut State,
|
||||
options: &'a [T],
|
||||
hovered_option: &'a mut Option<usize>,
|
||||
last_selection: &'a mut Option<T>,
|
||||
width: f32,
|
||||
padding: Padding,
|
||||
text_size: Option<f32>,
|
||||
font: Option<Renderer::Font>,
|
||||
style: <Renderer::Theme as StyleSheet>::Style,
|
||||
}
|
||||
|
||||
impl<'a, T, Renderer> Menu<'a, T, Renderer>
|
||||
where
|
||||
T: ToString + Clone,
|
||||
Renderer: text::Renderer + 'a,
|
||||
Renderer::Theme:
|
||||
StyleSheet + container::StyleSheet + scrollable::StyleSheet,
|
||||
{
|
||||
/// Creates a new [`Menu`] with the given [`State`], a list of options, and
|
||||
/// the message to produced when an option is selected.
|
||||
pub fn new(
|
||||
state: &'a mut State,
|
||||
options: &'a [T],
|
||||
hovered_option: &'a mut Option<usize>,
|
||||
last_selection: &'a mut Option<T>,
|
||||
) -> Self {
|
||||
Menu {
|
||||
state,
|
||||
options,
|
||||
hovered_option,
|
||||
last_selection,
|
||||
width: 0.0,
|
||||
padding: Padding::ZERO,
|
||||
text_size: None,
|
||||
font: None,
|
||||
style: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the width of the [`Menu`].
|
||||
pub fn width(mut self, width: f32) -> Self {
|
||||
self.width = width;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`Padding`] of the [`Menu`].
|
||||
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
|
||||
self.padding = padding.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the text size of the [`Menu`].
|
||||
pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self {
|
||||
self.text_size = Some(text_size.into().0);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the font of the [`Menu`].
|
||||
pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
|
||||
self.font = Some(font.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the [`Menu`].
|
||||
pub fn style(
|
||||
mut self,
|
||||
style: impl Into<<Renderer::Theme as StyleSheet>::Style>,
|
||||
) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Turns the [`Menu`] into an overlay [`Element`] at the given target
|
||||
/// position.
|
||||
///
|
||||
/// The `target_height` will be used to display the menu either on top
|
||||
/// of the target or under it, depending on the screen position and the
|
||||
/// dimensions of the [`Menu`].
|
||||
pub fn overlay<Message: 'a>(
|
||||
self,
|
||||
position: Point,
|
||||
target_height: f32,
|
||||
) -> overlay::Element<'a, Message, Renderer> {
|
||||
overlay::Element::new(
|
||||
position,
|
||||
Box::new(Overlay::new(self, target_height)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// The local state of a [`Menu`].
|
||||
#[derive(Debug)]
|
||||
pub struct State {
|
||||
tree: Tree,
|
||||
}
|
||||
|
||||
impl State {
|
||||
/// Creates a new [`State`] for a [`Menu`].
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
tree: Tree::empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for State {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
struct Overlay<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: crate::Renderer,
|
||||
Renderer::Theme: StyleSheet + container::StyleSheet,
|
||||
{
|
||||
state: &'a mut Tree,
|
||||
container: Container<'a, Message, Renderer>,
|
||||
width: f32,
|
||||
target_height: f32,
|
||||
style: <Renderer::Theme as StyleSheet>::Style,
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Overlay<'a, Message, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Renderer: 'a,
|
||||
Renderer: text::Renderer,
|
||||
Renderer::Theme:
|
||||
StyleSheet + container::StyleSheet + scrollable::StyleSheet,
|
||||
{
|
||||
pub fn new<T>(menu: Menu<'a, T, Renderer>, target_height: f32) -> Self
|
||||
where
|
||||
T: Clone + ToString,
|
||||
{
|
||||
let Menu {
|
||||
state,
|
||||
options,
|
||||
hovered_option,
|
||||
last_selection,
|
||||
width,
|
||||
padding,
|
||||
font,
|
||||
text_size,
|
||||
style,
|
||||
} = menu;
|
||||
|
||||
let container = Container::new(Scrollable::new(List {
|
||||
options,
|
||||
hovered_option,
|
||||
last_selection,
|
||||
font,
|
||||
text_size,
|
||||
padding,
|
||||
style: style.clone(),
|
||||
}));
|
||||
|
||||
state.tree.diff(&container as &dyn Widget<_, _>);
|
||||
|
||||
Self {
|
||||
state: &mut state.tree,
|
||||
container,
|
||||
width,
|
||||
target_height,
|
||||
style,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> crate::Overlay<Message, Renderer>
|
||||
for Overlay<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: text::Renderer,
|
||||
Renderer::Theme: StyleSheet + container::StyleSheet,
|
||||
{
|
||||
fn layout(
|
||||
&self,
|
||||
renderer: &Renderer,
|
||||
bounds: Size,
|
||||
position: Point,
|
||||
) -> layout::Node {
|
||||
let space_below = bounds.height - (position.y + self.target_height);
|
||||
let space_above = position.y;
|
||||
|
||||
let limits = layout::Limits::new(
|
||||
Size::ZERO,
|
||||
Size::new(
|
||||
bounds.width - position.x,
|
||||
if space_below > space_above {
|
||||
space_below
|
||||
} else {
|
||||
space_above
|
||||
},
|
||||
),
|
||||
)
|
||||
.width(self.width);
|
||||
|
||||
let mut node = self.container.layout(renderer, &limits);
|
||||
|
||||
node.move_to(if space_below > space_above {
|
||||
position + Vector::new(0.0, self.target_height)
|
||||
} else {
|
||||
position - Vector::new(0.0, node.size().height)
|
||||
});
|
||||
|
||||
node
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
self.container.on_event(
|
||||
self.state,
|
||||
event,
|
||||
layout,
|
||||
cursor_position,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
)
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
self.container.mouse_interaction(
|
||||
self.state,
|
||||
layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
renderer,
|
||||
)
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
) {
|
||||
let appearance = theme.appearance(&self.style);
|
||||
let bounds = layout.bounds();
|
||||
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds,
|
||||
border_color: appearance.border_color,
|
||||
border_width: appearance.border_width,
|
||||
border_radius: appearance.border_radius.into(),
|
||||
},
|
||||
appearance.background,
|
||||
);
|
||||
|
||||
self.container.draw(
|
||||
self.state,
|
||||
renderer,
|
||||
theme,
|
||||
style,
|
||||
layout,
|
||||
cursor_position,
|
||||
&bounds,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
struct List<'a, T, Renderer>
|
||||
where
|
||||
Renderer: text::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
options: &'a [T],
|
||||
hovered_option: &'a mut Option<usize>,
|
||||
last_selection: &'a mut Option<T>,
|
||||
padding: Padding,
|
||||
text_size: Option<f32>,
|
||||
font: Option<Renderer::Font>,
|
||||
style: <Renderer::Theme as StyleSheet>::Style,
|
||||
}
|
||||
|
||||
impl<'a, T, Message, Renderer> Widget<Message, Renderer>
|
||||
for List<'a, T, Renderer>
|
||||
where
|
||||
T: Clone + ToString,
|
||||
Renderer: text::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
fn width(&self) -> Length {
|
||||
Length::Fill
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
Length::Shrink
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
use std::f32;
|
||||
|
||||
let limits = limits.width(Length::Fill).height(Length::Shrink);
|
||||
let text_size =
|
||||
self.text_size.unwrap_or_else(|| renderer.default_size());
|
||||
|
||||
let size = {
|
||||
let intrinsic = Size::new(
|
||||
0.0,
|
||||
(text_size * 1.2 + self.padding.vertical())
|
||||
* self.options.len() as f32,
|
||||
);
|
||||
|
||||
limits.resolve(intrinsic)
|
||||
};
|
||||
|
||||
layout::Node::new(size)
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
_state: &mut Tree,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
renderer: &Renderer,
|
||||
_clipboard: &mut dyn Clipboard,
|
||||
_shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
match event {
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
|
||||
let bounds = layout.bounds();
|
||||
|
||||
if bounds.contains(cursor_position) {
|
||||
if let Some(index) = *self.hovered_option {
|
||||
if let Some(option) = self.options.get(index) {
|
||||
*self.last_selection = Some(option.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse::Event::CursorMoved { .. }) => {
|
||||
let bounds = layout.bounds();
|
||||
|
||||
if bounds.contains(cursor_position) {
|
||||
let text_size = self
|
||||
.text_size
|
||||
.unwrap_or_else(|| renderer.default_size());
|
||||
|
||||
*self.hovered_option = Some(
|
||||
((cursor_position.y - bounds.y)
|
||||
/ (text_size * 1.2 + self.padding.vertical()))
|
||||
as usize,
|
||||
);
|
||||
}
|
||||
}
|
||||
Event::Touch(touch::Event::FingerPressed { .. }) => {
|
||||
let bounds = layout.bounds();
|
||||
|
||||
if bounds.contains(cursor_position) {
|
||||
let text_size = self
|
||||
.text_size
|
||||
.unwrap_or_else(|| renderer.default_size());
|
||||
|
||||
*self.hovered_option = Some(
|
||||
((cursor_position.y - bounds.y)
|
||||
/ (text_size * 1.2 + self.padding.vertical()))
|
||||
as usize,
|
||||
);
|
||||
|
||||
if let Some(index) = *self.hovered_option {
|
||||
if let Some(option) = self.options.get(index) {
|
||||
*self.last_selection = Some(option.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
event::Status::Ignored
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
_state: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
_renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
let is_mouse_over = layout.bounds().contains(cursor_position);
|
||||
|
||||
if is_mouse_over {
|
||||
mouse::Interaction::Pointer
|
||||
} else {
|
||||
mouse::Interaction::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
_state: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
_style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
_cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
let appearance = theme.appearance(&self.style);
|
||||
let bounds = layout.bounds();
|
||||
|
||||
let text_size =
|
||||
self.text_size.unwrap_or_else(|| renderer.default_size());
|
||||
let option_height =
|
||||
(text_size * 1.2 + self.padding.vertical()) as usize;
|
||||
|
||||
let offset = viewport.y - bounds.y;
|
||||
let start = (offset / option_height as f32) as usize;
|
||||
let end =
|
||||
((offset + viewport.height) / option_height as f32).ceil() as usize;
|
||||
|
||||
let visible_options = &self.options[start..end.min(self.options.len())];
|
||||
|
||||
for (i, option) in visible_options.iter().enumerate() {
|
||||
let i = start + i;
|
||||
let is_selected = *self.hovered_option == Some(i);
|
||||
|
||||
let bounds = Rectangle {
|
||||
x: bounds.x,
|
||||
y: bounds.y + (option_height * i) as f32,
|
||||
width: bounds.width,
|
||||
height: text_size * 1.2 + self.padding.vertical(),
|
||||
};
|
||||
|
||||
if is_selected {
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds,
|
||||
border_color: Color::TRANSPARENT,
|
||||
border_width: 0.0,
|
||||
border_radius: appearance.border_radius.into(),
|
||||
},
|
||||
appearance.selected_background,
|
||||
);
|
||||
}
|
||||
|
||||
renderer.fill_text(Text {
|
||||
content: &option.to_string(),
|
||||
bounds: Rectangle {
|
||||
x: bounds.x + self.padding.left,
|
||||
y: bounds.center_y(),
|
||||
width: f32::INFINITY,
|
||||
..bounds
|
||||
},
|
||||
size: text_size,
|
||||
font: self.font.unwrap_or_else(|| renderer.default_font()),
|
||||
color: if is_selected {
|
||||
appearance.selected_text_color
|
||||
} else {
|
||||
appearance.text_color
|
||||
},
|
||||
horizontal_alignment: alignment::Horizontal::Left,
|
||||
vertical_alignment: alignment::Vertical::Center,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T, Message, Renderer> From<List<'a, T, Renderer>>
|
||||
for Element<'a, Message, Renderer>
|
||||
where
|
||||
T: ToString + Clone,
|
||||
Message: 'a,
|
||||
Renderer: 'a + text::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
fn from(list: List<'a, T, Renderer>) -> Self {
|
||||
Element::new(list)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
//! Build interactive programs using The Elm Architecture.
|
||||
use crate::text;
|
||||
use crate::{Command, Element, Renderer};
|
||||
use crate::Command;
|
||||
|
||||
use iced_core::text;
|
||||
use iced_core::{Element, Renderer};
|
||||
|
||||
mod state;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
use crate::application;
|
||||
use crate::event::{self, Event};
|
||||
use crate::mouse;
|
||||
use crate::renderer;
|
||||
use crate::core::event::{self, Event};
|
||||
use crate::core::mouse;
|
||||
use crate::core::renderer;
|
||||
use crate::core::{Clipboard, Point, Size};
|
||||
use crate::user_interface::{self, UserInterface};
|
||||
use crate::{Clipboard, Command, Debug, Point, Program, Size};
|
||||
use crate::{Command, Debug, Program};
|
||||
|
||||
/// The execution state of a [`Program`]. It leverages caching, event
|
||||
/// processing, and rendering primitive storage.
|
||||
|
|
@ -22,7 +22,6 @@ where
|
|||
impl<P> State<P>
|
||||
where
|
||||
P: Program + 'static,
|
||||
<P::Renderer as crate::Renderer>::Theme: application::StyleSheet,
|
||||
{
|
||||
/// Creates a new [`State`] with the provided [`Program`], initializing its
|
||||
/// primitive with the given logical bounds and renderer.
|
||||
|
|
@ -91,7 +90,7 @@ where
|
|||
bounds: Size,
|
||||
cursor_position: Point,
|
||||
renderer: &mut P::Renderer,
|
||||
theme: &<P::Renderer as crate::Renderer>::Theme,
|
||||
theme: &<P::Renderer as iced_core::Renderer>::Theme,
|
||||
style: &renderer::Style,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
debug: &mut Debug,
|
||||
|
|
@ -182,10 +181,7 @@ fn build_user_interface<'a, P: Program>(
|
|||
renderer: &mut P::Renderer,
|
||||
size: Size,
|
||||
debug: &mut Debug,
|
||||
) -> UserInterface<'a, P::Message, P::Renderer>
|
||||
where
|
||||
<P::Renderer as crate::Renderer>::Theme: application::StyleSheet,
|
||||
{
|
||||
) -> UserInterface<'a, P::Message, P::Renderer> {
|
||||
debug.view_started();
|
||||
let view = program.view();
|
||||
debug.view_finished();
|
||||
|
|
|
|||
|
|
@ -1,98 +0,0 @@
|
|||
//! Write your own renderer.
|
||||
#[cfg(debug_assertions)]
|
||||
mod null;
|
||||
#[cfg(debug_assertions)]
|
||||
pub use null::Null;
|
||||
|
||||
use crate::layout;
|
||||
use crate::{Background, Color, Element, Rectangle, Vector};
|
||||
|
||||
/// A component that can be used by widgets to draw themselves on a screen.
|
||||
pub trait Renderer: Sized {
|
||||
/// The supported theme of the [`Renderer`].
|
||||
type Theme;
|
||||
|
||||
/// Lays out the elements of a user interface.
|
||||
///
|
||||
/// You should override this if you need to perform any operations before or
|
||||
/// after layouting. For instance, trimming the measurements cache.
|
||||
fn layout<Message>(
|
||||
&mut self,
|
||||
element: &Element<'_, Message, Self>,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
element.as_widget().layout(self, limits)
|
||||
}
|
||||
|
||||
/// Draws the primitives recorded in the given closure in a new layer.
|
||||
///
|
||||
/// The layer will clip its contents to the provided `bounds`.
|
||||
fn with_layer(&mut self, bounds: Rectangle, f: impl FnOnce(&mut Self));
|
||||
|
||||
/// Applies a `translation` to the primitives recorded in the given closure.
|
||||
fn with_translation(
|
||||
&mut self,
|
||||
translation: Vector,
|
||||
f: impl FnOnce(&mut Self),
|
||||
);
|
||||
|
||||
/// Fills a [`Quad`] with the provided [`Background`].
|
||||
fn fill_quad(&mut self, quad: Quad, background: impl Into<Background>);
|
||||
|
||||
/// Clears all of the recorded primitives in the [`Renderer`].
|
||||
fn clear(&mut self);
|
||||
}
|
||||
|
||||
/// A polygon with four sides.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Quad {
|
||||
/// The bounds of the [`Quad`].
|
||||
pub bounds: Rectangle,
|
||||
|
||||
/// The border radius of the [`Quad`].
|
||||
pub border_radius: BorderRadius,
|
||||
|
||||
/// The border width of the [`Quad`].
|
||||
pub border_width: f32,
|
||||
|
||||
/// The border color of the [`Quad`].
|
||||
pub border_color: Color,
|
||||
}
|
||||
|
||||
/// The border radi for the corners of a graphics primitive in the order:
|
||||
/// top-left, top-right, bottom-right, bottom-left.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||
pub struct BorderRadius([f32; 4]);
|
||||
|
||||
impl From<f32> for BorderRadius {
|
||||
fn from(w: f32) -> Self {
|
||||
Self([w; 4])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<[f32; 4]> for BorderRadius {
|
||||
fn from(radi: [f32; 4]) -> Self {
|
||||
Self(radi)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BorderRadius> for [f32; 4] {
|
||||
fn from(radi: BorderRadius) -> Self {
|
||||
radi.0
|
||||
}
|
||||
}
|
||||
|
||||
/// The styling attributes of a [`Renderer`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Style {
|
||||
/// The text color
|
||||
pub text_color: Color,
|
||||
}
|
||||
|
||||
impl Default for Style {
|
||||
fn default() -> Self {
|
||||
Style {
|
||||
text_color: Color::BLACK,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
use crate::renderer::{self, Renderer};
|
||||
use crate::text::{self, Text};
|
||||
use crate::{Background, Font, Point, Rectangle, Size, Theme, Vector};
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// A renderer that does nothing.
|
||||
///
|
||||
/// It can be useful if you are writing tests!
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct Null;
|
||||
|
||||
impl Null {
|
||||
/// Creates a new [`Null`] renderer.
|
||||
pub fn new() -> Null {
|
||||
Null
|
||||
}
|
||||
}
|
||||
|
||||
impl Renderer for Null {
|
||||
type Theme = Theme;
|
||||
|
||||
fn with_layer(&mut self, _bounds: Rectangle, _f: impl FnOnce(&mut Self)) {}
|
||||
|
||||
fn with_translation(
|
||||
&mut self,
|
||||
_translation: Vector,
|
||||
_f: impl FnOnce(&mut Self),
|
||||
) {
|
||||
}
|
||||
|
||||
fn clear(&mut self) {}
|
||||
|
||||
fn fill_quad(
|
||||
&mut self,
|
||||
_quad: renderer::Quad,
|
||||
_background: impl Into<Background>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
impl text::Renderer for Null {
|
||||
type Font = Font;
|
||||
|
||||
const ICON_FONT: Font = Font::SansSerif;
|
||||
const CHECKMARK_ICON: char = '0';
|
||||
const ARROW_DOWN_ICON: char = '0';
|
||||
|
||||
fn default_font(&self) -> Self::Font {
|
||||
Font::SansSerif
|
||||
}
|
||||
|
||||
fn default_size(&self) -> f32 {
|
||||
16.0
|
||||
}
|
||||
|
||||
fn load_font(&mut self, _font: Cow<'static, [u8]>) {}
|
||||
|
||||
fn measure(
|
||||
&self,
|
||||
_content: &str,
|
||||
_size: f32,
|
||||
_font: Font,
|
||||
_bounds: Size,
|
||||
) -> (f32, f32) {
|
||||
(0.0, 20.0)
|
||||
}
|
||||
|
||||
fn hit_test(
|
||||
&self,
|
||||
_contents: &str,
|
||||
_size: f32,
|
||||
_font: Self::Font,
|
||||
_bounds: Size,
|
||||
_point: Point,
|
||||
_nearest_only: bool,
|
||||
) -> Option<text::Hit> {
|
||||
None
|
||||
}
|
||||
|
||||
fn fill_text(&mut self, _text: Text<'_, Self::Font>) {}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
//! Run commands and subscriptions.
|
||||
use crate::event::{self, Event};
|
||||
use crate::Hasher;
|
||||
use iced_core::event::{self, Event};
|
||||
use iced_core::Hasher;
|
||||
|
||||
/// A native runtime with a generic executor and receiver of results.
|
||||
///
|
||||
|
|
|
|||
|
|
@ -1,108 +0,0 @@
|
|||
use crate::window;
|
||||
|
||||
/// A connection to the state of a shell.
|
||||
///
|
||||
/// A [`Widget`] can leverage a [`Shell`] to trigger changes in an application,
|
||||
/// like publishing messages or invalidating the current layout.
|
||||
///
|
||||
/// [`Widget`]: crate::Widget
|
||||
#[derive(Debug)]
|
||||
pub struct Shell<'a, Message> {
|
||||
messages: &'a mut Vec<Message>,
|
||||
redraw_request: Option<window::RedrawRequest>,
|
||||
is_layout_invalid: bool,
|
||||
are_widgets_invalid: bool,
|
||||
}
|
||||
|
||||
impl<'a, Message> Shell<'a, Message> {
|
||||
/// Creates a new [`Shell`] with the provided buffer of messages.
|
||||
pub fn new(messages: &'a mut Vec<Message>) -> Self {
|
||||
Self {
|
||||
messages,
|
||||
redraw_request: None,
|
||||
is_layout_invalid: false,
|
||||
are_widgets_invalid: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the [`Shell`] contains no published messages
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.messages.is_empty()
|
||||
}
|
||||
|
||||
/// Publish the given `Message` for an application to process it.
|
||||
pub fn publish(&mut self, message: Message) {
|
||||
self.messages.push(message);
|
||||
}
|
||||
|
||||
/// Requests a new frame to be drawn at the given [`Instant`].
|
||||
pub fn request_redraw(&mut self, request: window::RedrawRequest) {
|
||||
match self.redraw_request {
|
||||
None => {
|
||||
self.redraw_request = Some(request);
|
||||
}
|
||||
Some(current) if request < current => {
|
||||
self.redraw_request = Some(request);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the requested [`Instant`] a redraw should happen, if any.
|
||||
pub fn redraw_request(&self) -> Option<window::RedrawRequest> {
|
||||
self.redraw_request
|
||||
}
|
||||
|
||||
/// Returns whether the current layout is invalid or not.
|
||||
pub fn is_layout_invalid(&self) -> bool {
|
||||
self.is_layout_invalid
|
||||
}
|
||||
|
||||
/// Invalidates the current application layout.
|
||||
///
|
||||
/// The shell will relayout the application widgets.
|
||||
pub fn invalidate_layout(&mut self) {
|
||||
self.is_layout_invalid = true;
|
||||
}
|
||||
|
||||
/// Triggers the given function if the layout is invalid, cleaning it in the
|
||||
/// process.
|
||||
pub fn revalidate_layout(&mut self, f: impl FnOnce()) {
|
||||
if self.is_layout_invalid {
|
||||
self.is_layout_invalid = false;
|
||||
|
||||
f()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether the widgets of the current application have been
|
||||
/// invalidated.
|
||||
pub fn are_widgets_invalid(&self) -> bool {
|
||||
self.are_widgets_invalid
|
||||
}
|
||||
|
||||
/// Invalidates the current application widgets.
|
||||
///
|
||||
/// The shell will rebuild and relayout the widget tree.
|
||||
pub fn invalidate_widgets(&mut self) {
|
||||
self.are_widgets_invalid = true;
|
||||
}
|
||||
|
||||
/// Merges the current [`Shell`] with another one by applying the given
|
||||
/// function to the messages of the latter.
|
||||
///
|
||||
/// This method is useful for composition.
|
||||
pub fn merge<B>(&mut self, other: Shell<'_, B>, f: impl Fn(B) -> Message) {
|
||||
self.messages.extend(other.messages.drain(..).map(f));
|
||||
|
||||
if let Some(at) = other.redraw_request {
|
||||
self.request_redraw(at);
|
||||
}
|
||||
|
||||
self.is_layout_invalid =
|
||||
self.is_layout_invalid || other.is_layout_invalid;
|
||||
|
||||
self.are_widgets_invalid =
|
||||
self.are_widgets_invalid || other.are_widgets_invalid;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,9 @@
|
|||
//! Listen to external events in your application.
|
||||
use crate::event::{self, Event};
|
||||
use crate::window;
|
||||
use crate::Hasher;
|
||||
|
||||
use iced_futures::futures::{self, Future, Stream};
|
||||
use iced_futures::{BoxStream, MaybeSend};
|
||||
use crate::core::event::{self, Event};
|
||||
use crate::core::window;
|
||||
use crate::core::Hasher;
|
||||
use crate::futures::futures::{self, Future, Stream};
|
||||
use crate::futures::{BoxStream, MaybeSend};
|
||||
|
||||
use std::hash::Hash;
|
||||
|
||||
|
|
@ -144,7 +143,9 @@ where
|
|||
///
|
||||
/// ```
|
||||
/// use iced_native::subscription::{self, Subscription};
|
||||
/// use iced_native::futures::channel::mpsc;
|
||||
/// use iced_native::futures::futures;
|
||||
///
|
||||
/// use futures::channel::mpsc;
|
||||
///
|
||||
/// pub enum Event {
|
||||
/// Ready(mpsc::Sender<Input>),
|
||||
|
|
@ -174,7 +175,7 @@ where
|
|||
/// (Some(Event::Ready(sender)), State::Ready(receiver))
|
||||
/// }
|
||||
/// State::Ready(mut receiver) => {
|
||||
/// use iced_native::futures::StreamExt;
|
||||
/// use futures::StreamExt;
|
||||
///
|
||||
/// // Read next input sent from `Application`
|
||||
/// let input = receiver.select_next_some().await;
|
||||
|
|
|
|||
|
|
@ -1,89 +0,0 @@
|
|||
//! Load and draw vector graphics.
|
||||
use crate::{Color, Hasher, Rectangle, Size};
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::hash::{Hash, Hasher as _};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// A handle of Svg data.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Handle {
|
||||
id: u64,
|
||||
data: Arc<Data>,
|
||||
}
|
||||
|
||||
impl Handle {
|
||||
/// Creates an SVG [`Handle`] pointing to the vector image of the given
|
||||
/// path.
|
||||
pub fn from_path(path: impl Into<PathBuf>) -> Handle {
|
||||
Self::from_data(Data::Path(path.into()))
|
||||
}
|
||||
|
||||
/// Creates an SVG [`Handle`] from raw bytes containing either an SVG string
|
||||
/// or gzip compressed data.
|
||||
///
|
||||
/// This is useful if you already have your SVG data in-memory, maybe
|
||||
/// because you downloaded or generated it procedurally.
|
||||
pub fn from_memory(bytes: impl Into<Cow<'static, [u8]>>) -> Handle {
|
||||
Self::from_data(Data::Bytes(bytes.into()))
|
||||
}
|
||||
|
||||
fn from_data(data: Data) -> Handle {
|
||||
let mut hasher = Hasher::default();
|
||||
data.hash(&mut hasher);
|
||||
|
||||
Handle {
|
||||
id: hasher.finish(),
|
||||
data: Arc::new(data),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the unique identifier of the [`Handle`].
|
||||
pub fn id(&self) -> u64 {
|
||||
self.id
|
||||
}
|
||||
|
||||
/// Returns a reference to the SVG [`Data`].
|
||||
pub fn data(&self) -> &Data {
|
||||
&self.data
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for Handle {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.id.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
/// The data of a vectorial image.
|
||||
#[derive(Clone, Hash)]
|
||||
pub enum Data {
|
||||
/// File data
|
||||
Path(PathBuf),
|
||||
|
||||
/// In-memory data
|
||||
///
|
||||
/// Can contain an SVG string or a gzip compressed data.
|
||||
Bytes(Cow<'static, [u8]>),
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Data {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Data::Path(path) => write!(f, "Path({path:?})"),
|
||||
Data::Bytes(_) => write!(f, "Bytes(...)"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`Renderer`] that can render vector graphics.
|
||||
///
|
||||
/// [renderer]: crate::renderer
|
||||
pub trait Renderer: crate::Renderer {
|
||||
/// Returns the default dimensions of an SVG for the given [`Handle`].
|
||||
fn dimensions(&self, handle: &Handle) -> Size<u32>;
|
||||
|
||||
/// Draws an SVG with the given [`Handle`], an optional [`Color`] filter, and inside the provided `bounds`.
|
||||
fn draw(&mut self, handle: Handle, color: Option<Color>, bounds: Rectangle);
|
||||
}
|
||||
|
|
@ -1,111 +0,0 @@
|
|||
//! Draw and interact with text.
|
||||
use crate::alignment;
|
||||
use crate::{Color, Point, Rectangle, Size};
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// A paragraph.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Text<'a, Font> {
|
||||
/// The content of the paragraph.
|
||||
pub content: &'a str,
|
||||
|
||||
/// The bounds of the paragraph.
|
||||
pub bounds: Rectangle,
|
||||
|
||||
/// The size of the [`Text`].
|
||||
pub size: f32,
|
||||
|
||||
/// The color of the [`Text`].
|
||||
pub color: Color,
|
||||
|
||||
/// The font of the [`Text`].
|
||||
pub font: Font,
|
||||
|
||||
/// The horizontal alignment of the [`Text`].
|
||||
pub horizontal_alignment: alignment::Horizontal,
|
||||
|
||||
/// The vertical alignment of the [`Text`].
|
||||
pub vertical_alignment: alignment::Vertical,
|
||||
}
|
||||
|
||||
/// The result of hit testing on text.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum Hit {
|
||||
/// The point was within the bounds of the returned character index.
|
||||
CharOffset(usize),
|
||||
}
|
||||
|
||||
impl Hit {
|
||||
/// Computes the cursor position of the [`Hit`] .
|
||||
pub fn cursor(self) -> usize {
|
||||
match self {
|
||||
Self::CharOffset(i) => i,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A renderer capable of measuring and drawing [`Text`].
|
||||
pub trait Renderer: crate::Renderer {
|
||||
/// The font type used.
|
||||
type Font: Copy;
|
||||
|
||||
/// The icon font of the backend.
|
||||
const ICON_FONT: Self::Font;
|
||||
|
||||
/// The `char` representing a ✔ icon in the [`ICON_FONT`].
|
||||
///
|
||||
/// [`ICON_FONT`]: Self::ICON_FONT
|
||||
const CHECKMARK_ICON: char;
|
||||
|
||||
/// The `char` representing a ▼ icon in the built-in [`ICON_FONT`].
|
||||
///
|
||||
/// [`ICON_FONT`]: Self::ICON_FONT
|
||||
const ARROW_DOWN_ICON: char;
|
||||
|
||||
/// Returns the default [`Self::Font`].
|
||||
fn default_font(&self) -> Self::Font;
|
||||
|
||||
/// Returns the default size of [`Text`].
|
||||
fn default_size(&self) -> f32;
|
||||
|
||||
/// Measures the text in the given bounds and returns the minimum boundaries
|
||||
/// that can fit the contents.
|
||||
fn measure(
|
||||
&self,
|
||||
content: &str,
|
||||
size: f32,
|
||||
font: Self::Font,
|
||||
bounds: Size,
|
||||
) -> (f32, f32);
|
||||
|
||||
/// Measures the width of the text as if it were laid out in a single line.
|
||||
fn measure_width(&self, content: &str, size: f32, font: Self::Font) -> f32 {
|
||||
let (width, _) = self.measure(content, size, font, Size::INFINITY);
|
||||
|
||||
width
|
||||
}
|
||||
|
||||
/// Tests whether the provided point is within the boundaries of text
|
||||
/// laid out with the given parameters, returning information about
|
||||
/// the nearest character.
|
||||
///
|
||||
/// If `nearest_only` is true, the hit test does not consider whether the
|
||||
/// the point is interior to any glyph bounds, returning only the character
|
||||
/// with the nearest centeroid.
|
||||
fn hit_test(
|
||||
&self,
|
||||
contents: &str,
|
||||
size: f32,
|
||||
font: Self::Font,
|
||||
bounds: Size,
|
||||
point: Point,
|
||||
nearest_only: bool,
|
||||
) -> Option<Hit>;
|
||||
|
||||
/// Loads a [`Self::Font`] from its bytes.
|
||||
fn load_font(&mut self, font: Cow<'static, [u8]>);
|
||||
|
||||
/// Draws the given [`Text`].
|
||||
fn fill_text(&mut self, text: Text<'_, Self::Font>);
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
//! Build touch events.
|
||||
use crate::Point;
|
||||
|
||||
/// A touch interaction.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
#[allow(missing_docs)]
|
||||
pub enum Event {
|
||||
/// A touch interaction was started.
|
||||
FingerPressed { id: Finger, position: Point },
|
||||
|
||||
/// An on-going touch interaction was moved.
|
||||
FingerMoved { id: Finger, position: Point },
|
||||
|
||||
/// A touch interaction was ended.
|
||||
FingerLifted { id: Finger, position: Point },
|
||||
|
||||
/// A touch interaction was canceled.
|
||||
FingerLost { id: Finger, position: Point },
|
||||
}
|
||||
|
||||
/// A unique identifier representing a finger on a touch interaction.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct Finger(pub u64);
|
||||
|
|
@ -1,14 +1,12 @@
|
|||
//! Implement your own event loop to drive a user interface.
|
||||
use crate::application;
|
||||
use crate::event::{self, Event};
|
||||
use crate::layout;
|
||||
use crate::mouse;
|
||||
use crate::renderer;
|
||||
use crate::widget;
|
||||
use crate::window;
|
||||
use crate::{
|
||||
Clipboard, Element, Layout, Point, Rectangle, Shell, Size, Vector,
|
||||
};
|
||||
use crate::core::event::{self, Event};
|
||||
use crate::core::layout;
|
||||
use crate::core::mouse;
|
||||
use crate::core::renderer;
|
||||
use crate::core::widget;
|
||||
use crate::core::window;
|
||||
use crate::core::{Clipboard, Point, Rectangle, Size, Vector};
|
||||
use crate::core::{Element, Layout, Shell};
|
||||
|
||||
/// A set of interactive graphical elements with a specific [`Layout`].
|
||||
///
|
||||
|
|
@ -34,8 +32,7 @@ pub struct UserInterface<'a, Message, Renderer> {
|
|||
|
||||
impl<'a, Message, Renderer> UserInterface<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: crate::Renderer,
|
||||
Renderer::Theme: application::StyleSheet,
|
||||
Renderer: iced_core::Renderer,
|
||||
{
|
||||
/// Builds a user interface for an [`Element`].
|
||||
///
|
||||
|
|
@ -48,24 +45,21 @@ where
|
|||
/// is naive way to set up our application loop:
|
||||
///
|
||||
/// ```no_run
|
||||
/// use iced_native::Size;
|
||||
/// use iced_native::user_interface::{self, UserInterface};
|
||||
/// use iced_wgpu::Renderer;
|
||||
///
|
||||
/// # mod iced_wgpu {
|
||||
/// # pub use iced_native::renderer::Null as Renderer;
|
||||
/// # pub use iced_native::core::renderer::Null as Renderer;
|
||||
/// # }
|
||||
/// #
|
||||
/// # use iced_native::widget::Column;
|
||||
/// #
|
||||
/// # pub struct Counter;
|
||||
/// #
|
||||
/// # impl Counter {
|
||||
/// # pub fn new() -> Self { Counter }
|
||||
/// # pub fn view(&self) -> Column<(), Renderer> {
|
||||
/// # Column::new()
|
||||
/// # }
|
||||
/// # pub fn view(&self) -> iced_core::Element<(), Renderer> { unimplemented!() }
|
||||
/// # pub fn update(&mut self, _: ()) {}
|
||||
/// # }
|
||||
/// use iced_native::core::Size;
|
||||
/// use iced_native::user_interface::{self, UserInterface};
|
||||
/// use iced_wgpu::Renderer;
|
||||
///
|
||||
/// // Initialization
|
||||
/// let mut counter = Counter::new();
|
||||
/// let mut cache = user_interface::Cache::new();
|
||||
|
|
@ -124,25 +118,21 @@ where
|
|||
/// completing [the previous example](#example):
|
||||
///
|
||||
/// ```no_run
|
||||
/// use iced_native::{clipboard, Size, Point};
|
||||
/// use iced_native::user_interface::{self, UserInterface};
|
||||
/// use iced_wgpu::Renderer;
|
||||
///
|
||||
/// # mod iced_wgpu {
|
||||
/// # pub use iced_native::renderer::Null as Renderer;
|
||||
/// # pub use iced_native::core::renderer::Null as Renderer;
|
||||
/// # }
|
||||
/// #
|
||||
/// # use iced_native::widget::Column;
|
||||
/// #
|
||||
/// # pub struct Counter;
|
||||
/// #
|
||||
/// # impl Counter {
|
||||
/// # pub fn new() -> Self { Counter }
|
||||
/// # pub fn view(&self) -> Column<(), Renderer> {
|
||||
/// # Column::new()
|
||||
/// # }
|
||||
/// # pub fn update(&mut self, message: ()) {}
|
||||
/// # pub fn view(&self) -> iced_core::Element<(), Renderer> { unimplemented!() }
|
||||
/// # pub fn update(&mut self, _: ()) {}
|
||||
/// # }
|
||||
/// use iced_native::core::{clipboard, Size, Point};
|
||||
/// use iced_native::user_interface::{self, UserInterface};
|
||||
/// use iced_wgpu::Renderer;
|
||||
///
|
||||
/// let mut counter = Counter::new();
|
||||
/// let mut cache = user_interface::Cache::new();
|
||||
/// let mut renderer = Renderer::new();
|
||||
|
|
@ -357,27 +347,24 @@ where
|
|||
/// [completing the last example](#example-1):
|
||||
///
|
||||
/// ```no_run
|
||||
/// use iced_native::clipboard;
|
||||
/// use iced_native::renderer;
|
||||
/// use iced_native::user_interface::{self, UserInterface};
|
||||
/// use iced_native::{Size, Point, Theme};
|
||||
/// use iced_wgpu::Renderer;
|
||||
///
|
||||
/// # mod iced_wgpu {
|
||||
/// # pub use iced_native::renderer::Null as Renderer;
|
||||
/// # pub use iced_native::core::renderer::Null as Renderer;
|
||||
/// # pub type Theme = ();
|
||||
/// # }
|
||||
/// #
|
||||
/// # use iced_native::widget::Column;
|
||||
/// #
|
||||
/// # pub struct Counter;
|
||||
/// #
|
||||
/// # impl Counter {
|
||||
/// # pub fn new() -> Self { Counter }
|
||||
/// # pub fn view(&self) -> Column<(), Renderer> {
|
||||
/// # Column::new()
|
||||
/// # }
|
||||
/// # pub fn update(&mut self, message: ()) {}
|
||||
/// # pub fn view(&self) -> Element<(), Renderer> { unimplemented!() }
|
||||
/// # pub fn update(&mut self, _: ()) {}
|
||||
/// # }
|
||||
/// use iced_native::core::clipboard;
|
||||
/// use iced_native::core::renderer;
|
||||
/// use iced_native::core::{Element, Size, Point};
|
||||
/// use iced_native::user_interface::{self, UserInterface};
|
||||
/// use iced_wgpu::{Renderer, Theme};
|
||||
///
|
||||
/// let mut counter = Counter::new();
|
||||
/// let mut cache = user_interface::Cache::new();
|
||||
/// let mut renderer = Renderer::new();
|
||||
|
|
@ -386,6 +373,7 @@ where
|
|||
/// let mut clipboard = clipboard::Null;
|
||||
/// let mut events = Vec::new();
|
||||
/// let mut messages = Vec::new();
|
||||
/// let mut theme = Theme::default();
|
||||
///
|
||||
/// loop {
|
||||
/// // Obtain system events...
|
||||
|
|
@ -407,7 +395,7 @@ where
|
|||
/// );
|
||||
///
|
||||
/// // Draw the user interface
|
||||
/// let mouse_cursor = user_interface.draw(&mut renderer, &Theme::default(), &renderer::Style::default(), cursor_position);
|
||||
/// let mouse_cursor = user_interface.draw(&mut renderer, &theme, &renderer::Style::default(), cursor_position);
|
||||
///
|
||||
/// cache = user_interface.into_cache();
|
||||
///
|
||||
|
|
|
|||
|
|
@ -11,212 +11,6 @@
|
|||
//! source of inspiration.
|
||||
//!
|
||||
//! [renderer]: crate::renderer
|
||||
pub mod button;
|
||||
pub mod checkbox;
|
||||
pub mod column;
|
||||
pub mod container;
|
||||
pub mod helpers;
|
||||
pub mod image;
|
||||
pub mod operation;
|
||||
pub mod pane_grid;
|
||||
pub mod pick_list;
|
||||
pub mod progress_bar;
|
||||
pub mod radio;
|
||||
pub mod row;
|
||||
pub mod rule;
|
||||
pub mod scrollable;
|
||||
pub mod slider;
|
||||
pub mod space;
|
||||
pub mod svg;
|
||||
pub mod text;
|
||||
pub mod text_input;
|
||||
pub mod toggler;
|
||||
pub mod tooltip;
|
||||
pub mod tree;
|
||||
pub mod vertical_slider;
|
||||
|
||||
mod action;
|
||||
mod id;
|
||||
|
||||
#[doc(no_inline)]
|
||||
pub use button::Button;
|
||||
#[doc(no_inline)]
|
||||
pub use checkbox::Checkbox;
|
||||
#[doc(no_inline)]
|
||||
pub use column::Column;
|
||||
#[doc(no_inline)]
|
||||
pub use container::Container;
|
||||
#[doc(no_inline)]
|
||||
pub use helpers::*;
|
||||
#[doc(no_inline)]
|
||||
pub use image::Image;
|
||||
#[doc(no_inline)]
|
||||
pub use pane_grid::PaneGrid;
|
||||
#[doc(no_inline)]
|
||||
pub use pick_list::PickList;
|
||||
#[doc(no_inline)]
|
||||
pub use progress_bar::ProgressBar;
|
||||
#[doc(no_inline)]
|
||||
pub use radio::Radio;
|
||||
#[doc(no_inline)]
|
||||
pub use row::Row;
|
||||
#[doc(no_inline)]
|
||||
pub use rule::Rule;
|
||||
#[doc(no_inline)]
|
||||
pub use scrollable::Scrollable;
|
||||
#[doc(no_inline)]
|
||||
pub use slider::Slider;
|
||||
#[doc(no_inline)]
|
||||
pub use space::Space;
|
||||
#[doc(no_inline)]
|
||||
pub use svg::Svg;
|
||||
#[doc(no_inline)]
|
||||
pub use text::Text;
|
||||
#[doc(no_inline)]
|
||||
pub use text_input::TextInput;
|
||||
#[doc(no_inline)]
|
||||
pub use toggler::Toggler;
|
||||
#[doc(no_inline)]
|
||||
pub use tooltip::Tooltip;
|
||||
#[doc(no_inline)]
|
||||
pub use tree::Tree;
|
||||
#[doc(no_inline)]
|
||||
pub use vertical_slider::VerticalSlider;
|
||||
|
||||
pub use action::Action;
|
||||
pub use id::Id;
|
||||
pub use operation::Operation;
|
||||
|
||||
use crate::event::{self, Event};
|
||||
use crate::layout;
|
||||
use crate::mouse;
|
||||
use crate::overlay;
|
||||
use crate::renderer;
|
||||
use crate::{Clipboard, Layout, Length, Point, Rectangle, Shell};
|
||||
|
||||
/// A component that displays information and allows interaction.
|
||||
///
|
||||
/// If you want to build your own widgets, you will need to implement this
|
||||
/// trait.
|
||||
///
|
||||
/// # Examples
|
||||
/// The repository has some [examples] showcasing how to implement a custom
|
||||
/// widget:
|
||||
///
|
||||
/// - [`bezier_tool`], a Paint-like tool for drawing Bézier curves using
|
||||
/// [`lyon`].
|
||||
/// - [`custom_widget`], a demonstration of how to build a custom widget that
|
||||
/// draws a circle.
|
||||
/// - [`geometry`], a custom widget showcasing how to draw geometry with the
|
||||
/// `Mesh2D` primitive in [`iced_wgpu`].
|
||||
///
|
||||
/// [examples]: https://github.com/iced-rs/iced/tree/0.8/examples
|
||||
/// [`bezier_tool`]: https://github.com/iced-rs/iced/tree/0.8/examples/bezier_tool
|
||||
/// [`custom_widget`]: https://github.com/iced-rs/iced/tree/0.8/examples/custom_widget
|
||||
/// [`geometry`]: https://github.com/iced-rs/iced/tree/0.8/examples/geometry
|
||||
/// [`lyon`]: https://github.com/nical/lyon
|
||||
/// [`iced_wgpu`]: https://github.com/iced-rs/iced/tree/0.8/wgpu
|
||||
pub trait Widget<Message, Renderer>
|
||||
where
|
||||
Renderer: crate::Renderer,
|
||||
{
|
||||
/// Returns the width of the [`Widget`].
|
||||
fn width(&self) -> Length;
|
||||
|
||||
/// Returns the height of the [`Widget`].
|
||||
fn height(&self) -> Length;
|
||||
|
||||
/// Returns the [`layout::Node`] of the [`Widget`].
|
||||
///
|
||||
/// This [`layout::Node`] is used by the runtime to compute the [`Layout`] of the
|
||||
/// user interface.
|
||||
fn layout(
|
||||
&self,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node;
|
||||
|
||||
/// Draws the [`Widget`] using the associated `Renderer`.
|
||||
fn draw(
|
||||
&self,
|
||||
state: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
);
|
||||
|
||||
/// Returns the [`Tag`] of the [`Widget`].
|
||||
///
|
||||
/// [`Tag`]: tree::Tag
|
||||
fn tag(&self) -> tree::Tag {
|
||||
tree::Tag::stateless()
|
||||
}
|
||||
|
||||
/// Returns the [`State`] of the [`Widget`].
|
||||
///
|
||||
/// [`State`]: tree::State
|
||||
fn state(&self) -> tree::State {
|
||||
tree::State::None
|
||||
}
|
||||
|
||||
/// Returns the state [`Tree`] of the children of the [`Widget`].
|
||||
fn children(&self) -> Vec<Tree> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
/// Reconciliates the [`Widget`] with the provided [`Tree`].
|
||||
fn diff(&self, _tree: &mut Tree) {}
|
||||
|
||||
/// Applies an [`Operation`] to the [`Widget`].
|
||||
fn operate(
|
||||
&self,
|
||||
_state: &mut Tree,
|
||||
_layout: Layout<'_>,
|
||||
_renderer: &Renderer,
|
||||
_operation: &mut dyn Operation<Message>,
|
||||
) {
|
||||
}
|
||||
|
||||
/// Processes a runtime [`Event`].
|
||||
///
|
||||
/// By default, it does nothing.
|
||||
fn on_event(
|
||||
&mut self,
|
||||
_state: &mut Tree,
|
||||
_event: Event,
|
||||
_layout: Layout<'_>,
|
||||
_cursor_position: Point,
|
||||
_renderer: &Renderer,
|
||||
_clipboard: &mut dyn Clipboard,
|
||||
_shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
event::Status::Ignored
|
||||
}
|
||||
|
||||
/// Returns the current [`mouse::Interaction`] of the [`Widget`].
|
||||
///
|
||||
/// By default, it returns [`mouse::Interaction::Idle`].
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
_state: &Tree,
|
||||
_layout: Layout<'_>,
|
||||
_cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
_renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
mouse::Interaction::Idle
|
||||
}
|
||||
|
||||
/// Returns the overlay of the [`Widget`], if there is any.
|
||||
fn overlay<'a>(
|
||||
&'a mut self,
|
||||
_state: &'a mut Tree,
|
||||
_layout: Layout<'_>,
|
||||
_renderer: &Renderer,
|
||||
) -> Option<overlay::Element<'a, Message, Renderer>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
use crate::widget::operation::{
|
||||
use iced_core::widget::operation::{
|
||||
self, Focusable, Operation, Scrollable, TextInput,
|
||||
};
|
||||
use crate::widget::Id;
|
||||
|
||||
use iced_core::widget::Id;
|
||||
use iced_futures::MaybeSend;
|
||||
|
||||
use std::any::Any;
|
||||
|
|
|
|||
|
|
@ -1,455 +0,0 @@
|
|||
//! Allow your users to perform actions by pressing a button.
|
||||
//!
|
||||
//! A [`Button`] has some local [`State`].
|
||||
use crate::event::{self, Event};
|
||||
use crate::layout;
|
||||
use crate::mouse;
|
||||
use crate::overlay;
|
||||
use crate::renderer;
|
||||
use crate::touch;
|
||||
use crate::widget::tree::{self, Tree};
|
||||
use crate::widget::Operation;
|
||||
use crate::{
|
||||
Background, Clipboard, Color, Element, Layout, Length, Padding, Point,
|
||||
Rectangle, Shell, Vector, Widget,
|
||||
};
|
||||
|
||||
pub use iced_style::button::{Appearance, StyleSheet};
|
||||
|
||||
/// A generic widget that produces a message when pressed.
|
||||
///
|
||||
/// ```
|
||||
/// # type Button<'a, Message> =
|
||||
/// # iced_native::widget::Button<'a, Message, iced_native::renderer::Null>;
|
||||
/// #
|
||||
/// #[derive(Clone)]
|
||||
/// enum Message {
|
||||
/// ButtonPressed,
|
||||
/// }
|
||||
///
|
||||
/// let button = Button::new("Press me!").on_press(Message::ButtonPressed);
|
||||
/// ```
|
||||
///
|
||||
/// If a [`Button::on_press`] handler is not set, the resulting [`Button`] will
|
||||
/// be disabled:
|
||||
///
|
||||
/// ```
|
||||
/// # type Button<'a, Message> =
|
||||
/// # iced_native::widget::Button<'a, Message, iced_native::renderer::Null>;
|
||||
/// #
|
||||
/// #[derive(Clone)]
|
||||
/// enum Message {
|
||||
/// ButtonPressed,
|
||||
/// }
|
||||
///
|
||||
/// fn disabled_button<'a>() -> Button<'a, Message> {
|
||||
/// Button::new("I'm disabled!")
|
||||
/// }
|
||||
///
|
||||
/// fn enabled_button<'a>() -> Button<'a, Message> {
|
||||
/// disabled_button().on_press(Message::ButtonPressed)
|
||||
/// }
|
||||
/// ```
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Button<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: crate::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
content: Element<'a, Message, Renderer>,
|
||||
on_press: Option<Message>,
|
||||
width: Length,
|
||||
height: Length,
|
||||
padding: Padding,
|
||||
style: <Renderer::Theme as StyleSheet>::Style,
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Button<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: crate::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
/// Creates a new [`Button`] with the given content.
|
||||
pub fn new(content: impl Into<Element<'a, Message, Renderer>>) -> Self {
|
||||
Button {
|
||||
content: content.into(),
|
||||
on_press: None,
|
||||
width: Length::Shrink,
|
||||
height: Length::Shrink,
|
||||
padding: Padding::new(5.0),
|
||||
style: <Renderer::Theme as StyleSheet>::Style::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the width of the [`Button`].
|
||||
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||
self.width = width.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the height of the [`Button`].
|
||||
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
||||
self.height = height.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`Padding`] of the [`Button`].
|
||||
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
|
||||
self.padding = padding.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the message that will be produced when the [`Button`] is pressed.
|
||||
///
|
||||
/// Unless `on_press` is called, the [`Button`] will be disabled.
|
||||
pub fn on_press(mut self, msg: Message) -> Self {
|
||||
self.on_press = Some(msg);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style variant of this [`Button`].
|
||||
pub fn style(
|
||||
mut self,
|
||||
style: <Renderer::Theme as StyleSheet>::Style,
|
||||
) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Widget<Message, Renderer>
|
||||
for Button<'a, Message, Renderer>
|
||||
where
|
||||
Message: 'a + Clone,
|
||||
Renderer: 'a + crate::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
fn tag(&self) -> tree::Tag {
|
||||
tree::Tag::of::<State>()
|
||||
}
|
||||
|
||||
fn state(&self) -> tree::State {
|
||||
tree::State::new(State::new())
|
||||
}
|
||||
|
||||
fn children(&self) -> Vec<Tree> {
|
||||
vec![Tree::new(&self.content)]
|
||||
}
|
||||
|
||||
fn diff(&self, tree: &mut Tree) {
|
||||
tree.diff_children(std::slice::from_ref(&self.content))
|
||||
}
|
||||
|
||||
fn width(&self) -> Length {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
layout(
|
||||
renderer,
|
||||
limits,
|
||||
self.width,
|
||||
self.height,
|
||||
self.padding,
|
||||
|renderer, limits| {
|
||||
self.content.as_widget().layout(renderer, limits)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn operate(
|
||||
&self,
|
||||
tree: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn Operation<Message>,
|
||||
) {
|
||||
operation.container(None, &mut |operation| {
|
||||
self.content.as_widget().operate(
|
||||
&mut tree.children[0],
|
||||
layout.children().next().unwrap(),
|
||||
renderer,
|
||||
operation,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
if let event::Status::Captured = self.content.as_widget_mut().on_event(
|
||||
&mut tree.children[0],
|
||||
event.clone(),
|
||||
layout.children().next().unwrap(),
|
||||
cursor_position,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
) {
|
||||
return event::Status::Captured;
|
||||
}
|
||||
|
||||
update(
|
||||
event,
|
||||
layout,
|
||||
cursor_position,
|
||||
shell,
|
||||
&self.on_press,
|
||||
|| tree.state.downcast_mut::<State>(),
|
||||
)
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
_style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
) {
|
||||
let bounds = layout.bounds();
|
||||
let content_layout = layout.children().next().unwrap();
|
||||
|
||||
let styling = draw(
|
||||
renderer,
|
||||
bounds,
|
||||
cursor_position,
|
||||
self.on_press.is_some(),
|
||||
theme,
|
||||
&self.style,
|
||||
|| tree.state.downcast_ref::<State>(),
|
||||
);
|
||||
|
||||
self.content.as_widget().draw(
|
||||
&tree.children[0],
|
||||
renderer,
|
||||
theme,
|
||||
&renderer::Style {
|
||||
text_color: styling.text_color,
|
||||
},
|
||||
content_layout,
|
||||
cursor_position,
|
||||
&bounds,
|
||||
);
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
_tree: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
_renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
mouse_interaction(layout, cursor_position, self.on_press.is_some())
|
||||
}
|
||||
|
||||
fn overlay<'b>(
|
||||
&'b mut self,
|
||||
tree: &'b mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
) -> Option<overlay::Element<'b, Message, Renderer>> {
|
||||
self.content.as_widget_mut().overlay(
|
||||
&mut tree.children[0],
|
||||
layout.children().next().unwrap(),
|
||||
renderer,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> From<Button<'a, Message, Renderer>>
|
||||
for Element<'a, Message, Renderer>
|
||||
where
|
||||
Message: Clone + 'a,
|
||||
Renderer: crate::Renderer + 'a,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
fn from(button: Button<'a, Message, Renderer>) -> Self {
|
||||
Self::new(button)
|
||||
}
|
||||
}
|
||||
|
||||
/// The local state of a [`Button`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct State {
|
||||
is_pressed: bool,
|
||||
}
|
||||
|
||||
impl State {
|
||||
/// Creates a new [`State`].
|
||||
pub fn new() -> State {
|
||||
State::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Processes the given [`Event`] and updates the [`State`] of a [`Button`]
|
||||
/// accordingly.
|
||||
pub fn update<'a, Message: Clone>(
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
on_press: &Option<Message>,
|
||||
state: impl FnOnce() -> &'a mut State,
|
||||
) -> event::Status {
|
||||
match event {
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
|
||||
| Event::Touch(touch::Event::FingerPressed { .. }) => {
|
||||
if on_press.is_some() {
|
||||
let bounds = layout.bounds();
|
||||
|
||||
if bounds.contains(cursor_position) {
|
||||
let state = state();
|
||||
|
||||
state.is_pressed = true;
|
||||
|
||||
return event::Status::Captured;
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
|
||||
| Event::Touch(touch::Event::FingerLifted { .. }) => {
|
||||
if let Some(on_press) = on_press.clone() {
|
||||
let state = state();
|
||||
|
||||
if state.is_pressed {
|
||||
state.is_pressed = false;
|
||||
|
||||
let bounds = layout.bounds();
|
||||
|
||||
if bounds.contains(cursor_position) {
|
||||
shell.publish(on_press);
|
||||
}
|
||||
|
||||
return event::Status::Captured;
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::Touch(touch::Event::FingerLost { .. }) => {
|
||||
let state = state();
|
||||
|
||||
state.is_pressed = false;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
event::Status::Ignored
|
||||
}
|
||||
|
||||
/// Draws a [`Button`].
|
||||
pub fn draw<'a, Renderer: crate::Renderer>(
|
||||
renderer: &mut Renderer,
|
||||
bounds: Rectangle,
|
||||
cursor_position: Point,
|
||||
is_enabled: bool,
|
||||
style_sheet: &dyn StyleSheet<
|
||||
Style = <Renderer::Theme as StyleSheet>::Style,
|
||||
>,
|
||||
style: &<Renderer::Theme as StyleSheet>::Style,
|
||||
state: impl FnOnce() -> &'a State,
|
||||
) -> Appearance
|
||||
where
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
let is_mouse_over = bounds.contains(cursor_position);
|
||||
|
||||
let styling = if !is_enabled {
|
||||
style_sheet.disabled(style)
|
||||
} else if is_mouse_over {
|
||||
let state = state();
|
||||
|
||||
if state.is_pressed {
|
||||
style_sheet.pressed(style)
|
||||
} else {
|
||||
style_sheet.hovered(style)
|
||||
}
|
||||
} else {
|
||||
style_sheet.active(style)
|
||||
};
|
||||
|
||||
if styling.background.is_some() || styling.border_width > 0.0 {
|
||||
if styling.shadow_offset != Vector::default() {
|
||||
// TODO: Implement proper shadow support
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: Rectangle {
|
||||
x: bounds.x + styling.shadow_offset.x,
|
||||
y: bounds.y + styling.shadow_offset.y,
|
||||
..bounds
|
||||
},
|
||||
border_radius: styling.border_radius.into(),
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
},
|
||||
Background::Color([0.0, 0.0, 0.0, 0.5].into()),
|
||||
);
|
||||
}
|
||||
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds,
|
||||
border_radius: styling.border_radius.into(),
|
||||
border_width: styling.border_width,
|
||||
border_color: styling.border_color,
|
||||
},
|
||||
styling
|
||||
.background
|
||||
.unwrap_or(Background::Color(Color::TRANSPARENT)),
|
||||
);
|
||||
}
|
||||
|
||||
styling
|
||||
}
|
||||
|
||||
/// Computes the layout of a [`Button`].
|
||||
pub fn layout<Renderer>(
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
width: Length,
|
||||
height: Length,
|
||||
padding: Padding,
|
||||
layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node,
|
||||
) -> layout::Node {
|
||||
let limits = limits.width(width).height(height);
|
||||
|
||||
let mut content = layout_content(renderer, &limits.pad(padding));
|
||||
let padding = padding.fit(content.size(), limits.max());
|
||||
let size = limits.pad(padding).resolve(content.size()).pad(padding);
|
||||
|
||||
content.move_to(Point::new(padding.left, padding.top));
|
||||
|
||||
layout::Node::with_children(size, vec![content])
|
||||
}
|
||||
|
||||
/// Returns the [`mouse::Interaction`] of a [`Button`].
|
||||
pub fn mouse_interaction(
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
is_enabled: bool,
|
||||
) -> mouse::Interaction {
|
||||
let is_mouse_over = layout.bounds().contains(cursor_position);
|
||||
|
||||
if is_mouse_over && is_enabled {
|
||||
mouse::Interaction::Pointer
|
||||
} else {
|
||||
mouse::Interaction::default()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,321 +0,0 @@
|
|||
//! Show toggle controls using checkboxes.
|
||||
use crate::alignment;
|
||||
use crate::event::{self, Event};
|
||||
use crate::layout;
|
||||
use crate::mouse;
|
||||
use crate::renderer;
|
||||
use crate::text;
|
||||
use crate::touch;
|
||||
use crate::widget::{self, Row, Text, Tree};
|
||||
use crate::{
|
||||
Alignment, Clipboard, Element, Layout, Length, Pixels, Point, Rectangle,
|
||||
Shell, Widget,
|
||||
};
|
||||
|
||||
pub use iced_style::checkbox::{Appearance, StyleSheet};
|
||||
|
||||
/// The icon in a [`Checkbox`].
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Icon<Font> {
|
||||
/// Font that will be used to display the `code_point`,
|
||||
pub font: Font,
|
||||
/// The unicode code point that will be used as the icon.
|
||||
pub code_point: char,
|
||||
/// Font size of the content.
|
||||
pub size: Option<f32>,
|
||||
}
|
||||
|
||||
/// A box that can be checked.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # type Checkbox<'a, Message> = iced_native::widget::Checkbox<'a, Message, iced_native::renderer::Null>;
|
||||
/// #
|
||||
/// pub enum Message {
|
||||
/// CheckboxToggled(bool),
|
||||
/// }
|
||||
///
|
||||
/// let is_checked = true;
|
||||
///
|
||||
/// Checkbox::new("Toggle me!", is_checked, Message::CheckboxToggled);
|
||||
/// ```
|
||||
///
|
||||
/// 
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Checkbox<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: text::Renderer,
|
||||
Renderer::Theme: StyleSheet + widget::text::StyleSheet,
|
||||
{
|
||||
is_checked: bool,
|
||||
on_toggle: Box<dyn Fn(bool) -> Message + 'a>,
|
||||
label: String,
|
||||
width: Length,
|
||||
size: f32,
|
||||
spacing: f32,
|
||||
text_size: Option<f32>,
|
||||
font: Option<Renderer::Font>,
|
||||
icon: Icon<Renderer::Font>,
|
||||
style: <Renderer::Theme as StyleSheet>::Style,
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Checkbox<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: text::Renderer,
|
||||
Renderer::Theme: StyleSheet + widget::text::StyleSheet,
|
||||
{
|
||||
/// The default size of a [`Checkbox`].
|
||||
const DEFAULT_SIZE: f32 = 20.0;
|
||||
|
||||
/// The default spacing of a [`Checkbox`].
|
||||
const DEFAULT_SPACING: f32 = 15.0;
|
||||
|
||||
/// Creates a new [`Checkbox`].
|
||||
///
|
||||
/// It expects:
|
||||
/// * a boolean describing whether the [`Checkbox`] is checked or not
|
||||
/// * the label of the [`Checkbox`]
|
||||
/// * a function that will be called when the [`Checkbox`] is toggled. It
|
||||
/// will receive the new state of the [`Checkbox`] and must produce a
|
||||
/// `Message`.
|
||||
pub fn new<F>(label: impl Into<String>, is_checked: bool, f: F) -> Self
|
||||
where
|
||||
F: 'a + Fn(bool) -> Message,
|
||||
{
|
||||
Checkbox {
|
||||
is_checked,
|
||||
on_toggle: Box::new(f),
|
||||
label: label.into(),
|
||||
width: Length::Shrink,
|
||||
size: Self::DEFAULT_SIZE,
|
||||
spacing: Self::DEFAULT_SPACING,
|
||||
text_size: None,
|
||||
font: None,
|
||||
icon: Icon {
|
||||
font: Renderer::ICON_FONT,
|
||||
code_point: Renderer::CHECKMARK_ICON,
|
||||
size: None,
|
||||
},
|
||||
style: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the size of the [`Checkbox`].
|
||||
pub fn size(mut self, size: impl Into<Pixels>) -> Self {
|
||||
self.size = size.into().0;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the width of the [`Checkbox`].
|
||||
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||
self.width = width.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the spacing between the [`Checkbox`] and the text.
|
||||
pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
|
||||
self.spacing = spacing.into().0;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the text size of the [`Checkbox`].
|
||||
pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self {
|
||||
self.text_size = Some(text_size.into().0);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`Font`] of the text of the [`Checkbox`].
|
||||
///
|
||||
/// [`Font`]: crate::text::Renderer::Font
|
||||
pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
|
||||
self.font = Some(font.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`Icon`] of the [`Checkbox`].
|
||||
pub fn icon(mut self, icon: Icon<Renderer::Font>) -> Self {
|
||||
self.icon = icon;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the [`Checkbox`].
|
||||
pub fn style(
|
||||
mut self,
|
||||
style: impl Into<<Renderer::Theme as StyleSheet>::Style>,
|
||||
) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Widget<Message, Renderer>
|
||||
for Checkbox<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: text::Renderer,
|
||||
Renderer::Theme: StyleSheet + widget::text::StyleSheet,
|
||||
{
|
||||
fn width(&self) -> Length {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
Length::Shrink
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
Row::<(), Renderer>::new()
|
||||
.width(self.width)
|
||||
.spacing(self.spacing)
|
||||
.align_items(Alignment::Center)
|
||||
.push(Row::new().width(self.size).height(self.size))
|
||||
.push(
|
||||
Text::new(&self.label)
|
||||
.font(self.font.unwrap_or_else(|| renderer.default_font()))
|
||||
.width(self.width)
|
||||
.size(
|
||||
self.text_size
|
||||
.unwrap_or_else(|| renderer.default_size()),
|
||||
),
|
||||
)
|
||||
.layout(renderer, limits)
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
_tree: &mut Tree,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
_renderer: &Renderer,
|
||||
_clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
match event {
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
|
||||
| Event::Touch(touch::Event::FingerPressed { .. }) => {
|
||||
let mouse_over = layout.bounds().contains(cursor_position);
|
||||
|
||||
if mouse_over {
|
||||
shell.publish((self.on_toggle)(!self.is_checked));
|
||||
|
||||
return event::Status::Captured;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
event::Status::Ignored
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
_tree: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
_renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
if layout.bounds().contains(cursor_position) {
|
||||
mouse::Interaction::Pointer
|
||||
} else {
|
||||
mouse::Interaction::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
_tree: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
) {
|
||||
let bounds = layout.bounds();
|
||||
let is_mouse_over = bounds.contains(cursor_position);
|
||||
|
||||
let mut children = layout.children();
|
||||
|
||||
let custom_style = if is_mouse_over {
|
||||
theme.hovered(&self.style, self.is_checked)
|
||||
} else {
|
||||
theme.active(&self.style, self.is_checked)
|
||||
};
|
||||
|
||||
{
|
||||
let layout = children.next().unwrap();
|
||||
let bounds = layout.bounds();
|
||||
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds,
|
||||
border_radius: custom_style.border_radius.into(),
|
||||
border_width: custom_style.border_width,
|
||||
border_color: custom_style.border_color,
|
||||
},
|
||||
custom_style.background,
|
||||
);
|
||||
|
||||
let Icon {
|
||||
font,
|
||||
code_point,
|
||||
size,
|
||||
} = &self.icon;
|
||||
let size = size.unwrap_or(bounds.height * 0.7);
|
||||
|
||||
if self.is_checked {
|
||||
renderer.fill_text(text::Text {
|
||||
content: &code_point.to_string(),
|
||||
font: *font,
|
||||
size,
|
||||
bounds: Rectangle {
|
||||
x: bounds.center_x(),
|
||||
y: bounds.center_y(),
|
||||
..bounds
|
||||
},
|
||||
color: custom_style.icon_color,
|
||||
horizontal_alignment: alignment::Horizontal::Center,
|
||||
vertical_alignment: alignment::Vertical::Center,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let label_layout = children.next().unwrap();
|
||||
|
||||
widget::text::draw(
|
||||
renderer,
|
||||
style,
|
||||
label_layout,
|
||||
&self.label,
|
||||
self.text_size,
|
||||
self.font,
|
||||
widget::text::Appearance {
|
||||
color: custom_style.text_color,
|
||||
},
|
||||
alignment::Horizontal::Left,
|
||||
alignment::Vertical::Center,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> From<Checkbox<'a, Message, Renderer>>
|
||||
for Element<'a, Message, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Renderer: 'a + text::Renderer,
|
||||
Renderer::Theme: StyleSheet + widget::text::StyleSheet,
|
||||
{
|
||||
fn from(
|
||||
checkbox: Checkbox<'a, Message, Renderer>,
|
||||
) -> Element<'a, Message, Renderer> {
|
||||
Element::new(checkbox)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,264 +0,0 @@
|
|||
//! Distribute content vertically.
|
||||
use crate::event::{self, Event};
|
||||
use crate::layout;
|
||||
use crate::mouse;
|
||||
use crate::overlay;
|
||||
use crate::renderer;
|
||||
use crate::widget::{Operation, Tree};
|
||||
use crate::{
|
||||
Alignment, Clipboard, Element, Layout, Length, Padding, Pixels, Point,
|
||||
Rectangle, Shell, Widget,
|
||||
};
|
||||
|
||||
/// A container that distributes its contents vertically.
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Column<'a, Message, Renderer> {
|
||||
spacing: f32,
|
||||
padding: Padding,
|
||||
width: Length,
|
||||
height: Length,
|
||||
max_width: f32,
|
||||
align_items: Alignment,
|
||||
children: Vec<Element<'a, Message, Renderer>>,
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Column<'a, Message, Renderer> {
|
||||
/// Creates an empty [`Column`].
|
||||
pub fn new() -> Self {
|
||||
Self::with_children(Vec::new())
|
||||
}
|
||||
|
||||
/// Creates a [`Column`] with the given elements.
|
||||
pub fn with_children(
|
||||
children: Vec<Element<'a, Message, Renderer>>,
|
||||
) -> Self {
|
||||
Column {
|
||||
spacing: 0.0,
|
||||
padding: Padding::ZERO,
|
||||
width: Length::Shrink,
|
||||
height: Length::Shrink,
|
||||
max_width: f32::INFINITY,
|
||||
align_items: Alignment::Start,
|
||||
children,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the vertical spacing _between_ elements.
|
||||
///
|
||||
/// Custom margins per element do not exist in iced. You should use this
|
||||
/// method instead! While less flexible, it helps you keep spacing between
|
||||
/// elements consistent.
|
||||
pub fn spacing(mut self, amount: impl Into<Pixels>) -> Self {
|
||||
self.spacing = amount.into().0;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`Padding`] of the [`Column`].
|
||||
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
|
||||
self.padding = padding.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the width of the [`Column`].
|
||||
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||
self.width = width.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the height of the [`Column`].
|
||||
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
||||
self.height = height.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the maximum width of the [`Column`].
|
||||
pub fn max_width(mut self, max_width: impl Into<Pixels>) -> Self {
|
||||
self.max_width = max_width.into().0;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the horizontal alignment of the contents of the [`Column`] .
|
||||
pub fn align_items(mut self, align: Alignment) -> Self {
|
||||
self.align_items = align;
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds an element to the [`Column`].
|
||||
pub fn push(
|
||||
mut self,
|
||||
child: impl Into<Element<'a, Message, Renderer>>,
|
||||
) -> Self {
|
||||
self.children.push(child.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Default for Column<'a, Message, Renderer> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Widget<Message, Renderer>
|
||||
for Column<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: crate::Renderer,
|
||||
{
|
||||
fn children(&self) -> Vec<Tree> {
|
||||
self.children.iter().map(Tree::new).collect()
|
||||
}
|
||||
|
||||
fn diff(&self, tree: &mut Tree) {
|
||||
tree.diff_children(&self.children);
|
||||
}
|
||||
|
||||
fn width(&self) -> Length {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
let limits = limits
|
||||
.max_width(self.max_width)
|
||||
.width(self.width)
|
||||
.height(self.height);
|
||||
|
||||
layout::flex::resolve(
|
||||
layout::flex::Axis::Vertical,
|
||||
renderer,
|
||||
&limits,
|
||||
self.padding,
|
||||
self.spacing,
|
||||
self.align_items,
|
||||
&self.children,
|
||||
)
|
||||
}
|
||||
|
||||
fn operate(
|
||||
&self,
|
||||
tree: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn Operation<Message>,
|
||||
) {
|
||||
operation.container(None, &mut |operation| {
|
||||
self.children
|
||||
.iter()
|
||||
.zip(&mut tree.children)
|
||||
.zip(layout.children())
|
||||
.for_each(|((child, state), layout)| {
|
||||
child
|
||||
.as_widget()
|
||||
.operate(state, layout, renderer, operation);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
self.children
|
||||
.iter_mut()
|
||||
.zip(&mut tree.children)
|
||||
.zip(layout.children())
|
||||
.map(|((child, state), layout)| {
|
||||
child.as_widget_mut().on_event(
|
||||
state,
|
||||
event.clone(),
|
||||
layout,
|
||||
cursor_position,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
)
|
||||
})
|
||||
.fold(event::Status::Ignored, event::Status::merge)
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
self.children
|
||||
.iter()
|
||||
.zip(&tree.children)
|
||||
.zip(layout.children())
|
||||
.map(|((child, state), layout)| {
|
||||
child.as_widget().mouse_interaction(
|
||||
state,
|
||||
layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
renderer,
|
||||
)
|
||||
})
|
||||
.max()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
for ((child, state), layout) in self
|
||||
.children
|
||||
.iter()
|
||||
.zip(&tree.children)
|
||||
.zip(layout.children())
|
||||
{
|
||||
child.as_widget().draw(
|
||||
state,
|
||||
renderer,
|
||||
theme,
|
||||
style,
|
||||
layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn overlay<'b>(
|
||||
&'b mut self,
|
||||
tree: &'b mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
) -> Option<overlay::Element<'b, Message, Renderer>> {
|
||||
overlay::from_children(&mut self.children, tree, layout, renderer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> From<Column<'a, Message, Renderer>>
|
||||
for Element<'a, Message, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Renderer: crate::Renderer + 'a,
|
||||
{
|
||||
fn from(column: Column<'a, Message, Renderer>) -> Self {
|
||||
Self::new(column)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,368 +0,0 @@
|
|||
//! Decorate content and apply alignment.
|
||||
use crate::alignment::{self, Alignment};
|
||||
use crate::event::{self, Event};
|
||||
use crate::layout;
|
||||
use crate::mouse;
|
||||
use crate::overlay;
|
||||
use crate::renderer;
|
||||
use crate::widget::{self, Operation, Tree};
|
||||
use crate::{
|
||||
Background, Clipboard, Color, Element, Layout, Length, Padding, Pixels,
|
||||
Point, Rectangle, Shell, Widget,
|
||||
};
|
||||
|
||||
pub use iced_style::container::{Appearance, StyleSheet};
|
||||
|
||||
/// An element decorating some content.
|
||||
///
|
||||
/// It is normally used for alignment purposes.
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Container<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: crate::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
id: Option<Id>,
|
||||
padding: Padding,
|
||||
width: Length,
|
||||
height: Length,
|
||||
max_width: f32,
|
||||
max_height: f32,
|
||||
horizontal_alignment: alignment::Horizontal,
|
||||
vertical_alignment: alignment::Vertical,
|
||||
style: <Renderer::Theme as StyleSheet>::Style,
|
||||
content: Element<'a, Message, Renderer>,
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Container<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: crate::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
/// Creates an empty [`Container`].
|
||||
pub fn new<T>(content: T) -> Self
|
||||
where
|
||||
T: Into<Element<'a, Message, Renderer>>,
|
||||
{
|
||||
Container {
|
||||
id: None,
|
||||
padding: Padding::ZERO,
|
||||
width: Length::Shrink,
|
||||
height: Length::Shrink,
|
||||
max_width: f32::INFINITY,
|
||||
max_height: f32::INFINITY,
|
||||
horizontal_alignment: alignment::Horizontal::Left,
|
||||
vertical_alignment: alignment::Vertical::Top,
|
||||
style: Default::default(),
|
||||
content: content.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the [`Id`] of the [`Container`].
|
||||
pub fn id(mut self, id: Id) -> Self {
|
||||
self.id = Some(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`Padding`] of the [`Container`].
|
||||
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
|
||||
self.padding = padding.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the width of the [`Container`].
|
||||
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||
self.width = width.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the height of the [`Container`].
|
||||
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
||||
self.height = height.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the maximum width of the [`Container`].
|
||||
pub fn max_width(mut self, max_width: impl Into<Pixels>) -> Self {
|
||||
self.max_width = max_width.into().0;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the maximum height of the [`Container`].
|
||||
pub fn max_height(mut self, max_height: impl Into<Pixels>) -> Self {
|
||||
self.max_height = max_height.into().0;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the content alignment for the horizontal axis of the [`Container`].
|
||||
pub fn align_x(mut self, alignment: alignment::Horizontal) -> Self {
|
||||
self.horizontal_alignment = alignment;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the content alignment for the vertical axis of the [`Container`].
|
||||
pub fn align_y(mut self, alignment: alignment::Vertical) -> Self {
|
||||
self.vertical_alignment = alignment;
|
||||
self
|
||||
}
|
||||
|
||||
/// Centers the contents in the horizontal axis of the [`Container`].
|
||||
pub fn center_x(mut self) -> Self {
|
||||
self.horizontal_alignment = alignment::Horizontal::Center;
|
||||
self
|
||||
}
|
||||
|
||||
/// Centers the contents in the vertical axis of the [`Container`].
|
||||
pub fn center_y(mut self) -> Self {
|
||||
self.vertical_alignment = alignment::Vertical::Center;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the [`Container`].
|
||||
pub fn style(
|
||||
mut self,
|
||||
style: impl Into<<Renderer::Theme as StyleSheet>::Style>,
|
||||
) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Widget<Message, Renderer>
|
||||
for Container<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: crate::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
fn children(&self) -> Vec<Tree> {
|
||||
vec![Tree::new(&self.content)]
|
||||
}
|
||||
|
||||
fn diff(&self, tree: &mut Tree) {
|
||||
tree.diff_children(std::slice::from_ref(&self.content))
|
||||
}
|
||||
|
||||
fn width(&self) -> Length {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
layout(
|
||||
renderer,
|
||||
limits,
|
||||
self.width,
|
||||
self.height,
|
||||
self.max_width,
|
||||
self.max_height,
|
||||
self.padding,
|
||||
self.horizontal_alignment,
|
||||
self.vertical_alignment,
|
||||
|renderer, limits| {
|
||||
self.content.as_widget().layout(renderer, limits)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn operate(
|
||||
&self,
|
||||
tree: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn Operation<Message>,
|
||||
) {
|
||||
operation.container(
|
||||
self.id.as_ref().map(|id| &id.0),
|
||||
&mut |operation| {
|
||||
self.content.as_widget().operate(
|
||||
&mut tree.children[0],
|
||||
layout.children().next().unwrap(),
|
||||
renderer,
|
||||
operation,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
self.content.as_widget_mut().on_event(
|
||||
&mut tree.children[0],
|
||||
event,
|
||||
layout.children().next().unwrap(),
|
||||
cursor_position,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
)
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
self.content.as_widget().mouse_interaction(
|
||||
&tree.children[0],
|
||||
layout.children().next().unwrap(),
|
||||
cursor_position,
|
||||
viewport,
|
||||
renderer,
|
||||
)
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
renderer_style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
let style = theme.appearance(&self.style);
|
||||
|
||||
draw_background(renderer, &style, layout.bounds());
|
||||
|
||||
self.content.as_widget().draw(
|
||||
&tree.children[0],
|
||||
renderer,
|
||||
theme,
|
||||
&renderer::Style {
|
||||
text_color: style
|
||||
.text_color
|
||||
.unwrap_or(renderer_style.text_color),
|
||||
},
|
||||
layout.children().next().unwrap(),
|
||||
cursor_position,
|
||||
viewport,
|
||||
);
|
||||
}
|
||||
|
||||
fn overlay<'b>(
|
||||
&'b mut self,
|
||||
tree: &'b mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
) -> Option<overlay::Element<'b, Message, Renderer>> {
|
||||
self.content.as_widget_mut().overlay(
|
||||
&mut tree.children[0],
|
||||
layout.children().next().unwrap(),
|
||||
renderer,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> From<Container<'a, Message, Renderer>>
|
||||
for Element<'a, Message, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Renderer: 'a + crate::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
fn from(
|
||||
column: Container<'a, Message, Renderer>,
|
||||
) -> Element<'a, Message, Renderer> {
|
||||
Element::new(column)
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes the layout of a [`Container`].
|
||||
pub fn layout<Renderer>(
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
width: Length,
|
||||
height: Length,
|
||||
max_width: f32,
|
||||
max_height: f32,
|
||||
padding: Padding,
|
||||
horizontal_alignment: alignment::Horizontal,
|
||||
vertical_alignment: alignment::Vertical,
|
||||
layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node,
|
||||
) -> layout::Node {
|
||||
let limits = limits
|
||||
.loose()
|
||||
.max_width(max_width)
|
||||
.max_height(max_height)
|
||||
.width(width)
|
||||
.height(height);
|
||||
|
||||
let mut content = layout_content(renderer, &limits.pad(padding).loose());
|
||||
let padding = padding.fit(content.size(), limits.max());
|
||||
let size = limits.pad(padding).resolve(content.size());
|
||||
|
||||
content.move_to(Point::new(padding.left, padding.top));
|
||||
content.align(
|
||||
Alignment::from(horizontal_alignment),
|
||||
Alignment::from(vertical_alignment),
|
||||
size,
|
||||
);
|
||||
|
||||
layout::Node::with_children(size.pad(padding), vec![content])
|
||||
}
|
||||
|
||||
/// Draws the background of a [`Container`] given its [`Appearance`] and its `bounds`.
|
||||
pub fn draw_background<Renderer>(
|
||||
renderer: &mut Renderer,
|
||||
appearance: &Appearance,
|
||||
bounds: Rectangle,
|
||||
) where
|
||||
Renderer: crate::Renderer,
|
||||
{
|
||||
if appearance.background.is_some() || appearance.border_width > 0.0 {
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds,
|
||||
border_radius: appearance.border_radius.into(),
|
||||
border_width: appearance.border_width,
|
||||
border_color: appearance.border_color,
|
||||
},
|
||||
appearance
|
||||
.background
|
||||
.unwrap_or(Background::Color(Color::TRANSPARENT)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The identifier of a [`Container`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct Id(widget::Id);
|
||||
|
||||
impl Id {
|
||||
/// Creates a custom [`Id`].
|
||||
pub fn new(id: impl Into<std::borrow::Cow<'static, str>>) -> Self {
|
||||
Self(widget::Id::new(id))
|
||||
}
|
||||
|
||||
/// Creates a unique [`Id`].
|
||||
///
|
||||
/// This function produces a different [`Id`] every time it is called.
|
||||
pub fn unique() -> Self {
|
||||
Self(widget::Id::unique())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Id> for widget::Id {
|
||||
fn from(id: Id) -> Self {
|
||||
id.0
|
||||
}
|
||||
}
|
||||
|
|
@ -1,317 +0,0 @@
|
|||
//! Helper functions to create pure widgets.
|
||||
use crate::overlay;
|
||||
use crate::widget;
|
||||
use crate::{Element, Length, Pixels};
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
/// Creates a [`Column`] with the given children.
|
||||
///
|
||||
/// [`Column`]: widget::Column
|
||||
#[macro_export]
|
||||
macro_rules! column {
|
||||
() => (
|
||||
$crate::widget::Column::new()
|
||||
);
|
||||
($($x:expr),+ $(,)?) => (
|
||||
$crate::widget::Column::with_children(vec![$($crate::Element::from($x)),+])
|
||||
);
|
||||
}
|
||||
|
||||
/// Creates a [`Row`] with the given children.
|
||||
///
|
||||
/// [`Row`]: widget::Row
|
||||
#[macro_export]
|
||||
macro_rules! row {
|
||||
() => (
|
||||
$crate::widget::Row::new()
|
||||
);
|
||||
($($x:expr),+ $(,)?) => (
|
||||
$crate::widget::Row::with_children(vec![$($crate::Element::from($x)),+])
|
||||
);
|
||||
}
|
||||
|
||||
/// Creates a new [`Container`] with the provided content.
|
||||
///
|
||||
/// [`Container`]: widget::Container
|
||||
pub fn container<'a, Message, Renderer>(
|
||||
content: impl Into<Element<'a, Message, Renderer>>,
|
||||
) -> widget::Container<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: crate::Renderer,
|
||||
Renderer::Theme: widget::container::StyleSheet,
|
||||
{
|
||||
widget::Container::new(content)
|
||||
}
|
||||
|
||||
/// Creates a new [`Column`] with the given children.
|
||||
///
|
||||
/// [`Column`]: widget::Column
|
||||
pub fn column<Message, Renderer>(
|
||||
children: Vec<Element<'_, Message, Renderer>>,
|
||||
) -> widget::Column<'_, Message, Renderer> {
|
||||
widget::Column::with_children(children)
|
||||
}
|
||||
|
||||
/// Creates a new [`Row`] with the given children.
|
||||
///
|
||||
/// [`Row`]: widget::Row
|
||||
pub fn row<Message, Renderer>(
|
||||
children: Vec<Element<'_, Message, Renderer>>,
|
||||
) -> widget::Row<'_, Message, Renderer> {
|
||||
widget::Row::with_children(children)
|
||||
}
|
||||
|
||||
/// Creates a new [`Scrollable`] with the provided content.
|
||||
///
|
||||
/// [`Scrollable`]: widget::Scrollable
|
||||
pub fn scrollable<'a, Message, Renderer>(
|
||||
content: impl Into<Element<'a, Message, Renderer>>,
|
||||
) -> widget::Scrollable<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: crate::Renderer,
|
||||
Renderer::Theme: widget::scrollable::StyleSheet,
|
||||
{
|
||||
widget::Scrollable::new(content)
|
||||
}
|
||||
|
||||
/// Creates a new [`Button`] with the provided content.
|
||||
///
|
||||
/// [`Button`]: widget::Button
|
||||
pub fn button<'a, Message, Renderer>(
|
||||
content: impl Into<Element<'a, Message, Renderer>>,
|
||||
) -> widget::Button<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: crate::Renderer,
|
||||
Renderer::Theme: widget::button::StyleSheet,
|
||||
<Renderer::Theme as widget::button::StyleSheet>::Style: Default,
|
||||
{
|
||||
widget::Button::new(content)
|
||||
}
|
||||
|
||||
/// Creates a new [`Tooltip`] with the provided content, tooltip text, and [`tooltip::Position`].
|
||||
///
|
||||
/// [`Tooltip`]: widget::Tooltip
|
||||
/// [`tooltip::Position`]: widget::tooltip::Position
|
||||
pub fn tooltip<'a, Message, Renderer>(
|
||||
content: impl Into<Element<'a, Message, Renderer>>,
|
||||
tooltip: impl ToString,
|
||||
position: widget::tooltip::Position,
|
||||
) -> widget::Tooltip<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: crate::text::Renderer,
|
||||
Renderer::Theme: widget::container::StyleSheet + widget::text::StyleSheet,
|
||||
{
|
||||
widget::Tooltip::new(content, tooltip.to_string(), position)
|
||||
}
|
||||
|
||||
/// Creates a new [`Text`] widget with the provided content.
|
||||
///
|
||||
/// [`Text`]: widget::Text
|
||||
pub fn text<'a, Renderer>(text: impl ToString) -> widget::Text<'a, Renderer>
|
||||
where
|
||||
Renderer: crate::text::Renderer,
|
||||
Renderer::Theme: widget::text::StyleSheet,
|
||||
{
|
||||
widget::Text::new(text.to_string())
|
||||
}
|
||||
|
||||
/// Creates a new [`Checkbox`].
|
||||
///
|
||||
/// [`Checkbox`]: widget::Checkbox
|
||||
pub fn checkbox<'a, Message, Renderer>(
|
||||
label: impl Into<String>,
|
||||
is_checked: bool,
|
||||
f: impl Fn(bool) -> Message + 'a,
|
||||
) -> widget::Checkbox<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: crate::text::Renderer,
|
||||
Renderer::Theme: widget::checkbox::StyleSheet + widget::text::StyleSheet,
|
||||
{
|
||||
widget::Checkbox::new(label, is_checked, f)
|
||||
}
|
||||
|
||||
/// Creates a new [`Radio`].
|
||||
///
|
||||
/// [`Radio`]: widget::Radio
|
||||
pub fn radio<Message, Renderer, V>(
|
||||
label: impl Into<String>,
|
||||
value: V,
|
||||
selected: Option<V>,
|
||||
on_click: impl FnOnce(V) -> Message,
|
||||
) -> widget::Radio<Message, Renderer>
|
||||
where
|
||||
Message: Clone,
|
||||
Renderer: crate::text::Renderer,
|
||||
Renderer::Theme: widget::radio::StyleSheet,
|
||||
V: Copy + Eq,
|
||||
{
|
||||
widget::Radio::new(value, label, selected, on_click)
|
||||
}
|
||||
|
||||
/// Creates a new [`Toggler`].
|
||||
///
|
||||
/// [`Toggler`]: widget::Toggler
|
||||
pub fn toggler<'a, Message, Renderer>(
|
||||
label: impl Into<Option<String>>,
|
||||
is_checked: bool,
|
||||
f: impl Fn(bool) -> Message + 'a,
|
||||
) -> widget::Toggler<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: crate::text::Renderer,
|
||||
Renderer::Theme: widget::toggler::StyleSheet,
|
||||
{
|
||||
widget::Toggler::new(label, is_checked, f)
|
||||
}
|
||||
|
||||
/// Creates a new [`TextInput`].
|
||||
///
|
||||
/// [`TextInput`]: widget::TextInput
|
||||
pub fn text_input<'a, Message, Renderer>(
|
||||
placeholder: &str,
|
||||
value: &str,
|
||||
on_change: impl Fn(String) -> Message + 'a,
|
||||
) -> widget::TextInput<'a, Message, Renderer>
|
||||
where
|
||||
Message: Clone,
|
||||
Renderer: crate::text::Renderer,
|
||||
Renderer::Theme: widget::text_input::StyleSheet,
|
||||
{
|
||||
widget::TextInput::new(placeholder, value, on_change)
|
||||
}
|
||||
|
||||
/// Creates a new [`Slider`].
|
||||
///
|
||||
/// [`Slider`]: widget::Slider
|
||||
pub fn slider<'a, T, Message, Renderer>(
|
||||
range: std::ops::RangeInclusive<T>,
|
||||
value: T,
|
||||
on_change: impl Fn(T) -> Message + 'a,
|
||||
) -> widget::Slider<'a, T, Message, Renderer>
|
||||
where
|
||||
T: Copy + From<u8> + std::cmp::PartialOrd,
|
||||
Message: Clone,
|
||||
Renderer: crate::Renderer,
|
||||
Renderer::Theme: widget::slider::StyleSheet,
|
||||
{
|
||||
widget::Slider::new(range, value, on_change)
|
||||
}
|
||||
|
||||
/// Creates a new [`VerticalSlider`].
|
||||
///
|
||||
/// [`VerticalSlider`]: widget::VerticalSlider
|
||||
pub fn vertical_slider<'a, T, Message, Renderer>(
|
||||
range: std::ops::RangeInclusive<T>,
|
||||
value: T,
|
||||
on_change: impl Fn(T) -> Message + 'a,
|
||||
) -> widget::VerticalSlider<'a, T, Message, Renderer>
|
||||
where
|
||||
T: Copy + From<u8> + std::cmp::PartialOrd,
|
||||
Message: Clone,
|
||||
Renderer: crate::Renderer,
|
||||
Renderer::Theme: widget::slider::StyleSheet,
|
||||
{
|
||||
widget::VerticalSlider::new(range, value, on_change)
|
||||
}
|
||||
|
||||
/// Creates a new [`PickList`].
|
||||
///
|
||||
/// [`PickList`]: widget::PickList
|
||||
pub fn pick_list<'a, Message, Renderer, T>(
|
||||
options: impl Into<Cow<'a, [T]>>,
|
||||
selected: Option<T>,
|
||||
on_selected: impl Fn(T) -> Message + 'a,
|
||||
) -> widget::PickList<'a, T, Message, Renderer>
|
||||
where
|
||||
T: ToString + Eq + 'static,
|
||||
[T]: ToOwned<Owned = Vec<T>>,
|
||||
Renderer: crate::text::Renderer,
|
||||
Renderer::Theme: widget::pick_list::StyleSheet
|
||||
+ widget::scrollable::StyleSheet
|
||||
+ overlay::menu::StyleSheet
|
||||
+ widget::container::StyleSheet,
|
||||
<Renderer::Theme as overlay::menu::StyleSheet>::Style:
|
||||
From<<Renderer::Theme as widget::pick_list::StyleSheet>::Style>,
|
||||
{
|
||||
widget::PickList::new(options, selected, on_selected)
|
||||
}
|
||||
|
||||
/// Creates a new [`Image`].
|
||||
///
|
||||
/// [`Image`]: widget::Image
|
||||
pub fn image<Handle>(handle: impl Into<Handle>) -> widget::Image<Handle> {
|
||||
widget::Image::new(handle.into())
|
||||
}
|
||||
|
||||
/// Creates a new horizontal [`Space`] with the given [`Length`].
|
||||
///
|
||||
/// [`Space`]: widget::Space
|
||||
pub fn horizontal_space(width: impl Into<Length>) -> widget::Space {
|
||||
widget::Space::with_width(width)
|
||||
}
|
||||
|
||||
/// Creates a new vertical [`Space`] with the given [`Length`].
|
||||
///
|
||||
/// [`Space`]: widget::Space
|
||||
pub fn vertical_space(height: impl Into<Length>) -> widget::Space {
|
||||
widget::Space::with_height(height)
|
||||
}
|
||||
|
||||
/// Creates a horizontal [`Rule`] with the given height.
|
||||
///
|
||||
/// [`Rule`]: widget::Rule
|
||||
pub fn horizontal_rule<Renderer>(
|
||||
height: impl Into<Pixels>,
|
||||
) -> widget::Rule<Renderer>
|
||||
where
|
||||
Renderer: crate::Renderer,
|
||||
Renderer::Theme: widget::rule::StyleSheet,
|
||||
{
|
||||
widget::Rule::horizontal(height)
|
||||
}
|
||||
|
||||
/// Creates a vertical [`Rule`] with the given width.
|
||||
///
|
||||
/// [`Rule`]: widget::Rule
|
||||
pub fn vertical_rule<Renderer>(
|
||||
width: impl Into<Pixels>,
|
||||
) -> widget::Rule<Renderer>
|
||||
where
|
||||
Renderer: crate::Renderer,
|
||||
Renderer::Theme: widget::rule::StyleSheet,
|
||||
{
|
||||
widget::Rule::vertical(width)
|
||||
}
|
||||
|
||||
/// Creates a new [`ProgressBar`].
|
||||
///
|
||||
/// It expects:
|
||||
/// * an inclusive range of possible values, and
|
||||
/// * the current value of the [`ProgressBar`].
|
||||
///
|
||||
/// [`ProgressBar`]: widget::ProgressBar
|
||||
pub fn progress_bar<Renderer>(
|
||||
range: RangeInclusive<f32>,
|
||||
value: f32,
|
||||
) -> widget::ProgressBar<Renderer>
|
||||
where
|
||||
Renderer: crate::Renderer,
|
||||
Renderer::Theme: widget::progress_bar::StyleSheet,
|
||||
{
|
||||
widget::ProgressBar::new(range, value)
|
||||
}
|
||||
|
||||
/// Creates a new [`Svg`] widget from the given [`Handle`].
|
||||
///
|
||||
/// [`Svg`]: widget::Svg
|
||||
/// [`Handle`]: widget::svg::Handle
|
||||
pub fn svg<Renderer>(
|
||||
handle: impl Into<widget::svg::Handle>,
|
||||
) -> widget::Svg<Renderer>
|
||||
where
|
||||
Renderer: crate::svg::Renderer,
|
||||
Renderer::Theme: widget::svg::StyleSheet,
|
||||
{
|
||||
widget::Svg::new(handle)
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
use std::borrow;
|
||||
use std::sync::atomic::{self, AtomicUsize};
|
||||
|
||||
static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
/// The identifier of a generic widget.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct Id(Internal);
|
||||
|
||||
impl Id {
|
||||
/// Creates a custom [`Id`].
|
||||
pub fn new(id: impl Into<borrow::Cow<'static, str>>) -> Self {
|
||||
Self(Internal::Custom(id.into()))
|
||||
}
|
||||
|
||||
/// Creates a unique [`Id`].
|
||||
///
|
||||
/// This function produces a different [`Id`] every time it is called.
|
||||
pub fn unique() -> Self {
|
||||
let id = NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed);
|
||||
|
||||
Self(Internal::Unique(id))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum Internal {
|
||||
Unique(usize),
|
||||
Custom(borrow::Cow<'static, str>),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Id;
|
||||
|
||||
#[test]
|
||||
fn unique_generates_different_ids() {
|
||||
let a = Id::unique();
|
||||
let b = Id::unique();
|
||||
|
||||
assert_ne!(a, b);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,204 +0,0 @@
|
|||
//! Display images in your user interface.
|
||||
pub mod viewer;
|
||||
pub use viewer::Viewer;
|
||||
|
||||
use crate::image;
|
||||
use crate::layout;
|
||||
use crate::renderer;
|
||||
use crate::widget::Tree;
|
||||
use crate::{
|
||||
ContentFit, Element, Layout, Length, Point, Rectangle, Size, Vector, Widget,
|
||||
};
|
||||
|
||||
use std::hash::Hash;
|
||||
|
||||
/// Creates a new [`Viewer`] with the given image `Handle`.
|
||||
pub fn viewer<Handle>(handle: Handle) -> Viewer<Handle> {
|
||||
Viewer::new(handle)
|
||||
}
|
||||
|
||||
/// A frame that displays an image while keeping aspect ratio.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use iced_native::widget::Image;
|
||||
/// # use iced_native::image;
|
||||
/// #
|
||||
/// let image = Image::<image::Handle>::new("resources/ferris.png");
|
||||
/// ```
|
||||
///
|
||||
/// <img src="https://github.com/iced-rs/iced/blob/9712b319bb7a32848001b96bd84977430f14b623/examples/resources/ferris.png?raw=true" width="300">
|
||||
#[derive(Debug)]
|
||||
pub struct Image<Handle> {
|
||||
handle: Handle,
|
||||
width: Length,
|
||||
height: Length,
|
||||
content_fit: ContentFit,
|
||||
}
|
||||
|
||||
impl<Handle> Image<Handle> {
|
||||
/// Creates a new [`Image`] with the given path.
|
||||
pub fn new<T: Into<Handle>>(handle: T) -> Self {
|
||||
Image {
|
||||
handle: handle.into(),
|
||||
width: Length::Shrink,
|
||||
height: Length::Shrink,
|
||||
content_fit: ContentFit::Contain,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the width of the [`Image`] boundaries.
|
||||
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||
self.width = width.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the height of the [`Image`] boundaries.
|
||||
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
||||
self.height = height.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`ContentFit`] of the [`Image`].
|
||||
///
|
||||
/// Defaults to [`ContentFit::Contain`]
|
||||
pub fn content_fit(self, content_fit: ContentFit) -> Self {
|
||||
Self {
|
||||
content_fit,
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes the layout of an [`Image`].
|
||||
pub fn layout<Renderer, Handle>(
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
handle: &Handle,
|
||||
width: Length,
|
||||
height: Length,
|
||||
content_fit: ContentFit,
|
||||
) -> layout::Node
|
||||
where
|
||||
Renderer: image::Renderer<Handle = Handle>,
|
||||
{
|
||||
// The raw w/h of the underlying image
|
||||
let image_size = {
|
||||
let Size { width, height } = renderer.dimensions(handle);
|
||||
|
||||
Size::new(width as f32, height as f32)
|
||||
};
|
||||
|
||||
// The size to be available to the widget prior to `Shrink`ing
|
||||
let raw_size = limits.width(width).height(height).resolve(image_size);
|
||||
|
||||
// The uncropped size of the image when fit to the bounds above
|
||||
let full_size = content_fit.fit(image_size, raw_size);
|
||||
|
||||
// Shrink the widget to fit the resized image, if requested
|
||||
let final_size = Size {
|
||||
width: match width {
|
||||
Length::Shrink => f32::min(raw_size.width, full_size.width),
|
||||
_ => raw_size.width,
|
||||
},
|
||||
height: match height {
|
||||
Length::Shrink => f32::min(raw_size.height, full_size.height),
|
||||
_ => raw_size.height,
|
||||
},
|
||||
};
|
||||
|
||||
layout::Node::new(final_size)
|
||||
}
|
||||
|
||||
/// Draws an [`Image`]
|
||||
pub fn draw<Renderer, Handle>(
|
||||
renderer: &mut Renderer,
|
||||
layout: Layout<'_>,
|
||||
handle: &Handle,
|
||||
content_fit: ContentFit,
|
||||
) where
|
||||
Renderer: image::Renderer<Handle = Handle>,
|
||||
Handle: Clone + Hash,
|
||||
{
|
||||
let Size { width, height } = renderer.dimensions(handle);
|
||||
let image_size = Size::new(width as f32, height as f32);
|
||||
|
||||
let bounds = layout.bounds();
|
||||
let adjusted_fit = content_fit.fit(image_size, bounds.size());
|
||||
|
||||
let render = |renderer: &mut Renderer| {
|
||||
let offset = Vector::new(
|
||||
(bounds.width - adjusted_fit.width).max(0.0) / 2.0,
|
||||
(bounds.height - adjusted_fit.height).max(0.0) / 2.0,
|
||||
);
|
||||
|
||||
let drawing_bounds = Rectangle {
|
||||
width: adjusted_fit.width,
|
||||
height: adjusted_fit.height,
|
||||
..bounds
|
||||
};
|
||||
|
||||
renderer.draw(handle.clone(), drawing_bounds + offset)
|
||||
};
|
||||
|
||||
if adjusted_fit.width > bounds.width || adjusted_fit.height > bounds.height
|
||||
{
|
||||
renderer.with_layer(bounds, render);
|
||||
} else {
|
||||
render(renderer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Message, Renderer, Handle> Widget<Message, Renderer> for Image<Handle>
|
||||
where
|
||||
Renderer: image::Renderer<Handle = Handle>,
|
||||
Handle: Clone + Hash,
|
||||
{
|
||||
fn width(&self) -> Length {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
layout(
|
||||
renderer,
|
||||
limits,
|
||||
&self.handle,
|
||||
self.width,
|
||||
self.height,
|
||||
self.content_fit,
|
||||
)
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
_state: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
_theme: &Renderer::Theme,
|
||||
_style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
_cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
) {
|
||||
draw(renderer, layout, &self.handle, self.content_fit)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer, Handle> From<Image<Handle>>
|
||||
for Element<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: image::Renderer<Handle = Handle>,
|
||||
Handle: Clone + Hash + 'a,
|
||||
{
|
||||
fn from(image: Image<Handle>) -> Element<'a, Message, Renderer> {
|
||||
Element::new(image)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,428 +0,0 @@
|
|||
//! Zoom and pan on an image.
|
||||
use crate::event::{self, Event};
|
||||
use crate::image;
|
||||
use crate::layout;
|
||||
use crate::mouse;
|
||||
use crate::renderer;
|
||||
use crate::widget::tree::{self, Tree};
|
||||
use crate::{
|
||||
Clipboard, Element, Layout, Length, Pixels, Point, Rectangle, Shell, Size,
|
||||
Vector, Widget,
|
||||
};
|
||||
|
||||
use std::hash::Hash;
|
||||
|
||||
/// A frame that displays an image with the ability to zoom in/out and pan.
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Viewer<Handle> {
|
||||
padding: f32,
|
||||
width: Length,
|
||||
height: Length,
|
||||
min_scale: f32,
|
||||
max_scale: f32,
|
||||
scale_step: f32,
|
||||
handle: Handle,
|
||||
}
|
||||
|
||||
impl<Handle> Viewer<Handle> {
|
||||
/// Creates a new [`Viewer`] with the given [`State`].
|
||||
pub fn new(handle: Handle) -> Self {
|
||||
Viewer {
|
||||
padding: 0.0,
|
||||
width: Length::Shrink,
|
||||
height: Length::Shrink,
|
||||
min_scale: 0.25,
|
||||
max_scale: 10.0,
|
||||
scale_step: 0.10,
|
||||
handle,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the padding of the [`Viewer`].
|
||||
pub fn padding(mut self, padding: impl Into<Pixels>) -> Self {
|
||||
self.padding = padding.into().0;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the width of the [`Viewer`].
|
||||
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||
self.width = width.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the height of the [`Viewer`].
|
||||
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
||||
self.height = height.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the max scale applied to the image of the [`Viewer`].
|
||||
///
|
||||
/// Default is `10.0`
|
||||
pub fn max_scale(mut self, max_scale: f32) -> Self {
|
||||
self.max_scale = max_scale;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the min scale applied to the image of the [`Viewer`].
|
||||
///
|
||||
/// Default is `0.25`
|
||||
pub fn min_scale(mut self, min_scale: f32) -> Self {
|
||||
self.min_scale = min_scale;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the percentage the image of the [`Viewer`] will be scaled by
|
||||
/// when zoomed in / out.
|
||||
///
|
||||
/// Default is `0.10`
|
||||
pub fn scale_step(mut self, scale_step: f32) -> Self {
|
||||
self.scale_step = scale_step;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<Message, Renderer, Handle> Widget<Message, Renderer> for Viewer<Handle>
|
||||
where
|
||||
Renderer: image::Renderer<Handle = Handle>,
|
||||
Handle: Clone + Hash,
|
||||
{
|
||||
fn tag(&self) -> tree::Tag {
|
||||
tree::Tag::of::<State>()
|
||||
}
|
||||
|
||||
fn state(&self) -> tree::State {
|
||||
tree::State::new(State::new())
|
||||
}
|
||||
|
||||
fn width(&self) -> Length {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
let Size { width, height } = renderer.dimensions(&self.handle);
|
||||
|
||||
let mut size = limits
|
||||
.width(self.width)
|
||||
.height(self.height)
|
||||
.resolve(Size::new(width as f32, height as f32));
|
||||
|
||||
let expansion_size = if height > width {
|
||||
self.width
|
||||
} else {
|
||||
self.height
|
||||
};
|
||||
|
||||
// Only calculate viewport sizes if the images are constrained to a limited space.
|
||||
// If they are Fill|Portion let them expand within their alotted space.
|
||||
match expansion_size {
|
||||
Length::Shrink | Length::Fixed(_) => {
|
||||
let aspect_ratio = width as f32 / height as f32;
|
||||
let viewport_aspect_ratio = size.width / size.height;
|
||||
if viewport_aspect_ratio > aspect_ratio {
|
||||
size.width = width as f32 * size.height / height as f32;
|
||||
} else {
|
||||
size.height = height as f32 * size.width / width as f32;
|
||||
}
|
||||
}
|
||||
Length::Fill | Length::FillPortion(_) => {}
|
||||
}
|
||||
|
||||
layout::Node::new(size)
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
renderer: &Renderer,
|
||||
_clipboard: &mut dyn Clipboard,
|
||||
_shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
let bounds = layout.bounds();
|
||||
let is_mouse_over = bounds.contains(cursor_position);
|
||||
|
||||
match event {
|
||||
Event::Mouse(mouse::Event::WheelScrolled { delta })
|
||||
if is_mouse_over =>
|
||||
{
|
||||
match delta {
|
||||
mouse::ScrollDelta::Lines { y, .. }
|
||||
| mouse::ScrollDelta::Pixels { y, .. } => {
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
let previous_scale = state.scale;
|
||||
|
||||
if y < 0.0 && previous_scale > self.min_scale
|
||||
|| y > 0.0 && previous_scale < self.max_scale
|
||||
{
|
||||
state.scale = (if y > 0.0 {
|
||||
state.scale * (1.0 + self.scale_step)
|
||||
} else {
|
||||
state.scale / (1.0 + self.scale_step)
|
||||
})
|
||||
.clamp(self.min_scale, self.max_scale);
|
||||
|
||||
let image_size = image_size(
|
||||
renderer,
|
||||
&self.handle,
|
||||
state,
|
||||
bounds.size(),
|
||||
);
|
||||
|
||||
let factor = state.scale / previous_scale - 1.0;
|
||||
|
||||
let cursor_to_center =
|
||||
cursor_position - bounds.center();
|
||||
|
||||
let adjustment = cursor_to_center * factor
|
||||
+ state.current_offset * factor;
|
||||
|
||||
state.current_offset = Vector::new(
|
||||
if image_size.width > bounds.width {
|
||||
state.current_offset.x + adjustment.x
|
||||
} else {
|
||||
0.0
|
||||
},
|
||||
if image_size.height > bounds.height {
|
||||
state.current_offset.y + adjustment.y
|
||||
} else {
|
||||
0.0
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
event::Status::Captured
|
||||
}
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
|
||||
if is_mouse_over =>
|
||||
{
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
|
||||
state.cursor_grabbed_at = Some(cursor_position);
|
||||
state.starting_offset = state.current_offset;
|
||||
|
||||
event::Status::Captured
|
||||
}
|
||||
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
|
||||
if state.cursor_grabbed_at.is_some() {
|
||||
state.cursor_grabbed_at = None;
|
||||
|
||||
event::Status::Captured
|
||||
} else {
|
||||
event::Status::Ignored
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse::Event::CursorMoved { position }) => {
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
|
||||
if let Some(origin) = state.cursor_grabbed_at {
|
||||
let image_size = image_size(
|
||||
renderer,
|
||||
&self.handle,
|
||||
state,
|
||||
bounds.size(),
|
||||
);
|
||||
|
||||
let hidden_width = (image_size.width - bounds.width / 2.0)
|
||||
.max(0.0)
|
||||
.round();
|
||||
|
||||
let hidden_height = (image_size.height
|
||||
- bounds.height / 2.0)
|
||||
.max(0.0)
|
||||
.round();
|
||||
|
||||
let delta = position - origin;
|
||||
|
||||
let x = if bounds.width < image_size.width {
|
||||
(state.starting_offset.x - delta.x)
|
||||
.clamp(-hidden_width, hidden_width)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let y = if bounds.height < image_size.height {
|
||||
(state.starting_offset.y - delta.y)
|
||||
.clamp(-hidden_height, hidden_height)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
state.current_offset = Vector::new(x, y);
|
||||
|
||||
event::Status::Captured
|
||||
} else {
|
||||
event::Status::Ignored
|
||||
}
|
||||
}
|
||||
_ => event::Status::Ignored,
|
||||
}
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
_renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
let state = tree.state.downcast_ref::<State>();
|
||||
let bounds = layout.bounds();
|
||||
let is_mouse_over = bounds.contains(cursor_position);
|
||||
|
||||
if state.is_cursor_grabbed() {
|
||||
mouse::Interaction::Grabbing
|
||||
} else if is_mouse_over {
|
||||
mouse::Interaction::Grab
|
||||
} else {
|
||||
mouse::Interaction::Idle
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
_theme: &Renderer::Theme,
|
||||
_style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
_cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
) {
|
||||
let state = tree.state.downcast_ref::<State>();
|
||||
let bounds = layout.bounds();
|
||||
|
||||
let image_size =
|
||||
image_size(renderer, &self.handle, state, bounds.size());
|
||||
|
||||
let translation = {
|
||||
let image_top_left = Vector::new(
|
||||
bounds.width / 2.0 - image_size.width / 2.0,
|
||||
bounds.height / 2.0 - image_size.height / 2.0,
|
||||
);
|
||||
|
||||
image_top_left - state.offset(bounds, image_size)
|
||||
};
|
||||
|
||||
renderer.with_layer(bounds, |renderer| {
|
||||
renderer.with_translation(translation, |renderer| {
|
||||
image::Renderer::draw(
|
||||
renderer,
|
||||
self.handle.clone(),
|
||||
Rectangle {
|
||||
x: bounds.x,
|
||||
y: bounds.y,
|
||||
..Rectangle::with_size(image_size)
|
||||
},
|
||||
)
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// The local state of a [`Viewer`].
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct State {
|
||||
scale: f32,
|
||||
starting_offset: Vector,
|
||||
current_offset: Vector,
|
||||
cursor_grabbed_at: Option<Point>,
|
||||
}
|
||||
|
||||
impl Default for State {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
scale: 1.0,
|
||||
starting_offset: Vector::default(),
|
||||
current_offset: Vector::default(),
|
||||
cursor_grabbed_at: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl State {
|
||||
/// Creates a new [`State`].
|
||||
pub fn new() -> Self {
|
||||
State::default()
|
||||
}
|
||||
|
||||
/// Returns the current offset of the [`State`], given the bounds
|
||||
/// of the [`Viewer`] and its image.
|
||||
fn offset(&self, bounds: Rectangle, image_size: Size) -> Vector {
|
||||
let hidden_width =
|
||||
(image_size.width - bounds.width / 2.0).max(0.0).round();
|
||||
|
||||
let hidden_height =
|
||||
(image_size.height - bounds.height / 2.0).max(0.0).round();
|
||||
|
||||
Vector::new(
|
||||
self.current_offset.x.clamp(-hidden_width, hidden_width),
|
||||
self.current_offset.y.clamp(-hidden_height, hidden_height),
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns if the cursor is currently grabbed by the [`Viewer`].
|
||||
pub fn is_cursor_grabbed(&self) -> bool {
|
||||
self.cursor_grabbed_at.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer, Handle> From<Viewer<Handle>>
|
||||
for Element<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: 'a + image::Renderer<Handle = Handle>,
|
||||
Message: 'a,
|
||||
Handle: Clone + Hash + 'a,
|
||||
{
|
||||
fn from(viewer: Viewer<Handle>) -> Element<'a, Message, Renderer> {
|
||||
Element::new(viewer)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the bounds of the underlying image, given the bounds of
|
||||
/// the [`Viewer`]. Scaling will be applied and original aspect ratio
|
||||
/// will be respected.
|
||||
pub fn image_size<Renderer>(
|
||||
renderer: &Renderer,
|
||||
handle: &<Renderer as image::Renderer>::Handle,
|
||||
state: &State,
|
||||
bounds: Size,
|
||||
) -> Size
|
||||
where
|
||||
Renderer: image::Renderer,
|
||||
{
|
||||
let Size { width, height } = renderer.dimensions(handle);
|
||||
|
||||
let (width, height) = {
|
||||
let dimensions = (width as f32, height as f32);
|
||||
|
||||
let width_ratio = bounds.width / dimensions.0;
|
||||
let height_ratio = bounds.height / dimensions.1;
|
||||
|
||||
let ratio = width_ratio.min(height_ratio);
|
||||
let scale = state.scale;
|
||||
|
||||
if ratio < 1.0 {
|
||||
(dimensions.0 * ratio * scale, dimensions.1 * ratio * scale)
|
||||
} else {
|
||||
(dimensions.0 * scale, dimensions.1 * scale)
|
||||
}
|
||||
};
|
||||
|
||||
Size::new(width, height)
|
||||
}
|
||||
|
|
@ -1,112 +0,0 @@
|
|||
//! Query or update internal widget state.
|
||||
pub mod focusable;
|
||||
pub mod scrollable;
|
||||
pub mod text_input;
|
||||
|
||||
pub use focusable::Focusable;
|
||||
pub use scrollable::Scrollable;
|
||||
pub use text_input::TextInput;
|
||||
|
||||
use crate::widget::Id;
|
||||
|
||||
use std::any::Any;
|
||||
use std::fmt;
|
||||
|
||||
/// A piece of logic that can traverse the widget tree of an application in
|
||||
/// order to query or update some widget state.
|
||||
pub trait Operation<T> {
|
||||
/// Operates on a widget that contains other widgets.
|
||||
///
|
||||
/// The `operate_on_children` function can be called to return control to
|
||||
/// the widget tree and keep traversing it.
|
||||
fn container(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
|
||||
);
|
||||
|
||||
/// Operates on a widget that can be focused.
|
||||
fn focusable(&mut self, _state: &mut dyn Focusable, _id: Option<&Id>) {}
|
||||
|
||||
/// Operates on a widget that can be scrolled.
|
||||
fn scrollable(&mut self, _state: &mut dyn Scrollable, _id: Option<&Id>) {}
|
||||
|
||||
/// Operates on a widget that has text input.
|
||||
fn text_input(&mut self, _state: &mut dyn TextInput, _id: Option<&Id>) {}
|
||||
|
||||
/// Operates on a custom widget with some state.
|
||||
fn custom(&mut self, _state: &mut dyn Any, _id: Option<&Id>) {}
|
||||
|
||||
/// Finishes the [`Operation`] and returns its [`Outcome`].
|
||||
fn finish(&self) -> Outcome<T> {
|
||||
Outcome::None
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of an [`Operation`].
|
||||
pub enum Outcome<T> {
|
||||
/// The [`Operation`] produced no result.
|
||||
None,
|
||||
|
||||
/// The [`Operation`] produced some result.
|
||||
Some(T),
|
||||
|
||||
/// The [`Operation`] needs to be followed by another [`Operation`].
|
||||
Chain(Box<dyn Operation<T>>),
|
||||
}
|
||||
|
||||
impl<T> fmt::Debug for Outcome<T>
|
||||
where
|
||||
T: fmt::Debug,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::None => write!(f, "Outcome::None"),
|
||||
Self::Some(output) => write!(f, "Outcome::Some({output:?})"),
|
||||
Self::Chain(_) => write!(f, "Outcome::Chain(...)"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Produces an [`Operation`] that applies the given [`Operation`] to the
|
||||
/// children of a container with the given [`Id`].
|
||||
pub fn scoped<T: 'static>(
|
||||
target: Id,
|
||||
operation: impl Operation<T> + 'static,
|
||||
) -> impl Operation<T> {
|
||||
struct ScopedOperation<Message> {
|
||||
target: Id,
|
||||
operation: Box<dyn Operation<Message>>,
|
||||
}
|
||||
|
||||
impl<Message: 'static> Operation<Message> for ScopedOperation<Message> {
|
||||
fn container(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
operate_on_children: &mut dyn FnMut(&mut dyn Operation<Message>),
|
||||
) {
|
||||
if id == Some(&self.target) {
|
||||
operate_on_children(self.operation.as_mut());
|
||||
} else {
|
||||
operate_on_children(self);
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(&self) -> Outcome<Message> {
|
||||
match self.operation.finish() {
|
||||
Outcome::Chain(next) => {
|
||||
Outcome::Chain(Box::new(ScopedOperation {
|
||||
target: self.target.clone(),
|
||||
operation: next,
|
||||
}))
|
||||
}
|
||||
outcome => outcome,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ScopedOperation {
|
||||
target,
|
||||
operation: Box::new(operation),
|
||||
}
|
||||
}
|
||||
|
|
@ -1,203 +0,0 @@
|
|||
//! Operate on widgets that can be focused.
|
||||
use crate::widget::operation::{Operation, Outcome};
|
||||
use crate::widget::Id;
|
||||
|
||||
/// The internal state of a widget that can be focused.
|
||||
pub trait Focusable {
|
||||
/// Returns whether the widget is focused or not.
|
||||
fn is_focused(&self) -> bool;
|
||||
|
||||
/// Focuses the widget.
|
||||
fn focus(&mut self);
|
||||
|
||||
/// Unfocuses the widget.
|
||||
fn unfocus(&mut self);
|
||||
}
|
||||
|
||||
/// A summary of the focusable widgets present on a widget tree.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct Count {
|
||||
/// The index of the current focused widget, if any.
|
||||
pub focused: Option<usize>,
|
||||
|
||||
/// The total amount of focusable widgets.
|
||||
pub total: usize,
|
||||
}
|
||||
|
||||
/// Produces an [`Operation`] that focuses the widget with the given [`Id`].
|
||||
pub fn focus<T>(target: Id) -> impl Operation<T> {
|
||||
struct Focus {
|
||||
target: Id,
|
||||
}
|
||||
|
||||
impl<T> Operation<T> for Focus {
|
||||
fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) {
|
||||
match id {
|
||||
Some(id) if id == &self.target => {
|
||||
state.focus();
|
||||
}
|
||||
_ => {
|
||||
state.unfocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn container(
|
||||
&mut self,
|
||||
_id: Option<&Id>,
|
||||
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
|
||||
) {
|
||||
operate_on_children(self)
|
||||
}
|
||||
}
|
||||
|
||||
Focus { target }
|
||||
}
|
||||
|
||||
/// Produces an [`Operation`] that generates a [`Count`] and chains it with the
|
||||
/// provided function to build a new [`Operation`].
|
||||
pub fn count<T, O>(f: fn(Count) -> O) -> impl Operation<T>
|
||||
where
|
||||
O: Operation<T> + 'static,
|
||||
{
|
||||
struct CountFocusable<O> {
|
||||
count: Count,
|
||||
next: fn(Count) -> O,
|
||||
}
|
||||
|
||||
impl<T, O> Operation<T> for CountFocusable<O>
|
||||
where
|
||||
O: Operation<T> + 'static,
|
||||
{
|
||||
fn focusable(&mut self, state: &mut dyn Focusable, _id: Option<&Id>) {
|
||||
if state.is_focused() {
|
||||
self.count.focused = Some(self.count.total);
|
||||
}
|
||||
|
||||
self.count.total += 1;
|
||||
}
|
||||
|
||||
fn container(
|
||||
&mut self,
|
||||
_id: Option<&Id>,
|
||||
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
|
||||
) {
|
||||
operate_on_children(self)
|
||||
}
|
||||
|
||||
fn finish(&self) -> Outcome<T> {
|
||||
Outcome::Chain(Box::new((self.next)(self.count)))
|
||||
}
|
||||
}
|
||||
|
||||
CountFocusable {
|
||||
count: Count::default(),
|
||||
next: f,
|
||||
}
|
||||
}
|
||||
|
||||
/// Produces an [`Operation`] that searches for the current focused widget, and
|
||||
/// - if found, focuses the previous focusable widget.
|
||||
/// - if not found, focuses the last focusable widget.
|
||||
pub fn focus_previous<T>() -> impl Operation<T> {
|
||||
struct FocusPrevious {
|
||||
count: Count,
|
||||
current: usize,
|
||||
}
|
||||
|
||||
impl<T> Operation<T> for FocusPrevious {
|
||||
fn focusable(&mut self, state: &mut dyn Focusable, _id: Option<&Id>) {
|
||||
if self.count.total == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
match self.count.focused {
|
||||
None if self.current == self.count.total - 1 => state.focus(),
|
||||
Some(0) if self.current == 0 => state.unfocus(),
|
||||
Some(0) => {}
|
||||
Some(focused) if focused == self.current => state.unfocus(),
|
||||
Some(focused) if focused - 1 == self.current => state.focus(),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
self.current += 1;
|
||||
}
|
||||
|
||||
fn container(
|
||||
&mut self,
|
||||
_id: Option<&Id>,
|
||||
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
|
||||
) {
|
||||
operate_on_children(self)
|
||||
}
|
||||
}
|
||||
|
||||
count(|count| FocusPrevious { count, current: 0 })
|
||||
}
|
||||
|
||||
/// Produces an [`Operation`] that searches for the current focused widget, and
|
||||
/// - if found, focuses the next focusable widget.
|
||||
/// - if not found, focuses the first focusable widget.
|
||||
pub fn focus_next<T>() -> impl Operation<T> {
|
||||
struct FocusNext {
|
||||
count: Count,
|
||||
current: usize,
|
||||
}
|
||||
|
||||
impl<T> Operation<T> for FocusNext {
|
||||
fn focusable(&mut self, state: &mut dyn Focusable, _id: Option<&Id>) {
|
||||
match self.count.focused {
|
||||
None if self.current == 0 => state.focus(),
|
||||
Some(focused) if focused == self.current => state.unfocus(),
|
||||
Some(focused) if focused + 1 == self.current => state.focus(),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
self.current += 1;
|
||||
}
|
||||
|
||||
fn container(
|
||||
&mut self,
|
||||
_id: Option<&Id>,
|
||||
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
|
||||
) {
|
||||
operate_on_children(self)
|
||||
}
|
||||
}
|
||||
|
||||
count(|count| FocusNext { count, current: 0 })
|
||||
}
|
||||
|
||||
/// Produces an [`Operation`] that searches for the current focused widget
|
||||
/// and stores its ID. This ignores widgets that do not have an ID.
|
||||
pub fn find_focused() -> impl Operation<Id> {
|
||||
struct FindFocused {
|
||||
focused: Option<Id>,
|
||||
}
|
||||
|
||||
impl Operation<Id> for FindFocused {
|
||||
fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) {
|
||||
if state.is_focused() && id.is_some() {
|
||||
self.focused = id.cloned();
|
||||
}
|
||||
}
|
||||
|
||||
fn container(
|
||||
&mut self,
|
||||
_id: Option<&Id>,
|
||||
operate_on_children: &mut dyn FnMut(&mut dyn Operation<Id>),
|
||||
) {
|
||||
operate_on_children(self)
|
||||
}
|
||||
|
||||
fn finish(&self) -> Outcome<Id> {
|
||||
if let Some(id) = &self.focused {
|
||||
Outcome::Some(id.clone())
|
||||
} else {
|
||||
Outcome::None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FindFocused { focused: None }
|
||||
}
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
//! Operate on widgets that can be scrolled.
|
||||
use crate::widget::{Id, Operation};
|
||||
|
||||
/// The internal state of a widget that can be scrolled.
|
||||
pub trait Scrollable {
|
||||
/// Snaps the scroll of the widget to the given `percentage` along the horizontal & vertical axis.
|
||||
fn snap_to(&mut self, offset: RelativeOffset);
|
||||
}
|
||||
|
||||
/// Produces an [`Operation`] that snaps the widget with the given [`Id`] to
|
||||
/// the provided `percentage`.
|
||||
pub fn snap_to<T>(target: Id, offset: RelativeOffset) -> impl Operation<T> {
|
||||
struct SnapTo {
|
||||
target: Id,
|
||||
offset: RelativeOffset,
|
||||
}
|
||||
|
||||
impl<T> Operation<T> for SnapTo {
|
||||
fn container(
|
||||
&mut self,
|
||||
_id: Option<&Id>,
|
||||
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
|
||||
) {
|
||||
operate_on_children(self)
|
||||
}
|
||||
|
||||
fn scrollable(&mut self, state: &mut dyn Scrollable, id: Option<&Id>) {
|
||||
if Some(&self.target) == id {
|
||||
state.snap_to(self.offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SnapTo { target, offset }
|
||||
}
|
||||
|
||||
/// The amount of offset in each direction of a [`Scrollable`].
|
||||
///
|
||||
/// A value of `0.0` means start, while `1.0` means end.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||
pub struct RelativeOffset {
|
||||
/// The amount of horizontal offset
|
||||
pub x: f32,
|
||||
/// The amount of vertical offset
|
||||
pub y: f32,
|
||||
}
|
||||
|
||||
impl RelativeOffset {
|
||||
/// A relative offset that points to the top-left of a [`Scrollable`].
|
||||
pub const START: Self = Self { x: 0.0, y: 0.0 };
|
||||
|
||||
/// A relative offset that points to the bottom-right of a [`Scrollable`].
|
||||
pub const END: Self = Self { x: 1.0, y: 1.0 };
|
||||
}
|
||||
|
|
@ -1,131 +0,0 @@
|
|||
//! Operate on widgets that have text input.
|
||||
use crate::widget::operation::Operation;
|
||||
use crate::widget::Id;
|
||||
|
||||
/// The internal state of a widget that has text input.
|
||||
pub trait TextInput {
|
||||
/// Moves the cursor of the text input to the front of the input text.
|
||||
fn move_cursor_to_front(&mut self);
|
||||
/// Moves the cursor of the text input to the end of the input text.
|
||||
fn move_cursor_to_end(&mut self);
|
||||
/// Moves the cursor of the text input to an arbitrary location.
|
||||
fn move_cursor_to(&mut self, position: usize);
|
||||
/// Selects all the content of the text input.
|
||||
fn select_all(&mut self);
|
||||
}
|
||||
|
||||
/// Produces an [`Operation`] that moves the cursor of the widget with the given [`Id`] to the
|
||||
/// front.
|
||||
pub fn move_cursor_to_front<T>(target: Id) -> impl Operation<T> {
|
||||
struct MoveCursor {
|
||||
target: Id,
|
||||
}
|
||||
|
||||
impl<T> Operation<T> for MoveCursor {
|
||||
fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) {
|
||||
match id {
|
||||
Some(id) if id == &self.target => {
|
||||
state.move_cursor_to_front();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn container(
|
||||
&mut self,
|
||||
_id: Option<&Id>,
|
||||
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
|
||||
) {
|
||||
operate_on_children(self)
|
||||
}
|
||||
}
|
||||
|
||||
MoveCursor { target }
|
||||
}
|
||||
|
||||
/// Produces an [`Operation`] that moves the cursor of the widget with the given [`Id`] to the
|
||||
/// end.
|
||||
pub fn move_cursor_to_end<T>(target: Id) -> impl Operation<T> {
|
||||
struct MoveCursor {
|
||||
target: Id,
|
||||
}
|
||||
|
||||
impl<T> Operation<T> for MoveCursor {
|
||||
fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) {
|
||||
match id {
|
||||
Some(id) if id == &self.target => {
|
||||
state.move_cursor_to_end();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn container(
|
||||
&mut self,
|
||||
_id: Option<&Id>,
|
||||
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
|
||||
) {
|
||||
operate_on_children(self)
|
||||
}
|
||||
}
|
||||
|
||||
MoveCursor { target }
|
||||
}
|
||||
|
||||
/// Produces an [`Operation`] that moves the cursor of the widget with the given [`Id`] to the
|
||||
/// provided position.
|
||||
pub fn move_cursor_to<T>(target: Id, position: usize) -> impl Operation<T> {
|
||||
struct MoveCursor {
|
||||
target: Id,
|
||||
position: usize,
|
||||
}
|
||||
|
||||
impl<T> Operation<T> for MoveCursor {
|
||||
fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) {
|
||||
match id {
|
||||
Some(id) if id == &self.target => {
|
||||
state.move_cursor_to(self.position);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn container(
|
||||
&mut self,
|
||||
_id: Option<&Id>,
|
||||
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
|
||||
) {
|
||||
operate_on_children(self)
|
||||
}
|
||||
}
|
||||
|
||||
MoveCursor { target, position }
|
||||
}
|
||||
|
||||
/// Produces an [`Operation`] that selects all the content of the widget with the given [`Id`].
|
||||
pub fn select_all<T>(target: Id) -> impl Operation<T> {
|
||||
struct MoveCursor {
|
||||
target: Id,
|
||||
}
|
||||
|
||||
impl<T> Operation<T> for MoveCursor {
|
||||
fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) {
|
||||
match id {
|
||||
Some(id) if id == &self.target => {
|
||||
state.select_all();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn container(
|
||||
&mut self,
|
||||
_id: Option<&Id>,
|
||||
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
|
||||
) {
|
||||
operate_on_children(self)
|
||||
}
|
||||
}
|
||||
|
||||
MoveCursor { target }
|
||||
}
|
||||
|
|
@ -1,991 +0,0 @@
|
|||
//! Let your users split regions of your application and organize layout dynamically.
|
||||
//!
|
||||
//! [](https://gfycat.com/mixedflatjellyfish)
|
||||
//!
|
||||
//! # Example
|
||||
//! The [`pane_grid` example] showcases how to use a [`PaneGrid`] with resizing,
|
||||
//! drag and drop, and hotkey support.
|
||||
//!
|
||||
//! [`pane_grid` example]: https://github.com/iced-rs/iced/tree/0.8/examples/pane_grid
|
||||
mod axis;
|
||||
mod configuration;
|
||||
mod content;
|
||||
mod direction;
|
||||
mod draggable;
|
||||
mod node;
|
||||
mod pane;
|
||||
mod split;
|
||||
mod title_bar;
|
||||
|
||||
pub mod state;
|
||||
|
||||
pub use axis::Axis;
|
||||
pub use configuration::Configuration;
|
||||
pub use content::Content;
|
||||
pub use direction::Direction;
|
||||
pub use draggable::Draggable;
|
||||
pub use node::Node;
|
||||
pub use pane::Pane;
|
||||
pub use split::Split;
|
||||
pub use state::State;
|
||||
pub use title_bar::TitleBar;
|
||||
|
||||
pub use iced_style::pane_grid::{Line, StyleSheet};
|
||||
|
||||
use crate::event::{self, Event};
|
||||
use crate::layout;
|
||||
use crate::mouse;
|
||||
use crate::overlay::{self, Group};
|
||||
use crate::renderer;
|
||||
use crate::touch;
|
||||
use crate::widget;
|
||||
use crate::widget::container;
|
||||
use crate::widget::tree::{self, Tree};
|
||||
use crate::{
|
||||
Clipboard, Color, Element, Layout, Length, Pixels, Point, Rectangle, Shell,
|
||||
Size, Vector, Widget,
|
||||
};
|
||||
|
||||
/// A collection of panes distributed using either vertical or horizontal splits
|
||||
/// to completely fill the space available.
|
||||
///
|
||||
/// [](https://gfycat.com/frailfreshairedaleterrier)
|
||||
///
|
||||
/// This distribution of space is common in tiling window managers (like
|
||||
/// [`awesome`](https://awesomewm.org/), [`i3`](https://i3wm.org/), or even
|
||||
/// [`tmux`](https://github.com/tmux/tmux)).
|
||||
///
|
||||
/// A [`PaneGrid`] supports:
|
||||
///
|
||||
/// * Vertical and horizontal splits
|
||||
/// * Tracking of the last active pane
|
||||
/// * Mouse-based resizing
|
||||
/// * Drag and drop to reorganize panes
|
||||
/// * Hotkey support
|
||||
/// * Configurable modifier keys
|
||||
/// * [`State`] API to perform actions programmatically (`split`, `swap`, `resize`, etc.)
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```
|
||||
/// # use iced_native::widget::{pane_grid, text};
|
||||
/// #
|
||||
/// # type PaneGrid<'a, Message> =
|
||||
/// # iced_native::widget::PaneGrid<'a, Message, iced_native::renderer::Null>;
|
||||
/// #
|
||||
/// enum PaneState {
|
||||
/// SomePane,
|
||||
/// AnotherKindOfPane,
|
||||
/// }
|
||||
///
|
||||
/// enum Message {
|
||||
/// PaneDragged(pane_grid::DragEvent),
|
||||
/// PaneResized(pane_grid::ResizeEvent),
|
||||
/// }
|
||||
///
|
||||
/// let (mut state, _) = pane_grid::State::new(PaneState::SomePane);
|
||||
///
|
||||
/// let pane_grid =
|
||||
/// PaneGrid::new(&state, |pane, state, is_maximized| {
|
||||
/// pane_grid::Content::new(match state {
|
||||
/// PaneState::SomePane => text("This is some pane"),
|
||||
/// PaneState::AnotherKindOfPane => text("This is another kind of pane"),
|
||||
/// })
|
||||
/// })
|
||||
/// .on_drag(Message::PaneDragged)
|
||||
/// .on_resize(10, Message::PaneResized);
|
||||
/// ```
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct PaneGrid<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: crate::Renderer,
|
||||
Renderer::Theme: StyleSheet + container::StyleSheet,
|
||||
{
|
||||
contents: Contents<'a, Content<'a, Message, Renderer>>,
|
||||
width: Length,
|
||||
height: Length,
|
||||
spacing: f32,
|
||||
on_click: Option<Box<dyn Fn(Pane) -> Message + 'a>>,
|
||||
on_drag: Option<Box<dyn Fn(DragEvent) -> Message + 'a>>,
|
||||
on_resize: Option<(f32, Box<dyn Fn(ResizeEvent) -> Message + 'a>)>,
|
||||
style: <Renderer::Theme as StyleSheet>::Style,
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> PaneGrid<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: crate::Renderer,
|
||||
Renderer::Theme: StyleSheet + container::StyleSheet,
|
||||
{
|
||||
/// Creates a [`PaneGrid`] with the given [`State`] and view function.
|
||||
///
|
||||
/// The view function will be called to display each [`Pane`] present in the
|
||||
/// [`State`]. [`bool`] is set if the pane is maximized.
|
||||
pub fn new<T>(
|
||||
state: &'a State<T>,
|
||||
view: impl Fn(Pane, &'a T, bool) -> Content<'a, Message, Renderer>,
|
||||
) -> Self {
|
||||
let contents = if let Some((pane, pane_state)) =
|
||||
state.maximized.and_then(|pane| {
|
||||
state.panes.get(&pane).map(|pane_state| (pane, pane_state))
|
||||
}) {
|
||||
Contents::Maximized(
|
||||
pane,
|
||||
view(pane, pane_state, true),
|
||||
Node::Pane(pane),
|
||||
)
|
||||
} else {
|
||||
Contents::All(
|
||||
state
|
||||
.panes
|
||||
.iter()
|
||||
.map(|(pane, pane_state)| {
|
||||
(*pane, view(*pane, pane_state, false))
|
||||
})
|
||||
.collect(),
|
||||
&state.internal,
|
||||
)
|
||||
};
|
||||
|
||||
Self {
|
||||
contents,
|
||||
width: Length::Fill,
|
||||
height: Length::Fill,
|
||||
spacing: 0.0,
|
||||
on_click: None,
|
||||
on_drag: None,
|
||||
on_resize: None,
|
||||
style: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the width of the [`PaneGrid`].
|
||||
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||
self.width = width.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the height of the [`PaneGrid`].
|
||||
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
||||
self.height = height.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the spacing _between_ the panes of the [`PaneGrid`].
|
||||
pub fn spacing(mut self, amount: impl Into<Pixels>) -> Self {
|
||||
self.spacing = amount.into().0;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the message that will be produced when a [`Pane`] of the
|
||||
/// [`PaneGrid`] is clicked.
|
||||
pub fn on_click<F>(mut self, f: F) -> Self
|
||||
where
|
||||
F: 'a + Fn(Pane) -> Message,
|
||||
{
|
||||
self.on_click = Some(Box::new(f));
|
||||
self
|
||||
}
|
||||
|
||||
/// Enables the drag and drop interactions of the [`PaneGrid`], which will
|
||||
/// use the provided function to produce messages.
|
||||
pub fn on_drag<F>(mut self, f: F) -> Self
|
||||
where
|
||||
F: 'a + Fn(DragEvent) -> Message,
|
||||
{
|
||||
self.on_drag = Some(Box::new(f));
|
||||
self
|
||||
}
|
||||
|
||||
/// Enables the resize interactions of the [`PaneGrid`], which will
|
||||
/// use the provided function to produce messages.
|
||||
///
|
||||
/// The `leeway` describes the amount of space around a split that can be
|
||||
/// used to grab it.
|
||||
///
|
||||
/// The grabbable area of a split will have a length of `spacing + leeway`,
|
||||
/// properly centered. In other words, a length of
|
||||
/// `(spacing + leeway) / 2.0` on either side of the split line.
|
||||
pub fn on_resize<F>(mut self, leeway: impl Into<Pixels>, f: F) -> Self
|
||||
where
|
||||
F: 'a + Fn(ResizeEvent) -> Message,
|
||||
{
|
||||
self.on_resize = Some((leeway.into().0, Box::new(f)));
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the [`PaneGrid`].
|
||||
pub fn style(
|
||||
mut self,
|
||||
style: impl Into<<Renderer::Theme as StyleSheet>::Style>,
|
||||
) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
fn drag_enabled(&self) -> bool {
|
||||
(!self.contents.is_maximized())
|
||||
.then(|| self.on_drag.is_some())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Widget<Message, Renderer>
|
||||
for PaneGrid<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: crate::Renderer,
|
||||
Renderer::Theme: StyleSheet + container::StyleSheet,
|
||||
{
|
||||
fn tag(&self) -> tree::Tag {
|
||||
tree::Tag::of::<state::Action>()
|
||||
}
|
||||
|
||||
fn state(&self) -> tree::State {
|
||||
tree::State::new(state::Action::Idle)
|
||||
}
|
||||
|
||||
fn children(&self) -> Vec<Tree> {
|
||||
self.contents
|
||||
.iter()
|
||||
.map(|(_, content)| content.state())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn diff(&self, tree: &mut Tree) {
|
||||
match &self.contents {
|
||||
Contents::All(contents, _) => tree.diff_children_custom(
|
||||
contents,
|
||||
|state, (_, content)| content.diff(state),
|
||||
|(_, content)| content.state(),
|
||||
),
|
||||
Contents::Maximized(_, content, _) => tree.diff_children_custom(
|
||||
&[content],
|
||||
|state, content| content.diff(state),
|
||||
|content| content.state(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn width(&self) -> Length {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
layout(
|
||||
renderer,
|
||||
limits,
|
||||
self.contents.layout(),
|
||||
self.width,
|
||||
self.height,
|
||||
self.spacing,
|
||||
self.contents.iter(),
|
||||
|content, renderer, limits| content.layout(renderer, limits),
|
||||
)
|
||||
}
|
||||
|
||||
fn operate(
|
||||
&self,
|
||||
tree: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn widget::Operation<Message>,
|
||||
) {
|
||||
operation.container(None, &mut |operation| {
|
||||
self.contents
|
||||
.iter()
|
||||
.zip(&mut tree.children)
|
||||
.zip(layout.children())
|
||||
.for_each(|(((_pane, content), state), layout)| {
|
||||
content.operate(state, layout, renderer, operation);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
let action = tree.state.downcast_mut::<state::Action>();
|
||||
|
||||
let on_drag = if self.drag_enabled() {
|
||||
&self.on_drag
|
||||
} else {
|
||||
&None
|
||||
};
|
||||
|
||||
let event_status = update(
|
||||
action,
|
||||
self.contents.layout(),
|
||||
&event,
|
||||
layout,
|
||||
cursor_position,
|
||||
shell,
|
||||
self.spacing,
|
||||
self.contents.iter(),
|
||||
&self.on_click,
|
||||
on_drag,
|
||||
&self.on_resize,
|
||||
);
|
||||
|
||||
let picked_pane = action.picked_pane().map(|(pane, _)| pane);
|
||||
|
||||
self.contents
|
||||
.iter_mut()
|
||||
.zip(&mut tree.children)
|
||||
.zip(layout.children())
|
||||
.map(|(((pane, content), tree), layout)| {
|
||||
let is_picked = picked_pane == Some(pane);
|
||||
|
||||
content.on_event(
|
||||
tree,
|
||||
event.clone(),
|
||||
layout,
|
||||
cursor_position,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
is_picked,
|
||||
)
|
||||
})
|
||||
.fold(event_status, event::Status::merge)
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
mouse_interaction(
|
||||
tree.state.downcast_ref(),
|
||||
self.contents.layout(),
|
||||
layout,
|
||||
cursor_position,
|
||||
self.spacing,
|
||||
self.on_resize.as_ref().map(|(leeway, _)| *leeway),
|
||||
)
|
||||
.unwrap_or_else(|| {
|
||||
self.contents
|
||||
.iter()
|
||||
.zip(&tree.children)
|
||||
.zip(layout.children())
|
||||
.map(|(((_pane, content), tree), layout)| {
|
||||
content.mouse_interaction(
|
||||
tree,
|
||||
layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
renderer,
|
||||
self.drag_enabled(),
|
||||
)
|
||||
})
|
||||
.max()
|
||||
.unwrap_or_default()
|
||||
})
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
draw(
|
||||
tree.state.downcast_ref(),
|
||||
self.contents.layout(),
|
||||
layout,
|
||||
cursor_position,
|
||||
renderer,
|
||||
theme,
|
||||
style,
|
||||
viewport,
|
||||
self.spacing,
|
||||
self.on_resize.as_ref().map(|(leeway, _)| *leeway),
|
||||
&self.style,
|
||||
self.contents
|
||||
.iter()
|
||||
.zip(&tree.children)
|
||||
.map(|((pane, content), tree)| (pane, (content, tree))),
|
||||
|(content, tree),
|
||||
renderer,
|
||||
style,
|
||||
layout,
|
||||
cursor_position,
|
||||
rectangle| {
|
||||
content.draw(
|
||||
tree,
|
||||
renderer,
|
||||
theme,
|
||||
style,
|
||||
layout,
|
||||
cursor_position,
|
||||
rectangle,
|
||||
);
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn overlay<'b>(
|
||||
&'b mut self,
|
||||
tree: &'b mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
) -> Option<overlay::Element<'_, Message, Renderer>> {
|
||||
let children = self
|
||||
.contents
|
||||
.iter_mut()
|
||||
.zip(&mut tree.children)
|
||||
.zip(layout.children())
|
||||
.filter_map(|(((_, content), state), layout)| {
|
||||
content.overlay(state, layout, renderer)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
(!children.is_empty()).then(|| Group::with_children(children).overlay())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> From<PaneGrid<'a, Message, Renderer>>
|
||||
for Element<'a, Message, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Renderer: 'a + crate::Renderer,
|
||||
Renderer::Theme: StyleSheet + container::StyleSheet,
|
||||
{
|
||||
fn from(
|
||||
pane_grid: PaneGrid<'a, Message, Renderer>,
|
||||
) -> Element<'a, Message, Renderer> {
|
||||
Element::new(pane_grid)
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculates the [`Layout`] of a [`PaneGrid`].
|
||||
pub fn layout<Renderer, T>(
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
node: &Node,
|
||||
width: Length,
|
||||
height: Length,
|
||||
spacing: f32,
|
||||
contents: impl Iterator<Item = (Pane, T)>,
|
||||
layout_content: impl Fn(T, &Renderer, &layout::Limits) -> layout::Node,
|
||||
) -> layout::Node {
|
||||
let limits = limits.width(width).height(height);
|
||||
let size = limits.resolve(Size::ZERO);
|
||||
|
||||
let regions = node.pane_regions(spacing, size);
|
||||
let children = contents
|
||||
.filter_map(|(pane, content)| {
|
||||
let region = regions.get(&pane)?;
|
||||
let size = Size::new(region.width, region.height);
|
||||
|
||||
let mut node = layout_content(
|
||||
content,
|
||||
renderer,
|
||||
&layout::Limits::new(size, size),
|
||||
);
|
||||
|
||||
node.move_to(Point::new(region.x, region.y));
|
||||
|
||||
Some(node)
|
||||
})
|
||||
.collect();
|
||||
|
||||
layout::Node::with_children(size, children)
|
||||
}
|
||||
|
||||
/// Processes an [`Event`] and updates the [`state`] of a [`PaneGrid`]
|
||||
/// accordingly.
|
||||
pub fn update<'a, Message, T: Draggable>(
|
||||
action: &mut state::Action,
|
||||
node: &Node,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
spacing: f32,
|
||||
contents: impl Iterator<Item = (Pane, T)>,
|
||||
on_click: &Option<Box<dyn Fn(Pane) -> Message + 'a>>,
|
||||
on_drag: &Option<Box<dyn Fn(DragEvent) -> Message + 'a>>,
|
||||
on_resize: &Option<(f32, Box<dyn Fn(ResizeEvent) -> Message + 'a>)>,
|
||||
) -> event::Status {
|
||||
let mut event_status = event::Status::Ignored;
|
||||
|
||||
match event {
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
|
||||
| Event::Touch(touch::Event::FingerPressed { .. }) => {
|
||||
let bounds = layout.bounds();
|
||||
|
||||
if bounds.contains(cursor_position) {
|
||||
event_status = event::Status::Captured;
|
||||
|
||||
match on_resize {
|
||||
Some((leeway, _)) => {
|
||||
let relative_cursor = Point::new(
|
||||
cursor_position.x - bounds.x,
|
||||
cursor_position.y - bounds.y,
|
||||
);
|
||||
|
||||
let splits = node.split_regions(
|
||||
spacing,
|
||||
Size::new(bounds.width, bounds.height),
|
||||
);
|
||||
|
||||
let clicked_split = hovered_split(
|
||||
splits.iter(),
|
||||
spacing + leeway,
|
||||
relative_cursor,
|
||||
);
|
||||
|
||||
if let Some((split, axis, _)) = clicked_split {
|
||||
if action.picked_pane().is_none() {
|
||||
*action =
|
||||
state::Action::Resizing { split, axis };
|
||||
}
|
||||
} else {
|
||||
click_pane(
|
||||
action,
|
||||
layout,
|
||||
cursor_position,
|
||||
shell,
|
||||
contents,
|
||||
on_click,
|
||||
on_drag,
|
||||
);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
click_pane(
|
||||
action,
|
||||
layout,
|
||||
cursor_position,
|
||||
shell,
|
||||
contents,
|
||||
on_click,
|
||||
on_drag,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
|
||||
| Event::Touch(touch::Event::FingerLifted { .. })
|
||||
| Event::Touch(touch::Event::FingerLost { .. }) => {
|
||||
if let Some((pane, _)) = action.picked_pane() {
|
||||
if let Some(on_drag) = on_drag {
|
||||
let mut dropped_region = contents
|
||||
.zip(layout.children())
|
||||
.filter(|(_, layout)| {
|
||||
layout.bounds().contains(cursor_position)
|
||||
});
|
||||
|
||||
let event = match dropped_region.next() {
|
||||
Some(((target, _), _)) if pane != target => {
|
||||
DragEvent::Dropped { pane, target }
|
||||
}
|
||||
_ => DragEvent::Canceled { pane },
|
||||
};
|
||||
|
||||
shell.publish(on_drag(event));
|
||||
}
|
||||
|
||||
*action = state::Action::Idle;
|
||||
|
||||
event_status = event::Status::Captured;
|
||||
} else if action.picked_split().is_some() {
|
||||
*action = state::Action::Idle;
|
||||
|
||||
event_status = event::Status::Captured;
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse::Event::CursorMoved { .. })
|
||||
| Event::Touch(touch::Event::FingerMoved { .. }) => {
|
||||
if let Some((_, on_resize)) = on_resize {
|
||||
if let Some((split, _)) = action.picked_split() {
|
||||
let bounds = layout.bounds();
|
||||
|
||||
let splits = node.split_regions(
|
||||
spacing,
|
||||
Size::new(bounds.width, bounds.height),
|
||||
);
|
||||
|
||||
if let Some((axis, rectangle, _)) = splits.get(&split) {
|
||||
let ratio = match axis {
|
||||
Axis::Horizontal => {
|
||||
let position =
|
||||
cursor_position.y - bounds.y - rectangle.y;
|
||||
|
||||
(position / rectangle.height).clamp(0.1, 0.9)
|
||||
}
|
||||
Axis::Vertical => {
|
||||
let position =
|
||||
cursor_position.x - bounds.x - rectangle.x;
|
||||
|
||||
(position / rectangle.width).clamp(0.1, 0.9)
|
||||
}
|
||||
};
|
||||
|
||||
shell.publish(on_resize(ResizeEvent { split, ratio }));
|
||||
|
||||
event_status = event::Status::Captured;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
event_status
|
||||
}
|
||||
|
||||
fn click_pane<'a, Message, T>(
|
||||
action: &mut state::Action,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
contents: impl Iterator<Item = (Pane, T)>,
|
||||
on_click: &Option<Box<dyn Fn(Pane) -> Message + 'a>>,
|
||||
on_drag: &Option<Box<dyn Fn(DragEvent) -> Message + 'a>>,
|
||||
) where
|
||||
T: Draggable,
|
||||
{
|
||||
let mut clicked_region = contents
|
||||
.zip(layout.children())
|
||||
.filter(|(_, layout)| layout.bounds().contains(cursor_position));
|
||||
|
||||
if let Some(((pane, content), layout)) = clicked_region.next() {
|
||||
if let Some(on_click) = &on_click {
|
||||
shell.publish(on_click(pane));
|
||||
}
|
||||
|
||||
if let Some(on_drag) = &on_drag {
|
||||
if content.can_be_dragged_at(layout, cursor_position) {
|
||||
let pane_position = layout.position();
|
||||
|
||||
let origin = cursor_position
|
||||
- Vector::new(pane_position.x, pane_position.y);
|
||||
|
||||
*action = state::Action::Dragging { pane, origin };
|
||||
|
||||
shell.publish(on_drag(DragEvent::Picked { pane }));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the current [`mouse::Interaction`] of a [`PaneGrid`].
|
||||
pub fn mouse_interaction(
|
||||
action: &state::Action,
|
||||
node: &Node,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
spacing: f32,
|
||||
resize_leeway: Option<f32>,
|
||||
) -> Option<mouse::Interaction> {
|
||||
if action.picked_pane().is_some() {
|
||||
return Some(mouse::Interaction::Grabbing);
|
||||
}
|
||||
|
||||
let resize_axis =
|
||||
action.picked_split().map(|(_, axis)| axis).or_else(|| {
|
||||
resize_leeway.and_then(|leeway| {
|
||||
let bounds = layout.bounds();
|
||||
|
||||
let splits = node.split_regions(spacing, bounds.size());
|
||||
|
||||
let relative_cursor = Point::new(
|
||||
cursor_position.x - bounds.x,
|
||||
cursor_position.y - bounds.y,
|
||||
);
|
||||
|
||||
hovered_split(splits.iter(), spacing + leeway, relative_cursor)
|
||||
.map(|(_, axis, _)| axis)
|
||||
})
|
||||
});
|
||||
|
||||
if let Some(resize_axis) = resize_axis {
|
||||
return Some(match resize_axis {
|
||||
Axis::Horizontal => mouse::Interaction::ResizingVertically,
|
||||
Axis::Vertical => mouse::Interaction::ResizingHorizontally,
|
||||
});
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Draws a [`PaneGrid`].
|
||||
pub fn draw<Renderer, T>(
|
||||
action: &state::Action,
|
||||
node: &Node,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
default_style: &renderer::Style,
|
||||
viewport: &Rectangle,
|
||||
spacing: f32,
|
||||
resize_leeway: Option<f32>,
|
||||
style: &<Renderer::Theme as StyleSheet>::Style,
|
||||
contents: impl Iterator<Item = (Pane, T)>,
|
||||
draw_pane: impl Fn(
|
||||
T,
|
||||
&mut Renderer,
|
||||
&renderer::Style,
|
||||
Layout<'_>,
|
||||
Point,
|
||||
&Rectangle,
|
||||
),
|
||||
) where
|
||||
Renderer: crate::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
let picked_pane = action.picked_pane();
|
||||
|
||||
let picked_split = action
|
||||
.picked_split()
|
||||
.and_then(|(split, axis)| {
|
||||
let bounds = layout.bounds();
|
||||
|
||||
let splits = node.split_regions(spacing, bounds.size());
|
||||
|
||||
let (_axis, region, ratio) = splits.get(&split)?;
|
||||
|
||||
let region = axis.split_line_bounds(*region, *ratio, spacing);
|
||||
|
||||
Some((axis, region + Vector::new(bounds.x, bounds.y), true))
|
||||
})
|
||||
.or_else(|| match resize_leeway {
|
||||
Some(leeway) => {
|
||||
let bounds = layout.bounds();
|
||||
|
||||
let relative_cursor = Point::new(
|
||||
cursor_position.x - bounds.x,
|
||||
cursor_position.y - bounds.y,
|
||||
);
|
||||
|
||||
let splits = node.split_regions(spacing, bounds.size());
|
||||
|
||||
let (_split, axis, region) = hovered_split(
|
||||
splits.iter(),
|
||||
spacing + leeway,
|
||||
relative_cursor,
|
||||
)?;
|
||||
|
||||
Some((axis, region + Vector::new(bounds.x, bounds.y), false))
|
||||
}
|
||||
None => None,
|
||||
});
|
||||
|
||||
let pane_cursor_position = if picked_pane.is_some() {
|
||||
// TODO: Remove once cursor availability is encoded in the type
|
||||
// system
|
||||
Point::new(-1.0, -1.0)
|
||||
} else {
|
||||
cursor_position
|
||||
};
|
||||
|
||||
let mut render_picked_pane = None;
|
||||
|
||||
for ((id, pane), layout) in contents.zip(layout.children()) {
|
||||
match picked_pane {
|
||||
Some((dragging, origin)) if id == dragging => {
|
||||
render_picked_pane = Some((pane, origin, layout));
|
||||
}
|
||||
_ => {
|
||||
draw_pane(
|
||||
pane,
|
||||
renderer,
|
||||
default_style,
|
||||
layout,
|
||||
pane_cursor_position,
|
||||
viewport,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Render picked pane last
|
||||
if let Some((pane, origin, layout)) = render_picked_pane {
|
||||
let bounds = layout.bounds();
|
||||
|
||||
renderer.with_translation(
|
||||
cursor_position
|
||||
- Point::new(bounds.x + origin.x, bounds.y + origin.y),
|
||||
|renderer| {
|
||||
renderer.with_layer(bounds, |renderer| {
|
||||
draw_pane(
|
||||
pane,
|
||||
renderer,
|
||||
default_style,
|
||||
layout,
|
||||
pane_cursor_position,
|
||||
viewport,
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
if let Some((axis, split_region, is_picked)) = picked_split {
|
||||
let highlight = if is_picked {
|
||||
theme.picked_split(style)
|
||||
} else {
|
||||
theme.hovered_split(style)
|
||||
};
|
||||
|
||||
if let Some(highlight) = highlight {
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: match axis {
|
||||
Axis::Horizontal => Rectangle {
|
||||
x: split_region.x,
|
||||
y: (split_region.y
|
||||
+ (split_region.height - highlight.width)
|
||||
/ 2.0)
|
||||
.round(),
|
||||
width: split_region.width,
|
||||
height: highlight.width,
|
||||
},
|
||||
Axis::Vertical => Rectangle {
|
||||
x: (split_region.x
|
||||
+ (split_region.width - highlight.width) / 2.0)
|
||||
.round(),
|
||||
y: split_region.y,
|
||||
width: highlight.width,
|
||||
height: split_region.height,
|
||||
},
|
||||
},
|
||||
border_radius: 0.0.into(),
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
},
|
||||
highlight.color,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An event produced during a drag and drop interaction of a [`PaneGrid`].
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum DragEvent {
|
||||
/// A [`Pane`] was picked for dragging.
|
||||
Picked {
|
||||
/// The picked [`Pane`].
|
||||
pane: Pane,
|
||||
},
|
||||
|
||||
/// A [`Pane`] was dropped on top of another [`Pane`].
|
||||
Dropped {
|
||||
/// The picked [`Pane`].
|
||||
pane: Pane,
|
||||
|
||||
/// The [`Pane`] where the picked one was dropped on.
|
||||
target: Pane,
|
||||
},
|
||||
|
||||
/// A [`Pane`] was picked and then dropped outside of other [`Pane`]
|
||||
/// boundaries.
|
||||
Canceled {
|
||||
/// The picked [`Pane`].
|
||||
pane: Pane,
|
||||
},
|
||||
}
|
||||
|
||||
/// An event produced during a resize interaction of a [`PaneGrid`].
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct ResizeEvent {
|
||||
/// The [`Split`] that is being dragged for resizing.
|
||||
pub split: Split,
|
||||
|
||||
/// The new ratio of the [`Split`].
|
||||
///
|
||||
/// The ratio is a value in [0, 1], representing the exact position of a
|
||||
/// [`Split`] between two panes.
|
||||
pub ratio: f32,
|
||||
}
|
||||
|
||||
/*
|
||||
* Helpers
|
||||
*/
|
||||
fn hovered_split<'a>(
|
||||
splits: impl Iterator<Item = (&'a Split, &'a (Axis, Rectangle, f32))>,
|
||||
spacing: f32,
|
||||
cursor_position: Point,
|
||||
) -> Option<(Split, Axis, Rectangle)> {
|
||||
splits
|
||||
.filter_map(|(split, (axis, region, ratio))| {
|
||||
let bounds = axis.split_line_bounds(*region, *ratio, spacing);
|
||||
|
||||
if bounds.contains(cursor_position) {
|
||||
Some((*split, *axis, bounds))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.next()
|
||||
}
|
||||
|
||||
/// The visible contents of the [`PaneGrid`]
|
||||
#[derive(Debug)]
|
||||
pub enum Contents<'a, T> {
|
||||
/// All panes are visible
|
||||
All(Vec<(Pane, T)>, &'a state::Internal),
|
||||
/// A maximized pane is visible
|
||||
Maximized(Pane, T, Node),
|
||||
}
|
||||
|
||||
impl<'a, T> Contents<'a, T> {
|
||||
/// Returns the layout [`Node`] of the [`Contents`]
|
||||
pub fn layout(&self) -> &Node {
|
||||
match self {
|
||||
Contents::All(_, state) => state.layout(),
|
||||
Contents::Maximized(_, _, layout) => layout,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an iterator over the values of the [`Contents`]
|
||||
pub fn iter(&self) -> Box<dyn Iterator<Item = (Pane, &T)> + '_> {
|
||||
match self {
|
||||
Contents::All(contents, _) => Box::new(
|
||||
contents.iter().map(|(pane, content)| (*pane, content)),
|
||||
),
|
||||
Contents::Maximized(pane, content, _) => {
|
||||
Box::new(std::iter::once((*pane, content)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn iter_mut(&mut self) -> Box<dyn Iterator<Item = (Pane, &mut T)> + '_> {
|
||||
match self {
|
||||
Contents::All(contents, _) => Box::new(
|
||||
contents.iter_mut().map(|(pane, content)| (*pane, content)),
|
||||
),
|
||||
Contents::Maximized(pane, content, _) => {
|
||||
Box::new(std::iter::once((*pane, content)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_maximized(&self) -> bool {
|
||||
matches!(self, Self::Maximized(..))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,241 +0,0 @@
|
|||
use crate::Rectangle;
|
||||
|
||||
/// A fixed reference line for the measurement of coordinates.
|
||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
||||
pub enum Axis {
|
||||
/// The horizontal axis: —
|
||||
Horizontal,
|
||||
/// The vertical axis: |
|
||||
Vertical,
|
||||
}
|
||||
|
||||
impl Axis {
|
||||
/// Splits the provided [`Rectangle`] on the current [`Axis`] with the
|
||||
/// given `ratio` and `spacing`.
|
||||
pub fn split(
|
||||
&self,
|
||||
rectangle: &Rectangle,
|
||||
ratio: f32,
|
||||
spacing: f32,
|
||||
) -> (Rectangle, Rectangle) {
|
||||
match self {
|
||||
Axis::Horizontal => {
|
||||
let height_top =
|
||||
(rectangle.height * ratio - spacing / 2.0).round();
|
||||
let height_bottom = rectangle.height - height_top - spacing;
|
||||
|
||||
(
|
||||
Rectangle {
|
||||
height: height_top,
|
||||
..*rectangle
|
||||
},
|
||||
Rectangle {
|
||||
y: rectangle.y + height_top + spacing,
|
||||
height: height_bottom,
|
||||
..*rectangle
|
||||
},
|
||||
)
|
||||
}
|
||||
Axis::Vertical => {
|
||||
let width_left =
|
||||
(rectangle.width * ratio - spacing / 2.0).round();
|
||||
let width_right = rectangle.width - width_left - spacing;
|
||||
|
||||
(
|
||||
Rectangle {
|
||||
width: width_left,
|
||||
..*rectangle
|
||||
},
|
||||
Rectangle {
|
||||
x: rectangle.x + width_left + spacing,
|
||||
width: width_right,
|
||||
..*rectangle
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculates the bounds of the split line in a [`Rectangle`] region.
|
||||
pub fn split_line_bounds(
|
||||
&self,
|
||||
rectangle: Rectangle,
|
||||
ratio: f32,
|
||||
spacing: f32,
|
||||
) -> Rectangle {
|
||||
match self {
|
||||
Axis::Horizontal => Rectangle {
|
||||
x: rectangle.x,
|
||||
y: (rectangle.y + rectangle.height * ratio - spacing / 2.0)
|
||||
.round(),
|
||||
width: rectangle.width,
|
||||
height: spacing,
|
||||
},
|
||||
Axis::Vertical => Rectangle {
|
||||
x: (rectangle.x + rectangle.width * ratio - spacing / 2.0)
|
||||
.round(),
|
||||
y: rectangle.y,
|
||||
width: spacing,
|
||||
height: rectangle.height,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
enum Case {
|
||||
Horizontal {
|
||||
overall_height: f32,
|
||||
spacing: f32,
|
||||
top_height: f32,
|
||||
bottom_y: f32,
|
||||
bottom_height: f32,
|
||||
},
|
||||
Vertical {
|
||||
overall_width: f32,
|
||||
spacing: f32,
|
||||
left_width: f32,
|
||||
right_x: f32,
|
||||
right_width: f32,
|
||||
},
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split() {
|
||||
let cases = vec![
|
||||
// Even height, even spacing
|
||||
Case::Horizontal {
|
||||
overall_height: 10.0,
|
||||
spacing: 2.0,
|
||||
top_height: 4.0,
|
||||
bottom_y: 6.0,
|
||||
bottom_height: 4.0,
|
||||
},
|
||||
// Odd height, even spacing
|
||||
Case::Horizontal {
|
||||
overall_height: 9.0,
|
||||
spacing: 2.0,
|
||||
top_height: 4.0,
|
||||
bottom_y: 6.0,
|
||||
bottom_height: 3.0,
|
||||
},
|
||||
// Even height, odd spacing
|
||||
Case::Horizontal {
|
||||
overall_height: 10.0,
|
||||
spacing: 1.0,
|
||||
top_height: 5.0,
|
||||
bottom_y: 6.0,
|
||||
bottom_height: 4.0,
|
||||
},
|
||||
// Odd height, odd spacing
|
||||
Case::Horizontal {
|
||||
overall_height: 9.0,
|
||||
spacing: 1.0,
|
||||
top_height: 4.0,
|
||||
bottom_y: 5.0,
|
||||
bottom_height: 4.0,
|
||||
},
|
||||
// Even width, even spacing
|
||||
Case::Vertical {
|
||||
overall_width: 10.0,
|
||||
spacing: 2.0,
|
||||
left_width: 4.0,
|
||||
right_x: 6.0,
|
||||
right_width: 4.0,
|
||||
},
|
||||
// Odd width, even spacing
|
||||
Case::Vertical {
|
||||
overall_width: 9.0,
|
||||
spacing: 2.0,
|
||||
left_width: 4.0,
|
||||
right_x: 6.0,
|
||||
right_width: 3.0,
|
||||
},
|
||||
// Even width, odd spacing
|
||||
Case::Vertical {
|
||||
overall_width: 10.0,
|
||||
spacing: 1.0,
|
||||
left_width: 5.0,
|
||||
right_x: 6.0,
|
||||
right_width: 4.0,
|
||||
},
|
||||
// Odd width, odd spacing
|
||||
Case::Vertical {
|
||||
overall_width: 9.0,
|
||||
spacing: 1.0,
|
||||
left_width: 4.0,
|
||||
right_x: 5.0,
|
||||
right_width: 4.0,
|
||||
},
|
||||
];
|
||||
for case in cases {
|
||||
match case {
|
||||
Case::Horizontal {
|
||||
overall_height,
|
||||
spacing,
|
||||
top_height,
|
||||
bottom_y,
|
||||
bottom_height,
|
||||
} => {
|
||||
let a = Axis::Horizontal;
|
||||
let r = Rectangle {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
width: 10.0,
|
||||
height: overall_height,
|
||||
};
|
||||
let (top, bottom) = a.split(&r, 0.5, spacing);
|
||||
assert_eq!(
|
||||
top,
|
||||
Rectangle {
|
||||
height: top_height,
|
||||
..r
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
bottom,
|
||||
Rectangle {
|
||||
y: bottom_y,
|
||||
height: bottom_height,
|
||||
..r
|
||||
}
|
||||
);
|
||||
}
|
||||
Case::Vertical {
|
||||
overall_width,
|
||||
spacing,
|
||||
left_width,
|
||||
right_x,
|
||||
right_width,
|
||||
} => {
|
||||
let a = Axis::Vertical;
|
||||
let r = Rectangle {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
width: overall_width,
|
||||
height: 10.0,
|
||||
};
|
||||
let (left, right) = a.split(&r, 0.5, spacing);
|
||||
assert_eq!(
|
||||
left,
|
||||
Rectangle {
|
||||
width: left_width,
|
||||
..r
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
right,
|
||||
Rectangle {
|
||||
x: right_x,
|
||||
width: right_width,
|
||||
..r
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
use crate::widget::pane_grid::Axis;
|
||||
|
||||
/// The arrangement of a [`PaneGrid`].
|
||||
///
|
||||
/// [`PaneGrid`]: crate::widget::PaneGrid
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Configuration<T> {
|
||||
/// A split of the available space.
|
||||
Split {
|
||||
/// The direction of the split.
|
||||
axis: Axis,
|
||||
|
||||
/// The ratio of the split in [0.0, 1.0].
|
||||
ratio: f32,
|
||||
|
||||
/// The left/top [`Configuration`] of the split.
|
||||
a: Box<Configuration<T>>,
|
||||
|
||||
/// The right/bottom [`Configuration`] of the split.
|
||||
b: Box<Configuration<T>>,
|
||||
},
|
||||
/// A [`Pane`].
|
||||
///
|
||||
/// [`Pane`]: crate::widget::pane_grid::Pane
|
||||
Pane(T),
|
||||
}
|
||||
|
|
@ -1,373 +0,0 @@
|
|||
use crate::event::{self, Event};
|
||||
use crate::layout;
|
||||
use crate::mouse;
|
||||
use crate::overlay;
|
||||
use crate::renderer;
|
||||
use crate::widget::container;
|
||||
use crate::widget::pane_grid::{Draggable, TitleBar};
|
||||
use crate::widget::{self, Tree};
|
||||
use crate::{Clipboard, Element, Layout, Point, Rectangle, Shell, Size};
|
||||
|
||||
/// The content of a [`Pane`].
|
||||
///
|
||||
/// [`Pane`]: crate::widget::pane_grid::Pane
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Content<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: crate::Renderer,
|
||||
Renderer::Theme: container::StyleSheet,
|
||||
{
|
||||
title_bar: Option<TitleBar<'a, Message, Renderer>>,
|
||||
body: Element<'a, Message, Renderer>,
|
||||
style: <Renderer::Theme as container::StyleSheet>::Style,
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Content<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: crate::Renderer,
|
||||
Renderer::Theme: container::StyleSheet,
|
||||
{
|
||||
/// Creates a new [`Content`] with the provided body.
|
||||
pub fn new(body: impl Into<Element<'a, Message, Renderer>>) -> Self {
|
||||
Self {
|
||||
title_bar: None,
|
||||
body: body.into(),
|
||||
style: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the [`TitleBar`] of this [`Content`].
|
||||
pub fn title_bar(
|
||||
mut self,
|
||||
title_bar: TitleBar<'a, Message, Renderer>,
|
||||
) -> Self {
|
||||
self.title_bar = Some(title_bar);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the [`Content`].
|
||||
pub fn style(
|
||||
mut self,
|
||||
style: impl Into<<Renderer::Theme as container::StyleSheet>::Style>,
|
||||
) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Content<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: crate::Renderer,
|
||||
Renderer::Theme: container::StyleSheet,
|
||||
{
|
||||
pub(super) fn state(&self) -> Tree {
|
||||
let children = if let Some(title_bar) = self.title_bar.as_ref() {
|
||||
vec![Tree::new(&self.body), title_bar.state()]
|
||||
} else {
|
||||
vec![Tree::new(&self.body), Tree::empty()]
|
||||
};
|
||||
|
||||
Tree {
|
||||
children,
|
||||
..Tree::empty()
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn diff(&self, tree: &mut Tree) {
|
||||
if tree.children.len() == 2 {
|
||||
if let Some(title_bar) = self.title_bar.as_ref() {
|
||||
title_bar.diff(&mut tree.children[1]);
|
||||
}
|
||||
|
||||
tree.children[0].diff(&self.body);
|
||||
} else {
|
||||
*tree = self.state();
|
||||
}
|
||||
}
|
||||
|
||||
/// Draws the [`Content`] with the provided [`Renderer`] and [`Layout`].
|
||||
///
|
||||
/// [`Renderer`]: crate::Renderer
|
||||
pub fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
use container::StyleSheet;
|
||||
|
||||
let bounds = layout.bounds();
|
||||
|
||||
{
|
||||
let style = theme.appearance(&self.style);
|
||||
|
||||
container::draw_background(renderer, &style, bounds);
|
||||
}
|
||||
|
||||
if let Some(title_bar) = &self.title_bar {
|
||||
let mut children = layout.children();
|
||||
let title_bar_layout = children.next().unwrap();
|
||||
let body_layout = children.next().unwrap();
|
||||
|
||||
let show_controls = bounds.contains(cursor_position);
|
||||
|
||||
self.body.as_widget().draw(
|
||||
&tree.children[0],
|
||||
renderer,
|
||||
theme,
|
||||
style,
|
||||
body_layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
);
|
||||
|
||||
title_bar.draw(
|
||||
&tree.children[1],
|
||||
renderer,
|
||||
theme,
|
||||
style,
|
||||
title_bar_layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
show_controls,
|
||||
);
|
||||
} else {
|
||||
self.body.as_widget().draw(
|
||||
&tree.children[0],
|
||||
renderer,
|
||||
theme,
|
||||
style,
|
||||
layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn layout(
|
||||
&self,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
if let Some(title_bar) = &self.title_bar {
|
||||
let max_size = limits.max();
|
||||
|
||||
let title_bar_layout = title_bar
|
||||
.layout(renderer, &layout::Limits::new(Size::ZERO, max_size));
|
||||
|
||||
let title_bar_size = title_bar_layout.size();
|
||||
|
||||
let mut body_layout = self.body.as_widget().layout(
|
||||
renderer,
|
||||
&layout::Limits::new(
|
||||
Size::ZERO,
|
||||
Size::new(
|
||||
max_size.width,
|
||||
max_size.height - title_bar_size.height,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
body_layout.move_to(Point::new(0.0, title_bar_size.height));
|
||||
|
||||
layout::Node::with_children(
|
||||
max_size,
|
||||
vec![title_bar_layout, body_layout],
|
||||
)
|
||||
} else {
|
||||
self.body.as_widget().layout(renderer, limits)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn operate(
|
||||
&self,
|
||||
tree: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn widget::Operation<Message>,
|
||||
) {
|
||||
let body_layout = if let Some(title_bar) = &self.title_bar {
|
||||
let mut children = layout.children();
|
||||
|
||||
title_bar.operate(
|
||||
&mut tree.children[1],
|
||||
children.next().unwrap(),
|
||||
renderer,
|
||||
operation,
|
||||
);
|
||||
|
||||
children.next().unwrap()
|
||||
} else {
|
||||
layout
|
||||
};
|
||||
|
||||
self.body.as_widget().operate(
|
||||
&mut tree.children[0],
|
||||
body_layout,
|
||||
renderer,
|
||||
operation,
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn on_event(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
is_picked: bool,
|
||||
) -> event::Status {
|
||||
let mut event_status = event::Status::Ignored;
|
||||
|
||||
let body_layout = if let Some(title_bar) = &mut self.title_bar {
|
||||
let mut children = layout.children();
|
||||
|
||||
event_status = title_bar.on_event(
|
||||
&mut tree.children[1],
|
||||
event.clone(),
|
||||
children.next().unwrap(),
|
||||
cursor_position,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
);
|
||||
|
||||
children.next().unwrap()
|
||||
} else {
|
||||
layout
|
||||
};
|
||||
|
||||
let body_status = if is_picked {
|
||||
event::Status::Ignored
|
||||
} else {
|
||||
self.body.as_widget_mut().on_event(
|
||||
&mut tree.children[0],
|
||||
event,
|
||||
body_layout,
|
||||
cursor_position,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
)
|
||||
};
|
||||
|
||||
event_status.merge(body_status)
|
||||
}
|
||||
|
||||
pub(crate) fn mouse_interaction(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
renderer: &Renderer,
|
||||
drag_enabled: bool,
|
||||
) -> mouse::Interaction {
|
||||
let (body_layout, title_bar_interaction) =
|
||||
if let Some(title_bar) = &self.title_bar {
|
||||
let mut children = layout.children();
|
||||
let title_bar_layout = children.next().unwrap();
|
||||
|
||||
let is_over_pick_area = title_bar
|
||||
.is_over_pick_area(title_bar_layout, cursor_position);
|
||||
|
||||
if is_over_pick_area && drag_enabled {
|
||||
return mouse::Interaction::Grab;
|
||||
}
|
||||
|
||||
let mouse_interaction = title_bar.mouse_interaction(
|
||||
&tree.children[1],
|
||||
title_bar_layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
renderer,
|
||||
);
|
||||
|
||||
(children.next().unwrap(), mouse_interaction)
|
||||
} else {
|
||||
(layout, mouse::Interaction::default())
|
||||
};
|
||||
|
||||
self.body
|
||||
.as_widget()
|
||||
.mouse_interaction(
|
||||
&tree.children[0],
|
||||
body_layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
renderer,
|
||||
)
|
||||
.max(title_bar_interaction)
|
||||
}
|
||||
|
||||
pub(crate) fn overlay<'b>(
|
||||
&'b mut self,
|
||||
tree: &'b mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
) -> Option<overlay::Element<'b, Message, Renderer>> {
|
||||
if let Some(title_bar) = self.title_bar.as_mut() {
|
||||
let mut children = layout.children();
|
||||
let title_bar_layout = children.next()?;
|
||||
|
||||
let mut states = tree.children.iter_mut();
|
||||
let body_state = states.next().unwrap();
|
||||
let title_bar_state = states.next().unwrap();
|
||||
|
||||
match title_bar.overlay(title_bar_state, title_bar_layout, renderer)
|
||||
{
|
||||
Some(overlay) => Some(overlay),
|
||||
None => self.body.as_widget_mut().overlay(
|
||||
body_state,
|
||||
children.next()?,
|
||||
renderer,
|
||||
),
|
||||
}
|
||||
} else {
|
||||
self.body.as_widget_mut().overlay(
|
||||
&mut tree.children[0],
|
||||
layout,
|
||||
renderer,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Draggable for &Content<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: crate::Renderer,
|
||||
Renderer::Theme: container::StyleSheet,
|
||||
{
|
||||
fn can_be_dragged_at(
|
||||
&self,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
) -> bool {
|
||||
if let Some(title_bar) = &self.title_bar {
|
||||
let mut children = layout.children();
|
||||
let title_bar_layout = children.next().unwrap();
|
||||
|
||||
title_bar.is_over_pick_area(title_bar_layout, cursor_position)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T, Message, Renderer> From<T> for Content<'a, Message, Renderer>
|
||||
where
|
||||
T: Into<Element<'a, Message, Renderer>>,
|
||||
Renderer: crate::Renderer,
|
||||
Renderer::Theme: container::StyleSheet,
|
||||
{
|
||||
fn from(element: T) -> Self {
|
||||
Self::new(element)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
/// A four cardinal direction.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Direction {
|
||||
/// ↑
|
||||
Up,
|
||||
/// ↓
|
||||
Down,
|
||||
/// ←
|
||||
Left,
|
||||
/// →
|
||||
Right,
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
use crate::{Layout, Point};
|
||||
|
||||
/// A pane that can be dragged.
|
||||
pub trait Draggable {
|
||||
/// Returns whether the [`Draggable`] with the given [`Layout`] can be picked
|
||||
/// at the provided cursor position.
|
||||
fn can_be_dragged_at(
|
||||
&self,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
) -> bool;
|
||||
}
|
||||
|
|
@ -1,250 +0,0 @@
|
|||
use crate::widget::pane_grid::{Axis, Pane, Split};
|
||||
use crate::{Rectangle, Size};
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
/// A layout node of a [`PaneGrid`].
|
||||
///
|
||||
/// [`PaneGrid`]: crate::widget::PaneGrid
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Node {
|
||||
/// The region of this [`Node`] is split into two.
|
||||
Split {
|
||||
/// The [`Split`] of this [`Node`].
|
||||
id: Split,
|
||||
|
||||
/// The direction of the split.
|
||||
axis: Axis,
|
||||
|
||||
/// The ratio of the split in [0.0, 1.0].
|
||||
ratio: f32,
|
||||
|
||||
/// The left/top [`Node`] of the split.
|
||||
a: Box<Node>,
|
||||
|
||||
/// The right/bottom [`Node`] of the split.
|
||||
b: Box<Node>,
|
||||
},
|
||||
/// The region of this [`Node`] is taken by a [`Pane`].
|
||||
Pane(Pane),
|
||||
}
|
||||
|
||||
impl Node {
|
||||
/// Returns an iterator over each [`Split`] in this [`Node`].
|
||||
pub fn splits(&self) -> impl Iterator<Item = &Split> {
|
||||
let mut unvisited_nodes = vec![self];
|
||||
|
||||
std::iter::from_fn(move || {
|
||||
while let Some(node) = unvisited_nodes.pop() {
|
||||
if let Node::Split { id, a, b, .. } = node {
|
||||
unvisited_nodes.push(a);
|
||||
unvisited_nodes.push(b);
|
||||
|
||||
return Some(id);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the rectangular region for each [`Pane`] in the [`Node`] given
|
||||
/// the spacing between panes and the total available space.
|
||||
pub fn pane_regions(
|
||||
&self,
|
||||
spacing: f32,
|
||||
size: Size,
|
||||
) -> BTreeMap<Pane, Rectangle> {
|
||||
let mut regions = BTreeMap::new();
|
||||
|
||||
self.compute_regions(
|
||||
spacing,
|
||||
&Rectangle {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
},
|
||||
&mut regions,
|
||||
);
|
||||
|
||||
regions
|
||||
}
|
||||
|
||||
/// Returns the axis, rectangular region, and ratio for each [`Split`] in
|
||||
/// the [`Node`] given the spacing between panes and the total available
|
||||
/// space.
|
||||
pub fn split_regions(
|
||||
&self,
|
||||
spacing: f32,
|
||||
size: Size,
|
||||
) -> BTreeMap<Split, (Axis, Rectangle, f32)> {
|
||||
let mut splits = BTreeMap::new();
|
||||
|
||||
self.compute_splits(
|
||||
spacing,
|
||||
&Rectangle {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
},
|
||||
&mut splits,
|
||||
);
|
||||
|
||||
splits
|
||||
}
|
||||
|
||||
pub(crate) fn find(&mut self, pane: &Pane) -> Option<&mut Node> {
|
||||
match self {
|
||||
Node::Split { a, b, .. } => {
|
||||
a.find(pane).or_else(move || b.find(pane))
|
||||
}
|
||||
Node::Pane(p) => {
|
||||
if p == pane {
|
||||
Some(self)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn split(&mut self, id: Split, axis: Axis, new_pane: Pane) {
|
||||
*self = Node::Split {
|
||||
id,
|
||||
axis,
|
||||
ratio: 0.5,
|
||||
a: Box::new(self.clone()),
|
||||
b: Box::new(Node::Pane(new_pane)),
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn update(&mut self, f: &impl Fn(&mut Node)) {
|
||||
if let Node::Split { a, b, .. } = self {
|
||||
a.update(f);
|
||||
b.update(f);
|
||||
}
|
||||
|
||||
f(self);
|
||||
}
|
||||
|
||||
pub(crate) fn resize(&mut self, split: &Split, percentage: f32) -> bool {
|
||||
match self {
|
||||
Node::Split {
|
||||
id, ratio, a, b, ..
|
||||
} => {
|
||||
if id == split {
|
||||
*ratio = percentage;
|
||||
|
||||
true
|
||||
} else if a.resize(split, percentage) {
|
||||
true
|
||||
} else {
|
||||
b.resize(split, percentage)
|
||||
}
|
||||
}
|
||||
Node::Pane(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn remove(&mut self, pane: &Pane) -> Option<Pane> {
|
||||
match self {
|
||||
Node::Split { a, b, .. } => {
|
||||
if a.pane() == Some(*pane) {
|
||||
*self = *b.clone();
|
||||
Some(self.first_pane())
|
||||
} else if b.pane() == Some(*pane) {
|
||||
*self = *a.clone();
|
||||
Some(self.first_pane())
|
||||
} else {
|
||||
a.remove(pane).or_else(|| b.remove(pane))
|
||||
}
|
||||
}
|
||||
Node::Pane(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn pane(&self) -> Option<Pane> {
|
||||
match self {
|
||||
Node::Split { .. } => None,
|
||||
Node::Pane(pane) => Some(*pane),
|
||||
}
|
||||
}
|
||||
|
||||
fn first_pane(&self) -> Pane {
|
||||
match self {
|
||||
Node::Split { a, .. } => a.first_pane(),
|
||||
Node::Pane(pane) => *pane,
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_regions(
|
||||
&self,
|
||||
spacing: f32,
|
||||
current: &Rectangle,
|
||||
regions: &mut BTreeMap<Pane, Rectangle>,
|
||||
) {
|
||||
match self {
|
||||
Node::Split {
|
||||
axis, ratio, a, b, ..
|
||||
} => {
|
||||
let (region_a, region_b) = axis.split(current, *ratio, spacing);
|
||||
|
||||
a.compute_regions(spacing, ®ion_a, regions);
|
||||
b.compute_regions(spacing, ®ion_b, regions);
|
||||
}
|
||||
Node::Pane(pane) => {
|
||||
let _ = regions.insert(*pane, *current);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_splits(
|
||||
&self,
|
||||
spacing: f32,
|
||||
current: &Rectangle,
|
||||
splits: &mut BTreeMap<Split, (Axis, Rectangle, f32)>,
|
||||
) {
|
||||
match self {
|
||||
Node::Split {
|
||||
axis,
|
||||
ratio,
|
||||
a,
|
||||
b,
|
||||
id,
|
||||
} => {
|
||||
let (region_a, region_b) = axis.split(current, *ratio, spacing);
|
||||
|
||||
let _ = splits.insert(*id, (*axis, *current, *ratio));
|
||||
|
||||
a.compute_splits(spacing, ®ion_a, splits);
|
||||
b.compute_splits(spacing, ®ion_b, splits);
|
||||
}
|
||||
Node::Pane(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::hash::Hash for Node {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
match self {
|
||||
Node::Split {
|
||||
id,
|
||||
axis,
|
||||
ratio,
|
||||
a,
|
||||
b,
|
||||
} => {
|
||||
id.hash(state);
|
||||
axis.hash(state);
|
||||
((ratio * 100_000.0) as u32).hash(state);
|
||||
a.hash(state);
|
||||
b.hash(state);
|
||||
}
|
||||
Node::Pane(pane) => {
|
||||
pane.hash(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
/// A rectangular region in a [`PaneGrid`] used to display widgets.
|
||||
///
|
||||
/// [`PaneGrid`]: crate::widget::PaneGrid
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct Pane(pub(super) usize);
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
/// A divider that splits a region in a [`PaneGrid`] into two different panes.
|
||||
///
|
||||
/// [`PaneGrid`]: crate::widget::PaneGrid
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct Split(pub(super) usize);
|
||||
|
|
@ -1,350 +0,0 @@
|
|||
//! The state of a [`PaneGrid`].
|
||||
//!
|
||||
//! [`PaneGrid`]: crate::widget::PaneGrid
|
||||
use crate::widget::pane_grid::{
|
||||
Axis, Configuration, Direction, Node, Pane, Split,
|
||||
};
|
||||
use crate::{Point, Size};
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// The state of a [`PaneGrid`].
|
||||
///
|
||||
/// It keeps track of the state of each [`Pane`] and the position of each
|
||||
/// [`Split`].
|
||||
///
|
||||
/// The [`State`] needs to own any mutable contents a [`Pane`] may need. This is
|
||||
/// why this struct is generic over the type `T`. Values of this type are
|
||||
/// provided to the view function of [`PaneGrid::new`] for displaying each
|
||||
/// [`Pane`].
|
||||
///
|
||||
/// [`PaneGrid`]: crate::widget::PaneGrid
|
||||
/// [`PaneGrid::new`]: crate::widget::PaneGrid::new
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct State<T> {
|
||||
/// The panes of the [`PaneGrid`].
|
||||
///
|
||||
/// [`PaneGrid`]: crate::widget::PaneGrid
|
||||
pub panes: HashMap<Pane, T>,
|
||||
|
||||
/// The internal state of the [`PaneGrid`].
|
||||
///
|
||||
/// [`PaneGrid`]: crate::widget::PaneGrid
|
||||
pub internal: Internal,
|
||||
|
||||
/// The maximized [`Pane`] of the [`PaneGrid`].
|
||||
///
|
||||
/// [`PaneGrid`]: crate::widget::PaneGrid
|
||||
pub(super) maximized: Option<Pane>,
|
||||
}
|
||||
|
||||
impl<T> State<T> {
|
||||
/// Creates a new [`State`], initializing the first pane with the provided
|
||||
/// state.
|
||||
///
|
||||
/// Alongside the [`State`], it returns the first [`Pane`] identifier.
|
||||
pub fn new(first_pane_state: T) -> (Self, Pane) {
|
||||
(
|
||||
Self::with_configuration(Configuration::Pane(first_pane_state)),
|
||||
Pane(0),
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a new [`State`] with the given [`Configuration`].
|
||||
pub fn with_configuration(config: impl Into<Configuration<T>>) -> Self {
|
||||
let mut panes = HashMap::new();
|
||||
|
||||
let internal =
|
||||
Internal::from_configuration(&mut panes, config.into(), 0);
|
||||
|
||||
State {
|
||||
panes,
|
||||
internal,
|
||||
maximized: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the total amount of panes in the [`State`].
|
||||
pub fn len(&self) -> usize {
|
||||
self.panes.len()
|
||||
}
|
||||
|
||||
/// Returns `true` if the amount of panes in the [`State`] is 0.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
|
||||
/// Returns the internal state of the given [`Pane`], if it exists.
|
||||
pub fn get(&self, pane: &Pane) -> Option<&T> {
|
||||
self.panes.get(pane)
|
||||
}
|
||||
|
||||
/// Returns the internal state of the given [`Pane`] with mutability, if it
|
||||
/// exists.
|
||||
pub fn get_mut(&mut self, pane: &Pane) -> Option<&mut T> {
|
||||
self.panes.get_mut(pane)
|
||||
}
|
||||
|
||||
/// Returns an iterator over all the panes of the [`State`], alongside its
|
||||
/// internal state.
|
||||
pub fn iter(&self) -> impl Iterator<Item = (&Pane, &T)> {
|
||||
self.panes.iter()
|
||||
}
|
||||
|
||||
/// Returns a mutable iterator over all the panes of the [`State`],
|
||||
/// alongside its internal state.
|
||||
pub fn iter_mut(&mut self) -> impl Iterator<Item = (&Pane, &mut T)> {
|
||||
self.panes.iter_mut()
|
||||
}
|
||||
|
||||
/// Returns the layout of the [`State`].
|
||||
pub fn layout(&self) -> &Node {
|
||||
&self.internal.layout
|
||||
}
|
||||
|
||||
/// Returns the adjacent [`Pane`] of another [`Pane`] in the given
|
||||
/// direction, if there is one.
|
||||
pub fn adjacent(&self, pane: &Pane, direction: Direction) -> Option<Pane> {
|
||||
let regions = self
|
||||
.internal
|
||||
.layout
|
||||
.pane_regions(0.0, Size::new(4096.0, 4096.0));
|
||||
|
||||
let current_region = regions.get(pane)?;
|
||||
|
||||
let target = match direction {
|
||||
Direction::Left => {
|
||||
Point::new(current_region.x - 1.0, current_region.y + 1.0)
|
||||
}
|
||||
Direction::Right => Point::new(
|
||||
current_region.x + current_region.width + 1.0,
|
||||
current_region.y + 1.0,
|
||||
),
|
||||
Direction::Up => {
|
||||
Point::new(current_region.x + 1.0, current_region.y - 1.0)
|
||||
}
|
||||
Direction::Down => Point::new(
|
||||
current_region.x + 1.0,
|
||||
current_region.y + current_region.height + 1.0,
|
||||
),
|
||||
};
|
||||
|
||||
let mut colliding_regions =
|
||||
regions.iter().filter(|(_, region)| region.contains(target));
|
||||
|
||||
let (pane, _) = colliding_regions.next()?;
|
||||
|
||||
Some(*pane)
|
||||
}
|
||||
|
||||
/// Splits the given [`Pane`] into two in the given [`Axis`] and
|
||||
/// initializing the new [`Pane`] with the provided internal state.
|
||||
pub fn split(
|
||||
&mut self,
|
||||
axis: Axis,
|
||||
pane: &Pane,
|
||||
state: T,
|
||||
) -> Option<(Pane, Split)> {
|
||||
let node = self.internal.layout.find(pane)?;
|
||||
|
||||
let new_pane = {
|
||||
self.internal.last_id = self.internal.last_id.checked_add(1)?;
|
||||
|
||||
Pane(self.internal.last_id)
|
||||
};
|
||||
|
||||
let new_split = {
|
||||
self.internal.last_id = self.internal.last_id.checked_add(1)?;
|
||||
|
||||
Split(self.internal.last_id)
|
||||
};
|
||||
|
||||
node.split(new_split, axis, new_pane);
|
||||
|
||||
let _ = self.panes.insert(new_pane, state);
|
||||
let _ = self.maximized.take();
|
||||
|
||||
Some((new_pane, new_split))
|
||||
}
|
||||
|
||||
/// Swaps the position of the provided panes in the [`State`].
|
||||
///
|
||||
/// If you want to swap panes on drag and drop in your [`PaneGrid`], you
|
||||
/// will need to call this method when handling a [`DragEvent`].
|
||||
///
|
||||
/// [`PaneGrid`]: crate::widget::PaneGrid
|
||||
/// [`DragEvent`]: crate::widget::pane_grid::DragEvent
|
||||
pub fn swap(&mut self, a: &Pane, b: &Pane) {
|
||||
self.internal.layout.update(&|node| match node {
|
||||
Node::Split { .. } => {}
|
||||
Node::Pane(pane) => {
|
||||
if pane == a {
|
||||
*node = Node::Pane(*b);
|
||||
} else if pane == b {
|
||||
*node = Node::Pane(*a);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Resizes two panes by setting the position of the provided [`Split`].
|
||||
///
|
||||
/// The ratio is a value in [0, 1], representing the exact position of a
|
||||
/// [`Split`] between two panes.
|
||||
///
|
||||
/// If you want to enable resize interactions in your [`PaneGrid`], you will
|
||||
/// need to call this method when handling a [`ResizeEvent`].
|
||||
///
|
||||
/// [`PaneGrid`]: crate::widget::PaneGrid
|
||||
/// [`ResizeEvent`]: crate::widget::pane_grid::ResizeEvent
|
||||
pub fn resize(&mut self, split: &Split, ratio: f32) {
|
||||
let _ = self.internal.layout.resize(split, ratio);
|
||||
}
|
||||
|
||||
/// Closes the given [`Pane`] and returns its internal state and its closest
|
||||
/// sibling, if it exists.
|
||||
pub fn close(&mut self, pane: &Pane) -> Option<(T, Pane)> {
|
||||
if self.maximized == Some(*pane) {
|
||||
let _ = self.maximized.take();
|
||||
}
|
||||
|
||||
if let Some(sibling) = self.internal.layout.remove(pane) {
|
||||
self.panes.remove(pane).map(|state| (state, sibling))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Maximize the given [`Pane`]. Only this pane will be rendered by the
|
||||
/// [`PaneGrid`] until [`Self::restore()`] is called.
|
||||
///
|
||||
/// [`PaneGrid`]: crate::widget::PaneGrid
|
||||
pub fn maximize(&mut self, pane: &Pane) {
|
||||
self.maximized = Some(*pane);
|
||||
}
|
||||
|
||||
/// Restore the currently maximized [`Pane`] to it's normal size. All panes
|
||||
/// will be rendered by the [`PaneGrid`].
|
||||
///
|
||||
/// [`PaneGrid`]: crate::widget::PaneGrid
|
||||
pub fn restore(&mut self) {
|
||||
let _ = self.maximized.take();
|
||||
}
|
||||
|
||||
/// Returns the maximized [`Pane`] of the [`PaneGrid`].
|
||||
///
|
||||
/// [`PaneGrid`]: crate::widget::PaneGrid
|
||||
pub fn maximized(&self) -> Option<Pane> {
|
||||
self.maximized
|
||||
}
|
||||
}
|
||||
|
||||
/// The internal state of a [`PaneGrid`].
|
||||
///
|
||||
/// [`PaneGrid`]: crate::widget::PaneGrid
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Internal {
|
||||
layout: Node,
|
||||
last_id: usize,
|
||||
}
|
||||
|
||||
impl Internal {
|
||||
/// Initializes the [`Internal`] state of a [`PaneGrid`] from a
|
||||
/// [`Configuration`].
|
||||
///
|
||||
/// [`PaneGrid`]: crate::widget::PaneGrid
|
||||
pub fn from_configuration<T>(
|
||||
panes: &mut HashMap<Pane, T>,
|
||||
content: Configuration<T>,
|
||||
next_id: usize,
|
||||
) -> Self {
|
||||
let (layout, last_id) = match content {
|
||||
Configuration::Split { axis, ratio, a, b } => {
|
||||
let Internal {
|
||||
layout: a,
|
||||
last_id: next_id,
|
||||
..
|
||||
} = Self::from_configuration(panes, *a, next_id);
|
||||
|
||||
let Internal {
|
||||
layout: b,
|
||||
last_id: next_id,
|
||||
..
|
||||
} = Self::from_configuration(panes, *b, next_id);
|
||||
|
||||
(
|
||||
Node::Split {
|
||||
id: Split(next_id),
|
||||
axis,
|
||||
ratio,
|
||||
a: Box::new(a),
|
||||
b: Box::new(b),
|
||||
},
|
||||
next_id + 1,
|
||||
)
|
||||
}
|
||||
Configuration::Pane(state) => {
|
||||
let id = Pane(next_id);
|
||||
let _ = panes.insert(id, state);
|
||||
|
||||
(Node::Pane(id), next_id + 1)
|
||||
}
|
||||
};
|
||||
|
||||
Self { layout, last_id }
|
||||
}
|
||||
}
|
||||
|
||||
/// The current action of a [`PaneGrid`].
|
||||
///
|
||||
/// [`PaneGrid`]: crate::widget::PaneGrid
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum Action {
|
||||
/// The [`PaneGrid`] is idle.
|
||||
///
|
||||
/// [`PaneGrid`]: crate::widget::PaneGrid
|
||||
Idle,
|
||||
/// A [`Pane`] in the [`PaneGrid`] is being dragged.
|
||||
///
|
||||
/// [`PaneGrid`]: crate::widget::PaneGrid
|
||||
Dragging {
|
||||
/// The [`Pane`] being dragged.
|
||||
pane: Pane,
|
||||
/// The starting [`Point`] of the drag interaction.
|
||||
origin: Point,
|
||||
},
|
||||
/// A [`Split`] in the [`PaneGrid`] is being dragged.
|
||||
///
|
||||
/// [`PaneGrid`]: crate::widget::PaneGrid
|
||||
Resizing {
|
||||
/// The [`Split`] being dragged.
|
||||
split: Split,
|
||||
/// The [`Axis`] of the [`Split`].
|
||||
axis: Axis,
|
||||
},
|
||||
}
|
||||
|
||||
impl Action {
|
||||
/// Returns the current [`Pane`] that is being dragged, if any.
|
||||
pub fn picked_pane(&self) -> Option<(Pane, Point)> {
|
||||
match *self {
|
||||
Action::Dragging { pane, origin, .. } => Some((pane, origin)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the current [`Split`] that is being dragged, if any.
|
||||
pub fn picked_split(&self) -> Option<(Split, Axis)> {
|
||||
match *self {
|
||||
Action::Resizing { split, axis, .. } => Some((split, axis)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Internal {
|
||||
/// The layout [`Node`] of the [`Internal`] state
|
||||
pub fn layout(&self) -> &Node {
|
||||
&self.layout
|
||||
}
|
||||
}
|
||||
|
|
@ -1,432 +0,0 @@
|
|||
use crate::event::{self, Event};
|
||||
use crate::layout;
|
||||
use crate::mouse;
|
||||
use crate::overlay;
|
||||
use crate::renderer;
|
||||
use crate::widget::container;
|
||||
use crate::widget::{self, Tree};
|
||||
use crate::{
|
||||
Clipboard, Element, Layout, Padding, Point, Rectangle, Shell, Size,
|
||||
};
|
||||
|
||||
/// The title bar of a [`Pane`].
|
||||
///
|
||||
/// [`Pane`]: crate::widget::pane_grid::Pane
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct TitleBar<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: crate::Renderer,
|
||||
Renderer::Theme: container::StyleSheet,
|
||||
{
|
||||
content: Element<'a, Message, Renderer>,
|
||||
controls: Option<Element<'a, Message, Renderer>>,
|
||||
padding: Padding,
|
||||
always_show_controls: bool,
|
||||
style: <Renderer::Theme as container::StyleSheet>::Style,
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> TitleBar<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: crate::Renderer,
|
||||
Renderer::Theme: container::StyleSheet,
|
||||
{
|
||||
/// Creates a new [`TitleBar`] with the given content.
|
||||
pub fn new<E>(content: E) -> Self
|
||||
where
|
||||
E: Into<Element<'a, Message, Renderer>>,
|
||||
{
|
||||
Self {
|
||||
content: content.into(),
|
||||
controls: None,
|
||||
padding: Padding::ZERO,
|
||||
always_show_controls: false,
|
||||
style: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the controls of the [`TitleBar`].
|
||||
pub fn controls(
|
||||
mut self,
|
||||
controls: impl Into<Element<'a, Message, Renderer>>,
|
||||
) -> Self {
|
||||
self.controls = Some(controls.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`Padding`] of the [`TitleBar`].
|
||||
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
|
||||
self.padding = padding.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the [`TitleBar`].
|
||||
pub fn style(
|
||||
mut self,
|
||||
style: impl Into<<Renderer::Theme as container::StyleSheet>::Style>,
|
||||
) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets whether or not the [`controls`] attached to this [`TitleBar`] are
|
||||
/// always visible.
|
||||
///
|
||||
/// By default, the controls are only visible when the [`Pane`] of this
|
||||
/// [`TitleBar`] is hovered.
|
||||
///
|
||||
/// [`controls`]: Self::controls
|
||||
/// [`Pane`]: crate::widget::pane_grid::Pane
|
||||
pub fn always_show_controls(mut self) -> Self {
|
||||
self.always_show_controls = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> TitleBar<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: crate::Renderer,
|
||||
Renderer::Theme: container::StyleSheet,
|
||||
{
|
||||
pub(super) fn state(&self) -> Tree {
|
||||
let children = if let Some(controls) = self.controls.as_ref() {
|
||||
vec![Tree::new(&self.content), Tree::new(controls)]
|
||||
} else {
|
||||
vec![Tree::new(&self.content), Tree::empty()]
|
||||
};
|
||||
|
||||
Tree {
|
||||
children,
|
||||
..Tree::empty()
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn diff(&self, tree: &mut Tree) {
|
||||
if tree.children.len() == 2 {
|
||||
if let Some(controls) = self.controls.as_ref() {
|
||||
tree.children[1].diff(controls);
|
||||
}
|
||||
|
||||
tree.children[0].diff(&self.content);
|
||||
} else {
|
||||
*tree = self.state();
|
||||
}
|
||||
}
|
||||
|
||||
/// Draws the [`TitleBar`] with the provided [`Renderer`] and [`Layout`].
|
||||
///
|
||||
/// [`Renderer`]: crate::Renderer
|
||||
pub fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
inherited_style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
show_controls: bool,
|
||||
) {
|
||||
use container::StyleSheet;
|
||||
|
||||
let bounds = layout.bounds();
|
||||
let style = theme.appearance(&self.style);
|
||||
let inherited_style = renderer::Style {
|
||||
text_color: style.text_color.unwrap_or(inherited_style.text_color),
|
||||
};
|
||||
|
||||
container::draw_background(renderer, &style, bounds);
|
||||
|
||||
let mut children = layout.children();
|
||||
let padded = children.next().unwrap();
|
||||
|
||||
let mut children = padded.children();
|
||||
let title_layout = children.next().unwrap();
|
||||
let mut show_title = true;
|
||||
|
||||
if let Some(controls) = &self.controls {
|
||||
if show_controls || self.always_show_controls {
|
||||
let controls_layout = children.next().unwrap();
|
||||
if title_layout.bounds().width + controls_layout.bounds().width
|
||||
> padded.bounds().width
|
||||
{
|
||||
show_title = false;
|
||||
}
|
||||
|
||||
controls.as_widget().draw(
|
||||
&tree.children[1],
|
||||
renderer,
|
||||
theme,
|
||||
&inherited_style,
|
||||
controls_layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if show_title {
|
||||
self.content.as_widget().draw(
|
||||
&tree.children[0],
|
||||
renderer,
|
||||
theme,
|
||||
&inherited_style,
|
||||
title_layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether the mouse cursor is over the pick area of the
|
||||
/// [`TitleBar`] or not.
|
||||
///
|
||||
/// The whole [`TitleBar`] is a pick area, except its controls.
|
||||
pub fn is_over_pick_area(
|
||||
&self,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
) -> bool {
|
||||
if layout.bounds().contains(cursor_position) {
|
||||
let mut children = layout.children();
|
||||
let padded = children.next().unwrap();
|
||||
let mut children = padded.children();
|
||||
let title_layout = children.next().unwrap();
|
||||
|
||||
if self.controls.is_some() {
|
||||
let controls_layout = children.next().unwrap();
|
||||
|
||||
if title_layout.bounds().width + controls_layout.bounds().width
|
||||
> padded.bounds().width
|
||||
{
|
||||
!controls_layout.bounds().contains(cursor_position)
|
||||
} else {
|
||||
!controls_layout.bounds().contains(cursor_position)
|
||||
&& !title_layout.bounds().contains(cursor_position)
|
||||
}
|
||||
} else {
|
||||
!title_layout.bounds().contains(cursor_position)
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn layout(
|
||||
&self,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
let limits = limits.pad(self.padding);
|
||||
let max_size = limits.max();
|
||||
|
||||
let title_layout = self
|
||||
.content
|
||||
.as_widget()
|
||||
.layout(renderer, &layout::Limits::new(Size::ZERO, max_size));
|
||||
|
||||
let title_size = title_layout.size();
|
||||
|
||||
let mut node = if let Some(controls) = &self.controls {
|
||||
let mut controls_layout = controls
|
||||
.as_widget()
|
||||
.layout(renderer, &layout::Limits::new(Size::ZERO, max_size));
|
||||
|
||||
let controls_size = controls_layout.size();
|
||||
let space_before_controls = max_size.width - controls_size.width;
|
||||
|
||||
let height = title_size.height.max(controls_size.height);
|
||||
|
||||
controls_layout.move_to(Point::new(space_before_controls, 0.0));
|
||||
|
||||
layout::Node::with_children(
|
||||
Size::new(max_size.width, height),
|
||||
vec![title_layout, controls_layout],
|
||||
)
|
||||
} else {
|
||||
layout::Node::with_children(
|
||||
Size::new(max_size.width, title_size.height),
|
||||
vec![title_layout],
|
||||
)
|
||||
};
|
||||
|
||||
node.move_to(Point::new(self.padding.left, self.padding.top));
|
||||
|
||||
layout::Node::with_children(node.size().pad(self.padding), vec![node])
|
||||
}
|
||||
|
||||
pub(crate) fn operate(
|
||||
&self,
|
||||
tree: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn widget::Operation<Message>,
|
||||
) {
|
||||
let mut children = layout.children();
|
||||
let padded = children.next().unwrap();
|
||||
|
||||
let mut children = padded.children();
|
||||
let title_layout = children.next().unwrap();
|
||||
let mut show_title = true;
|
||||
|
||||
if let Some(controls) = &self.controls {
|
||||
let controls_layout = children.next().unwrap();
|
||||
|
||||
if title_layout.bounds().width + controls_layout.bounds().width
|
||||
> padded.bounds().width
|
||||
{
|
||||
show_title = false;
|
||||
}
|
||||
|
||||
controls.as_widget().operate(
|
||||
&mut tree.children[1],
|
||||
controls_layout,
|
||||
renderer,
|
||||
operation,
|
||||
)
|
||||
};
|
||||
|
||||
if show_title {
|
||||
self.content.as_widget().operate(
|
||||
&mut tree.children[0],
|
||||
title_layout,
|
||||
renderer,
|
||||
operation,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn on_event(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
let mut children = layout.children();
|
||||
let padded = children.next().unwrap();
|
||||
|
||||
let mut children = padded.children();
|
||||
let title_layout = children.next().unwrap();
|
||||
let mut show_title = true;
|
||||
|
||||
let control_status = if let Some(controls) = &mut self.controls {
|
||||
let controls_layout = children.next().unwrap();
|
||||
if title_layout.bounds().width + controls_layout.bounds().width
|
||||
> padded.bounds().width
|
||||
{
|
||||
show_title = false;
|
||||
}
|
||||
|
||||
controls.as_widget_mut().on_event(
|
||||
&mut tree.children[1],
|
||||
event.clone(),
|
||||
controls_layout,
|
||||
cursor_position,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
)
|
||||
} else {
|
||||
event::Status::Ignored
|
||||
};
|
||||
|
||||
let title_status = if show_title {
|
||||
self.content.as_widget_mut().on_event(
|
||||
&mut tree.children[0],
|
||||
event,
|
||||
title_layout,
|
||||
cursor_position,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
)
|
||||
} else {
|
||||
event::Status::Ignored
|
||||
};
|
||||
|
||||
control_status.merge(title_status)
|
||||
}
|
||||
|
||||
pub(crate) fn mouse_interaction(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
let mut children = layout.children();
|
||||
let padded = children.next().unwrap();
|
||||
|
||||
let mut children = padded.children();
|
||||
let title_layout = children.next().unwrap();
|
||||
|
||||
let title_interaction = self.content.as_widget().mouse_interaction(
|
||||
&tree.children[0],
|
||||
title_layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
renderer,
|
||||
);
|
||||
|
||||
if let Some(controls) = &self.controls {
|
||||
let controls_layout = children.next().unwrap();
|
||||
let controls_interaction = controls.as_widget().mouse_interaction(
|
||||
&tree.children[1],
|
||||
controls_layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
renderer,
|
||||
);
|
||||
|
||||
if title_layout.bounds().width + controls_layout.bounds().width
|
||||
> padded.bounds().width
|
||||
{
|
||||
controls_interaction
|
||||
} else {
|
||||
controls_interaction.max(title_interaction)
|
||||
}
|
||||
} else {
|
||||
title_interaction
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn overlay<'b>(
|
||||
&'b mut self,
|
||||
tree: &'b mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
) -> Option<overlay::Element<'b, Message, Renderer>> {
|
||||
let mut children = layout.children();
|
||||
let padded = children.next()?;
|
||||
|
||||
let mut children = padded.children();
|
||||
let title_layout = children.next()?;
|
||||
|
||||
let Self {
|
||||
content, controls, ..
|
||||
} = self;
|
||||
|
||||
let mut states = tree.children.iter_mut();
|
||||
let title_state = states.next().unwrap();
|
||||
let controls_state = states.next().unwrap();
|
||||
|
||||
content
|
||||
.as_widget_mut()
|
||||
.overlay(title_state, title_layout, renderer)
|
||||
.or_else(move || {
|
||||
controls.as_mut().and_then(|controls| {
|
||||
let controls_layout = children.next()?;
|
||||
|
||||
controls.as_widget_mut().overlay(
|
||||
controls_state,
|
||||
controls_layout,
|
||||
renderer,
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,657 +0,0 @@
|
|||
//! Display a dropdown list of selectable values.
|
||||
use crate::alignment;
|
||||
use crate::event::{self, Event};
|
||||
use crate::keyboard;
|
||||
use crate::layout;
|
||||
use crate::mouse;
|
||||
use crate::overlay;
|
||||
use crate::overlay::menu::{self, Menu};
|
||||
use crate::renderer;
|
||||
use crate::text::{self, Text};
|
||||
use crate::touch;
|
||||
use crate::widget::container;
|
||||
use crate::widget::scrollable;
|
||||
use crate::widget::tree::{self, Tree};
|
||||
use crate::{
|
||||
Clipboard, Element, Layout, Length, Padding, Pixels, Point, Rectangle,
|
||||
Shell, Size, Widget,
|
||||
};
|
||||
use std::borrow::Cow;
|
||||
|
||||
pub use iced_style::pick_list::{Appearance, StyleSheet};
|
||||
|
||||
/// A widget for selecting a single value from a list of options.
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct PickList<'a, T, Message, Renderer>
|
||||
where
|
||||
[T]: ToOwned<Owned = Vec<T>>,
|
||||
Renderer: text::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
on_selected: Box<dyn Fn(T) -> Message + 'a>,
|
||||
options: Cow<'a, [T]>,
|
||||
placeholder: Option<String>,
|
||||
selected: Option<T>,
|
||||
width: Length,
|
||||
padding: Padding,
|
||||
text_size: Option<f32>,
|
||||
font: Option<Renderer::Font>,
|
||||
handle: Handle<Renderer::Font>,
|
||||
style: <Renderer::Theme as StyleSheet>::Style,
|
||||
}
|
||||
|
||||
impl<'a, T: 'a, Message, Renderer> PickList<'a, T, Message, Renderer>
|
||||
where
|
||||
T: ToString + Eq,
|
||||
[T]: ToOwned<Owned = Vec<T>>,
|
||||
Renderer: text::Renderer,
|
||||
Renderer::Theme: StyleSheet
|
||||
+ scrollable::StyleSheet
|
||||
+ menu::StyleSheet
|
||||
+ container::StyleSheet,
|
||||
<Renderer::Theme as menu::StyleSheet>::Style:
|
||||
From<<Renderer::Theme as StyleSheet>::Style>,
|
||||
{
|
||||
/// The default padding of a [`PickList`].
|
||||
pub const DEFAULT_PADDING: Padding = Padding::new(5.0);
|
||||
|
||||
/// Creates a new [`PickList`] with the given list of options, the current
|
||||
/// selected value, and the message to produce when an option is selected.
|
||||
pub fn new(
|
||||
options: impl Into<Cow<'a, [T]>>,
|
||||
selected: Option<T>,
|
||||
on_selected: impl Fn(T) -> Message + 'a,
|
||||
) -> Self {
|
||||
Self {
|
||||
on_selected: Box::new(on_selected),
|
||||
options: options.into(),
|
||||
placeholder: None,
|
||||
selected,
|
||||
width: Length::Shrink,
|
||||
padding: Self::DEFAULT_PADDING,
|
||||
text_size: None,
|
||||
font: None,
|
||||
handle: Default::default(),
|
||||
style: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the placeholder of the [`PickList`].
|
||||
pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
|
||||
self.placeholder = Some(placeholder.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the width of the [`PickList`].
|
||||
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||
self.width = width.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`Padding`] of the [`PickList`].
|
||||
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
|
||||
self.padding = padding.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the text size of the [`PickList`].
|
||||
pub fn text_size(mut self, size: impl Into<Pixels>) -> Self {
|
||||
self.text_size = Some(size.into().0);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the font of the [`PickList`].
|
||||
pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
|
||||
self.font = Some(font.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`Handle`] of the [`PickList`].
|
||||
pub fn handle(mut self, handle: Handle<Renderer::Font>) -> Self {
|
||||
self.handle = handle;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the [`PickList`].
|
||||
pub fn style(
|
||||
mut self,
|
||||
style: impl Into<<Renderer::Theme as StyleSheet>::Style>,
|
||||
) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: 'a, Message, Renderer> Widget<Message, Renderer>
|
||||
for PickList<'a, T, Message, Renderer>
|
||||
where
|
||||
T: Clone + ToString + Eq + 'static,
|
||||
[T]: ToOwned<Owned = Vec<T>>,
|
||||
Message: 'a,
|
||||
Renderer: text::Renderer + 'a,
|
||||
Renderer::Theme: StyleSheet
|
||||
+ scrollable::StyleSheet
|
||||
+ menu::StyleSheet
|
||||
+ container::StyleSheet,
|
||||
<Renderer::Theme as menu::StyleSheet>::Style:
|
||||
From<<Renderer::Theme as StyleSheet>::Style>,
|
||||
{
|
||||
fn tag(&self) -> tree::Tag {
|
||||
tree::Tag::of::<State<T>>()
|
||||
}
|
||||
|
||||
fn state(&self) -> tree::State {
|
||||
tree::State::new(State::<T>::new())
|
||||
}
|
||||
|
||||
fn width(&self) -> Length {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
Length::Shrink
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
layout(
|
||||
renderer,
|
||||
limits,
|
||||
self.width,
|
||||
self.padding,
|
||||
self.text_size,
|
||||
self.font,
|
||||
self.placeholder.as_deref(),
|
||||
&self.options,
|
||||
)
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
_renderer: &Renderer,
|
||||
_clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
update(
|
||||
event,
|
||||
layout,
|
||||
cursor_position,
|
||||
shell,
|
||||
self.on_selected.as_ref(),
|
||||
self.selected.as_ref(),
|
||||
&self.options,
|
||||
|| tree.state.downcast_mut::<State<T>>(),
|
||||
)
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
_tree: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
_renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
mouse_interaction(layout, cursor_position)
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
_style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
) {
|
||||
let font = self.font.unwrap_or_else(|| renderer.default_font());
|
||||
draw(
|
||||
renderer,
|
||||
theme,
|
||||
layout,
|
||||
cursor_position,
|
||||
self.padding,
|
||||
self.text_size,
|
||||
font,
|
||||
self.placeholder.as_deref(),
|
||||
self.selected.as_ref(),
|
||||
&self.handle,
|
||||
&self.style,
|
||||
|| tree.state.downcast_ref::<State<T>>(),
|
||||
)
|
||||
}
|
||||
|
||||
fn overlay<'b>(
|
||||
&'b mut self,
|
||||
tree: &'b mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
) -> Option<overlay::Element<'b, Message, Renderer>> {
|
||||
let state = tree.state.downcast_mut::<State<T>>();
|
||||
|
||||
overlay(
|
||||
layout,
|
||||
state,
|
||||
self.padding,
|
||||
self.text_size,
|
||||
self.font.unwrap_or_else(|| renderer.default_font()),
|
||||
&self.options,
|
||||
self.style.clone(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: 'a, Message, Renderer> From<PickList<'a, T, Message, Renderer>>
|
||||
for Element<'a, Message, Renderer>
|
||||
where
|
||||
T: Clone + ToString + Eq + 'static,
|
||||
[T]: ToOwned<Owned = Vec<T>>,
|
||||
Message: 'a,
|
||||
Renderer: text::Renderer + 'a,
|
||||
Renderer::Theme: StyleSheet
|
||||
+ scrollable::StyleSheet
|
||||
+ menu::StyleSheet
|
||||
+ container::StyleSheet,
|
||||
<Renderer::Theme as menu::StyleSheet>::Style:
|
||||
From<<Renderer::Theme as StyleSheet>::Style>,
|
||||
{
|
||||
fn from(pick_list: PickList<'a, T, Message, Renderer>) -> Self {
|
||||
Self::new(pick_list)
|
||||
}
|
||||
}
|
||||
|
||||
/// The local state of a [`PickList`].
|
||||
#[derive(Debug)]
|
||||
pub struct State<T> {
|
||||
menu: menu::State,
|
||||
keyboard_modifiers: keyboard::Modifiers,
|
||||
is_open: bool,
|
||||
hovered_option: Option<usize>,
|
||||
last_selection: Option<T>,
|
||||
}
|
||||
|
||||
impl<T> State<T> {
|
||||
/// Creates a new [`State`] for a [`PickList`].
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
menu: menu::State::default(),
|
||||
keyboard_modifiers: keyboard::Modifiers::default(),
|
||||
is_open: bool::default(),
|
||||
hovered_option: Option::default(),
|
||||
last_selection: Option::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Default for State<T> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// The handle to the right side of the [`PickList`].
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Handle<Font> {
|
||||
/// Displays an arrow icon (▼).
|
||||
///
|
||||
/// This is the default.
|
||||
Arrow {
|
||||
/// Font size of the content.
|
||||
size: Option<f32>,
|
||||
},
|
||||
/// A custom static handle.
|
||||
Static(Icon<Font>),
|
||||
/// A custom dynamic handle.
|
||||
Dynamic {
|
||||
/// The [`Icon`] used when [`PickList`] is closed.
|
||||
closed: Icon<Font>,
|
||||
/// The [`Icon`] used when [`PickList`] is open.
|
||||
open: Icon<Font>,
|
||||
},
|
||||
/// No handle will be shown.
|
||||
None,
|
||||
}
|
||||
|
||||
impl<Font> Default for Handle<Font> {
|
||||
fn default() -> Self {
|
||||
Self::Arrow { size: None }
|
||||
}
|
||||
}
|
||||
|
||||
/// The icon of a [`Handle`].
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Icon<Font> {
|
||||
/// Font that will be used to display the `code_point`,
|
||||
pub font: Font,
|
||||
/// The unicode code point that will be used as the icon.
|
||||
pub code_point: char,
|
||||
/// Font size of the content.
|
||||
pub size: Option<f32>,
|
||||
}
|
||||
|
||||
/// Computes the layout of a [`PickList`].
|
||||
pub fn layout<Renderer, T>(
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
width: Length,
|
||||
padding: Padding,
|
||||
text_size: Option<f32>,
|
||||
font: Option<Renderer::Font>,
|
||||
placeholder: Option<&str>,
|
||||
options: &[T],
|
||||
) -> layout::Node
|
||||
where
|
||||
Renderer: text::Renderer,
|
||||
T: ToString,
|
||||
{
|
||||
use std::f32;
|
||||
|
||||
let limits = limits.width(width).height(Length::Shrink).pad(padding);
|
||||
let text_size = text_size.unwrap_or_else(|| renderer.default_size());
|
||||
|
||||
let max_width = match width {
|
||||
Length::Shrink => {
|
||||
let measure = |label: &str| -> f32 {
|
||||
let (width, _) = renderer.measure(
|
||||
label,
|
||||
text_size,
|
||||
font.unwrap_or_else(|| renderer.default_font()),
|
||||
Size::new(f32::INFINITY, f32::INFINITY),
|
||||
);
|
||||
|
||||
width.round()
|
||||
};
|
||||
|
||||
let labels = options.iter().map(ToString::to_string);
|
||||
|
||||
let labels_width = labels
|
||||
.map(|label| measure(&label))
|
||||
.fold(100.0, |candidate, current| current.max(candidate));
|
||||
|
||||
let placeholder_width = placeholder.map(measure).unwrap_or(100.0);
|
||||
|
||||
labels_width.max(placeholder_width)
|
||||
}
|
||||
_ => 0.0,
|
||||
};
|
||||
|
||||
let size = {
|
||||
let intrinsic =
|
||||
Size::new(max_width + text_size + padding.left, text_size * 1.2);
|
||||
|
||||
limits.resolve(intrinsic).pad(padding)
|
||||
};
|
||||
|
||||
layout::Node::new(size)
|
||||
}
|
||||
|
||||
/// Processes an [`Event`] and updates the [`State`] of a [`PickList`]
|
||||
/// accordingly.
|
||||
pub fn update<'a, T, Message>(
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
on_selected: &dyn Fn(T) -> Message,
|
||||
selected: Option<&T>,
|
||||
options: &[T],
|
||||
state: impl FnOnce() -> &'a mut State<T>,
|
||||
) -> event::Status
|
||||
where
|
||||
T: PartialEq + Clone + 'a,
|
||||
{
|
||||
match event {
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
|
||||
| Event::Touch(touch::Event::FingerPressed { .. }) => {
|
||||
let state = state();
|
||||
|
||||
let event_status = if state.is_open {
|
||||
// Event wasn't processed by overlay, so cursor was clicked either outside it's
|
||||
// bounds or on the drop-down, either way we close the overlay.
|
||||
state.is_open = false;
|
||||
|
||||
event::Status::Captured
|
||||
} else if layout.bounds().contains(cursor_position) {
|
||||
state.is_open = true;
|
||||
state.hovered_option =
|
||||
options.iter().position(|option| Some(option) == selected);
|
||||
|
||||
event::Status::Captured
|
||||
} else {
|
||||
event::Status::Ignored
|
||||
};
|
||||
|
||||
if let Some(last_selection) = state.last_selection.take() {
|
||||
shell.publish((on_selected)(last_selection));
|
||||
|
||||
state.is_open = false;
|
||||
|
||||
event::Status::Captured
|
||||
} else {
|
||||
event_status
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse::Event::WheelScrolled {
|
||||
delta: mouse::ScrollDelta::Lines { y, .. },
|
||||
}) => {
|
||||
let state = state();
|
||||
|
||||
if state.keyboard_modifiers.command()
|
||||
&& layout.bounds().contains(cursor_position)
|
||||
&& !state.is_open
|
||||
{
|
||||
fn find_next<'a, T: PartialEq>(
|
||||
selected: &'a T,
|
||||
mut options: impl Iterator<Item = &'a T>,
|
||||
) -> Option<&'a T> {
|
||||
let _ = options.find(|&option| option == selected);
|
||||
|
||||
options.next()
|
||||
}
|
||||
|
||||
let next_option = if y < 0.0 {
|
||||
if let Some(selected) = selected {
|
||||
find_next(selected, options.iter())
|
||||
} else {
|
||||
options.first()
|
||||
}
|
||||
} else if y > 0.0 {
|
||||
if let Some(selected) = selected {
|
||||
find_next(selected, options.iter().rev())
|
||||
} else {
|
||||
options.last()
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(next_option) = next_option {
|
||||
shell.publish((on_selected)(next_option.clone()));
|
||||
}
|
||||
|
||||
event::Status::Captured
|
||||
} else {
|
||||
event::Status::Ignored
|
||||
}
|
||||
}
|
||||
Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
|
||||
let state = state();
|
||||
|
||||
state.keyboard_modifiers = modifiers;
|
||||
|
||||
event::Status::Ignored
|
||||
}
|
||||
_ => event::Status::Ignored,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the current [`mouse::Interaction`] of a [`PickList`].
|
||||
pub fn mouse_interaction(
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
) -> mouse::Interaction {
|
||||
let bounds = layout.bounds();
|
||||
let is_mouse_over = bounds.contains(cursor_position);
|
||||
|
||||
if is_mouse_over {
|
||||
mouse::Interaction::Pointer
|
||||
} else {
|
||||
mouse::Interaction::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the current overlay of a [`PickList`].
|
||||
pub fn overlay<'a, T, Message, Renderer>(
|
||||
layout: Layout<'_>,
|
||||
state: &'a mut State<T>,
|
||||
padding: Padding,
|
||||
text_size: Option<f32>,
|
||||
font: Renderer::Font,
|
||||
options: &'a [T],
|
||||
style: <Renderer::Theme as StyleSheet>::Style,
|
||||
) -> Option<overlay::Element<'a, Message, Renderer>>
|
||||
where
|
||||
T: Clone + ToString,
|
||||
Message: 'a,
|
||||
Renderer: text::Renderer + 'a,
|
||||
Renderer::Theme: StyleSheet
|
||||
+ scrollable::StyleSheet
|
||||
+ menu::StyleSheet
|
||||
+ container::StyleSheet,
|
||||
<Renderer::Theme as menu::StyleSheet>::Style:
|
||||
From<<Renderer::Theme as StyleSheet>::Style>,
|
||||
{
|
||||
if state.is_open {
|
||||
let bounds = layout.bounds();
|
||||
|
||||
let mut menu = Menu::new(
|
||||
&mut state.menu,
|
||||
options,
|
||||
&mut state.hovered_option,
|
||||
&mut state.last_selection,
|
||||
)
|
||||
.width(bounds.width)
|
||||
.padding(padding)
|
||||
.font(font)
|
||||
.style(style);
|
||||
|
||||
if let Some(text_size) = text_size {
|
||||
menu = menu.text_size(text_size);
|
||||
}
|
||||
|
||||
Some(menu.overlay(layout.position(), bounds.height))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Draws a [`PickList`].
|
||||
pub fn draw<'a, T, Renderer>(
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
padding: Padding,
|
||||
text_size: Option<f32>,
|
||||
font: Renderer::Font,
|
||||
placeholder: Option<&str>,
|
||||
selected: Option<&T>,
|
||||
handle: &Handle<Renderer::Font>,
|
||||
style: &<Renderer::Theme as StyleSheet>::Style,
|
||||
state: impl FnOnce() -> &'a State<T>,
|
||||
) where
|
||||
Renderer: text::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
T: ToString + 'a,
|
||||
{
|
||||
let bounds = layout.bounds();
|
||||
let is_mouse_over = bounds.contains(cursor_position);
|
||||
let is_selected = selected.is_some();
|
||||
|
||||
let style = if is_mouse_over {
|
||||
theme.hovered(style)
|
||||
} else {
|
||||
theme.active(style)
|
||||
};
|
||||
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds,
|
||||
border_color: style.border_color,
|
||||
border_width: style.border_width,
|
||||
border_radius: style.border_radius.into(),
|
||||
},
|
||||
style.background,
|
||||
);
|
||||
|
||||
let handle = match handle {
|
||||
Handle::Arrow { size } => {
|
||||
Some((Renderer::ICON_FONT, Renderer::ARROW_DOWN_ICON, *size))
|
||||
}
|
||||
Handle::Static(Icon {
|
||||
font,
|
||||
code_point,
|
||||
size,
|
||||
}) => Some((*font, *code_point, *size)),
|
||||
Handle::Dynamic { open, closed } => {
|
||||
if state().is_open {
|
||||
Some((open.font, open.code_point, open.size))
|
||||
} else {
|
||||
Some((closed.font, closed.code_point, closed.size))
|
||||
}
|
||||
}
|
||||
Handle::None => None,
|
||||
};
|
||||
|
||||
if let Some((font, code_point, size)) = handle {
|
||||
let size = size.unwrap_or_else(|| renderer.default_size());
|
||||
|
||||
renderer.fill_text(Text {
|
||||
content: &code_point.to_string(),
|
||||
size,
|
||||
font,
|
||||
color: style.handle_color,
|
||||
bounds: Rectangle {
|
||||
x: bounds.x + bounds.width - padding.horizontal(),
|
||||
y: bounds.center_y(),
|
||||
height: size * 1.2,
|
||||
..bounds
|
||||
},
|
||||
horizontal_alignment: alignment::Horizontal::Right,
|
||||
vertical_alignment: alignment::Vertical::Center,
|
||||
});
|
||||
}
|
||||
|
||||
let label = selected.map(ToString::to_string);
|
||||
|
||||
if let Some(label) = label.as_deref().or(placeholder) {
|
||||
let text_size = text_size.unwrap_or_else(|| renderer.default_size());
|
||||
|
||||
renderer.fill_text(Text {
|
||||
content: label,
|
||||
size: text_size,
|
||||
font,
|
||||
color: if is_selected {
|
||||
style.text_color
|
||||
} else {
|
||||
style.placeholder_color
|
||||
},
|
||||
bounds: Rectangle {
|
||||
x: bounds.x + padding.left,
|
||||
y: bounds.center_y(),
|
||||
width: bounds.width - padding.horizontal(),
|
||||
height: text_size * 1.2,
|
||||
},
|
||||
horizontal_alignment: alignment::Horizontal::Left,
|
||||
vertical_alignment: alignment::Vertical::Center,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,168 +0,0 @@
|
|||
//! Provide progress feedback to your users.
|
||||
use crate::layout;
|
||||
use crate::renderer;
|
||||
use crate::widget::Tree;
|
||||
use crate::{Color, Element, Layout, Length, Point, Rectangle, Size, Widget};
|
||||
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
pub use iced_style::progress_bar::{Appearance, StyleSheet};
|
||||
|
||||
/// A bar that displays progress.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # type ProgressBar = iced_native::widget::ProgressBar<iced_native::renderer::Null>;
|
||||
/// let value = 50.0;
|
||||
///
|
||||
/// ProgressBar::new(0.0..=100.0, value);
|
||||
/// ```
|
||||
///
|
||||
/// 
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct ProgressBar<Renderer>
|
||||
where
|
||||
Renderer: crate::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
range: RangeInclusive<f32>,
|
||||
value: f32,
|
||||
width: Length,
|
||||
height: Option<Length>,
|
||||
style: <Renderer::Theme as StyleSheet>::Style,
|
||||
}
|
||||
|
||||
impl<Renderer> ProgressBar<Renderer>
|
||||
where
|
||||
Renderer: crate::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
/// The default height of a [`ProgressBar`].
|
||||
pub const DEFAULT_HEIGHT: f32 = 30.0;
|
||||
|
||||
/// Creates a new [`ProgressBar`].
|
||||
///
|
||||
/// It expects:
|
||||
/// * an inclusive range of possible values
|
||||
/// * the current value of the [`ProgressBar`]
|
||||
pub fn new(range: RangeInclusive<f32>, value: f32) -> Self {
|
||||
ProgressBar {
|
||||
value: value.clamp(*range.start(), *range.end()),
|
||||
range,
|
||||
width: Length::Fill,
|
||||
height: None,
|
||||
style: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the width of the [`ProgressBar`].
|
||||
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||
self.width = width.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the height of the [`ProgressBar`].
|
||||
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
||||
self.height = Some(height.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the [`ProgressBar`].
|
||||
pub fn style(
|
||||
mut self,
|
||||
style: impl Into<<Renderer::Theme as StyleSheet>::Style>,
|
||||
) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<Message, Renderer> Widget<Message, Renderer> for ProgressBar<Renderer>
|
||||
where
|
||||
Renderer: crate::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
fn width(&self) -> Length {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
self.height.unwrap_or(Length::Fixed(Self::DEFAULT_HEIGHT))
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
_renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
let limits = limits
|
||||
.width(self.width)
|
||||
.height(self.height.unwrap_or(Length::Fixed(Self::DEFAULT_HEIGHT)));
|
||||
|
||||
let size = limits.resolve(Size::ZERO);
|
||||
|
||||
layout::Node::new(size)
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
_state: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
_style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
_cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
) {
|
||||
let bounds = layout.bounds();
|
||||
let (range_start, range_end) = self.range.clone().into_inner();
|
||||
|
||||
let active_progress_width = if range_start >= range_end {
|
||||
0.0
|
||||
} else {
|
||||
bounds.width * (self.value - range_start)
|
||||
/ (range_end - range_start)
|
||||
};
|
||||
|
||||
let style = theme.appearance(&self.style);
|
||||
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: Rectangle { ..bounds },
|
||||
border_radius: style.border_radius.into(),
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
},
|
||||
style.background,
|
||||
);
|
||||
|
||||
if active_progress_width > 0.0 {
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: Rectangle {
|
||||
width: active_progress_width,
|
||||
..bounds
|
||||
},
|
||||
border_radius: style.border_radius.into(),
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
},
|
||||
style.bar,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> From<ProgressBar<Renderer>>
|
||||
for Element<'a, Message, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Renderer: 'a + crate::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
fn from(
|
||||
progress_bar: ProgressBar<Renderer>,
|
||||
) -> Element<'a, Message, Renderer> {
|
||||
Element::new(progress_bar)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,299 +0,0 @@
|
|||
//! Create choices using radio buttons.
|
||||
use crate::alignment;
|
||||
use crate::event::{self, Event};
|
||||
use crate::layout;
|
||||
use crate::mouse;
|
||||
use crate::renderer;
|
||||
use crate::text;
|
||||
use crate::touch;
|
||||
use crate::widget::{self, Row, Text, Tree};
|
||||
use crate::{
|
||||
Alignment, Clipboard, Color, Element, Layout, Length, Pixels, Point,
|
||||
Rectangle, Shell, Widget,
|
||||
};
|
||||
|
||||
pub use iced_style::radio::{Appearance, StyleSheet};
|
||||
|
||||
/// A circular button representing a choice.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # type Radio<Message> =
|
||||
/// # iced_native::widget::Radio<Message, iced_native::renderer::Null>;
|
||||
/// #
|
||||
/// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
/// pub enum Choice {
|
||||
/// A,
|
||||
/// B,
|
||||
/// }
|
||||
///
|
||||
/// #[derive(Debug, Clone, Copy)]
|
||||
/// pub enum Message {
|
||||
/// RadioSelected(Choice),
|
||||
/// }
|
||||
///
|
||||
/// let selected_choice = Some(Choice::A);
|
||||
///
|
||||
/// Radio::new(Choice::A, "This is A", selected_choice, Message::RadioSelected);
|
||||
///
|
||||
/// Radio::new(Choice::B, "This is B", selected_choice, Message::RadioSelected);
|
||||
/// ```
|
||||
///
|
||||
/// 
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Radio<Message, Renderer>
|
||||
where
|
||||
Renderer: text::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
is_selected: bool,
|
||||
on_click: Message,
|
||||
label: String,
|
||||
width: Length,
|
||||
size: f32,
|
||||
spacing: f32,
|
||||
text_size: Option<f32>,
|
||||
font: Option<Renderer::Font>,
|
||||
style: <Renderer::Theme as StyleSheet>::Style,
|
||||
}
|
||||
|
||||
impl<Message, Renderer> Radio<Message, Renderer>
|
||||
where
|
||||
Message: Clone,
|
||||
Renderer: text::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
/// The default size of a [`Radio`] button.
|
||||
pub const DEFAULT_SIZE: f32 = 28.0;
|
||||
|
||||
/// The default spacing of a [`Radio`] button.
|
||||
pub const DEFAULT_SPACING: f32 = 15.0;
|
||||
|
||||
/// Creates a new [`Radio`] button.
|
||||
///
|
||||
/// It expects:
|
||||
/// * the value related to the [`Radio`] button
|
||||
/// * the label of the [`Radio`] button
|
||||
/// * the current selected value
|
||||
/// * a function that will be called when the [`Radio`] is selected. It
|
||||
/// receives the value of the radio and must produce a `Message`.
|
||||
pub fn new<F, V>(
|
||||
value: V,
|
||||
label: impl Into<String>,
|
||||
selected: Option<V>,
|
||||
f: F,
|
||||
) -> Self
|
||||
where
|
||||
V: Eq + Copy,
|
||||
F: FnOnce(V) -> Message,
|
||||
{
|
||||
Radio {
|
||||
is_selected: Some(value) == selected,
|
||||
on_click: f(value),
|
||||
label: label.into(),
|
||||
width: Length::Shrink,
|
||||
size: Self::DEFAULT_SIZE,
|
||||
spacing: Self::DEFAULT_SPACING, //15
|
||||
text_size: None,
|
||||
font: None,
|
||||
style: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the size of the [`Radio`] button.
|
||||
pub fn size(mut self, size: impl Into<Pixels>) -> Self {
|
||||
self.size = size.into().0;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the width of the [`Radio`] button.
|
||||
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||
self.width = width.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the spacing between the [`Radio`] button and the text.
|
||||
pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
|
||||
self.spacing = spacing.into().0;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the text size of the [`Radio`] button.
|
||||
pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self {
|
||||
self.text_size = Some(text_size.into().0);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the text font of the [`Radio`] button.
|
||||
pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
|
||||
self.font = Some(font.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the [`Radio`] button.
|
||||
pub fn style(
|
||||
mut self,
|
||||
style: impl Into<<Renderer::Theme as StyleSheet>::Style>,
|
||||
) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<Message, Renderer> Widget<Message, Renderer> for Radio<Message, Renderer>
|
||||
where
|
||||
Message: Clone,
|
||||
Renderer: text::Renderer,
|
||||
Renderer::Theme: StyleSheet + widget::text::StyleSheet,
|
||||
{
|
||||
fn width(&self) -> Length {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
Length::Shrink
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
Row::<(), Renderer>::new()
|
||||
.width(self.width)
|
||||
.spacing(self.spacing)
|
||||
.align_items(Alignment::Center)
|
||||
.push(Row::new().width(self.size).height(self.size))
|
||||
.push(Text::new(&self.label).width(self.width).size(
|
||||
self.text_size.unwrap_or_else(|| renderer.default_size()),
|
||||
))
|
||||
.layout(renderer, limits)
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
_state: &mut Tree,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
_renderer: &Renderer,
|
||||
_clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
match event {
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
|
||||
| Event::Touch(touch::Event::FingerPressed { .. }) => {
|
||||
if layout.bounds().contains(cursor_position) {
|
||||
shell.publish(self.on_click.clone());
|
||||
|
||||
return event::Status::Captured;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
event::Status::Ignored
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
_state: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
_renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
if layout.bounds().contains(cursor_position) {
|
||||
mouse::Interaction::Pointer
|
||||
} else {
|
||||
mouse::Interaction::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
_state: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
) {
|
||||
let bounds = layout.bounds();
|
||||
let is_mouse_over = bounds.contains(cursor_position);
|
||||
|
||||
let mut children = layout.children();
|
||||
|
||||
let custom_style = if is_mouse_over {
|
||||
theme.hovered(&self.style, self.is_selected)
|
||||
} else {
|
||||
theme.active(&self.style, self.is_selected)
|
||||
};
|
||||
|
||||
{
|
||||
let layout = children.next().unwrap();
|
||||
let bounds = layout.bounds();
|
||||
|
||||
let size = bounds.width;
|
||||
let dot_size = size / 2.0;
|
||||
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds,
|
||||
border_radius: (size / 2.0).into(),
|
||||
border_width: custom_style.border_width,
|
||||
border_color: custom_style.border_color,
|
||||
},
|
||||
custom_style.background,
|
||||
);
|
||||
|
||||
if self.is_selected {
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: Rectangle {
|
||||
x: bounds.x + dot_size / 2.0,
|
||||
y: bounds.y + dot_size / 2.0,
|
||||
width: bounds.width - dot_size,
|
||||
height: bounds.height - dot_size,
|
||||
},
|
||||
border_radius: (dot_size / 2.0).into(),
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
},
|
||||
custom_style.dot_color,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let label_layout = children.next().unwrap();
|
||||
|
||||
widget::text::draw(
|
||||
renderer,
|
||||
style,
|
||||
label_layout,
|
||||
&self.label,
|
||||
self.text_size,
|
||||
self.font,
|
||||
widget::text::Appearance {
|
||||
color: custom_style.text_color,
|
||||
},
|
||||
alignment::Horizontal::Left,
|
||||
alignment::Vertical::Center,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> From<Radio<Message, Renderer>>
|
||||
for Element<'a, Message, Renderer>
|
||||
where
|
||||
Message: 'a + Clone,
|
||||
Renderer: 'a + text::Renderer,
|
||||
Renderer::Theme: StyleSheet + widget::text::StyleSheet,
|
||||
{
|
||||
fn from(radio: Radio<Message, Renderer>) -> Element<'a, Message, Renderer> {
|
||||
Element::new(radio)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,253 +0,0 @@
|
|||
//! Distribute content horizontally.
|
||||
use crate::event::{self, Event};
|
||||
use crate::layout::{self, Layout};
|
||||
use crate::mouse;
|
||||
use crate::overlay;
|
||||
use crate::renderer;
|
||||
use crate::widget::{Operation, Tree};
|
||||
use crate::{
|
||||
Alignment, Clipboard, Element, Length, Padding, Pixels, Point, Rectangle,
|
||||
Shell, Widget,
|
||||
};
|
||||
|
||||
/// A container that distributes its contents horizontally.
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Row<'a, Message, Renderer> {
|
||||
spacing: f32,
|
||||
padding: Padding,
|
||||
width: Length,
|
||||
height: Length,
|
||||
align_items: Alignment,
|
||||
children: Vec<Element<'a, Message, Renderer>>,
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Row<'a, Message, Renderer> {
|
||||
/// Creates an empty [`Row`].
|
||||
pub fn new() -> Self {
|
||||
Self::with_children(Vec::new())
|
||||
}
|
||||
|
||||
/// Creates a [`Row`] with the given elements.
|
||||
pub fn with_children(
|
||||
children: Vec<Element<'a, Message, Renderer>>,
|
||||
) -> Self {
|
||||
Row {
|
||||
spacing: 0.0,
|
||||
padding: Padding::ZERO,
|
||||
width: Length::Shrink,
|
||||
height: Length::Shrink,
|
||||
align_items: Alignment::Start,
|
||||
children,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the horizontal spacing _between_ elements.
|
||||
///
|
||||
/// Custom margins per element do not exist in iced. You should use this
|
||||
/// method instead! While less flexible, it helps you keep spacing between
|
||||
/// elements consistent.
|
||||
pub fn spacing(mut self, amount: impl Into<Pixels>) -> Self {
|
||||
self.spacing = amount.into().0;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`Padding`] of the [`Row`].
|
||||
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
|
||||
self.padding = padding.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the width of the [`Row`].
|
||||
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||
self.width = width.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the height of the [`Row`].
|
||||
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
||||
self.height = height.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the vertical alignment of the contents of the [`Row`] .
|
||||
pub fn align_items(mut self, align: Alignment) -> Self {
|
||||
self.align_items = align;
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds an [`Element`] to the [`Row`].
|
||||
pub fn push(
|
||||
mut self,
|
||||
child: impl Into<Element<'a, Message, Renderer>>,
|
||||
) -> Self {
|
||||
self.children.push(child.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Default for Row<'a, Message, Renderer> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Widget<Message, Renderer>
|
||||
for Row<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: crate::Renderer,
|
||||
{
|
||||
fn children(&self) -> Vec<Tree> {
|
||||
self.children.iter().map(Tree::new).collect()
|
||||
}
|
||||
|
||||
fn diff(&self, tree: &mut Tree) {
|
||||
tree.diff_children(&self.children)
|
||||
}
|
||||
|
||||
fn width(&self) -> Length {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
let limits = limits.width(self.width).height(self.height);
|
||||
|
||||
layout::flex::resolve(
|
||||
layout::flex::Axis::Horizontal,
|
||||
renderer,
|
||||
&limits,
|
||||
self.padding,
|
||||
self.spacing,
|
||||
self.align_items,
|
||||
&self.children,
|
||||
)
|
||||
}
|
||||
|
||||
fn operate(
|
||||
&self,
|
||||
tree: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn Operation<Message>,
|
||||
) {
|
||||
operation.container(None, &mut |operation| {
|
||||
self.children
|
||||
.iter()
|
||||
.zip(&mut tree.children)
|
||||
.zip(layout.children())
|
||||
.for_each(|((child, state), layout)| {
|
||||
child
|
||||
.as_widget()
|
||||
.operate(state, layout, renderer, operation);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
self.children
|
||||
.iter_mut()
|
||||
.zip(&mut tree.children)
|
||||
.zip(layout.children())
|
||||
.map(|((child, state), layout)| {
|
||||
child.as_widget_mut().on_event(
|
||||
state,
|
||||
event.clone(),
|
||||
layout,
|
||||
cursor_position,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
)
|
||||
})
|
||||
.fold(event::Status::Ignored, event::Status::merge)
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
self.children
|
||||
.iter()
|
||||
.zip(&tree.children)
|
||||
.zip(layout.children())
|
||||
.map(|((child, state), layout)| {
|
||||
child.as_widget().mouse_interaction(
|
||||
state,
|
||||
layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
renderer,
|
||||
)
|
||||
})
|
||||
.max()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
for ((child, state), layout) in self
|
||||
.children
|
||||
.iter()
|
||||
.zip(&tree.children)
|
||||
.zip(layout.children())
|
||||
{
|
||||
child.as_widget().draw(
|
||||
state,
|
||||
renderer,
|
||||
theme,
|
||||
style,
|
||||
layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn overlay<'b>(
|
||||
&'b mut self,
|
||||
tree: &'b mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
) -> Option<overlay::Element<'b, Message, Renderer>> {
|
||||
overlay::from_children(&mut self.children, tree, layout, renderer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> From<Row<'a, Message, Renderer>>
|
||||
for Element<'a, Message, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Renderer: crate::Renderer + 'a,
|
||||
{
|
||||
fn from(row: Row<'a, Message, Renderer>) -> Self {
|
||||
Self::new(row)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,147 +0,0 @@
|
|||
//! Display a horizontal or vertical rule for dividing content.
|
||||
use crate::layout;
|
||||
use crate::renderer;
|
||||
use crate::widget::Tree;
|
||||
use crate::{
|
||||
Color, Element, Layout, Length, Pixels, Point, Rectangle, Size, Widget,
|
||||
};
|
||||
|
||||
pub use iced_style::rule::{Appearance, FillMode, StyleSheet};
|
||||
|
||||
/// Display a horizontal or vertical rule for dividing content.
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Rule<Renderer>
|
||||
where
|
||||
Renderer: crate::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
width: Length,
|
||||
height: Length,
|
||||
is_horizontal: bool,
|
||||
style: <Renderer::Theme as StyleSheet>::Style,
|
||||
}
|
||||
|
||||
impl<Renderer> Rule<Renderer>
|
||||
where
|
||||
Renderer: crate::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
/// Creates a horizontal [`Rule`] with the given height.
|
||||
pub fn horizontal(height: impl Into<Pixels>) -> Self {
|
||||
Rule {
|
||||
width: Length::Fill,
|
||||
height: Length::Fixed(height.into().0),
|
||||
is_horizontal: true,
|
||||
style: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a vertical [`Rule`] with the given width.
|
||||
pub fn vertical(width: impl Into<Pixels>) -> Self {
|
||||
Rule {
|
||||
width: Length::Fixed(width.into().0),
|
||||
height: Length::Fill,
|
||||
is_horizontal: false,
|
||||
style: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the style of the [`Rule`].
|
||||
pub fn style(
|
||||
mut self,
|
||||
style: impl Into<<Renderer::Theme as StyleSheet>::Style>,
|
||||
) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<Message, Renderer> Widget<Message, Renderer> for Rule<Renderer>
|
||||
where
|
||||
Renderer: crate::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
fn width(&self) -> Length {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
_renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
let limits = limits.width(self.width).height(self.height);
|
||||
|
||||
layout::Node::new(limits.resolve(Size::ZERO))
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
_state: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
_style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
_cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
) {
|
||||
let bounds = layout.bounds();
|
||||
let style = theme.appearance(&self.style);
|
||||
|
||||
let bounds = if self.is_horizontal {
|
||||
let line_y = (bounds.y + (bounds.height / 2.0)
|
||||
- (style.width as f32 / 2.0))
|
||||
.round();
|
||||
|
||||
let (offset, line_width) = style.fill_mode.fill(bounds.width);
|
||||
let line_x = bounds.x + offset;
|
||||
|
||||
Rectangle {
|
||||
x: line_x,
|
||||
y: line_y,
|
||||
width: line_width,
|
||||
height: style.width as f32,
|
||||
}
|
||||
} else {
|
||||
let line_x = (bounds.x + (bounds.width / 2.0)
|
||||
- (style.width as f32 / 2.0))
|
||||
.round();
|
||||
|
||||
let (offset, line_height) = style.fill_mode.fill(bounds.height);
|
||||
let line_y = bounds.y + offset;
|
||||
|
||||
Rectangle {
|
||||
x: line_x,
|
||||
y: line_y,
|
||||
width: style.width as f32,
|
||||
height: line_height,
|
||||
}
|
||||
};
|
||||
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds,
|
||||
border_radius: style.radius.into(),
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
},
|
||||
style.color,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> From<Rule<Renderer>>
|
||||
for Element<'a, Message, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Renderer: 'a + crate::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
fn from(rule: Rule<Renderer>) -> Element<'a, Message, Renderer> {
|
||||
Element::new(rule)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,473 +0,0 @@
|
|||
//! Display an interactive selector of a single value from a range of values.
|
||||
//!
|
||||
//! A [`Slider`] has some local [`State`].
|
||||
use crate::event::{self, Event};
|
||||
use crate::layout;
|
||||
use crate::mouse;
|
||||
use crate::renderer;
|
||||
use crate::touch;
|
||||
use crate::widget::tree::{self, Tree};
|
||||
use crate::{
|
||||
Background, Clipboard, Color, Element, Layout, Length, Pixels, Point,
|
||||
Rectangle, Shell, Size, Widget,
|
||||
};
|
||||
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
pub use iced_style::slider::{Appearance, Handle, HandleShape, StyleSheet};
|
||||
|
||||
/// An horizontal bar and a handle that selects a single value from a range of
|
||||
/// values.
|
||||
///
|
||||
/// A [`Slider`] will try to fill the horizontal space of its container.
|
||||
///
|
||||
/// The [`Slider`] range of numeric values is generic and its step size defaults
|
||||
/// to 1 unit.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use iced_native::widget::slider;
|
||||
/// # use iced_native::renderer::Null;
|
||||
/// #
|
||||
/// # type Slider<'a, T, Message> = slider::Slider<'a, T, Message, Null>;
|
||||
/// #
|
||||
/// #[derive(Clone)]
|
||||
/// pub enum Message {
|
||||
/// SliderChanged(f32),
|
||||
/// }
|
||||
///
|
||||
/// let value = 50.0;
|
||||
///
|
||||
/// Slider::new(0.0..=100.0, value, Message::SliderChanged);
|
||||
/// ```
|
||||
///
|
||||
/// 
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Slider<'a, T, Message, Renderer>
|
||||
where
|
||||
Renderer: crate::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
range: RangeInclusive<T>,
|
||||
step: T,
|
||||
value: T,
|
||||
on_change: Box<dyn Fn(T) -> Message + 'a>,
|
||||
on_release: Option<Message>,
|
||||
width: Length,
|
||||
height: f32,
|
||||
style: <Renderer::Theme as StyleSheet>::Style,
|
||||
}
|
||||
|
||||
impl<'a, T, Message, Renderer> Slider<'a, T, Message, Renderer>
|
||||
where
|
||||
T: Copy + From<u8> + std::cmp::PartialOrd,
|
||||
Message: Clone,
|
||||
Renderer: crate::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
/// The default height of a [`Slider`].
|
||||
pub const DEFAULT_HEIGHT: f32 = 22.0;
|
||||
|
||||
/// Creates a new [`Slider`].
|
||||
///
|
||||
/// It expects:
|
||||
/// * an inclusive range of possible values
|
||||
/// * the current value of the [`Slider`]
|
||||
/// * a function that will be called when the [`Slider`] is dragged.
|
||||
/// It receives the new value of the [`Slider`] and must produce a
|
||||
/// `Message`.
|
||||
pub fn new<F>(range: RangeInclusive<T>, value: T, on_change: F) -> Self
|
||||
where
|
||||
F: 'a + Fn(T) -> Message,
|
||||
{
|
||||
let value = if value >= *range.start() {
|
||||
value
|
||||
} else {
|
||||
*range.start()
|
||||
};
|
||||
|
||||
let value = if value <= *range.end() {
|
||||
value
|
||||
} else {
|
||||
*range.end()
|
||||
};
|
||||
|
||||
Slider {
|
||||
value,
|
||||
range,
|
||||
step: T::from(1),
|
||||
on_change: Box::new(on_change),
|
||||
on_release: None,
|
||||
width: Length::Fill,
|
||||
height: Self::DEFAULT_HEIGHT,
|
||||
style: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the release message of the [`Slider`].
|
||||
/// This is called when the mouse is released from the slider.
|
||||
///
|
||||
/// Typically, the user's interaction with the slider is finished when this message is produced.
|
||||
/// This is useful if you need to spawn a long-running task from the slider's result, where
|
||||
/// the default on_change message could create too many events.
|
||||
pub fn on_release(mut self, on_release: Message) -> Self {
|
||||
self.on_release = Some(on_release);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the width of the [`Slider`].
|
||||
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||
self.width = width.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the height of the [`Slider`].
|
||||
pub fn height(mut self, height: impl Into<Pixels>) -> Self {
|
||||
self.height = height.into().0;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the [`Slider`].
|
||||
pub fn style(
|
||||
mut self,
|
||||
style: impl Into<<Renderer::Theme as StyleSheet>::Style>,
|
||||
) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the step size of the [`Slider`].
|
||||
pub fn step(mut self, step: T) -> Self {
|
||||
self.step = step;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T, Message, Renderer> Widget<Message, Renderer>
|
||||
for Slider<'a, T, Message, Renderer>
|
||||
where
|
||||
T: Copy + Into<f64> + num_traits::FromPrimitive,
|
||||
Message: Clone,
|
||||
Renderer: crate::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
fn tag(&self) -> tree::Tag {
|
||||
tree::Tag::of::<State>()
|
||||
}
|
||||
|
||||
fn state(&self) -> tree::State {
|
||||
tree::State::new(State::new())
|
||||
}
|
||||
|
||||
fn width(&self) -> Length {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
Length::Shrink
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
_renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
let limits = limits.width(self.width).height(self.height);
|
||||
let size = limits.resolve(Size::ZERO);
|
||||
|
||||
layout::Node::new(size)
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
_renderer: &Renderer,
|
||||
_clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
update(
|
||||
event,
|
||||
layout,
|
||||
cursor_position,
|
||||
shell,
|
||||
tree.state.downcast_mut::<State>(),
|
||||
&mut self.value,
|
||||
&self.range,
|
||||
self.step,
|
||||
self.on_change.as_ref(),
|
||||
&self.on_release,
|
||||
)
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
_style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
) {
|
||||
draw(
|
||||
renderer,
|
||||
layout,
|
||||
cursor_position,
|
||||
tree.state.downcast_ref::<State>(),
|
||||
self.value,
|
||||
&self.range,
|
||||
theme,
|
||||
&self.style,
|
||||
)
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
_renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
mouse_interaction(
|
||||
layout,
|
||||
cursor_position,
|
||||
tree.state.downcast_ref::<State>(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T, Message, Renderer> From<Slider<'a, T, Message, Renderer>>
|
||||
for Element<'a, Message, Renderer>
|
||||
where
|
||||
T: 'a + Copy + Into<f64> + num_traits::FromPrimitive,
|
||||
Message: 'a + Clone,
|
||||
Renderer: 'a + crate::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
fn from(
|
||||
slider: Slider<'a, T, Message, Renderer>,
|
||||
) -> Element<'a, Message, Renderer> {
|
||||
Element::new(slider)
|
||||
}
|
||||
}
|
||||
|
||||
/// Processes an [`Event`] and updates the [`State`] of a [`Slider`]
|
||||
/// accordingly.
|
||||
pub fn update<Message, T>(
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
state: &mut State,
|
||||
value: &mut T,
|
||||
range: &RangeInclusive<T>,
|
||||
step: T,
|
||||
on_change: &dyn Fn(T) -> Message,
|
||||
on_release: &Option<Message>,
|
||||
) -> event::Status
|
||||
where
|
||||
T: Copy + Into<f64> + num_traits::FromPrimitive,
|
||||
Message: Clone,
|
||||
{
|
||||
let is_dragging = state.is_dragging;
|
||||
|
||||
let mut change = || {
|
||||
let bounds = layout.bounds();
|
||||
let new_value = if cursor_position.x <= bounds.x {
|
||||
*range.start()
|
||||
} else if cursor_position.x >= bounds.x + bounds.width {
|
||||
*range.end()
|
||||
} else {
|
||||
let step = step.into();
|
||||
let start = (*range.start()).into();
|
||||
let end = (*range.end()).into();
|
||||
|
||||
let percent = f64::from(cursor_position.x - bounds.x)
|
||||
/ f64::from(bounds.width);
|
||||
|
||||
let steps = (percent * (end - start) / step).round();
|
||||
let value = steps * step + start;
|
||||
|
||||
if let Some(value) = T::from_f64(value) {
|
||||
value
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if ((*value).into() - new_value.into()).abs() > f64::EPSILON {
|
||||
shell.publish((on_change)(new_value));
|
||||
|
||||
*value = new_value;
|
||||
}
|
||||
};
|
||||
|
||||
match event {
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
|
||||
| Event::Touch(touch::Event::FingerPressed { .. }) => {
|
||||
if layout.bounds().contains(cursor_position) {
|
||||
change();
|
||||
state.is_dragging = true;
|
||||
|
||||
return event::Status::Captured;
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
|
||||
| Event::Touch(touch::Event::FingerLifted { .. })
|
||||
| Event::Touch(touch::Event::FingerLost { .. }) => {
|
||||
if is_dragging {
|
||||
if let Some(on_release) = on_release.clone() {
|
||||
shell.publish(on_release);
|
||||
}
|
||||
state.is_dragging = false;
|
||||
|
||||
return event::Status::Captured;
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse::Event::CursorMoved { .. })
|
||||
| Event::Touch(touch::Event::FingerMoved { .. }) => {
|
||||
if is_dragging {
|
||||
change();
|
||||
|
||||
return event::Status::Captured;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
event::Status::Ignored
|
||||
}
|
||||
|
||||
/// Draws a [`Slider`].
|
||||
pub fn draw<T, R>(
|
||||
renderer: &mut R,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
state: &State,
|
||||
value: T,
|
||||
range: &RangeInclusive<T>,
|
||||
style_sheet: &dyn StyleSheet<Style = <R::Theme as StyleSheet>::Style>,
|
||||
style: &<R::Theme as StyleSheet>::Style,
|
||||
) where
|
||||
T: Into<f64> + Copy,
|
||||
R: crate::Renderer,
|
||||
R::Theme: StyleSheet,
|
||||
{
|
||||
let bounds = layout.bounds();
|
||||
let is_mouse_over = bounds.contains(cursor_position);
|
||||
|
||||
let style = if state.is_dragging {
|
||||
style_sheet.dragging(style)
|
||||
} else if is_mouse_over {
|
||||
style_sheet.hovered(style)
|
||||
} else {
|
||||
style_sheet.active(style)
|
||||
};
|
||||
|
||||
let rail_y = bounds.y + (bounds.height / 2.0).round();
|
||||
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: Rectangle {
|
||||
x: bounds.x,
|
||||
y: rail_y - 1.0,
|
||||
width: bounds.width,
|
||||
height: 2.0,
|
||||
},
|
||||
border_radius: 0.0.into(),
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
},
|
||||
style.rail_colors.0,
|
||||
);
|
||||
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: Rectangle {
|
||||
x: bounds.x,
|
||||
y: rail_y + 1.0,
|
||||
width: bounds.width,
|
||||
height: 2.0,
|
||||
},
|
||||
border_radius: 0.0.into(),
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
},
|
||||
Background::Color(style.rail_colors.1),
|
||||
);
|
||||
|
||||
let (handle_width, handle_height, handle_border_radius) = match style
|
||||
.handle
|
||||
.shape
|
||||
{
|
||||
HandleShape::Circle { radius } => (radius * 2.0, radius * 2.0, radius),
|
||||
HandleShape::Rectangle {
|
||||
width,
|
||||
border_radius,
|
||||
} => (f32::from(width), bounds.height, border_radius),
|
||||
};
|
||||
|
||||
let value = value.into() as f32;
|
||||
let (range_start, range_end) = {
|
||||
let (start, end) = range.clone().into_inner();
|
||||
|
||||
(start.into() as f32, end.into() as f32)
|
||||
};
|
||||
|
||||
let handle_offset = if range_start >= range_end {
|
||||
0.0
|
||||
} else {
|
||||
(bounds.width - handle_width) * (value - range_start)
|
||||
/ (range_end - range_start)
|
||||
};
|
||||
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: Rectangle {
|
||||
x: bounds.x + handle_offset.round(),
|
||||
y: rail_y - handle_height / 2.0,
|
||||
width: handle_width,
|
||||
height: handle_height,
|
||||
},
|
||||
border_radius: handle_border_radius.into(),
|
||||
border_width: style.handle.border_width,
|
||||
border_color: style.handle.border_color,
|
||||
},
|
||||
style.handle.color,
|
||||
);
|
||||
}
|
||||
|
||||
/// Computes the current [`mouse::Interaction`] of a [`Slider`].
|
||||
pub fn mouse_interaction(
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
state: &State,
|
||||
) -> mouse::Interaction {
|
||||
let bounds = layout.bounds();
|
||||
let is_mouse_over = bounds.contains(cursor_position);
|
||||
|
||||
if state.is_dragging {
|
||||
mouse::Interaction::Grabbing
|
||||
} else if is_mouse_over {
|
||||
mouse::Interaction::Grab
|
||||
} else {
|
||||
mouse::Interaction::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// The local state of a [`Slider`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct State {
|
||||
is_dragging: bool,
|
||||
}
|
||||
|
||||
impl State {
|
||||
/// Creates a new [`State`].
|
||||
pub fn new() -> State {
|
||||
State::default()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
//! Distribute content vertically.
|
||||
use crate::layout;
|
||||
use crate::renderer;
|
||||
use crate::widget::Tree;
|
||||
use crate::{Element, Layout, Length, Point, Rectangle, Size, Widget};
|
||||
|
||||
/// An amount of empty space.
|
||||
///
|
||||
/// It can be useful if you want to fill some space with nothing.
|
||||
#[derive(Debug)]
|
||||
pub struct Space {
|
||||
width: Length,
|
||||
height: Length,
|
||||
}
|
||||
|
||||
impl Space {
|
||||
/// Creates an amount of empty [`Space`] with the given width and height.
|
||||
pub fn new(width: impl Into<Length>, height: impl Into<Length>) -> Self {
|
||||
Space {
|
||||
width: width.into(),
|
||||
height: height.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an amount of horizontal [`Space`].
|
||||
pub fn with_width(width: impl Into<Length>) -> Self {
|
||||
Space {
|
||||
width: width.into(),
|
||||
height: Length::Shrink,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an amount of vertical [`Space`].
|
||||
pub fn with_height(height: impl Into<Length>) -> Self {
|
||||
Space {
|
||||
width: Length::Shrink,
|
||||
height: height.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Message, Renderer> Widget<Message, Renderer> for Space
|
||||
where
|
||||
Renderer: crate::Renderer,
|
||||
{
|
||||
fn width(&self) -> Length {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
_renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
let limits = limits.width(self.width).height(self.height);
|
||||
|
||||
layout::Node::new(limits.resolve(Size::ZERO))
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
_state: &Tree,
|
||||
_renderer: &mut Renderer,
|
||||
_theme: &Renderer::Theme,
|
||||
_style: &renderer::Style,
|
||||
_layout: Layout<'_>,
|
||||
_cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> From<Space> for Element<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: crate::Renderer,
|
||||
Message: 'a,
|
||||
{
|
||||
fn from(space: Space) -> Element<'a, Message, Renderer> {
|
||||
Element::new(space)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,195 +0,0 @@
|
|||
//! Display vector graphics in your application.
|
||||
use crate::layout;
|
||||
use crate::renderer;
|
||||
use crate::svg;
|
||||
use crate::widget::Tree;
|
||||
use crate::{
|
||||
ContentFit, Element, Layout, Length, Point, Rectangle, Size, Vector, Widget,
|
||||
};
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub use iced_style::svg::{Appearance, StyleSheet};
|
||||
pub use svg::Handle;
|
||||
|
||||
/// A vector graphics image.
|
||||
///
|
||||
/// An [`Svg`] image resizes smoothly without losing any quality.
|
||||
///
|
||||
/// [`Svg`] images can have a considerable rendering cost when resized,
|
||||
/// specially when they are complex.
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Svg<Renderer>
|
||||
where
|
||||
Renderer: svg::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
handle: Handle,
|
||||
width: Length,
|
||||
height: Length,
|
||||
content_fit: ContentFit,
|
||||
style: <Renderer::Theme as StyleSheet>::Style,
|
||||
}
|
||||
|
||||
impl<Renderer> Svg<Renderer>
|
||||
where
|
||||
Renderer: svg::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
/// Creates a new [`Svg`] from the given [`Handle`].
|
||||
pub fn new(handle: impl Into<Handle>) -> Self {
|
||||
Svg {
|
||||
handle: handle.into(),
|
||||
width: Length::Fill,
|
||||
height: Length::Shrink,
|
||||
content_fit: ContentFit::Contain,
|
||||
style: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new [`Svg`] that will display the contents of the file at the
|
||||
/// provided path.
|
||||
#[must_use]
|
||||
pub fn from_path(path: impl Into<PathBuf>) -> Self {
|
||||
Self::new(Handle::from_path(path))
|
||||
}
|
||||
|
||||
/// Sets the width of the [`Svg`].
|
||||
#[must_use]
|
||||
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||
self.width = width.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the height of the [`Svg`].
|
||||
#[must_use]
|
||||
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
||||
self.height = height.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`ContentFit`] of the [`Svg`].
|
||||
///
|
||||
/// Defaults to [`ContentFit::Contain`]
|
||||
#[must_use]
|
||||
pub fn content_fit(self, content_fit: ContentFit) -> Self {
|
||||
Self {
|
||||
content_fit,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the style variant of this [`Svg`].
|
||||
#[must_use]
|
||||
pub fn style(
|
||||
mut self,
|
||||
style: <Renderer::Theme as StyleSheet>::Style,
|
||||
) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<Message, Renderer> Widget<Message, Renderer> for Svg<Renderer>
|
||||
where
|
||||
Renderer: svg::Renderer,
|
||||
Renderer::Theme: iced_style::svg::StyleSheet,
|
||||
{
|
||||
fn width(&self) -> Length {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
// The raw w/h of the underlying image
|
||||
let Size { width, height } = renderer.dimensions(&self.handle);
|
||||
let image_size = Size::new(width as f32, height as f32);
|
||||
|
||||
// The size to be available to the widget prior to `Shrink`ing
|
||||
let raw_size = limits
|
||||
.width(self.width)
|
||||
.height(self.height)
|
||||
.resolve(image_size);
|
||||
|
||||
// The uncropped size of the image when fit to the bounds above
|
||||
let full_size = self.content_fit.fit(image_size, raw_size);
|
||||
|
||||
// Shrink the widget to fit the resized image, if requested
|
||||
let final_size = Size {
|
||||
width: match self.width {
|
||||
Length::Shrink => f32::min(raw_size.width, full_size.width),
|
||||
_ => raw_size.width,
|
||||
},
|
||||
height: match self.height {
|
||||
Length::Shrink => f32::min(raw_size.height, full_size.height),
|
||||
_ => raw_size.height,
|
||||
},
|
||||
};
|
||||
|
||||
layout::Node::new(final_size)
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
_state: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
_style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
_cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
) {
|
||||
let Size { width, height } = renderer.dimensions(&self.handle);
|
||||
let image_size = Size::new(width as f32, height as f32);
|
||||
|
||||
let bounds = layout.bounds();
|
||||
let adjusted_fit = self.content_fit.fit(image_size, bounds.size());
|
||||
|
||||
let render = |renderer: &mut Renderer| {
|
||||
let offset = Vector::new(
|
||||
(bounds.width - adjusted_fit.width).max(0.0) / 2.0,
|
||||
(bounds.height - adjusted_fit.height).max(0.0) / 2.0,
|
||||
);
|
||||
|
||||
let drawing_bounds = Rectangle {
|
||||
width: adjusted_fit.width,
|
||||
height: adjusted_fit.height,
|
||||
..bounds
|
||||
};
|
||||
|
||||
let appearance = theme.appearance(&self.style);
|
||||
|
||||
renderer.draw(
|
||||
self.handle.clone(),
|
||||
appearance.color,
|
||||
drawing_bounds + offset,
|
||||
);
|
||||
};
|
||||
|
||||
if adjusted_fit.width > bounds.width
|
||||
|| adjusted_fit.height > bounds.height
|
||||
{
|
||||
renderer.with_layer(bounds, render);
|
||||
} else {
|
||||
render(renderer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> From<Svg<Renderer>>
|
||||
for Element<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: svg::Renderer + 'a,
|
||||
Renderer::Theme: iced_style::svg::StyleSheet,
|
||||
{
|
||||
fn from(icon: Svg<Renderer>) -> Element<'a, Message, Renderer> {
|
||||
Element::new(icon)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,263 +0,0 @@
|
|||
//! Write some text for your users to read.
|
||||
use crate::alignment;
|
||||
use crate::layout;
|
||||
use crate::renderer;
|
||||
use crate::text;
|
||||
use crate::widget::Tree;
|
||||
use crate::{Element, Layout, Length, Pixels, Point, Rectangle, Size, Widget};
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
pub use iced_style::text::{Appearance, StyleSheet};
|
||||
|
||||
/// A paragraph of text.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use iced_native::Color;
|
||||
/// #
|
||||
/// # type Text<'a> = iced_native::widget::Text<'a, iced_native::renderer::Null>;
|
||||
/// #
|
||||
/// Text::new("I <3 iced!")
|
||||
/// .size(40)
|
||||
/// .style(Color::from([0.0, 0.0, 1.0]));
|
||||
/// ```
|
||||
///
|
||||
/// 
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Text<'a, Renderer>
|
||||
where
|
||||
Renderer: text::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
content: Cow<'a, str>,
|
||||
size: Option<f32>,
|
||||
width: Length,
|
||||
height: Length,
|
||||
horizontal_alignment: alignment::Horizontal,
|
||||
vertical_alignment: alignment::Vertical,
|
||||
font: Option<Renderer::Font>,
|
||||
style: <Renderer::Theme as StyleSheet>::Style,
|
||||
}
|
||||
|
||||
impl<'a, Renderer> Text<'a, Renderer>
|
||||
where
|
||||
Renderer: text::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
/// Create a new fragment of [`Text`] with the given contents.
|
||||
pub fn new(content: impl Into<Cow<'a, str>>) -> Self {
|
||||
Text {
|
||||
content: content.into(),
|
||||
size: None,
|
||||
font: None,
|
||||
width: Length::Shrink,
|
||||
height: Length::Shrink,
|
||||
horizontal_alignment: alignment::Horizontal::Left,
|
||||
vertical_alignment: alignment::Vertical::Top,
|
||||
style: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the size of the [`Text`].
|
||||
pub fn size(mut self, size: impl Into<Pixels>) -> Self {
|
||||
self.size = Some(size.into().0);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`Font`] of the [`Text`].
|
||||
///
|
||||
/// [`Font`]: crate::text::Renderer::Font
|
||||
pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
|
||||
self.font = Some(font.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the [`Text`].
|
||||
pub fn style(
|
||||
mut self,
|
||||
style: impl Into<<Renderer::Theme as StyleSheet>::Style>,
|
||||
) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the width of the [`Text`] boundaries.
|
||||
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||
self.width = width.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the height of the [`Text`] boundaries.
|
||||
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
||||
self.height = height.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`alignment::Horizontal`] of the [`Text`].
|
||||
pub fn horizontal_alignment(
|
||||
mut self,
|
||||
alignment: alignment::Horizontal,
|
||||
) -> Self {
|
||||
self.horizontal_alignment = alignment;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`alignment::Vertical`] of the [`Text`].
|
||||
pub fn vertical_alignment(
|
||||
mut self,
|
||||
alignment: alignment::Vertical,
|
||||
) -> Self {
|
||||
self.vertical_alignment = alignment;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Widget<Message, Renderer> for Text<'a, Renderer>
|
||||
where
|
||||
Renderer: text::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
fn width(&self) -> Length {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
let limits = limits.width(self.width).height(self.height);
|
||||
|
||||
let size = self.size.unwrap_or_else(|| renderer.default_size());
|
||||
|
||||
let bounds = limits.max();
|
||||
|
||||
let (width, height) = renderer.measure(
|
||||
&self.content,
|
||||
size,
|
||||
self.font.unwrap_or_else(|| renderer.default_font()),
|
||||
bounds,
|
||||
);
|
||||
|
||||
let size = limits.resolve(Size::new(width, height));
|
||||
|
||||
layout::Node::new(size)
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
_state: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
_cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
) {
|
||||
draw(
|
||||
renderer,
|
||||
style,
|
||||
layout,
|
||||
&self.content,
|
||||
self.size,
|
||||
self.font,
|
||||
theme.appearance(self.style),
|
||||
self.horizontal_alignment,
|
||||
self.vertical_alignment,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Draws text using the same logic as the [`Text`] widget.
|
||||
///
|
||||
/// Specifically:
|
||||
///
|
||||
/// * If no `size` is provided, the default text size of the `Renderer` will be
|
||||
/// used.
|
||||
/// * If no `color` is provided, the [`renderer::Style::text_color`] will be
|
||||
/// used.
|
||||
/// * The alignment attributes do not affect the position of the bounds of the
|
||||
/// [`Layout`].
|
||||
pub fn draw<Renderer>(
|
||||
renderer: &mut Renderer,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
content: &str,
|
||||
size: Option<f32>,
|
||||
font: Option<Renderer::Font>,
|
||||
appearance: Appearance,
|
||||
horizontal_alignment: alignment::Horizontal,
|
||||
vertical_alignment: alignment::Vertical,
|
||||
) where
|
||||
Renderer: text::Renderer,
|
||||
{
|
||||
let bounds = layout.bounds();
|
||||
|
||||
let x = match horizontal_alignment {
|
||||
alignment::Horizontal::Left => bounds.x,
|
||||
alignment::Horizontal::Center => bounds.center_x(),
|
||||
alignment::Horizontal::Right => bounds.x + bounds.width,
|
||||
};
|
||||
|
||||
let y = match vertical_alignment {
|
||||
alignment::Vertical::Top => bounds.y,
|
||||
alignment::Vertical::Center => bounds.center_y(),
|
||||
alignment::Vertical::Bottom => bounds.y + bounds.height,
|
||||
};
|
||||
|
||||
renderer.fill_text(crate::text::Text {
|
||||
content,
|
||||
size: size.unwrap_or_else(|| renderer.default_size()),
|
||||
bounds: Rectangle { x, y, ..bounds },
|
||||
color: appearance.color.unwrap_or(style.text_color),
|
||||
font: font.unwrap_or_else(|| renderer.default_font()),
|
||||
horizontal_alignment,
|
||||
vertical_alignment,
|
||||
});
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> From<Text<'a, Renderer>>
|
||||
for Element<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: text::Renderer + 'a,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
fn from(text: Text<'a, Renderer>) -> Element<'a, Message, Renderer> {
|
||||
Element::new(text)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Renderer> Clone for Text<'a, Renderer>
|
||||
where
|
||||
Renderer: text::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
content: self.content.clone(),
|
||||
size: self.size,
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
horizontal_alignment: self.horizontal_alignment,
|
||||
vertical_alignment: self.vertical_alignment,
|
||||
font: self.font,
|
||||
style: self.style,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> From<&'a str> for Element<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: text::Renderer + 'a,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
fn from(contents: &'a str) -> Self {
|
||||
Text::new(contents).into()
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,189 +0,0 @@
|
|||
//! Track the cursor of a text input.
|
||||
use crate::widget::text_input::Value;
|
||||
|
||||
/// The cursor of a text input.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct Cursor {
|
||||
state: State,
|
||||
}
|
||||
|
||||
/// The state of a [`Cursor`].
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum State {
|
||||
/// Cursor without a selection
|
||||
Index(usize),
|
||||
|
||||
/// Cursor selecting a range of text
|
||||
Selection {
|
||||
/// The start of the selection
|
||||
start: usize,
|
||||
/// The end of the selection
|
||||
end: usize,
|
||||
},
|
||||
}
|
||||
|
||||
impl Default for Cursor {
|
||||
fn default() -> Self {
|
||||
Cursor {
|
||||
state: State::Index(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Cursor {
|
||||
/// Returns the [`State`] of the [`Cursor`].
|
||||
pub fn state(&self, value: &Value) -> State {
|
||||
match self.state {
|
||||
State::Index(index) => State::Index(index.min(value.len())),
|
||||
State::Selection { start, end } => {
|
||||
let start = start.min(value.len());
|
||||
let end = end.min(value.len());
|
||||
|
||||
if start == end {
|
||||
State::Index(start)
|
||||
} else {
|
||||
State::Selection { start, end }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the current selection of the [`Cursor`] for the given [`Value`].
|
||||
///
|
||||
/// `start` is guaranteed to be <= than `end`.
|
||||
pub fn selection(&self, value: &Value) -> Option<(usize, usize)> {
|
||||
match self.state(value) {
|
||||
State::Selection { start, end } => {
|
||||
Some((start.min(end), start.max(end)))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn move_to(&mut self, position: usize) {
|
||||
self.state = State::Index(position);
|
||||
}
|
||||
|
||||
pub(crate) fn move_right(&mut self, value: &Value) {
|
||||
self.move_right_by_amount(value, 1)
|
||||
}
|
||||
|
||||
pub(crate) fn move_right_by_words(&mut self, value: &Value) {
|
||||
self.move_to(value.next_end_of_word(self.right(value)))
|
||||
}
|
||||
|
||||
pub(crate) fn move_right_by_amount(
|
||||
&mut self,
|
||||
value: &Value,
|
||||
amount: usize,
|
||||
) {
|
||||
match self.state(value) {
|
||||
State::Index(index) => {
|
||||
self.move_to(index.saturating_add(amount).min(value.len()))
|
||||
}
|
||||
State::Selection { start, end } => self.move_to(end.max(start)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn move_left(&mut self, value: &Value) {
|
||||
match self.state(value) {
|
||||
State::Index(index) if index > 0 => self.move_to(index - 1),
|
||||
State::Selection { start, end } => self.move_to(start.min(end)),
|
||||
_ => self.move_to(0),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn move_left_by_words(&mut self, value: &Value) {
|
||||
self.move_to(value.previous_start_of_word(self.left(value)));
|
||||
}
|
||||
|
||||
pub(crate) fn select_range(&mut self, start: usize, end: usize) {
|
||||
if start == end {
|
||||
self.state = State::Index(start);
|
||||
} else {
|
||||
self.state = State::Selection { start, end };
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn select_left(&mut self, value: &Value) {
|
||||
match self.state(value) {
|
||||
State::Index(index) if index > 0 => {
|
||||
self.select_range(index, index - 1)
|
||||
}
|
||||
State::Selection { start, end } if end > 0 => {
|
||||
self.select_range(start, end - 1)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn select_right(&mut self, value: &Value) {
|
||||
match self.state(value) {
|
||||
State::Index(index) if index < value.len() => {
|
||||
self.select_range(index, index + 1)
|
||||
}
|
||||
State::Selection { start, end } if end < value.len() => {
|
||||
self.select_range(start, end + 1)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn select_left_by_words(&mut self, value: &Value) {
|
||||
match self.state(value) {
|
||||
State::Index(index) => {
|
||||
self.select_range(index, value.previous_start_of_word(index))
|
||||
}
|
||||
State::Selection { start, end } => {
|
||||
self.select_range(start, value.previous_start_of_word(end))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn select_right_by_words(&mut self, value: &Value) {
|
||||
match self.state(value) {
|
||||
State::Index(index) => {
|
||||
self.select_range(index, value.next_end_of_word(index))
|
||||
}
|
||||
State::Selection { start, end } => {
|
||||
self.select_range(start, value.next_end_of_word(end))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn select_all(&mut self, value: &Value) {
|
||||
self.select_range(0, value.len());
|
||||
}
|
||||
|
||||
pub(crate) fn start(&self, value: &Value) -> usize {
|
||||
let start = match self.state {
|
||||
State::Index(index) => index,
|
||||
State::Selection { start, .. } => start,
|
||||
};
|
||||
|
||||
start.min(value.len())
|
||||
}
|
||||
|
||||
pub(crate) fn end(&self, value: &Value) -> usize {
|
||||
let end = match self.state {
|
||||
State::Index(index) => index,
|
||||
State::Selection { end, .. } => end,
|
||||
};
|
||||
|
||||
end.min(value.len())
|
||||
}
|
||||
|
||||
fn left(&self, value: &Value) -> usize {
|
||||
match self.state(value) {
|
||||
State::Index(index) => index,
|
||||
State::Selection { start, end } => start.min(end),
|
||||
}
|
||||
}
|
||||
|
||||
fn right(&self, value: &Value) -> usize {
|
||||
match self.state(value) {
|
||||
State::Index(index) => index,
|
||||
State::Selection { start, end } => start.max(end),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
use crate::widget::text_input::{Cursor, Value};
|
||||
|
||||
pub struct Editor<'a> {
|
||||
value: &'a mut Value,
|
||||
cursor: &'a mut Cursor,
|
||||
}
|
||||
|
||||
impl<'a> Editor<'a> {
|
||||
pub fn new(value: &'a mut Value, cursor: &'a mut Cursor) -> Editor<'a> {
|
||||
Editor { value, cursor }
|
||||
}
|
||||
|
||||
pub fn contents(&self) -> String {
|
||||
self.value.to_string()
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, character: char) {
|
||||
if let Some((left, right)) = self.cursor.selection(self.value) {
|
||||
self.cursor.move_left(self.value);
|
||||
self.value.remove_many(left, right);
|
||||
}
|
||||
|
||||
self.value.insert(self.cursor.end(self.value), character);
|
||||
self.cursor.move_right(self.value);
|
||||
}
|
||||
|
||||
pub fn paste(&mut self, content: Value) {
|
||||
let length = content.len();
|
||||
if let Some((left, right)) = self.cursor.selection(self.value) {
|
||||
self.cursor.move_left(self.value);
|
||||
self.value.remove_many(left, right);
|
||||
}
|
||||
|
||||
self.value.insert_many(self.cursor.end(self.value), content);
|
||||
|
||||
self.cursor.move_right_by_amount(self.value, length);
|
||||
}
|
||||
|
||||
pub fn backspace(&mut self) {
|
||||
match self.cursor.selection(self.value) {
|
||||
Some((start, end)) => {
|
||||
self.cursor.move_left(self.value);
|
||||
self.value.remove_many(start, end);
|
||||
}
|
||||
None => {
|
||||
let start = self.cursor.start(self.value);
|
||||
|
||||
if start > 0 {
|
||||
self.cursor.move_left(self.value);
|
||||
self.value.remove(start - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete(&mut self) {
|
||||
match self.cursor.selection(self.value) {
|
||||
Some(_) => {
|
||||
self.backspace();
|
||||
}
|
||||
None => {
|
||||
let end = self.cursor.end(self.value);
|
||||
|
||||
if end < self.value.len() {
|
||||
self.value.remove(end);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,133 +0,0 @@
|
|||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
/// The value of a [`TextInput`].
|
||||
///
|
||||
/// [`TextInput`]: crate::widget::TextInput
|
||||
// TODO: Reduce allocations, cache results (?)
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Value {
|
||||
graphemes: Vec<String>,
|
||||
}
|
||||
|
||||
impl Value {
|
||||
/// Creates a new [`Value`] from a string slice.
|
||||
pub fn new(string: &str) -> Self {
|
||||
let graphemes = UnicodeSegmentation::graphemes(string, true)
|
||||
.map(String::from)
|
||||
.collect();
|
||||
|
||||
Self { graphemes }
|
||||
}
|
||||
|
||||
/// Returns whether the [`Value`] is empty or not.
|
||||
///
|
||||
/// A [`Value`] is empty when it contains no graphemes.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
|
||||
/// Returns the total amount of graphemes in the [`Value`].
|
||||
pub fn len(&self) -> usize {
|
||||
self.graphemes.len()
|
||||
}
|
||||
|
||||
/// Returns the position of the previous start of a word from the given
|
||||
/// grapheme `index`.
|
||||
pub fn previous_start_of_word(&self, index: usize) -> usize {
|
||||
let previous_string =
|
||||
&self.graphemes[..index.min(self.graphemes.len())].concat();
|
||||
|
||||
UnicodeSegmentation::split_word_bound_indices(previous_string as &str)
|
||||
.filter(|(_, word)| !word.trim_start().is_empty())
|
||||
.next_back()
|
||||
.map(|(i, previous_word)| {
|
||||
index
|
||||
- UnicodeSegmentation::graphemes(previous_word, true)
|
||||
.count()
|
||||
- UnicodeSegmentation::graphemes(
|
||||
&previous_string[i + previous_word.len()..] as &str,
|
||||
true,
|
||||
)
|
||||
.count()
|
||||
})
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Returns the position of the next end of a word from the given grapheme
|
||||
/// `index`.
|
||||
pub fn next_end_of_word(&self, index: usize) -> usize {
|
||||
let next_string = &self.graphemes[index..].concat();
|
||||
|
||||
UnicodeSegmentation::split_word_bound_indices(next_string as &str)
|
||||
.find(|(_, word)| !word.trim_start().is_empty())
|
||||
.map(|(i, next_word)| {
|
||||
index
|
||||
+ UnicodeSegmentation::graphemes(next_word, true).count()
|
||||
+ UnicodeSegmentation::graphemes(
|
||||
&next_string[..i] as &str,
|
||||
true,
|
||||
)
|
||||
.count()
|
||||
})
|
||||
.unwrap_or(self.len())
|
||||
}
|
||||
|
||||
/// Returns a new [`Value`] containing the graphemes from `start` until the
|
||||
/// given `end`.
|
||||
pub fn select(&self, start: usize, end: usize) -> Self {
|
||||
let graphemes =
|
||||
self.graphemes[start.min(self.len())..end.min(self.len())].to_vec();
|
||||
|
||||
Self { graphemes }
|
||||
}
|
||||
|
||||
/// Returns a new [`Value`] containing the graphemes until the given
|
||||
/// `index`.
|
||||
pub fn until(&self, index: usize) -> Self {
|
||||
let graphemes = self.graphemes[..index.min(self.len())].to_vec();
|
||||
|
||||
Self { graphemes }
|
||||
}
|
||||
|
||||
/// Converts the [`Value`] into a `String`.
|
||||
pub fn to_string(&self) -> String {
|
||||
self.graphemes.concat()
|
||||
}
|
||||
|
||||
/// Inserts a new `char` at the given grapheme `index`.
|
||||
pub fn insert(&mut self, index: usize, c: char) {
|
||||
self.graphemes.insert(index, c.to_string());
|
||||
|
||||
self.graphemes =
|
||||
UnicodeSegmentation::graphemes(&self.to_string() as &str, true)
|
||||
.map(String::from)
|
||||
.collect();
|
||||
}
|
||||
|
||||
/// Inserts a bunch of graphemes at the given grapheme `index`.
|
||||
pub fn insert_many(&mut self, index: usize, mut value: Value) {
|
||||
let _ = self
|
||||
.graphemes
|
||||
.splice(index..index, value.graphemes.drain(..));
|
||||
}
|
||||
|
||||
/// Removes the grapheme at the given `index`.
|
||||
pub fn remove(&mut self, index: usize) {
|
||||
let _ = self.graphemes.remove(index);
|
||||
}
|
||||
|
||||
/// Removes the graphemes from `start` to `end`.
|
||||
pub fn remove_many(&mut self, start: usize, end: usize) {
|
||||
let _ = self.graphemes.splice(start..end, std::iter::empty());
|
||||
}
|
||||
|
||||
/// Returns a new [`Value`] with all its graphemes replaced with the
|
||||
/// dot ('•') character.
|
||||
pub fn secure(&self) -> Self {
|
||||
Self {
|
||||
graphemes: std::iter::repeat(String::from("•"))
|
||||
.take(self.graphemes.len())
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,324 +0,0 @@
|
|||
//! Show toggle controls using togglers.
|
||||
use crate::alignment;
|
||||
use crate::event;
|
||||
use crate::layout;
|
||||
use crate::mouse;
|
||||
use crate::renderer;
|
||||
use crate::text;
|
||||
use crate::widget::{self, Row, Text, Tree};
|
||||
use crate::{
|
||||
Alignment, Clipboard, Element, Event, Layout, Length, Pixels, Point,
|
||||
Rectangle, Shell, Widget,
|
||||
};
|
||||
|
||||
pub use iced_style::toggler::{Appearance, StyleSheet};
|
||||
|
||||
/// A toggler widget.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # type Toggler<'a, Message> = iced_native::widget::Toggler<'a, Message, iced_native::renderer::Null>;
|
||||
/// #
|
||||
/// pub enum Message {
|
||||
/// TogglerToggled(bool),
|
||||
/// }
|
||||
///
|
||||
/// let is_toggled = true;
|
||||
///
|
||||
/// Toggler::new(String::from("Toggle me!"), is_toggled, |b| Message::TogglerToggled(b));
|
||||
/// ```
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Toggler<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: text::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
is_toggled: bool,
|
||||
on_toggle: Box<dyn Fn(bool) -> Message + 'a>,
|
||||
label: Option<String>,
|
||||
width: Length,
|
||||
size: f32,
|
||||
text_size: Option<f32>,
|
||||
text_alignment: alignment::Horizontal,
|
||||
spacing: f32,
|
||||
font: Option<Renderer::Font>,
|
||||
style: <Renderer::Theme as StyleSheet>::Style,
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Toggler<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: text::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
/// The default size of a [`Toggler`].
|
||||
pub const DEFAULT_SIZE: f32 = 20.0;
|
||||
|
||||
/// Creates a new [`Toggler`].
|
||||
///
|
||||
/// It expects:
|
||||
/// * a boolean describing whether the [`Toggler`] is checked or not
|
||||
/// * An optional label for the [`Toggler`]
|
||||
/// * a function that will be called when the [`Toggler`] is toggled. It
|
||||
/// will receive the new state of the [`Toggler`] and must produce a
|
||||
/// `Message`.
|
||||
pub fn new<F>(
|
||||
label: impl Into<Option<String>>,
|
||||
is_toggled: bool,
|
||||
f: F,
|
||||
) -> Self
|
||||
where
|
||||
F: 'a + Fn(bool) -> Message,
|
||||
{
|
||||
Toggler {
|
||||
is_toggled,
|
||||
on_toggle: Box::new(f),
|
||||
label: label.into(),
|
||||
width: Length::Fill,
|
||||
size: Self::DEFAULT_SIZE,
|
||||
text_size: None,
|
||||
text_alignment: alignment::Horizontal::Left,
|
||||
spacing: 0.0,
|
||||
font: None,
|
||||
style: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the size of the [`Toggler`].
|
||||
pub fn size(mut self, size: impl Into<Pixels>) -> Self {
|
||||
self.size = size.into().0;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the width of the [`Toggler`].
|
||||
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||
self.width = width.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the text size o the [`Toggler`].
|
||||
pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self {
|
||||
self.text_size = Some(text_size.into().0);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the horizontal alignment of the text of the [`Toggler`]
|
||||
pub fn text_alignment(mut self, alignment: alignment::Horizontal) -> Self {
|
||||
self.text_alignment = alignment;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the spacing between the [`Toggler`] and the text.
|
||||
pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
|
||||
self.spacing = spacing.into().0;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`Font`] of the text of the [`Toggler`]
|
||||
///
|
||||
/// [`Font`]: crate::text::Renderer::Font
|
||||
pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
|
||||
self.font = Some(font.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the [`Toggler`].
|
||||
pub fn style(
|
||||
mut self,
|
||||
style: impl Into<<Renderer::Theme as StyleSheet>::Style>,
|
||||
) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Widget<Message, Renderer>
|
||||
for Toggler<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: text::Renderer,
|
||||
Renderer::Theme: StyleSheet + widget::text::StyleSheet,
|
||||
{
|
||||
fn width(&self) -> Length {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
Length::Shrink
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
let mut row = Row::<(), Renderer>::new()
|
||||
.width(self.width)
|
||||
.spacing(self.spacing)
|
||||
.align_items(Alignment::Center);
|
||||
|
||||
if let Some(label) = &self.label {
|
||||
row = row.push(
|
||||
Text::new(label)
|
||||
.horizontal_alignment(self.text_alignment)
|
||||
.font(self.font.unwrap_or_else(|| renderer.default_font()))
|
||||
.width(self.width)
|
||||
.size(
|
||||
self.text_size
|
||||
.unwrap_or_else(|| renderer.default_size()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
row = row.push(Row::new().width(2.0 * self.size).height(self.size));
|
||||
|
||||
row.layout(renderer, limits)
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
_state: &mut Tree,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
_renderer: &Renderer,
|
||||
_clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
match event {
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
|
||||
let mouse_over = layout.bounds().contains(cursor_position);
|
||||
|
||||
if mouse_over {
|
||||
shell.publish((self.on_toggle)(!self.is_toggled));
|
||||
|
||||
event::Status::Captured
|
||||
} else {
|
||||
event::Status::Ignored
|
||||
}
|
||||
}
|
||||
_ => event::Status::Ignored,
|
||||
}
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
_state: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
_renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
if layout.bounds().contains(cursor_position) {
|
||||
mouse::Interaction::Pointer
|
||||
} else {
|
||||
mouse::Interaction::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
_state: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
) {
|
||||
/// Makes sure that the border radius of the toggler looks good at every size.
|
||||
const BORDER_RADIUS_RATIO: f32 = 32.0 / 13.0;
|
||||
|
||||
/// The space ratio between the background Quad and the Toggler bounds, and
|
||||
/// between the background Quad and foreground Quad.
|
||||
const SPACE_RATIO: f32 = 0.05;
|
||||
|
||||
let mut children = layout.children();
|
||||
|
||||
if let Some(label) = &self.label {
|
||||
let label_layout = children.next().unwrap();
|
||||
|
||||
crate::widget::text::draw(
|
||||
renderer,
|
||||
style,
|
||||
label_layout,
|
||||
label,
|
||||
self.text_size,
|
||||
self.font,
|
||||
Default::default(),
|
||||
self.text_alignment,
|
||||
alignment::Vertical::Center,
|
||||
);
|
||||
}
|
||||
|
||||
let toggler_layout = children.next().unwrap();
|
||||
let bounds = toggler_layout.bounds();
|
||||
|
||||
let is_mouse_over = bounds.contains(cursor_position);
|
||||
|
||||
let style = if is_mouse_over {
|
||||
theme.hovered(&self.style, self.is_toggled)
|
||||
} else {
|
||||
theme.active(&self.style, self.is_toggled)
|
||||
};
|
||||
|
||||
let border_radius = bounds.height / BORDER_RADIUS_RATIO;
|
||||
let space = SPACE_RATIO * bounds.height;
|
||||
|
||||
let toggler_background_bounds = Rectangle {
|
||||
x: bounds.x + space,
|
||||
y: bounds.y + space,
|
||||
width: bounds.width - (2.0 * space),
|
||||
height: bounds.height - (2.0 * space),
|
||||
};
|
||||
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: toggler_background_bounds,
|
||||
border_radius: border_radius.into(),
|
||||
border_width: 1.0,
|
||||
border_color: style
|
||||
.background_border
|
||||
.unwrap_or(style.background),
|
||||
},
|
||||
style.background,
|
||||
);
|
||||
|
||||
let toggler_foreground_bounds = Rectangle {
|
||||
x: bounds.x
|
||||
+ if self.is_toggled {
|
||||
bounds.width - 2.0 * space - (bounds.height - (4.0 * space))
|
||||
} else {
|
||||
2.0 * space
|
||||
},
|
||||
y: bounds.y + (2.0 * space),
|
||||
width: bounds.height - (4.0 * space),
|
||||
height: bounds.height - (4.0 * space),
|
||||
};
|
||||
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: toggler_foreground_bounds,
|
||||
border_radius: border_radius.into(),
|
||||
border_width: 1.0,
|
||||
border_color: style
|
||||
.foreground_border
|
||||
.unwrap_or(style.foreground),
|
||||
},
|
||||
style.foreground,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> From<Toggler<'a, Message, Renderer>>
|
||||
for Element<'a, Message, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Renderer: 'a + text::Renderer,
|
||||
Renderer::Theme: StyleSheet + widget::text::StyleSheet,
|
||||
{
|
||||
fn from(
|
||||
toggler: Toggler<'a, Message, Renderer>,
|
||||
) -> Element<'a, Message, Renderer> {
|
||||
Element::new(toggler)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,387 +0,0 @@
|
|||
//! Display a widget over another.
|
||||
use crate::event;
|
||||
use crate::layout;
|
||||
use crate::mouse;
|
||||
use crate::renderer;
|
||||
use crate::text;
|
||||
use crate::widget;
|
||||
use crate::widget::container;
|
||||
use crate::widget::overlay;
|
||||
use crate::widget::{Text, Tree};
|
||||
use crate::{
|
||||
Clipboard, Element, Event, Layout, Length, Padding, Pixels, Point,
|
||||
Rectangle, Shell, Size, Vector, Widget,
|
||||
};
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// An element to display a widget over another.
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Tooltip<'a, Message, Renderer: text::Renderer>
|
||||
where
|
||||
Renderer: text::Renderer,
|
||||
Renderer::Theme: container::StyleSheet + widget::text::StyleSheet,
|
||||
{
|
||||
content: Element<'a, Message, Renderer>,
|
||||
tooltip: Text<'a, Renderer>,
|
||||
position: Position,
|
||||
gap: f32,
|
||||
padding: f32,
|
||||
snap_within_viewport: bool,
|
||||
style: <Renderer::Theme as container::StyleSheet>::Style,
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Tooltip<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: text::Renderer,
|
||||
Renderer::Theme: container::StyleSheet + widget::text::StyleSheet,
|
||||
{
|
||||
/// The default padding of a [`Tooltip`] drawn by this renderer.
|
||||
const DEFAULT_PADDING: f32 = 5.0;
|
||||
|
||||
/// Creates a new [`Tooltip`].
|
||||
///
|
||||
/// [`Tooltip`]: struct.Tooltip.html
|
||||
pub fn new(
|
||||
content: impl Into<Element<'a, Message, Renderer>>,
|
||||
tooltip: impl Into<Cow<'a, str>>,
|
||||
position: Position,
|
||||
) -> Self {
|
||||
Tooltip {
|
||||
content: content.into(),
|
||||
tooltip: Text::new(tooltip),
|
||||
position,
|
||||
gap: 0.0,
|
||||
padding: Self::DEFAULT_PADDING,
|
||||
snap_within_viewport: true,
|
||||
style: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the size of the text of the [`Tooltip`].
|
||||
pub fn size(mut self, size: impl Into<Pixels>) -> Self {
|
||||
self.tooltip = self.tooltip.size(size);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the font of the [`Tooltip`].
|
||||
///
|
||||
/// [`Font`]: Renderer::Font
|
||||
pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
|
||||
self.tooltip = self.tooltip.font(font);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the gap between the content and its [`Tooltip`].
|
||||
pub fn gap(mut self, gap: impl Into<Pixels>) -> Self {
|
||||
self.gap = gap.into().0;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the padding of the [`Tooltip`].
|
||||
pub fn padding(mut self, padding: impl Into<Pixels>) -> Self {
|
||||
self.padding = padding.into().0;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets whether the [`Tooltip`] is snapped within the viewport.
|
||||
pub fn snap_within_viewport(mut self, snap: bool) -> Self {
|
||||
self.snap_within_viewport = snap;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the [`Tooltip`].
|
||||
pub fn style(
|
||||
mut self,
|
||||
style: impl Into<<Renderer::Theme as container::StyleSheet>::Style>,
|
||||
) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Widget<Message, Renderer>
|
||||
for Tooltip<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: text::Renderer,
|
||||
Renderer::Theme: container::StyleSheet + widget::text::StyleSheet,
|
||||
{
|
||||
fn children(&self) -> Vec<Tree> {
|
||||
vec![Tree::new(&self.content)]
|
||||
}
|
||||
|
||||
fn diff(&self, tree: &mut Tree) {
|
||||
tree.diff_children(std::slice::from_ref(&self.content))
|
||||
}
|
||||
|
||||
fn width(&self) -> Length {
|
||||
self.content.as_widget().width()
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
self.content.as_widget().height()
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
self.content.as_widget().layout(renderer, limits)
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
self.content.as_widget_mut().on_event(
|
||||
&mut tree.children[0],
|
||||
event,
|
||||
layout,
|
||||
cursor_position,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
)
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
self.content.as_widget().mouse_interaction(
|
||||
&tree.children[0],
|
||||
layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
renderer,
|
||||
)
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
inherited_style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
self.content.as_widget().draw(
|
||||
&tree.children[0],
|
||||
renderer,
|
||||
theme,
|
||||
inherited_style,
|
||||
layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
);
|
||||
|
||||
let tooltip = &self.tooltip;
|
||||
|
||||
draw(
|
||||
renderer,
|
||||
theme,
|
||||
inherited_style,
|
||||
layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
self.position,
|
||||
self.gap,
|
||||
self.padding,
|
||||
self.snap_within_viewport,
|
||||
&self.style,
|
||||
|renderer, limits| {
|
||||
Widget::<(), Renderer>::layout(tooltip, renderer, limits)
|
||||
},
|
||||
|renderer, defaults, layout, cursor_position, viewport| {
|
||||
Widget::<(), Renderer>::draw(
|
||||
tooltip,
|
||||
&Tree::empty(),
|
||||
renderer,
|
||||
theme,
|
||||
defaults,
|
||||
layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn overlay<'b>(
|
||||
&'b mut self,
|
||||
tree: &'b mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
) -> Option<overlay::Element<'b, Message, Renderer>> {
|
||||
self.content.as_widget_mut().overlay(
|
||||
&mut tree.children[0],
|
||||
layout,
|
||||
renderer,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> From<Tooltip<'a, Message, Renderer>>
|
||||
for Element<'a, Message, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Renderer: 'a + text::Renderer,
|
||||
Renderer::Theme: container::StyleSheet + widget::text::StyleSheet,
|
||||
{
|
||||
fn from(
|
||||
tooltip: Tooltip<'a, Message, Renderer>,
|
||||
) -> Element<'a, Message, Renderer> {
|
||||
Element::new(tooltip)
|
||||
}
|
||||
}
|
||||
|
||||
/// The position of the tooltip. Defaults to following the cursor.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Position {
|
||||
/// The tooltip will follow the cursor.
|
||||
FollowCursor,
|
||||
/// The tooltip will appear on the top of the widget.
|
||||
Top,
|
||||
/// The tooltip will appear on the bottom of the widget.
|
||||
Bottom,
|
||||
/// The tooltip will appear on the left of the widget.
|
||||
Left,
|
||||
/// The tooltip will appear on the right of the widget.
|
||||
Right,
|
||||
}
|
||||
|
||||
/// Draws a [`Tooltip`].
|
||||
pub fn draw<Renderer>(
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
inherited_style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
position: Position,
|
||||
gap: f32,
|
||||
padding: f32,
|
||||
snap_within_viewport: bool,
|
||||
style: &<Renderer::Theme as container::StyleSheet>::Style,
|
||||
layout_text: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node,
|
||||
draw_text: impl FnOnce(
|
||||
&mut Renderer,
|
||||
&renderer::Style,
|
||||
Layout<'_>,
|
||||
Point,
|
||||
&Rectangle,
|
||||
),
|
||||
) where
|
||||
Renderer: crate::Renderer,
|
||||
Renderer::Theme: container::StyleSheet,
|
||||
{
|
||||
use container::StyleSheet;
|
||||
|
||||
let bounds = layout.bounds();
|
||||
|
||||
if bounds.contains(cursor_position) {
|
||||
let style = theme.appearance(style);
|
||||
|
||||
let defaults = renderer::Style {
|
||||
text_color: style.text_color.unwrap_or(inherited_style.text_color),
|
||||
};
|
||||
|
||||
let text_layout = layout_text(
|
||||
renderer,
|
||||
&layout::Limits::new(
|
||||
Size::ZERO,
|
||||
snap_within_viewport
|
||||
.then(|| viewport.size())
|
||||
.unwrap_or(Size::INFINITY),
|
||||
)
|
||||
.pad(Padding::new(padding)),
|
||||
);
|
||||
|
||||
let text_bounds = text_layout.bounds();
|
||||
let x_center = bounds.x + (bounds.width - text_bounds.width) / 2.0;
|
||||
let y_center = bounds.y + (bounds.height - text_bounds.height) / 2.0;
|
||||
|
||||
let mut tooltip_bounds = {
|
||||
let offset = match position {
|
||||
Position::Top => Vector::new(
|
||||
x_center,
|
||||
bounds.y - text_bounds.height - gap - padding,
|
||||
),
|
||||
Position::Bottom => Vector::new(
|
||||
x_center,
|
||||
bounds.y + bounds.height + gap + padding,
|
||||
),
|
||||
Position::Left => Vector::new(
|
||||
bounds.x - text_bounds.width - gap - padding,
|
||||
y_center,
|
||||
),
|
||||
Position::Right => Vector::new(
|
||||
bounds.x + bounds.width + gap + padding,
|
||||
y_center,
|
||||
),
|
||||
Position::FollowCursor => Vector::new(
|
||||
cursor_position.x,
|
||||
cursor_position.y - text_bounds.height,
|
||||
),
|
||||
};
|
||||
|
||||
Rectangle {
|
||||
x: offset.x - padding,
|
||||
y: offset.y - padding,
|
||||
width: text_bounds.width + padding * 2.0,
|
||||
height: text_bounds.height + padding * 2.0,
|
||||
}
|
||||
};
|
||||
|
||||
if snap_within_viewport {
|
||||
if tooltip_bounds.x < viewport.x {
|
||||
tooltip_bounds.x = viewport.x;
|
||||
} else if viewport.x + viewport.width
|
||||
< tooltip_bounds.x + tooltip_bounds.width
|
||||
{
|
||||
tooltip_bounds.x =
|
||||
viewport.x + viewport.width - tooltip_bounds.width;
|
||||
}
|
||||
|
||||
if tooltip_bounds.y < viewport.y {
|
||||
tooltip_bounds.y = viewport.y;
|
||||
} else if viewport.y + viewport.height
|
||||
< tooltip_bounds.y + tooltip_bounds.height
|
||||
{
|
||||
tooltip_bounds.y =
|
||||
viewport.y + viewport.height - tooltip_bounds.height;
|
||||
}
|
||||
}
|
||||
|
||||
renderer.with_layer(Rectangle::with_size(Size::INFINITY), |renderer| {
|
||||
container::draw_background(renderer, &style, tooltip_bounds);
|
||||
|
||||
draw_text(
|
||||
renderer,
|
||||
&defaults,
|
||||
Layout::with_offset(
|
||||
Vector::new(
|
||||
tooltip_bounds.x + padding,
|
||||
tooltip_bounds.y + padding,
|
||||
),
|
||||
&text_layout,
|
||||
),
|
||||
cursor_position,
|
||||
viewport,
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,187 +0,0 @@
|
|||
//! Store internal widget state in a state tree to ensure continuity.
|
||||
use crate::Widget;
|
||||
|
||||
use std::any::{self, Any};
|
||||
use std::borrow::Borrow;
|
||||
use std::fmt;
|
||||
|
||||
/// A persistent state widget tree.
|
||||
///
|
||||
/// A [`Tree`] is normally associated with a specific widget in the widget tree.
|
||||
#[derive(Debug)]
|
||||
pub struct Tree {
|
||||
/// The tag of the [`Tree`].
|
||||
pub tag: Tag,
|
||||
|
||||
/// The [`State`] of the [`Tree`].
|
||||
pub state: State,
|
||||
|
||||
/// The children of the root widget of the [`Tree`].
|
||||
pub children: Vec<Tree>,
|
||||
}
|
||||
|
||||
impl Tree {
|
||||
/// Creates an empty, stateless [`Tree`] with no children.
|
||||
pub fn empty() -> Self {
|
||||
Self {
|
||||
tag: Tag::stateless(),
|
||||
state: State::None,
|
||||
children: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new [`Tree`] for the provided [`Widget`].
|
||||
pub fn new<'a, Message, Renderer>(
|
||||
widget: impl Borrow<dyn Widget<Message, Renderer> + 'a>,
|
||||
) -> Self
|
||||
where
|
||||
Renderer: crate::Renderer,
|
||||
{
|
||||
let widget = widget.borrow();
|
||||
|
||||
Self {
|
||||
tag: widget.tag(),
|
||||
state: widget.state(),
|
||||
children: widget.children(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Reconciliates the current tree with the provided [`Widget`].
|
||||
///
|
||||
/// If the tag of the [`Widget`] matches the tag of the [`Tree`], then the
|
||||
/// [`Widget`] proceeds with the reconciliation (i.e. [`Widget::diff`] is called).
|
||||
///
|
||||
/// Otherwise, the whole [`Tree`] is recreated.
|
||||
///
|
||||
/// [`Widget::diff`]: crate::Widget::diff
|
||||
pub fn diff<'a, Message, Renderer>(
|
||||
&mut self,
|
||||
new: impl Borrow<dyn Widget<Message, Renderer> + 'a>,
|
||||
) where
|
||||
Renderer: crate::Renderer,
|
||||
{
|
||||
if self.tag == new.borrow().tag() {
|
||||
new.borrow().diff(self)
|
||||
} else {
|
||||
*self = Self::new(new);
|
||||
}
|
||||
}
|
||||
|
||||
/// Reconciliates the children of the tree with the provided list of widgets.
|
||||
pub fn diff_children<'a, Message, Renderer>(
|
||||
&mut self,
|
||||
new_children: &[impl Borrow<dyn Widget<Message, Renderer> + 'a>],
|
||||
) where
|
||||
Renderer: crate::Renderer,
|
||||
{
|
||||
self.diff_children_custom(
|
||||
new_children,
|
||||
|tree, widget| tree.diff(widget.borrow()),
|
||||
|widget| Self::new(widget.borrow()),
|
||||
)
|
||||
}
|
||||
|
||||
/// Reconciliates the children of the tree with the provided list of widgets using custom
|
||||
/// logic both for diffing and creating new widget state.
|
||||
pub fn diff_children_custom<T>(
|
||||
&mut self,
|
||||
new_children: &[T],
|
||||
diff: impl Fn(&mut Tree, &T),
|
||||
new_state: impl Fn(&T) -> Self,
|
||||
) {
|
||||
if self.children.len() > new_children.len() {
|
||||
self.children.truncate(new_children.len());
|
||||
}
|
||||
|
||||
for (child_state, new) in
|
||||
self.children.iter_mut().zip(new_children.iter())
|
||||
{
|
||||
diff(child_state, new);
|
||||
}
|
||||
|
||||
if self.children.len() < new_children.len() {
|
||||
self.children.extend(
|
||||
new_children[self.children.len()..].iter().map(new_state),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The identifier of some widget state.
|
||||
#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)]
|
||||
pub struct Tag(any::TypeId);
|
||||
|
||||
impl Tag {
|
||||
/// Creates a [`Tag`] for a state of type `T`.
|
||||
pub fn of<T>() -> Self
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
Self(any::TypeId::of::<T>())
|
||||
}
|
||||
|
||||
/// Creates a [`Tag`] for a stateless widget.
|
||||
pub fn stateless() -> Self {
|
||||
Self::of::<()>()
|
||||
}
|
||||
}
|
||||
|
||||
/// The internal [`State`] of a widget.
|
||||
pub enum State {
|
||||
/// No meaningful internal state.
|
||||
None,
|
||||
|
||||
/// Some meaningful internal state.
|
||||
Some(Box<dyn Any>),
|
||||
}
|
||||
|
||||
impl State {
|
||||
/// Creates a new [`State`].
|
||||
pub fn new<T>(state: T) -> Self
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
State::Some(Box::new(state))
|
||||
}
|
||||
|
||||
/// Downcasts the [`State`] to `T` and returns a reference to it.
|
||||
///
|
||||
/// # Panics
|
||||
/// This method will panic if the downcast fails or the [`State`] is [`State::None`].
|
||||
pub fn downcast_ref<T>(&self) -> &T
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
match self {
|
||||
State::None => panic!("Downcast on stateless state"),
|
||||
State::Some(state) => {
|
||||
state.downcast_ref().expect("Downcast widget state")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Downcasts the [`State`] to `T` and returns a mutable reference to it.
|
||||
///
|
||||
/// # Panics
|
||||
/// This method will panic if the downcast fails or the [`State`] is [`State::None`].
|
||||
pub fn downcast_mut<T>(&mut self) -> &mut T
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
match self {
|
||||
State::None => panic!("Downcast on stateless state"),
|
||||
State::Some(state) => {
|
||||
state.downcast_mut().expect("Downcast widget state")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for State {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::None => write!(f, "State::None"),
|
||||
Self::Some(_) => write!(f, "State::Some"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,468 +0,0 @@
|
|||
//! Display an interactive selector of a single value from a range of values.
|
||||
//!
|
||||
//! A [`VerticalSlider`] has some local [`State`].
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
pub use iced_style::slider::{Appearance, Handle, HandleShape, StyleSheet};
|
||||
|
||||
use crate::event::{self, Event};
|
||||
use crate::widget::tree::{self, Tree};
|
||||
use crate::{
|
||||
layout, mouse, renderer, touch, Background, Clipboard, Color, Element,
|
||||
Layout, Length, Pixels, Point, Rectangle, Shell, Size, Widget,
|
||||
};
|
||||
|
||||
/// An vertical bar and a handle that selects a single value from a range of
|
||||
/// values.
|
||||
///
|
||||
/// A [`VerticalSlider`] will try to fill the vertical space of its container.
|
||||
///
|
||||
/// The [`VerticalSlider`] range of numeric values is generic and its step size defaults
|
||||
/// to 1 unit.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use iced_native::widget::vertical_slider;
|
||||
/// # use iced_native::renderer::Null;
|
||||
/// #
|
||||
/// # type VerticalSlider<'a, T, Message> = vertical_slider::VerticalSlider<'a, T, Message, Null>;
|
||||
/// #
|
||||
/// #[derive(Clone)]
|
||||
/// pub enum Message {
|
||||
/// SliderChanged(f32),
|
||||
/// }
|
||||
///
|
||||
/// let value = 50.0;
|
||||
///
|
||||
/// VerticalSlider::new(0.0..=100.0, value, Message::SliderChanged);
|
||||
/// ```
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct VerticalSlider<'a, T, Message, Renderer>
|
||||
where
|
||||
Renderer: crate::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
range: RangeInclusive<T>,
|
||||
step: T,
|
||||
value: T,
|
||||
on_change: Box<dyn Fn(T) -> Message + 'a>,
|
||||
on_release: Option<Message>,
|
||||
width: f32,
|
||||
height: Length,
|
||||
style: <Renderer::Theme as StyleSheet>::Style,
|
||||
}
|
||||
|
||||
impl<'a, T, Message, Renderer> VerticalSlider<'a, T, Message, Renderer>
|
||||
where
|
||||
T: Copy + From<u8> + std::cmp::PartialOrd,
|
||||
Message: Clone,
|
||||
Renderer: crate::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
/// The default width of a [`VerticalSlider`].
|
||||
pub const DEFAULT_WIDTH: f32 = 22.0;
|
||||
|
||||
/// Creates a new [`VerticalSlider`].
|
||||
///
|
||||
/// It expects:
|
||||
/// * an inclusive range of possible values
|
||||
/// * the current value of the [`VerticalSlider`]
|
||||
/// * a function that will be called when the [`VerticalSlider`] is dragged.
|
||||
/// It receives the new value of the [`VerticalSlider`] and must produce a
|
||||
/// `Message`.
|
||||
pub fn new<F>(range: RangeInclusive<T>, value: T, on_change: F) -> Self
|
||||
where
|
||||
F: 'a + Fn(T) -> Message,
|
||||
{
|
||||
let value = if value >= *range.start() {
|
||||
value
|
||||
} else {
|
||||
*range.start()
|
||||
};
|
||||
|
||||
let value = if value <= *range.end() {
|
||||
value
|
||||
} else {
|
||||
*range.end()
|
||||
};
|
||||
|
||||
VerticalSlider {
|
||||
value,
|
||||
range,
|
||||
step: T::from(1),
|
||||
on_change: Box::new(on_change),
|
||||
on_release: None,
|
||||
width: Self::DEFAULT_WIDTH,
|
||||
height: Length::Fill,
|
||||
style: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the release message of the [`VerticalSlider`].
|
||||
/// This is called when the mouse is released from the slider.
|
||||
///
|
||||
/// Typically, the user's interaction with the slider is finished when this message is produced.
|
||||
/// This is useful if you need to spawn a long-running task from the slider's result, where
|
||||
/// the default on_change message could create too many events.
|
||||
pub fn on_release(mut self, on_release: Message) -> Self {
|
||||
self.on_release = Some(on_release);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the width of the [`VerticalSlider`].
|
||||
pub fn width(mut self, width: impl Into<Pixels>) -> Self {
|
||||
self.width = width.into().0;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the height of the [`VerticalSlider`].
|
||||
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
||||
self.height = height.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the [`VerticalSlider`].
|
||||
pub fn style(
|
||||
mut self,
|
||||
style: impl Into<<Renderer::Theme as StyleSheet>::Style>,
|
||||
) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the step size of the [`VerticalSlider`].
|
||||
pub fn step(mut self, step: T) -> Self {
|
||||
self.step = step;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T, Message, Renderer> Widget<Message, Renderer>
|
||||
for VerticalSlider<'a, T, Message, Renderer>
|
||||
where
|
||||
T: Copy + Into<f64> + num_traits::FromPrimitive,
|
||||
Message: Clone,
|
||||
Renderer: crate::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
fn tag(&self) -> tree::Tag {
|
||||
tree::Tag::of::<State>()
|
||||
}
|
||||
|
||||
fn state(&self) -> tree::State {
|
||||
tree::State::new(State::new())
|
||||
}
|
||||
|
||||
fn width(&self) -> Length {
|
||||
Length::Shrink
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
_renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
let limits = limits.width(self.width).height(self.height);
|
||||
let size = limits.resolve(Size::ZERO);
|
||||
|
||||
layout::Node::new(size)
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
_renderer: &Renderer,
|
||||
_clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
update(
|
||||
event,
|
||||
layout,
|
||||
cursor_position,
|
||||
shell,
|
||||
tree.state.downcast_mut::<State>(),
|
||||
&mut self.value,
|
||||
&self.range,
|
||||
self.step,
|
||||
self.on_change.as_ref(),
|
||||
&self.on_release,
|
||||
)
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
_style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
) {
|
||||
draw(
|
||||
renderer,
|
||||
layout,
|
||||
cursor_position,
|
||||
tree.state.downcast_ref::<State>(),
|
||||
self.value,
|
||||
&self.range,
|
||||
theme,
|
||||
&self.style,
|
||||
)
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
_renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
mouse_interaction(
|
||||
layout,
|
||||
cursor_position,
|
||||
tree.state.downcast_ref::<State>(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T, Message, Renderer> From<VerticalSlider<'a, T, Message, Renderer>>
|
||||
for Element<'a, Message, Renderer>
|
||||
where
|
||||
T: 'a + Copy + Into<f64> + num_traits::FromPrimitive,
|
||||
Message: 'a + Clone,
|
||||
Renderer: 'a + crate::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
fn from(
|
||||
slider: VerticalSlider<'a, T, Message, Renderer>,
|
||||
) -> Element<'a, Message, Renderer> {
|
||||
Element::new(slider)
|
||||
}
|
||||
}
|
||||
|
||||
/// Processes an [`Event`] and updates the [`State`] of a [`VerticalSlider`]
|
||||
/// accordingly.
|
||||
pub fn update<Message, T>(
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
state: &mut State,
|
||||
value: &mut T,
|
||||
range: &RangeInclusive<T>,
|
||||
step: T,
|
||||
on_change: &dyn Fn(T) -> Message,
|
||||
on_release: &Option<Message>,
|
||||
) -> event::Status
|
||||
where
|
||||
T: Copy + Into<f64> + num_traits::FromPrimitive,
|
||||
Message: Clone,
|
||||
{
|
||||
let is_dragging = state.is_dragging;
|
||||
|
||||
let mut change = || {
|
||||
let bounds = layout.bounds();
|
||||
let new_value = if cursor_position.y >= bounds.y + bounds.height {
|
||||
*range.start()
|
||||
} else if cursor_position.y <= bounds.y {
|
||||
*range.end()
|
||||
} else {
|
||||
let step = step.into();
|
||||
let start = (*range.start()).into();
|
||||
let end = (*range.end()).into();
|
||||
|
||||
let percent = 1.0
|
||||
- f64::from(cursor_position.y - bounds.y)
|
||||
/ f64::from(bounds.height);
|
||||
|
||||
let steps = (percent * (end - start) / step).round();
|
||||
let value = steps * step + start;
|
||||
|
||||
if let Some(value) = T::from_f64(value) {
|
||||
value
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if ((*value).into() - new_value.into()).abs() > f64::EPSILON {
|
||||
shell.publish((on_change)(new_value));
|
||||
|
||||
*value = new_value;
|
||||
}
|
||||
};
|
||||
|
||||
match event {
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
|
||||
| Event::Touch(touch::Event::FingerPressed { .. }) => {
|
||||
if layout.bounds().contains(cursor_position) {
|
||||
change();
|
||||
state.is_dragging = true;
|
||||
|
||||
return event::Status::Captured;
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
|
||||
| Event::Touch(touch::Event::FingerLifted { .. })
|
||||
| Event::Touch(touch::Event::FingerLost { .. }) => {
|
||||
if is_dragging {
|
||||
if let Some(on_release) = on_release.clone() {
|
||||
shell.publish(on_release);
|
||||
}
|
||||
state.is_dragging = false;
|
||||
|
||||
return event::Status::Captured;
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse::Event::CursorMoved { .. })
|
||||
| Event::Touch(touch::Event::FingerMoved { .. }) => {
|
||||
if is_dragging {
|
||||
change();
|
||||
|
||||
return event::Status::Captured;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
event::Status::Ignored
|
||||
}
|
||||
|
||||
/// Draws a [`VerticalSlider`].
|
||||
pub fn draw<T, R>(
|
||||
renderer: &mut R,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
state: &State,
|
||||
value: T,
|
||||
range: &RangeInclusive<T>,
|
||||
style_sheet: &dyn StyleSheet<Style = <R::Theme as StyleSheet>::Style>,
|
||||
style: &<R::Theme as StyleSheet>::Style,
|
||||
) where
|
||||
T: Into<f64> + Copy,
|
||||
R: crate::Renderer,
|
||||
R::Theme: StyleSheet,
|
||||
{
|
||||
let bounds = layout.bounds();
|
||||
let is_mouse_over = bounds.contains(cursor_position);
|
||||
|
||||
let style = if state.is_dragging {
|
||||
style_sheet.dragging(style)
|
||||
} else if is_mouse_over {
|
||||
style_sheet.hovered(style)
|
||||
} else {
|
||||
style_sheet.active(style)
|
||||
};
|
||||
|
||||
let rail_x = bounds.x + (bounds.width / 2.0).round();
|
||||
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: Rectangle {
|
||||
x: rail_x - 1.0,
|
||||
y: bounds.y,
|
||||
width: 2.0,
|
||||
height: bounds.height,
|
||||
},
|
||||
border_radius: 0.0.into(),
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
},
|
||||
style.rail_colors.0,
|
||||
);
|
||||
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: Rectangle {
|
||||
x: rail_x + 1.0,
|
||||
y: bounds.y,
|
||||
width: 2.0,
|
||||
height: bounds.height,
|
||||
},
|
||||
border_radius: 0.0.into(),
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
},
|
||||
Background::Color(style.rail_colors.1),
|
||||
);
|
||||
|
||||
let (handle_width, handle_height, handle_border_radius) = match style
|
||||
.handle
|
||||
.shape
|
||||
{
|
||||
HandleShape::Circle { radius } => (radius * 2.0, radius * 2.0, radius),
|
||||
HandleShape::Rectangle {
|
||||
width,
|
||||
border_radius,
|
||||
} => (f32::from(width), bounds.width, border_radius),
|
||||
};
|
||||
|
||||
let value = value.into() as f32;
|
||||
let (range_start, range_end) = {
|
||||
let (start, end) = range.clone().into_inner();
|
||||
|
||||
(start.into() as f32, end.into() as f32)
|
||||
};
|
||||
|
||||
let handle_offset = if range_start >= range_end {
|
||||
0.0
|
||||
} else {
|
||||
(bounds.height - handle_width) * (value - range_end)
|
||||
/ (range_start - range_end)
|
||||
};
|
||||
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: Rectangle {
|
||||
x: rail_x - (handle_height / 2.0),
|
||||
y: bounds.y + handle_offset.round(),
|
||||
width: handle_height,
|
||||
height: handle_width,
|
||||
},
|
||||
border_radius: handle_border_radius.into(),
|
||||
border_width: style.handle.border_width,
|
||||
border_color: style.handle.border_color,
|
||||
},
|
||||
style.handle.color,
|
||||
);
|
||||
}
|
||||
|
||||
/// Computes the current [`mouse::Interaction`] of a [`VerticalSlider`].
|
||||
pub fn mouse_interaction(
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
state: &State,
|
||||
) -> mouse::Interaction {
|
||||
let bounds = layout.bounds();
|
||||
let is_mouse_over = bounds.contains(cursor_position);
|
||||
|
||||
if state.is_dragging {
|
||||
mouse::Interaction::Grabbing
|
||||
} else if is_mouse_over {
|
||||
mouse::Interaction::Grab
|
||||
} else {
|
||||
mouse::Interaction::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// The local state of a [`VerticalSlider`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct State {
|
||||
is_dragging: bool,
|
||||
}
|
||||
|
||||
impl State {
|
||||
/// Creates a new [`State`].
|
||||
pub fn new() -> State {
|
||||
State::default()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +1,11 @@
|
|||
//! Build window-based GUI applications.
|
||||
mod action;
|
||||
mod event;
|
||||
mod mode;
|
||||
mod redraw_request;
|
||||
mod user_attention;
|
||||
|
||||
pub use action::Action;
|
||||
pub use event::Event;
|
||||
pub use mode::Mode;
|
||||
pub use redraw_request::RedrawRequest;
|
||||
pub use user_attention::UserAttention;
|
||||
|
||||
use crate::core::time::Instant;
|
||||
use crate::core::window::Event;
|
||||
use crate::subscription::{self, Subscription};
|
||||
use crate::time::Instant;
|
||||
|
||||
/// Subscribes to the frames of the window of the running application.
|
||||
///
|
||||
|
|
@ -24,7 +17,7 @@ use crate::time::Instant;
|
|||
/// animations without missing any frames.
|
||||
pub fn frames() -> Subscription<Instant> {
|
||||
subscription::raw_events(|event, _status| match event {
|
||||
crate::Event::Window(Event::RedrawRequested(at)) => Some(at),
|
||||
iced_core::Event::Window(Event::RedrawRequested(at)) => Some(at),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use crate::window::{Mode, UserAttention};
|
||||
use crate::core::window::{Mode, UserAttention};
|
||||
use crate::futures::MaybeSend;
|
||||
|
||||
use iced_futures::MaybeSend;
|
||||
use std::fmt;
|
||||
|
||||
/// An operation to be performed on some window.
|
||||
|
|
|
|||
|
|
@ -1,58 +0,0 @@
|
|||
use crate::time::Instant;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// A window-related event.
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
pub enum Event {
|
||||
/// A window was moved.
|
||||
Moved {
|
||||
/// The new logical x location of the window
|
||||
x: i32,
|
||||
/// The new logical y location of the window
|
||||
y: i32,
|
||||
},
|
||||
|
||||
/// A window was resized.
|
||||
Resized {
|
||||
/// The new logical width of the window
|
||||
width: u32,
|
||||
/// The new logical height of the window
|
||||
height: u32,
|
||||
},
|
||||
|
||||
/// A window redraw was requested.
|
||||
///
|
||||
/// The [`Instant`] contains the current time.
|
||||
RedrawRequested(Instant),
|
||||
|
||||
/// The user has requested for the window to close.
|
||||
///
|
||||
/// Usually, you will want to terminate the execution whenever this event
|
||||
/// occurs.
|
||||
CloseRequested,
|
||||
|
||||
/// A window was focused.
|
||||
Focused,
|
||||
|
||||
/// A window was unfocused.
|
||||
Unfocused,
|
||||
|
||||
/// A file is being hovered over the window.
|
||||
///
|
||||
/// When the user hovers multiple files at once, this event will be emitted
|
||||
/// for each file separately.
|
||||
FileHovered(PathBuf),
|
||||
|
||||
/// A file has beend dropped into the window.
|
||||
///
|
||||
/// When the user drops multiple files at once, this event will be emitted
|
||||
/// for each file separately.
|
||||
FileDropped(PathBuf),
|
||||
|
||||
/// A file was hovered, but has exited the window.
|
||||
///
|
||||
/// There will be a single `FilesHoveredLeft` event triggered even if
|
||||
/// multiple files were hovered.
|
||||
FilesHoveredLeft,
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
/// The mode of a window-based application.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Mode {
|
||||
/// The application appears in its own window.
|
||||
Windowed,
|
||||
|
||||
/// The application takes the whole screen of its current monitor.
|
||||
Fullscreen,
|
||||
|
||||
/// The application is hidden
|
||||
Hidden,
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
use crate::time::Instant;
|
||||
|
||||
/// A request to redraw a window.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum RedrawRequest {
|
||||
/// Redraw the next frame.
|
||||
NextFrame,
|
||||
|
||||
/// Redraw at the given time.
|
||||
At(Instant),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
#[test]
|
||||
fn ordering() {
|
||||
let now = Instant::now();
|
||||
let later = now + Duration::from_millis(10);
|
||||
|
||||
assert_eq!(RedrawRequest::NextFrame, RedrawRequest::NextFrame);
|
||||
assert_eq!(RedrawRequest::At(now), RedrawRequest::At(now));
|
||||
|
||||
assert!(RedrawRequest::NextFrame < RedrawRequest::At(now));
|
||||
assert!(RedrawRequest::At(now) > RedrawRequest::NextFrame);
|
||||
assert!(RedrawRequest::At(now) < RedrawRequest::At(later));
|
||||
assert!(RedrawRequest::At(later) > RedrawRequest::At(now));
|
||||
|
||||
assert!(RedrawRequest::NextFrame <= RedrawRequest::NextFrame);
|
||||
assert!(RedrawRequest::NextFrame <= RedrawRequest::At(now));
|
||||
assert!(RedrawRequest::At(now) >= RedrawRequest::NextFrame);
|
||||
assert!(RedrawRequest::At(now) <= RedrawRequest::At(now));
|
||||
assert!(RedrawRequest::At(now) <= RedrawRequest::At(later));
|
||||
assert!(RedrawRequest::At(later) >= RedrawRequest::At(now));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
/// The type of user attention to request.
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - **X11:** Sets the WM's `XUrgencyHint`. No distinction between [`Critical`] and [`Informational`].
|
||||
///
|
||||
/// [`Critical`]: Self::Critical
|
||||
/// [`Informational`]: Self::Informational
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum UserAttention {
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - **macOS:** Bounces the dock icon until the application is in focus.
|
||||
/// - **Windows:** Flashes both the window and the taskbar button until the application is in focus.
|
||||
Critical,
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - **macOS:** Bounces the dock icon once.
|
||||
/// - **Windows:** Flashes the taskbar button until the application is in focus.
|
||||
Informational,
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue