diff options
author | 2021-02-24 01:42:08 +0100 | |
---|---|---|
committer | 2021-02-24 01:42:08 +0100 | |
commit | a5fddf9ee648927b294ef34e8819855d5e117b26 (patch) | |
tree | 96f5454cdb07249beefdb0b804de3b10f5ff6cb6 | |
parent | 6759a5c56fc53286d77698ac9a86812b6d7b03ff (diff) | |
parent | 2736e4ca35f17a92768f0be682acf6da3b574cb6 (diff) | |
download | iced-a5fddf9ee648927b294ef34e8819855d5e117b26.tar.gz iced-a5fddf9ee648927b294ef34e8819855d5e117b26.tar.bz2 iced-a5fddf9ee648927b294ef34e8819855d5e117b26.zip |
Merge pull request #465 from yusdacra/tooltip-widget
Tooltip widget
-rw-r--r-- | Cargo.toml | 1 | ||||
-rw-r--r-- | examples/tooltip/Cargo.toml | 9 | ||||
-rw-r--r-- | examples/tooltip/README.md | 14 | ||||
-rw-r--r-- | examples/tooltip/src/main.rs | 138 | ||||
-rw-r--r-- | glow/src/widget.rs | 3 | ||||
-rw-r--r-- | glow/src/widget/tooltip.rs | 6 | ||||
-rw-r--r-- | graphics/src/layer.rs | 40 | ||||
-rw-r--r-- | graphics/src/widget.rs | 3 | ||||
-rw-r--r-- | graphics/src/widget/tooltip.rs | 145 | ||||
-rw-r--r-- | native/src/layout.rs | 7 | ||||
-rw-r--r-- | native/src/widget.rs | 3 | ||||
-rw-r--r-- | native/src/widget/tooltip.rs | 210 | ||||
-rw-r--r-- | src/widget.rs | 5 | ||||
-rw-r--r-- | wgpu/src/widget.rs | 3 | ||||
-rw-r--r-- | wgpu/src/widget/tooltip.rs | 6 |
15 files changed, 577 insertions, 16 deletions
@@ -83,6 +83,7 @@ members = [ "examples/svg", "examples/todos", "examples/tour", + "examples/tooltip", ] [dependencies] diff --git a/examples/tooltip/Cargo.toml b/examples/tooltip/Cargo.toml new file mode 100644 index 00000000..1171de00 --- /dev/null +++ b/examples/tooltip/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "tooltip" +version = "0.1.0" +authors = ["Yusuf Bera Ertan <y.bera003.06@protonmail.com>"] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../..", features = ["debug"] } diff --git a/examples/tooltip/README.md b/examples/tooltip/README.md new file mode 100644 index 00000000..4ccf6578 --- /dev/null +++ b/examples/tooltip/README.md @@ -0,0 +1,14 @@ +## Tooltip + +A tooltip. + +It displays and positions a widget on another based on cursor position. + +The __[`main`]__ file contains all the code of the example. + +You can run it with `cargo run`: +``` +cargo run --package tooltip +``` + +[`main`]: src/main.rs diff --git a/examples/tooltip/src/main.rs b/examples/tooltip/src/main.rs new file mode 100644 index 00000000..d6c8b8e1 --- /dev/null +++ b/examples/tooltip/src/main.rs @@ -0,0 +1,138 @@ +use iced::tooltip::{self, Tooltip}; +use iced::{ + button, Button, Column, Container, Element, HorizontalAlignment, Length, + Row, Sandbox, Settings, Text, VerticalAlignment, +}; + +pub fn main() { + Example::run(Settings::default()).unwrap() +} + +#[derive(Default)] +struct Example { + top: button::State, + bottom: button::State, + right: button::State, + left: button::State, + follow_cursor: button::State, +} + +#[derive(Debug, Clone, Copy)] +struct Message; + +impl Sandbox for Example { + type Message = Message; + + fn new() -> Self { + Self::default() + } + + fn title(&self) -> String { + String::from("Tooltip - Iced") + } + + fn update(&mut self, _message: Message) {} + + fn view(&mut self) -> Element<Message> { + let top = + tooltip("Tooltip at top", &mut self.top, tooltip::Position::Top); + + let bottom = tooltip( + "Tooltip at bottom", + &mut self.bottom, + tooltip::Position::Bottom, + ); + + let left = + tooltip("Tooltip at left", &mut self.left, tooltip::Position::Left); + + let right = tooltip( + "Tooltip at right", + &mut self.right, + tooltip::Position::Right, + ); + + let fixed_tooltips = Row::with_children(vec![ + top.into(), + bottom.into(), + left.into(), + right.into(), + ]) + .width(Length::Fill) + .height(Length::Fill) + .align_items(iced::Align::Center) + .spacing(50); + + let follow_cursor = tooltip( + "Tooltip follows cursor", + &mut self.follow_cursor, + tooltip::Position::FollowCursor, + ); + + let content = Column::with_children(vec![ + Container::new(fixed_tooltips) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .into(), + follow_cursor.into(), + ]) + .width(Length::Fill) + .height(Length::Fill) + .spacing(50); + + Container::new(content) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .padding(50) + .into() + } +} + +fn tooltip<'a>( + label: &str, + button_state: &'a mut button::State, + position: tooltip::Position, +) -> Element<'a, Message> { + Tooltip::new( + Button::new( + button_state, + Text::new(label) + .size(40) + .width(Length::Fill) + .height(Length::Fill) + .horizontal_alignment(HorizontalAlignment::Center) + .vertical_alignment(VerticalAlignment::Center), + ) + .on_press(Message) + .width(Length::Fill) + .height(Length::Fill), + "Tooltip", + position, + ) + .gap(5) + .padding(10) + .style(style::Tooltip) + .into() +} + +mod style { + use iced::container; + use iced::Color; + + pub struct Tooltip; + + impl container::StyleSheet for Tooltip { + fn style(&self) -> container::Style { + container::Style { + text_color: Some(Color::from_rgb8(0xEE, 0xEE, 0xEE)), + background: Some(Color::from_rgb(0.11, 0.42, 0.87).into()), + border_radius: 12.0, + ..container::Style::default() + } + } + } +} diff --git a/glow/src/widget.rs b/glow/src/widget.rs index b5c84c56..5481216a 100644 --- a/glow/src/widget.rs +++ b/glow/src/widget.rs @@ -20,6 +20,7 @@ pub mod rule; pub mod scrollable; pub mod slider; pub mod text_input; +pub mod tooltip; #[doc(no_inline)] pub use button::Button; @@ -43,6 +44,8 @@ pub use scrollable::Scrollable; pub use slider::Slider; #[doc(no_inline)] pub use text_input::TextInput; +#[doc(no_inline)] +pub use tooltip::Tooltip; #[cfg(feature = "canvas")] #[cfg_attr(docsrs, doc(cfg(feature = "canvas")))] diff --git a/glow/src/widget/tooltip.rs b/glow/src/widget/tooltip.rs new file mode 100644 index 00000000..89ab3a15 --- /dev/null +++ b/glow/src/widget/tooltip.rs @@ -0,0 +1,6 @@ +//! Display a widget over another. +/// A widget allowing the selection of a single value from a list of options. +pub type Tooltip<'a, Message> = + iced_native::Tooltip<'a, Message, crate::Renderer>; + +pub use iced_native::tooltip::Position; diff --git a/graphics/src/layer.rs b/graphics/src/layer.rs index ab40b114..7dce1d4c 100644 --- a/graphics/src/layer.rs +++ b/graphics/src/layer.rs @@ -82,7 +82,12 @@ impl<'a> Layer<'a> { let mut layers = vec![first_layer]; - Self::process_primitive(&mut layers, Vector::new(0.0, 0.0), primitive); + Self::process_primitive( + &mut layers, + Vector::new(0.0, 0.0), + primitive, + 0, + ); layers } @@ -91,13 +96,19 @@ impl<'a> Layer<'a> { layers: &mut Vec<Self>, translation: Vector, primitive: &'a Primitive, + current_layer: usize, ) { match primitive { Primitive::None => {} Primitive::Group { primitives } => { // TODO: Inspect a bit and regroup (?) for primitive in primitives { - Self::process_primitive(layers, translation, primitive) + Self::process_primitive( + layers, + translation, + primitive, + current_layer, + ) } } Primitive::Text { @@ -109,7 +120,7 @@ impl<'a> Layer<'a> { horizontal_alignment, vertical_alignment, } => { - let layer = layers.last_mut().unwrap(); + let layer = &mut layers[current_layer]; layer.text.push(Text { content, @@ -128,7 +139,7 @@ impl<'a> Layer<'a> { border_width, border_color, } => { - let layer = layers.last_mut().unwrap(); + let layer = &mut layers[current_layer]; // TODO: Move some of these computations to the GPU (?) layer.quads.push(Quad { @@ -146,7 +157,7 @@ impl<'a> Layer<'a> { }); } Primitive::Mesh2D { buffers, size } => { - let layer = layers.last_mut().unwrap(); + let layer = &mut layers[current_layer]; let bounds = Rectangle::new( Point::new(translation.x, translation.y), @@ -167,7 +178,7 @@ impl<'a> Layer<'a> { offset, content, } => { - let layer = layers.last_mut().unwrap(); + let layer = &mut layers[current_layer]; let translated_bounds = *bounds + translation; // Only draw visible content @@ -175,16 +186,15 @@ impl<'a> Layer<'a> { layer.bounds.intersection(&translated_bounds) { let clip_layer = Layer::new(clip_bounds); - let new_layer = Layer::new(layer.bounds); - layers.push(clip_layer); + Self::process_primitive( layers, translation - Vector::new(offset.x as f32, offset.y as f32), content, + layers.len() - 1, ); - layers.push(new_layer); } } Primitive::Translate { @@ -195,13 +205,19 @@ impl<'a> Layer<'a> { layers, translation + *new_translation, &content, + current_layer, ); } Primitive::Cached { cache } => { - Self::process_primitive(layers, translation, &cache); + Self::process_primitive( + layers, + translation, + &cache, + current_layer, + ); } Primitive::Image { handle, bounds } => { - let layer = layers.last_mut().unwrap(); + let layer = &mut layers[current_layer]; layer.images.push(Image::Raster { handle: handle.clone(), @@ -209,7 +225,7 @@ impl<'a> Layer<'a> { }); } Primitive::Svg { handle, bounds } => { - let layer = layers.last_mut().unwrap(); + let layer = &mut layers[current_layer]; layer.images.push(Image::Vector { handle: handle.clone(), diff --git a/graphics/src/widget.rs b/graphics/src/widget.rs index 159ca91b..190ea9c0 100644 --- a/graphics/src/widget.rs +++ b/graphics/src/widget.rs @@ -20,6 +20,7 @@ pub mod scrollable; pub mod slider; pub mod svg; pub mod text_input; +pub mod tooltip; mod column; mod row; @@ -48,6 +49,8 @@ pub use scrollable::Scrollable; pub use slider::Slider; #[doc(no_inline)] pub use text_input::TextInput; +#[doc(no_inline)] +pub use tooltip::Tooltip; pub use column::Column; pub use image::Image; diff --git a/graphics/src/widget/tooltip.rs b/graphics/src/widget/tooltip.rs new file mode 100644 index 00000000..51b465a5 --- /dev/null +++ b/graphics/src/widget/tooltip.rs @@ -0,0 +1,145 @@ +//! Decorate content and apply alignment. +use crate::backend::{self, Backend}; +use crate::defaults::{self, Defaults}; +use crate::{Primitive, Renderer, Vector}; + +use iced_native::container; +use iced_native::layout::{self, Layout}; +use iced_native::{Element, Point, Rectangle, Size, Text}; + +/// An element decorating some content. +/// +/// This is an alias of an `iced_native` tooltip with a default +/// `Renderer`. +pub type Tooltip<'a, Message, Backend> = + iced_native::Tooltip<'a, Message, Renderer<Backend>>; + +pub use iced_native::tooltip::Position; + +impl<B> iced_native::tooltip::Renderer for Renderer<B> +where + B: Backend + backend::Text, +{ + const DEFAULT_PADDING: u16 = 5; + + fn draw<Message>( + &mut self, + defaults: &Defaults, + cursor_position: Point, + content_layout: Layout<'_>, + viewport: &Rectangle, + content: &Element<'_, Message, Self>, + tooltip: &Text<Self>, + position: Position, + style_sheet: &<Self as container::Renderer>::Style, + gap: u16, + padding: u16, + ) -> Self::Output { + let (content, mouse_interaction) = content.draw( + self, + &defaults, + content_layout, + cursor_position, + viewport, + ); + + let bounds = content_layout.bounds(); + + if bounds.contains(cursor_position) { + use iced_native::Widget; + + let gap = f32::from(gap); + let padding = f32::from(padding); + let style = style_sheet.style(); + + let defaults = Defaults { + text: defaults::Text { + color: style.text_color.unwrap_or(defaults.text.color), + }, + }; + + let tooltip_layout = Widget::<(), Self>::layout( + tooltip, + self, + &layout::Limits::new(Size::ZERO, viewport.size()) + .pad(f32::from(padding)), + ); + + let tooltip_bounds = tooltip_layout.bounds(); + + let x_center = + bounds.x + (bounds.width - tooltip_bounds.width) / 2.0; + + let y_center = + bounds.y + (bounds.height - tooltip_bounds.height) / 2.0; + + let offset = match position { + Position::Top => Vector::new( + x_center, + bounds.y - tooltip_bounds.height - gap - padding, + ), + Position::Bottom => Vector::new( + x_center, + bounds.y + bounds.height + gap + padding, + ), + Position::Left => Vector::new( + bounds.x - tooltip_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 - tooltip_bounds.height, + ), + }; + + let (tooltip, _) = Widget::<(), Self>::draw( + tooltip, + self, + &defaults, + Layout::with_offset(offset, &tooltip_layout), + cursor_position, + viewport, + ); + + let tooltip_bounds = Rectangle { + x: offset.x - padding, + y: offset.y - padding, + width: tooltip_bounds.width + padding * 2.0, + height: tooltip_bounds.height + padding * 2.0, + }; + + ( + Primitive::Group { + primitives: vec![ + content, + Primitive::Clip { + bounds: *viewport, + offset: Vector::new(0, 0), + content: Box::new( + if let Some(background) = + crate::container::background( + tooltip_bounds, + &style, + ) + { + Primitive::Group { + primitives: vec![background, tooltip], + } + } else { + tooltip + }, + ), + }, + ], + }, + mouse_interaction, + ) + } else { + (content, mouse_interaction) + } + } +} diff --git a/native/src/layout.rs b/native/src/layout.rs index 6d144902..b4b4a021 100644 --- a/native/src/layout.rs +++ b/native/src/layout.rs @@ -19,11 +19,14 @@ pub struct Layout<'a> { } impl<'a> Layout<'a> { - pub(crate) fn new(node: &'a Node) -> Self { + /// Creates a new [`Layout`] for the given [`Node`] at the origin. + pub fn new(node: &'a Node) -> Self { Self::with_offset(Vector::new(0.0, 0.0), node) } - pub(crate) fn with_offset(offset: Vector, node: &'a Node) -> Self { + /// Creates a new [`Layout`] for the given [`Node`] with the provided offset + /// from the origin. + pub fn with_offset(offset: Vector, node: &'a Node) -> Self { let bounds = node.bounds(); Self { diff --git a/native/src/widget.rs b/native/src/widget.rs index 3677713a..d5c353df 100644 --- a/native/src/widget.rs +++ b/native/src/widget.rs @@ -36,6 +36,7 @@ pub mod space; pub mod svg; pub mod text; pub mod text_input; +pub mod tooltip; #[doc(no_inline)] pub use button::Button; @@ -71,6 +72,8 @@ pub use svg::Svg; pub use text::Text; #[doc(no_inline)] pub use text_input::TextInput; +#[doc(no_inline)] +pub use tooltip::Tooltip; use crate::event::{self, Event}; use crate::layout; diff --git a/native/src/widget/tooltip.rs b/native/src/widget/tooltip.rs new file mode 100644 index 00000000..ab07868c --- /dev/null +++ b/native/src/widget/tooltip.rs @@ -0,0 +1,210 @@ +//! Display a widget over another. +use std::hash::Hash; + +use iced_core::Rectangle; + +use crate::widget::container; +use crate::widget::text::{self, Text}; +use crate::{ + event, layout, Clipboard, Element, Event, Hasher, Layout, Length, Point, + Widget, +}; + +/// An element to display a widget over another. +#[allow(missing_debug_implementations)] +pub struct Tooltip<'a, Message, Renderer: self::Renderer> { + content: Element<'a, Message, Renderer>, + tooltip: Text<Renderer>, + position: Position, + style: <Renderer as container::Renderer>::Style, + gap: u16, + padding: u16, +} + +impl<'a, Message, Renderer> Tooltip<'a, Message, Renderer> +where + Renderer: self::Renderer, +{ + /// Creates an empty [`Tooltip`]. + /// + /// [`Tooltip`]: struct.Tooltip.html + pub fn new( + content: impl Into<Element<'a, Message, Renderer>>, + tooltip: impl ToString, + position: Position, + ) -> Self { + Tooltip { + content: content.into(), + tooltip: Text::new(tooltip.to_string()), + position, + style: Default::default(), + gap: 0, + padding: Renderer::DEFAULT_PADDING, + } + } + + /// Sets the size of the text of the [`Tooltip`]. + pub fn size(mut self, size: u16) -> Self { + self.tooltip = self.tooltip.size(size); + self + } + + /// Sets the font of the [`Tooltip`]. + /// + /// [`Font`]: Renderer::Font + pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self { + self.tooltip = self.tooltip.font(font); + self + } + + /// Sets the gap between the content and its [`Tooltip`]. + pub fn gap(mut self, gap: u16) -> Self { + self.gap = gap; + self + } + + /// Sets the padding of the [`Tooltip`]. + pub fn padding(mut self, padding: u16) -> Self { + self.padding = padding; + self + } + + /// Sets the style of the [`Tooltip`]. + pub fn style( + mut self, + style: impl Into<<Renderer as container::Renderer>::Style>, + ) -> Self { + self.style = style.into(); + self + } +} + +/// The position of the tooltip. Defaults to following the cursor. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Position { + /// The tooltip will follow the cursor. + FollowCursor, + /// The tooltip will appear on the top of the widget. + Top, + /// The tooltip will appear on the bottom of the widget. + Bottom, + /// The tooltip will appear on the left of the widget. + Left, + /// The tooltip will appear on the right of the widget. + Right, +} + +impl<'a, Message, Renderer> Widget<Message, Renderer> + for Tooltip<'a, Message, Renderer> +where + Renderer: self::Renderer, +{ + fn width(&self) -> Length { + self.content.width() + } + + fn height(&self) -> Length { + self.content.height() + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + self.content.layout(renderer, limits) + } + + fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + messages: &mut Vec<Message>, + renderer: &Renderer, + clipboard: Option<&dyn Clipboard>, + ) -> event::Status { + self.content.widget.on_event( + event, + layout, + cursor_position, + messages, + renderer, + clipboard, + ) + } + + fn draw( + &self, + renderer: &mut Renderer, + defaults: &Renderer::Defaults, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) -> Renderer::Output { + self::Renderer::draw( + renderer, + defaults, + cursor_position, + layout, + viewport, + &self.content, + &self.tooltip, + self.position, + &self.style, + self.gap, + self.padding, + ) + } + + fn hash_layout(&self, state: &mut Hasher) { + struct Marker; + std::any::TypeId::of::<Marker>().hash(state); + + self.content.hash_layout(state); + } +} + +/// 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, + Message: 'a, +{ + fn from( + column: Tooltip<'a, Message, Renderer>, + ) -> Element<'a, Message, Renderer> { + Element::new(column) + } +} diff --git a/src/widget.rs b/src/widget.rs index edd35d2d..eac50d57 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -17,7 +17,8 @@ mod platform { pub use crate::renderer::widget::{ button, checkbox, container, pane_grid, pick_list, progress_bar, radio, - rule, scrollable, slider, text_input, Column, Row, Space, Text, + rule, scrollable, slider, text_input, tooltip, Column, Row, Space, + Text, }; #[cfg(any(feature = "canvas", feature = "glow_canvas"))] @@ -52,7 +53,7 @@ mod platform { button::Button, checkbox::Checkbox, container::Container, image::Image, pane_grid::PaneGrid, pick_list::PickList, progress_bar::ProgressBar, radio::Radio, rule::Rule, scrollable::Scrollable, slider::Slider, - svg::Svg, text_input::TextInput, + svg::Svg, text_input::TextInput, tooltip::Tooltip, }; #[cfg(any(feature = "canvas", feature = "glow_canvas"))] diff --git a/wgpu/src/widget.rs b/wgpu/src/widget.rs index 177ae1b6..304bb726 100644 --- a/wgpu/src/widget.rs +++ b/wgpu/src/widget.rs @@ -20,6 +20,7 @@ pub mod rule; pub mod scrollable; pub mod slider; pub mod text_input; +pub mod tooltip; #[doc(no_inline)] pub use button::Button; @@ -43,6 +44,8 @@ pub use scrollable::Scrollable; pub use slider::Slider; #[doc(no_inline)] pub use text_input::TextInput; +#[doc(no_inline)] +pub use tooltip::Tooltip; #[cfg(feature = "canvas")] #[cfg_attr(docsrs, doc(cfg(feature = "canvas")))] diff --git a/wgpu/src/widget/tooltip.rs b/wgpu/src/widget/tooltip.rs new file mode 100644 index 00000000..89ab3a15 --- /dev/null +++ b/wgpu/src/widget/tooltip.rs @@ -0,0 +1,6 @@ +//! Display a widget over another. +/// A widget allowing the selection of a single value from a list of options. +pub type Tooltip<'a, Message> = + iced_native::Tooltip<'a, Message, crate::Renderer>; + +pub use iced_native::tooltip::Position; |