diff options
Diffstat (limited to 'core')
36 files changed, 4120 insertions, 0 deletions
| diff --git a/core/Cargo.toml b/core/Cargo.toml index 7ccb7b7a..9edc20f6 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -10,6 +10,7 @@ repository = "https://github.com/iced-rs/iced"  [dependencies]  bitflags = "1.2"  thiserror = "1" +twox-hash = { version = "1.5", default-features = false }  [dependencies.palette]  version = "0.6" diff --git a/core/src/clipboard.rs b/core/src/clipboard.rs new file mode 100644 index 00000000..081b4004 --- /dev/null +++ b/core/src/clipboard.rs @@ -0,0 +1,23 @@ +//! Access the clipboard. + +/// 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) {} +} diff --git a/core/src/element.rs b/core/src/element.rs new file mode 100644 index 00000000..98c53737 --- /dev/null +++ b/core/src/element.rs @@ -0,0 +1,608 @@ +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: +    /// +    /// ```no_run +    /// # mod counter { +    /// #     #[derive(Debug, Clone, Copy)] +    /// #     pub enum Message {} +    /// #     pub struct Counter; +    /// # +    /// #     impl Counter { +    /// #         pub fn view( +    /// #             &self, +    /// #         ) -> iced_core::Element<Message, iced_core::renderer::Null> { +    /// #             unimplemented!() +    /// #         } +    /// #     } +    /// # } +    /// # +    /// # mod iced { +    /// #     pub use iced_core::renderer::Null as Renderer; +    /// #     pub use iced_core::Element; +    /// # +    /// #     pub mod widget { +    /// #         pub struct Row<Message> { +    /// #             _t: std::marker::PhantomData<Message>, +    /// #         } +    /// # +    /// #         impl<Message> Row<Message> { +    /// #             pub fn new() -> Self { +    /// #                 unimplemented!() +    /// #             } +    /// # +    /// #             pub fn spacing(mut self, _: u32) -> Self { +    /// #                 unimplemented!() +    /// #             } +    /// # +    /// #             pub fn push( +    /// #                 mut self, +    /// #                 _: iced_core::Element<Message, iced_core::renderer::Null>, +    /// #             ) -> Self { +    /// #                 unimplemented!() +    /// #             } +    /// #         } +    /// #     } +    /// # } +    /// # +    /// use counter::Counter; +    /// +    /// use iced::widget::Row; +    /// use iced::{Element, Renderer}; +    /// +    /// struct ManyCounters { +    ///     counters: Vec<Counter>, +    /// } +    /// +    /// #[derive(Debug, Clone, Copy)] +    /// pub enum Message { +    ///     Counter(usize, counter::Message), +    /// } +    /// +    /// impl ManyCounters { +    ///     pub fn view(&mut self) -> Row<Message> { +    ///         // 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) +    } +} diff --git a/core/src/event.rs b/core/src/event.rs new file mode 100644 index 00000000..953cd73f --- /dev/null +++ b/core/src/event.rs @@ -0,0 +1,78 @@ +//! 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_core::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, +        } +    } +} diff --git a/core/src/hasher.rs b/core/src/hasher.rs new file mode 100644 index 00000000..fa52f16d --- /dev/null +++ b/core/src/hasher.rs @@ -0,0 +1,13 @@ +/// 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() +    } +} diff --git a/core/src/image.rs b/core/src/image.rs new file mode 100644 index 00000000..70fbade0 --- /dev/null +++ b/core/src/image.rs @@ -0,0 +1,174 @@ +//! 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); +} diff --git a/core/src/layout.rs b/core/src/layout.rs new file mode 100644 index 00000000..04954fb9 --- /dev/null +++ b/core/src/layout.rs @@ -0,0 +1,65 @@ +//! 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, +            ) +        }) +    } +} diff --git a/core/src/layout/DRUID_LICENSE b/core/src/layout/DRUID_LICENSE new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/core/src/layout/DRUID_LICENSE @@ -0,0 +1,202 @@ + +                                 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. diff --git a/core/src/layout/flex.rs b/core/src/layout/flex.rs new file mode 100644 index 00000000..5d70c2fc --- /dev/null +++ b/core/src/layout/flex.rs @@ -0,0 +1,232 @@ +//! 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) +} diff --git a/core/src/layout/limits.rs b/core/src/layout/limits.rs new file mode 100644 index 00000000..5d3c1556 --- /dev/null +++ b/core/src/layout/limits.rs @@ -0,0 +1,163 @@ +#![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), +        ) +    } +} diff --git a/core/src/layout/node.rs b/core/src/layout/node.rs new file mode 100644 index 00000000..e0c7dcb2 --- /dev/null +++ b/core/src/layout/node.rs @@ -0,0 +1,91 @@ +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 +        } +    } +} diff --git a/core/src/lib.rs b/core/src/lib.rs index 1e4f0411..5bdcee6a 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -25,33 +25,57 @@  #![forbid(unsafe_code, rust_2018_idioms)]  #![allow(clippy::inherent_to_string, clippy::type_complexity)]  pub mod alignment; +pub mod clipboard; +pub mod event;  pub mod font;  pub mod gradient; +pub mod image;  pub mod keyboard; +pub mod layout;  pub mod mouse; +pub mod overlay; +pub mod renderer; +pub mod svg; +pub mod text;  pub mod time; +pub mod touch; +pub mod widget; +pub mod window;  mod background;  mod color;  mod content_fit; +mod element; +mod hasher;  mod length;  mod padding;  mod pixels;  mod point;  mod rectangle; +mod shell;  mod size;  mod vector;  pub use alignment::Alignment;  pub use background::Background; +pub use clipboard::Clipboard;  pub use color::Color;  pub use content_fit::ContentFit; +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 length::Length; +pub use overlay::Overlay;  pub use padding::Padding;  pub use pixels::Pixels;  pub use point::Point;  pub use rectangle::Rectangle; +pub use renderer::Renderer; +pub use shell::Shell;  pub use size::Size; +pub use text::Text;  pub use vector::Vector; +pub use widget::Widget; diff --git a/core/src/mouse.rs b/core/src/mouse.rs index 48214f65..0c405ce6 100644 --- a/core/src/mouse.rs +++ b/core/src/mouse.rs @@ -1,8 +1,11 @@  //! Handle mouse events. +pub mod click; +  mod button;  mod event;  mod interaction;  pub use button::Button; +pub use click::Click;  pub use event::{Event, ScrollDelta};  pub use interaction::Interaction; diff --git a/core/src/mouse/click.rs b/core/src/mouse/click.rs new file mode 100644 index 00000000..4a7d796c --- /dev/null +++ b/core/src/mouse/click.rs @@ -0,0 +1,76 @@ +//! 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) +    } +} diff --git a/core/src/overlay.rs b/core/src/overlay.rs new file mode 100644 index 00000000..b9f3c735 --- /dev/null +++ b/core/src/overlay.rs @@ -0,0 +1,122 @@ +//! Display interactive elements on top of other widgets. +mod element; +mod group; + +pub use element::Element; +pub use group::Group; + +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()) +} diff --git a/core/src/overlay/element.rs b/core/src/overlay/element.rs new file mode 100644 index 00000000..237d25d1 --- /dev/null +++ b/core/src/overlay/element.rs @@ -0,0 +1,270 @@ +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) +    } +} diff --git a/core/src/overlay/group.rs b/core/src/overlay/group.rs new file mode 100644 index 00000000..0c48df34 --- /dev/null +++ b/core/src/overlay/group.rs @@ -0,0 +1,172 @@ +use crate::event; +use crate::layout; +use crate::mouse; +use crate::overlay; +use crate::renderer; +use crate::widget; +use crate::{Clipboard, Event, Layout, Overlay, Point, Rectangle, Shell, Size}; + +/// 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() +    } +} diff --git a/core/src/renderer.rs b/core/src/renderer.rs new file mode 100644 index 00000000..d6247e39 --- /dev/null +++ b/core/src/renderer.rs @@ -0,0 +1,99 @@ +//! 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, +        } +    } +} diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs new file mode 100644 index 00000000..d93338ae --- /dev/null +++ b/core/src/renderer/null.rs @@ -0,0 +1,82 @@ +use crate::renderer::{self, Renderer}; +use crate::text::{self, Text}; +use crate::{Background, Font, Point, Rectangle, Size, 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 = (); + +    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>) {} +} diff --git a/core/src/shell.rs b/core/src/shell.rs new file mode 100644 index 00000000..74a5c616 --- /dev/null +++ b/core/src/shell.rs @@ -0,0 +1,108 @@ +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; +    } +} diff --git a/core/src/svg.rs b/core/src/svg.rs new file mode 100644 index 00000000..9b98877a --- /dev/null +++ b/core/src/svg.rs @@ -0,0 +1,89 @@ +//! 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); +} diff --git a/core/src/text.rs b/core/src/text.rs new file mode 100644 index 00000000..4c72abc3 --- /dev/null +++ b/core/src/text.rs @@ -0,0 +1,111 @@ +//! 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>); +} diff --git a/core/src/touch.rs b/core/src/touch.rs new file mode 100644 index 00000000..18120644 --- /dev/null +++ b/core/src/touch.rs @@ -0,0 +1,23 @@ +//! 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); diff --git a/core/src/widget.rs b/core/src/widget.rs new file mode 100644 index 00000000..70e2c2d9 --- /dev/null +++ b/core/src/widget.rs @@ -0,0 +1,145 @@ +//! Create custom widgets and operate on them. +pub mod operation; +pub mod text; +pub mod tree; + +mod id; + +pub use id::Id; +pub use operation::Operation; +pub use text::Text; +pub use tree::Tree; + +use crate::event::{self, Event}; +use crate::layout::{self, Layout}; +use crate::mouse; +use crate::overlay; +use crate::renderer; +use crate::{Clipboard, 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 +    } +} diff --git a/core/src/widget/id.rs b/core/src/widget/id.rs new file mode 100644 index 00000000..ae739bb7 --- /dev/null +++ b/core/src/widget/id.rs @@ -0,0 +1,43 @@ +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)] +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); +    } +} diff --git a/core/src/widget/operation.rs b/core/src/widget/operation.rs new file mode 100644 index 00000000..53688a21 --- /dev/null +++ b/core/src/widget/operation.rs @@ -0,0 +1,112 @@ +//! 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), +    } +} diff --git a/core/src/widget/operation/focusable.rs b/core/src/widget/operation/focusable.rs new file mode 100644 index 00000000..312e4894 --- /dev/null +++ b/core/src/widget/operation/focusable.rs @@ -0,0 +1,203 @@ +//! 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 } +} diff --git a/core/src/widget/operation/scrollable.rs b/core/src/widget/operation/scrollable.rs new file mode 100644 index 00000000..3b20631f --- /dev/null +++ b/core/src/widget/operation/scrollable.rs @@ -0,0 +1,54 @@ +//! 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 }; +} diff --git a/core/src/widget/operation/text_input.rs b/core/src/widget/operation/text_input.rs new file mode 100644 index 00000000..4c773e99 --- /dev/null +++ b/core/src/widget/operation/text_input.rs @@ -0,0 +1,131 @@ +//! 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 } +} diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs new file mode 100644 index 00000000..485bb542 --- /dev/null +++ b/core/src/widget/text.rs @@ -0,0 +1,277 @@ +//! 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::{ +    Color, Element, Layout, Length, Pixels, Point, Rectangle, Size, Widget, +}; + +use std::borrow::Cow; + +/// A paragraph of text. +#[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 { +        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, Renderer> From<&'a str> for Text<'a, Renderer> +where +    Renderer: text::Renderer, +    Renderer::Theme: StyleSheet, +{ +    fn from(content: &'a str) -> Self { +        Self::new(content) +    } +} + +impl<'a, Message, Renderer> From<&'a str> for Element<'a, Message, Renderer> +where +    Renderer: text::Renderer + 'a, +    Renderer::Theme: StyleSheet, +{ +    fn from(content: &'a str) -> Self { +        Text::from(content).into() +    } +} + +/// The style sheet of some text. +pub trait StyleSheet { +    /// The supported style of the [`StyleSheet`]. +    type Style: Default + Copy; + +    /// Produces the [`Appearance`] of some text. +    fn appearance(&self, style: Self::Style) -> Appearance; +} + +/// The apperance of some text. +#[derive(Debug, Clone, Copy, Default)] +pub struct Appearance { +    /// The [`Color`] of the text. +    /// +    /// The default, `None`, means using the inherited color. +    pub color: Option<Color>, +} diff --git a/core/src/widget/tree.rs b/core/src/widget/tree.rs new file mode 100644 index 00000000..0af40c33 --- /dev/null +++ b/core/src/widget/tree.rs @@ -0,0 +1,187 @@ +//! 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"), +        } +    } +} diff --git a/core/src/window.rs b/core/src/window.rs new file mode 100644 index 00000000..d829a4b4 --- /dev/null +++ b/core/src/window.rs @@ -0,0 +1,10 @@ +//! Build window-based GUI applications. +mod event; +mod mode; +mod redraw_request; +mod user_attention; + +pub use event::Event; +pub use mode::Mode; +pub use redraw_request::RedrawRequest; +pub use user_attention::UserAttention; diff --git a/core/src/window/event.rs b/core/src/window/event.rs new file mode 100644 index 00000000..e2fb5e66 --- /dev/null +++ b/core/src/window/event.rs @@ -0,0 +1,58 @@ +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, +} diff --git a/core/src/window/mode.rs b/core/src/window/mode.rs new file mode 100644 index 00000000..fdce8e23 --- /dev/null +++ b/core/src/window/mode.rs @@ -0,0 +1,12 @@ +/// 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, +} diff --git a/core/src/window/redraw_request.rs b/core/src/window/redraw_request.rs new file mode 100644 index 00000000..3b4f0fd3 --- /dev/null +++ b/core/src/window/redraw_request.rs @@ -0,0 +1,38 @@ +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)); +    } +} diff --git a/core/src/window/user_attention.rs b/core/src/window/user_attention.rs new file mode 100644 index 00000000..b03dfeef --- /dev/null +++ b/core/src/window/user_attention.rs @@ -0,0 +1,21 @@ +/// 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, +} | 
