diff options
author | 2021-11-07 15:15:33 +0700 | |
---|---|---|
committer | 2021-11-07 15:15:33 +0700 | |
commit | eafad00af2a9bae9f3ed8124e2a6f6e59ee5d253 (patch) | |
tree | 76413948c9c9723075189d51d4c2e02c0f8fdd23 /native | |
parent | 61c747b53589d98f477fea95f85d2ea5349666d3 (diff) | |
parent | 07b5097bc92ced376d09115d787ff1d2ebe00836 (diff) | |
download | iced-eafad00af2a9bae9f3ed8124e2a6f6e59ee5d253.tar.gz iced-eafad00af2a9bae9f3ed8124e2a6f6e59ee5d253.tar.bz2 iced-eafad00af2a9bae9f3ed8124e2a6f6e59ee5d253.zip |
Merge pull request #1110 from iced-rs/remove-renderer-traits
Reduce the surface of the `Renderer` APIs
Diffstat (limited to 'native')
42 files changed, 2649 insertions, 1937 deletions
diff --git a/native/Cargo.toml b/native/Cargo.toml index a3134ef4..5de99b2e 100644 --- a/native/Cargo.toml +++ b/native/Cargo.toml @@ -23,3 +23,7 @@ path = "../core" version = "0.3" path = "../futures" features = ["thread-pool"] + +[dependencies.iced_style] +version = "0.3" +path = "../style" diff --git a/native/src/element.rs b/native/src/element.rs index 5c84a388..ee404a1c 100644 --- a/native/src/element.rs +++ b/native/src/element.rs @@ -1,6 +1,8 @@ use crate::event::{self, Event}; use crate::layout; +use crate::mouse; use crate::overlay; +use crate::renderer; use crate::{ Clipboard, Color, Hasher, Layout, Length, Point, Rectangle, Widget, }; @@ -77,7 +79,7 @@ where /// /// ``` /// # mod counter { - /// # type Text = iced_native::Text<iced_native::renderer::Null>; + /// # type Text = iced_native::widget::Text<iced_native::renderer::Null>; /// # /// # #[derive(Debug, Clone, Copy)] /// # pub enum Message {} @@ -104,7 +106,8 @@ where /// # pub enum Message { /// # Counter(usize, counter::Message) /// # } - /// use iced_native::{Element, Row}; + /// use iced_native::Element; + /// use iced_native::widget::Row; /// use iced_wgpu::Renderer; /// /// impl ManyCounters { @@ -189,7 +192,7 @@ where ) -> Element<'a, Message, Renderer> where Message: 'static, - Renderer: 'a + layout::Debugger, + Renderer: 'a, { Element { widget: Box::new(Explain::new(self, color.into())), @@ -241,13 +244,24 @@ where pub fn draw( &self, renderer: &mut Renderer, - defaults: &Renderer::Defaults, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + self.widget + .draw(renderer, style, layout, cursor_position, viewport) + } + + /// Returns the current [`mouse::Interaction`] of the [`Element`]. + pub fn mouse_interaction( + &self, layout: Layout<'_>, cursor_position: Point, viewport: &Rectangle, - ) -> Renderer::Output { + ) -> mouse::Interaction { self.widget - .draw(renderer, defaults, layout, cursor_position, viewport) + .mouse_interaction(layout, cursor_position, viewport) } /// Computes the _layout_ hash of the [`Element`]. @@ -336,13 +350,23 @@ where fn draw( &self, renderer: &mut Renderer, - defaults: &Renderer::Defaults, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + self.widget + .draw(renderer, style, layout, cursor_position, viewport) + } + + fn mouse_interaction( + &self, layout: Layout<'_>, cursor_position: Point, viewport: &Rectangle, - ) -> Renderer::Output { + ) -> mouse::Interaction { self.widget - .draw(renderer, defaults, layout, cursor_position, viewport) + .mouse_interaction(layout, cursor_position, viewport) } fn hash_layout(&self, state: &mut Hasher) { @@ -378,7 +402,7 @@ where impl<'a, Message, Renderer> Widget<Message, Renderer> for Explain<'a, Message, Renderer> where - Renderer: crate::Renderer + layout::Debugger, + Renderer: crate::Renderer, { fn width(&self) -> Length { self.element.widget.width() @@ -418,19 +442,51 @@ where fn draw( &self, renderer: &mut Renderer, - defaults: &Renderer::Defaults, + style: &renderer::Style, layout: Layout<'_>, cursor_position: Point, viewport: &Rectangle, - ) -> Renderer::Output { - renderer.explain( - defaults, - self.element.widget.as_ref(), + ) { + 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, + }, + Color::TRANSPARENT, + ); + + for child in layout.children() { + explain_layout(renderer, color, child); + } + } + + self.element.widget.draw( + renderer, + style, layout, cursor_position, viewport, - self.color, - ) + ); + + explain_layout(renderer, self.color, layout); + } + + fn mouse_interaction( + &self, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) -> mouse::Interaction { + self.element + .widget + .mouse_interaction(layout, cursor_position, viewport) } fn hash_layout(&self, state: &mut Hasher) { diff --git a/native/src/image.rs b/native/src/image.rs new file mode 100644 index 00000000..00379417 --- /dev/null +++ b/native/src/image.rs @@ -0,0 +1,124 @@ +//! Load and draw raster graphics. +use crate::{Hasher, Rectangle}; + +use std::hash::{Hash, Hasher as _}; +use std::path::PathBuf; +use std::sync::Arc; + +/// An [`Image`] handle. +#[derive(Debug, Clone)] +pub struct Handle { + id: u64, + data: Arc<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 BGRA + /// pixels. + /// + /// This is useful if you have already decoded your image. + pub fn from_pixels(width: u32, height: u32, pixels: Vec<u8>) -> Handle { + Self::from_data(Data::Pixels { + width, + height, + 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: Vec<u8>) -> Handle { + Self::from_data(Data::Bytes(bytes)) + } + + 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 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); + } +} + +/// The data of an [`Image`]. +#[derive(Clone, Hash)] +pub enum Data { + /// File data + Path(PathBuf), + + /// In-memory data + Bytes(Vec<u8>), + + /// Decoded image pixels in BGRA format. + Pixels { + /// The width of the image. + width: u32, + /// The height of the image. + height: u32, + /// The pixels. + pixels: Vec<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(...)"), + Data::Pixels { width, height, .. } => { + write!(f, "Pixels({} * {})", width, height) + } + } + } +} + +/// A [`Renderer`] that can render raster graphics. +/// +/// [renderer]: crate::renderer +pub trait Renderer: crate::Renderer { + /// Returns the dimensions of an image for the given [`Handle`]. + fn dimensions(&self, handle: &Handle) -> (u32, u32); + + /// Draws an image with the given [`Handle`] and inside the provided + /// `bounds`. + fn draw(&mut self, handle: Handle, bounds: Rectangle); +} diff --git a/native/src/layout.rs b/native/src/layout.rs index b4b4a021..04954fb9 100644 --- a/native/src/layout.rs +++ b/native/src/layout.rs @@ -1,11 +1,9 @@ //! Position your widgets properly. -mod debugger; mod limits; mod node; pub mod flex; -pub use debugger::Debugger; pub use limits::Limits; pub use node::Node; diff --git a/native/src/layout/debugger.rs b/native/src/layout/debugger.rs deleted file mode 100644 index 0759613f..00000000 --- a/native/src/layout/debugger.rs +++ /dev/null @@ -1,24 +0,0 @@ -use crate::{Color, Layout, Point, Rectangle, Renderer, Widget}; - -/// A renderer able to graphically explain a [`Layout`]. -pub trait Debugger: Renderer { - /// Explains the [`Layout`] of an [`Element`] for debugging purposes. - /// - /// This will be called when [`Element::explain`] has been used. It should - /// _explain_ the given [`Layout`] graphically. - /// - /// A common approach consists in recursively rendering the bounds of the - /// [`Layout`] and its children. - /// - /// [`Element`]: crate::Element - /// [`Element::explain`]: crate::Element::explain - fn explain<Message>( - &mut self, - defaults: &Self::Defaults, - widget: &dyn Widget<Message, Self>, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - color: Color, - ) -> Self::Output; -} diff --git a/native/src/lib.rs b/native/src/lib.rs index 170a588b..51b232e9 100644 --- a/native/src/lib.rs +++ b/native/src/lib.rs @@ -36,6 +36,7 @@ pub mod clipboard; pub mod command; pub mod event; +pub mod image; pub mod keyboard; pub mod layout; pub mod mouse; @@ -43,6 +44,8 @@ pub mod overlay; pub mod program; pub mod renderer; pub mod subscription; +pub mod svg; +pub mod text; pub mod touch; pub mod widget; pub mod window; @@ -84,4 +87,4 @@ pub use renderer::Renderer; pub use runtime::Runtime; pub use subscription::Subscription; pub use user_interface::{Cache, UserInterface}; -pub use widget::*; +pub use widget::Widget; diff --git a/native/src/overlay.rs b/native/src/overlay.rs index 84145e7f..1ac3cea5 100644 --- a/native/src/overlay.rs +++ b/native/src/overlay.rs @@ -8,7 +8,9 @@ pub use menu::Menu; use crate::event::{self, Event}; use crate::layout; -use crate::{Clipboard, Hasher, Layout, Point, Size}; +use crate::mouse; +use crate::renderer; +use crate::{Clipboard, Hasher, Layout, Point, Rectangle, Size}; /// An interactive component that can be displayed on top of other widgets. pub trait Overlay<Message, Renderer> @@ -32,10 +34,10 @@ where fn draw( &self, renderer: &mut Renderer, - defaults: &Renderer::Defaults, + style: &renderer::Style, layout: Layout<'_>, cursor_position: Point, - ) -> Renderer::Output; + ); /// Computes the _layout_ hash of the [`Overlay`]. /// @@ -73,4 +75,16 @@ where ) -> event::Status { event::Status::Ignored } + + /// Returns the current [`mouse::Interaction`] of the [`Widget`]. + /// + /// By default, it returns [`mouse::Interaction::Idle`]. + fn mouse_interaction( + &self, + _layout: Layout<'_>, + _cursor_position: Point, + _viewport: &Rectangle, + ) -> mouse::Interaction { + mouse::Interaction::Idle + } } diff --git a/native/src/overlay/element.rs b/native/src/overlay/element.rs index e4819037..f418a518 100644 --- a/native/src/overlay/element.rs +++ b/native/src/overlay/element.rs @@ -2,7 +2,9 @@ pub use crate::Overlay; use crate::event::{self, Event}; use crate::layout; -use crate::{Clipboard, Hasher, Layout, Point, Size, Vector}; +use crate::mouse; +use crate::renderer; +use crate::{Clipboard, Hasher, Layout, Point, Rectangle, Size, Vector}; /// A generic [`Overlay`]. #[allow(missing_debug_implementations)] @@ -67,16 +69,26 @@ where ) } + /// Returns the current [`mouse::Interaction`] of the [`Element`]. + pub fn mouse_interaction( + &self, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) -> mouse::Interaction { + self.overlay + .mouse_interaction(layout, cursor_position, viewport) + } + /// Draws the [`Element`] and its children using the given [`Layout`]. pub fn draw( &self, renderer: &mut Renderer, - defaults: &Renderer::Defaults, + style: &renderer::Style, layout: Layout<'_>, cursor_position: Point, - ) -> Renderer::Output { - self.overlay - .draw(renderer, defaults, layout, cursor_position) + ) { + self.overlay.draw(renderer, style, layout, cursor_position) } /// Computes the _layout_ hash of the [`Element`]. @@ -139,15 +151,24 @@ where event_status } + fn mouse_interaction( + &self, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) -> mouse::Interaction { + self.content + .mouse_interaction(layout, cursor_position, viewport) + } + fn draw( &self, renderer: &mut Renderer, - defaults: &Renderer::Defaults, + style: &renderer::Style, layout: Layout<'_>, cursor_position: Point, - ) -> Renderer::Output { - self.content - .draw(renderer, defaults, layout, cursor_position) + ) { + self.content.draw(renderer, style, layout, cursor_position) } fn hash_layout(&self, state: &mut Hasher, position: Point) { diff --git a/native/src/overlay/menu.rs b/native/src/overlay/menu.rs index f62dcb46..ee3bee6e 100644 --- a/native/src/overlay/menu.rs +++ b/native/src/overlay/menu.rs @@ -1,20 +1,24 @@ //! Build and show dropdown menus. -use crate::container; +use crate::alignment; use crate::event::{self, Event}; use crate::layout; use crate::mouse; use crate::overlay; -use crate::scrollable; -use crate::text; +use crate::renderer; +use crate::text::{self, Text}; use crate::touch; +use crate::widget::scrollable::{self, Scrollable}; +use crate::widget::Container; use crate::{ - Clipboard, Container, Element, Hasher, Layout, Length, Padding, Point, - Rectangle, Scrollable, Size, Vector, Widget, + Clipboard, Color, Element, Hasher, Layout, Length, Padding, Point, + Rectangle, Size, Vector, Widget, }; +pub use iced_style::menu::Style; + /// A list of selectable options. #[allow(missing_debug_implementations)] -pub struct Menu<'a, T, Renderer: self::Renderer> { +pub struct Menu<'a, T, Renderer: text::Renderer> { state: &'a mut State, options: &'a [T], hovered_option: &'a mut Option<usize>, @@ -23,13 +27,13 @@ pub struct Menu<'a, T, Renderer: self::Renderer> { padding: Padding, text_size: Option<u16>, font: Renderer::Font, - style: <Renderer as self::Renderer>::Style, + style: Style, } impl<'a, T, Renderer> Menu<'a, T, Renderer> where T: ToString + Clone, - Renderer: self::Renderer + 'a, + Renderer: text::Renderer + 'a, { /// Creates a new [`Menu`] with the given [`State`], a list of options, and /// the message to produced when an option is selected. @@ -77,10 +81,7 @@ where } /// Sets the style of the [`Menu`]. - pub fn style( - mut self, - style: impl Into<<Renderer as self::Renderer>::Style>, - ) -> Self { + pub fn style(mut self, style: impl Into<Style>) -> Self { self.style = style.into(); self } @@ -116,14 +117,14 @@ impl State { } } -struct Overlay<'a, Message, Renderer: self::Renderer> { +struct Overlay<'a, Message, Renderer: text::Renderer> { container: Container<'a, Message, Renderer>, width: u16, target_height: f32, - style: <Renderer as self::Renderer>::Style, + style: Style, } -impl<'a, Message, Renderer: self::Renderer> Overlay<'a, Message, Renderer> +impl<'a, Message, Renderer: text::Renderer> Overlay<'a, Message, Renderer> where Message: 'a, Renderer: 'a, @@ -168,7 +169,7 @@ where impl<'a, Message, Renderer> crate::Overlay<Message, Renderer> for Overlay<'a, Message, Renderer> where - Renderer: self::Renderer, + Renderer: text::Renderer, { fn layout( &self, @@ -233,45 +234,55 @@ where ) } + fn mouse_interaction( + &self, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) -> mouse::Interaction { + self.container + .mouse_interaction(layout, cursor_position, viewport) + } + fn draw( &self, renderer: &mut Renderer, - defaults: &Renderer::Defaults, + style: &renderer::Style, layout: Layout<'_>, cursor_position: Point, - ) -> Renderer::Output { - let primitives = self.container.draw( - renderer, - defaults, - layout, - cursor_position, - &layout.bounds(), + ) { + let bounds = layout.bounds(); + + renderer.fill_quad( + renderer::Quad { + bounds, + border_color: self.style.border_color, + border_width: self.style.border_width, + border_radius: 0.0, + }, + self.style.background, ); - renderer.decorate( - layout.bounds(), - cursor_position, - &self.style, - primitives, - ) + self.container + .draw(renderer, style, layout, cursor_position, &bounds); } } -struct List<'a, T, Renderer: self::Renderer> { +struct List<'a, T, Renderer: text::Renderer> { options: &'a [T], hovered_option: &'a mut Option<usize>, last_selection: &'a mut Option<T>, padding: Padding, text_size: Option<u16>, font: Renderer::Font, - style: <Renderer as self::Renderer>::Style, + style: Style, } -impl<'a, T, Message, Renderer: self::Renderer> Widget<Message, Renderer> +impl<'a, T, Message, Renderer> Widget<Message, Renderer> for List<'a, T, Renderer> where T: Clone + ToString, - Renderer: self::Renderer, + Renderer: text::Renderer, { fn width(&self) -> Length { Length::Fill @@ -376,65 +387,84 @@ where event::Status::Ignored } - fn draw( + fn mouse_interaction( &self, - renderer: &mut Renderer, - _defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, - viewport: &Rectangle, - ) -> Renderer::Output { - self::Renderer::draw( - renderer, - layout.bounds(), - cursor_position, - viewport, - self.options, - *self.hovered_option, - self.padding, - self.text_size.unwrap_or(renderer.default_size()), - self.font, - &self.style, - ) + _viewport: &Rectangle, + ) -> mouse::Interaction { + let is_mouse_over = layout.bounds().contains(cursor_position); + + if is_mouse_over { + mouse::Interaction::Pointer + } else { + mouse::Interaction::default() + } } -} -/// The renderer of a [`Menu`]. -/// -/// Your [renderer] will need to implement this trait before being -/// able to use a [`Menu`] in your user interface. -/// -/// [renderer]: crate::renderer -pub trait Renderer: - scrollable::Renderer + container::Renderer + text::Renderer -{ - /// The [`Menu`] style supported by this renderer. - type Style: Default + Clone; + fn draw( + &self, + renderer: &mut Renderer, + _style: &renderer::Style, + layout: Layout<'_>, + _cursor_position: Point, + viewport: &Rectangle, + ) { + let bounds = layout.bounds(); - /// Decorates a the list of options of a [`Menu`]. - /// - /// This method can be used to draw a background for the [`Menu`]. - fn decorate( - &mut self, - bounds: Rectangle, - cursor_position: Point, - style: &<Self as Renderer>::Style, - primitive: Self::Output, - ) -> Self::Output; + let text_size = self.text_size.unwrap_or(renderer.default_size()); + let option_height = (text_size + self.padding.vertical()) as usize; + + let offset = viewport.y - bounds.y; + let start = (offset / option_height as f32) as usize; + let end = + ((offset + viewport.height) / option_height as f32).ceil() as usize; + + let visible_options = &self.options[start..end.min(self.options.len())]; + + for (i, option) in visible_options.iter().enumerate() { + let i = start + i; + let is_selected = *self.hovered_option == Some(i); + + let bounds = Rectangle { + x: bounds.x, + y: bounds.y + (option_height * i) as f32, + width: bounds.width, + height: f32::from(text_size + self.padding.vertical()), + }; + + if is_selected { + renderer.fill_quad( + renderer::Quad { + bounds, + border_color: Color::TRANSPARENT, + border_width: 0.0, + border_radius: 0.0, + }, + self.style.selected_background, + ); + } - /// Draws the list of options of a [`Menu`]. - fn draw<T: ToString>( - &mut self, - bounds: Rectangle, - cursor_position: Point, - viewport: &Rectangle, - options: &[T], - hovered_option: Option<usize>, - padding: Padding, - text_size: u16, - font: Self::Font, - style: &<Self as Renderer>::Style, - ) -> Self::Output; + renderer.fill_text(Text { + content: &option.to_string(), + bounds: Rectangle { + x: bounds.x + self.padding.left as f32, + y: bounds.center_y(), + width: f32::INFINITY, + ..bounds + }, + size: f32::from(text_size), + font: self.font, + color: if is_selected { + self.style.selected_text_color + } else { + self.style.text_color + }, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Center, + }); + } + } } impl<'a, T, Message, Renderer> Into<Element<'a, Message, Renderer>> @@ -442,7 +472,7 @@ impl<'a, T, Message, Renderer> Into<Element<'a, Message, Renderer>> where T: ToString + Clone, Message: 'a, - Renderer: 'a + self::Renderer, + Renderer: 'a + text::Renderer, { fn into(self) -> Element<'a, Message, Renderer> { Element::new(self) diff --git a/native/src/program/state.rs b/native/src/program/state.rs index 3f5f6069..26c0eb21 100644 --- a/native/src/program/state.rs +++ b/native/src/program/state.rs @@ -1,5 +1,6 @@ +use crate::mouse; use crate::{ - Cache, Clipboard, Command, Debug, Event, Point, Program, Renderer, Size, + Cache, Clipboard, Command, Debug, Event, Point, Program, Size, UserInterface, }; @@ -12,9 +13,9 @@ where { program: P, cache: Option<Cache>, - primitive: <P::Renderer as Renderer>::Output, queued_events: Vec<Event>, queued_messages: Vec<P::Message>, + mouse_interaction: mouse::Interaction, } impl<P> State<P> @@ -26,11 +27,10 @@ where pub fn new( mut program: P, bounds: Size, - cursor_position: Point, renderer: &mut P::Renderer, debug: &mut Debug, ) -> Self { - let mut user_interface = build_user_interface( + let user_interface = build_user_interface( &mut program, Cache::default(), renderer, @@ -38,18 +38,14 @@ where debug, ); - debug.draw_started(); - let primitive = user_interface.draw(renderer, cursor_position); - debug.draw_finished(); - let cache = Some(user_interface.into_cache()); State { program, cache, - primitive, queued_events: Vec::new(), queued_messages: Vec::new(), + mouse_interaction: mouse::Interaction::Idle, } } @@ -58,11 +54,6 @@ where &self.program } - /// Returns a reference to the current rendering primitive of the [`State`]. - pub fn primitive(&self) -> &<P::Renderer as Renderer>::Output { - &self.primitive - } - /// Queues an event in the [`State`] for processing during an [`update`]. /// /// [`update`]: Self::update @@ -82,6 +73,11 @@ where self.queued_events.is_empty() && self.queued_messages.is_empty() } + /// Returns the current [`mouse::Interaction`] of the [`State`]. + pub fn mouse_interaction(&self) -> mouse::Interaction { + self.mouse_interaction + } + /// Processes all the queued events and messages, rebuilding and redrawing /// the widgets of the linked [`Program`] if necessary. /// @@ -120,7 +116,8 @@ where if messages.is_empty() { debug.draw_started(); - self.primitive = user_interface.draw(renderer, cursor_position); + self.mouse_interaction = + user_interface.draw(renderer, cursor_position); debug.draw_finished(); self.cache = Some(user_interface.into_cache()); @@ -151,7 +148,8 @@ where ); debug.draw_started(); - self.primitive = user_interface.draw(renderer, cursor_position); + self.mouse_interaction = + user_interface.draw(renderer, cursor_position); debug.draw_finished(); self.cache = Some(user_interface.into_cache()); diff --git a/native/src/renderer.rs b/native/src/renderer.rs index 39a6cff1..ca7ad5a2 100644 --- a/native/src/renderer.rs +++ b/native/src/renderer.rs @@ -19,28 +19,17 @@ //! [`text::Renderer`]: crate::widget::text::Renderer //! [`Checkbox`]: crate::widget::Checkbox //! [`checkbox::Renderer`]: crate::widget::checkbox::Renderer - #[cfg(debug_assertions)] mod null; #[cfg(debug_assertions)] pub use null::Null; -use crate::{layout, Element, Rectangle}; +use crate::layout; +use crate::{Background, Color, Element, Rectangle, Vector}; /// A component that can take the state of a user interface and produce an /// output for its users. pub trait Renderer: Sized { - /// The type of output of the [`Renderer`]. - /// - /// If you are implementing a graphical renderer, your output will most - /// likely be a tree of visual primitives. - type Output; - - /// The default styling attributes of the [`Renderer`]. - /// - /// This type can be leveraged to implement style inheritance. - type Defaults: Default; - /// Lays out the elements of a user interface. /// /// You should override this if you need to perform any operations before or @@ -53,12 +42,52 @@ pub trait Renderer: Sized { element.layout(self, limits) } - /// Overlays the `overlay` output with the given bounds on top of the `base` - /// output. - fn overlay( + /// 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, - base: Self::Output, - overlay: Self::Output, - overlay_bounds: Rectangle, - ) -> Self::Output; + translation: Vector, + f: impl FnOnce(&mut Self), + ); + + /// Clears all of the recorded primitives in the [`Renderer`]. + fn clear(&mut self); + + /// Fills a [`Quad`] with the provided [`Background`]. + fn fill_quad(&mut self, quad: Quad, background: impl Into<Background>); +} + +/// 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: f32, + + /// The border width of the [`Quad`]. + pub border_width: f32, + + /// The border color of the [`Quad`]. + pub border_color: Color, +} + +/// 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/native/src/renderer/null.rs b/native/src/renderer/null.rs index b5921582..a5b2f277 100644 --- a/native/src/renderer/null.rs +++ b/native/src/renderer/null.rs @@ -1,20 +1,6 @@ -use crate::alignment; -use crate::button; -use crate::checkbox; -use crate::column; -use crate::container; -use crate::pane_grid; -use crate::progress_bar; -use crate::radio; -use crate::row; -use crate::scrollable; -use crate::slider; -use crate::text; -use crate::text_input; -use crate::toggler; -use crate::{ - Color, Element, Font, Layout, Padding, Point, Rectangle, Renderer, Size, -}; +use crate::renderer::{self, Renderer}; +use crate::text::{self, Text}; +use crate::{Background, Font, Point, Rectangle, Size, Vector}; /// A renderer that does nothing. /// @@ -30,33 +16,21 @@ impl Null { } impl Renderer for Null { - type Output = (); - type Defaults = (); + fn with_layer(&mut self, _bounds: Rectangle, _f: impl FnOnce(&mut Self)) {} - fn overlay(&mut self, _base: (), _overlay: (), _overlay_bounds: Rectangle) { - } -} - -impl column::Renderer for Null { - fn draw<Message>( + fn with_translation( &mut self, - _defaults: &Self::Defaults, - _content: &[Element<'_, Message, Self>], - _layout: Layout<'_>, - _cursor_position: Point, - _viewport: &Rectangle, + _translation: Vector, + _f: impl FnOnce(&mut Self), ) { } -} -impl row::Renderer for Null { - fn draw<Message>( + fn clear(&mut self) {} + + fn fill_quad( &mut self, - _defaults: &Self::Defaults, - _content: &[Element<'_, Message, Self>], - _layout: Layout<'_>, - _cursor_position: Point, - _viewport: &Rectangle, + _quad: renderer::Quad, + _background: impl Into<Background>, ) { } } @@ -64,6 +38,10 @@ impl row::Renderer for Null { impl text::Renderer for Null { type Font = Font; + const ICON_FONT: Font = Font::Default; + const CHECKMARK_ICON: char = '0'; + const ARROW_DOWN_ICON: char = '0'; + fn default_size(&self) -> u16 { 20 } @@ -90,240 +68,5 @@ impl text::Renderer for Null { None } - fn draw( - &mut self, - _defaults: &Self::Defaults, - _bounds: Rectangle, - _content: &str, - _size: u16, - _font: Font, - _color: Option<Color>, - _horizontal_alignment: alignment::Horizontal, - _vertical_alignment: alignment::Vertical, - ) { - } -} - -impl scrollable::Renderer for Null { - type Style = (); - - fn scrollbar( - &self, - _bounds: Rectangle, - _content_bounds: Rectangle, - _offset: u32, - _scrollbar_width: u16, - _scrollbar_margin: u16, - _scroller_width: u16, - ) -> Option<scrollable::Scrollbar> { - None - } - - fn draw( - &mut self, - _scrollable: &scrollable::State, - _bounds: Rectangle, - _content_bounds: Rectangle, - _is_mouse_over: bool, - _is_mouse_over_scrollbar: bool, - _scrollbar: Option<scrollable::Scrollbar>, - _offset: u32, - _style: &Self::Style, - _content: Self::Output, - ) { - } -} - -impl text_input::Renderer for Null { - type Style = (); - - fn measure_value(&self, _value: &str, _size: u16, _font: Font) -> f32 { - 0.0 - } - - fn offset( - &self, - _text_bounds: Rectangle, - _font: Font, - _size: u16, - _value: &text_input::Value, - _state: &text_input::State, - ) -> f32 { - 0.0 - } - - fn draw( - &mut self, - _bounds: Rectangle, - _text_bounds: Rectangle, - _cursor_position: Point, - _font: Font, - _size: u16, - _placeholder: &str, - _value: &text_input::Value, - _state: &text_input::State, - _style: &Self::Style, - ) -> Self::Output { - } -} - -impl button::Renderer for Null { - const DEFAULT_PADDING: Padding = Padding::ZERO; - - type Style = (); - - fn draw<Message>( - &mut self, - _defaults: &Self::Defaults, - _bounds: Rectangle, - _cursor_position: Point, - _is_disabled: bool, - _is_pressed: bool, - _style: &Self::Style, - _content: &Element<'_, Message, Self>, - _content_layout: Layout<'_>, - ) -> Self::Output { - } -} - -impl radio::Renderer for Null { - type Style = (); - - const DEFAULT_SIZE: u16 = 20; - const DEFAULT_SPACING: u16 = 15; - - fn draw( - &mut self, - _bounds: Rectangle, - _is_selected: bool, - _is_mouse_over: bool, - _label: Self::Output, - _style: &Self::Style, - ) { - } -} - -impl checkbox::Renderer for Null { - type Style = (); - - const DEFAULT_SIZE: u16 = 20; - const DEFAULT_SPACING: u16 = 15; - - fn draw( - &mut self, - _bounds: Rectangle, - _is_checked: bool, - _is_mouse_over: bool, - _label: Self::Output, - _style: &Self::Style, - ) { - } -} - -impl slider::Renderer for Null { - type Style = (); - - const DEFAULT_HEIGHT: u16 = 30; - - fn draw( - &mut self, - _bounds: Rectangle, - _cursor_position: Point, - _range: std::ops::RangeInclusive<f32>, - _value: f32, - _is_dragging: bool, - _style_sheet: &Self::Style, - ) { - } -} - -impl progress_bar::Renderer for Null { - type Style = (); - - const DEFAULT_HEIGHT: u16 = 30; - - fn draw( - &self, - _bounds: Rectangle, - _range: std::ops::RangeInclusive<f32>, - _value: f32, - _style: &Self::Style, - ) { - } -} - -impl container::Renderer for Null { - type Style = (); - - fn draw<Message>( - &mut self, - _defaults: &Self::Defaults, - _bounds: Rectangle, - _cursor_position: Point, - _viewport: &Rectangle, - _style: &Self::Style, - _content: &Element<'_, Message, Self>, - _content_layout: Layout<'_>, - ) { - } -} - -impl pane_grid::Renderer for Null { - type Style = (); - - fn draw<Message>( - &mut self, - _defaults: &Self::Defaults, - _content: &[(pane_grid::Pane, pane_grid::Content<'_, Message, Self>)], - _dragging: Option<(pane_grid::Pane, Point)>, - _resizing: Option<(pane_grid::Axis, Rectangle, bool)>, - _layout: Layout<'_>, - _style: &<Self as pane_grid::Renderer>::Style, - _cursor_position: Point, - _viewport: &Rectangle, - ) { - } - - fn draw_pane<Message>( - &mut self, - _defaults: &Self::Defaults, - _bounds: Rectangle, - _style: &<Self as container::Renderer>::Style, - _title_bar: Option<( - &pane_grid::TitleBar<'_, Message, Self>, - Layout<'_>, - )>, - _body: (&Element<'_, Message, Self>, Layout<'_>), - _cursor_position: Point, - _viewport: &Rectangle, - ) { - } - - fn draw_title_bar<Message>( - &mut self, - _defaults: &Self::Defaults, - _bounds: Rectangle, - _style: &<Self as container::Renderer>::Style, - _content: (&Element<'_, Message, Self>, Layout<'_>), - _controls: Option<(&Element<'_, Message, Self>, Layout<'_>)>, - _cursor_position: Point, - _viewport: &Rectangle, - ) { - } -} - -impl toggler::Renderer for Null { - type Style = (); - - const DEFAULT_SIZE: u16 = 20; - - fn draw( - &mut self, - _bounds: Rectangle, - _is_checked: bool, - _is_mouse_over: bool, - _label: Option<Self::Output>, - _style: &Self::Style, - ) { - } + fn fill_text(&mut self, _text: Text<'_, Self::Font>) {} } diff --git a/native/src/svg.rs b/native/src/svg.rs new file mode 100644 index 00000000..90eff87e --- /dev/null +++ b/native/src/svg.rs @@ -0,0 +1,88 @@ +//! Load and draw vector graphics. +use crate::{Hasher, Rectangle}; + +use std::hash::{Hash, Hasher as _}; +use std::path::PathBuf; +use std::sync::Arc; + +/// An [`Svg`] handle. +#[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<Vec<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 an [`Svg`]. +#[derive(Clone, Hash)] +pub enum Data { + /// File data + Path(PathBuf), + + /// In-memory data + /// + /// Can contain an SVG string or a gzip compressed data. + Bytes(Vec<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) -> (u32, u32); + + /// Draws an SVG with the given [`Handle`] and inside the provided `bounds`. + fn draw(&mut self, handle: Handle, bounds: Rectangle); +} diff --git a/native/src/text.rs b/native/src/text.rs new file mode 100644 index 00000000..8b9205e3 --- /dev/null +++ b/native/src/text.rs @@ -0,0 +1,114 @@ +//! Draw and interact with text. +use crate::alignment; +use crate::{Color, Point, Rectangle, Size, Vector}; + +/// 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), + /// The provided point was not within the bounds of a glyph. The index + /// of the character with the closest centeroid position is returned, + /// as well as its delta. + NearestCharOffset(usize, Vector), +} + +impl Hit { + /// Computes the cursor position corresponding to this [`HitTestResult`] . + pub fn cursor(self) -> usize { + match self { + Self::CharOffset(i) => i, + Self::NearestCharOffset(i, delta) => { + if delta.x > f32::EPSILON { + i + 1 + } else { + i + } + } + } + } +} + +/// A renderer capable of measuring and drawing [`Text`]. +pub trait Renderer: crate::Renderer { + /// The font type used. + type Font: Default + 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 size of [`Text`]. + fn default_size(&self) -> u16; + + /// Measures the text in the given bounds and returns the minimum boundaries + /// that can fit the contents. + fn measure( + &self, + content: &str, + size: u16, + 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: u16, 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>; + + /// Draws the given [`Text`]. + fn fill_text(&mut self, text: Text<'_, Self::Font>); +} diff --git a/native/src/user_interface.rs b/native/src/user_interface.rs index 8e0d7d1c..39cac559 100644 --- a/native/src/user_interface.rs +++ b/native/src/user_interface.rs @@ -1,6 +1,8 @@ use crate::event::{self, Event}; use crate::layout; +use crate::mouse; use crate::overlay; +use crate::renderer; use crate::{Clipboard, Element, Layout, Point, Rectangle, Size}; use std::hash::Hasher; @@ -47,7 +49,7 @@ where /// # pub use iced_native::renderer::Null as Renderer; /// # } /// # - /// # use iced_native::Column; + /// # use iced_native::widget::Column; /// # /// # pub struct Counter; /// # @@ -141,7 +143,7 @@ where /// # pub use iced_native::renderer::Null as Renderer; /// # } /// # - /// # use iced_native::Column; + /// # use iced_native::widget::Column; /// # /// # pub struct Counter; /// # @@ -277,7 +279,7 @@ where /// # pub use iced_native::renderer::Null as Renderer; /// # } /// # - /// # use iced_native::Column; + /// # use iced_native::widget::Column; /// # /// # pub struct Counter; /// # @@ -333,10 +335,13 @@ where &mut self, renderer: &mut Renderer, cursor_position: Point, - ) -> Renderer::Output { + ) -> mouse::Interaction { + // TODO: Move to shell level (?) + renderer.clear(); + let viewport = Rectangle::with_size(self.bounds); - let overlay = if let Some(mut overlay) = + if let Some(mut overlay) = self.root.overlay(Layout::new(&self.base.layout)) { let layer = Self::overlay_layer( @@ -346,51 +351,81 @@ where renderer, ); - let overlay_bounds = layer.layout.bounds(); - - let overlay_primitives = overlay.draw( - renderer, - &Renderer::Defaults::default(), - Layout::new(&layer.layout), - cursor_position, - ); - self.overlay = Some(layer); - - Some((overlay_primitives, overlay_bounds)) - } else { - None }; - if let Some((overlay_primitives, overlay_bounds)) = overlay { - let base_cursor = if overlay_bounds.contains(cursor_position) { + if let Some(layer) = &self.overlay { + let base_cursor = if layer.layout.bounds().contains(cursor_position) + { Point::new(-1.0, -1.0) } else { cursor_position }; - let base_primitives = self.root.widget.draw( + self.root.widget.draw( renderer, - &Renderer::Defaults::default(), + &renderer::Style::default(), Layout::new(&self.base.layout), base_cursor, &viewport, ); - - renderer.overlay( - base_primitives, - overlay_primitives, - overlay_bounds, - ) } else { self.root.widget.draw( renderer, - &Renderer::Defaults::default(), + &renderer::Style::default(), Layout::new(&self.base.layout), cursor_position, &viewport, - ) - } + ); + }; + + let base_interaction = self.root.widget.mouse_interaction( + Layout::new(&self.base.layout), + cursor_position, + &viewport, + ); + + let Self { + overlay, + root, + base, + .. + } = self; + + // TODO: Currently, we need to call Widget::overlay twice to + // implement the painter's algorithm properly. + // + // Once we have a proper persistent widget tree, we should be able to + // avoid this additional call. + overlay + .as_ref() + .and_then(|layer| { + root.overlay(Layout::new(&base.layout)).map(|overlay| { + let overlay_interaction = overlay.mouse_interaction( + Layout::new(&layer.layout), + cursor_position, + &viewport, + ); + + let overlay_bounds = layer.layout.bounds(); + + renderer.with_layer(viewport, |renderer| { + overlay.draw( + renderer, + &renderer::Style::default(), + Layout::new(&layer.layout), + cursor_position, + ); + }); + + if overlay_bounds.contains(cursor_position) { + overlay_interaction + } else { + base_interaction + } + }) + }) + .unwrap_or(base_interaction) } /// Relayouts and returns a new [`UserInterface`] using the provided diff --git a/native/src/widget.rs b/native/src/widget.rs index 43c1b023..07214b16 100644 --- a/native/src/widget.rs +++ b/native/src/widget.rs @@ -10,14 +10,6 @@ //! [`Widget`] trait. You can use the API of the built-in widgets as a guide or //! source of inspiration. //! -//! # Re-exports -//! For convenience, the contents of this module are available at the root -//! module. Therefore, you can directly type: -//! -//! ``` -//! use iced_native::{button, Button, Widget}; -//! ``` -//! //! [renderer]: crate::renderer pub mod button; pub mod checkbox; @@ -80,7 +72,9 @@ pub use tooltip::Tooltip; use crate::event::{self, Event}; use crate::layout; +use crate::mouse; use crate::overlay; +use crate::renderer; use crate::{Clipboard, Hasher, Layout, Length, Point, Rectangle}; /// A component that displays information and allows interaction. @@ -131,11 +125,11 @@ where fn draw( &self, renderer: &mut Renderer, - defaults: &Renderer::Defaults, + style: &renderer::Style, layout: Layout<'_>, cursor_position: Point, viewport: &Rectangle, - ) -> Renderer::Output; + ); /// Computes the _layout_ hash of the [`Widget`]. /// @@ -174,6 +168,18 @@ where event::Status::Ignored } + /// Returns the current [`mouse::Interaction`] of the [`Widget`]. + /// + /// By default, it returns [`mouse::Interaction::Idle`]. + fn mouse_interaction( + &self, + _layout: Layout<'_>, + _cursor_position: Point, + _viewport: &Rectangle, + ) -> mouse::Interaction { + mouse::Interaction::Idle + } + /// Returns the overlay of the [`Widget`], if there is any. fn overlay( &mut self, diff --git a/native/src/widget/button.rs b/native/src/widget/button.rs index c469a0e5..1d785f35 100644 --- a/native/src/widget/button.rs +++ b/native/src/widget/button.rs @@ -5,20 +5,24 @@ use crate::event::{self, Event}; use crate::layout; use crate::mouse; use crate::overlay; +use crate::renderer; use crate::touch; use crate::{ - Clipboard, Element, Hasher, Layout, Length, Padding, Point, Rectangle, - Widget, + Background, Clipboard, Color, Element, Hasher, Layout, Length, Padding, + Point, Rectangle, Vector, Widget, }; + use std::hash::Hash; +pub use iced_style::button::{Style, StyleSheet}; + /// A generic widget that produces a message when pressed. /// /// ``` -/// # use iced_native::{button, Text}; +/// # use iced_native::widget::{button, Text}; /// # /// # type Button<'a, Message> = -/// # iced_native::Button<'a, Message, iced_native::renderer::Null>; +/// # iced_native::widget::Button<'a, Message, iced_native::renderer::Null>; /// # /// #[derive(Clone)] /// enum Message { @@ -34,10 +38,10 @@ use std::hash::Hash; /// be disabled: /// /// ``` -/// # use iced_native::{button, Text}; +/// # use iced_native::widget::{button, Text}; /// # /// # type Button<'a, Message> = -/// # iced_native::Button<'a, Message, iced_native::renderer::Null>; +/// # iced_native::widget::Button<'a, Message, iced_native::renderer::Null>; /// # /// #[derive(Clone)] /// enum Message { @@ -53,7 +57,7 @@ use std::hash::Hash; /// } /// ``` #[allow(missing_debug_implementations)] -pub struct Button<'a, Message, Renderer: self::Renderer> { +pub struct Button<'a, Message, Renderer> { state: &'a mut State, content: Element<'a, Message, Renderer>, on_press: Option<Message>, @@ -62,13 +66,13 @@ pub struct Button<'a, Message, Renderer: self::Renderer> { min_width: u32, min_height: u32, padding: Padding, - style: Renderer::Style, + style_sheet: Box<dyn StyleSheet + 'a>, } impl<'a, Message, Renderer> Button<'a, Message, Renderer> where Message: Clone, - Renderer: self::Renderer, + Renderer: crate::Renderer, { /// Creates a new [`Button`] with some local [`State`] and the given /// content. @@ -84,8 +88,8 @@ where height: Length::Shrink, min_width: 0, min_height: 0, - padding: Renderer::DEFAULT_PADDING, - style: Renderer::Style::default(), + padding: Padding::new(5), + style_sheet: Default::default(), } } @@ -127,8 +131,11 @@ where } /// Sets the style of the [`Button`]. - pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { - self.style = style.into(); + pub fn style( + mut self, + style_sheet: impl Into<Box<dyn StyleSheet + 'a>>, + ) -> Self { + self.style_sheet = style_sheet.into(); self } } @@ -150,7 +157,7 @@ impl<'a, Message, Renderer> Widget<Message, Renderer> for Button<'a, Message, Renderer> where Message: Clone, - Renderer: self::Renderer, + Renderer: crate::Renderer, { fn width(&self) -> Length { self.width @@ -241,24 +248,88 @@ where event::Status::Ignored } + fn mouse_interaction( + &self, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) -> mouse::Interaction { + let is_mouse_over = layout.bounds().contains(cursor_position); + let is_disabled = self.on_press.is_none(); + + if is_mouse_over && !is_disabled { + mouse::Interaction::Pointer + } else { + mouse::Interaction::default() + } + } + fn draw( &self, renderer: &mut Renderer, - defaults: &Renderer::Defaults, + _style: &renderer::Style, layout: Layout<'_>, cursor_position: Point, _viewport: &Rectangle, - ) -> Renderer::Output { - renderer.draw( - defaults, - layout.bounds(), + ) { + let bounds = layout.bounds(); + let content_layout = layout.children().next().unwrap(); + + let is_mouse_over = bounds.contains(cursor_position); + let is_disabled = self.on_press.is_none(); + + let styling = if is_disabled { + self.style_sheet.disabled() + } else if is_mouse_over { + if self.state.is_pressed { + self.style_sheet.pressed() + } else { + self.style_sheet.hovered() + } + } else { + self.style_sheet.active() + }; + + if styling.background.is_some() || styling.border_width > 0.0 { + if styling.shadow_offset != Vector::default() { + // TODO: Implement proper shadow support + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x + styling.shadow_offset.x, + y: bounds.y + styling.shadow_offset.y, + ..bounds + }, + border_radius: styling.border_radius, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + Background::Color([0.0, 0.0, 0.0, 0.5].into()), + ); + } + + renderer.fill_quad( + renderer::Quad { + bounds, + border_radius: styling.border_radius, + border_width: styling.border_width, + border_color: styling.border_color, + }, + styling + .background + .unwrap_or(Background::Color(Color::TRANSPARENT)), + ); + } + + self.content.draw( + renderer, + &renderer::Style { + text_color: styling.text_color, + }, + content_layout, cursor_position, - self.on_press.is_none(), - self.state.is_pressed, - &self.style, - &self.content, - layout.children().next().unwrap(), - ) + &bounds, + ); } fn hash_layout(&self, state: &mut Hasher) { @@ -277,38 +348,11 @@ where } } -/// The renderer of a [`Button`]. -/// -/// Your [renderer] will need to implement this trait before being -/// able to use a [`Button`] in your user interface. -/// -/// [renderer]: crate::renderer -pub trait Renderer: crate::Renderer + Sized { - /// The default padding of a [`Button`]. - const DEFAULT_PADDING: Padding; - - /// The style supported by this renderer. - type Style: Default; - - /// Draws a [`Button`]. - fn draw<Message>( - &mut self, - defaults: &Self::Defaults, - bounds: Rectangle, - cursor_position: Point, - is_disabled: bool, - is_pressed: bool, - style: &Self::Style, - content: &Element<'_, Message, Self>, - content_layout: Layout<'_>, - ) -> Self::Output; -} - impl<'a, Message, Renderer> From<Button<'a, Message, Renderer>> for Element<'a, Message, Renderer> where Message: 'a + Clone, - Renderer: 'a + self::Renderer, + Renderer: 'a + crate::Renderer, { fn from( button: Button<'a, Message, Renderer>, diff --git a/native/src/widget/checkbox.rs b/native/src/widget/checkbox.rs index 8bdb6b78..0d4a43ec 100644 --- a/native/src/widget/checkbox.rs +++ b/native/src/widget/checkbox.rs @@ -1,24 +1,27 @@ //! Show toggle controls using checkboxes. use std::hash::Hash; -use crate::alignment::{self, Alignment}; +use crate::alignment; use crate::event::{self, Event}; use crate::layout; use crate::mouse; -use crate::row; +use crate::renderer; use crate::text; use crate::touch; +use crate::widget::{self, Row, Text}; use crate::{ - Clipboard, Color, Element, Hasher, Layout, Length, Point, Rectangle, Row, - Text, Widget, + Alignment, Clipboard, Color, Element, Hasher, Layout, Length, Point, + Rectangle, Widget, }; +pub use iced_style::checkbox::{Style, StyleSheet}; + /// A box that can be checked. /// /// # Example /// /// ``` -/// # type Checkbox<Message> = iced_native::Checkbox<Message, iced_native::renderer::Null>; +/// # type Checkbox<'a, Message> = iced_native::widget::Checkbox<'a, Message, iced_native::renderer::Null>; /// # /// pub enum Message { /// CheckboxToggled(bool), @@ -31,7 +34,7 @@ use crate::{ /// ///  #[allow(missing_debug_implementations)] -pub struct Checkbox<Message, Renderer: self::Renderer + text::Renderer> { +pub struct Checkbox<'a, Message, Renderer: text::Renderer> { is_checked: bool, on_toggle: Box<dyn Fn(bool) -> Message>, label: String, @@ -41,12 +44,16 @@ pub struct Checkbox<Message, Renderer: self::Renderer + text::Renderer> { text_size: Option<u16>, font: Renderer::Font, text_color: Option<Color>, - style: Renderer::Style, + style_sheet: Box<dyn StyleSheet + 'a>, } -impl<Message, Renderer: self::Renderer + text::Renderer> - Checkbox<Message, Renderer> -{ +impl<'a, Message, Renderer: text::Renderer> Checkbox<'a, Message, Renderer> { + /// The default size of a [`Checkbox`]. + const DEFAULT_SIZE: u16 = 20; + + /// The default spacing of a [`Checkbox`]. + const DEFAULT_SPACING: u16 = 15; + /// Creates a new [`Checkbox`]. /// /// It expects: @@ -64,12 +71,12 @@ impl<Message, Renderer: self::Renderer + text::Renderer> on_toggle: Box::new(f), label: label.into(), width: Length::Shrink, - size: <Renderer as self::Renderer>::DEFAULT_SIZE, - spacing: Renderer::DEFAULT_SPACING, + size: Self::DEFAULT_SIZE, + spacing: Self::DEFAULT_SPACING, text_size: None, font: Renderer::Font::default(), text_color: None, - style: Renderer::Style::default(), + style_sheet: Default::default(), } } @@ -112,16 +119,19 @@ impl<Message, Renderer: self::Renderer + text::Renderer> } /// Sets the style of the [`Checkbox`]. - pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { - self.style = style.into(); + pub fn style( + mut self, + style_sheet: impl Into<Box<dyn StyleSheet + 'a>>, + ) -> Self { + self.style_sheet = style_sheet.into(); self } } -impl<Message, Renderer> Widget<Message, Renderer> - for Checkbox<Message, Renderer> +impl<'a, Message, Renderer> Widget<Message, Renderer> + for Checkbox<'a, Message, Renderer> where - Renderer: self::Renderer + text::Renderer + row::Renderer, + Renderer: text::Renderer, { fn width(&self) -> Length { self.width @@ -180,43 +190,84 @@ where event::Status::Ignored } + fn mouse_interaction( + &self, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) -> mouse::Interaction { + if layout.bounds().contains(cursor_position) { + mouse::Interaction::Pointer + } else { + mouse::Interaction::default() + } + } + fn draw( &self, renderer: &mut Renderer, - defaults: &Renderer::Defaults, + style: &renderer::Style, layout: Layout<'_>, cursor_position: Point, _viewport: &Rectangle, - ) -> Renderer::Output { + ) { let bounds = layout.bounds(); + let is_mouse_over = bounds.contains(cursor_position); + let mut children = layout.children(); - let checkbox_layout = children.next().unwrap(); - let label_layout = children.next().unwrap(); - let checkbox_bounds = checkbox_layout.bounds(); - - let label = text::Renderer::draw( - renderer, - defaults, - label_layout.bounds(), - &self.label, - self.text_size.unwrap_or(renderer.default_size()), - self.font, - self.text_color, - alignment::Horizontal::Left, - alignment::Vertical::Center, - ); + { + let layout = children.next().unwrap(); + let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); + let style = if is_mouse_over { + self.style_sheet.hovered(self.is_checked) + } else { + self.style_sheet.active(self.is_checked) + }; + + renderer.fill_quad( + renderer::Quad { + bounds, + border_radius: style.border_radius, + border_width: style.border_width, + border_color: style.border_color, + }, + style.background, + ); - self::Renderer::draw( - renderer, - checkbox_bounds, - self.is_checked, - is_mouse_over, - label, - &self.style, - ) + if self.is_checked { + renderer.fill_text(text::Text { + content: &Renderer::CHECKMARK_ICON.to_string(), + font: Renderer::ICON_FONT, + size: bounds.height * 0.7, + bounds: Rectangle { + x: bounds.center_x(), + y: bounds.center_y(), + ..bounds + }, + color: style.checkmark_color, + horizontal_alignment: alignment::Horizontal::Center, + vertical_alignment: alignment::Vertical::Center, + }); + } + } + + { + let label_layout = children.next().unwrap(); + + widget::text::draw( + renderer, + style, + label_layout, + &self.label, + self.font, + self.text_size, + self.text_color, + alignment::Horizontal::Left, + alignment::Vertical::Center, + ); + } } fn hash_layout(&self, state: &mut Hasher) { @@ -227,47 +278,14 @@ where } } -/// The renderer of a [`Checkbox`]. -/// -/// Your [renderer] will need to implement this trait before being -/// able to use a [`Checkbox`] in your user interface. -/// -/// [renderer]: crate::Renderer -pub trait Renderer: crate::Renderer { - /// The style supported by this renderer. - type Style: Default; - - /// The default size of a [`Checkbox`]. - const DEFAULT_SIZE: u16; - - /// The default spacing of a [`Checkbox`]. - const DEFAULT_SPACING: u16; - - /// Draws a [`Checkbox`]. - /// - /// It receives: - /// * the bounds of the [`Checkbox`] - /// * whether the [`Checkbox`] is selected or not - /// * whether the mouse is over the [`Checkbox`] or not - /// * the drawn label of the [`Checkbox`] - fn draw( - &mut self, - bounds: Rectangle, - is_checked: bool, - is_mouse_over: bool, - label: Self::Output, - style: &Self::Style, - ) -> Self::Output; -} - -impl<'a, Message, Renderer> From<Checkbox<Message, Renderer>> +impl<'a, Message, Renderer> From<Checkbox<'a, Message, Renderer>> for Element<'a, Message, Renderer> where - Renderer: 'a + self::Renderer + text::Renderer + row::Renderer, + Renderer: 'a + text::Renderer, Message: 'a, { fn from( - checkbox: Checkbox<Message, Renderer>, + checkbox: Checkbox<'a, Message, Renderer>, ) -> Element<'a, Message, Renderer> { Element::new(checkbox) } diff --git a/native/src/widget/column.rs b/native/src/widget/column.rs index 30cf0781..0d4d6fa7 100644 --- a/native/src/widget/column.rs +++ b/native/src/widget/column.rs @@ -3,7 +3,9 @@ use std::hash::Hash; use crate::event::{self, Event}; use crate::layout; +use crate::mouse; use crate::overlay; +use crate::renderer; use crate::{ Alignment, Clipboard, Element, Hasher, Layout, Length, Padding, Point, Rectangle, Widget, @@ -105,7 +107,7 @@ impl<'a, Message, Renderer> Column<'a, Message, Renderer> { impl<'a, Message, Renderer> Widget<Message, Renderer> for Column<'a, Message, Renderer> where - Renderer: self::Renderer, + Renderer: crate::Renderer, { fn width(&self) -> Length { self.width @@ -162,21 +164,37 @@ where .fold(event::Status::Ignored, event::Status::merge) } + fn mouse_interaction( + &self, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) -> mouse::Interaction { + self.children + .iter() + .zip(layout.children()) + .map(|(child, layout)| { + child.widget.mouse_interaction( + layout, + cursor_position, + viewport, + ) + }) + .max() + .unwrap_or_default() + } + fn draw( &self, renderer: &mut Renderer, - defaults: &Renderer::Defaults, + style: &renderer::Style, layout: Layout<'_>, cursor_position: Point, viewport: &Rectangle, - ) -> Renderer::Output { - renderer.draw( - defaults, - &self.children, - layout, - cursor_position, - viewport, - ) + ) { + for (child, layout) in self.children.iter().zip(layout.children()) { + child.draw(renderer, style, layout, cursor_position, viewport); + } } fn hash_layout(&self, state: &mut Hasher) { @@ -208,33 +226,10 @@ where } } -/// The renderer of a [`Column`]. -/// -/// Your [renderer] will need to implement this trait before being -/// able to use a [`Column`] in your user interface. -/// -/// [renderer]: crate::renderer -pub trait Renderer: crate::Renderer + Sized { - /// Draws a [`Column`]. - /// - /// It receives: - /// - the children of the [`Column`] - /// - the [`Layout`] of the [`Column`] and its children - /// - the cursor position - fn draw<Message>( - &mut self, - defaults: &Self::Defaults, - content: &[Element<'_, Message, Self>], - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - ) -> Self::Output; -} - impl<'a, Message, Renderer> From<Column<'a, Message, Renderer>> for Element<'a, Message, Renderer> where - Renderer: 'a + self::Renderer, + Renderer: 'a + crate::Renderer, Message: 'a, { fn from( diff --git a/native/src/widget/container.rs b/native/src/widget/container.rs index 0e86ab62..596af7fd 100644 --- a/native/src/widget/container.rs +++ b/native/src/widget/container.rs @@ -4,19 +4,23 @@ use std::hash::Hash; use crate::alignment::{self, Alignment}; use crate::event::{self, Event}; use crate::layout; +use crate::mouse; use crate::overlay; +use crate::renderer; use crate::{ - Clipboard, Element, Hasher, Layout, Length, Padding, Point, Rectangle, - Widget, + Background, Clipboard, Color, Element, Hasher, Layout, Length, Padding, + Point, Rectangle, Widget, }; use std::u32; +pub use iced_style::container::{Style, StyleSheet}; + /// An element decorating some content. /// /// It is normally used for alignment purposes. #[allow(missing_debug_implementations)] -pub struct Container<'a, Message, Renderer: self::Renderer> { +pub struct Container<'a, Message, Renderer> { padding: Padding, width: Length, height: Length, @@ -24,13 +28,13 @@ pub struct Container<'a, Message, Renderer: self::Renderer> { max_height: u32, horizontal_alignment: alignment::Horizontal, vertical_alignment: alignment::Vertical, - style: Renderer::Style, + style_sheet: Box<dyn StyleSheet + 'a>, content: Element<'a, Message, Renderer>, } impl<'a, Message, Renderer> Container<'a, Message, Renderer> where - Renderer: self::Renderer, + Renderer: crate::Renderer, { /// Creates an empty [`Container`]. pub fn new<T>(content: T) -> Self @@ -45,7 +49,7 @@ where max_height: u32::MAX, horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, - style: Renderer::Style::default(), + style_sheet: Default::default(), content: content.into(), } } @@ -105,8 +109,11 @@ where } /// Sets the style of the [`Container`]. - pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { - self.style = style.into(); + pub fn style( + mut self, + style_sheet: impl Into<Box<dyn StyleSheet + 'a>>, + ) -> Self { + self.style_sheet = style_sheet.into(); self } } @@ -114,7 +121,7 @@ where impl<'a, Message, Renderer> Widget<Message, Renderer> for Container<'a, Message, Renderer> where - Renderer: self::Renderer, + Renderer: crate::Renderer, { fn width(&self) -> Length { self.width @@ -172,23 +179,42 @@ where ) } + fn mouse_interaction( + &self, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) -> mouse::Interaction { + self.content.widget.mouse_interaction( + layout.children().next().unwrap(), + cursor_position, + viewport, + ) + } + fn draw( &self, renderer: &mut Renderer, - defaults: &Renderer::Defaults, + renderer_style: &renderer::Style, layout: Layout<'_>, cursor_position: Point, viewport: &Rectangle, - ) -> Renderer::Output { - renderer.draw( - defaults, - layout.bounds(), + ) { + let style = self.style_sheet.style(); + + draw_background(renderer, &style, layout.bounds()); + + self.content.draw( + renderer, + &renderer::Style { + text_color: style + .text_color + .unwrap_or(renderer_style.text_color), + }, + layout.children().next().unwrap(), cursor_position, viewport, - &self.style, - &self.content, - layout.children().next().unwrap(), - ) + ); } fn hash_layout(&self, state: &mut Hasher) { @@ -212,33 +238,33 @@ where } } -/// The renderer of a [`Container`]. -/// -/// Your [renderer] will need to implement this trait before being -/// able to use a [`Container`] in your user interface. -/// -/// [renderer]: crate::renderer -pub trait Renderer: crate::Renderer { - /// The style supported by this renderer. - type Style: Default; - - /// Draws a [`Container`]. - fn draw<Message>( - &mut self, - defaults: &Self::Defaults, - bounds: Rectangle, - cursor_position: Point, - viewport: &Rectangle, - style: &Self::Style, - content: &Element<'_, Message, Self>, - content_layout: Layout<'_>, - ) -> Self::Output; +/// Draws the background of a [`Container`] given its [`Style`] and its `bounds`. +pub fn draw_background<Renderer>( + renderer: &mut Renderer, + style: &Style, + bounds: Rectangle, +) where + Renderer: crate::Renderer, +{ + if style.background.is_some() || style.border_width > 0.0 { + renderer.fill_quad( + renderer::Quad { + bounds, + border_radius: style.border_radius, + border_width: style.border_width, + border_color: style.border_color, + }, + style + .background + .unwrap_or(Background::Color(Color::TRANSPARENT)), + ); + } } impl<'a, Message, Renderer> From<Container<'a, Message, Renderer>> for Element<'a, Message, Renderer> where - Renderer: 'a + self::Renderer, + Renderer: 'a + crate::Renderer, Message: 'a, { fn from( diff --git a/native/src/widget/image.rs b/native/src/widget/image.rs index 4d8e0a3f..66e95265 100644 --- a/native/src/widget/image.rs +++ b/native/src/widget/image.rs @@ -2,21 +2,19 @@ pub mod viewer; pub use viewer::Viewer; +use crate::image::{self, Handle}; use crate::layout; +use crate::renderer; use crate::{Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget}; -use std::{ - hash::{Hash, Hasher as _}, - path::PathBuf, - sync::Arc, -}; +use std::hash::Hash; /// A frame that displays an image while keeping aspect ratio. /// /// # Example /// /// ``` -/// # use iced_native::Image; +/// # use iced_native::widget::Image; /// # /// let image = Image::new("resources/ferris.png"); /// ``` @@ -54,7 +52,7 @@ impl Image { impl<Message, Renderer> Widget<Message, Renderer> for Image where - Renderer: self::Renderer, + Renderer: image::Renderer, { fn width(&self) -> Length { self.width @@ -92,12 +90,12 @@ where fn draw( &self, renderer: &mut Renderer, - _defaults: &Renderer::Defaults, + _style: &renderer::Style, layout: Layout<'_>, _cursor_position: Point, _viewport: &Rectangle, - ) -> Renderer::Output { - renderer.draw(self.handle.clone(), layout) + ) { + renderer.draw(self.handle.clone(), layout.bounds()); } fn hash_layout(&self, state: &mut Hasher) { @@ -110,129 +108,9 @@ where } } -/// An [`Image`] handle. -#[derive(Debug, Clone)] -pub struct Handle { - id: u64, - data: Arc<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 BGRA - /// pixels. - /// - /// This is useful if you have already decoded your image. - pub fn from_pixels(width: u32, height: u32, pixels: Vec<u8>) -> Handle { - Self::from_data(Data::Pixels { - width, - height, - 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: Vec<u8>) -> Handle { - Self::from_data(Data::Bytes(bytes)) - } - - 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 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); - } -} - -/// The data of an [`Image`]. -#[derive(Clone, Hash)] -pub enum Data { - /// File data - Path(PathBuf), - - /// In-memory data - Bytes(Vec<u8>), - - /// Decoded image pixels in BGRA format. - Pixels { - /// The width of the image. - width: u32, - /// The height of the image. - height: u32, - /// The pixels. - pixels: Vec<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(...)"), - Data::Pixels { width, height, .. } => { - write!(f, "Pixels({} * {})", width, height) - } - } - } -} - -/// The renderer of an [`Image`]. -/// -/// Your [renderer] will need to implement this trait before being able to use -/// an [`Image`] in your user interface. -/// -/// [renderer]: crate::renderer -pub trait Renderer: crate::Renderer { - /// Returns the dimensions of an [`Image`] located on the given path. - fn dimensions(&self, handle: &Handle) -> (u32, u32); - - /// Draws an [`Image`]. - fn draw(&mut self, handle: Handle, layout: Layout<'_>) -> Self::Output; -} - impl<'a, Message, Renderer> From<Image> for Element<'a, Message, Renderer> where - Renderer: self::Renderer, + Renderer: image::Renderer, { fn from(image: Image) -> Element<'a, Message, Renderer> { Element::new(image) diff --git a/native/src/widget/image/viewer.rs b/native/src/widget/image/viewer.rs index 405daf00..95e5c6e4 100644 --- a/native/src/widget/image/viewer.rs +++ b/native/src/widget/image/viewer.rs @@ -3,6 +3,7 @@ use crate::event::{self, Event}; use crate::image; use crate::layout; use crate::mouse; +use crate::renderer; use crate::{ Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Size, Vector, Widget, @@ -88,7 +89,7 @@ impl<'a> Viewer<'a> { /// will be respected. fn image_size<Renderer>(&self, renderer: &Renderer, bounds: Size) -> Size where - Renderer: self::Renderer + image::Renderer, + Renderer: image::Renderer, { let (width, height) = renderer.dimensions(&self.handle); @@ -115,7 +116,7 @@ impl<'a> Viewer<'a> { impl<'a, Message, Renderer> Widget<Message, Renderer> for Viewer<'a> where - Renderer: self::Renderer + image::Renderer, + Renderer: image::Renderer, { fn width(&self) -> Length { self.width @@ -280,14 +281,32 @@ where } } + fn mouse_interaction( + &self, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) -> mouse::Interaction { + let bounds = layout.bounds(); + let is_mouse_over = bounds.contains(cursor_position); + + if self.state.is_cursor_grabbed() { + mouse::Interaction::Grabbing + } else if is_mouse_over { + mouse::Interaction::Grab + } else { + mouse::Interaction::Idle + } + } + fn draw( &self, renderer: &mut Renderer, - _defaults: &Renderer::Defaults, + _style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + _cursor_position: Point, _viewport: &Rectangle, - ) -> Renderer::Output { + ) { let bounds = layout.bounds(); let image_size = self.image_size(renderer, bounds.size()); @@ -301,17 +320,19 @@ where image_top_left - self.state.offset(bounds, image_size) }; - let is_mouse_over = bounds.contains(cursor_position); - - self::Renderer::draw( - renderer, - &self.state, - bounds, - image_size, - translation, - self.handle.clone(), - is_mouse_over, - ) + renderer.with_layer(bounds, |renderer| { + renderer.with_translation(translation, |renderer| { + image::Renderer::draw( + renderer, + self.handle.clone(), + Rectangle { + x: bounds.x, + y: bounds.y, + ..Rectangle::with_size(image_size) + }, + ) + }); + }); } fn hash_layout(&self, state: &mut Hasher) { @@ -373,38 +394,9 @@ impl State { } } -/// The renderer of an [`Viewer`]. -/// -/// Your [renderer] will need to implement this trait before being -/// able to use a [`Viewer`] in your user interface. -/// -/// [renderer]: crate::renderer -pub trait Renderer: crate::Renderer + Sized { - /// Draws the [`Viewer`]. - /// - /// It receives: - /// - the [`State`] of the [`Viewer`] - /// - the bounds of the [`Viewer`] widget - /// - the [`Size`] of the scaled [`Viewer`] image - /// - the translation of the clipped image - /// - the [`Handle`] to the underlying image - /// - whether the mouse is over the [`Viewer`] or not - /// - /// [`Handle`]: image::Handle - fn draw( - &mut self, - state: &State, - bounds: Rectangle, - image_size: Size, - translation: Vector, - handle: image::Handle, - is_mouse_over: bool, - ) -> Self::Output; -} - impl<'a, Message, Renderer> From<Viewer<'a>> for Element<'a, Message, Renderer> where - Renderer: 'a + self::Renderer + image::Renderer, + Renderer: 'a + image::Renderer, Message: 'a, { fn from(viewer: Viewer<'a>) -> Element<'a, Message, Renderer> { diff --git a/native/src/widget/pane_grid.rs b/native/src/widget/pane_grid.rs index 26a72409..20616ed4 100644 --- a/native/src/widget/pane_grid.rs +++ b/native/src/widget/pane_grid.rs @@ -27,18 +27,19 @@ pub use split::Split; pub use state::State; pub use title_bar::TitleBar; -use crate::container; use crate::event::{self, Event}; use crate::layout; use crate::mouse; use crate::overlay; -use crate::row; +use crate::renderer; use crate::touch; use crate::{ - Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Size, Vector, - Widget, + Clipboard, Color, Element, Hasher, Layout, Length, Point, Rectangle, Size, + Vector, Widget, }; +pub use iced_style::pane_grid::{Line, StyleSheet}; + /// A collection of panes distributed using either vertical or horizontal splits /// to completely fill the space available. /// @@ -61,10 +62,10 @@ use crate::{ /// ## Example /// /// ``` -/// # use iced_native::{pane_grid, Text}; +/// # use iced_native::widget::{pane_grid, Text}; /// # /// # type PaneGrid<'a, Message> = -/// # iced_native::PaneGrid<'a, Message, iced_native::renderer::Null>; +/// # iced_native::widget::PaneGrid<'a, Message, iced_native::renderer::Null>; /// # /// enum PaneState { /// SomePane, @@ -89,7 +90,7 @@ use crate::{ /// .on_resize(10, Message::PaneResized); /// ``` #[allow(missing_debug_implementations)] -pub struct PaneGrid<'a, Message, Renderer: self::Renderer> { +pub struct PaneGrid<'a, Message, Renderer> { state: &'a mut state::Internal, elements: Vec<(Pane, Content<'a, Message, Renderer>)>, width: Length, @@ -98,12 +99,12 @@ pub struct PaneGrid<'a, Message, Renderer: self::Renderer> { on_click: Option<Box<dyn Fn(Pane) -> Message + 'a>>, on_drag: Option<Box<dyn Fn(DragEvent) -> Message + 'a>>, on_resize: Option<(u16, Box<dyn Fn(ResizeEvent) -> Message + 'a>)>, - style: <Renderer as self::Renderer>::Style, + style_sheet: Box<dyn StyleSheet + 'a>, } impl<'a, Message, Renderer> PaneGrid<'a, Message, Renderer> where - Renderer: self::Renderer, + Renderer: crate::Renderer, { /// Creates a [`PaneGrid`] with the given [`State`] and view function. /// @@ -130,7 +131,7 @@ where on_click: None, on_drag: None, on_resize: None, - style: Default::default(), + style_sheet: Default::default(), } } @@ -190,18 +191,15 @@ where } /// Sets the style of the [`PaneGrid`]. - pub fn style( - mut self, - style: impl Into<<Renderer as self::Renderer>::Style>, - ) -> Self { - self.style = style.into(); + pub fn style(mut self, style: impl Into<Box<dyn StyleSheet + 'a>>) -> Self { + self.style_sheet = style.into(); self } } impl<'a, Message, Renderer> PaneGrid<'a, Message, Renderer> where - Renderer: self::Renderer, + Renderer: crate::Renderer, { fn click_pane( &mut self, @@ -318,7 +316,7 @@ pub struct ResizeEvent { impl<'a, Message, Renderer> Widget<Message, Renderer> for PaneGrid<'a, Message, Renderer> where - Renderer: self::Renderer + container::Renderer, + Renderer: crate::Renderer, { fn width(&self) -> Length { self.width @@ -473,14 +471,43 @@ where .fold(event_status, event::Status::merge) } + fn mouse_interaction( + &self, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) -> mouse::Interaction { + if self.state.picked_pane().is_some() { + return mouse::Interaction::Grab; + } + + if let Some((_, axis)) = self.state.picked_split() { + return match axis { + Axis::Horizontal => mouse::Interaction::ResizingHorizontally, + Axis::Vertical => mouse::Interaction::ResizingVertically, + }; + } + + self.elements + .iter() + .zip(layout.children()) + .map(|((_pane, content), layout)| { + content.mouse_interaction(layout, cursor_position, viewport) + }) + .max() + .unwrap_or_default() + } + fn draw( &self, renderer: &mut Renderer, - defaults: &Renderer::Defaults, + style: &renderer::Style, layout: Layout<'_>, cursor_position: Point, viewport: &Rectangle, - ) -> Renderer::Output { + ) { + let picked_pane = self.state.picked_pane(); + let picked_split = self .state .picked_split() @@ -529,17 +556,89 @@ where None => None, }); - self::Renderer::draw( - renderer, - defaults, - &self.elements, - self.state.picked_pane(), - picked_split, - layout, - &self.style, - cursor_position, - viewport, - ) + let pane_cursor_position = if picked_pane.is_some() { + // TODO: Remove once cursor availability is encoded in the type + // system + Point::new(-1.0, -1.0) + } else { + cursor_position + }; + + for ((id, pane), layout) in self.elements.iter().zip(layout.children()) + { + match picked_pane { + Some((dragging, origin)) if *id == dragging => { + let bounds = layout.bounds(); + + renderer.with_translation( + cursor_position + - Point::new( + bounds.x + origin.x, + bounds.y + origin.y, + ), + |renderer| { + renderer.with_layer(bounds, |renderer| { + pane.draw( + renderer, + style, + layout, + pane_cursor_position, + viewport, + ); + }); + }, + ); + } + _ => { + pane.draw( + renderer, + style, + layout, + pane_cursor_position, + viewport, + ); + } + } + } + + if let Some((axis, split_region, is_picked)) = picked_split { + let highlight = if is_picked { + self.style_sheet.picked_split() + } else { + self.style_sheet.hovered_split() + }; + + if let Some(highlight) = highlight { + renderer.fill_quad( + renderer::Quad { + bounds: match axis { + Axis::Horizontal => Rectangle { + x: split_region.x, + y: (split_region.y + + (split_region.height - highlight.width) + / 2.0) + .round(), + width: split_region.width, + height: highlight.width, + }, + Axis::Vertical => Rectangle { + x: (split_region.x + + (split_region.width - highlight.width) + / 2.0) + .round(), + y: split_region.y, + width: highlight.width, + height: split_region.height, + }, + }, + border_radius: 0.0, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + highlight.color, + ); + } + } } fn hash_layout(&self, state: &mut Hasher) { @@ -569,78 +668,10 @@ where } } -/// The renderer of a [`PaneGrid`]. -/// -/// Your [renderer] will need to implement this trait before being -/// able to use a [`PaneGrid`] in your user interface. -/// -/// [renderer]: crate::renderer -pub trait Renderer: crate::Renderer + container::Renderer + Sized { - /// The style supported by this renderer. - type Style: Default; - - /// Draws a [`PaneGrid`]. - /// - /// It receives: - /// - the elements of the [`PaneGrid`] - /// - the [`Pane`] that is currently being dragged - /// - the [`Axis`] that is currently being resized - /// - the [`Layout`] of the [`PaneGrid`] and its elements - /// - the cursor position - fn draw<Message>( - &mut self, - defaults: &Self::Defaults, - content: &[(Pane, Content<'_, Message, Self>)], - dragging: Option<(Pane, Point)>, - resizing: Option<(Axis, Rectangle, bool)>, - layout: Layout<'_>, - style: &<Self as self::Renderer>::Style, - cursor_position: Point, - viewport: &Rectangle, - ) -> Self::Output; - - /// Draws a [`Pane`]. - /// - /// It receives: - /// - the [`TitleBar`] of the [`Pane`], if any - /// - the [`Content`] of the [`Pane`] - /// - the [`Layout`] of the [`Pane`] and its elements - /// - the cursor position - fn draw_pane<Message>( - &mut self, - defaults: &Self::Defaults, - bounds: Rectangle, - style: &<Self as container::Renderer>::Style, - title_bar: Option<(&TitleBar<'_, Message, Self>, Layout<'_>)>, - body: (&Element<'_, Message, Self>, Layout<'_>), - cursor_position: Point, - viewport: &Rectangle, - ) -> Self::Output; - - /// Draws a [`TitleBar`]. - /// - /// It receives: - /// - the bounds, style of the [`TitleBar`] - /// - the style of the [`TitleBar`] - /// - the content of the [`TitleBar`] with its layout - /// - the controls of the [`TitleBar`] with their [`Layout`], if any - /// - the cursor position - fn draw_title_bar<Message>( - &mut self, - defaults: &Self::Defaults, - bounds: Rectangle, - style: &<Self as container::Renderer>::Style, - content: (&Element<'_, Message, Self>, Layout<'_>), - controls: Option<(&Element<'_, Message, Self>, Layout<'_>)>, - cursor_position: Point, - viewport: &Rectangle, - ) -> Self::Output; -} - impl<'a, Message, Renderer> From<PaneGrid<'a, Message, Renderer>> for Element<'a, Message, Renderer> where - Renderer: 'a + self::Renderer + row::Renderer, + Renderer: 'a + crate::Renderer, Message: 'a, { fn from( diff --git a/native/src/widget/pane_grid/configuration.rs b/native/src/widget/pane_grid/configuration.rs index 4c43826e..4c52bad4 100644 --- a/native/src/widget/pane_grid/configuration.rs +++ b/native/src/widget/pane_grid/configuration.rs @@ -1,4 +1,4 @@ -use crate::pane_grid::Axis; +use crate::widget::pane_grid::Axis; /// The arrangement of a [`PaneGrid`]. /// diff --git a/native/src/widget/pane_grid/content.rs b/native/src/widget/pane_grid/content.rs index bac9fdd4..c44506dd 100644 --- a/native/src/widget/pane_grid/content.rs +++ b/native/src/widget/pane_grid/content.rs @@ -1,30 +1,32 @@ -use crate::container; use crate::event::{self, Event}; use crate::layout; +use crate::mouse; use crate::overlay; -use crate::pane_grid::{self, TitleBar}; +use crate::renderer; +use crate::widget::container; +use crate::widget::pane_grid::TitleBar; use crate::{Clipboard, Element, Hasher, Layout, Point, Rectangle, Size}; /// The content of a [`Pane`]. /// /// [`Pane`]: crate::widget::pane_grid::Pane #[allow(missing_debug_implementations)] -pub struct Content<'a, Message, Renderer: pane_grid::Renderer> { +pub struct Content<'a, Message, Renderer> { title_bar: Option<TitleBar<'a, Message, Renderer>>, body: Element<'a, Message, Renderer>, - style: <Renderer as container::Renderer>::Style, + style_sheet: Box<dyn container::StyleSheet + 'a>, } impl<'a, Message, Renderer> Content<'a, Message, Renderer> where - Renderer: pane_grid::Renderer, + Renderer: crate::Renderer, { /// Creates a new [`Content`] with the provided body. pub fn new(body: impl Into<Element<'a, Message, Renderer>>) -> Self { Self { title_bar: None, body: body.into(), - style: Default::default(), + style_sheet: Default::default(), } } @@ -40,16 +42,16 @@ where /// Sets the style of the [`Content`]. pub fn style( mut self, - style: impl Into<<Renderer as container::Renderer>::Style>, + style_sheet: impl Into<Box<dyn container::StyleSheet + 'a>>, ) -> Self { - self.style = style.into(); + self.style_sheet = style_sheet.into(); self } } impl<'a, Message, Renderer> Content<'a, Message, Renderer> where - Renderer: pane_grid::Renderer, + Renderer: crate::Renderer, { /// Draws the [`Content`] with the provided [`Renderer`] and [`Layout`]. /// @@ -57,35 +59,45 @@ where pub fn draw( &self, renderer: &mut Renderer, - defaults: &Renderer::Defaults, + style: &renderer::Style, layout: Layout<'_>, cursor_position: Point, viewport: &Rectangle, - ) -> Renderer::Output { + ) { + let bounds = layout.bounds(); + + { + let style = self.style_sheet.style(); + + container::draw_background(renderer, &style, bounds); + } + if let Some(title_bar) = &self.title_bar { let mut children = layout.children(); let title_bar_layout = children.next().unwrap(); let body_layout = children.next().unwrap(); - renderer.draw_pane( - defaults, - layout.bounds(), - &self.style, - Some((title_bar, title_bar_layout)), - (&self.body, body_layout), + let show_controls = bounds.contains(cursor_position); + + title_bar.draw( + renderer, + style, + title_bar_layout, cursor_position, viewport, - ) - } else { - renderer.draw_pane( - defaults, - layout.bounds(), - &self.style, - None, - (&self.body, layout), + show_controls, + ); + + self.body.draw( + renderer, + style, + body_layout, cursor_position, viewport, - ) + ); + } else { + self.body + .draw(renderer, style, layout, cursor_position, viewport); } } @@ -186,6 +198,40 @@ where event_status.merge(body_status) } + pub(crate) fn mouse_interaction( + &self, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) -> mouse::Interaction { + let (body_layout, title_bar_interaction) = + if let Some(title_bar) = &self.title_bar { + let mut children = layout.children(); + let title_bar_layout = children.next().unwrap(); + + let is_over_pick_area = title_bar + .is_over_pick_area(title_bar_layout, cursor_position); + + if is_over_pick_area { + return mouse::Interaction::Grab; + } + + let mouse_interaction = title_bar.mouse_interaction( + title_bar_layout, + cursor_position, + viewport, + ); + + (children.next().unwrap(), mouse_interaction) + } else { + (layout, mouse::Interaction::default()) + }; + + self.body + .mouse_interaction(body_layout, cursor_position, viewport) + .max(title_bar_interaction) + } + pub(crate) fn hash_layout(&self, state: &mut Hasher) { if let Some(title_bar) = &self.title_bar { title_bar.hash_layout(state); @@ -215,7 +261,7 @@ where impl<'a, T, Message, Renderer> From<T> for Content<'a, Message, Renderer> where T: Into<Element<'a, Message, Renderer>>, - Renderer: pane_grid::Renderer + container::Renderer, + Renderer: crate::Renderer, { fn from(element: T) -> Self { Self::new(element) diff --git a/native/src/widget/pane_grid/node.rs b/native/src/widget/pane_grid/node.rs index 84714e00..af6573a0 100644 --- a/native/src/widget/pane_grid/node.rs +++ b/native/src/widget/pane_grid/node.rs @@ -1,7 +1,5 @@ -use crate::{ - pane_grid::{Axis, Pane, Split}, - Rectangle, Size, -}; +use crate::widget::pane_grid::{Axis, Pane, Split}; +use crate::{Rectangle, Size}; use std::collections::BTreeMap; diff --git a/native/src/widget/pane_grid/state.rs b/native/src/widget/pane_grid/state.rs index fb96f89f..bcc724a8 100644 --- a/native/src/widget/pane_grid/state.rs +++ b/native/src/widget/pane_grid/state.rs @@ -1,7 +1,7 @@ -use crate::{ - pane_grid::{Axis, Configuration, Direction, Node, Pane, Split}, - Hasher, Point, Rectangle, Size, +use crate::widget::pane_grid::{ + Axis, Configuration, Direction, Node, Pane, Split, }; +use crate::{Hasher, Point, Rectangle, Size}; use std::collections::{BTreeMap, HashMap}; diff --git a/native/src/widget/pane_grid/title_bar.rs b/native/src/widget/pane_grid/title_bar.rs index 070010f8..070cf404 100644 --- a/native/src/widget/pane_grid/title_bar.rs +++ b/native/src/widget/pane_grid/title_bar.rs @@ -1,8 +1,9 @@ -use crate::container; use crate::event::{self, Event}; use crate::layout; +use crate::mouse; use crate::overlay; -use crate::pane_grid; +use crate::renderer; +use crate::widget::container; use crate::{ Clipboard, Element, Hasher, Layout, Padding, Point, Rectangle, Size, }; @@ -11,17 +12,17 @@ use crate::{ /// /// [`Pane`]: crate::widget::pane_grid::Pane #[allow(missing_debug_implementations)] -pub struct TitleBar<'a, Message, Renderer: pane_grid::Renderer> { +pub struct TitleBar<'a, Message, Renderer> { content: Element<'a, Message, Renderer>, controls: Option<Element<'a, Message, Renderer>>, padding: Padding, always_show_controls: bool, - style: <Renderer as container::Renderer>::Style, + style_sheet: Box<dyn container::StyleSheet + 'a>, } impl<'a, Message, Renderer> TitleBar<'a, Message, Renderer> where - Renderer: pane_grid::Renderer, + Renderer: crate::Renderer, { /// Creates a new [`TitleBar`] with the given content. pub fn new<E>(content: E) -> Self @@ -33,7 +34,7 @@ where controls: None, padding: Padding::ZERO, always_show_controls: false, - style: Default::default(), + style_sheet: Default::default(), } } @@ -55,9 +56,9 @@ where /// Sets the style of the [`TitleBar`]. pub fn style( mut self, - style: impl Into<<Renderer as container::Renderer>::Style>, + style: impl Into<Box<dyn container::StyleSheet + 'a>>, ) -> Self { - self.style = style.into(); + self.style_sheet = style.into(); self } @@ -77,7 +78,7 @@ where impl<'a, Message, Renderer> TitleBar<'a, Message, Renderer> where - Renderer: pane_grid::Renderer, + Renderer: crate::Renderer, { /// Draws the [`TitleBar`] with the provided [`Renderer`] and [`Layout`]. /// @@ -85,39 +86,47 @@ where pub fn draw( &self, renderer: &mut Renderer, - defaults: &Renderer::Defaults, + inherited_style: &renderer::Style, layout: Layout<'_>, cursor_position: Point, viewport: &Rectangle, show_controls: bool, - ) -> Renderer::Output { + ) { + let bounds = layout.bounds(); + let style = self.style_sheet.style(); + let inherited_style = renderer::Style { + text_color: style.text_color.unwrap_or(inherited_style.text_color), + }; + + container::draw_background(renderer, &style, bounds); + let mut children = layout.children(); let padded = children.next().unwrap(); let mut children = padded.children(); let title_layout = children.next().unwrap(); - let controls = if let Some(controls) = &self.controls { + self.content.draw( + renderer, + &inherited_style, + title_layout, + cursor_position, + viewport, + ); + + if let Some(controls) = &self.controls { let controls_layout = children.next().unwrap(); if show_controls || self.always_show_controls { - Some((controls, controls_layout)) - } else { - None + controls.draw( + renderer, + &inherited_style, + controls_layout, + cursor_position, + viewport, + ); } - } else { - None - }; - - renderer.draw_title_bar( - defaults, - layout.bounds(), - &self.style, - (&self.content, title_layout), - controls, - cursor_position, - viewport, - ) + } } /// Returns whether the mouse cursor is over the pick area of the @@ -244,6 +253,35 @@ where control_status.merge(title_status) } + pub(crate) fn mouse_interaction( + &self, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) -> mouse::Interaction { + let mut children = layout.children(); + let padded = children.next().unwrap(); + + let mut children = padded.children(); + let title_layout = children.next().unwrap(); + + let title_interaction = self.content.mouse_interaction( + title_layout, + cursor_position, + viewport, + ); + + if let Some(controls) = &self.controls { + let controls_layout = children.next().unwrap(); + + controls + .mouse_interaction(controls_layout, cursor_position, viewport) + .max(title_interaction) + } else { + title_interaction + } + } + pub(crate) fn overlay( &mut self, layout: Layout<'_>, diff --git a/native/src/widget/pick_list.rs b/native/src/widget/pick_list.rs index d7792000..9d1a86ec 100644 --- a/native/src/widget/pick_list.rs +++ b/native/src/widget/pick_list.rs @@ -1,12 +1,13 @@ //! Display a dropdown list of selectable values. +use crate::alignment; use crate::event::{self, Event}; use crate::keyboard; use crate::layout; use crate::mouse; use crate::overlay; use crate::overlay::menu::{self, Menu}; -use crate::scrollable; -use crate::text; +use crate::renderer; +use crate::text::{self, Text}; use crate::touch; use crate::{ Clipboard, Element, Hasher, Layout, Length, Padding, Point, Rectangle, @@ -14,9 +15,11 @@ use crate::{ }; use std::borrow::Cow; +pub use iced_style::pick_list::{Style, StyleSheet}; + /// A widget for selecting a single value from a list of options. #[allow(missing_debug_implementations)] -pub struct PickList<'a, T, Message, Renderer: self::Renderer> +pub struct PickList<'a, T, Message, Renderer: text::Renderer> where [T]: ToOwned<Owned = Vec<T>>, { @@ -33,7 +36,7 @@ where padding: Padding, text_size: Option<u16>, font: Renderer::Font, - style: <Renderer as self::Renderer>::Style, + style_sheet: Box<dyn StyleSheet + 'a>, } /// The local state of a [`PickList`]. @@ -58,12 +61,15 @@ impl<T> Default for State<T> { } } -impl<'a, T: 'a, Message, Renderer: self::Renderer> +impl<'a, T: 'a, Message, Renderer: text::Renderer> PickList<'a, T, Message, Renderer> where T: ToString + Eq, [T]: ToOwned<Owned = Vec<T>>, { + /// The default padding of a [`PickList`]. + pub const DEFAULT_PADDING: Padding = Padding::new(5); + /// Creates a new [`PickList`] with the given [`State`], a list of options, /// the current selected value, and the message to produce when an option is /// selected. @@ -93,9 +99,9 @@ where selected, width: Length::Shrink, text_size: None, - padding: Renderer::DEFAULT_PADDING, + padding: Self::DEFAULT_PADDING, font: Default::default(), - style: Default::default(), + style_sheet: Default::default(), } } @@ -132,9 +138,9 @@ where /// Sets the style of the [`PickList`]. pub fn style( mut self, - style: impl Into<<Renderer as self::Renderer>::Style>, + style_sheet: impl Into<Box<dyn StyleSheet + 'a>>, ) -> Self { - self.style = style.into(); + self.style_sheet = style_sheet.into(); self } } @@ -145,7 +151,7 @@ where T: Clone + ToString + Eq, [T]: ToOwned<Owned = Vec<T>>, Message: 'static, - Renderer: self::Renderer + scrollable::Renderer + 'a, + Renderer: text::Renderer + 'a, { fn width(&self) -> Length { self.width @@ -320,25 +326,90 @@ where } } + fn mouse_interaction( + &self, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) -> mouse::Interaction { + let bounds = layout.bounds(); + let is_mouse_over = bounds.contains(cursor_position); + + if is_mouse_over { + mouse::Interaction::Pointer + } else { + mouse::Interaction::default() + } + } + fn draw( &self, renderer: &mut Renderer, - _defaults: &Renderer::Defaults, + _style: &renderer::Style, layout: Layout<'_>, cursor_position: Point, _viewport: &Rectangle, - ) -> Renderer::Output { - self::Renderer::draw( - renderer, - layout.bounds(), - cursor_position, - self.selected.as_ref().map(ToString::to_string), - self.placeholder.as_ref().map(String::as_str), - self.padding, - self.text_size.unwrap_or(renderer.default_size()), - self.font, - &self.style, - ) + ) { + let bounds = layout.bounds(); + let is_mouse_over = bounds.contains(cursor_position); + let is_selected = self.selected.is_some(); + + let style = if is_mouse_over { + self.style_sheet.hovered() + } else { + self.style_sheet.active() + }; + + renderer.fill_quad( + renderer::Quad { + bounds, + border_color: style.border_color, + border_width: style.border_width, + border_radius: style.border_radius, + }, + style.background, + ); + + renderer.fill_text(Text { + content: &Renderer::ARROW_DOWN_ICON.to_string(), + font: Renderer::ICON_FONT, + size: bounds.height * style.icon_size, + bounds: Rectangle { + x: bounds.x + bounds.width + - f32::from(self.padding.horizontal()), + y: bounds.center_y(), + ..bounds + }, + color: style.text_color, + horizontal_alignment: alignment::Horizontal::Right, + vertical_alignment: alignment::Vertical::Center, + }); + + if let Some(label) = self + .selected + .as_ref() + .map(ToString::to_string) + .as_ref() + .or_else(|| self.placeholder.as_ref()) + { + renderer.fill_text(Text { + content: label, + size: f32::from( + self.text_size.unwrap_or(renderer.default_size()), + ), + font: self.font, + color: is_selected + .then(|| style.text_color) + .unwrap_or(style.placeholder_color), + bounds: Rectangle { + x: bounds.x + f32::from(self.padding.left), + y: bounds.center_y(), + ..bounds + }, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Center, + }) + } } fn overlay( @@ -357,7 +428,7 @@ where .width(bounds.width.round() as u16) .padding(self.padding) .font(self.font) - .style(Renderer::menu_style(&self.style)); + .style(self.style_sheet.menu()); if let Some(text_size) = self.text_size { menu = menu.text_size(text_size); @@ -370,44 +441,12 @@ where } } -/// The renderer of a [`PickList`]. -/// -/// Your [renderer] will need to implement this trait before being -/// able to use a [`PickList`] in your user interface. -/// -/// [renderer]: crate::renderer -pub trait Renderer: text::Renderer + menu::Renderer { - /// The default padding of a [`PickList`]. - const DEFAULT_PADDING: Padding; - - /// The [`PickList`] style supported by this renderer. - type Style: Default; - - /// Returns the style of the [`Menu`] of the [`PickList`]. - fn menu_style( - style: &<Self as Renderer>::Style, - ) -> <Self as menu::Renderer>::Style; - - /// Draws a [`PickList`]. - fn draw( - &mut self, - bounds: Rectangle, - cursor_position: Point, - selected: Option<String>, - placeholder: Option<&str>, - padding: Padding, - text_size: u16, - font: Self::Font, - style: &<Self as Renderer>::Style, - ) -> Self::Output; -} - impl<'a, T: 'a, Message, Renderer> Into<Element<'a, Message, Renderer>> for PickList<'a, T, Message, Renderer> where T: Clone + ToString + Eq, [T]: ToOwned<Owned = Vec<T>>, - Renderer: self::Renderer + 'a, + Renderer: text::Renderer + 'a, Message: 'static, { fn into(self) -> Element<'a, Message, Renderer> { diff --git a/native/src/widget/progress_bar.rs b/native/src/widget/progress_bar.rs index d294f198..69eb8c09 100644 --- a/native/src/widget/progress_bar.rs +++ b/native/src/widget/progress_bar.rs @@ -1,17 +1,19 @@ //! Provide progress feedback to your users. +use crate::layout; +use crate::renderer; use crate::{ - layout, Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget, + Color, Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget, }; use std::{hash::Hash, ops::RangeInclusive}; +pub use iced_style::progress_bar::{Style, StyleSheet}; + /// A bar that displays progress. /// /// # Example /// ``` -/// # use iced_native::renderer::Null; -/// # -/// # pub type ProgressBar = iced_native::ProgressBar<Null>; +/// # use iced_native::widget::ProgressBar; /// let value = 50.0; /// /// ProgressBar::new(0.0..=100.0, value); @@ -19,15 +21,18 @@ use std::{hash::Hash, ops::RangeInclusive}; /// ///  #[allow(missing_debug_implementations)] -pub struct ProgressBar<Renderer: self::Renderer> { +pub struct ProgressBar<'a> { range: RangeInclusive<f32>, value: f32, width: Length, height: Option<Length>, - style: Renderer::Style, + style_sheet: Box<dyn StyleSheet + 'a>, } -impl<Renderer: self::Renderer> ProgressBar<Renderer> { +impl<'a> ProgressBar<'a> { + /// The default height of a [`ProgressBar`]. + pub const DEFAULT_HEIGHT: u16 = 30; + /// Creates a new [`ProgressBar`]. /// /// It expects: @@ -39,7 +44,7 @@ impl<Renderer: self::Renderer> ProgressBar<Renderer> { range, width: Length::Fill, height: None, - style: Renderer::Style::default(), + style_sheet: Default::default(), } } @@ -56,23 +61,25 @@ impl<Renderer: self::Renderer> ProgressBar<Renderer> { } /// Sets the style of the [`ProgressBar`]. - pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { - self.style = style.into(); + pub fn style( + mut self, + style_sheet: impl Into<Box<dyn StyleSheet + 'a>>, + ) -> Self { + self.style_sheet = style_sheet.into(); self } } -impl<Message, Renderer> Widget<Message, Renderer> for ProgressBar<Renderer> +impl<'a, Message, Renderer> Widget<Message, Renderer> for ProgressBar<'a> where - Renderer: self::Renderer, + Renderer: crate::Renderer, { fn width(&self) -> Length { self.width } fn height(&self) -> Length { - self.height - .unwrap_or(Length::Units(Renderer::DEFAULT_HEIGHT)) + self.height.unwrap_or(Length::Units(Self::DEFAULT_HEIGHT)) } fn layout( @@ -80,10 +87,9 @@ where _renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let limits = limits.width(self.width).height( - self.height - .unwrap_or(Length::Units(Renderer::DEFAULT_HEIGHT)), - ); + let limits = limits + .width(self.width) + .height(self.height.unwrap_or(Length::Units(Self::DEFAULT_HEIGHT))); let size = limits.resolve(Size::ZERO); @@ -93,17 +99,47 @@ where fn draw( &self, renderer: &mut Renderer, - _defaults: &Renderer::Defaults, + _style: &renderer::Style, layout: Layout<'_>, _cursor_position: Point, _viewport: &Rectangle, - ) -> Renderer::Output { - renderer.draw( - layout.bounds(), - self.range.clone(), - self.value, - &self.style, - ) + ) { + let bounds = layout.bounds(); + let (range_start, range_end) = self.range.clone().into_inner(); + + let active_progress_width = if range_start >= range_end { + 0.0 + } else { + bounds.width * (self.value - range_start) + / (range_end - range_start) + }; + + let style = self.style_sheet.style(); + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { ..bounds }, + border_radius: style.border_radius, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + style.background, + ); + + if active_progress_width > 0.0 { + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + width: active_progress_width, + ..bounds + }, + border_radius: style.border_radius, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + style.bar, + ); + } } fn hash_layout(&self, state: &mut Hasher) { @@ -115,45 +151,13 @@ where } } -/// The renderer of a [`ProgressBar`]. -/// -/// Your [renderer] will need to implement this trait before being -/// able to use a [`ProgressBar`] in your user interface. -/// -/// [renderer]: crate::renderer -pub trait Renderer: crate::Renderer { - /// The style supported by this renderer. - type Style: Default; - - /// The default height of a [`ProgressBar`]. - const DEFAULT_HEIGHT: u16; - - /// Draws a [`ProgressBar`]. - /// - /// It receives: - /// * the bounds of the [`ProgressBar`] - /// * the range of values of the [`ProgressBar`] - /// * the current value of the [`ProgressBar`] - /// * maybe a specific background of the [`ProgressBar`] - /// * maybe a specific active color of the [`ProgressBar`] - fn draw( - &self, - bounds: Rectangle, - range: RangeInclusive<f32>, - value: f32, - style: &Self::Style, - ) -> Self::Output; -} - -impl<'a, Message, Renderer> From<ProgressBar<Renderer>> +impl<'a, Message, Renderer> From<ProgressBar<'a>> for Element<'a, Message, Renderer> where - Renderer: 'a + self::Renderer, + Renderer: 'a + crate::Renderer, Message: 'a, { - fn from( - progress_bar: ProgressBar<Renderer>, - ) -> Element<'a, Message, Renderer> { + fn from(progress_bar: ProgressBar<'a>) -> Element<'a, Message, Renderer> { Element::new(progress_bar) } } diff --git a/native/src/widget/radio.rs b/native/src/widget/radio.rs index 513b2fce..86ad4c4e 100644 --- a/native/src/widget/radio.rs +++ b/native/src/widget/radio.rs @@ -1,24 +1,27 @@ //! Create choices using radio buttons. use std::hash::Hash; -use crate::alignment::{self, Alignment}; +use crate::alignment; use crate::event::{self, Event}; use crate::layout; use crate::mouse; -use crate::row; +use crate::renderer; use crate::text; use crate::touch; +use crate::widget::{self, Row, Text}; use crate::{ - Clipboard, Color, Element, Hasher, Layout, Length, Point, Rectangle, Row, - Text, Widget, + Alignment, Clipboard, Color, Element, Hasher, Layout, Length, Point, + Rectangle, Widget, }; +pub use iced_style::radio::{Style, StyleSheet}; + /// A circular button representing a choice. /// /// # Example /// ``` -/// # type Radio<Message> = -/// # iced_native::Radio<Message, iced_native::renderer::Null>; +/// # type Radio<'a, Message> = +/// # iced_native::widget::Radio<'a, Message, iced_native::renderer::Null>; /// # /// #[derive(Debug, Clone, Copy, PartialEq, Eq)] /// pub enum Choice { @@ -40,7 +43,7 @@ use crate::{ /// ///  #[allow(missing_debug_implementations)] -pub struct Radio<Message, Renderer: self::Renderer + text::Renderer> { +pub struct Radio<'a, Message, Renderer: text::Renderer> { is_selected: bool, on_click: Message, label: String, @@ -50,14 +53,19 @@ pub struct Radio<Message, Renderer: self::Renderer + text::Renderer> { text_size: Option<u16>, text_color: Option<Color>, font: Renderer::Font, - style: Renderer::Style, + style_sheet: Box<dyn StyleSheet + 'a>, } -impl<Message, Renderer: self::Renderer + text::Renderer> - Radio<Message, Renderer> +impl<'a, Message, Renderer: text::Renderer> Radio<'a, Message, Renderer> where Message: Clone, { + /// The default size of a [`Radio`] button. + pub const DEFAULT_SIZE: u16 = 28; + + /// The default spacing of a [`Radio`] button. + pub const DEFAULT_SPACING: u16 = 15; + /// Creates a new [`Radio`] button. /// /// It expects: @@ -81,12 +89,12 @@ where on_click: f(value), label: label.into(), width: Length::Shrink, - size: <Renderer as self::Renderer>::DEFAULT_SIZE, - spacing: Renderer::DEFAULT_SPACING, //15 + size: Self::DEFAULT_SIZE, + spacing: Self::DEFAULT_SPACING, //15 text_size: None, text_color: None, font: Default::default(), - style: Renderer::Style::default(), + style_sheet: Default::default(), } } @@ -127,16 +135,20 @@ where } /// Sets the style of the [`Radio`] button. - pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { - self.style = style.into(); + pub fn style( + mut self, + style_sheet: impl Into<Box<dyn StyleSheet + 'a>>, + ) -> Self { + self.style_sheet = style_sheet.into(); self } } -impl<Message, Renderer> Widget<Message, Renderer> for Radio<Message, Renderer> +impl<'a, Message, Renderer> Widget<Message, Renderer> + for Radio<'a, Message, Renderer> where Message: Clone, - Renderer: self::Renderer + text::Renderer + row::Renderer, + Renderer: text::Renderer, { fn width(&self) -> Length { self.width @@ -192,43 +204,88 @@ where event::Status::Ignored } + fn mouse_interaction( + &self, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) -> mouse::Interaction { + if layout.bounds().contains(cursor_position) { + mouse::Interaction::Pointer + } else { + mouse::Interaction::default() + } + } + fn draw( &self, renderer: &mut Renderer, - defaults: &Renderer::Defaults, + style: &renderer::Style, layout: Layout<'_>, cursor_position: Point, _viewport: &Rectangle, - ) -> Renderer::Output { + ) { let bounds = layout.bounds(); + let is_mouse_over = bounds.contains(cursor_position); + let mut children = layout.children(); - let radio_layout = children.next().unwrap(); - let label_layout = children.next().unwrap(); - let radio_bounds = radio_layout.bounds(); - - let label = text::Renderer::draw( - renderer, - defaults, - label_layout.bounds(), - &self.label, - self.text_size.unwrap_or(renderer.default_size()), - self.font, - self.text_color, - alignment::Horizontal::Left, - alignment::Vertical::Center, - ); + { + let layout = children.next().unwrap(); + let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); + let size = bounds.width; + let dot_size = size / 2.0; + + let style = if is_mouse_over { + self.style_sheet.hovered() + } else { + self.style_sheet.active() + }; + + renderer.fill_quad( + renderer::Quad { + bounds, + border_radius: size / 2.0, + border_width: style.border_width, + border_color: style.border_color, + }, + style.background, + ); - self::Renderer::draw( - renderer, - radio_bounds, - self.is_selected, - is_mouse_over, - label, - &self.style, - ) + if self.is_selected { + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x + dot_size / 2.0, + y: bounds.y + dot_size / 2.0, + width: bounds.width - dot_size, + height: bounds.height - dot_size, + }, + border_radius: dot_size / 2.0, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + style.dot_color, + ); + } + } + + { + let label_layout = children.next().unwrap(); + + widget::text::draw( + renderer, + style, + label_layout, + &self.label, + self.font, + self.text_size, + self.text_color, + alignment::Horizontal::Left, + alignment::Vertical::Center, + ); + } } fn hash_layout(&self, state: &mut Hasher) { @@ -239,46 +296,15 @@ where } } -/// The renderer of a [`Radio`] button. -/// -/// Your [renderer] will need to implement this trait before being -/// able to use a [`Radio`] button in your user interface. -/// -/// [renderer]: crate::renderer -pub trait Renderer: crate::Renderer { - /// The style supported by this renderer. - type Style: Default; - - /// The default size of a [`Radio`] button. - const DEFAULT_SIZE: u16; - - /// The default spacing of a [`Radio`] button. - const DEFAULT_SPACING: u16; - - /// Draws a [`Radio`] button. - /// - /// It receives: - /// * the bounds of the [`Radio`] - /// * whether the [`Radio`] is selected or not - /// * whether the mouse is over the [`Radio`] or not - /// * the drawn label of the [`Radio`] - fn draw( - &mut self, - bounds: Rectangle, - is_selected: bool, - is_mouse_over: bool, - label: Self::Output, - style: &Self::Style, - ) -> Self::Output; -} - -impl<'a, Message, Renderer> From<Radio<Message, Renderer>> +impl<'a, Message, Renderer> From<Radio<'a, Message, Renderer>> for Element<'a, Message, Renderer> where Message: 'a + Clone, - Renderer: 'a + self::Renderer + row::Renderer + text::Renderer, + Renderer: 'a + text::Renderer, { - fn from(radio: Radio<Message, Renderer>) -> Element<'a, Message, Renderer> { + fn from( + radio: Radio<'a, Message, Renderer>, + ) -> Element<'a, Message, Renderer> { Element::new(radio) } } diff --git a/native/src/widget/row.rs b/native/src/widget/row.rs index 1923f213..6fe3284b 100644 --- a/native/src/widget/row.rs +++ b/native/src/widget/row.rs @@ -1,7 +1,9 @@ //! Distribute content horizontally. use crate::event::{self, Event}; use crate::layout; +use crate::mouse; use crate::overlay; +use crate::renderer; use crate::{ Alignment, Clipboard, Element, Hasher, Layout, Length, Padding, Point, Rectangle, Widget, @@ -104,7 +106,7 @@ impl<'a, Message, Renderer> Row<'a, Message, Renderer> { impl<'a, Message, Renderer> Widget<Message, Renderer> for Row<'a, Message, Renderer> where - Renderer: self::Renderer, + Renderer: crate::Renderer, { fn width(&self) -> Length { self.width @@ -161,21 +163,37 @@ where .fold(event::Status::Ignored, event::Status::merge) } + fn mouse_interaction( + &self, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) -> mouse::Interaction { + self.children + .iter() + .zip(layout.children()) + .map(|(child, layout)| { + child.widget.mouse_interaction( + layout, + cursor_position, + viewport, + ) + }) + .max() + .unwrap_or_default() + } + fn draw( &self, renderer: &mut Renderer, - defaults: &Renderer::Defaults, + style: &renderer::Style, layout: Layout<'_>, cursor_position: Point, viewport: &Rectangle, - ) -> Renderer::Output { - renderer.draw( - defaults, - &self.children, - layout, - cursor_position, - viewport, - ) + ) { + for (child, layout) in self.children.iter().zip(layout.children()) { + child.draw(renderer, style, layout, cursor_position, viewport); + } } fn hash_layout(&self, state: &mut Hasher) { @@ -207,33 +225,10 @@ where } } -/// The renderer of a [`Row`]. -/// -/// Your [renderer] will need to implement this trait before being -/// able to use a [`Row`] in your user interface. -/// -/// [renderer]: crate::renderer -pub trait Renderer: crate::Renderer + Sized { - /// Draws a [`Row`]. - /// - /// It receives: - /// - the children of the [`Row`] - /// - the [`Layout`] of the [`Row`] and its children - /// - the cursor position - fn draw<Message>( - &mut self, - defaults: &Self::Defaults, - children: &[Element<'_, Message, Self>], - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - ) -> Self::Output; -} - impl<'a, Message, Renderer> From<Row<'a, Message, Renderer>> for Element<'a, Message, Renderer> where - Renderer: 'a + self::Renderer, + Renderer: 'a + crate::Renderer, Message: 'a, { fn from(row: Row<'a, Message, Renderer>) -> Element<'a, Message, Renderer> { diff --git a/native/src/widget/rule.rs b/native/src/widget/rule.rs index 18c88658..7c8c5dbc 100644 --- a/native/src/widget/rule.rs +++ b/native/src/widget/rule.rs @@ -1,28 +1,31 @@ //! Display a horizontal or vertical rule for dividing content. +use crate::layout; +use crate::renderer; +use crate::{ + Color, Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget, +}; use std::hash::Hash; -use crate::{ - layout, Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget, -}; +pub use iced_style::rule::{FillMode, Style, StyleSheet}; /// Display a horizontal or vertical rule for dividing content. -#[derive(Debug, Copy, Clone)] -pub struct Rule<Renderer: self::Renderer> { +#[allow(missing_debug_implementations)] +pub struct Rule<'a> { width: Length, height: Length, - style: Renderer::Style, is_horizontal: bool, + style_sheet: Box<dyn StyleSheet + 'a>, } -impl<Renderer: self::Renderer> Rule<Renderer> { +impl<'a> Rule<'a> { /// Creates a horizontal [`Rule`] for dividing content by the given vertical spacing. pub fn horizontal(spacing: u16) -> Self { Rule { width: Length::Fill, height: Length::from(Length::Units(spacing)), - style: Renderer::Style::default(), is_horizontal: true, + style_sheet: Default::default(), } } @@ -31,21 +34,24 @@ impl<Renderer: self::Renderer> Rule<Renderer> { Rule { width: Length::from(Length::Units(spacing)), height: Length::Fill, - style: Renderer::Style::default(), is_horizontal: false, + style_sheet: Default::default(), } } /// Sets the style of the [`Rule`]. - pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { - self.style = style.into(); + pub fn style( + mut self, + style_sheet: impl Into<Box<dyn StyleSheet + 'a>>, + ) -> Self { + self.style_sheet = style_sheet.into(); self } } -impl<Message, Renderer> Widget<Message, Renderer> for Rule<Renderer> +impl<'a, Message, Renderer> Widget<Message, Renderer> for Rule<'a> where - Renderer: self::Renderer, + Renderer: crate::Renderer, { fn width(&self) -> Length { self.width @@ -68,12 +74,53 @@ where fn draw( &self, renderer: &mut Renderer, - _defaults: &Renderer::Defaults, + _style: &renderer::Style, layout: Layout<'_>, _cursor_position: Point, _viewport: &Rectangle, - ) -> Renderer::Output { - renderer.draw(layout.bounds(), &self.style, self.is_horizontal) + ) { + let bounds = layout.bounds(); + let style = self.style_sheet.style(); + + let bounds = if self.is_horizontal { + let line_y = (bounds.y + (bounds.height / 2.0) + - (style.width as f32 / 2.0)) + .round(); + + let (offset, line_width) = style.fill_mode.fill(bounds.width); + let line_x = bounds.x + offset; + + Rectangle { + x: line_x, + y: line_y, + width: line_width, + height: style.width as f32, + } + } else { + let line_x = (bounds.x + (bounds.width / 2.0) + - (style.width as f32 / 2.0)) + .round(); + + let (offset, line_height) = style.fill_mode.fill(bounds.height); + let line_y = bounds.y + offset; + + Rectangle { + x: line_x, + y: line_y, + width: style.width as f32, + height: line_height, + } + }; + + renderer.fill_quad( + renderer::Quad { + bounds, + border_radius: style.radius, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + style.color, + ); } fn hash_layout(&self, state: &mut Hasher) { @@ -85,32 +132,12 @@ where } } -/// The renderer of a [`Rule`]. -pub trait Renderer: crate::Renderer { - /// The style supported by this renderer. - type Style: Default; - - /// Draws a [`Rule`]. - /// - /// It receives: - /// * the bounds of the [`Rule`] - /// * the style of the [`Rule`] - /// * whether the [`Rule`] is horizontal (true) or vertical (false) - fn draw( - &mut self, - bounds: Rectangle, - style: &Self::Style, - is_horizontal: bool, - ) -> Self::Output; -} - -impl<'a, Message, Renderer> From<Rule<Renderer>> - for Element<'a, Message, Renderer> +impl<'a, Message, Renderer> From<Rule<'a>> for Element<'a, Message, Renderer> where - Renderer: 'a + self::Renderer, + Renderer: 'a + crate::Renderer, Message: 'a, { - fn from(rule: Rule<Renderer>) -> Element<'a, Message, Renderer> { + fn from(rule: Rule<'a>) -> Element<'a, Message, Renderer> { Element::new(rule) } } diff --git a/native/src/widget/scrollable.rs b/native/src/widget/scrollable.rs index a8e467d3..2bf2ea5e 100644 --- a/native/src/widget/scrollable.rs +++ b/native/src/widget/scrollable.rs @@ -1,21 +1,24 @@ //! Navigate an endless amount of content with a scrollbar. -use crate::column; use crate::event::{self, Event}; use crate::layout; use crate::mouse; use crate::overlay; +use crate::renderer; use crate::touch; +use crate::widget::Column; use crate::{ - Alignment, Clipboard, Column, Element, Hasher, Layout, Length, Padding, - Point, Rectangle, Size, Vector, Widget, + Alignment, Background, Clipboard, Color, Element, Hasher, Layout, Length, + Padding, Point, Rectangle, Size, Vector, Widget, }; use std::{f32, hash::Hash, u32}; +pub use iced_style::scrollable::StyleSheet; + /// A widget that can vertically display an infinite amount of content with a /// scrollbar. #[allow(missing_debug_implementations)] -pub struct Scrollable<'a, Message, Renderer: self::Renderer> { +pub struct Scrollable<'a, Message, Renderer> { state: &'a mut State, height: Length, max_height: u32, @@ -24,10 +27,10 @@ pub struct Scrollable<'a, Message, Renderer: self::Renderer> { scroller_width: u16, content: Column<'a, Message, Renderer>, on_scroll: Option<Box<dyn Fn(f32) -> Message>>, - style: Renderer::Style, + style_sheet: Box<dyn StyleSheet + 'a>, } -impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> { +impl<'a, Message, Renderer: crate::Renderer> Scrollable<'a, Message, Renderer> { /// Creates a new [`Scrollable`] with the given [`State`]. pub fn new(state: &'a mut State) -> Self { Scrollable { @@ -39,7 +42,7 @@ impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> { scroller_width: 10, content: Column::new(), on_scroll: None, - style: Renderer::Style::default(), + style_sheet: Default::default(), } } @@ -120,8 +123,11 @@ impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> { } /// Sets the style of the [`Scrollable`] . - pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { - self.style = style.into(); + pub fn style( + mut self, + style_sheet: impl Into<Box<dyn StyleSheet + 'a>>, + ) -> Self { + self.style_sheet = style_sheet.into(); self } @@ -151,12 +157,63 @@ impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> { )); } } + + fn scrollbar( + &self, + bounds: Rectangle, + content_bounds: Rectangle, + ) -> Option<Scrollbar> { + let offset = self.state.offset(bounds, content_bounds); + + if content_bounds.height > bounds.height { + let outer_width = self.scrollbar_width.max(self.scroller_width) + + 2 * self.scrollbar_margin; + + let outer_bounds = Rectangle { + x: bounds.x + bounds.width - outer_width as f32, + y: bounds.y, + width: outer_width as f32, + height: bounds.height, + }; + + let scrollbar_bounds = Rectangle { + x: bounds.x + bounds.width + - f32::from(outer_width / 2 + self.scrollbar_width / 2), + y: bounds.y, + width: self.scrollbar_width as f32, + height: bounds.height, + }; + + let ratio = bounds.height / content_bounds.height; + let scroller_height = bounds.height * ratio; + let y_offset = offset as f32 * ratio; + + let scroller_bounds = Rectangle { + x: bounds.x + bounds.width + - f32::from(outer_width / 2 + self.scroller_width / 2), + y: scrollbar_bounds.y + y_offset, + width: self.scroller_width as f32, + height: scroller_height, + }; + + Some(Scrollbar { + outer_bounds, + bounds: scrollbar_bounds, + margin: self.scrollbar_margin, + scroller: Scroller { + bounds: scroller_bounds, + }, + }) + } else { + None + } + } } impl<'a, Message, Renderer> Widget<Message, Renderer> for Scrollable<'a, Message, Renderer> where - Renderer: self::Renderer, + Renderer: crate::Renderer, { fn width(&self) -> Length { Widget::<Message, Renderer>::width(&self.content) @@ -202,15 +259,7 @@ where let content = layout.children().next().unwrap(); let content_bounds = content.bounds(); - let offset = self.state.offset(bounds, content_bounds); - let scrollbar = renderer.scrollbar( - bounds, - content_bounds, - offset, - self.scrollbar_width, - self.scrollbar_margin, - self.scroller_width, - ); + let scrollbar = self.scrollbar(bounds, content_bounds); let is_mouse_over_scrollbar = scrollbar .as_ref() .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) @@ -374,26 +423,16 @@ where event::Status::Ignored } - fn draw( + fn mouse_interaction( &self, - renderer: &mut Renderer, - defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, _viewport: &Rectangle, - ) -> Renderer::Output { + ) -> mouse::Interaction { let bounds = layout.bounds(); let content_layout = layout.children().next().unwrap(); let content_bounds = content_layout.bounds(); - let offset = self.state.offset(bounds, content_bounds); - let scrollbar = renderer.scrollbar( - bounds, - content_bounds, - offset, - self.scrollbar_width, - self.scrollbar_margin, - self.scroller_width, - ); + let scrollbar = self.scrollbar(bounds, content_bounds); let is_mouse_over = bounds.contains(cursor_position); let is_mouse_over_scrollbar = scrollbar @@ -401,16 +440,18 @@ where .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) .unwrap_or(false); - let content = { + if is_mouse_over_scrollbar || self.state.is_scroller_grabbed() { + mouse::Interaction::Idle + } else { + let offset = self.state.offset(bounds, content_bounds); + let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar { Point::new(cursor_position.x, cursor_position.y + offset as f32) } else { Point::new(cursor_position.x, -1.0) }; - self.content.draw( - renderer, - defaults, + self.content.mouse_interaction( content_layout, cursor_position, &Rectangle { @@ -418,20 +459,114 @@ where ..bounds }, ) + } + } + + fn draw( + &self, + renderer: &mut Renderer, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) { + let bounds = layout.bounds(); + let content_layout = layout.children().next().unwrap(); + let content_bounds = content_layout.bounds(); + let offset = self.state.offset(bounds, content_bounds); + let scrollbar = self.scrollbar(bounds, content_bounds); + + let is_mouse_over = bounds.contains(cursor_position); + let is_mouse_over_scrollbar = scrollbar + .as_ref() + .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) + .unwrap_or(false); + + let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar { + Point::new(cursor_position.x, cursor_position.y + offset as f32) + } else { + Point::new(cursor_position.x, -1.0) }; - self::Renderer::draw( - renderer, - &self.state, - bounds, - content_layout.bounds(), - is_mouse_over, - is_mouse_over_scrollbar, - scrollbar, - offset, - &self.style, - content, - ) + if let Some(scrollbar) = scrollbar { + renderer.with_layer(bounds, |renderer| { + renderer.with_translation( + Vector::new(0.0, -(offset as f32)), + |renderer| { + self.content.draw( + renderer, + style, + content_layout, + cursor_position, + &Rectangle { + y: bounds.y + offset as f32, + ..bounds + }, + ); + }, + ); + }); + + let style = if self.state.is_scroller_grabbed() { + self.style_sheet.dragging() + } else if is_mouse_over_scrollbar { + self.style_sheet.hovered() + } else { + self.style_sheet.active() + }; + + let is_scrollbar_visible = + style.background.is_some() || style.border_width > 0.0; + + renderer.with_layer( + Rectangle { + width: bounds.width + 2.0, + height: bounds.height + 2.0, + ..bounds + }, + |renderer| { + if is_scrollbar_visible { + renderer.fill_quad( + renderer::Quad { + bounds: scrollbar.bounds, + border_radius: style.border_radius, + border_width: style.border_width, + border_color: style.border_color, + }, + style.background.unwrap_or(Background::Color( + Color::TRANSPARENT, + )), + ); + } + + if is_mouse_over + || self.state.is_scroller_grabbed() + || is_scrollbar_visible + { + renderer.fill_quad( + renderer::Quad { + bounds: scrollbar.scroller.bounds, + border_radius: style.scroller.border_radius, + border_width: style.scroller.border_width, + border_color: style.scroller.border_color, + }, + style.scroller.color, + ); + } + }, + ); + } else { + self.content.draw( + renderer, + style, + content_layout, + cursor_position, + &Rectangle { + y: bounds.y + offset as f32, + ..bounds + }, + ); + } } fn hash_layout(&self, state: &mut Hasher) { @@ -577,19 +712,19 @@ impl State { /// The scrollbar of a [`Scrollable`]. #[derive(Debug)] -pub struct Scrollbar { +struct Scrollbar { /// The outer bounds of the scrollable, including the [`Scrollbar`] and /// [`Scroller`]. - pub outer_bounds: Rectangle, + outer_bounds: Rectangle, /// The bounds of the [`Scrollbar`]. - pub bounds: Rectangle, + bounds: Rectangle, /// The margin within the [`Scrollbar`]. - pub margin: u16, + margin: u16, /// The bounds of the [`Scroller`]. - pub scroller: Scroller, + scroller: Scroller, } impl Scrollbar { @@ -624,62 +759,15 @@ impl Scrollbar { /// The handle of a [`Scrollbar`]. #[derive(Debug, Clone, Copy)] -pub struct Scroller { +struct Scroller { /// The bounds of the [`Scroller`]. - pub bounds: Rectangle, -} - -/// The renderer of a [`Scrollable`]. -/// -/// Your [renderer] will need to implement this trait before being -/// able to use a [`Scrollable`] in your user interface. -/// -/// [renderer]: crate::renderer -pub trait Renderer: column::Renderer + Sized { - /// The style supported by this renderer. - type Style: Default; - - /// Returns the [`Scrollbar`] given the bounds and content bounds of a - /// [`Scrollable`]. - fn scrollbar( - &self, - bounds: Rectangle, - content_bounds: Rectangle, - offset: u32, - scrollbar_width: u16, - scrollbar_margin: u16, - scroller_width: u16, - ) -> Option<Scrollbar>; - - /// Draws the [`Scrollable`]. - /// - /// It receives: - /// - the [`State`] of the [`Scrollable`] - /// - the bounds of the [`Scrollable`] widget - /// - the bounds of the [`Scrollable`] content - /// - whether the mouse is over the [`Scrollable`] or not - /// - whether the mouse is over the [`Scrollbar`] or not - /// - a optional [`Scrollbar`] to be rendered - /// - the scrolling offset - /// - the drawn content - fn draw( - &mut self, - scrollable: &State, - bounds: Rectangle, - content_bounds: Rectangle, - is_mouse_over: bool, - is_mouse_over_scrollbar: bool, - scrollbar: Option<Scrollbar>, - offset: u32, - style: &Self::Style, - content: Self::Output, - ) -> Self::Output; + bounds: Rectangle, } impl<'a, Message, Renderer> From<Scrollable<'a, Message, Renderer>> for Element<'a, Message, Renderer> where - Renderer: 'a + self::Renderer, + Renderer: 'a + crate::Renderer, Message: 'a, { fn from( diff --git a/native/src/widget/slider.rs b/native/src/widget/slider.rs index 2a74d5a3..49bafab4 100644 --- a/native/src/widget/slider.rs +++ b/native/src/widget/slider.rs @@ -4,12 +4,17 @@ use crate::event::{self, Event}; use crate::layout; use crate::mouse; +use crate::renderer; use crate::touch; use crate::{ - Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget, + Background, Clipboard, Color, Element, Hasher, Layout, Length, Point, + Rectangle, Size, Widget, }; -use std::{hash::Hash, ops::RangeInclusive}; +use std::hash::Hash; +use std::ops::RangeInclusive; + +pub use iced_style::slider::{Handle, HandleShape, Style, StyleSheet}; /// An horizontal bar and a handle that selects a single value from a range of /// values. @@ -21,9 +26,8 @@ use std::{hash::Hash, ops::RangeInclusive}; /// /// # Example /// ``` -/// # use iced_native::{slider, renderer::Null}; +/// # use iced_native::widget::slider::{self, Slider}; /// # -/// # pub type Slider<'a, T, Message> = iced_native::Slider<'a, T, Message, Null>; /// #[derive(Clone)] /// pub enum Message { /// SliderChanged(f32), @@ -37,7 +41,7 @@ use std::{hash::Hash, ops::RangeInclusive}; /// ///  #[allow(missing_debug_implementations)] -pub struct Slider<'a, T, Message, Renderer: self::Renderer> { +pub struct Slider<'a, T, Message> { state: &'a mut State, range: RangeInclusive<T>, step: T, @@ -46,15 +50,17 @@ pub struct Slider<'a, T, Message, Renderer: self::Renderer> { on_release: Option<Message>, width: Length, height: u16, - style: Renderer::Style, + style_sheet: Box<dyn StyleSheet + 'a>, } -impl<'a, T, Message, Renderer> Slider<'a, T, Message, Renderer> +impl<'a, T, Message> Slider<'a, T, Message> where T: Copy + From<u8> + std::cmp::PartialOrd, Message: Clone, - Renderer: self::Renderer, { + /// The default height of a [`Slider`]. + pub const DEFAULT_HEIGHT: u16 = 22; + /// Creates a new [`Slider`]. /// /// It expects: @@ -93,8 +99,8 @@ where on_change: Box::new(on_change), on_release: None, width: Length::Fill, - height: Renderer::DEFAULT_HEIGHT, - style: Renderer::Style::default(), + height: Self::DEFAULT_HEIGHT, + style_sheet: Default::default(), } } @@ -122,8 +128,11 @@ where } /// Sets the style of the [`Slider`]. - pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { - self.style = style.into(); + pub fn style( + mut self, + style_sheet: impl Into<Box<dyn StyleSheet + 'a>>, + ) -> Self { + self.style_sheet = style_sheet.into(); self } @@ -148,11 +157,11 @@ impl State { } impl<'a, T, Message, Renderer> Widget<Message, Renderer> - for Slider<'a, T, Message, Renderer> + for Slider<'a, T, Message> where T: Copy + Into<f64> + num_traits::FromPrimitive, Message: Clone, - Renderer: self::Renderer, + Renderer: crate::Renderer, { fn width(&self) -> Length { self.width @@ -246,22 +255,113 @@ where fn draw( &self, renderer: &mut Renderer, - _defaults: &Renderer::Defaults, + _style: &renderer::Style, layout: Layout<'_>, cursor_position: Point, _viewport: &Rectangle, - ) -> Renderer::Output { - let start = *self.range.start(); - let end = *self.range.end(); - - renderer.draw( - layout.bounds(), - cursor_position, - start.into() as f32..=end.into() as f32, - self.value.into() as f32, - self.state.is_dragging, - &self.style, - ) + ) { + let bounds = layout.bounds(); + let is_mouse_over = bounds.contains(cursor_position); + + let style = if self.state.is_dragging { + self.style_sheet.dragging() + } else if is_mouse_over { + self.style_sheet.hovered() + } else { + self.style_sheet.active() + }; + + let rail_y = bounds.y + (bounds.height / 2.0).round(); + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x, + y: rail_y, + width: bounds.width, + height: 2.0, + }, + border_radius: 0.0, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + style.rail_colors.0, + ); + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x, + y: rail_y + 2.0, + width: bounds.width, + height: 2.0, + }, + border_radius: 0.0, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + Background::Color(style.rail_colors.1), + ); + + let (handle_width, handle_height, handle_border_radius) = match style + .handle + .shape + { + HandleShape::Circle { radius } => { + (radius * 2.0, radius * 2.0, radius) + } + HandleShape::Rectangle { + width, + border_radius, + } => (f32::from(width), f32::from(bounds.height), border_radius), + }; + + let value = self.value.into() as f32; + let (range_start, range_end) = { + let (start, end) = self.range.clone().into_inner(); + + (start.into() as f32, end.into() as f32) + }; + + let handle_offset = if range_start >= range_end { + 0.0 + } else { + (bounds.width - handle_width) * (value - range_start) + / (range_end - range_start) + }; + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x + handle_offset.round(), + y: rail_y - handle_height / 2.0, + width: handle_width, + height: handle_height, + }, + border_radius: handle_border_radius, + border_width: style.handle.border_width, + border_color: style.handle.border_color, + }, + style.handle.color, + ); + } + + fn mouse_interaction( + &self, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) -> mouse::Interaction { + let bounds = layout.bounds(); + let is_mouse_over = bounds.contains(cursor_position); + + if self.state.is_dragging { + mouse::Interaction::Grabbing + } else if is_mouse_over { + mouse::Interaction::Grab + } else { + mouse::Interaction::default() + } } fn hash_layout(&self, state: &mut Hasher) { @@ -272,48 +372,14 @@ where } } -/// The renderer of a [`Slider`]. -/// -/// Your [renderer] will need to implement this trait before being -/// able to use a [`Slider`] in your user interface. -/// -/// [renderer]: crate::renderer -pub trait Renderer: crate::Renderer { - /// The style supported by this renderer. - type Style: Default; - - /// The default height of a [`Slider`]. - const DEFAULT_HEIGHT: u16; - - /// Draws a [`Slider`]. - /// - /// It receives: - /// * the current cursor position - /// * the bounds of the [`Slider`] - /// * the local state of the [`Slider`] - /// * the range of values of the [`Slider`] - /// * the current value of the [`Slider`] - fn draw( - &mut self, - bounds: Rectangle, - cursor_position: Point, - range: RangeInclusive<f32>, - value: f32, - is_dragging: bool, - style: &Self::Style, - ) -> Self::Output; -} - -impl<'a, T, Message, Renderer> From<Slider<'a, T, Message, Renderer>> +impl<'a, T, Message, Renderer> From<Slider<'a, T, Message>> for Element<'a, Message, Renderer> where T: 'a + Copy + Into<f64> + num_traits::FromPrimitive, Message: 'a + Clone, - Renderer: 'a + self::Renderer, + Renderer: 'a + crate::Renderer, { - fn from( - slider: Slider<'a, T, Message, Renderer>, - ) -> Element<'a, Message, Renderer> { + fn from(slider: Slider<'a, T, Message>) -> Element<'a, Message, Renderer> { Element::new(slider) } } diff --git a/native/src/widget/space.rs b/native/src/widget/space.rs index 6b34ece8..3373f3b7 100644 --- a/native/src/widget/space.rs +++ b/native/src/widget/space.rs @@ -1,9 +1,9 @@ //! Distribute content vertically. -use std::hash::Hash; +use crate::layout; +use crate::renderer; +use crate::{Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget}; -use crate::{ - layout, Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget, -}; +use std::hash::Hash; /// An amount of empty space. /// @@ -39,7 +39,7 @@ impl Space { impl<Message, Renderer> Widget<Message, Renderer> for Space where - Renderer: self::Renderer, + Renderer: crate::Renderer, { fn width(&self) -> Length { self.width @@ -61,13 +61,12 @@ where fn draw( &self, - renderer: &mut Renderer, - _defaults: &Renderer::Defaults, - layout: Layout<'_>, + _renderer: &mut Renderer, + _style: &renderer::Style, + _layout: Layout<'_>, _cursor_position: Point, _viewport: &Rectangle, - ) -> Renderer::Output { - renderer.draw(layout.bounds()) + ) { } fn hash_layout(&self, state: &mut Hasher) { @@ -78,17 +77,9 @@ where } } -/// The renderer of an amount of [`Space`]. -pub trait Renderer: crate::Renderer { - /// Draws an amount of empty [`Space`]. - /// - /// You should most likely return an empty primitive here. - fn draw(&mut self, bounds: Rectangle) -> Self::Output; -} - impl<'a, Message, Renderer> From<Space> for Element<'a, Message, Renderer> where - Renderer: self::Renderer, + Renderer: crate::Renderer, Message: 'a, { fn from(space: Space) -> Element<'a, Message, Renderer> { diff --git a/native/src/widget/svg.rs b/native/src/widget/svg.rs index 9cd61918..f212dfcb 100644 --- a/native/src/widget/svg.rs +++ b/native/src/widget/svg.rs @@ -1,12 +1,11 @@ //! Display vector graphics in your application. use crate::layout; +use crate::renderer; +use crate::svg::{self, Handle}; use crate::{Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget}; -use std::{ - hash::{Hash, Hasher as _}, - path::PathBuf, - sync::Arc, -}; +use std::hash::Hash; +use std::path::PathBuf; /// A vector graphics image. /// @@ -52,7 +51,7 @@ impl Svg { impl<Message, Renderer> Widget<Message, Renderer> for Svg where - Renderer: self::Renderer, + Renderer: svg::Renderer, { fn width(&self) -> Length { self.width @@ -90,12 +89,12 @@ where fn draw( &self, renderer: &mut Renderer, - _defaults: &Renderer::Defaults, + _style: &renderer::Style, layout: Layout<'_>, _cursor_position: Point, _viewport: &Rectangle, - ) -> Renderer::Output { - renderer.draw(self.handle.clone(), layout) + ) { + renderer.draw(self.handle.clone(), layout.bounds()) } fn hash_layout(&self, state: &mut Hasher) { @@ -107,94 +106,9 @@ where } } -/// An [`Svg`] handle. -#[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<Vec<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 an [`Svg`]. -#[derive(Clone, Hash)] -pub enum Data { - /// File data - Path(PathBuf), - - /// In-memory data - /// - /// Can contain an SVG string or a gzip compressed data. - Bytes(Vec<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(...)"), - } - } -} - -/// The renderer of an [`Svg`]. -/// -/// Your [renderer] will need to implement this trait before being able to use -/// an [`Svg`] in your user interface. -/// -/// [renderer]: crate::renderer -pub trait Renderer: crate::Renderer { - /// Returns the default dimensions of an [`Svg`] for the given [`Handle`]. - fn dimensions(&self, handle: &Handle) -> (u32, u32); - - /// Draws an [`Svg`]. - fn draw(&mut self, handle: Handle, layout: Layout<'_>) -> Self::Output; -} - impl<'a, Message, Renderer> From<Svg> for Element<'a, Message, Renderer> where - Renderer: self::Renderer, + Renderer: svg::Renderer, { fn from(icon: Svg) -> Element<'a, Message, Renderer> { Element::new(icon) diff --git a/native/src/widget/text.rs b/native/src/widget/text.rs index 168d49c2..4dbc4a65 100644 --- a/native/src/widget/text.rs +++ b/native/src/widget/text.rs @@ -1,12 +1,12 @@ //! Write some text for your users to read. use crate::alignment; use crate::layout; +use crate::renderer; +use crate::text; use crate::{ Color, Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget, }; -pub use iced_core::text::Hit; - use std::hash::Hash; /// A paragraph of text. @@ -14,7 +14,7 @@ use std::hash::Hash; /// # Example /// /// ``` -/// # type Text = iced_native::Text<iced_native::renderer::Null>; +/// # type Text = iced_native::widget::Text<iced_native::renderer::Null>; /// # /// Text::new("I <3 iced!") /// .color([0.0, 0.0, 1.0]) @@ -23,7 +23,7 @@ use std::hash::Hash; /// ///  #[derive(Debug)] -pub struct Text<Renderer: self::Renderer> { +pub struct Text<Renderer: text::Renderer> { content: String, size: Option<u16>, color: Option<Color>, @@ -34,7 +34,7 @@ pub struct Text<Renderer: self::Renderer> { vertical_alignment: alignment::Vertical, } -impl<Renderer: self::Renderer> Text<Renderer> { +impl<Renderer: text::Renderer> Text<Renderer> { /// Create a new fragment of [`Text`] with the given contents. pub fn new<T: Into<String>>(label: T) -> Self { Text { @@ -102,7 +102,7 @@ impl<Renderer: self::Renderer> Text<Renderer> { impl<Message, Renderer> Widget<Message, Renderer> for Text<Renderer> where - Renderer: self::Renderer, + Renderer: text::Renderer, { fn width(&self) -> Length { self.width @@ -134,21 +134,22 @@ where fn draw( &self, renderer: &mut Renderer, - defaults: &Renderer::Defaults, + style: &renderer::Style, layout: Layout<'_>, _cursor_position: Point, _viewport: &Rectangle, - ) -> Renderer::Output { - renderer.draw( - defaults, - layout.bounds(), + ) { + draw( + renderer, + style, + layout, &self.content, - self.size.unwrap_or(renderer.default_size()), self.font, + self.size, self.color, self.horizontal_alignment, self.vertical_alignment, - ) + ); } fn hash_layout(&self, state: &mut Hasher) { @@ -162,79 +163,65 @@ where } } -/// The renderer of a [`Text`] fragment. +/// Draws text using the same logic as the [`Text`] widget. /// -/// Your [renderer] will need to implement this trait before being -/// able to use [`Text`] in your user interface. +/// Specifically: /// -/// [renderer]: crate::Renderer -pub trait Renderer: crate::Renderer { - /// The font type used for [`Text`]. - type Font: Default + Copy; - - /// Returns the default size of [`Text`]. - fn default_size(&self) -> u16; - - /// Measures the [`Text`] in the given bounds and returns the minimum - /// boundaries that can fit the contents. - fn measure( - &self, - content: &str, - size: u16, - font: Self::Font, - bounds: Size, - ) -> (f32, f32); - - /// 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>; - - /// Draws a [`Text`] fragment. - /// - /// It receives: - /// * the bounds of the [`Text`] - /// * the contents of the [`Text`] - /// * the size of the [`Text`] - /// * the color of the [`Text`] - /// * the [`HorizontalAlignment`] of the [`Text`] - /// * the [`VerticalAlignment`] of the [`Text`] - fn draw( - &mut self, - defaults: &Self::Defaults, - bounds: Rectangle, - content: &str, - size: u16, - font: Self::Font, - color: Option<Color>, - horizontal_alignment: alignment::Horizontal, - vertical_alignment: alignment::Vertical, - ) -> Self::Output; +/// * 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, + font: Renderer::Font, + size: Option<u16>, + color: Option<Color>, + horizontal_alignment: alignment::Horizontal, + vertical_alignment: alignment::Vertical, +) where + Renderer: text::Renderer, +{ + let bounds = layout.bounds(); + + let x = match horizontal_alignment { + alignment::Horizontal::Left => bounds.x, + alignment::Horizontal::Center => bounds.center_x(), + alignment::Horizontal::Right => bounds.x + bounds.width, + }; + + let y = match vertical_alignment { + alignment::Vertical::Top => bounds.y, + alignment::Vertical::Center => bounds.center_y(), + alignment::Vertical::Bottom => bounds.y + bounds.height, + }; + + renderer.fill_text(crate::text::Text { + content, + size: f32::from(size.unwrap_or(renderer.default_size())), + bounds: Rectangle { x, y, ..bounds }, + color: color.unwrap_or(style.text_color), + font, + horizontal_alignment, + vertical_alignment, + }); } impl<'a, Message, Renderer> From<Text<Renderer>> for Element<'a, Message, Renderer> where - Renderer: self::Renderer + 'a, + Renderer: text::Renderer + 'a, { fn from(text: Text<Renderer>) -> Element<'a, Message, Renderer> { Element::new(text) } } -impl<Renderer: self::Renderer> Clone for Text<Renderer> { +impl<Renderer: text::Renderer> Clone for Text<Renderer> { fn clone(&self) -> Self { Self { content: self.content.clone(), diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs index d4d197d3..40c6c573 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -11,26 +11,31 @@ pub use value::Value; use editor::Editor; +use crate::alignment; use crate::event::{self, Event}; use crate::keyboard; use crate::layout; use crate::mouse::{self, click}; -use crate::text; +use crate::renderer; +use crate::text::{self, Text}; use crate::touch; use crate::{ - Clipboard, Element, Hasher, Layout, Length, Padding, Point, Rectangle, - Size, Widget, + Clipboard, Color, Element, Hasher, Layout, Length, Padding, Point, + Rectangle, Size, Vector, Widget, }; use std::u32; +pub use iced_style::text_input::{Style, StyleSheet}; + /// A field that can be filled with text. /// /// # Example /// ``` -/// # use iced_native::{text_input, renderer::Null}; +/// # use iced_native::renderer::Null; +/// # use iced_native::widget::text_input; /// # -/// # pub type TextInput<'a, Message> = iced_native::TextInput<'a, Message, Null>; +/// # pub type TextInput<'a, Message> = iced_native::widget::TextInput<'a, Message, Null>; /// #[derive(Debug, Clone)] /// enum Message { /// TextInputChanged(String), @@ -49,7 +54,7 @@ use std::u32; /// ``` ///  #[allow(missing_debug_implementations)] -pub struct TextInput<'a, Message, Renderer: self::Renderer> { +pub struct TextInput<'a, Message, Renderer: text::Renderer> { state: &'a mut State, placeholder: String, value: Value, @@ -61,13 +66,13 @@ pub struct TextInput<'a, Message, Renderer: self::Renderer> { size: Option<u16>, on_change: Box<dyn Fn(String) -> Message>, on_submit: Option<Message>, - style: Renderer::Style, + style_sheet: Box<dyn StyleSheet + 'a>, } impl<'a, Message, Renderer> TextInput<'a, Message, Renderer> where Message: Clone, - Renderer: self::Renderer, + Renderer: text::Renderer, { /// Creates a new [`TextInput`]. /// @@ -97,7 +102,7 @@ where size: None, on_change: Box::new(on_change), on_submit: None, - style: Renderer::Style::default(), + style_sheet: Default::default(), } } @@ -147,8 +152,11 @@ where } /// Sets the style of the [`TextInput`]. - pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { - self.style = style.into(); + pub fn style( + mut self, + style_sheet: impl Into<Box<dyn StyleSheet + 'a>>, + ) -> Self { + self.style_sheet = style_sheet.into(); self } @@ -160,7 +168,7 @@ where impl<'a, Message, Renderer> TextInput<'a, Message, Renderer> where - Renderer: self::Renderer, + Renderer: text::Renderer, { /// Draws the [`TextInput`] with the given [`Renderer`], overriding its /// [`Value`] if provided. @@ -170,37 +178,165 @@ where layout: Layout<'_>, cursor_position: Point, value: Option<&Value>, - ) -> Renderer::Output { + ) { let value = value.unwrap_or(&self.value); + let secure_value = self.is_secure.then(|| value.secure()); + let value = secure_value.as_ref().unwrap_or(&value); + let bounds = layout.bounds(); let text_bounds = layout.children().next().unwrap().bounds(); - if self.is_secure { - self::Renderer::draw( - renderer, - bounds, - text_bounds, - cursor_position, - self.font, - self.size.unwrap_or(renderer.default_size()), - &self.placeholder, - &value.secure(), - &self.state, - &self.style, - ) + let is_mouse_over = bounds.contains(cursor_position); + + let style = if self.state.is_focused() { + self.style_sheet.focused() + } else if is_mouse_over { + self.style_sheet.hovered() } else { - self::Renderer::draw( - renderer, + self.style_sheet.active() + }; + + renderer.fill_quad( + renderer::Quad { bounds, - text_bounds, - cursor_position, - self.font, - self.size.unwrap_or(renderer.default_size()), - &self.placeholder, - value, - &self.state, - &self.style, - ) + border_radius: style.border_radius, + border_width: style.border_width, + border_color: style.border_color, + }, + style.background, + ); + + let text = value.to_string(); + let size = self.size.unwrap_or(renderer.default_size()); + + let (cursor, offset) = if self.state.is_focused() { + match self.state.cursor.state(&value) { + cursor::State::Index(position) => { + let (text_value_width, offset) = + measure_cursor_and_scroll_offset( + renderer, + text_bounds, + &value, + size, + position, + self.font, + ); + + ( + Some(( + renderer::Quad { + bounds: Rectangle { + x: text_bounds.x + text_value_width, + y: text_bounds.y, + width: 1.0, + height: text_bounds.height, + }, + border_radius: 0.0, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + self.style_sheet.value_color(), + )), + offset, + ) + } + cursor::State::Selection { start, end } => { + let left = start.min(end); + let right = end.max(start); + + let (left_position, left_offset) = + measure_cursor_and_scroll_offset( + renderer, + text_bounds, + &value, + size, + left, + self.font, + ); + + let (right_position, right_offset) = + measure_cursor_and_scroll_offset( + renderer, + text_bounds, + &value, + size, + right, + self.font, + ); + + let width = right_position - left_position; + + ( + Some(( + renderer::Quad { + bounds: Rectangle { + x: text_bounds.x + left_position, + y: text_bounds.y, + width, + height: text_bounds.height, + }, + border_radius: 0.0, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + self.style_sheet.selection_color(), + )), + if end == right { + right_offset + } else { + left_offset + }, + ) + } + } + } else { + (None, 0.0) + }; + + let text_width = renderer.measure_width( + if text.is_empty() { + &self.placeholder + } else { + &text + }, + size, + self.font, + ); + + let render = |renderer: &mut Renderer| { + if let Some((cursor, color)) = cursor { + renderer.fill_quad(cursor, color); + } + + renderer.fill_text(Text { + content: if text.is_empty() { + &self.placeholder + } else { + &text + }, + color: if text.is_empty() { + self.style_sheet.placeholder_color() + } else { + self.style_sheet.value_color() + }, + font: self.font, + bounds: Rectangle { + y: text_bounds.center_y(), + width: f32::INFINITY, + ..text_bounds + }, + size: f32::from(size), + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Center, + }); + }; + + if text_width > text_bounds.width { + renderer.with_layer(text_bounds, |renderer| { + renderer.with_translation(Vector::new(-offset, 0.0), render) + }); + } else { + render(renderer); } } } @@ -209,7 +345,7 @@ impl<'a, Message, Renderer> Widget<Message, Renderer> for TextInput<'a, Message, Renderer> where Message: Clone, - Renderer: self::Renderer, + Renderer: text::Renderer, { fn width(&self) -> Length { self.width @@ -275,7 +411,8 @@ where self.value.clone() }; - renderer.find_cursor_position( + find_cursor_position( + renderer, text_layout.bounds(), self.font, self.size, @@ -294,16 +431,16 @@ where if self.is_secure { self.state.cursor.select_all(&self.value); } else { - let position = renderer - .find_cursor_position( - text_layout.bounds(), - self.font, - self.size, - &self.value, - &self.state, - target, - ) - .unwrap_or(0); + let position = find_cursor_position( + renderer, + text_layout.bounds(), + self.font, + self.size, + &self.value, + &self.state, + target, + ) + .unwrap_or(0); self.state.cursor.select_range( self.value.previous_start_of_word(position), @@ -341,16 +478,16 @@ where self.value.clone() }; - let position = renderer - .find_cursor_position( - text_layout.bounds(), - self.font, - self.size, - &value, - &self.state, - target, - ) - .unwrap_or(0); + let position = find_cursor_position( + renderer, + text_layout.bounds(), + self.font, + self.size, + &value, + &self.state, + target, + ) + .unwrap_or(0); self.state.cursor.select_range( self.state.cursor.start(&value), @@ -621,14 +758,27 @@ where event::Status::Ignored } + fn mouse_interaction( + &self, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) -> mouse::Interaction { + if layout.bounds().contains(cursor_position) { + mouse::Interaction::Text + } else { + mouse::Interaction::default() + } + } + fn draw( &self, renderer: &mut Renderer, - _defaults: &Renderer::Defaults, + _style: &renderer::Style, layout: Layout<'_>, cursor_position: Point, _viewport: &Rectangle, - ) -> Renderer::Output { + ) { self.draw(renderer, layout, cursor_position, None) } @@ -644,87 +794,11 @@ where } } -/// The renderer of a [`TextInput`]. -/// -/// Your [renderer] will need to implement this trait before being -/// able to use a [`TextInput`] in your user interface. -/// -/// [renderer]: crate::renderer -pub trait Renderer: text::Renderer + Sized { - /// The style supported by this renderer. - type Style: Default; - - /// Returns the width of the value of the [`TextInput`]. - fn measure_value(&self, value: &str, size: u16, font: Self::Font) -> f32; - - /// Returns the current horizontal offset of the value of the - /// [`TextInput`]. - /// - /// This is the amount of horizontal scrolling applied when the [`Value`] - /// does not fit the [`TextInput`]. - fn offset( - &self, - text_bounds: Rectangle, - font: Self::Font, - size: u16, - value: &Value, - state: &State, - ) -> f32; - - /// Draws a [`TextInput`]. - /// - /// It receives: - /// - the bounds of the [`TextInput`] - /// - the bounds of the text (i.e. the current value) - /// - the cursor position - /// - the placeholder to show when the value is empty - /// - the current [`Value`] - /// - the current [`State`] - fn draw( - &mut self, - bounds: Rectangle, - text_bounds: Rectangle, - cursor_position: Point, - font: Self::Font, - size: u16, - placeholder: &str, - value: &Value, - state: &State, - style: &Self::Style, - ) -> Self::Output; - - /// Computes the position of the text cursor at the given X coordinate of - /// a [`TextInput`]. - fn find_cursor_position( - &self, - text_bounds: Rectangle, - font: Self::Font, - size: Option<u16>, - value: &Value, - state: &State, - x: f32, - ) -> Option<usize> { - let size = size.unwrap_or(self.default_size()); - - let offset = self.offset(text_bounds, font, size, &value, &state); - - self.hit_test( - &value.to_string(), - size.into(), - font, - Size::INFINITY, - Point::new(x + offset, text_bounds.height / 2.0), - true, - ) - .map(text::Hit::cursor) - } -} - impl<'a, Message, Renderer> From<TextInput<'a, Message, Renderer>> for Element<'a, Message, Renderer> where Message: 'a + Clone, - Renderer: 'a + self::Renderer, + Renderer: 'a + text::Renderer, { fn from( text_input: TextInput<'a, Message, Renderer>, @@ -815,3 +889,88 @@ mod platform { } } } + +fn offset<Renderer>( + renderer: &Renderer, + text_bounds: Rectangle, + font: Renderer::Font, + size: u16, + value: &Value, + state: &State, +) -> f32 +where + Renderer: text::Renderer, +{ + if state.is_focused() { + let cursor = state.cursor(); + + let focus_position = match cursor.state(value) { + cursor::State::Index(i) => i, + cursor::State::Selection { end, .. } => end, + }; + + let (_, offset) = measure_cursor_and_scroll_offset( + renderer, + text_bounds, + value, + size, + focus_position, + font, + ); + + offset + } else { + 0.0 + } +} + +fn measure_cursor_and_scroll_offset<Renderer>( + renderer: &Renderer, + text_bounds: Rectangle, + value: &Value, + size: u16, + cursor_index: usize, + font: Renderer::Font, +) -> (f32, f32) +where + Renderer: text::Renderer, +{ + let text_before_cursor = value.until(cursor_index).to_string(); + + let text_value_width = + renderer.measure_width(&text_before_cursor, size, font); + + let offset = ((text_value_width + 5.0) - text_bounds.width).max(0.0); + + (text_value_width, offset) +} + +/// Computes the position of the text cursor at the given X coordinate of +/// a [`TextInput`]. +fn find_cursor_position<Renderer>( + renderer: &Renderer, + text_bounds: Rectangle, + font: Renderer::Font, + size: Option<u16>, + value: &Value, + state: &State, + x: f32, +) -> Option<usize> +where + Renderer: text::Renderer, +{ + let size = size.unwrap_or(renderer.default_size()); + + let offset = offset(renderer, text_bounds, font, size, &value, &state); + + renderer + .hit_test( + &value.to_string(), + size.into(), + font, + Size::INFINITY, + Point::new(x + offset, text_bounds.height / 2.0), + true, + ) + .map(text::Hit::cursor) +} diff --git a/native/src/widget/text_input/editor.rs b/native/src/widget/text_input/editor.rs index 0b50a382..bac530e1 100644 --- a/native/src/widget/text_input/editor.rs +++ b/native/src/widget/text_input/editor.rs @@ -1,4 +1,4 @@ -use crate::text_input::{Cursor, Value}; +use crate::widget::text_input::{Cursor, Value}; pub struct Editor<'a> { value: &'a mut Value, diff --git a/native/src/widget/toggler.rs b/native/src/widget/toggler.rs index c624be4c..2dcc3ffe 100644 --- a/native/src/widget/toggler.rs +++ b/native/src/widget/toggler.rs @@ -5,19 +5,22 @@ use crate::alignment; use crate::event; use crate::layout; use crate::mouse; -use crate::row; +use crate::renderer; use crate::text; +use crate::widget::{Row, Text}; use crate::{ Alignment, Clipboard, Element, Event, Hasher, Layout, Length, Point, - Rectangle, Row, Text, Widget, + Rectangle, Widget, }; +pub use iced_style::toggler::{Style, StyleSheet}; + /// A toggler widget /// /// # Example /// /// ``` -/// # type Toggler<Message> = iced_native::Toggler<Message, iced_native::renderer::Null>; +/// # type Toggler<'a, Message> = iced_native::widget::Toggler<'a, Message, iced_native::renderer::Null>; /// # /// pub enum Message { /// TogglerToggled(bool), @@ -28,7 +31,7 @@ use crate::{ /// Toggler::new(is_active, String::from("Toggle me!"), |b| Message::TogglerToggled(b)); /// ``` #[allow(missing_debug_implementations)] -pub struct Toggler<Message, Renderer: self::Renderer + text::Renderer> { +pub struct Toggler<'a, Message, Renderer: text::Renderer> { is_active: bool, on_toggle: Box<dyn Fn(bool) -> Message>, label: Option<String>, @@ -38,12 +41,13 @@ pub struct Toggler<Message, Renderer: self::Renderer + text::Renderer> { text_alignment: alignment::Horizontal, spacing: u16, font: Renderer::Font, - style: Renderer::Style, + style_sheet: Box<dyn StyleSheet + 'a>, } -impl<Message, Renderer: self::Renderer + text::Renderer> - Toggler<Message, Renderer> -{ +impl<'a, Message, Renderer: text::Renderer> Toggler<'a, Message, Renderer> { + /// The default size of a [`Toggler`]. + pub const DEFAULT_SIZE: u16 = 20; + /// Creates a new [`Toggler`]. /// /// It expects: @@ -65,12 +69,12 @@ impl<Message, Renderer: self::Renderer + text::Renderer> on_toggle: Box::new(f), label: label.into(), width: Length::Fill, - size: <Renderer as self::Renderer>::DEFAULT_SIZE, + size: Self::DEFAULT_SIZE, text_size: None, text_alignment: alignment::Horizontal::Left, spacing: 0, font: Renderer::Font::default(), - style: Renderer::Style::default(), + style_sheet: Default::default(), } } @@ -111,15 +115,19 @@ impl<Message, Renderer: self::Renderer + text::Renderer> } /// Sets the style of the [`Toggler`]. - pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { - self.style = style.into(); + pub fn style( + mut self, + style_sheet: impl Into<Box<dyn StyleSheet + 'a>>, + ) -> Self { + self.style_sheet = style_sheet.into(); self } } -impl<Message, Renderer> Widget<Message, Renderer> for Toggler<Message, Renderer> +impl<'a, Message, Renderer> Widget<Message, Renderer> + for Toggler<'a, Message, Renderer> where - Renderer: self::Renderer + text::Renderer + row::Renderer, + Renderer: text::Renderer, { fn width(&self) -> Length { self.width @@ -183,50 +191,108 @@ where } } + fn mouse_interaction( + &self, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) -> mouse::Interaction { + if layout.bounds().contains(cursor_position) { + mouse::Interaction::Pointer + } else { + mouse::Interaction::default() + } + } + fn draw( &self, renderer: &mut Renderer, - defaults: &Renderer::Defaults, + style: &renderer::Style, layout: Layout<'_>, cursor_position: Point, _viewport: &Rectangle, - ) -> Renderer::Output { - let bounds = layout.bounds(); - let mut children = layout.children(); + ) { + /// Makes sure that the border radius of the toggler looks good at every size. + const BORDER_RADIUS_RATIO: f32 = 32.0 / 13.0; - let label = match &self.label { - Some(label) => { - let label_layout = children.next().unwrap(); - - Some(text::Renderer::draw( - renderer, - defaults, - label_layout.bounds(), - &label, - self.text_size.unwrap_or(renderer.default_size()), - self.font, - None, - self.text_alignment, - alignment::Vertical::Center, - )) - } + /// The space ratio between the background Quad and the Toggler bounds, and + /// between the background Quad and foreground Quad. + const SPACE_RATIO: f32 = 0.05; - None => None, - }; + let mut children = layout.children(); + + if let Some(label) = &self.label { + let label_layout = children.next().unwrap(); + + crate::widget::text::draw( + renderer, + style, + label_layout, + &label, + self.font, + self.text_size, + None, + self.text_alignment, + alignment::Vertical::Center, + ); + } let toggler_layout = children.next().unwrap(); - let toggler_bounds = toggler_layout.bounds(); + let bounds = toggler_layout.bounds(); let is_mouse_over = bounds.contains(cursor_position); - self::Renderer::draw( - renderer, - toggler_bounds, - self.is_active, - is_mouse_over, - label, - &self.style, - ) + let style = if is_mouse_over { + self.style_sheet.hovered(self.is_active) + } else { + self.style_sheet.active(self.is_active) + }; + + let border_radius = bounds.height as f32 / BORDER_RADIUS_RATIO; + let space = SPACE_RATIO * bounds.height as f32; + + let toggler_background_bounds = Rectangle { + x: bounds.x + space, + y: bounds.y + space, + width: bounds.width - (2.0 * space), + height: bounds.height - (2.0 * space), + }; + + renderer.fill_quad( + renderer::Quad { + bounds: toggler_background_bounds, + border_radius, + border_width: 1.0, + border_color: style + .background_border + .unwrap_or(style.background), + }, + style.background, + ); + + let toggler_foreground_bounds = Rectangle { + x: bounds.x + + if self.is_active { + bounds.width - 2.0 * space - (bounds.height - (4.0 * space)) + } else { + 2.0 * space + }, + y: bounds.y + (2.0 * space), + width: bounds.height - (4.0 * space), + height: bounds.height - (4.0 * space), + }; + + renderer.fill_quad( + renderer::Quad { + bounds: toggler_foreground_bounds, + border_radius, + border_width: 1.0, + border_color: style + .foreground_border + .unwrap_or(style.foreground), + }, + style.foreground, + ); } fn hash_layout(&self, state: &mut Hasher) { @@ -237,45 +303,14 @@ where } } -/// The renderer of a [`Toggler`]. -/// -/// Your [renderer] will need to implement this trait before being -/// able to use a [`Toggler`] in your user interface. -/// -/// [renderer]: ../../renderer/index.html -pub trait Renderer: crate::Renderer { - /// The style supported by this renderer. - type Style: Default; - - /// The default size of a [`Toggler`]. - const DEFAULT_SIZE: u16; - - /// Draws a [`Toggler`]. - /// - /// It receives: - /// * the bounds of the [`Toggler`] - /// * whether the [`Toggler`] is activated or not - /// * whether the mouse is over the [`Toggler`] or not - /// * the drawn label of the [`Toggler`] - /// * the style of the [`Toggler`] - fn draw( - &mut self, - bounds: Rectangle, - is_active: bool, - is_mouse_over: bool, - label: Option<Self::Output>, - style: &Self::Style, - ) -> Self::Output; -} - -impl<'a, Message, Renderer> From<Toggler<Message, Renderer>> +impl<'a, Message, Renderer> From<Toggler<'a, Message, Renderer>> for Element<'a, Message, Renderer> where - Renderer: 'a + self::Renderer + text::Renderer + row::Renderer, + Renderer: 'a + text::Renderer, Message: 'a, { fn from( - toggler: Toggler<Message, Renderer>, + toggler: Toggler<'a, Message, Renderer>, ) -> Element<'a, Message, Renderer> { Element::new(toggler) } diff --git a/native/src/widget/tooltip.rs b/native/src/widget/tooltip.rs index 276afd41..c35005e0 100644 --- a/native/src/widget/tooltip.rs +++ b/native/src/widget/tooltip.rs @@ -3,28 +3,36 @@ use std::hash::Hash; use iced_core::Rectangle; +use crate::event; +use crate::layout; +use crate::mouse; +use crate::renderer; +use crate::text; use crate::widget::container; -use crate::widget::text::{self, Text}; +use crate::widget::text::Text; use crate::{ - event, layout, Clipboard, Element, Event, Hasher, Layout, Length, Point, - Widget, + Clipboard, Element, Event, Hasher, Layout, Length, Padding, Point, Size, + Vector, Widget, }; /// An element to display a widget over another. #[allow(missing_debug_implementations)] -pub struct Tooltip<'a, Message, Renderer: self::Renderer> { +pub struct Tooltip<'a, Message, Renderer: text::Renderer> { content: Element<'a, Message, Renderer>, tooltip: Text<Renderer>, position: Position, - style: <Renderer as container::Renderer>::Style, + style_sheet: Box<dyn container::StyleSheet + 'a>, gap: u16, padding: u16, } impl<'a, Message, Renderer> Tooltip<'a, Message, Renderer> where - Renderer: self::Renderer, + Renderer: text::Renderer, { + /// The default padding of a [`Tooltip`] drawn by this renderer. + const DEFAULT_PADDING: u16 = 5; + /// Creates an empty [`Tooltip`]. /// /// [`Tooltip`]: struct.Tooltip.html @@ -37,9 +45,9 @@ where content: content.into(), tooltip: Text::new(tooltip.to_string()), position, - style: Default::default(), + style_sheet: Default::default(), gap: 0, - padding: Renderer::DEFAULT_PADDING, + padding: Self::DEFAULT_PADDING, } } @@ -72,9 +80,9 @@ where /// Sets the style of the [`Tooltip`]. pub fn style( mut self, - style: impl Into<<Renderer as container::Renderer>::Style>, + style_sheet: impl Into<Box<dyn container::StyleSheet + 'a>>, ) -> Self { - self.style = style.into(); + self.style_sheet = style_sheet.into(); self } } @@ -97,7 +105,7 @@ pub enum Position { impl<'a, Message, Renderer> Widget<Message, Renderer> for Tooltip<'a, Message, Renderer> where - Renderer: self::Renderer, + Renderer: text::Renderer, { fn width(&self) -> Length { self.content.width() @@ -134,27 +142,126 @@ where ) } + fn mouse_interaction( + &self, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) -> mouse::Interaction { + self.content + .mouse_interaction(layout, cursor_position, viewport) + } + fn draw( &self, renderer: &mut Renderer, - defaults: &Renderer::Defaults, + inherited_style: &renderer::Style, layout: Layout<'_>, cursor_position: Point, viewport: &Rectangle, - ) -> Renderer::Output { - self::Renderer::draw( + ) { + self.content.draw( renderer, - defaults, - cursor_position, + inherited_style, layout, + cursor_position, viewport, - &self.content, - &self.tooltip, - self.position, - &self.style, - self.gap, - self.padding, - ) + ); + + let bounds = layout.bounds(); + + if bounds.contains(cursor_position) { + let gap = f32::from(self.gap); + let style = self.style_sheet.style(); + + let defaults = renderer::Style { + text_color: style + .text_color + .unwrap_or(inherited_style.text_color), + }; + + let text_layout = Widget::<(), Renderer>::layout( + &self.tooltip, + renderer, + &layout::Limits::new(Size::ZERO, viewport.size()) + .pad(Padding::new(self.padding)), + ); + + let padding = f32::from(self.padding); + let text_bounds = text_layout.bounds(); + let x_center = bounds.x + (bounds.width - text_bounds.width) / 2.0; + let y_center = + bounds.y + (bounds.height - text_bounds.height) / 2.0; + + let mut tooltip_bounds = { + let offset = match self.position { + Position::Top => Vector::new( + x_center, + bounds.y - text_bounds.height - gap - padding, + ), + Position::Bottom => Vector::new( + x_center, + bounds.y + bounds.height + gap + padding, + ), + Position::Left => Vector::new( + bounds.x - text_bounds.width - gap - padding, + y_center, + ), + Position::Right => Vector::new( + bounds.x + bounds.width + gap + padding, + y_center, + ), + Position::FollowCursor => Vector::new( + cursor_position.x, + cursor_position.y - text_bounds.height, + ), + }; + + Rectangle { + x: offset.x - padding, + y: offset.y - padding, + width: text_bounds.width + padding * 2.0, + height: text_bounds.height + padding * 2.0, + } + }; + + if tooltip_bounds.x < viewport.x { + tooltip_bounds.x = viewport.x; + } else if viewport.x + viewport.width + < tooltip_bounds.x + tooltip_bounds.width + { + tooltip_bounds.x = + viewport.x + viewport.width - tooltip_bounds.width; + } + + if tooltip_bounds.y < viewport.y { + tooltip_bounds.y = viewport.y; + } else if viewport.y + viewport.height + < tooltip_bounds.y + tooltip_bounds.height + { + tooltip_bounds.y = + viewport.y + viewport.height - tooltip_bounds.height; + } + + renderer.with_layer(*viewport, |renderer| { + container::draw_background(renderer, &style, tooltip_bounds); + + Widget::<(), Renderer>::draw( + &self.tooltip, + renderer, + &defaults, + Layout::with_offset( + Vector::new( + tooltip_bounds.x + padding, + tooltip_bounds.y + padding, + ), + &text_layout, + ), + cursor_position, + viewport, + ); + }); + } } fn hash_layout(&self, state: &mut Hasher) { @@ -165,41 +272,10 @@ where } } -/// The renderer of a [`Tooltip`]. -/// -/// Your [renderer] will need to implement this trait before being -/// able to use a [`Tooltip`] in your user interface. -/// -/// [`Tooltip`]: struct.Tooltip.html -/// [renderer]: ../../renderer/index.html -pub trait Renderer: - crate::Renderer + text::Renderer + container::Renderer -{ - /// The default padding of a [`Tooltip`] drawn by this renderer. - const DEFAULT_PADDING: u16; - - /// Draws a [`Tooltip`]. - /// - /// [`Tooltip`]: struct.Tooltip.html - fn draw<Message>( - &mut self, - defaults: &Self::Defaults, - cursor_position: Point, - content_layout: Layout<'_>, - viewport: &Rectangle, - content: &Element<'_, Message, Self>, - tooltip: &Text<Self>, - position: Position, - style: &<Self as container::Renderer>::Style, - gap: u16, - padding: u16, - ) -> Self::Output; -} - impl<'a, Message, Renderer> From<Tooltip<'a, Message, Renderer>> for Element<'a, Message, Renderer> where - Renderer: 'a + self::Renderer, + Renderer: 'a + text::Renderer, Message: 'a, { fn from( |