From 05af8d00d4c0f7b8e0ece85224fd90a92da86da8 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 19 May 2020 17:15:44 +0200 Subject: Draft new `iced_graphics` crate :tada: --- graphics/src/backend.rs | 30 +++ graphics/src/defaults.rs | 32 +++ graphics/src/lib.rs | 15 ++ graphics/src/primitive.rs | 107 ++++++++ graphics/src/renderer.rs | 86 +++++++ graphics/src/renderer/widget.rs | 15 ++ graphics/src/renderer/widget/button.rs | 98 +++++++ graphics/src/renderer/widget/checkbox.rs | 68 +++++ graphics/src/renderer/widget/column.rs | 39 +++ graphics/src/renderer/widget/container.rs | 53 ++++ graphics/src/renderer/widget/image.rs | 28 ++ graphics/src/renderer/widget/pane_grid.rs | 94 +++++++ graphics/src/renderer/widget/progress_bar.rs | 58 +++++ graphics/src/renderer/widget/radio.rs | 66 +++++ graphics/src/renderer/widget/row.rs | 39 +++ graphics/src/renderer/widget/scrollable.rs | 130 ++++++++++ graphics/src/renderer/widget/slider.rs | 109 ++++++++ graphics/src/renderer/widget/space.rs | 13 + graphics/src/renderer/widget/svg.rs | 26 ++ graphics/src/renderer/widget/text.rs | 65 +++++ graphics/src/renderer/widget/text_input.rs | 267 +++++++++++++++++++ graphics/src/triangle.rs | 26 ++ graphics/src/widget.rs | 49 ++++ graphics/src/widget/button.rs | 16 ++ graphics/src/widget/canvas.rs | 236 +++++++++++++++++ graphics/src/widget/canvas/cache.rs | 108 ++++++++ graphics/src/widget/canvas/cursor.rs | 72 ++++++ graphics/src/widget/canvas/event.rs | 10 + graphics/src/widget/canvas/fill.rs | 20 ++ graphics/src/widget/canvas/frame.rs | 367 +++++++++++++++++++++++++++ graphics/src/widget/canvas/geometry.rs | 34 +++ graphics/src/widget/canvas/path.rs | 79 ++++++ graphics/src/widget/canvas/path/arc.rs | 44 ++++ graphics/src/widget/canvas/path/builder.rs | 180 +++++++++++++ graphics/src/widget/canvas/program.rs | 85 +++++++ graphics/src/widget/canvas/stroke.rs | 115 +++++++++ graphics/src/widget/canvas/text.rs | 49 ++++ graphics/src/widget/checkbox.rs | 10 + graphics/src/widget/container.rs | 11 + graphics/src/widget/pane_grid.rs | 25 ++ graphics/src/widget/progress_bar.rs | 15 ++ graphics/src/widget/radio.rs | 11 + graphics/src/widget/scrollable.rs | 13 + graphics/src/widget/slider.rs | 17 ++ graphics/src/widget/text.rs | 7 + graphics/src/widget/text_input.rs | 16 ++ 46 files changed, 3053 insertions(+) create mode 100644 graphics/src/backend.rs create mode 100644 graphics/src/defaults.rs create mode 100644 graphics/src/lib.rs create mode 100644 graphics/src/primitive.rs create mode 100644 graphics/src/renderer.rs create mode 100644 graphics/src/renderer/widget.rs create mode 100644 graphics/src/renderer/widget/button.rs create mode 100644 graphics/src/renderer/widget/checkbox.rs create mode 100644 graphics/src/renderer/widget/column.rs create mode 100644 graphics/src/renderer/widget/container.rs create mode 100644 graphics/src/renderer/widget/image.rs create mode 100644 graphics/src/renderer/widget/pane_grid.rs create mode 100644 graphics/src/renderer/widget/progress_bar.rs create mode 100644 graphics/src/renderer/widget/radio.rs create mode 100644 graphics/src/renderer/widget/row.rs create mode 100644 graphics/src/renderer/widget/scrollable.rs create mode 100644 graphics/src/renderer/widget/slider.rs create mode 100644 graphics/src/renderer/widget/space.rs create mode 100644 graphics/src/renderer/widget/svg.rs create mode 100644 graphics/src/renderer/widget/text.rs create mode 100644 graphics/src/renderer/widget/text_input.rs create mode 100644 graphics/src/triangle.rs create mode 100644 graphics/src/widget.rs create mode 100644 graphics/src/widget/button.rs create mode 100644 graphics/src/widget/canvas.rs create mode 100644 graphics/src/widget/canvas/cache.rs create mode 100644 graphics/src/widget/canvas/cursor.rs create mode 100644 graphics/src/widget/canvas/event.rs create mode 100644 graphics/src/widget/canvas/fill.rs create mode 100644 graphics/src/widget/canvas/frame.rs create mode 100644 graphics/src/widget/canvas/geometry.rs create mode 100644 graphics/src/widget/canvas/path.rs create mode 100644 graphics/src/widget/canvas/path/arc.rs create mode 100644 graphics/src/widget/canvas/path/builder.rs create mode 100644 graphics/src/widget/canvas/program.rs create mode 100644 graphics/src/widget/canvas/stroke.rs create mode 100644 graphics/src/widget/canvas/text.rs create mode 100644 graphics/src/widget/checkbox.rs create mode 100644 graphics/src/widget/container.rs create mode 100644 graphics/src/widget/pane_grid.rs create mode 100644 graphics/src/widget/progress_bar.rs create mode 100644 graphics/src/widget/radio.rs create mode 100644 graphics/src/widget/scrollable.rs create mode 100644 graphics/src/widget/slider.rs create mode 100644 graphics/src/widget/text.rs create mode 100644 graphics/src/widget/text_input.rs (limited to 'graphics/src') diff --git a/graphics/src/backend.rs b/graphics/src/backend.rs new file mode 100644 index 00000000..3fcb42f7 --- /dev/null +++ b/graphics/src/backend.rs @@ -0,0 +1,30 @@ +use iced_native::image; +use iced_native::svg; +use iced_native::{Font, Size}; + +pub trait Backend { + fn trim_measurements(&mut self) {} +} + +pub trait Text { + const ICON_FONT: Font; + const CHECKMARK_ICON: char; + + fn measure( + &self, + contents: &str, + size: f32, + font: Font, + bounds: Size, + ) -> (f32, f32); + + fn space_width(&self, size: f32) -> f32; +} + +pub trait Image { + fn dimensions(&self, handle: &image::Handle) -> (u32, u32); +} + +pub trait Svg { + fn viewport_dimensions(&self, handle: &svg::Handle) -> (u32, u32); +} diff --git a/graphics/src/defaults.rs b/graphics/src/defaults.rs new file mode 100644 index 00000000..11718a87 --- /dev/null +++ b/graphics/src/defaults.rs @@ -0,0 +1,32 @@ +//! Use default styling attributes to inherit styles. +use iced_native::Color; + +/// Some default styling attributes. +#[derive(Debug, Clone, Copy)] +pub struct Defaults { + /// Text styling + pub text: Text, +} + +impl Default for Defaults { + fn default() -> Defaults { + Defaults { + text: Text::default(), + } + } +} + +/// Some default text styling attributes. +#[derive(Debug, Clone, Copy)] +pub struct Text { + /// The default color of text + pub color: Color, +} + +impl Default for Text { + fn default() -> Text { + Text { + color: Color::BLACK, + } + } +} diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs new file mode 100644 index 00000000..596f72a4 --- /dev/null +++ b/graphics/src/lib.rs @@ -0,0 +1,15 @@ +mod defaults; +mod primitive; +mod renderer; +mod widget; + +pub mod backend; +pub mod triangle; + +#[doc(no_inline)] +pub use widget::*; + +pub use backend::Backend; +pub use defaults::Defaults; +pub use primitive::Primitive; +pub use renderer::Renderer; diff --git a/graphics/src/primitive.rs b/graphics/src/primitive.rs new file mode 100644 index 00000000..e73227ef --- /dev/null +++ b/graphics/src/primitive.rs @@ -0,0 +1,107 @@ +use iced_native::{ + image, svg, Background, Color, Font, HorizontalAlignment, Rectangle, Size, + Vector, VerticalAlignment, +}; + +use crate::triangle; +use std::sync::Arc; + +/// A rendering primitive. +#[derive(Debug, Clone)] +pub enum Primitive { + /// An empty primitive + None, + /// A group of primitives + Group { + /// The primitives of the group + primitives: Vec, + }, + /// A text primitive + Text { + /// The contents of the text + content: String, + /// The bounds of the text + bounds: Rectangle, + /// The color of the text + color: Color, + /// The size of the text + size: f32, + /// The font of the text + font: Font, + /// The horizontal alignment of the text + horizontal_alignment: HorizontalAlignment, + /// The vertical alignment of the text + vertical_alignment: VerticalAlignment, + }, + /// A quad primitive + Quad { + /// The bounds of the quad + bounds: Rectangle, + /// The background of the quad + background: Background, + /// The border radius of the quad + border_radius: u16, + /// The border width of the quad + border_width: u16, + /// The border color of the quad + border_color: Color, + }, + /// An image primitive + Image { + /// The handle of the image + handle: image::Handle, + /// The bounds of the image + bounds: Rectangle, + }, + /// An SVG primitive + Svg { + /// The path of the SVG file + handle: svg::Handle, + + /// The bounds of the viewport + bounds: Rectangle, + }, + /// A clip primitive + Clip { + /// The bounds of the clip + bounds: Rectangle, + /// The offset transformation of the clip + offset: Vector, + /// The content of the clip + content: Box, + }, + /// A primitive that applies a translation + Translate { + /// The translation vector + translation: Vector, + + /// The primitive to translate + content: Box, + }, + /// A low-level primitive to render a mesh of triangles. + /// + /// It can be used to render many kinds of geometry freely. + Mesh2D { + /// The size of the drawable region of the mesh. + /// + /// Any geometry that falls out of this region will be clipped. + size: Size, + + /// The vertex and index buffers of the mesh + buffers: triangle::Mesh2D, + }, + /// A cached primitive. + /// + /// This can be useful if you are implementing a widget where primitive + /// generation is expensive. + Cached { + /// The cached primitive + cache: Arc, + }, +} + +impl Default for Primitive { + fn default() -> Primitive { + Primitive::None + } +} diff --git a/graphics/src/renderer.rs b/graphics/src/renderer.rs new file mode 100644 index 00000000..836ed58d --- /dev/null +++ b/graphics/src/renderer.rs @@ -0,0 +1,86 @@ +mod widget; + +use crate::{Backend, Defaults, Primitive}; +use iced_native::layout::{self, Layout}; +use iced_native::mouse; +use iced_native::{Background, Color, Element, Point, Widget}; + +pub struct Renderer { + backend: B, +} + +impl Renderer { + pub fn new(backend: B) -> Self { + Self { backend } + } + + pub fn backend(&self) -> &B { + &self.backend + } + + pub fn backend_mut(&mut self) -> &mut B { + &mut self.backend + } +} + +impl iced_native::Renderer for Renderer +where + B: Backend, +{ + type Output = (Primitive, mouse::Interaction); + type Defaults = Defaults; + + fn layout<'a, Message>( + &mut self, + element: &Element<'a, Message, Self>, + limits: &layout::Limits, + ) -> layout::Node { + let layout = element.layout(self, limits); + + self.backend.trim_measurements(); + + layout + } +} + +impl layout::Debugger for Renderer +where + B: Backend, +{ + fn explain( + &mut self, + defaults: &Defaults, + widget: &dyn Widget, + layout: Layout<'_>, + cursor_position: Point, + color: Color, + ) -> Self::Output { + let (primitive, cursor) = + widget.draw(self, defaults, layout, cursor_position); + + let mut primitives = Vec::new(); + + explain_layout(layout, color, &mut primitives); + primitives.push(primitive); + + (Primitive::Group { primitives }, cursor) + } +} + +fn explain_layout( + layout: Layout<'_>, + color: Color, + primitives: &mut Vec, +) { + primitives.push(Primitive::Quad { + bounds: layout.bounds(), + background: Background::Color(Color::TRANSPARENT), + border_radius: 0, + border_width: 1, + border_color: [0.6, 0.6, 0.6, 0.5].into(), + }); + + for child in layout.children() { + explain_layout(child, color, primitives); + } +} diff --git a/graphics/src/renderer/widget.rs b/graphics/src/renderer/widget.rs new file mode 100644 index 00000000..b652fdcf --- /dev/null +++ b/graphics/src/renderer/widget.rs @@ -0,0 +1,15 @@ +mod button; +mod checkbox; +mod column; +mod container; +mod image; +mod pane_grid; +mod progress_bar; +mod radio; +mod row; +mod scrollable; +mod slider; +mod space; +mod svg; +mod text; +mod text_input; diff --git a/graphics/src/renderer/widget/button.rs b/graphics/src/renderer/widget/button.rs new file mode 100644 index 00000000..6ee60df4 --- /dev/null +++ b/graphics/src/renderer/widget/button.rs @@ -0,0 +1,98 @@ +use crate::{ + button::StyleSheet, defaults, Backend, Defaults, Primitive, Renderer, +}; +use iced_native::{ + mouse, Background, Color, Element, Layout, Point, Rectangle, Vector, +}; + +impl iced_native::button::Renderer for Renderer +where + B: Backend, +{ + const DEFAULT_PADDING: u16 = 5; + + type Style = Box; + + fn draw( + &mut self, + _defaults: &Defaults, + bounds: Rectangle, + cursor_position: Point, + is_disabled: bool, + is_pressed: bool, + style: &Box, + content: &Element<'_, Message, Self>, + content_layout: Layout<'_>, + ) -> Self::Output { + let is_mouse_over = bounds.contains(cursor_position); + + let styling = if is_disabled { + style.disabled() + } else if is_mouse_over { + if is_pressed { + style.pressed() + } else { + style.hovered() + } + } else { + style.active() + }; + + let (content, _) = content.draw( + self, + &Defaults { + text: defaults::Text { + color: styling.text_color, + }, + }, + content_layout, + cursor_position, + ); + + ( + if styling.background.is_some() || styling.border_width > 0 { + let background = Primitive::Quad { + bounds, + background: styling + .background + .unwrap_or(Background::Color(Color::TRANSPARENT)), + border_radius: styling.border_radius, + border_width: styling.border_width, + border_color: styling.border_color, + }; + + if styling.shadow_offset == Vector::default() { + Primitive::Group { + primitives: vec![background, content], + } + } else { + // TODO: Implement proper shadow support + let shadow = Primitive::Quad { + bounds: Rectangle { + x: bounds.x + styling.shadow_offset.x, + y: bounds.y + styling.shadow_offset.y, + ..bounds + }, + background: Background::Color( + [0.0, 0.0, 0.0, 0.5].into(), + ), + border_radius: styling.border_radius, + border_width: 0, + border_color: Color::TRANSPARENT, + }; + + Primitive::Group { + primitives: vec![shadow, background, content], + } + } + } else { + content + }, + if is_mouse_over { + mouse::Interaction::Pointer + } else { + mouse::Interaction::default() + }, + ) + } +} diff --git a/graphics/src/renderer/widget/checkbox.rs b/graphics/src/renderer/widget/checkbox.rs new file mode 100644 index 00000000..75168629 --- /dev/null +++ b/graphics/src/renderer/widget/checkbox.rs @@ -0,0 +1,68 @@ +use crate::backend::{self, Backend}; +use crate::checkbox::StyleSheet; +use crate::{Primitive, Renderer}; +use iced_native::{ + checkbox, mouse, HorizontalAlignment, Rectangle, VerticalAlignment, +}; + +impl checkbox::Renderer for Renderer +where + B: Backend + backend::Text, +{ + type Style = Box; + + 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_sheet: &Self::Style, + ) -> Self::Output { + let style = if is_mouse_over { + style_sheet.hovered(is_checked) + } else { + style_sheet.active(is_checked) + }; + + let checkbox = Primitive::Quad { + bounds, + background: style.background, + border_radius: style.border_radius, + border_width: style.border_width, + border_color: style.border_color, + }; + + ( + Primitive::Group { + primitives: if is_checked { + let check = Primitive::Text { + content: B::CHECKMARK_ICON.to_string(), + font: B::ICON_FONT, + size: bounds.height * 0.7, + bounds: Rectangle { + x: bounds.center_x(), + y: bounds.center_y(), + ..bounds + }, + color: style.checkmark_color, + horizontal_alignment: HorizontalAlignment::Center, + vertical_alignment: VerticalAlignment::Center, + }; + + vec![checkbox, check, label] + } else { + vec![checkbox, label] + }, + }, + if is_mouse_over { + mouse::Interaction::Pointer + } else { + mouse::Interaction::default() + }, + ) + } +} diff --git a/graphics/src/renderer/widget/column.rs b/graphics/src/renderer/widget/column.rs new file mode 100644 index 00000000..b70d2338 --- /dev/null +++ b/graphics/src/renderer/widget/column.rs @@ -0,0 +1,39 @@ +use crate::{Backend, Primitive, Renderer}; +use iced_native::column; +use iced_native::mouse; +use iced_native::{Element, Layout, Point}; + +impl column::Renderer for Renderer +where + B: Backend, +{ + fn draw( + &mut self, + defaults: &Self::Defaults, + content: &[Element<'_, Message, Self>], + layout: Layout<'_>, + cursor_position: Point, + ) -> Self::Output { + let mut mouse_interaction = mouse::Interaction::default(); + + ( + Primitive::Group { + primitives: content + .iter() + .zip(layout.children()) + .map(|(child, layout)| { + let (primitive, new_mouse_interaction) = + child.draw(self, defaults, layout, cursor_position); + + if new_mouse_interaction > mouse_interaction { + mouse_interaction = new_mouse_interaction; + } + + primitive + }) + .collect(), + }, + mouse_interaction, + ) + } +} diff --git a/graphics/src/renderer/widget/container.rs b/graphics/src/renderer/widget/container.rs new file mode 100644 index 00000000..a1f6a211 --- /dev/null +++ b/graphics/src/renderer/widget/container.rs @@ -0,0 +1,53 @@ +use crate::container; +use crate::defaults::{self, Defaults}; +use crate::{Backend, Primitive, Renderer}; +use iced_native::{Background, Color, Element, Layout, Point, Rectangle}; + +impl iced_native::container::Renderer for Renderer +where + B: Backend, +{ + type Style = Box; + + fn draw( + &mut self, + defaults: &Defaults, + bounds: Rectangle, + cursor_position: Point, + style_sheet: &Self::Style, + content: &Element<'_, Message, Self>, + content_layout: Layout<'_>, + ) -> Self::Output { + let style = style_sheet.style(); + + let defaults = Defaults { + text: defaults::Text { + color: style.text_color.unwrap_or(defaults.text.color), + }, + }; + + let (content, mouse_interaction) = + content.draw(self, &defaults, content_layout, cursor_position); + + if style.background.is_some() || style.border_width > 0 { + let quad = Primitive::Quad { + bounds, + background: style + .background + .unwrap_or(Background::Color(Color::TRANSPARENT)), + border_radius: style.border_radius, + border_width: style.border_width, + border_color: style.border_color, + }; + + ( + Primitive::Group { + primitives: vec![quad, content], + }, + mouse_interaction, + ) + } else { + (content, mouse_interaction) + } + } +} diff --git a/graphics/src/renderer/widget/image.rs b/graphics/src/renderer/widget/image.rs new file mode 100644 index 00000000..3092237f --- /dev/null +++ b/graphics/src/renderer/widget/image.rs @@ -0,0 +1,28 @@ +use crate::backend::{self, Backend}; +use crate::{Primitive, Renderer}; +use iced_native::image; +use iced_native::mouse; +use iced_native::Layout; + +impl image::Renderer for Renderer +where + B: Backend + backend::Image, +{ + fn dimensions(&self, handle: &image::Handle) -> (u32, u32) { + self.backend().dimensions(handle) + } + + fn draw( + &mut self, + handle: image::Handle, + layout: Layout<'_>, + ) -> Self::Output { + ( + Primitive::Image { + handle, + bounds: layout.bounds(), + }, + mouse::Interaction::default(), + ) + } +} diff --git a/graphics/src/renderer/widget/pane_grid.rs b/graphics/src/renderer/widget/pane_grid.rs new file mode 100644 index 00000000..3fa171f5 --- /dev/null +++ b/graphics/src/renderer/widget/pane_grid.rs @@ -0,0 +1,94 @@ +use crate::{Backend, Primitive, Renderer}; +use iced_native::mouse; +use iced_native::pane_grid::{self, Axis, Pane}; +use iced_native::{Element, Layout, Point, Rectangle, Vector}; + +impl pane_grid::Renderer for Renderer +where + B: Backend, +{ + fn draw( + &mut self, + defaults: &Self::Defaults, + content: &[(Pane, Element<'_, Message, Self>)], + dragging: Option, + resizing: Option, + layout: Layout<'_>, + cursor_position: Point, + ) -> Self::Output { + let pane_cursor_position = if dragging.is_some() { + // TODO: Remove once cursor availability is encoded in the type + // system + Point::new(-1.0, -1.0) + } else { + cursor_position + }; + + let mut mouse_interaction = mouse::Interaction::default(); + let mut dragged_pane = None; + + let mut panes: Vec<_> = content + .iter() + .zip(layout.children()) + .enumerate() + .map(|(i, ((id, pane), layout))| { + let (primitive, new_mouse_interaction) = + pane.draw(self, defaults, layout, pane_cursor_position); + + if new_mouse_interaction > mouse_interaction { + mouse_interaction = new_mouse_interaction; + } + + if Some(*id) == dragging { + dragged_pane = Some((i, layout)); + } + + primitive + }) + .collect(); + + let primitives = if let Some((index, layout)) = dragged_pane { + let pane = panes.remove(index); + let bounds = layout.bounds(); + + // TODO: Fix once proper layering is implemented. + // This is a pretty hacky way to achieve layering. + let clip = Primitive::Clip { + bounds: Rectangle { + x: cursor_position.x - bounds.width / 2.0, + y: cursor_position.y - bounds.height / 2.0, + width: bounds.width + 0.5, + height: bounds.height + 0.5, + }, + offset: Vector::new(0, 0), + content: Box::new(Primitive::Translate { + translation: Vector::new( + cursor_position.x - bounds.x - bounds.width / 2.0, + cursor_position.y - bounds.y - bounds.height / 2.0, + ), + content: Box::new(pane), + }), + }; + + panes.push(clip); + + panes + } else { + panes + }; + + ( + Primitive::Group { primitives }, + if dragging.is_some() { + mouse::Interaction::Grabbing + } else if let Some(axis) = resizing { + match axis { + Axis::Horizontal => mouse::Interaction::ResizingVertically, + Axis::Vertical => mouse::Interaction::ResizingHorizontally, + } + } else { + mouse_interaction + }, + ) + } +} diff --git a/graphics/src/renderer/widget/progress_bar.rs b/graphics/src/renderer/widget/progress_bar.rs new file mode 100644 index 00000000..d8a145a4 --- /dev/null +++ b/graphics/src/renderer/widget/progress_bar.rs @@ -0,0 +1,58 @@ +use crate::progress_bar::StyleSheet; +use crate::{Backend, Primitive, Renderer}; +use iced_native::{mouse, progress_bar, Color, Rectangle}; + +impl progress_bar::Renderer for Renderer +where + B: Backend, +{ + type Style = Box; + + const DEFAULT_HEIGHT: u16 = 30; + + fn draw( + &self, + bounds: Rectangle, + range: std::ops::RangeInclusive, + value: f32, + style_sheet: &Self::Style, + ) -> Self::Output { + let style = style_sheet.style(); + + let (range_start, range_end) = range.into_inner(); + let active_progress_width = bounds.width + * ((value - range_start) / (range_end - range_start).max(1.0)); + + let background = Primitive::Group { + primitives: vec![Primitive::Quad { + bounds: Rectangle { ..bounds }, + background: style.background, + border_radius: style.border_radius, + border_width: 0, + border_color: Color::TRANSPARENT, + }], + }; + + ( + if active_progress_width > 0.0 { + let bar = Primitive::Quad { + bounds: Rectangle { + width: active_progress_width, + ..bounds + }, + background: style.bar, + border_radius: style.border_radius, + border_width: 0, + border_color: Color::TRANSPARENT, + }; + + Primitive::Group { + primitives: vec![background, bar], + } + } else { + background + }, + mouse::Interaction::default(), + ) + } +} diff --git a/graphics/src/renderer/widget/radio.rs b/graphics/src/renderer/widget/radio.rs new file mode 100644 index 00000000..4da2b60a --- /dev/null +++ b/graphics/src/renderer/widget/radio.rs @@ -0,0 +1,66 @@ +use crate::{radio::StyleSheet, Backend, Primitive, Renderer}; +use iced_native::{mouse, radio, Background, Color, Rectangle}; + +const SIZE: f32 = 28.0; +const DOT_SIZE: f32 = SIZE / 2.0; + +impl radio::Renderer for Renderer +where + B: Backend, +{ + type Style = Box; + + const DEFAULT_SIZE: u16 = SIZE as u16; + const DEFAULT_SPACING: u16 = 15; + + fn draw( + &mut self, + bounds: Rectangle, + is_selected: bool, + is_mouse_over: bool, + (label, _): Self::Output, + style_sheet: &Self::Style, + ) -> Self::Output { + let style = if is_mouse_over { + style_sheet.hovered() + } else { + style_sheet.active() + }; + + let radio = Primitive::Quad { + bounds, + background: style.background, + border_radius: (SIZE / 2.0) as u16, + border_width: style.border_width, + border_color: style.border_color, + }; + + ( + Primitive::Group { + primitives: if is_selected { + let radio_circle = Primitive::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, + }, + background: Background::Color(style.dot_color), + border_radius: (DOT_SIZE / 2.0) as u16, + border_width: 0, + border_color: Color::TRANSPARENT, + }; + + vec![radio, radio_circle, label] + } else { + vec![radio, label] + }, + }, + if is_mouse_over { + mouse::Interaction::Pointer + } else { + mouse::Interaction::default() + }, + ) + } +} diff --git a/graphics/src/renderer/widget/row.rs b/graphics/src/renderer/widget/row.rs new file mode 100644 index 00000000..b0bb0d2e --- /dev/null +++ b/graphics/src/renderer/widget/row.rs @@ -0,0 +1,39 @@ +use crate::{Backend, Primitive, Renderer}; +use iced_native::mouse; +use iced_native::row; +use iced_native::{Element, Layout, Point}; + +impl row::Renderer for Renderer +where + B: Backend, +{ + fn draw( + &mut self, + defaults: &Self::Defaults, + children: &[Element<'_, Message, Self>], + layout: Layout<'_>, + cursor_position: Point, + ) -> Self::Output { + let mut mouse_interaction = mouse::Interaction::default(); + + ( + Primitive::Group { + primitives: children + .iter() + .zip(layout.children()) + .map(|(child, layout)| { + let (primitive, new_mouse_interaction) = + child.draw(self, defaults, layout, cursor_position); + + if new_mouse_interaction > mouse_interaction { + mouse_interaction = new_mouse_interaction; + } + + primitive + }) + .collect(), + }, + mouse_interaction, + ) + } +} diff --git a/graphics/src/renderer/widget/scrollable.rs b/graphics/src/renderer/widget/scrollable.rs new file mode 100644 index 00000000..8db17bec --- /dev/null +++ b/graphics/src/renderer/widget/scrollable.rs @@ -0,0 +1,130 @@ +use crate::{Backend, Primitive, Renderer}; +use iced_native::mouse; +use iced_native::scrollable; +use iced_native::{Background, Color, Rectangle, Vector}; + +const SCROLLBAR_WIDTH: u16 = 10; +const SCROLLBAR_MARGIN: u16 = 2; + +impl scrollable::Renderer for Renderer +where + B: Backend, +{ + type Style = Box; + + fn scrollbar( + &self, + bounds: Rectangle, + content_bounds: Rectangle, + offset: u32, + ) -> Option { + if content_bounds.height > bounds.height { + let scrollbar_bounds = Rectangle { + x: bounds.x + bounds.width + - f32::from(SCROLLBAR_WIDTH + 2 * SCROLLBAR_MARGIN), + y: bounds.y, + width: f32::from(SCROLLBAR_WIDTH + 2 * SCROLLBAR_MARGIN), + height: bounds.height, + }; + + let ratio = bounds.height / content_bounds.height; + let scrollbar_height = bounds.height * ratio; + let y_offset = offset as f32 * ratio; + + let scroller_bounds = Rectangle { + x: scrollbar_bounds.x + f32::from(SCROLLBAR_MARGIN), + y: scrollbar_bounds.y + y_offset, + width: scrollbar_bounds.width - f32::from(2 * SCROLLBAR_MARGIN), + height: scrollbar_height, + }; + + Some(scrollable::Scrollbar { + bounds: scrollbar_bounds, + scroller: scrollable::Scroller { + bounds: scroller_bounds, + }, + }) + } else { + None + } + } + + fn draw( + &mut self, + state: &scrollable::State, + bounds: Rectangle, + _content_bounds: Rectangle, + is_mouse_over: bool, + is_mouse_over_scrollbar: bool, + scrollbar: Option, + offset: u32, + style_sheet: &Self::Style, + (content, mouse_interaction): Self::Output, + ) -> Self::Output { + ( + if let Some(scrollbar) = scrollbar { + let clip = Primitive::Clip { + bounds, + offset: Vector::new(0, offset), + content: Box::new(content), + }; + + let style = if state.is_scroller_grabbed() { + style_sheet.dragging() + } else if is_mouse_over_scrollbar { + style_sheet.hovered() + } else { + style_sheet.active() + }; + + let is_scrollbar_visible = + style.background.is_some() || style.border_width > 0; + + let scroller = if is_mouse_over + || state.is_scroller_grabbed() + || is_scrollbar_visible + { + Primitive::Quad { + bounds: scrollbar.scroller.bounds, + background: Background::Color(style.scroller.color), + border_radius: style.scroller.border_radius, + border_width: style.scroller.border_width, + border_color: style.scroller.border_color, + } + } else { + Primitive::None + }; + + let scrollbar = if is_scrollbar_visible { + Primitive::Quad { + bounds: Rectangle { + x: scrollbar.bounds.x + f32::from(SCROLLBAR_MARGIN), + width: scrollbar.bounds.width + - f32::from(2 * SCROLLBAR_MARGIN), + ..scrollbar.bounds + }, + background: style + .background + .unwrap_or(Background::Color(Color::TRANSPARENT)), + border_radius: style.border_radius, + border_width: style.border_width, + border_color: style.border_color, + } + } else { + Primitive::None + }; + + Primitive::Group { + primitives: vec![clip, scrollbar, scroller], + } + } else { + content + }, + if is_mouse_over_scrollbar || state.is_scroller_grabbed() { + mouse::Interaction::Idle + } else { + mouse_interaction + }, + ) + } +} diff --git a/graphics/src/renderer/widget/slider.rs b/graphics/src/renderer/widget/slider.rs new file mode 100644 index 00000000..95c843d0 --- /dev/null +++ b/graphics/src/renderer/widget/slider.rs @@ -0,0 +1,109 @@ +use crate::slider::{HandleShape, StyleSheet}; +use crate::{Backend, Primitive, Renderer}; +use iced_native::mouse; +use iced_native::slider; +use iced_native::{Background, Color, Point, Rectangle}; + +const HANDLE_HEIGHT: f32 = 22.0; + +impl slider::Renderer for Renderer +where + B: Backend, +{ + type Style = Box; + + fn height(&self) -> u32 { + 30 + } + + fn draw( + &mut self, + bounds: Rectangle, + cursor_position: Point, + range: std::ops::RangeInclusive, + value: f32, + is_dragging: bool, + style_sheet: &Self::Style, + ) -> Self::Output { + let is_mouse_over = bounds.contains(cursor_position); + + let style = if is_dragging { + style_sheet.dragging() + } else if is_mouse_over { + style_sheet.hovered() + } else { + style_sheet.active() + }; + + let rail_y = bounds.y + (bounds.height / 2.0).round(); + + let (rail_top, rail_bottom) = ( + Primitive::Quad { + bounds: Rectangle { + x: bounds.x, + y: rail_y, + width: bounds.width, + height: 2.0, + }, + background: Background::Color(style.rail_colors.0), + border_radius: 0, + border_width: 0, + border_color: Color::TRANSPARENT, + }, + Primitive::Quad { + bounds: Rectangle { + x: bounds.x, + y: rail_y + 2.0, + width: bounds.width, + height: 2.0, + }, + background: Background::Color(style.rail_colors.1), + border_radius: 0, + border_width: 0, + border_color: Color::TRANSPARENT, + }, + ); + + let (range_start, range_end) = range.into_inner(); + + let (handle_width, handle_height, handle_border_radius) = + match style.handle.shape { + HandleShape::Circle { radius } => { + (f32::from(radius * 2), f32::from(radius * 2), radius) + } + HandleShape::Rectangle { + width, + border_radius, + } => (f32::from(width), HANDLE_HEIGHT, border_radius), + }; + + let handle_offset = (bounds.width - handle_width) + * ((value - range_start) / (range_end - range_start).max(1.0)); + + let handle = Primitive::Quad { + bounds: Rectangle { + x: bounds.x + handle_offset.round(), + y: rail_y - handle_height / 2.0, + width: handle_width, + height: handle_height, + }, + background: Background::Color(style.handle.color), + border_radius: handle_border_radius, + border_width: style.handle.border_width, + border_color: style.handle.border_color, + }; + + ( + Primitive::Group { + primitives: vec![rail_top, rail_bottom, handle], + }, + if is_dragging { + mouse::Interaction::Grabbing + } else if is_mouse_over { + mouse::Interaction::Grab + } else { + mouse::Interaction::default() + }, + ) + } +} diff --git a/graphics/src/renderer/widget/space.rs b/graphics/src/renderer/widget/space.rs new file mode 100644 index 00000000..d0e82f8d --- /dev/null +++ b/graphics/src/renderer/widget/space.rs @@ -0,0 +1,13 @@ +use crate::{Backend, Primitive, Renderer}; +use iced_native::mouse; +use iced_native::space; +use iced_native::Rectangle; + +impl space::Renderer for Renderer +where + B: Backend, +{ + fn draw(&mut self, _bounds: Rectangle) -> Self::Output { + (Primitive::None, mouse::Interaction::default()) + } +} diff --git a/graphics/src/renderer/widget/svg.rs b/graphics/src/renderer/widget/svg.rs new file mode 100644 index 00000000..4d80869e --- /dev/null +++ b/graphics/src/renderer/widget/svg.rs @@ -0,0 +1,26 @@ +use crate::backend::{self, Backend}; +use crate::{Primitive, Renderer}; +use iced_native::{mouse, svg, Layout}; + +impl svg::Renderer for Renderer +where + B: Backend + backend::Svg, +{ + fn dimensions(&self, handle: &svg::Handle) -> (u32, u32) { + self.backend().viewport_dimensions(handle) + } + + fn draw( + &mut self, + handle: svg::Handle, + layout: Layout<'_>, + ) -> Self::Output { + ( + Primitive::Svg { + handle, + bounds: layout.bounds(), + }, + mouse::Interaction::default(), + ) + } +} diff --git a/graphics/src/renderer/widget/text.rs b/graphics/src/renderer/widget/text.rs new file mode 100644 index 00000000..45cb1be2 --- /dev/null +++ b/graphics/src/renderer/widget/text.rs @@ -0,0 +1,65 @@ +use crate::backend::{self, Backend}; +use crate::{Primitive, Renderer}; +use iced_native::{ + mouse, text, Color, Font, HorizontalAlignment, Rectangle, Size, + VerticalAlignment, +}; + +use std::f32; + +impl text::Renderer for Renderer +where + B: Backend + backend::Text, +{ + type Font = Font; + + const DEFAULT_SIZE: u16 = 20; + + fn measure( + &self, + content: &str, + size: u16, + font: Font, + bounds: Size, + ) -> (f32, f32) { + self.backend() + .measure(content, f32::from(size), font, bounds) + } + + fn draw( + &mut self, + defaults: &Self::Defaults, + bounds: Rectangle, + content: &str, + size: u16, + font: Font, + color: Option, + horizontal_alignment: HorizontalAlignment, + vertical_alignment: VerticalAlignment, + ) -> Self::Output { + let x = match horizontal_alignment { + iced_native::HorizontalAlignment::Left => bounds.x, + iced_native::HorizontalAlignment::Center => bounds.center_x(), + iced_native::HorizontalAlignment::Right => bounds.x + bounds.width, + }; + + let y = match vertical_alignment { + iced_native::VerticalAlignment::Top => bounds.y, + iced_native::VerticalAlignment::Center => bounds.center_y(), + iced_native::VerticalAlignment::Bottom => bounds.y + bounds.height, + }; + + ( + Primitive::Text { + content: content.to_string(), + size: f32::from(size), + bounds: Rectangle { x, y, ..bounds }, + color: color.unwrap_or(defaults.text.color), + font, + horizontal_alignment, + vertical_alignment, + }, + mouse::Interaction::default(), + ) + } +} diff --git a/graphics/src/renderer/widget/text_input.rs b/graphics/src/renderer/widget/text_input.rs new file mode 100644 index 00000000..33807b0f --- /dev/null +++ b/graphics/src/renderer/widget/text_input.rs @@ -0,0 +1,267 @@ +use crate::backend::{self, Backend}; +use crate::text_input::StyleSheet; +use crate::{Primitive, Renderer}; + +use iced_native::{ + mouse, + text_input::{self, cursor}, + Background, Color, Font, HorizontalAlignment, Point, Rectangle, Size, + Vector, VerticalAlignment, +}; +use std::f32; + +impl text_input::Renderer for Renderer +where + B: Backend + backend::Text, +{ + type Style = Box; + + fn default_size(&self) -> u16 { + // TODO: Make this configurable + 20 + } + + fn measure_value(&self, value: &str, size: u16, font: Font) -> f32 { + let backend = self.backend(); + + let (mut width, _) = + backend.measure(value, f32::from(size), font, Size::INFINITY); + + let spaces_around = value.len() - value.trim().len(); + + if spaces_around > 0 { + let space_width = backend.space_width(size as f32); + width += spaces_around as f32 * space_width; + } + + width + } + + fn offset( + &self, + text_bounds: Rectangle, + font: Font, + size: u16, + value: &text_input::Value, + state: &text_input::State, + ) -> f32 { + 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( + self, + text_bounds, + value, + size, + focus_position, + font, + ); + + offset + } else { + 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_sheet: &Self::Style, + ) -> Self::Output { + let is_mouse_over = bounds.contains(cursor_position); + + let style = if state.is_focused() { + style_sheet.focused() + } else if is_mouse_over { + style_sheet.hovered() + } else { + style_sheet.active() + }; + + let input = Primitive::Quad { + bounds, + background: style.background, + border_radius: style.border_radius, + border_width: style.border_width, + border_color: style.border_color, + }; + + let text = value.to_string(); + + let text_value = Primitive::Text { + content: if text.is_empty() { + placeholder.to_string() + } else { + text.clone() + }, + color: if text.is_empty() { + style_sheet.placeholder_color() + } else { + style_sheet.value_color() + }, + font, + bounds: Rectangle { + y: text_bounds.center_y(), + width: f32::INFINITY, + ..text_bounds + }, + size: f32::from(size), + horizontal_alignment: HorizontalAlignment::Left, + vertical_alignment: VerticalAlignment::Center, + }; + + let (contents_primitive, offset) = if state.is_focused() { + let cursor = state.cursor(); + + let (cursor_primitive, offset) = match cursor.state(value) { + cursor::State::Index(position) => { + let (text_value_width, offset) = + measure_cursor_and_scroll_offset( + self, + text_bounds, + value, + size, + position, + font, + ); + + ( + Primitive::Quad { + bounds: Rectangle { + x: text_bounds.x + text_value_width, + y: text_bounds.y, + width: 1.0, + height: text_bounds.height, + }, + background: Background::Color( + style_sheet.value_color(), + ), + border_radius: 0, + border_width: 0, + border_color: Color::TRANSPARENT, + }, + 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( + self, + text_bounds, + value, + size, + left, + font, + ); + + let (right_position, right_offset) = + measure_cursor_and_scroll_offset( + self, + text_bounds, + value, + size, + right, + font, + ); + + let width = right_position - left_position; + + ( + Primitive::Quad { + bounds: Rectangle { + x: text_bounds.x + left_position, + y: text_bounds.y, + width, + height: text_bounds.height, + }, + background: Background::Color( + style_sheet.selection_color(), + ), + border_radius: 0, + border_width: 0, + border_color: Color::TRANSPARENT, + }, + if end == right { + right_offset + } else { + left_offset + }, + ) + } + }; + + ( + Primitive::Group { + primitives: vec![cursor_primitive, text_value], + }, + Vector::new(offset as u32, 0), + ) + } else { + (text_value, Vector::new(0, 0)) + }; + + let text_width = self.measure_value( + if text.is_empty() { placeholder } else { &text }, + size, + font, + ); + + let contents = if text_width > text_bounds.width { + Primitive::Clip { + bounds: text_bounds, + offset, + content: Box::new(contents_primitive), + } + } else { + contents_primitive + }; + + ( + Primitive::Group { + primitives: vec![input, contents], + }, + if is_mouse_over { + mouse::Interaction::Text + } else { + mouse::Interaction::default() + }, + ) + } +} + +fn measure_cursor_and_scroll_offset( + renderer: &Renderer, + text_bounds: Rectangle, + value: &text_input::Value, + size: u16, + cursor_index: usize, + font: Font, +) -> (f32, f32) +where + B: Backend + backend::Text, +{ + use iced_native::text_input::Renderer; + + let text_before_cursor = value.until(cursor_index).to_string(); + + let text_value_width = + renderer.measure_value(&text_before_cursor, size, font); + let offset = ((text_value_width + 5.0) - text_bounds.width).max(0.0); + + (text_value_width, offset) +} diff --git a/graphics/src/triangle.rs b/graphics/src/triangle.rs new file mode 100644 index 00000000..2b157e3a --- /dev/null +++ b/graphics/src/triangle.rs @@ -0,0 +1,26 @@ +/// A set of [`Vertex2D`] and indices representing a list of triangles. +/// +/// [`Vertex2D`]: struct.Vertex2D.html +#[derive(Clone, Debug)] +pub struct Mesh2D { + /// The vertices of the mesh + pub vertices: Vec, + /// The list of vertex indices that defines the triangles of the mesh. + /// + /// Therefore, this list should always have a length that is a multiple of + /// 3. + pub indices: Vec, +} + +/// A two-dimensional vertex with some color in __linear__ RGBA. +#[derive(Copy, Clone, Debug)] +#[repr(C)] +pub struct Vertex2D { + /// The vertex position + pub position: [f32; 2], + /// The vertex color in __linear__ RGBA. + pub color: [f32; 4], +} + +unsafe impl bytemuck::Zeroable for Vertex2D {} +unsafe impl bytemuck::Pod for Vertex2D {} diff --git a/graphics/src/widget.rs b/graphics/src/widget.rs new file mode 100644 index 00000000..6a8e814b --- /dev/null +++ b/graphics/src/widget.rs @@ -0,0 +1,49 @@ +//! Use the widgets supported out-of-the-box. +//! +//! # Re-exports +//! For convenience, the contents of this module are available at the root +//! module. Therefore, you can directly type: +//! +//! ``` +//! use iced_graphics::{button, Button}; +//! ``` +pub mod button; +pub mod checkbox; +pub mod container; +pub mod pane_grid; +pub mod progress_bar; +pub mod radio; +pub mod scrollable; +pub mod slider; +pub mod text_input; + +mod text; + +#[doc(no_inline)] +pub use button::Button; +#[doc(no_inline)] +pub use checkbox::Checkbox; +#[doc(no_inline)] +pub use container::Container; +#[doc(no_inline)] +pub use pane_grid::PaneGrid; +#[doc(no_inline)] +pub use progress_bar::ProgressBar; +#[doc(no_inline)] +pub use radio::Radio; +#[doc(no_inline)] +pub use scrollable::Scrollable; +#[doc(no_inline)] +pub use slider::Slider; +#[doc(no_inline)] +pub use text_input::TextInput; + +pub use text::Text; + +#[cfg(feature = "canvas")] +#[cfg_attr(docsrs, doc(cfg(feature = "canvas")))] +pub mod canvas; + +#[cfg(feature = "canvas")] +#[doc(no_inline)] +pub use canvas::Canvas; diff --git a/graphics/src/widget/button.rs b/graphics/src/widget/button.rs new file mode 100644 index 00000000..9debf5c3 --- /dev/null +++ b/graphics/src/widget/button.rs @@ -0,0 +1,16 @@ +//! Allow your users to perform actions by pressing a button. +//! +//! A [`Button`] has some local [`State`]. +//! +//! [`Button`]: type.Button.html +//! [`State`]: struct.State.html +use crate::Renderer; + +pub use iced_native::button::State; +pub use iced_style::button::{Style, StyleSheet}; + +/// A widget that produces a message when clicked. +/// +/// This is an alias of an `iced_native` button with an `iced_wgpu::Renderer`. +pub type Button<'a, Message, Backend> = + iced_native::Button<'a, Message, Renderer>; diff --git a/graphics/src/widget/canvas.rs b/graphics/src/widget/canvas.rs new file mode 100644 index 00000000..d393a5c5 --- /dev/null +++ b/graphics/src/widget/canvas.rs @@ -0,0 +1,236 @@ +//! Draw 2D graphics for your users. +//! +//! A [`Canvas`] widget can be used to draw different kinds of 2D shapes in a +//! [`Frame`]. It can be used for animation, data visualization, game graphics, +//! and more! +//! +//! [`Canvas`]: struct.Canvas.html +//! [`Frame`]: struct.Frame.html +use crate::{Backend, Defaults, Primitive, Renderer}; +use iced_native::{ + layout, mouse, Clipboard, Element, Hasher, Layout, Length, Point, Size, + Vector, Widget, +}; +use std::hash::Hash; +use std::marker::PhantomData; + +pub mod path; + +mod cache; +mod cursor; +mod event; +mod fill; +mod frame; +mod geometry; +mod program; +mod stroke; +mod text; + +pub use cache::Cache; +pub use cursor::Cursor; +pub use event::Event; +pub use fill::Fill; +pub use frame::Frame; +pub use geometry::Geometry; +pub use path::Path; +pub use program::Program; +pub use stroke::{LineCap, LineJoin, Stroke}; +pub use text::Text; + +/// A widget capable of drawing 2D graphics. +/// +/// [`Canvas`]: struct.Canvas.html +/// +/// # Examples +/// The repository has a couple of [examples] showcasing how to use a +/// [`Canvas`]: +/// +/// - [`clock`], an application that uses the [`Canvas`] widget to draw a clock +/// and its hands to display the current time. +/// - [`game_of_life`], an interactive version of the Game of Life, invented by +/// John Conway. +/// - [`solar_system`], an animated solar system drawn using the [`Canvas`] widget +/// and showcasing how to compose different transforms. +/// +/// [examples]: https://github.com/hecrj/iced/tree/master/examples +/// [`clock`]: https://github.com/hecrj/iced/tree/master/examples/clock +/// [`game_of_life`]: https://github.com/hecrj/iced/tree/master/examples/game_of_life +/// [`solar_system`]: https://github.com/hecrj/iced/tree/master/examples/solar_system +/// +/// ## Drawing a simple circle +/// If you want to get a quick overview, here's how we can draw a simple circle: +/// +/// ```no_run +/// # mod iced { +/// # pub use iced_graphics::canvas; +/// # pub use iced_native::{Color, Rectangle}; +/// # } +/// use iced::canvas::{self, Canvas, Cursor, Fill, Frame, Geometry, Path, Program}; +/// use iced::{Color, Rectangle}; +/// +/// // First, we define the data we need for drawing +/// #[derive(Debug)] +/// struct Circle { +/// radius: f32, +/// } +/// +/// // Then, we implement the `Program` trait +/// impl Program<()> for Circle { +/// fn draw(&self, bounds: Rectangle, _cursor: Cursor) -> Vec{ +/// // We prepare a new `Frame` +/// let mut frame = Frame::new(bounds.size()); +/// +/// // We create a `Path` representing a simple circle +/// let circle = Path::circle(frame.center(), self.radius); +/// +/// // And fill it with some color +/// frame.fill(&circle, Fill::Color(Color::BLACK)); +/// +/// // Finally, we produce the geometry +/// vec![frame.into_geometry()] +/// } +/// } +/// +/// // Finally, we simply use our `Circle` to create the `Canvas`! +/// let canvas = Canvas::new(Circle { radius: 50.0 }); +/// ``` +#[derive(Debug)] +pub struct Canvas> { + width: Length, + height: Length, + program: P, + phantom: PhantomData, +} + +impl> Canvas { + const DEFAULT_SIZE: u16 = 100; + + /// Creates a new [`Canvas`]. + /// + /// [`Canvas`]: struct.Canvas.html + pub fn new(program: P) -> Self { + Canvas { + width: Length::Units(Self::DEFAULT_SIZE), + height: Length::Units(Self::DEFAULT_SIZE), + program, + phantom: PhantomData, + } + } + + /// Sets the width of the [`Canvas`]. + /// + /// [`Canvas`]: struct.Canvas.html + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + /// Sets the height of the [`Canvas`]. + /// + /// [`Canvas`]: struct.Canvas.html + pub fn height(mut self, height: Length) -> Self { + self.height = height; + self + } +} + +impl Widget> for Canvas +where + P: Program, + B: Backend, +{ + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + _renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits.width(self.width).height(self.height); + let size = limits.resolve(Size::ZERO); + + layout::Node::new(size) + } + + fn on_event( + &mut self, + event: iced_native::Event, + layout: Layout<'_>, + cursor_position: Point, + messages: &mut Vec, + _renderer: &Renderer, + _clipboard: Option<&dyn Clipboard>, + ) { + let bounds = layout.bounds(); + + let canvas_event = match event { + iced_native::Event::Mouse(mouse_event) => { + Some(Event::Mouse(mouse_event)) + } + _ => None, + }; + + let cursor = Cursor::from_window_position(cursor_position); + + if let Some(canvas_event) = canvas_event { + if let Some(message) = + self.program.update(canvas_event, bounds, cursor) + { + messages.push(message); + } + } + } + + fn draw( + &self, + _renderer: &mut Renderer, + _defaults: &Defaults, + layout: Layout<'_>, + cursor_position: Point, + ) -> (Primitive, mouse::Interaction) { + let bounds = layout.bounds(); + let translation = Vector::new(bounds.x, bounds.y); + let cursor = Cursor::from_window_position(cursor_position); + + ( + Primitive::Translate { + translation, + content: Box::new(Primitive::Group { + primitives: self + .program + .draw(bounds, cursor) + .into_iter() + .map(Geometry::into_primitive) + .collect(), + }), + }, + self.program.mouse_interaction(bounds, cursor), + ) + } + + fn hash_layout(&self, state: &mut Hasher) { + struct Marker; + std::any::TypeId::of::().hash(state); + + self.width.hash(state); + self.height.hash(state); + } +} + +impl<'a, Message, P, B> From> + for Element<'a, Message, Renderer> +where + Message: 'static, + P: Program + 'a, + B: Backend, +{ + fn from(canvas: Canvas) -> Element<'a, Message, Renderer> { + Element::new(canvas) + } +} diff --git a/graphics/src/widget/canvas/cache.rs b/graphics/src/widget/canvas/cache.rs new file mode 100644 index 00000000..4b28d164 --- /dev/null +++ b/graphics/src/widget/canvas/cache.rs @@ -0,0 +1,108 @@ +use crate::{ + canvas::{Frame, Geometry}, + Primitive, +}; + +use iced_native::Size; +use std::{cell::RefCell, sync::Arc}; + +enum State { + Empty, + Filled { + bounds: Size, + primitive: Arc, + }, +} + +impl Default for State { + fn default() -> Self { + State::Empty + } +} +/// A simple cache that stores generated [`Geometry`] to avoid recomputation. +/// +/// A [`Cache`] will not redraw its geometry unless the dimensions of its layer +/// change or it is explicitly cleared. +/// +/// [`Layer`]: ../trait.Layer.html +/// [`Cache`]: struct.Cache.html +/// [`Geometry`]: struct.Geometry.html +#[derive(Debug, Default)] +pub struct Cache { + state: RefCell, +} + +impl Cache { + /// Creates a new empty [`Cache`]. + /// + /// [`Cache`]: struct.Cache.html + pub fn new() -> Self { + Cache { + state: Default::default(), + } + } + + /// Clears the [`Cache`], forcing a redraw the next time it is used. + /// + /// [`Cache`]: struct.Cache.html + pub fn clear(&mut self) { + *self.state.borrow_mut() = State::Empty; + } + + /// Draws [`Geometry`] using the provided closure and stores it in the + /// [`Cache`]. + /// + /// The closure will only be called when + /// - the bounds have changed since the previous draw call. + /// - the [`Cache`] is empty or has been explicitly cleared. + /// + /// Otherwise, the previously stored [`Geometry`] will be returned. The + /// [`Cache`] is not cleared in this case. In other words, it will keep + /// returning the stored [`Geometry`] if needed. + /// + /// [`Cache`]: struct.Cache.html + pub fn draw(&self, bounds: Size, draw_fn: impl Fn(&mut Frame)) -> Geometry { + use std::ops::Deref; + + if let State::Filled { + bounds: cached_bounds, + primitive, + } = self.state.borrow().deref() + { + if *cached_bounds == bounds { + return Geometry::from_primitive(Primitive::Cached { + cache: primitive.clone(), + }); + } + } + + let mut frame = Frame::new(bounds); + draw_fn(&mut frame); + + let primitive = { + let geometry = frame.into_geometry(); + + Arc::new(geometry.into_primitive()) + }; + + *self.state.borrow_mut() = State::Filled { + bounds, + primitive: primitive.clone(), + }; + + Geometry::from_primitive(Primitive::Cached { cache: primitive }) + } +} + +impl std::fmt::Debug for State { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + State::Empty => write!(f, "Empty"), + State::Filled { primitive, bounds } => f + .debug_struct("Filled") + .field("primitive", primitive) + .field("bounds", bounds) + .finish(), + } + } +} diff --git a/graphics/src/widget/canvas/cursor.rs b/graphics/src/widget/canvas/cursor.rs new file mode 100644 index 00000000..456760ea --- /dev/null +++ b/graphics/src/widget/canvas/cursor.rs @@ -0,0 +1,72 @@ +use iced_native::{Point, Rectangle}; + +/// The mouse cursor state. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Cursor { + /// The cursor has a defined position. + Available(Point), + + /// The cursor is currently unavailable (i.e. out of bounds or busy). + Unavailable, +} + +impl Cursor { + // TODO: Remove this once this type is used in `iced_native` to encode + // proper cursor availability + pub(crate) fn from_window_position(position: Point) -> Self { + if position.x < 0.0 || position.y < 0.0 { + Cursor::Unavailable + } else { + Cursor::Available(position) + } + } + + /// Returns the absolute position of the [`Cursor`], if available. + /// + /// [`Cursor`]: enum.Cursor.html + pub fn position(&self) -> Option { + match self { + Cursor::Available(position) => Some(*position), + Cursor::Unavailable => None, + } + } + + /// Returns the relative position of the [`Cursor`] inside the given bounds, + /// if available. + /// + /// If the [`Cursor`] is not over the provided bounds, this method will + /// return `None`. + /// + /// [`Cursor`]: enum.Cursor.html + pub fn position_in(&self, bounds: &Rectangle) -> Option { + if self.is_over(bounds) { + self.position_from(bounds.position()) + } else { + None + } + } + + /// Returns the relative position of the [`Cursor`] from the given origin, + /// if available. + /// + /// [`Cursor`]: enum.Cursor.html + pub fn position_from(&self, origin: Point) -> Option { + match self { + Cursor::Available(position) => { + Some(Point::new(position.x - origin.x, position.y - origin.y)) + } + Cursor::Unavailable => None, + } + } + + /// Returns whether the [`Cursor`] is currently over the provided bounds + /// or not. + /// + /// [`Cursor`]: enum.Cursor.html + pub fn is_over(&self, bounds: &Rectangle) -> bool { + match self { + Cursor::Available(position) => bounds.contains(*position), + Cursor::Unavailable => false, + } + } +} diff --git a/graphics/src/widget/canvas/event.rs b/graphics/src/widget/canvas/event.rs new file mode 100644 index 00000000..ad11f51e --- /dev/null +++ b/graphics/src/widget/canvas/event.rs @@ -0,0 +1,10 @@ +use iced_native::mouse; + +/// A [`Canvas`] event. +/// +/// [`Canvas`]: struct.Event.html +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Event { + /// A mouse event. + Mouse(mouse::Event), +} diff --git a/graphics/src/widget/canvas/fill.rs b/graphics/src/widget/canvas/fill.rs new file mode 100644 index 00000000..a2010e45 --- /dev/null +++ b/graphics/src/widget/canvas/fill.rs @@ -0,0 +1,20 @@ +use iced_native::Color; + +/// The style used to fill geometry. +#[derive(Debug, Clone, Copy)] +pub enum Fill { + /// Fill with a color. + Color(Color), +} + +impl Default for Fill { + fn default() -> Fill { + Fill::Color(Color::BLACK) + } +} + +impl From for Fill { + fn from(color: Color) -> Fill { + Fill::Color(color) + } +} diff --git a/graphics/src/widget/canvas/frame.rs b/graphics/src/widget/canvas/frame.rs new file mode 100644 index 00000000..5262ab4e --- /dev/null +++ b/graphics/src/widget/canvas/frame.rs @@ -0,0 +1,367 @@ +use iced_native::{Point, Rectangle, Size, Vector}; + +use crate::{ + canvas::{Fill, Geometry, Path, Stroke, Text}, + triangle, Primitive, +}; + +/// The frame of a [`Canvas`]. +/// +/// [`Canvas`]: struct.Canvas.html +#[derive(Debug)] +pub struct Frame { + size: Size, + buffers: lyon::tessellation::VertexBuffers, + primitives: Vec, + transforms: Transforms, +} + +#[derive(Debug)] +struct Transforms { + previous: Vec, + current: Transform, +} + +#[derive(Debug, Clone, Copy)] +struct Transform { + raw: lyon::math::Transform, + is_identity: bool, +} + +impl Frame { + /// Creates a new empty [`Frame`] with the given dimensions. + /// + /// The default coordinate system of a [`Frame`] has its origin at the + /// top-left corner of its bounds. + /// + /// [`Frame`]: struct.Frame.html + pub fn new(size: Size) -> Frame { + Frame { + size, + buffers: lyon::tessellation::VertexBuffers::new(), + primitives: Vec::new(), + transforms: Transforms { + previous: Vec::new(), + current: Transform { + raw: lyon::math::Transform::identity(), + is_identity: true, + }, + }, + } + } + + /// Returns the width of the [`Frame`]. + /// + /// [`Frame`]: struct.Frame.html + #[inline] + pub fn width(&self) -> f32 { + self.size.width + } + + /// Returns the width of the [`Frame`]. + /// + /// [`Frame`]: struct.Frame.html + #[inline] + pub fn height(&self) -> f32 { + self.size.height + } + + /// Returns the dimensions of the [`Frame`]. + /// + /// [`Frame`]: struct.Frame.html + #[inline] + pub fn size(&self) -> Size { + self.size + } + + /// Returns the coordinate of the center of the [`Frame`]. + /// + /// [`Frame`]: struct.Frame.html + #[inline] + pub fn center(&self) -> Point { + Point::new(self.size.width / 2.0, self.size.height / 2.0) + } + + /// Draws the given [`Path`] on the [`Frame`] by filling it with the + /// provided style. + /// + /// [`Path`]: path/struct.Path.html + /// [`Frame`]: struct.Frame.html + pub fn fill(&mut self, path: &Path, fill: impl Into) { + use lyon::tessellation::{ + BuffersBuilder, FillOptions, FillTessellator, + }; + + let mut buffers = BuffersBuilder::new( + &mut self.buffers, + FillVertex(match fill.into() { + Fill::Color(color) => color.into_linear(), + }), + ); + + let mut tessellator = FillTessellator::new(); + + let result = if self.transforms.current.is_identity { + tessellator.tessellate_path( + path.raw(), + &FillOptions::default(), + &mut buffers, + ) + } else { + let path = path.transformed(&self.transforms.current.raw); + + tessellator.tessellate_path( + path.raw(), + &FillOptions::default(), + &mut buffers, + ) + }; + + let _ = result.expect("Tessellate path"); + } + + /// Draws an axis-aligned rectangle given its top-left corner coordinate and + /// its `Size` on the [`Frame`] by filling it with the provided style. + /// + /// [`Frame`]: struct.Frame.html + pub fn fill_rectangle( + &mut self, + top_left: Point, + size: Size, + fill: impl Into, + ) { + use lyon::tessellation::{BuffersBuilder, FillOptions}; + + let mut buffers = BuffersBuilder::new( + &mut self.buffers, + FillVertex(match fill.into() { + Fill::Color(color) => color.into_linear(), + }), + ); + + let top_left = + self.transforms.current.raw.transform_point( + lyon::math::Point::new(top_left.x, top_left.y), + ); + + let size = + self.transforms.current.raw.transform_vector( + lyon::math::Vector::new(size.width, size.height), + ); + + let _ = lyon::tessellation::basic_shapes::fill_rectangle( + &lyon::math::Rect::new(top_left, size.into()), + &FillOptions::default(), + &mut buffers, + ) + .expect("Fill rectangle"); + } + + /// Draws the stroke of the given [`Path`] on the [`Frame`] with the + /// provided style. + /// + /// [`Path`]: path/struct.Path.html + /// [`Frame`]: struct.Frame.html + pub fn stroke(&mut self, path: &Path, stroke: impl Into) { + use lyon::tessellation::{ + BuffersBuilder, StrokeOptions, StrokeTessellator, + }; + + let stroke = stroke.into(); + + let mut buffers = BuffersBuilder::new( + &mut self.buffers, + StrokeVertex(stroke.color.into_linear()), + ); + + let mut tessellator = StrokeTessellator::new(); + + let mut options = StrokeOptions::default(); + options.line_width = stroke.width; + options.start_cap = stroke.line_cap.into(); + options.end_cap = stroke.line_cap.into(); + options.line_join = stroke.line_join.into(); + + let result = if self.transforms.current.is_identity { + tessellator.tessellate_path(path.raw(), &options, &mut buffers) + } else { + let path = path.transformed(&self.transforms.current.raw); + + tessellator.tessellate_path(path.raw(), &options, &mut buffers) + }; + + let _ = result.expect("Stroke path"); + } + + /// Draws the characters of the given [`Text`] on the [`Frame`], filling + /// them with the given color. + /// + /// __Warning:__ Text currently does not work well with rotations and scale + /// transforms! The position will be correctly transformed, but the + /// resulting glyphs will not be rotated or scaled properly. + /// + /// Additionally, all text will be rendered on top of all the layers of + /// a [`Canvas`]. Therefore, it is currently only meant to be used for + /// overlays, which is the most common use case. + /// + /// Support for vectorial text is planned, and should address all these + /// limitations. + /// + /// [`Text`]: struct.Text.html + /// [`Frame`]: struct.Frame.html + /// [`Canvas`]: struct.Canvas.html + pub fn fill_text(&mut self, text: impl Into) { + use std::f32; + + let text = text.into(); + + let position = if self.transforms.current.is_identity { + text.position + } else { + let transformed = self.transforms.current.raw.transform_point( + lyon::math::Point::new(text.position.x, text.position.y), + ); + + Point::new(transformed.x, transformed.y) + }; + + // TODO: Use vectorial text instead of primitive + self.primitives.push(Primitive::Text { + content: text.content, + bounds: Rectangle { + x: position.x, + y: position.y, + width: f32::INFINITY, + height: f32::INFINITY, + }, + color: text.color, + size: text.size, + font: text.font, + horizontal_alignment: text.horizontal_alignment, + vertical_alignment: text.vertical_alignment, + }); + } + + /// Stores the current transform of the [`Frame`] and executes the given + /// drawing operations, restoring the transform afterwards. + /// + /// This method is useful to compose transforms and perform drawing + /// operations in different coordinate systems. + /// + /// [`Frame`]: struct.Frame.html + #[inline] + pub fn with_save(&mut self, f: impl FnOnce(&mut Frame)) { + self.transforms.previous.push(self.transforms.current); + + f(self); + + self.transforms.current = self.transforms.previous.pop().unwrap(); + } + + /// Applies a translation to the current transform of the [`Frame`]. + /// + /// [`Frame`]: struct.Frame.html + #[inline] + pub fn translate(&mut self, translation: Vector) { + self.transforms.current.raw = self + .transforms + .current + .raw + .pre_translate(lyon::math::Vector::new( + translation.x, + translation.y, + )); + self.transforms.current.is_identity = false; + } + + /// Applies a rotation to the current transform of the [`Frame`]. + /// + /// [`Frame`]: struct.Frame.html + #[inline] + pub fn rotate(&mut self, angle: f32) { + self.transforms.current.raw = self + .transforms + .current + .raw + .pre_rotate(lyon::math::Angle::radians(-angle)); + self.transforms.current.is_identity = false; + } + + /// Applies a scaling to the current transform of the [`Frame`]. + /// + /// [`Frame`]: struct.Frame.html + #[inline] + pub fn scale(&mut self, scale: f32) { + self.transforms.current.raw = + self.transforms.current.raw.pre_scale(scale, scale); + self.transforms.current.is_identity = false; + } + + /// Produces the [`Geometry`] representing everything drawn on the [`Frame`]. + /// + /// [`Frame`]: struct.Frame.html + /// [`Geometry`]: struct.Geometry.html + pub fn into_geometry(mut self) -> Geometry { + if !self.buffers.indices.is_empty() { + self.primitives.push(Primitive::Mesh2D { + size: self.size, + buffers: triangle::Mesh2D { + vertices: self.buffers.vertices, + indices: self.buffers.indices, + }, + }); + } + + Geometry::from_primitive(Primitive::Group { + primitives: self.primitives, + }) + } +} + +struct FillVertex([f32; 4]); + +impl lyon::tessellation::BasicVertexConstructor + for FillVertex +{ + fn new_vertex( + &mut self, + position: lyon::math::Point, + ) -> triangle::Vertex2D { + triangle::Vertex2D { + position: [position.x, position.y], + color: self.0, + } + } +} + +impl lyon::tessellation::FillVertexConstructor + for FillVertex +{ + fn new_vertex( + &mut self, + position: lyon::math::Point, + _attributes: lyon::tessellation::FillAttributes<'_>, + ) -> triangle::Vertex2D { + triangle::Vertex2D { + position: [position.x, position.y], + color: self.0, + } + } +} + +struct StrokeVertex([f32; 4]); + +impl lyon::tessellation::StrokeVertexConstructor + for StrokeVertex +{ + fn new_vertex( + &mut self, + position: lyon::math::Point, + _attributes: lyon::tessellation::StrokeAttributes<'_, '_>, + ) -> triangle::Vertex2D { + triangle::Vertex2D { + position: [position.x, position.y], + color: self.0, + } + } +} diff --git a/graphics/src/widget/canvas/geometry.rs b/graphics/src/widget/canvas/geometry.rs new file mode 100644 index 00000000..4cadee39 --- /dev/null +++ b/graphics/src/widget/canvas/geometry.rs @@ -0,0 +1,34 @@ +use crate::Primitive; + +/// A bunch of shapes that can be drawn. +/// +/// [`Geometry`] can be easily generated with a [`Frame`] or stored in a +/// [`Cache`]. +/// +/// [`Geometry`]: struct.Geometry.html +/// [`Frame`]: struct.Frame.html +/// [`Cache`]: struct.Cache.html +#[derive(Debug, Clone)] +pub struct Geometry(Primitive); + +impl Geometry { + pub(crate) fn from_primitive(primitive: Primitive) -> Self { + Self(primitive) + } + + /// Turns the [`Geometry`] into a [`Primitive`]. + /// + /// This can be useful if you are building a custom widget. + /// + /// [`Geometry`]: struct.Geometry.html + /// [`Primitive`]: ../enum.Primitive.html + pub fn into_primitive(self) -> Primitive { + self.0 + } +} + +impl From for Primitive { + fn from(geometry: Geometry) -> Primitive { + geometry.0 + } +} diff --git a/graphics/src/widget/canvas/path.rs b/graphics/src/widget/canvas/path.rs new file mode 100644 index 00000000..c26bf187 --- /dev/null +++ b/graphics/src/widget/canvas/path.rs @@ -0,0 +1,79 @@ +//! Build different kinds of 2D shapes. +pub mod arc; + +mod builder; + +#[doc(no_inline)] +pub use arc::Arc; +pub use builder::Builder; + +use iced_native::{Point, Size}; + +/// An immutable set of points that may or may not be connected. +/// +/// A single [`Path`] can represent different kinds of 2D shapes! +/// +/// [`Path`]: struct.Path.html +#[derive(Debug, Clone)] +pub struct Path { + raw: lyon::path::Path, +} + +impl Path { + /// Creates a new [`Path`] with the provided closure. + /// + /// Use the [`Builder`] to configure your [`Path`]. + /// + /// [`Path`]: struct.Path.html + /// [`Builder`]: struct.Builder.html + pub fn new(f: impl FnOnce(&mut Builder)) -> Self { + let mut builder = Builder::new(); + + // TODO: Make it pure instead of side-effect-based (?) + f(&mut builder); + + builder.build() + } + + /// Creates a new [`Path`] representing a line segment given its starting + /// and end points. + /// + /// [`Path`]: struct.Path.html + pub fn line(from: Point, to: Point) -> Self { + Self::new(|p| { + p.move_to(from); + p.line_to(to); + }) + } + + /// Creates a new [`Path`] representing a rectangle given its top-left + /// corner coordinate and its `Size`. + /// + /// [`Path`]: struct.Path.html + pub fn rectangle(top_left: Point, size: Size) -> Self { + Self::new(|p| p.rectangle(top_left, size)) + } + + /// Creates a new [`Path`] representing a circle given its center + /// coordinate and its radius. + /// + /// [`Path`]: struct.Path.html + pub fn circle(center: Point, radius: f32) -> Self { + Self::new(|p| p.circle(center, radius)) + } + + #[inline] + pub(crate) fn raw(&self) -> &lyon::path::Path { + &self.raw + } + + #[inline] + pub(crate) fn transformed( + &self, + transform: &lyon::math::Transform, + ) -> Path { + Path { + raw: self.raw.transformed(transform), + } + } +} diff --git a/graphics/src/widget/canvas/path/arc.rs b/graphics/src/widget/canvas/path/arc.rs new file mode 100644 index 00000000..343191f1 --- /dev/null +++ b/graphics/src/widget/canvas/path/arc.rs @@ -0,0 +1,44 @@ +//! Build and draw curves. +use iced_native::{Point, Vector}; + +/// A segment of a differentiable curve. +#[derive(Debug, Clone, Copy)] +pub struct Arc { + /// The center of the arc. + pub center: Point, + /// The radius of the arc. + pub radius: f32, + /// The start of the segment's angle, clockwise rotation. + pub start_angle: f32, + /// The end of the segment's angle, clockwise rotation. + pub end_angle: f32, +} + +/// An elliptical [`Arc`]. +/// +/// [`Arc`]: struct.Arc.html +#[derive(Debug, Clone, Copy)] +pub struct Elliptical { + /// The center of the arc. + pub center: Point, + /// The radii of the arc's ellipse, defining its axes. + pub radii: Vector, + /// The rotation of the arc's ellipse. + pub rotation: f32, + /// The start of the segment's angle, clockwise rotation. + pub start_angle: f32, + /// The end of the segment's angle, clockwise rotation. + pub end_angle: f32, +} + +impl From for Elliptical { + fn from(arc: Arc) -> Elliptical { + Elliptical { + center: arc.center, + radii: Vector::new(arc.radius, arc.radius), + rotation: 0.0, + start_angle: arc.start_angle, + end_angle: arc.end_angle, + } + } +} diff --git a/graphics/src/widget/canvas/path/builder.rs b/graphics/src/widget/canvas/path/builder.rs new file mode 100644 index 00000000..6511fa52 --- /dev/null +++ b/graphics/src/widget/canvas/path/builder.rs @@ -0,0 +1,180 @@ +use crate::canvas::path::{arc, Arc, Path}; + +use iced_native::{Point, Size}; +use lyon::path::builder::{Build, FlatPathBuilder, PathBuilder, SvgBuilder}; + +/// A [`Path`] builder. +/// +/// Once a [`Path`] is built, it can no longer be mutated. +/// +/// [`Path`]: struct.Path.html +#[allow(missing_debug_implementations)] +pub struct Builder { + raw: lyon::path::builder::SvgPathBuilder, +} + +impl Builder { + /// Creates a new [`Builder`]. + /// + /// [`Builder`]: struct.Builder.html + pub fn new() -> Builder { + Builder { + raw: lyon::path::Path::builder().with_svg(), + } + } + + /// Moves the starting point of a new sub-path to the given `Point`. + #[inline] + pub fn move_to(&mut self, point: Point) { + let _ = self.raw.move_to(lyon::math::Point::new(point.x, point.y)); + } + + /// Connects the last point in the [`Path`] to the given `Point` with a + /// straight line. + /// + /// [`Path`]: struct.Path.html + #[inline] + pub fn line_to(&mut self, point: Point) { + let _ = self.raw.line_to(lyon::math::Point::new(point.x, point.y)); + } + + /// Adds an [`Arc`] to the [`Path`] from `start_angle` to `end_angle` in + /// a clockwise direction. + /// + /// [`Arc`]: struct.Arc.html + /// [`Path`]: struct.Path.html + #[inline] + pub fn arc(&mut self, arc: Arc) { + self.ellipse(arc.into()); + } + + /// Adds a circular arc to the [`Path`] with the given control points and + /// radius. + /// + /// The arc is connected to the previous point by a straight line, if + /// necessary. + /// + /// [`Path`]: struct.Path.html + pub fn arc_to(&mut self, a: Point, b: Point, radius: f32) { + use lyon::{math, path}; + + let a = math::Point::new(a.x, a.y); + + if self.raw.current_position() != a { + let _ = self.raw.line_to(a); + } + + let _ = self.raw.arc_to( + math::Vector::new(radius, radius), + math::Angle::radians(0.0), + path::ArcFlags::default(), + math::Point::new(b.x, b.y), + ); + } + + /// Adds an [`Ellipse`] to the [`Path`] using a clockwise direction. + /// + /// [`Ellipse`]: struct.Arc.html + /// [`Path`]: struct.Path.html + pub fn ellipse(&mut self, arc: arc::Elliptical) { + use lyon::{geom, math}; + + let arc = geom::Arc { + center: math::Point::new(arc.center.x, arc.center.y), + radii: math::Vector::new(arc.radii.x, arc.radii.y), + x_rotation: math::Angle::radians(arc.rotation), + start_angle: math::Angle::radians(arc.start_angle), + sweep_angle: math::Angle::radians(arc.end_angle), + }; + + let _ = self.raw.move_to(arc.sample(0.0)); + + arc.for_each_quadratic_bezier(&mut |curve| { + let _ = self.raw.quadratic_bezier_to(curve.ctrl, curve.to); + }); + } + + /// Adds a cubic Bézier curve to the [`Path`] given its two control points + /// and its end point. + /// + /// [`Path`]: struct.Path.html + #[inline] + pub fn bezier_curve_to( + &mut self, + control_a: Point, + control_b: Point, + to: Point, + ) { + use lyon::math; + + let _ = self.raw.cubic_bezier_to( + math::Point::new(control_a.x, control_a.y), + math::Point::new(control_b.x, control_b.y), + math::Point::new(to.x, to.y), + ); + } + + /// Adds a quadratic Bézier curve to the [`Path`] given its control point + /// and its end point. + /// + /// [`Path`]: struct.Path.html + #[inline] + pub fn quadratic_curve_to(&mut self, control: Point, to: Point) { + use lyon::math; + + let _ = self.raw.quadratic_bezier_to( + math::Point::new(control.x, control.y), + math::Point::new(to.x, to.y), + ); + } + + /// Adds a rectangle to the [`Path`] given its top-left corner coordinate + /// and its `Size`. + /// + /// [`Path`]: struct.Path.html + #[inline] + pub fn rectangle(&mut self, top_left: Point, size: Size) { + self.move_to(top_left); + self.line_to(Point::new(top_left.x + size.width, top_left.y)); + self.line_to(Point::new( + top_left.x + size.width, + top_left.y + size.height, + )); + self.line_to(Point::new(top_left.x, top_left.y + size.height)); + self.close(); + } + + /// Adds a circle to the [`Path`] given its center coordinate and its + /// radius. + /// + /// [`Path`]: struct.Path.html + #[inline] + pub fn circle(&mut self, center: Point, radius: f32) { + self.arc(Arc { + center, + radius, + start_angle: 0.0, + end_angle: 2.0 * std::f32::consts::PI, + }); + } + + /// Closes the current sub-path in the [`Path`] with a straight line to + /// the starting point. + /// + /// [`Path`]: struct.Path.html + #[inline] + pub fn close(&mut self) { + self.raw.close() + } + + /// Builds the [`Path`] of this [`Builder`]. + /// + /// [`Path`]: struct.Path.html + /// [`Builder`]: struct.Builder.html + #[inline] + pub fn build(self) -> Path { + Path { + raw: self.raw.build(), + } + } +} diff --git a/graphics/src/widget/canvas/program.rs b/graphics/src/widget/canvas/program.rs new file mode 100644 index 00000000..725d9d72 --- /dev/null +++ b/graphics/src/widget/canvas/program.rs @@ -0,0 +1,85 @@ +use crate::canvas::{Cursor, Event, Geometry}; +use iced_native::{mouse, Rectangle}; + +/// The state and logic of a [`Canvas`]. +/// +/// A [`Program`] can mutate internal state and produce messages for an +/// application. +/// +/// [`Canvas`]: struct.Canvas.html +/// [`Program`]: trait.Program.html +pub trait Program { + /// Updates the state of the [`Program`]. + /// + /// When a [`Program`] is used in a [`Canvas`], the runtime will call this + /// method for each [`Event`]. + /// + /// This method can optionally return a `Message` to notify an application + /// of any meaningful interactions. + /// + /// By default, this method does and returns nothing. + /// + /// [`Program`]: trait.Program.html + /// [`Canvas`]: struct.Canvas.html + /// [`Event`]: enum.Event.html + fn update( + &mut self, + _event: Event, + _bounds: Rectangle, + _cursor: Cursor, + ) -> Option { + None + } + + /// Draws the state of the [`Program`], producing a bunch of [`Geometry`]. + /// + /// [`Geometry`] can be easily generated with a [`Frame`] or stored in a + /// [`Cache`]. + /// + /// [`Program`]: trait.Program.html + /// [`Geometry`]: struct.Geometry.html + /// [`Frame`]: struct.Frame.html + /// [`Cache`]: struct.Cache.html + fn draw(&self, bounds: Rectangle, cursor: Cursor) -> Vec; + + /// Returns the current mouse interaction of the [`Program`]. + /// + /// The interaction returned will be in effect even if the cursor position + /// is out of bounds of the program's [`Canvas`]. + /// + /// [`Program`]: trait.Program.html + /// [`Canvas`]: struct.Canvas.html + fn mouse_interaction( + &self, + _bounds: Rectangle, + _cursor: Cursor, + ) -> mouse::Interaction { + mouse::Interaction::default() + } +} + +impl Program for &mut T +where + T: Program, +{ + fn update( + &mut self, + event: Event, + bounds: Rectangle, + cursor: Cursor, + ) -> Option { + T::update(self, event, bounds, cursor) + } + + fn draw(&self, bounds: Rectangle, cursor: Cursor) -> Vec { + T::draw(self, bounds, cursor) + } + + fn mouse_interaction( + &self, + bounds: Rectangle, + cursor: Cursor, + ) -> mouse::Interaction { + T::mouse_interaction(self, bounds, cursor) + } +} diff --git a/graphics/src/widget/canvas/stroke.rs b/graphics/src/widget/canvas/stroke.rs new file mode 100644 index 00000000..5b6fc56a --- /dev/null +++ b/graphics/src/widget/canvas/stroke.rs @@ -0,0 +1,115 @@ +use iced_native::Color; + +/// The style of a stroke. +#[derive(Debug, Clone, Copy)] +pub struct Stroke { + /// The color of the stroke. + pub color: Color, + /// The distance between the two edges of the stroke. + pub width: f32, + /// The shape to be used at the end of open subpaths when they are stroked. + pub line_cap: LineCap, + /// The shape to be used at the corners of paths or basic shapes when they + /// are stroked. + pub line_join: LineJoin, +} + +impl Stroke { + /// Sets the color of the [`Stroke`]. + /// + /// [`Stroke`]: struct.Stroke.html + pub fn with_color(self, color: Color) -> Stroke { + Stroke { color, ..self } + } + + /// Sets the width of the [`Stroke`]. + /// + /// [`Stroke`]: struct.Stroke.html + pub fn with_width(self, width: f32) -> Stroke { + Stroke { width, ..self } + } + + /// Sets the [`LineCap`] of the [`Stroke`]. + /// + /// [`LineCap`]: enum.LineCap.html + /// [`Stroke`]: struct.Stroke.html + pub fn with_line_cap(self, line_cap: LineCap) -> Stroke { + Stroke { line_cap, ..self } + } + + /// Sets the [`LineJoin`] of the [`Stroke`]. + /// + /// [`LineJoin`]: enum.LineJoin.html + /// [`Stroke`]: struct.Stroke.html + pub fn with_line_join(self, line_join: LineJoin) -> Stroke { + Stroke { line_join, ..self } + } +} + +impl Default for Stroke { + fn default() -> Stroke { + Stroke { + color: Color::BLACK, + width: 1.0, + line_cap: LineCap::default(), + line_join: LineJoin::default(), + } + } +} + +/// The shape used at the end of open subpaths when they are stroked. +#[derive(Debug, Clone, Copy)] +pub enum LineCap { + /// The stroke for each sub-path does not extend beyond its two endpoints. + Butt, + /// At the end of each sub-path, the shape representing the stroke will be + /// extended by a square. + Square, + /// At the end of each sub-path, the shape representing the stroke will be + /// extended by a semicircle. + Round, +} + +impl Default for LineCap { + fn default() -> LineCap { + LineCap::Butt + } +} + +impl From for lyon::tessellation::LineCap { + fn from(line_cap: LineCap) -> lyon::tessellation::LineCap { + match line_cap { + LineCap::Butt => lyon::tessellation::LineCap::Butt, + LineCap::Square => lyon::tessellation::LineCap::Square, + LineCap::Round => lyon::tessellation::LineCap::Round, + } + } +} + +/// The shape used at the corners of paths or basic shapes when they are +/// stroked. +#[derive(Debug, Clone, Copy)] +pub enum LineJoin { + /// A sharp corner. + Miter, + /// A round corner. + Round, + /// A bevelled corner. + Bevel, +} + +impl Default for LineJoin { + fn default() -> LineJoin { + LineJoin::Miter + } +} + +impl From for lyon::tessellation::LineJoin { + fn from(line_join: LineJoin) -> lyon::tessellation::LineJoin { + match line_join { + LineJoin::Miter => lyon::tessellation::LineJoin::Miter, + LineJoin::Round => lyon::tessellation::LineJoin::Round, + LineJoin::Bevel => lyon::tessellation::LineJoin::Bevel, + } + } +} diff --git a/graphics/src/widget/canvas/text.rs b/graphics/src/widget/canvas/text.rs new file mode 100644 index 00000000..c4cae30e --- /dev/null +++ b/graphics/src/widget/canvas/text.rs @@ -0,0 +1,49 @@ +use iced_native::{Color, Font, HorizontalAlignment, Point, VerticalAlignment}; + +/// A bunch of text that can be drawn to a canvas +#[derive(Debug, Clone)] +pub struct Text { + /// The contents of the text + pub content: String, + /// The position where to begin drawing the text (top-left corner coordinates) + pub position: Point, + /// The color of the text + pub color: Color, + /// The size of the text + pub size: f32, + /// The font of the text + pub font: Font, + /// The horizontal alignment of the text + pub horizontal_alignment: HorizontalAlignment, + /// The vertical alignment of the text + pub vertical_alignment: VerticalAlignment, +} + +impl Default for Text { + fn default() -> Text { + Text { + content: String::new(), + position: Point::ORIGIN, + color: Color::BLACK, + size: 16.0, + font: Font::Default, + horizontal_alignment: HorizontalAlignment::Left, + vertical_alignment: VerticalAlignment::Top, + } + } +} + +impl From for Text { + fn from(content: String) -> Text { + Text { + content, + ..Default::default() + } + } +} + +impl From<&str> for Text { + fn from(content: &str) -> Text { + String::from(content).into() + } +} diff --git a/graphics/src/widget/checkbox.rs b/graphics/src/widget/checkbox.rs new file mode 100644 index 00000000..3c8b58de --- /dev/null +++ b/graphics/src/widget/checkbox.rs @@ -0,0 +1,10 @@ +//! Show toggle controls using checkboxes. +use crate::Renderer; + +pub use iced_style::checkbox::{Style, StyleSheet}; + +/// A box that can be checked. +/// +/// This is an alias of an `iced_native` checkbox with an `iced_wgpu::Renderer`. +pub type Checkbox = + iced_native::Checkbox>; diff --git a/graphics/src/widget/container.rs b/graphics/src/widget/container.rs new file mode 100644 index 00000000..c4c4e5ba --- /dev/null +++ b/graphics/src/widget/container.rs @@ -0,0 +1,11 @@ +//! Decorate content and apply alignment. +use crate::Renderer; + +pub use iced_style::container::{Style, StyleSheet}; + +/// An element decorating some content. +/// +/// This is an alias of an `iced_native` container with a default +/// `Renderer`. +pub type Container<'a, Message, Backend> = + iced_native::Container<'a, Message, Renderer>; diff --git a/graphics/src/widget/pane_grid.rs b/graphics/src/widget/pane_grid.rs new file mode 100644 index 00000000..34be8ee7 --- /dev/null +++ b/graphics/src/widget/pane_grid.rs @@ -0,0 +1,25 @@ +//! Let your users split regions of your application and organize layout dynamically. +//! +//! [![Pane grid - Iced](https://thumbs.gfycat.com/MixedFlatJellyfish-small.gif)](https://gfycat.com/mixedflatjellyfish) +//! +//! # Example +//! The [`pane_grid` example] showcases how to use a [`PaneGrid`] with resizing, +//! drag and drop, and hotkey support. +//! +//! [`pane_grid` example]: https://github.com/hecrj/iced/tree/0.1/examples/pane_grid +//! [`PaneGrid`]: type.PaneGrid.html +use crate::Renderer; + +pub use iced_native::pane_grid::{ + Axis, Direction, DragEvent, Focus, KeyPressEvent, Pane, ResizeEvent, Split, + State, +}; + +/// A collection of panes distributed using either vertical or horizontal splits +/// to completely fill the space available. +/// +/// [![Pane grid - Iced](https://thumbs.gfycat.com/MixedFlatJellyfish-small.gif)](https://gfycat.com/mixedflatjellyfish) +/// +/// This is an alias of an `iced_native` pane grid with an `iced_wgpu::Renderer`. +pub type PaneGrid<'a, Message, Backend> = + iced_native::PaneGrid<'a, Message, Renderer>; diff --git a/graphics/src/widget/progress_bar.rs b/graphics/src/widget/progress_bar.rs new file mode 100644 index 00000000..c1e5702e --- /dev/null +++ b/graphics/src/widget/progress_bar.rs @@ -0,0 +1,15 @@ +//! Allow your users to visually track the progress of a computation. +//! +//! A [`ProgressBar`] has a range of possible values and a current value, +//! as well as a length, height and style. +//! +//! [`ProgressBar`]: type.ProgressBar.html +use crate::Renderer; + +pub use iced_style::progress_bar::{Style, StyleSheet}; + +/// A bar that displays progress. +/// +/// This is an alias of an `iced_native` progress bar with an +/// `iced_wgpu::Renderer`. +pub type ProgressBar = iced_native::ProgressBar>; diff --git a/graphics/src/widget/radio.rs b/graphics/src/widget/radio.rs new file mode 100644 index 00000000..c621a26a --- /dev/null +++ b/graphics/src/widget/radio.rs @@ -0,0 +1,11 @@ +//! Create choices using radio buttons. +use crate::Renderer; + +pub use iced_style::radio::{Style, StyleSheet}; + +/// A circular button representing a choice. +/// +/// This is an alias of an `iced_native` radio button with an +/// `iced_wgpu::Renderer`. +pub type Radio = + iced_native::Radio>; diff --git a/graphics/src/widget/scrollable.rs b/graphics/src/widget/scrollable.rs new file mode 100644 index 00000000..61eae587 --- /dev/null +++ b/graphics/src/widget/scrollable.rs @@ -0,0 +1,13 @@ +//! Navigate an endless amount of content with a scrollbar. +use crate::Renderer; + +pub use iced_native::scrollable::State; +pub use iced_style::scrollable::{Scrollbar, Scroller, StyleSheet}; + +/// A widget that can vertically display an infinite amount of content +/// with a scrollbar. +/// +/// This is an alias of an `iced_native` scrollable with a default +/// `Renderer`. +pub type Scrollable<'a, Message, Backend> = + iced_native::Scrollable<'a, Message, Renderer>; diff --git a/graphics/src/widget/slider.rs b/graphics/src/widget/slider.rs new file mode 100644 index 00000000..035c7c41 --- /dev/null +++ b/graphics/src/widget/slider.rs @@ -0,0 +1,17 @@ +//! Display an interactive selector of a single value from a range of values. +//! +//! A [`Slider`] has some local [`State`]. +//! +//! [`Slider`]: struct.Slider.html +//! [`State`]: struct.State.html +use crate::Renderer; + +pub use iced_native::slider::State; +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. +/// +/// This is an alias of an `iced_native` slider with an `iced_wgpu::Renderer`. +pub type Slider<'a, Message, Backend> = + iced_native::Slider<'a, Message, Renderer>; diff --git a/graphics/src/widget/text.rs b/graphics/src/widget/text.rs new file mode 100644 index 00000000..ec0349f9 --- /dev/null +++ b/graphics/src/widget/text.rs @@ -0,0 +1,7 @@ +//! Write some text for your users to read. +use crate::Renderer; + +/// A paragraph of text. +/// +/// This is an alias of an `iced_native` text with an `iced_wgpu::Renderer`. +pub type Text = iced_native::Text>; diff --git a/graphics/src/widget/text_input.rs b/graphics/src/widget/text_input.rs new file mode 100644 index 00000000..8015626b --- /dev/null +++ b/graphics/src/widget/text_input.rs @@ -0,0 +1,16 @@ +//! Display fields that can be filled with text. +//! +//! A [`TextInput`] has some local [`State`]. +//! +//! [`TextInput`]: struct.TextInput.html +//! [`State`]: struct.State.html +use crate::Renderer; + +pub use iced_native::text_input::State; +pub use iced_style::text_input::{Style, StyleSheet}; + +/// A field that can be filled with text. +/// +/// This is an alias of an `iced_native` text input with an `iced_wgpu::Renderer`. +pub type TextInput<'a, Message, Backend> = + iced_native::TextInput<'a, Message, Renderer>; -- cgit From 750a441a8c7c76b240db238283e9cbdab8d6932d Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 19 May 2020 19:55:05 +0200 Subject: Move `Transformation` to `iced_graphics` --- graphics/src/lib.rs | 2 ++ graphics/src/transformation.rs | 54 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 graphics/src/transformation.rs (limited to 'graphics/src') diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs index 596f72a4..6e5105d2 100644 --- a/graphics/src/lib.rs +++ b/graphics/src/lib.rs @@ -1,6 +1,7 @@ mod defaults; mod primitive; mod renderer; +mod transformation; mod widget; pub mod backend; @@ -13,3 +14,4 @@ pub use backend::Backend; pub use defaults::Defaults; pub use primitive::Primitive; pub use renderer::Renderer; +pub use transformation::Transformation; diff --git a/graphics/src/transformation.rs b/graphics/src/transformation.rs new file mode 100644 index 00000000..ff3b1d00 --- /dev/null +++ b/graphics/src/transformation.rs @@ -0,0 +1,54 @@ +use glam::{Mat4, Vec3, Vec4}; +use std::ops::Mul; + +/// A 2D transformation matrix. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Transformation(Mat4); + +impl Transformation { + /// Get the identity transformation. + pub fn identity() -> Transformation { + Transformation(Mat4::identity()) + } + + /// Creates an orthographic projection. + #[rustfmt::skip] + pub fn orthographic(width: u32, height: u32) -> Transformation { + Transformation(Mat4::from_cols( + Vec4::new(2.0 / width as f32, 0.0, 0.0, 0.0), + Vec4::new(0.0, -2.0 / height as f32, 0.0, 0.0), + Vec4::new(0.0, 0.0, -1.0, 0.0), + Vec4::new(-1.0, 1.0, 0.0, 1.0) + )) + } + + /// Creates a translate transformation. + pub fn translate(x: f32, y: f32) -> Transformation { + Transformation(Mat4::from_translation(Vec3::new(x, y, 0.0))) + } + + /// Creates a scale transformation. + pub fn scale(x: f32, y: f32) -> Transformation { + Transformation(Mat4::from_scale(Vec3::new(x, y, 1.0))) + } +} + +impl Mul for Transformation { + type Output = Self; + + fn mul(self, rhs: Self) -> Self { + Transformation(self.0 * rhs.0) + } +} + +impl AsRef<[f32; 16]> for Transformation { + fn as_ref(&self) -> &[f32; 16] { + self.0.as_ref() + } +} + +impl From for [f32; 16] { + fn from(t: Transformation) -> [f32; 16] { + *t.as_ref() + } +} -- cgit From a0ac09122a68d9be7d11e5cc765f52cb526ae913 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 19 May 2020 19:57:42 +0200 Subject: Move `Viewport` to `iced_graphics` --- graphics/src/lib.rs | 2 ++ graphics/src/viewport.rs | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 graphics/src/viewport.rs (limited to 'graphics/src') diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs index 6e5105d2..bdaefb41 100644 --- a/graphics/src/lib.rs +++ b/graphics/src/lib.rs @@ -2,6 +2,7 @@ mod defaults; mod primitive; mod renderer; mod transformation; +mod viewport; mod widget; pub mod backend; @@ -15,3 +16,4 @@ pub use defaults::Defaults; pub use primitive::Primitive; pub use renderer::Renderer; pub use transformation::Transformation; +pub use viewport::Viewport; diff --git a/graphics/src/viewport.rs b/graphics/src/viewport.rs new file mode 100644 index 00000000..48e5a249 --- /dev/null +++ b/graphics/src/viewport.rs @@ -0,0 +1,33 @@ +use crate::Transformation; + +/// A viewing region for displaying computer graphics. +#[derive(Debug)] +pub struct Viewport { + width: u32, + height: u32, + transformation: Transformation, +} + +impl Viewport { + /// Creates a new [`Viewport`] with the given dimensions. + pub fn new(width: u32, height: u32) -> Viewport { + Viewport { + width, + height, + transformation: Transformation::orthographic(width, height), + } + } + + pub fn height(&self) -> u32 { + self.height + } + + /// Returns the dimensions of the [`Viewport`]. + pub fn dimensions(&self) -> (u32, u32) { + (self.width, self.height) + } + + pub fn transformation(&self) -> Transformation { + self.transformation + } +} -- cgit From e0c4f1a08e756f11c30a99cd739fe78267e5040b Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 19 May 2020 20:20:51 +0200 Subject: Move `font::Source` to `iced_graphics` --- graphics/src/font.rs | 10 ++++++++++ graphics/src/font/source.rs | 35 +++++++++++++++++++++++++++++++++++ graphics/src/lib.rs | 1 + 3 files changed, 46 insertions(+) create mode 100644 graphics/src/font.rs create mode 100644 graphics/src/font/source.rs (limited to 'graphics/src') diff --git a/graphics/src/font.rs b/graphics/src/font.rs new file mode 100644 index 00000000..3890beba --- /dev/null +++ b/graphics/src/font.rs @@ -0,0 +1,10 @@ +#[cfg(feature = "font-source")] +mod source; + +#[cfg(feature = "font-source")] +pub use source::Source; + +#[cfg(feature = "font-source")] +pub use font_kit::{ + error::SelectionError as LoadError, family_name::FamilyName as Family, +}; diff --git a/graphics/src/font/source.rs b/graphics/src/font/source.rs new file mode 100644 index 00000000..6855aa93 --- /dev/null +++ b/graphics/src/font/source.rs @@ -0,0 +1,35 @@ +use crate::font::{Family, LoadError}; + +pub struct Source { + raw: font_kit::source::SystemSource, +} + +impl Source { + pub fn new() -> Self { + Source { + raw: font_kit::source::SystemSource::new(), + } + } + + pub fn load(&self, families: &[Family]) -> Result, LoadError> { + let font = self.raw.select_best_match( + families, + &font_kit::properties::Properties::default(), + )?; + + match font { + font_kit::handle::Handle::Path { path, .. } => { + use std::io::Read; + + let mut buf = Vec::new(); + let mut reader = std::fs::File::open(path).expect("Read font"); + let _ = reader.read_to_end(&mut buf); + + Ok(buf) + } + font_kit::handle::Handle::Memory { bytes, .. } => { + Ok(bytes.as_ref().clone()) + } + } + } +} diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs index bdaefb41..152dc7b0 100644 --- a/graphics/src/lib.rs +++ b/graphics/src/lib.rs @@ -6,6 +6,7 @@ mod viewport; mod widget; pub mod backend; +pub mod font; pub mod triangle; #[doc(no_inline)] -- cgit From f0480854a9cd76f443848dbfa14256089b56abfe Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 19 May 2020 20:30:46 +0200 Subject: Move built-in fonts to `iced_graphics` --- graphics/src/font.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) (limited to 'graphics/src') diff --git a/graphics/src/font.rs b/graphics/src/font.rs index 3890beba..a490e609 100644 --- a/graphics/src/font.rs +++ b/graphics/src/font.rs @@ -8,3 +8,15 @@ pub use source::Source; pub use font_kit::{ error::SelectionError as LoadError, family_name::FamilyName as Family, }; + +#[cfg(feature = "font-fallback")] +pub const FALLBACK: &[u8] = include_bytes!("../fonts/Lato-Regular.ttf"); + +#[cfg(feature = "font-icons")] +pub const ICONS: iced_native::Font = iced_native::Font::External { + name: "iced_wgpu icons", + bytes: include_bytes!("../fonts/Icons.ttf"), +}; + +#[cfg(feature = "font-icons")] +pub const CHECKMARK_ICON: char = '\u{F00C}'; -- cgit From c2e0c52ce031ffe1c300b3cfa362b0e445ac5afd Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 19 May 2020 20:34:17 +0200 Subject: Move `Antialiasing` to `iced_graphics` --- graphics/src/antialiasing.rs | 23 +++++++++++++++++++++++ graphics/src/lib.rs | 2 ++ 2 files changed, 25 insertions(+) create mode 100644 graphics/src/antialiasing.rs (limited to 'graphics/src') diff --git a/graphics/src/antialiasing.rs b/graphics/src/antialiasing.rs new file mode 100644 index 00000000..f92b3692 --- /dev/null +++ b/graphics/src/antialiasing.rs @@ -0,0 +1,23 @@ +/// An antialiasing strategy. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Antialiasing { + /// Multisample AA with 2 samples + MSAAx2, + /// Multisample AA with 4 samples + MSAAx4, + /// Multisample AA with 8 samples + MSAAx8, + /// Multisample AA with 16 samples + MSAAx16, +} + +impl Antialiasing { + pub fn sample_count(self) -> u32 { + match self { + Antialiasing::MSAAx2 => 2, + Antialiasing::MSAAx4 => 4, + Antialiasing::MSAAx8 => 8, + Antialiasing::MSAAx16 => 16, + } + } +} diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs index 152dc7b0..5dc4040f 100644 --- a/graphics/src/lib.rs +++ b/graphics/src/lib.rs @@ -1,3 +1,4 @@ +mod antialiasing; mod defaults; mod primitive; mod renderer; @@ -12,6 +13,7 @@ pub mod triangle; #[doc(no_inline)] pub use widget::*; +pub use antialiasing::Antialiasing; pub use backend::Backend; pub use defaults::Defaults; pub use primitive::Primitive; -- cgit From e6180912488db4d59fbffcb46c5930282306cb92 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 19 May 2020 21:00:40 +0200 Subject: Merge unnecessary split widget modules --- graphics/src/renderer.rs | 2 - graphics/src/renderer/widget.rs | 15 -- graphics/src/renderer/widget/button.rs | 98 ---------- graphics/src/renderer/widget/checkbox.rs | 68 ------- graphics/src/renderer/widget/column.rs | 39 ---- graphics/src/renderer/widget/container.rs | 53 ------ graphics/src/renderer/widget/image.rs | 28 --- graphics/src/renderer/widget/pane_grid.rs | 94 ---------- graphics/src/renderer/widget/progress_bar.rs | 58 ------ graphics/src/renderer/widget/radio.rs | 66 ------- graphics/src/renderer/widget/row.rs | 39 ---- graphics/src/renderer/widget/scrollable.rs | 130 ------------- graphics/src/renderer/widget/slider.rs | 109 ----------- graphics/src/renderer/widget/space.rs | 13 -- graphics/src/renderer/widget/svg.rs | 26 --- graphics/src/renderer/widget/text.rs | 65 ------- graphics/src/renderer/widget/text_input.rs | 267 --------------------------- graphics/src/widget.rs | 10 + graphics/src/widget/button.rs | 99 +++++++++- graphics/src/widget/checkbox.rs | 68 ++++++- graphics/src/widget/column.rs | 42 +++++ graphics/src/widget/container.rs | 54 +++++- graphics/src/widget/image.rs | 30 +++ graphics/src/widget/pane_grid.rs | 95 +++++++++- graphics/src/widget/progress_bar.rs | 60 +++++- graphics/src/widget/radio.rs | 69 ++++++- graphics/src/widget/row.rs | 42 +++++ graphics/src/widget/scrollable.rs | 131 ++++++++++++- graphics/src/widget/slider.rs | 109 ++++++++++- graphics/src/widget/space.rs | 15 ++ graphics/src/widget/svg.rs | 28 +++ graphics/src/widget/text.rs | 67 ++++++- graphics/src/widget/text_input.rs | 266 +++++++++++++++++++++++++- 33 files changed, 1175 insertions(+), 1180 deletions(-) delete mode 100644 graphics/src/renderer/widget.rs delete mode 100644 graphics/src/renderer/widget/button.rs delete mode 100644 graphics/src/renderer/widget/checkbox.rs delete mode 100644 graphics/src/renderer/widget/column.rs delete mode 100644 graphics/src/renderer/widget/container.rs delete mode 100644 graphics/src/renderer/widget/image.rs delete mode 100644 graphics/src/renderer/widget/pane_grid.rs delete mode 100644 graphics/src/renderer/widget/progress_bar.rs delete mode 100644 graphics/src/renderer/widget/radio.rs delete mode 100644 graphics/src/renderer/widget/row.rs delete mode 100644 graphics/src/renderer/widget/scrollable.rs delete mode 100644 graphics/src/renderer/widget/slider.rs delete mode 100644 graphics/src/renderer/widget/space.rs delete mode 100644 graphics/src/renderer/widget/svg.rs delete mode 100644 graphics/src/renderer/widget/text.rs delete mode 100644 graphics/src/renderer/widget/text_input.rs create mode 100644 graphics/src/widget/column.rs create mode 100644 graphics/src/widget/image.rs create mode 100644 graphics/src/widget/row.rs create mode 100644 graphics/src/widget/space.rs create mode 100644 graphics/src/widget/svg.rs (limited to 'graphics/src') diff --git a/graphics/src/renderer.rs b/graphics/src/renderer.rs index 836ed58d..f16e04b1 100644 --- a/graphics/src/renderer.rs +++ b/graphics/src/renderer.rs @@ -1,5 +1,3 @@ -mod widget; - use crate::{Backend, Defaults, Primitive}; use iced_native::layout::{self, Layout}; use iced_native::mouse; diff --git a/graphics/src/renderer/widget.rs b/graphics/src/renderer/widget.rs deleted file mode 100644 index b652fdcf..00000000 --- a/graphics/src/renderer/widget.rs +++ /dev/null @@ -1,15 +0,0 @@ -mod button; -mod checkbox; -mod column; -mod container; -mod image; -mod pane_grid; -mod progress_bar; -mod radio; -mod row; -mod scrollable; -mod slider; -mod space; -mod svg; -mod text; -mod text_input; diff --git a/graphics/src/renderer/widget/button.rs b/graphics/src/renderer/widget/button.rs deleted file mode 100644 index 6ee60df4..00000000 --- a/graphics/src/renderer/widget/button.rs +++ /dev/null @@ -1,98 +0,0 @@ -use crate::{ - button::StyleSheet, defaults, Backend, Defaults, Primitive, Renderer, -}; -use iced_native::{ - mouse, Background, Color, Element, Layout, Point, Rectangle, Vector, -}; - -impl iced_native::button::Renderer for Renderer -where - B: Backend, -{ - const DEFAULT_PADDING: u16 = 5; - - type Style = Box; - - fn draw( - &mut self, - _defaults: &Defaults, - bounds: Rectangle, - cursor_position: Point, - is_disabled: bool, - is_pressed: bool, - style: &Box, - content: &Element<'_, Message, Self>, - content_layout: Layout<'_>, - ) -> Self::Output { - let is_mouse_over = bounds.contains(cursor_position); - - let styling = if is_disabled { - style.disabled() - } else if is_mouse_over { - if is_pressed { - style.pressed() - } else { - style.hovered() - } - } else { - style.active() - }; - - let (content, _) = content.draw( - self, - &Defaults { - text: defaults::Text { - color: styling.text_color, - }, - }, - content_layout, - cursor_position, - ); - - ( - if styling.background.is_some() || styling.border_width > 0 { - let background = Primitive::Quad { - bounds, - background: styling - .background - .unwrap_or(Background::Color(Color::TRANSPARENT)), - border_radius: styling.border_radius, - border_width: styling.border_width, - border_color: styling.border_color, - }; - - if styling.shadow_offset == Vector::default() { - Primitive::Group { - primitives: vec![background, content], - } - } else { - // TODO: Implement proper shadow support - let shadow = Primitive::Quad { - bounds: Rectangle { - x: bounds.x + styling.shadow_offset.x, - y: bounds.y + styling.shadow_offset.y, - ..bounds - }, - background: Background::Color( - [0.0, 0.0, 0.0, 0.5].into(), - ), - border_radius: styling.border_radius, - border_width: 0, - border_color: Color::TRANSPARENT, - }; - - Primitive::Group { - primitives: vec![shadow, background, content], - } - } - } else { - content - }, - if is_mouse_over { - mouse::Interaction::Pointer - } else { - mouse::Interaction::default() - }, - ) - } -} diff --git a/graphics/src/renderer/widget/checkbox.rs b/graphics/src/renderer/widget/checkbox.rs deleted file mode 100644 index 75168629..00000000 --- a/graphics/src/renderer/widget/checkbox.rs +++ /dev/null @@ -1,68 +0,0 @@ -use crate::backend::{self, Backend}; -use crate::checkbox::StyleSheet; -use crate::{Primitive, Renderer}; -use iced_native::{ - checkbox, mouse, HorizontalAlignment, Rectangle, VerticalAlignment, -}; - -impl checkbox::Renderer for Renderer -where - B: Backend + backend::Text, -{ - type Style = Box; - - 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_sheet: &Self::Style, - ) -> Self::Output { - let style = if is_mouse_over { - style_sheet.hovered(is_checked) - } else { - style_sheet.active(is_checked) - }; - - let checkbox = Primitive::Quad { - bounds, - background: style.background, - border_radius: style.border_radius, - border_width: style.border_width, - border_color: style.border_color, - }; - - ( - Primitive::Group { - primitives: if is_checked { - let check = Primitive::Text { - content: B::CHECKMARK_ICON.to_string(), - font: B::ICON_FONT, - size: bounds.height * 0.7, - bounds: Rectangle { - x: bounds.center_x(), - y: bounds.center_y(), - ..bounds - }, - color: style.checkmark_color, - horizontal_alignment: HorizontalAlignment::Center, - vertical_alignment: VerticalAlignment::Center, - }; - - vec![checkbox, check, label] - } else { - vec![checkbox, label] - }, - }, - if is_mouse_over { - mouse::Interaction::Pointer - } else { - mouse::Interaction::default() - }, - ) - } -} diff --git a/graphics/src/renderer/widget/column.rs b/graphics/src/renderer/widget/column.rs deleted file mode 100644 index b70d2338..00000000 --- a/graphics/src/renderer/widget/column.rs +++ /dev/null @@ -1,39 +0,0 @@ -use crate::{Backend, Primitive, Renderer}; -use iced_native::column; -use iced_native::mouse; -use iced_native::{Element, Layout, Point}; - -impl column::Renderer for Renderer -where - B: Backend, -{ - fn draw( - &mut self, - defaults: &Self::Defaults, - content: &[Element<'_, Message, Self>], - layout: Layout<'_>, - cursor_position: Point, - ) -> Self::Output { - let mut mouse_interaction = mouse::Interaction::default(); - - ( - Primitive::Group { - primitives: content - .iter() - .zip(layout.children()) - .map(|(child, layout)| { - let (primitive, new_mouse_interaction) = - child.draw(self, defaults, layout, cursor_position); - - if new_mouse_interaction > mouse_interaction { - mouse_interaction = new_mouse_interaction; - } - - primitive - }) - .collect(), - }, - mouse_interaction, - ) - } -} diff --git a/graphics/src/renderer/widget/container.rs b/graphics/src/renderer/widget/container.rs deleted file mode 100644 index a1f6a211..00000000 --- a/graphics/src/renderer/widget/container.rs +++ /dev/null @@ -1,53 +0,0 @@ -use crate::container; -use crate::defaults::{self, Defaults}; -use crate::{Backend, Primitive, Renderer}; -use iced_native::{Background, Color, Element, Layout, Point, Rectangle}; - -impl iced_native::container::Renderer for Renderer -where - B: Backend, -{ - type Style = Box; - - fn draw( - &mut self, - defaults: &Defaults, - bounds: Rectangle, - cursor_position: Point, - style_sheet: &Self::Style, - content: &Element<'_, Message, Self>, - content_layout: Layout<'_>, - ) -> Self::Output { - let style = style_sheet.style(); - - let defaults = Defaults { - text: defaults::Text { - color: style.text_color.unwrap_or(defaults.text.color), - }, - }; - - let (content, mouse_interaction) = - content.draw(self, &defaults, content_layout, cursor_position); - - if style.background.is_some() || style.border_width > 0 { - let quad = Primitive::Quad { - bounds, - background: style - .background - .unwrap_or(Background::Color(Color::TRANSPARENT)), - border_radius: style.border_radius, - border_width: style.border_width, - border_color: style.border_color, - }; - - ( - Primitive::Group { - primitives: vec![quad, content], - }, - mouse_interaction, - ) - } else { - (content, mouse_interaction) - } - } -} diff --git a/graphics/src/renderer/widget/image.rs b/graphics/src/renderer/widget/image.rs deleted file mode 100644 index 3092237f..00000000 --- a/graphics/src/renderer/widget/image.rs +++ /dev/null @@ -1,28 +0,0 @@ -use crate::backend::{self, Backend}; -use crate::{Primitive, Renderer}; -use iced_native::image; -use iced_native::mouse; -use iced_native::Layout; - -impl image::Renderer for Renderer -where - B: Backend + backend::Image, -{ - fn dimensions(&self, handle: &image::Handle) -> (u32, u32) { - self.backend().dimensions(handle) - } - - fn draw( - &mut self, - handle: image::Handle, - layout: Layout<'_>, - ) -> Self::Output { - ( - Primitive::Image { - handle, - bounds: layout.bounds(), - }, - mouse::Interaction::default(), - ) - } -} diff --git a/graphics/src/renderer/widget/pane_grid.rs b/graphics/src/renderer/widget/pane_grid.rs deleted file mode 100644 index 3fa171f5..00000000 --- a/graphics/src/renderer/widget/pane_grid.rs +++ /dev/null @@ -1,94 +0,0 @@ -use crate::{Backend, Primitive, Renderer}; -use iced_native::mouse; -use iced_native::pane_grid::{self, Axis, Pane}; -use iced_native::{Element, Layout, Point, Rectangle, Vector}; - -impl pane_grid::Renderer for Renderer -where - B: Backend, -{ - fn draw( - &mut self, - defaults: &Self::Defaults, - content: &[(Pane, Element<'_, Message, Self>)], - dragging: Option, - resizing: Option, - layout: Layout<'_>, - cursor_position: Point, - ) -> Self::Output { - let pane_cursor_position = if dragging.is_some() { - // TODO: Remove once cursor availability is encoded in the type - // system - Point::new(-1.0, -1.0) - } else { - cursor_position - }; - - let mut mouse_interaction = mouse::Interaction::default(); - let mut dragged_pane = None; - - let mut panes: Vec<_> = content - .iter() - .zip(layout.children()) - .enumerate() - .map(|(i, ((id, pane), layout))| { - let (primitive, new_mouse_interaction) = - pane.draw(self, defaults, layout, pane_cursor_position); - - if new_mouse_interaction > mouse_interaction { - mouse_interaction = new_mouse_interaction; - } - - if Some(*id) == dragging { - dragged_pane = Some((i, layout)); - } - - primitive - }) - .collect(); - - let primitives = if let Some((index, layout)) = dragged_pane { - let pane = panes.remove(index); - let bounds = layout.bounds(); - - // TODO: Fix once proper layering is implemented. - // This is a pretty hacky way to achieve layering. - let clip = Primitive::Clip { - bounds: Rectangle { - x: cursor_position.x - bounds.width / 2.0, - y: cursor_position.y - bounds.height / 2.0, - width: bounds.width + 0.5, - height: bounds.height + 0.5, - }, - offset: Vector::new(0, 0), - content: Box::new(Primitive::Translate { - translation: Vector::new( - cursor_position.x - bounds.x - bounds.width / 2.0, - cursor_position.y - bounds.y - bounds.height / 2.0, - ), - content: Box::new(pane), - }), - }; - - panes.push(clip); - - panes - } else { - panes - }; - - ( - Primitive::Group { primitives }, - if dragging.is_some() { - mouse::Interaction::Grabbing - } else if let Some(axis) = resizing { - match axis { - Axis::Horizontal => mouse::Interaction::ResizingVertically, - Axis::Vertical => mouse::Interaction::ResizingHorizontally, - } - } else { - mouse_interaction - }, - ) - } -} diff --git a/graphics/src/renderer/widget/progress_bar.rs b/graphics/src/renderer/widget/progress_bar.rs deleted file mode 100644 index d8a145a4..00000000 --- a/graphics/src/renderer/widget/progress_bar.rs +++ /dev/null @@ -1,58 +0,0 @@ -use crate::progress_bar::StyleSheet; -use crate::{Backend, Primitive, Renderer}; -use iced_native::{mouse, progress_bar, Color, Rectangle}; - -impl progress_bar::Renderer for Renderer -where - B: Backend, -{ - type Style = Box; - - const DEFAULT_HEIGHT: u16 = 30; - - fn draw( - &self, - bounds: Rectangle, - range: std::ops::RangeInclusive, - value: f32, - style_sheet: &Self::Style, - ) -> Self::Output { - let style = style_sheet.style(); - - let (range_start, range_end) = range.into_inner(); - let active_progress_width = bounds.width - * ((value - range_start) / (range_end - range_start).max(1.0)); - - let background = Primitive::Group { - primitives: vec![Primitive::Quad { - bounds: Rectangle { ..bounds }, - background: style.background, - border_radius: style.border_radius, - border_width: 0, - border_color: Color::TRANSPARENT, - }], - }; - - ( - if active_progress_width > 0.0 { - let bar = Primitive::Quad { - bounds: Rectangle { - width: active_progress_width, - ..bounds - }, - background: style.bar, - border_radius: style.border_radius, - border_width: 0, - border_color: Color::TRANSPARENT, - }; - - Primitive::Group { - primitives: vec![background, bar], - } - } else { - background - }, - mouse::Interaction::default(), - ) - } -} diff --git a/graphics/src/renderer/widget/radio.rs b/graphics/src/renderer/widget/radio.rs deleted file mode 100644 index 4da2b60a..00000000 --- a/graphics/src/renderer/widget/radio.rs +++ /dev/null @@ -1,66 +0,0 @@ -use crate::{radio::StyleSheet, Backend, Primitive, Renderer}; -use iced_native::{mouse, radio, Background, Color, Rectangle}; - -const SIZE: f32 = 28.0; -const DOT_SIZE: f32 = SIZE / 2.0; - -impl radio::Renderer for Renderer -where - B: Backend, -{ - type Style = Box; - - const DEFAULT_SIZE: u16 = SIZE as u16; - const DEFAULT_SPACING: u16 = 15; - - fn draw( - &mut self, - bounds: Rectangle, - is_selected: bool, - is_mouse_over: bool, - (label, _): Self::Output, - style_sheet: &Self::Style, - ) -> Self::Output { - let style = if is_mouse_over { - style_sheet.hovered() - } else { - style_sheet.active() - }; - - let radio = Primitive::Quad { - bounds, - background: style.background, - border_radius: (SIZE / 2.0) as u16, - border_width: style.border_width, - border_color: style.border_color, - }; - - ( - Primitive::Group { - primitives: if is_selected { - let radio_circle = Primitive::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, - }, - background: Background::Color(style.dot_color), - border_radius: (DOT_SIZE / 2.0) as u16, - border_width: 0, - border_color: Color::TRANSPARENT, - }; - - vec![radio, radio_circle, label] - } else { - vec![radio, label] - }, - }, - if is_mouse_over { - mouse::Interaction::Pointer - } else { - mouse::Interaction::default() - }, - ) - } -} diff --git a/graphics/src/renderer/widget/row.rs b/graphics/src/renderer/widget/row.rs deleted file mode 100644 index b0bb0d2e..00000000 --- a/graphics/src/renderer/widget/row.rs +++ /dev/null @@ -1,39 +0,0 @@ -use crate::{Backend, Primitive, Renderer}; -use iced_native::mouse; -use iced_native::row; -use iced_native::{Element, Layout, Point}; - -impl row::Renderer for Renderer -where - B: Backend, -{ - fn draw( - &mut self, - defaults: &Self::Defaults, - children: &[Element<'_, Message, Self>], - layout: Layout<'_>, - cursor_position: Point, - ) -> Self::Output { - let mut mouse_interaction = mouse::Interaction::default(); - - ( - Primitive::Group { - primitives: children - .iter() - .zip(layout.children()) - .map(|(child, layout)| { - let (primitive, new_mouse_interaction) = - child.draw(self, defaults, layout, cursor_position); - - if new_mouse_interaction > mouse_interaction { - mouse_interaction = new_mouse_interaction; - } - - primitive - }) - .collect(), - }, - mouse_interaction, - ) - } -} diff --git a/graphics/src/renderer/widget/scrollable.rs b/graphics/src/renderer/widget/scrollable.rs deleted file mode 100644 index 8db17bec..00000000 --- a/graphics/src/renderer/widget/scrollable.rs +++ /dev/null @@ -1,130 +0,0 @@ -use crate::{Backend, Primitive, Renderer}; -use iced_native::mouse; -use iced_native::scrollable; -use iced_native::{Background, Color, Rectangle, Vector}; - -const SCROLLBAR_WIDTH: u16 = 10; -const SCROLLBAR_MARGIN: u16 = 2; - -impl scrollable::Renderer for Renderer -where - B: Backend, -{ - type Style = Box; - - fn scrollbar( - &self, - bounds: Rectangle, - content_bounds: Rectangle, - offset: u32, - ) -> Option { - if content_bounds.height > bounds.height { - let scrollbar_bounds = Rectangle { - x: bounds.x + bounds.width - - f32::from(SCROLLBAR_WIDTH + 2 * SCROLLBAR_MARGIN), - y: bounds.y, - width: f32::from(SCROLLBAR_WIDTH + 2 * SCROLLBAR_MARGIN), - height: bounds.height, - }; - - let ratio = bounds.height / content_bounds.height; - let scrollbar_height = bounds.height * ratio; - let y_offset = offset as f32 * ratio; - - let scroller_bounds = Rectangle { - x: scrollbar_bounds.x + f32::from(SCROLLBAR_MARGIN), - y: scrollbar_bounds.y + y_offset, - width: scrollbar_bounds.width - f32::from(2 * SCROLLBAR_MARGIN), - height: scrollbar_height, - }; - - Some(scrollable::Scrollbar { - bounds: scrollbar_bounds, - scroller: scrollable::Scroller { - bounds: scroller_bounds, - }, - }) - } else { - None - } - } - - fn draw( - &mut self, - state: &scrollable::State, - bounds: Rectangle, - _content_bounds: Rectangle, - is_mouse_over: bool, - is_mouse_over_scrollbar: bool, - scrollbar: Option, - offset: u32, - style_sheet: &Self::Style, - (content, mouse_interaction): Self::Output, - ) -> Self::Output { - ( - if let Some(scrollbar) = scrollbar { - let clip = Primitive::Clip { - bounds, - offset: Vector::new(0, offset), - content: Box::new(content), - }; - - let style = if state.is_scroller_grabbed() { - style_sheet.dragging() - } else if is_mouse_over_scrollbar { - style_sheet.hovered() - } else { - style_sheet.active() - }; - - let is_scrollbar_visible = - style.background.is_some() || style.border_width > 0; - - let scroller = if is_mouse_over - || state.is_scroller_grabbed() - || is_scrollbar_visible - { - Primitive::Quad { - bounds: scrollbar.scroller.bounds, - background: Background::Color(style.scroller.color), - border_radius: style.scroller.border_radius, - border_width: style.scroller.border_width, - border_color: style.scroller.border_color, - } - } else { - Primitive::None - }; - - let scrollbar = if is_scrollbar_visible { - Primitive::Quad { - bounds: Rectangle { - x: scrollbar.bounds.x + f32::from(SCROLLBAR_MARGIN), - width: scrollbar.bounds.width - - f32::from(2 * SCROLLBAR_MARGIN), - ..scrollbar.bounds - }, - background: style - .background - .unwrap_or(Background::Color(Color::TRANSPARENT)), - border_radius: style.border_radius, - border_width: style.border_width, - border_color: style.border_color, - } - } else { - Primitive::None - }; - - Primitive::Group { - primitives: vec![clip, scrollbar, scroller], - } - } else { - content - }, - if is_mouse_over_scrollbar || state.is_scroller_grabbed() { - mouse::Interaction::Idle - } else { - mouse_interaction - }, - ) - } -} diff --git a/graphics/src/renderer/widget/slider.rs b/graphics/src/renderer/widget/slider.rs deleted file mode 100644 index 95c843d0..00000000 --- a/graphics/src/renderer/widget/slider.rs +++ /dev/null @@ -1,109 +0,0 @@ -use crate::slider::{HandleShape, StyleSheet}; -use crate::{Backend, Primitive, Renderer}; -use iced_native::mouse; -use iced_native::slider; -use iced_native::{Background, Color, Point, Rectangle}; - -const HANDLE_HEIGHT: f32 = 22.0; - -impl slider::Renderer for Renderer -where - B: Backend, -{ - type Style = Box; - - fn height(&self) -> u32 { - 30 - } - - fn draw( - &mut self, - bounds: Rectangle, - cursor_position: Point, - range: std::ops::RangeInclusive, - value: f32, - is_dragging: bool, - style_sheet: &Self::Style, - ) -> Self::Output { - let is_mouse_over = bounds.contains(cursor_position); - - let style = if is_dragging { - style_sheet.dragging() - } else if is_mouse_over { - style_sheet.hovered() - } else { - style_sheet.active() - }; - - let rail_y = bounds.y + (bounds.height / 2.0).round(); - - let (rail_top, rail_bottom) = ( - Primitive::Quad { - bounds: Rectangle { - x: bounds.x, - y: rail_y, - width: bounds.width, - height: 2.0, - }, - background: Background::Color(style.rail_colors.0), - border_radius: 0, - border_width: 0, - border_color: Color::TRANSPARENT, - }, - Primitive::Quad { - bounds: Rectangle { - x: bounds.x, - y: rail_y + 2.0, - width: bounds.width, - height: 2.0, - }, - background: Background::Color(style.rail_colors.1), - border_radius: 0, - border_width: 0, - border_color: Color::TRANSPARENT, - }, - ); - - let (range_start, range_end) = range.into_inner(); - - let (handle_width, handle_height, handle_border_radius) = - match style.handle.shape { - HandleShape::Circle { radius } => { - (f32::from(radius * 2), f32::from(radius * 2), radius) - } - HandleShape::Rectangle { - width, - border_radius, - } => (f32::from(width), HANDLE_HEIGHT, border_radius), - }; - - let handle_offset = (bounds.width - handle_width) - * ((value - range_start) / (range_end - range_start).max(1.0)); - - let handle = Primitive::Quad { - bounds: Rectangle { - x: bounds.x + handle_offset.round(), - y: rail_y - handle_height / 2.0, - width: handle_width, - height: handle_height, - }, - background: Background::Color(style.handle.color), - border_radius: handle_border_radius, - border_width: style.handle.border_width, - border_color: style.handle.border_color, - }; - - ( - Primitive::Group { - primitives: vec![rail_top, rail_bottom, handle], - }, - if is_dragging { - mouse::Interaction::Grabbing - } else if is_mouse_over { - mouse::Interaction::Grab - } else { - mouse::Interaction::default() - }, - ) - } -} diff --git a/graphics/src/renderer/widget/space.rs b/graphics/src/renderer/widget/space.rs deleted file mode 100644 index d0e82f8d..00000000 --- a/graphics/src/renderer/widget/space.rs +++ /dev/null @@ -1,13 +0,0 @@ -use crate::{Backend, Primitive, Renderer}; -use iced_native::mouse; -use iced_native::space; -use iced_native::Rectangle; - -impl space::Renderer for Renderer -where - B: Backend, -{ - fn draw(&mut self, _bounds: Rectangle) -> Self::Output { - (Primitive::None, mouse::Interaction::default()) - } -} diff --git a/graphics/src/renderer/widget/svg.rs b/graphics/src/renderer/widget/svg.rs deleted file mode 100644 index 4d80869e..00000000 --- a/graphics/src/renderer/widget/svg.rs +++ /dev/null @@ -1,26 +0,0 @@ -use crate::backend::{self, Backend}; -use crate::{Primitive, Renderer}; -use iced_native::{mouse, svg, Layout}; - -impl svg::Renderer for Renderer -where - B: Backend + backend::Svg, -{ - fn dimensions(&self, handle: &svg::Handle) -> (u32, u32) { - self.backend().viewport_dimensions(handle) - } - - fn draw( - &mut self, - handle: svg::Handle, - layout: Layout<'_>, - ) -> Self::Output { - ( - Primitive::Svg { - handle, - bounds: layout.bounds(), - }, - mouse::Interaction::default(), - ) - } -} diff --git a/graphics/src/renderer/widget/text.rs b/graphics/src/renderer/widget/text.rs deleted file mode 100644 index 45cb1be2..00000000 --- a/graphics/src/renderer/widget/text.rs +++ /dev/null @@ -1,65 +0,0 @@ -use crate::backend::{self, Backend}; -use crate::{Primitive, Renderer}; -use iced_native::{ - mouse, text, Color, Font, HorizontalAlignment, Rectangle, Size, - VerticalAlignment, -}; - -use std::f32; - -impl text::Renderer for Renderer -where - B: Backend + backend::Text, -{ - type Font = Font; - - const DEFAULT_SIZE: u16 = 20; - - fn measure( - &self, - content: &str, - size: u16, - font: Font, - bounds: Size, - ) -> (f32, f32) { - self.backend() - .measure(content, f32::from(size), font, bounds) - } - - fn draw( - &mut self, - defaults: &Self::Defaults, - bounds: Rectangle, - content: &str, - size: u16, - font: Font, - color: Option, - horizontal_alignment: HorizontalAlignment, - vertical_alignment: VerticalAlignment, - ) -> Self::Output { - let x = match horizontal_alignment { - iced_native::HorizontalAlignment::Left => bounds.x, - iced_native::HorizontalAlignment::Center => bounds.center_x(), - iced_native::HorizontalAlignment::Right => bounds.x + bounds.width, - }; - - let y = match vertical_alignment { - iced_native::VerticalAlignment::Top => bounds.y, - iced_native::VerticalAlignment::Center => bounds.center_y(), - iced_native::VerticalAlignment::Bottom => bounds.y + bounds.height, - }; - - ( - Primitive::Text { - content: content.to_string(), - size: f32::from(size), - bounds: Rectangle { x, y, ..bounds }, - color: color.unwrap_or(defaults.text.color), - font, - horizontal_alignment, - vertical_alignment, - }, - mouse::Interaction::default(), - ) - } -} diff --git a/graphics/src/renderer/widget/text_input.rs b/graphics/src/renderer/widget/text_input.rs deleted file mode 100644 index 33807b0f..00000000 --- a/graphics/src/renderer/widget/text_input.rs +++ /dev/null @@ -1,267 +0,0 @@ -use crate::backend::{self, Backend}; -use crate::text_input::StyleSheet; -use crate::{Primitive, Renderer}; - -use iced_native::{ - mouse, - text_input::{self, cursor}, - Background, Color, Font, HorizontalAlignment, Point, Rectangle, Size, - Vector, VerticalAlignment, -}; -use std::f32; - -impl text_input::Renderer for Renderer -where - B: Backend + backend::Text, -{ - type Style = Box; - - fn default_size(&self) -> u16 { - // TODO: Make this configurable - 20 - } - - fn measure_value(&self, value: &str, size: u16, font: Font) -> f32 { - let backend = self.backend(); - - let (mut width, _) = - backend.measure(value, f32::from(size), font, Size::INFINITY); - - let spaces_around = value.len() - value.trim().len(); - - if spaces_around > 0 { - let space_width = backend.space_width(size as f32); - width += spaces_around as f32 * space_width; - } - - width - } - - fn offset( - &self, - text_bounds: Rectangle, - font: Font, - size: u16, - value: &text_input::Value, - state: &text_input::State, - ) -> f32 { - 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( - self, - text_bounds, - value, - size, - focus_position, - font, - ); - - offset - } else { - 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_sheet: &Self::Style, - ) -> Self::Output { - let is_mouse_over = bounds.contains(cursor_position); - - let style = if state.is_focused() { - style_sheet.focused() - } else if is_mouse_over { - style_sheet.hovered() - } else { - style_sheet.active() - }; - - let input = Primitive::Quad { - bounds, - background: style.background, - border_radius: style.border_radius, - border_width: style.border_width, - border_color: style.border_color, - }; - - let text = value.to_string(); - - let text_value = Primitive::Text { - content: if text.is_empty() { - placeholder.to_string() - } else { - text.clone() - }, - color: if text.is_empty() { - style_sheet.placeholder_color() - } else { - style_sheet.value_color() - }, - font, - bounds: Rectangle { - y: text_bounds.center_y(), - width: f32::INFINITY, - ..text_bounds - }, - size: f32::from(size), - horizontal_alignment: HorizontalAlignment::Left, - vertical_alignment: VerticalAlignment::Center, - }; - - let (contents_primitive, offset) = if state.is_focused() { - let cursor = state.cursor(); - - let (cursor_primitive, offset) = match cursor.state(value) { - cursor::State::Index(position) => { - let (text_value_width, offset) = - measure_cursor_and_scroll_offset( - self, - text_bounds, - value, - size, - position, - font, - ); - - ( - Primitive::Quad { - bounds: Rectangle { - x: text_bounds.x + text_value_width, - y: text_bounds.y, - width: 1.0, - height: text_bounds.height, - }, - background: Background::Color( - style_sheet.value_color(), - ), - border_radius: 0, - border_width: 0, - border_color: Color::TRANSPARENT, - }, - 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( - self, - text_bounds, - value, - size, - left, - font, - ); - - let (right_position, right_offset) = - measure_cursor_and_scroll_offset( - self, - text_bounds, - value, - size, - right, - font, - ); - - let width = right_position - left_position; - - ( - Primitive::Quad { - bounds: Rectangle { - x: text_bounds.x + left_position, - y: text_bounds.y, - width, - height: text_bounds.height, - }, - background: Background::Color( - style_sheet.selection_color(), - ), - border_radius: 0, - border_width: 0, - border_color: Color::TRANSPARENT, - }, - if end == right { - right_offset - } else { - left_offset - }, - ) - } - }; - - ( - Primitive::Group { - primitives: vec![cursor_primitive, text_value], - }, - Vector::new(offset as u32, 0), - ) - } else { - (text_value, Vector::new(0, 0)) - }; - - let text_width = self.measure_value( - if text.is_empty() { placeholder } else { &text }, - size, - font, - ); - - let contents = if text_width > text_bounds.width { - Primitive::Clip { - bounds: text_bounds, - offset, - content: Box::new(contents_primitive), - } - } else { - contents_primitive - }; - - ( - Primitive::Group { - primitives: vec![input, contents], - }, - if is_mouse_over { - mouse::Interaction::Text - } else { - mouse::Interaction::default() - }, - ) - } -} - -fn measure_cursor_and_scroll_offset( - renderer: &Renderer, - text_bounds: Rectangle, - value: &text_input::Value, - size: u16, - cursor_index: usize, - font: Font, -) -> (f32, f32) -where - B: Backend + backend::Text, -{ - use iced_native::text_input::Renderer; - - let text_before_cursor = value.until(cursor_index).to_string(); - - let text_value_width = - renderer.measure_value(&text_before_cursor, size, font); - let offset = ((text_value_width + 5.0) - text_bounds.width).max(0.0); - - (text_value_width, offset) -} diff --git a/graphics/src/widget.rs b/graphics/src/widget.rs index 6a8e814b..d9c91d77 100644 --- a/graphics/src/widget.rs +++ b/graphics/src/widget.rs @@ -17,6 +17,11 @@ pub mod scrollable; pub mod slider; pub mod text_input; +mod column; +mod image; +mod row; +mod space; +mod svg; mod text; #[doc(no_inline)] @@ -38,6 +43,11 @@ pub use slider::Slider; #[doc(no_inline)] pub use text_input::TextInput; +pub use column::Column; +pub use image::Image; +pub use row::Row; +pub use space::Space; +pub use svg::Svg; pub use text::Text; #[cfg(feature = "canvas")] diff --git a/graphics/src/widget/button.rs b/graphics/src/widget/button.rs index 9debf5c3..aeb862d5 100644 --- a/graphics/src/widget/button.rs +++ b/graphics/src/widget/button.rs @@ -4,7 +4,12 @@ //! //! [`Button`]: type.Button.html //! [`State`]: struct.State.html -use crate::Renderer; +use crate::defaults::{self, Defaults}; +use crate::{Backend, Primitive, Renderer}; +use iced_native::mouse; +use iced_native::{ + Background, Color, Element, Layout, Point, Rectangle, Vector, +}; pub use iced_native::button::State; pub use iced_style::button::{Style, StyleSheet}; @@ -14,3 +19,95 @@ pub use iced_style::button::{Style, StyleSheet}; /// This is an alias of an `iced_native` button with an `iced_wgpu::Renderer`. pub type Button<'a, Message, Backend> = iced_native::Button<'a, Message, Renderer>; + +impl iced_native::button::Renderer for Renderer +where + B: Backend, +{ + const DEFAULT_PADDING: u16 = 5; + + type Style = Box; + + fn draw( + &mut self, + _defaults: &Defaults, + bounds: Rectangle, + cursor_position: Point, + is_disabled: bool, + is_pressed: bool, + style: &Box, + content: &Element<'_, Message, Self>, + content_layout: Layout<'_>, + ) -> Self::Output { + let is_mouse_over = bounds.contains(cursor_position); + + let styling = if is_disabled { + style.disabled() + } else if is_mouse_over { + if is_pressed { + style.pressed() + } else { + style.hovered() + } + } else { + style.active() + }; + + let (content, _) = content.draw( + self, + &Defaults { + text: defaults::Text { + color: styling.text_color, + }, + }, + content_layout, + cursor_position, + ); + + ( + if styling.background.is_some() || styling.border_width > 0 { + let background = Primitive::Quad { + bounds, + background: styling + .background + .unwrap_or(Background::Color(Color::TRANSPARENT)), + border_radius: styling.border_radius, + border_width: styling.border_width, + border_color: styling.border_color, + }; + + if styling.shadow_offset == Vector::default() { + Primitive::Group { + primitives: vec![background, content], + } + } else { + // TODO: Implement proper shadow support + let shadow = Primitive::Quad { + bounds: Rectangle { + x: bounds.x + styling.shadow_offset.x, + y: bounds.y + styling.shadow_offset.y, + ..bounds + }, + background: Background::Color( + [0.0, 0.0, 0.0, 0.5].into(), + ), + border_radius: styling.border_radius, + border_width: 0, + border_color: Color::TRANSPARENT, + }; + + Primitive::Group { + primitives: vec![shadow, background, content], + } + } + } else { + content + }, + if is_mouse_over { + mouse::Interaction::Pointer + } else { + mouse::Interaction::default() + }, + ) + } +} diff --git a/graphics/src/widget/checkbox.rs b/graphics/src/widget/checkbox.rs index 3c8b58de..cb7fd2cf 100644 --- a/graphics/src/widget/checkbox.rs +++ b/graphics/src/widget/checkbox.rs @@ -1,5 +1,9 @@ //! Show toggle controls using checkboxes. -use crate::Renderer; +use crate::backend::{self, Backend}; +use crate::{Primitive, Renderer}; +use iced_native::checkbox; +use iced_native::mouse; +use iced_native::{HorizontalAlignment, Rectangle, VerticalAlignment}; pub use iced_style::checkbox::{Style, StyleSheet}; @@ -8,3 +12,65 @@ pub use iced_style::checkbox::{Style, StyleSheet}; /// This is an alias of an `iced_native` checkbox with an `iced_wgpu::Renderer`. pub type Checkbox = iced_native::Checkbox>; + +impl checkbox::Renderer for Renderer +where + B: Backend + backend::Text, +{ + type Style = Box; + + 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_sheet: &Self::Style, + ) -> Self::Output { + let style = if is_mouse_over { + style_sheet.hovered(is_checked) + } else { + style_sheet.active(is_checked) + }; + + let checkbox = Primitive::Quad { + bounds, + background: style.background, + border_radius: style.border_radius, + border_width: style.border_width, + border_color: style.border_color, + }; + + ( + Primitive::Group { + primitives: if is_checked { + let check = Primitive::Text { + content: B::CHECKMARK_ICON.to_string(), + font: B::ICON_FONT, + size: bounds.height * 0.7, + bounds: Rectangle { + x: bounds.center_x(), + y: bounds.center_y(), + ..bounds + }, + color: style.checkmark_color, + horizontal_alignment: HorizontalAlignment::Center, + vertical_alignment: VerticalAlignment::Center, + }; + + vec![checkbox, check, label] + } else { + vec![checkbox, label] + }, + }, + if is_mouse_over { + mouse::Interaction::Pointer + } else { + mouse::Interaction::default() + }, + ) + } +} diff --git a/graphics/src/widget/column.rs b/graphics/src/widget/column.rs new file mode 100644 index 00000000..9183d2ee --- /dev/null +++ b/graphics/src/widget/column.rs @@ -0,0 +1,42 @@ +use crate::{Backend, Primitive, Renderer}; +use iced_native::column; +use iced_native::mouse; +use iced_native::{Element, Layout, Point}; + +pub type Column<'a, Message, Backend> = + iced_native::Column<'a, Message, Renderer>; + +impl column::Renderer for Renderer +where + B: Backend, +{ + fn draw( + &mut self, + defaults: &Self::Defaults, + content: &[Element<'_, Message, Self>], + layout: Layout<'_>, + cursor_position: Point, + ) -> Self::Output { + let mut mouse_interaction = mouse::Interaction::default(); + + ( + Primitive::Group { + primitives: content + .iter() + .zip(layout.children()) + .map(|(child, layout)| { + let (primitive, new_mouse_interaction) = + child.draw(self, defaults, layout, cursor_position); + + if new_mouse_interaction > mouse_interaction { + mouse_interaction = new_mouse_interaction; + } + + primitive + }) + .collect(), + }, + mouse_interaction, + ) + } +} diff --git a/graphics/src/widget/container.rs b/graphics/src/widget/container.rs index c4c4e5ba..070cb48b 100644 --- a/graphics/src/widget/container.rs +++ b/graphics/src/widget/container.rs @@ -1,5 +1,8 @@ //! Decorate content and apply alignment. -use crate::Renderer; +use crate::container; +use crate::defaults::{self, Defaults}; +use crate::{Backend, Primitive, Renderer}; +use iced_native::{Background, Color, Element, Layout, Point, Rectangle}; pub use iced_style::container::{Style, StyleSheet}; @@ -9,3 +12,52 @@ pub use iced_style::container::{Style, StyleSheet}; /// `Renderer`. pub type Container<'a, Message, Backend> = iced_native::Container<'a, Message, Renderer>; + +impl iced_native::container::Renderer for Renderer +where + B: Backend, +{ + type Style = Box; + + fn draw( + &mut self, + defaults: &Defaults, + bounds: Rectangle, + cursor_position: Point, + style_sheet: &Self::Style, + content: &Element<'_, Message, Self>, + content_layout: Layout<'_>, + ) -> Self::Output { + let style = style_sheet.style(); + + let defaults = Defaults { + text: defaults::Text { + color: style.text_color.unwrap_or(defaults.text.color), + }, + }; + + let (content, mouse_interaction) = + content.draw(self, &defaults, content_layout, cursor_position); + + if style.background.is_some() || style.border_width > 0 { + let quad = Primitive::Quad { + bounds, + background: style + .background + .unwrap_or(Background::Color(Color::TRANSPARENT)), + border_radius: style.border_radius, + border_width: style.border_width, + border_color: style.border_color, + }; + + ( + Primitive::Group { + primitives: vec![quad, content], + }, + mouse_interaction, + ) + } else { + (content, mouse_interaction) + } + } +} diff --git a/graphics/src/widget/image.rs b/graphics/src/widget/image.rs new file mode 100644 index 00000000..79d82cb1 --- /dev/null +++ b/graphics/src/widget/image.rs @@ -0,0 +1,30 @@ +use crate::backend::{self, Backend}; +use crate::{Primitive, Renderer}; +use iced_native::image; +use iced_native::mouse; +use iced_native::Layout; + +pub use iced_native::Image; + +impl image::Renderer for Renderer +where + B: Backend + backend::Image, +{ + fn dimensions(&self, handle: &image::Handle) -> (u32, u32) { + self.backend().dimensions(handle) + } + + fn draw( + &mut self, + handle: image::Handle, + layout: Layout<'_>, + ) -> Self::Output { + ( + Primitive::Image { + handle, + bounds: layout.bounds(), + }, + mouse::Interaction::default(), + ) + } +} diff --git a/graphics/src/widget/pane_grid.rs b/graphics/src/widget/pane_grid.rs index 34be8ee7..56af683d 100644 --- a/graphics/src/widget/pane_grid.rs +++ b/graphics/src/widget/pane_grid.rs @@ -8,7 +8,10 @@ //! //! [`pane_grid` example]: https://github.com/hecrj/iced/tree/0.1/examples/pane_grid //! [`PaneGrid`]: type.PaneGrid.html -use crate::Renderer; +use crate::{Backend, Primitive, Renderer}; +use iced_native::mouse; +use iced_native::pane_grid; +use iced_native::{Element, Layout, Point, Rectangle, Vector}; pub use iced_native::pane_grid::{ Axis, Direction, DragEvent, Focus, KeyPressEvent, Pane, ResizeEvent, Split, @@ -23,3 +26,93 @@ pub use iced_native::pane_grid::{ /// This is an alias of an `iced_native` pane grid with an `iced_wgpu::Renderer`. pub type PaneGrid<'a, Message, Backend> = iced_native::PaneGrid<'a, Message, Renderer>; + +impl pane_grid::Renderer for Renderer +where + B: Backend, +{ + fn draw( + &mut self, + defaults: &Self::Defaults, + content: &[(Pane, Element<'_, Message, Self>)], + dragging: Option, + resizing: Option, + layout: Layout<'_>, + cursor_position: Point, + ) -> Self::Output { + let pane_cursor_position = if dragging.is_some() { + // TODO: Remove once cursor availability is encoded in the type + // system + Point::new(-1.0, -1.0) + } else { + cursor_position + }; + + let mut mouse_interaction = mouse::Interaction::default(); + let mut dragged_pane = None; + + let mut panes: Vec<_> = content + .iter() + .zip(layout.children()) + .enumerate() + .map(|(i, ((id, pane), layout))| { + let (primitive, new_mouse_interaction) = + pane.draw(self, defaults, layout, pane_cursor_position); + + if new_mouse_interaction > mouse_interaction { + mouse_interaction = new_mouse_interaction; + } + + if Some(*id) == dragging { + dragged_pane = Some((i, layout)); + } + + primitive + }) + .collect(); + + let primitives = if let Some((index, layout)) = dragged_pane { + let pane = panes.remove(index); + let bounds = layout.bounds(); + + // TODO: Fix once proper layering is implemented. + // This is a pretty hacky way to achieve layering. + let clip = Primitive::Clip { + bounds: Rectangle { + x: cursor_position.x - bounds.width / 2.0, + y: cursor_position.y - bounds.height / 2.0, + width: bounds.width + 0.5, + height: bounds.height + 0.5, + }, + offset: Vector::new(0, 0), + content: Box::new(Primitive::Translate { + translation: Vector::new( + cursor_position.x - bounds.x - bounds.width / 2.0, + cursor_position.y - bounds.y - bounds.height / 2.0, + ), + content: Box::new(pane), + }), + }; + + panes.push(clip); + + panes + } else { + panes + }; + + ( + Primitive::Group { primitives }, + if dragging.is_some() { + mouse::Interaction::Grabbing + } else if let Some(axis) = resizing { + match axis { + Axis::Horizontal => mouse::Interaction::ResizingVertically, + Axis::Vertical => mouse::Interaction::ResizingHorizontally, + } + } else { + mouse_interaction + }, + ) + } +} diff --git a/graphics/src/widget/progress_bar.rs b/graphics/src/widget/progress_bar.rs index c1e5702e..48acb3c1 100644 --- a/graphics/src/widget/progress_bar.rs +++ b/graphics/src/widget/progress_bar.rs @@ -4,7 +4,10 @@ //! as well as a length, height and style. //! //! [`ProgressBar`]: type.ProgressBar.html -use crate::Renderer; +use crate::{Backend, Primitive, Renderer}; +use iced_native::mouse; +use iced_native::progress_bar; +use iced_native::{Color, Rectangle}; pub use iced_style::progress_bar::{Style, StyleSheet}; @@ -13,3 +16,58 @@ pub use iced_style::progress_bar::{Style, StyleSheet}; /// This is an alias of an `iced_native` progress bar with an /// `iced_wgpu::Renderer`. pub type ProgressBar = iced_native::ProgressBar>; + +impl progress_bar::Renderer for Renderer +where + B: Backend, +{ + type Style = Box; + + const DEFAULT_HEIGHT: u16 = 30; + + fn draw( + &self, + bounds: Rectangle, + range: std::ops::RangeInclusive, + value: f32, + style_sheet: &Self::Style, + ) -> Self::Output { + let style = style_sheet.style(); + + let (range_start, range_end) = range.into_inner(); + let active_progress_width = bounds.width + * ((value - range_start) / (range_end - range_start).max(1.0)); + + let background = Primitive::Group { + primitives: vec![Primitive::Quad { + bounds: Rectangle { ..bounds }, + background: style.background, + border_radius: style.border_radius, + border_width: 0, + border_color: Color::TRANSPARENT, + }], + }; + + ( + if active_progress_width > 0.0 { + let bar = Primitive::Quad { + bounds: Rectangle { + width: active_progress_width, + ..bounds + }, + background: style.bar, + border_radius: style.border_radius, + border_width: 0, + border_color: Color::TRANSPARENT, + }; + + Primitive::Group { + primitives: vec![background, bar], + } + } else { + background + }, + mouse::Interaction::default(), + ) + } +} diff --git a/graphics/src/widget/radio.rs b/graphics/src/widget/radio.rs index c621a26a..dd8b5f17 100644 --- a/graphics/src/widget/radio.rs +++ b/graphics/src/widget/radio.rs @@ -1,5 +1,8 @@ //! Create choices using radio buttons. -use crate::Renderer; +use crate::{Backend, Primitive, Renderer}; +use iced_native::mouse; +use iced_native::radio; +use iced_native::{Background, Color, Rectangle}; pub use iced_style::radio::{Style, StyleSheet}; @@ -9,3 +12,67 @@ pub use iced_style::radio::{Style, StyleSheet}; /// `iced_wgpu::Renderer`. pub type Radio = iced_native::Radio>; + +const SIZE: f32 = 28.0; +const DOT_SIZE: f32 = SIZE / 2.0; + +impl radio::Renderer for Renderer +where + B: Backend, +{ + type Style = Box; + + const DEFAULT_SIZE: u16 = SIZE as u16; + const DEFAULT_SPACING: u16 = 15; + + fn draw( + &mut self, + bounds: Rectangle, + is_selected: bool, + is_mouse_over: bool, + (label, _): Self::Output, + style_sheet: &Self::Style, + ) -> Self::Output { + let style = if is_mouse_over { + style_sheet.hovered() + } else { + style_sheet.active() + }; + + let radio = Primitive::Quad { + bounds, + background: style.background, + border_radius: (SIZE / 2.0) as u16, + border_width: style.border_width, + border_color: style.border_color, + }; + + ( + Primitive::Group { + primitives: if is_selected { + let radio_circle = Primitive::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, + }, + background: Background::Color(style.dot_color), + border_radius: (DOT_SIZE / 2.0) as u16, + border_width: 0, + border_color: Color::TRANSPARENT, + }; + + vec![radio, radio_circle, label] + } else { + vec![radio, label] + }, + }, + if is_mouse_over { + mouse::Interaction::Pointer + } else { + mouse::Interaction::default() + }, + ) + } +} diff --git a/graphics/src/widget/row.rs b/graphics/src/widget/row.rs new file mode 100644 index 00000000..9865d0de --- /dev/null +++ b/graphics/src/widget/row.rs @@ -0,0 +1,42 @@ +use crate::{Backend, Primitive, Renderer}; +use iced_native::mouse; +use iced_native::row; +use iced_native::{Element, Layout, Point}; + +pub type Row<'a, Message, Backend> = + iced_native::Row<'a, Message, Renderer>; + +impl row::Renderer for Renderer +where + B: Backend, +{ + fn draw( + &mut self, + defaults: &Self::Defaults, + content: &[Element<'_, Message, Self>], + layout: Layout<'_>, + cursor_position: Point, + ) -> Self::Output { + let mut mouse_interaction = mouse::Interaction::default(); + + ( + Primitive::Group { + primitives: content + .iter() + .zip(layout.children()) + .map(|(child, layout)| { + let (primitive, new_mouse_interaction) = + child.draw(self, defaults, layout, cursor_position); + + if new_mouse_interaction > mouse_interaction { + mouse_interaction = new_mouse_interaction; + } + + primitive + }) + .collect(), + }, + mouse_interaction, + ) + } +} diff --git a/graphics/src/widget/scrollable.rs b/graphics/src/widget/scrollable.rs index 61eae587..b149db0a 100644 --- a/graphics/src/widget/scrollable.rs +++ b/graphics/src/widget/scrollable.rs @@ -1,5 +1,8 @@ //! Navigate an endless amount of content with a scrollbar. -use crate::Renderer; +use crate::{Backend, Primitive, Renderer}; +use iced_native::mouse; +use iced_native::scrollable; +use iced_native::{Background, Color, Rectangle, Vector}; pub use iced_native::scrollable::State; pub use iced_style::scrollable::{Scrollbar, Scroller, StyleSheet}; @@ -11,3 +14,129 @@ pub use iced_style::scrollable::{Scrollbar, Scroller, StyleSheet}; /// `Renderer`. pub type Scrollable<'a, Message, Backend> = iced_native::Scrollable<'a, Message, Renderer>; + +const SCROLLBAR_WIDTH: u16 = 10; +const SCROLLBAR_MARGIN: u16 = 2; + +impl scrollable::Renderer for Renderer +where + B: Backend, +{ + type Style = Box; + + fn scrollbar( + &self, + bounds: Rectangle, + content_bounds: Rectangle, + offset: u32, + ) -> Option { + if content_bounds.height > bounds.height { + let scrollbar_bounds = Rectangle { + x: bounds.x + bounds.width + - f32::from(SCROLLBAR_WIDTH + 2 * SCROLLBAR_MARGIN), + y: bounds.y, + width: f32::from(SCROLLBAR_WIDTH + 2 * SCROLLBAR_MARGIN), + height: bounds.height, + }; + + let ratio = bounds.height / content_bounds.height; + let scrollbar_height = bounds.height * ratio; + let y_offset = offset as f32 * ratio; + + let scroller_bounds = Rectangle { + x: scrollbar_bounds.x + f32::from(SCROLLBAR_MARGIN), + y: scrollbar_bounds.y + y_offset, + width: scrollbar_bounds.width - f32::from(2 * SCROLLBAR_MARGIN), + height: scrollbar_height, + }; + + Some(scrollable::Scrollbar { + bounds: scrollbar_bounds, + scroller: scrollable::Scroller { + bounds: scroller_bounds, + }, + }) + } else { + None + } + } + + fn draw( + &mut self, + state: &scrollable::State, + bounds: Rectangle, + _content_bounds: Rectangle, + is_mouse_over: bool, + is_mouse_over_scrollbar: bool, + scrollbar: Option, + offset: u32, + style_sheet: &Self::Style, + (content, mouse_interaction): Self::Output, + ) -> Self::Output { + ( + if let Some(scrollbar) = scrollbar { + let clip = Primitive::Clip { + bounds, + offset: Vector::new(0, offset), + content: Box::new(content), + }; + + let style = if state.is_scroller_grabbed() { + style_sheet.dragging() + } else if is_mouse_over_scrollbar { + style_sheet.hovered() + } else { + style_sheet.active() + }; + + let is_scrollbar_visible = + style.background.is_some() || style.border_width > 0; + + let scroller = if is_mouse_over + || state.is_scroller_grabbed() + || is_scrollbar_visible + { + Primitive::Quad { + bounds: scrollbar.scroller.bounds, + background: Background::Color(style.scroller.color), + border_radius: style.scroller.border_radius, + border_width: style.scroller.border_width, + border_color: style.scroller.border_color, + } + } else { + Primitive::None + }; + + let scrollbar = if is_scrollbar_visible { + Primitive::Quad { + bounds: Rectangle { + x: scrollbar.bounds.x + f32::from(SCROLLBAR_MARGIN), + width: scrollbar.bounds.width + - f32::from(2 * SCROLLBAR_MARGIN), + ..scrollbar.bounds + }, + background: style + .background + .unwrap_or(Background::Color(Color::TRANSPARENT)), + border_radius: style.border_radius, + border_width: style.border_width, + border_color: style.border_color, + } + } else { + Primitive::None + }; + + Primitive::Group { + primitives: vec![clip, scrollbar, scroller], + } + } else { + content + }, + if is_mouse_over_scrollbar || state.is_scroller_grabbed() { + mouse::Interaction::Idle + } else { + mouse_interaction + }, + ) + } +} diff --git a/graphics/src/widget/slider.rs b/graphics/src/widget/slider.rs index 035c7c41..b00cde9a 100644 --- a/graphics/src/widget/slider.rs +++ b/graphics/src/widget/slider.rs @@ -4,7 +4,10 @@ //! //! [`Slider`]: struct.Slider.html //! [`State`]: struct.State.html -use crate::Renderer; +use crate::{Backend, Primitive, Renderer}; +use iced_native::mouse; +use iced_native::slider; +use iced_native::{Background, Color, Point, Rectangle}; pub use iced_native::slider::State; pub use iced_style::slider::{Handle, HandleShape, Style, StyleSheet}; @@ -15,3 +18,107 @@ pub use iced_style::slider::{Handle, HandleShape, Style, StyleSheet}; /// This is an alias of an `iced_native` slider with an `iced_wgpu::Renderer`. pub type Slider<'a, Message, Backend> = iced_native::Slider<'a, Message, Renderer>; + +const HANDLE_HEIGHT: f32 = 22.0; + +impl slider::Renderer for Renderer +where + B: Backend, +{ + type Style = Box; + + fn height(&self) -> u32 { + 30 + } + + fn draw( + &mut self, + bounds: Rectangle, + cursor_position: Point, + range: std::ops::RangeInclusive, + value: f32, + is_dragging: bool, + style_sheet: &Self::Style, + ) -> Self::Output { + let is_mouse_over = bounds.contains(cursor_position); + + let style = if is_dragging { + style_sheet.dragging() + } else if is_mouse_over { + style_sheet.hovered() + } else { + style_sheet.active() + }; + + let rail_y = bounds.y + (bounds.height / 2.0).round(); + + let (rail_top, rail_bottom) = ( + Primitive::Quad { + bounds: Rectangle { + x: bounds.x, + y: rail_y, + width: bounds.width, + height: 2.0, + }, + background: Background::Color(style.rail_colors.0), + border_radius: 0, + border_width: 0, + border_color: Color::TRANSPARENT, + }, + Primitive::Quad { + bounds: Rectangle { + x: bounds.x, + y: rail_y + 2.0, + width: bounds.width, + height: 2.0, + }, + background: Background::Color(style.rail_colors.1), + border_radius: 0, + border_width: 0, + border_color: Color::TRANSPARENT, + }, + ); + + let (range_start, range_end) = range.into_inner(); + + let (handle_width, handle_height, handle_border_radius) = + match style.handle.shape { + HandleShape::Circle { radius } => { + (f32::from(radius * 2), f32::from(radius * 2), radius) + } + HandleShape::Rectangle { + width, + border_radius, + } => (f32::from(width), HANDLE_HEIGHT, border_radius), + }; + + let handle_offset = (bounds.width - handle_width) + * ((value - range_start) / (range_end - range_start).max(1.0)); + + let handle = Primitive::Quad { + bounds: Rectangle { + x: bounds.x + handle_offset.round(), + y: rail_y - handle_height / 2.0, + width: handle_width, + height: handle_height, + }, + background: Background::Color(style.handle.color), + border_radius: handle_border_radius, + border_width: style.handle.border_width, + border_color: style.handle.border_color, + }; + + ( + Primitive::Group { + primitives: vec![rail_top, rail_bottom, handle], + }, + if is_dragging { + mouse::Interaction::Grabbing + } else if is_mouse_over { + mouse::Interaction::Grab + } else { + mouse::Interaction::default() + }, + ) + } +} diff --git a/graphics/src/widget/space.rs b/graphics/src/widget/space.rs new file mode 100644 index 00000000..1f31eabe --- /dev/null +++ b/graphics/src/widget/space.rs @@ -0,0 +1,15 @@ +use crate::{Backend, Primitive, Renderer}; +use iced_native::mouse; +use iced_native::space; +use iced_native::Rectangle; + +pub use iced_native::Space; + +impl space::Renderer for Renderer +where + B: Backend, +{ + fn draw(&mut self, _bounds: Rectangle) -> Self::Output { + (Primitive::None, mouse::Interaction::default()) + } +} diff --git a/graphics/src/widget/svg.rs b/graphics/src/widget/svg.rs new file mode 100644 index 00000000..b1aa7cea --- /dev/null +++ b/graphics/src/widget/svg.rs @@ -0,0 +1,28 @@ +use crate::backend::{self, Backend}; +use crate::{Primitive, Renderer}; +use iced_native::{mouse, svg, Layout}; + +pub use iced_native::Svg; + +impl svg::Renderer for Renderer +where + B: Backend + backend::Svg, +{ + fn dimensions(&self, handle: &svg::Handle) -> (u32, u32) { + self.backend().viewport_dimensions(handle) + } + + fn draw( + &mut self, + handle: svg::Handle, + layout: Layout<'_>, + ) -> Self::Output { + ( + Primitive::Svg { + handle, + bounds: layout.bounds(), + }, + mouse::Interaction::default(), + ) + } +} diff --git a/graphics/src/widget/text.rs b/graphics/src/widget/text.rs index ec0349f9..327f8e29 100644 --- a/graphics/src/widget/text.rs +++ b/graphics/src/widget/text.rs @@ -1,7 +1,72 @@ //! Write some text for your users to read. -use crate::Renderer; +use crate::backend::{self, Backend}; +use crate::{Primitive, Renderer}; +use iced_native::mouse; +use iced_native::text; +use iced_native::{ + Color, Font, HorizontalAlignment, Rectangle, Size, VerticalAlignment, +}; /// A paragraph of text. /// /// This is an alias of an `iced_native` text with an `iced_wgpu::Renderer`. pub type Text = iced_native::Text>; + +use std::f32; + +impl text::Renderer for Renderer +where + B: Backend + backend::Text, +{ + type Font = Font; + + const DEFAULT_SIZE: u16 = 20; + + fn measure( + &self, + content: &str, + size: u16, + font: Font, + bounds: Size, + ) -> (f32, f32) { + self.backend() + .measure(content, f32::from(size), font, bounds) + } + + fn draw( + &mut self, + defaults: &Self::Defaults, + bounds: Rectangle, + content: &str, + size: u16, + font: Font, + color: Option, + horizontal_alignment: HorizontalAlignment, + vertical_alignment: VerticalAlignment, + ) -> Self::Output { + let x = match horizontal_alignment { + iced_native::HorizontalAlignment::Left => bounds.x, + iced_native::HorizontalAlignment::Center => bounds.center_x(), + iced_native::HorizontalAlignment::Right => bounds.x + bounds.width, + }; + + let y = match vertical_alignment { + iced_native::VerticalAlignment::Top => bounds.y, + iced_native::VerticalAlignment::Center => bounds.center_y(), + iced_native::VerticalAlignment::Bottom => bounds.y + bounds.height, + }; + + ( + Primitive::Text { + content: content.to_string(), + size: f32::from(size), + bounds: Rectangle { x, y, ..bounds }, + color: color.unwrap_or(defaults.text.color), + font, + horizontal_alignment, + vertical_alignment, + }, + mouse::Interaction::default(), + ) + } +} diff --git a/graphics/src/widget/text_input.rs b/graphics/src/widget/text_input.rs index 8015626b..023bdd7f 100644 --- a/graphics/src/widget/text_input.rs +++ b/graphics/src/widget/text_input.rs @@ -4,7 +4,15 @@ //! //! [`TextInput`]: struct.TextInput.html //! [`State`]: struct.State.html -use crate::Renderer; +use crate::backend::{self, Backend}; +use crate::{Primitive, Renderer}; +use iced_native::mouse; +use iced_native::text_input::{self, cursor}; +use iced_native::{ + Background, Color, Font, HorizontalAlignment, Point, Rectangle, Size, + Vector, VerticalAlignment, +}; +use std::f32; pub use iced_native::text_input::State; pub use iced_style::text_input::{Style, StyleSheet}; @@ -14,3 +22,259 @@ pub use iced_style::text_input::{Style, StyleSheet}; /// This is an alias of an `iced_native` text input with an `iced_wgpu::Renderer`. pub type TextInput<'a, Message, Backend> = iced_native::TextInput<'a, Message, Renderer>; + +impl text_input::Renderer for Renderer +where + B: Backend + backend::Text, +{ + type Style = Box; + + fn default_size(&self) -> u16 { + // TODO: Make this configurable + 20 + } + + fn measure_value(&self, value: &str, size: u16, font: Font) -> f32 { + let backend = self.backend(); + + let (mut width, _) = + backend.measure(value, f32::from(size), font, Size::INFINITY); + + let spaces_around = value.len() - value.trim().len(); + + if spaces_around > 0 { + let space_width = backend.space_width(size as f32); + width += spaces_around as f32 * space_width; + } + + width + } + + fn offset( + &self, + text_bounds: Rectangle, + font: Font, + size: u16, + value: &text_input::Value, + state: &text_input::State, + ) -> f32 { + 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( + self, + text_bounds, + value, + size, + focus_position, + font, + ); + + offset + } else { + 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_sheet: &Self::Style, + ) -> Self::Output { + let is_mouse_over = bounds.contains(cursor_position); + + let style = if state.is_focused() { + style_sheet.focused() + } else if is_mouse_over { + style_sheet.hovered() + } else { + style_sheet.active() + }; + + let input = Primitive::Quad { + bounds, + background: style.background, + border_radius: style.border_radius, + border_width: style.border_width, + border_color: style.border_color, + }; + + let text = value.to_string(); + + let text_value = Primitive::Text { + content: if text.is_empty() { + placeholder.to_string() + } else { + text.clone() + }, + color: if text.is_empty() { + style_sheet.placeholder_color() + } else { + style_sheet.value_color() + }, + font, + bounds: Rectangle { + y: text_bounds.center_y(), + width: f32::INFINITY, + ..text_bounds + }, + size: f32::from(size), + horizontal_alignment: HorizontalAlignment::Left, + vertical_alignment: VerticalAlignment::Center, + }; + + let (contents_primitive, offset) = if state.is_focused() { + let cursor = state.cursor(); + + let (cursor_primitive, offset) = match cursor.state(value) { + cursor::State::Index(position) => { + let (text_value_width, offset) = + measure_cursor_and_scroll_offset( + self, + text_bounds, + value, + size, + position, + font, + ); + + ( + Primitive::Quad { + bounds: Rectangle { + x: text_bounds.x + text_value_width, + y: text_bounds.y, + width: 1.0, + height: text_bounds.height, + }, + background: Background::Color( + style_sheet.value_color(), + ), + border_radius: 0, + border_width: 0, + border_color: Color::TRANSPARENT, + }, + 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( + self, + text_bounds, + value, + size, + left, + font, + ); + + let (right_position, right_offset) = + measure_cursor_and_scroll_offset( + self, + text_bounds, + value, + size, + right, + font, + ); + + let width = right_position - left_position; + + ( + Primitive::Quad { + bounds: Rectangle { + x: text_bounds.x + left_position, + y: text_bounds.y, + width, + height: text_bounds.height, + }, + background: Background::Color( + style_sheet.selection_color(), + ), + border_radius: 0, + border_width: 0, + border_color: Color::TRANSPARENT, + }, + if end == right { + right_offset + } else { + left_offset + }, + ) + } + }; + + ( + Primitive::Group { + primitives: vec![cursor_primitive, text_value], + }, + Vector::new(offset as u32, 0), + ) + } else { + (text_value, Vector::new(0, 0)) + }; + + let text_width = self.measure_value( + if text.is_empty() { placeholder } else { &text }, + size, + font, + ); + + let contents = if text_width > text_bounds.width { + Primitive::Clip { + bounds: text_bounds, + offset, + content: Box::new(contents_primitive), + } + } else { + contents_primitive + }; + + ( + Primitive::Group { + primitives: vec![input, contents], + }, + if is_mouse_over { + mouse::Interaction::Text + } else { + mouse::Interaction::default() + }, + ) + } +} + +fn measure_cursor_and_scroll_offset( + renderer: &Renderer, + text_bounds: Rectangle, + value: &text_input::Value, + size: u16, + cursor_index: usize, + font: Font, +) -> (f32, f32) +where + B: Backend + backend::Text, +{ + use iced_native::text_input::Renderer; + + let text_before_cursor = value.until(cursor_index).to_string(); + + let text_value_width = + renderer.measure_value(&text_before_cursor, size, font); + let offset = ((text_value_width + 5.0) - text_bounds.width).max(0.0); + + (text_value_width, offset) +} -- cgit From 720e7756f2afe30706b6b1a7fbde86b9f15e1d8c Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 19 May 2020 22:55:12 +0200 Subject: Move `Layer` to `iced_graphics` --- graphics/src/layer.rs | 266 ++++++++++++++++++++++++++++++++++++ graphics/src/lib.rs | 7 + graphics/src/primitive.rs | 6 +- graphics/src/triangle.rs | 1 + graphics/src/widget.rs | 4 +- graphics/src/widget/canvas/frame.rs | 2 +- graphics/src/widget/image.rs | 2 +- graphics/src/widget/svg.rs | 2 +- 8 files changed, 282 insertions(+), 8 deletions(-) create mode 100644 graphics/src/layer.rs (limited to 'graphics/src') diff --git a/graphics/src/layer.rs b/graphics/src/layer.rs new file mode 100644 index 00000000..ae9c6ce0 --- /dev/null +++ b/graphics/src/layer.rs @@ -0,0 +1,266 @@ +use crate::image; +use crate::svg; +use crate::triangle; +use crate::{ + Background, Font, HorizontalAlignment, Point, Primitive, Rectangle, Size, + Vector, VerticalAlignment, Viewport, +}; + +pub struct Layer<'a> { + pub bounds: Rectangle, + pub quads: Vec, + pub meshes: Vec>, + pub text: Vec>, + pub images: Vec, +} + +impl<'a> Layer<'a> { + pub fn new(bounds: Rectangle) -> Self { + Self { + bounds, + quads: Vec::new(), + meshes: Vec::new(), + text: Vec::new(), + images: Vec::new(), + } + } + + pub fn overlay(lines: &'a [impl AsRef], viewport: &Viewport) -> Self { + let (width, height) = viewport.dimensions(); + + let mut overlay = Layer::new(Rectangle { + x: 0, + y: 0, + width, + height, + }); + + for (i, line) in lines.iter().enumerate() { + let text = Text { + content: line.as_ref(), + bounds: Rectangle::new( + Point::new(11.0, 11.0 + 25.0 * i as f32), + Size::INFINITY, + ), + color: [0.9, 0.9, 0.9, 1.0], + size: 20.0, + font: Font::Default, + horizontal_alignment: HorizontalAlignment::Left, + vertical_alignment: VerticalAlignment::Top, + }; + + overlay.text.push(text); + + overlay.text.push(Text { + bounds: text.bounds + Vector::new(-1.0, -1.0), + color: [0.0, 0.0, 0.0, 1.0], + ..text + }); + } + + overlay + } + + pub(crate) fn intersection( + &self, + rectangle: Rectangle, + ) -> Option> { + let layer_bounds: Rectangle = self.bounds.into(); + + layer_bounds.intersection(&rectangle).map(Into::into) + } + + pub fn generate( + primitive: &'a Primitive, + viewport: &Viewport, + ) -> Vec { + let mut layers = Vec::new(); + let (width, height) = viewport.dimensions(); + + layers.push(Layer::new(Rectangle { + x: 0, + y: 0, + width, + height, + })); + + Self::process_primitive(&mut layers, Vector::new(0.0, 0.0), primitive); + + layers + } + + fn process_primitive( + layers: &mut Vec, + translation: Vector, + primitive: &'a Primitive, + ) { + match primitive { + Primitive::None => {} + Primitive::Group { primitives } => { + // TODO: Inspect a bit and regroup (?) + for primitive in primitives { + Self::process_primitive(layers, translation, primitive) + } + } + Primitive::Text { + content, + bounds, + size, + color, + font, + horizontal_alignment, + vertical_alignment, + } => { + let layer = layers.last_mut().unwrap(); + + layer.text.push(Text { + content, + bounds: *bounds + translation, + size: *size, + color: color.into_linear(), + font: *font, + horizontal_alignment: *horizontal_alignment, + vertical_alignment: *vertical_alignment, + }); + } + Primitive::Quad { + bounds, + background, + border_radius, + border_width, + border_color, + } => { + let layer = layers.last_mut().unwrap(); + + // TODO: Move some of these computations to the GPU (?) + layer.quads.push(Quad { + position: [ + bounds.x + translation.x, + bounds.y + translation.y, + ], + scale: [bounds.width, bounds.height], + color: match background { + Background::Color(color) => color.into_linear(), + }, + border_radius: *border_radius as f32, + border_width: *border_width as f32, + border_color: border_color.into_linear(), + }); + } + Primitive::Mesh2D { buffers, size } => { + let layer = layers.last_mut().unwrap(); + + let bounds = Rectangle::new( + Point::new(translation.x, translation.y), + *size, + ); + + // Only draw visible content + if let Some(clip_bounds) = layer.intersection(bounds) { + layer.meshes.push(Mesh { + origin: Point::new(translation.x, translation.y), + buffers, + clip_bounds: clip_bounds.into(), + }); + } + } + Primitive::Clip { + bounds, + offset, + content, + } => { + let layer = layers.last_mut().unwrap(); + + // Only draw visible content + if let Some(clip_bounds) = + layer.intersection(*bounds + translation) + { + let clip_layer = Layer::new(clip_bounds.into()); + 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.push(new_layer); + } + } + Primitive::Translate { + translation: new_translation, + content, + } => { + Self::process_primitive( + layers, + translation + *new_translation, + &content, + ); + } + Primitive::Cached { cache } => { + Self::process_primitive(layers, translation, &cache); + } + Primitive::Image { handle, bounds } => { + let layer = layers.last_mut().unwrap(); + + layer.images.push(Image::Raster { + handle: handle.clone(), + bounds: *bounds + translation, + }); + } + Primitive::Svg { handle, bounds } => { + let layer = layers.last_mut().unwrap(); + + layer.images.push(Image::Vector { + handle: handle.clone(), + bounds: *bounds + translation, + }); + } + } + } +} + +#[derive(Debug, Clone, Copy)] +#[repr(C)] +pub struct Quad { + pub position: [f32; 2], + pub scale: [f32; 2], + pub color: [f32; 4], + pub border_color: [f32; 4], + pub border_radius: f32, + pub border_width: f32, +} + +#[derive(Debug, Clone, Copy)] +pub struct Mesh<'a> { + pub origin: Point, + pub buffers: &'a triangle::Mesh2D, + pub clip_bounds: Rectangle, +} + +#[derive(Debug, Clone, Copy)] +pub struct Text<'a> { + pub content: &'a str, + pub bounds: Rectangle, + pub color: [f32; 4], + pub size: f32, + pub font: Font, + pub horizontal_alignment: HorizontalAlignment, + pub vertical_alignment: VerticalAlignment, +} + +#[derive(Debug, Clone)] +pub enum Image { + Raster { + handle: image::Handle, + bounds: Rectangle, + }, + Vector { + handle: svg::Handle, + bounds: Rectangle, + }, +} + +unsafe impl bytemuck::Zeroable for Quad {} +unsafe impl bytemuck::Pod for Quad {} diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs index 5dc4040f..5ab333c7 100644 --- a/graphics/src/lib.rs +++ b/graphics/src/lib.rs @@ -8,6 +8,7 @@ mod widget; pub mod backend; pub mod font; +pub mod layer; pub mod triangle; #[doc(no_inline)] @@ -16,7 +17,13 @@ pub use widget::*; pub use antialiasing::Antialiasing; pub use backend::Backend; pub use defaults::Defaults; +pub use layer::Layer; pub use primitive::Primitive; pub use renderer::Renderer; pub use transformation::Transformation; pub use viewport::Viewport; + +pub use iced_native::{ + Background, Font, HorizontalAlignment, Point, Rectangle, Size, Vector, + VerticalAlignment, +}; diff --git a/graphics/src/primitive.rs b/graphics/src/primitive.rs index e73227ef..95dbf7dd 100644 --- a/graphics/src/primitive.rs +++ b/graphics/src/primitive.rs @@ -82,13 +82,13 @@ pub enum Primitive { /// /// It can be used to render many kinds of geometry freely. Mesh2D { + /// The vertex and index buffers of the mesh + buffers: triangle::Mesh2D, + /// The size of the drawable region of the mesh. /// /// Any geometry that falls out of this region will be clipped. size: Size, - - /// The vertex and index buffers of the mesh - buffers: triangle::Mesh2D, }, /// A cached primitive. /// diff --git a/graphics/src/triangle.rs b/graphics/src/triangle.rs index 2b157e3a..474f69b8 100644 --- a/graphics/src/triangle.rs +++ b/graphics/src/triangle.rs @@ -5,6 +5,7 @@ pub struct Mesh2D { /// The vertices of the mesh pub vertices: Vec, + /// The list of vertex indices that defines the triangles of the mesh. /// /// Therefore, this list should always have a length that is a multiple of diff --git a/graphics/src/widget.rs b/graphics/src/widget.rs index d9c91d77..1f6d6559 100644 --- a/graphics/src/widget.rs +++ b/graphics/src/widget.rs @@ -10,18 +10,18 @@ pub mod button; pub mod checkbox; pub mod container; +pub mod image; pub mod pane_grid; pub mod progress_bar; pub mod radio; pub mod scrollable; pub mod slider; +pub mod svg; pub mod text_input; mod column; -mod image; mod row; mod space; -mod svg; mod text; #[doc(no_inline)] diff --git a/graphics/src/widget/canvas/frame.rs b/graphics/src/widget/canvas/frame.rs index 5262ab4e..48d28d95 100644 --- a/graphics/src/widget/canvas/frame.rs +++ b/graphics/src/widget/canvas/frame.rs @@ -304,11 +304,11 @@ impl Frame { pub fn into_geometry(mut self) -> Geometry { if !self.buffers.indices.is_empty() { self.primitives.push(Primitive::Mesh2D { - size: self.size, buffers: triangle::Mesh2D { vertices: self.buffers.vertices, indices: self.buffers.indices, }, + size: self.size, }); } diff --git a/graphics/src/widget/image.rs b/graphics/src/widget/image.rs index 79d82cb1..ea49febe 100644 --- a/graphics/src/widget/image.rs +++ b/graphics/src/widget/image.rs @@ -4,7 +4,7 @@ use iced_native::image; use iced_native::mouse; use iced_native::Layout; -pub use iced_native::Image; +pub use iced_native::image::{Handle, Image}; impl image::Renderer for Renderer where diff --git a/graphics/src/widget/svg.rs b/graphics/src/widget/svg.rs index b1aa7cea..8c681478 100644 --- a/graphics/src/widget/svg.rs +++ b/graphics/src/widget/svg.rs @@ -2,7 +2,7 @@ use crate::backend::{self, Backend}; use crate::{Primitive, Renderer}; use iced_native::{mouse, svg, Layout}; -pub use iced_native::Svg; +pub use iced_native::svg::{Handle, Svg}; impl svg::Renderer for Renderer where -- cgit From a1a5fcfd46622d5b18d1716aa2adb4659835ccf3 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 20 May 2020 20:28:35 +0200 Subject: Refactor `Viewport` and `Compositor` --- graphics/src/layer.rs | 45 +++++++++-------------------- graphics/src/lib.rs | 1 + graphics/src/viewport.rs | 47 +++++++++++++++++++----------- graphics/src/window.rs | 3 ++ graphics/src/window/compositor.rs | 60 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 108 insertions(+), 48 deletions(-) create mode 100644 graphics/src/window.rs create mode 100644 graphics/src/window/compositor.rs (limited to 'graphics/src') diff --git a/graphics/src/layer.rs b/graphics/src/layer.rs index ae9c6ce0..916b5c83 100644 --- a/graphics/src/layer.rs +++ b/graphics/src/layer.rs @@ -7,7 +7,7 @@ use crate::{ }; pub struct Layer<'a> { - pub bounds: Rectangle, + pub bounds: Rectangle, pub quads: Vec, pub meshes: Vec>, pub text: Vec>, @@ -15,7 +15,7 @@ pub struct Layer<'a> { } impl<'a> Layer<'a> { - pub fn new(bounds: Rectangle) -> Self { + pub fn new(bounds: Rectangle) -> Self { Self { bounds, quads: Vec::new(), @@ -26,14 +26,8 @@ impl<'a> Layer<'a> { } pub fn overlay(lines: &'a [impl AsRef], viewport: &Viewport) -> Self { - let (width, height) = viewport.dimensions(); - - let mut overlay = Layer::new(Rectangle { - x: 0, - y: 0, - width, - height, - }); + let mut overlay = + Layer::new(Rectangle::with_size(viewport.logical_size())); for (i, line) in lines.iter().enumerate() { let text = Text { @@ -61,28 +55,14 @@ impl<'a> Layer<'a> { overlay } - pub(crate) fn intersection( - &self, - rectangle: Rectangle, - ) -> Option> { - let layer_bounds: Rectangle = self.bounds.into(); - - layer_bounds.intersection(&rectangle).map(Into::into) - } - pub fn generate( primitive: &'a Primitive, viewport: &Viewport, ) -> Vec { - let mut layers = Vec::new(); - let (width, height) = viewport.dimensions(); + let first_layer = + Layer::new(Rectangle::with_size(viewport.logical_size())); - layers.push(Layer::new(Rectangle { - x: 0, - y: 0, - width, - height, - })); + let mut layers = vec![first_layer]; Self::process_primitive(&mut layers, Vector::new(0.0, 0.0), primitive); @@ -156,11 +136,11 @@ impl<'a> Layer<'a> { ); // Only draw visible content - if let Some(clip_bounds) = layer.intersection(bounds) { + if let Some(clip_bounds) = layer.bounds.intersection(&bounds) { layer.meshes.push(Mesh { origin: Point::new(translation.x, translation.y), buffers, - clip_bounds: clip_bounds.into(), + clip_bounds, }); } } @@ -170,12 +150,13 @@ impl<'a> Layer<'a> { content, } => { let layer = layers.last_mut().unwrap(); + let translated_bounds = *bounds + translation; // Only draw visible content if let Some(clip_bounds) = - layer.intersection(*bounds + translation) + layer.bounds.intersection(&translated_bounds) { - let clip_layer = Layer::new(clip_bounds.into()); + let clip_layer = Layer::new(clip_bounds); let new_layer = Layer::new(layer.bounds); layers.push(clip_layer); @@ -236,7 +217,7 @@ pub struct Quad { pub struct Mesh<'a> { pub origin: Point, pub buffers: &'a triangle::Mesh2D, - pub clip_bounds: Rectangle, + pub clip_bounds: Rectangle, } #[derive(Debug, Clone, Copy)] diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs index 5ab333c7..2de9d21c 100644 --- a/graphics/src/lib.rs +++ b/graphics/src/lib.rs @@ -10,6 +10,7 @@ pub mod backend; pub mod font; pub mod layer; pub mod triangle; +pub mod window; #[doc(no_inline)] pub use widget::*; diff --git a/graphics/src/viewport.rs b/graphics/src/viewport.rs index 48e5a249..745ef339 100644 --- a/graphics/src/viewport.rs +++ b/graphics/src/viewport.rs @@ -1,33 +1,48 @@ -use crate::Transformation; +use crate::{Size, Transformation}; /// A viewing region for displaying computer graphics. #[derive(Debug)] pub struct Viewport { - width: u32, - height: u32, - transformation: Transformation, + physical_size: Size, + logical_size: Size, + scale_factor: f64, + projection: Transformation, } impl Viewport { - /// Creates a new [`Viewport`] with the given dimensions. - pub fn new(width: u32, height: u32) -> Viewport { + /// Creates a new [`Viewport`] with the given physical dimensions and scale + /// factor. + /// + /// [`Viewport`]: struct.Viewport.html + pub fn with_physical_size(size: Size, scale_factor: f64) -> Viewport { Viewport { - width, - height, - transformation: Transformation::orthographic(width, height), + physical_size: size, + logical_size: Size::new( + (size.width as f64 / scale_factor) as f32, + (size.height as f64 / scale_factor) as f32, + ), + scale_factor, + projection: Transformation::orthographic(size.width, size.height), } } - pub fn height(&self) -> u32 { - self.height + pub fn physical_size(&self) -> Size { + self.physical_size } - /// Returns the dimensions of the [`Viewport`]. - pub fn dimensions(&self) -> (u32, u32) { - (self.width, self.height) + pub fn physical_height(&self) -> u32 { + self.physical_size.height } - pub fn transformation(&self) -> Transformation { - self.transformation + pub fn logical_size(&self) -> Size { + self.logical_size + } + + pub fn scale_factor(&self) -> f64 { + self.scale_factor + } + + pub fn projection(&self) -> Transformation { + self.projection } } diff --git a/graphics/src/window.rs b/graphics/src/window.rs new file mode 100644 index 00000000..a7c8911b --- /dev/null +++ b/graphics/src/window.rs @@ -0,0 +1,3 @@ +mod compositor; + +pub use compositor::Compositor; diff --git a/graphics/src/window/compositor.rs b/graphics/src/window/compositor.rs new file mode 100644 index 00000000..82faa6e1 --- /dev/null +++ b/graphics/src/window/compositor.rs @@ -0,0 +1,60 @@ +use crate::Viewport; +use iced_native::mouse; +use raw_window_handle::HasRawWindowHandle; + +/// A graphics compositor that can draw to windows. +pub trait Compositor: Sized { + /// The settings of the backend. + type Settings: Default + Clone; + + /// The iced renderer of the backend. + type Renderer: iced_native::Renderer; + + /// The surface of the backend. + type Surface; + + /// The swap chain of the backend. + type SwapChain; + + /// Creates a new [`Backend`]. + /// + /// [`Backend`]: trait.Backend.html + fn new(settings: Self::Settings) -> Self; + + /// Crates a new [`Surface`] for the given window. + /// + /// [`Surface`]: #associatedtype.Surface + fn create_surface( + &mut self, + window: &W, + ) -> Self::Surface; + + /// Crates a new [`Renderer`]. + /// + /// [`Renderer`]: #associatedtype.Renderer + fn create_renderer(&mut self, settings: Self::Settings) -> Self::Renderer; + + /// Crates a new [`SwapChain`] for the given [`Surface`]. + /// + /// [`SwapChain`]: #associatedtype.SwapChain + /// [`Surface`]: #associatedtype.Surface + fn create_swap_chain( + &mut self, + surface: &Self::Surface, + width: u32, + height: u32, + ) -> Self::SwapChain; + + /// Draws the output primitives to the next frame of the given [`SwapChain`]. + /// + /// [`SwapChain`]: #associatedtype.SwapChain + /// [`Surface`]: #associatedtype.Surface + fn draw>( + &mut self, + renderer: &mut Self::Renderer, + swap_chain: &mut Self::SwapChain, + viewport: &Viewport, + output: &::Output, + overlay: &[T], + ) -> mouse::Interaction; +} -- cgit From e0e4ee73feead3f05730625c7e1917b63f0b384e Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 21 May 2020 00:37:47 +0200 Subject: Implement `iced_glutin` :tada: --- graphics/src/layer.rs | 1 + graphics/src/window.rs | 6 ++++++ graphics/src/window/gl_compositor.rs | 24 ++++++++++++++++++++++++ 3 files changed, 31 insertions(+) create mode 100644 graphics/src/window/gl_compositor.rs (limited to 'graphics/src') diff --git a/graphics/src/layer.rs b/graphics/src/layer.rs index 916b5c83..59b792f0 100644 --- a/graphics/src/layer.rs +++ b/graphics/src/layer.rs @@ -6,6 +6,7 @@ use crate::{ Vector, VerticalAlignment, Viewport, }; +#[derive(Debug, Clone)] pub struct Layer<'a> { pub bounds: Rectangle, pub quads: Vec, diff --git a/graphics/src/window.rs b/graphics/src/window.rs index a7c8911b..380efb8c 100644 --- a/graphics/src/window.rs +++ b/graphics/src/window.rs @@ -1,3 +1,9 @@ mod compositor; +#[cfg(feature = "opengl")] +mod gl_compositor; + pub use compositor::Compositor; + +#[cfg(feature = "opengl")] +pub use gl_compositor::GLCompositor; diff --git a/graphics/src/window/gl_compositor.rs b/graphics/src/window/gl_compositor.rs new file mode 100644 index 00000000..979d891e --- /dev/null +++ b/graphics/src/window/gl_compositor.rs @@ -0,0 +1,24 @@ +use crate::{Size, Viewport}; +use iced_native::mouse; + +use core::ffi::c_void; + +pub trait GLCompositor: Sized { + type Renderer: iced_native::Renderer; + type Settings: Default; + + unsafe fn new( + settings: Self::Settings, + loader_function: impl FnMut(&str) -> *const c_void, + ) -> (Self, Self::Renderer); + + fn resize_viewport(&mut self, physical_size: Size); + + fn draw>( + &mut self, + renderer: &mut Self::Renderer, + viewport: &Viewport, + output: &::Output, + overlay: &[T], + ) -> mouse::Interaction; +} -- cgit From ae5e2c6c734894d71b2034a498a858b7997c5d3d Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 21 May 2020 04:27:31 +0200 Subject: Introduce `Program` and `State` --- graphics/src/window/compositor.rs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) (limited to 'graphics/src') diff --git a/graphics/src/window/compositor.rs b/graphics/src/window/compositor.rs index 82faa6e1..d5920c95 100644 --- a/graphics/src/window/compositor.rs +++ b/graphics/src/window/compositor.rs @@ -5,7 +5,7 @@ use raw_window_handle::HasRawWindowHandle; /// A graphics compositor that can draw to windows. pub trait Compositor: Sized { /// The settings of the backend. - type Settings: Default + Clone; + type Settings: Default; /// The iced renderer of the backend. type Renderer: iced_native::Renderer; @@ -19,7 +19,7 @@ pub trait Compositor: Sized { /// Creates a new [`Backend`]. /// /// [`Backend`]: trait.Backend.html - fn new(settings: Self::Settings) -> Self; + fn new(settings: Self::Settings) -> (Self, Self::Renderer); /// Crates a new [`Surface`] for the given window. /// @@ -29,11 +29,6 @@ pub trait Compositor: Sized { window: &W, ) -> Self::Surface; - /// Crates a new [`Renderer`]. - /// - /// [`Renderer`]: #associatedtype.Renderer - fn create_renderer(&mut self, settings: Self::Settings) -> Self::Renderer; - /// Crates a new [`SwapChain`] for the given [`Surface`]. /// /// [`SwapChain`]: #associatedtype.SwapChain -- cgit From 1dd79c4697ce39589bea84142334b3cbd242fb59 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 22 May 2020 19:15:39 +0200 Subject: Use built-in OpenGL multisampling in `iced_glow` --- graphics/src/window/gl_compositor.rs | 2 ++ 1 file changed, 2 insertions(+) (limited to 'graphics/src') diff --git a/graphics/src/window/gl_compositor.rs b/graphics/src/window/gl_compositor.rs index 979d891e..aea898e3 100644 --- a/graphics/src/window/gl_compositor.rs +++ b/graphics/src/window/gl_compositor.rs @@ -12,6 +12,8 @@ pub trait GLCompositor: Sized { loader_function: impl FnMut(&str) -> *const c_void, ) -> (Self, Self::Renderer); + fn sample_count(settings: &Self::Settings) -> u32; + fn resize_viewport(&mut self, physical_size: Size); fn draw>( -- cgit From 823ea1573245b849a0696543838a7ad1d0f914d8 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 27 May 2020 23:09:27 +0200 Subject: Update `glyph_brush` and `glow_glyph` --- graphics/src/backend.rs | 2 -- graphics/src/widget/text_input.rs | 9 +-------- 2 files changed, 1 insertion(+), 10 deletions(-) (limited to 'graphics/src') diff --git a/graphics/src/backend.rs b/graphics/src/backend.rs index 3fcb42f7..ee4eca0a 100644 --- a/graphics/src/backend.rs +++ b/graphics/src/backend.rs @@ -17,8 +17,6 @@ pub trait Text { font: Font, bounds: Size, ) -> (f32, f32); - - fn space_width(&self, size: f32) -> f32; } pub trait Image { diff --git a/graphics/src/widget/text_input.rs b/graphics/src/widget/text_input.rs index 023bdd7f..893197d1 100644 --- a/graphics/src/widget/text_input.rs +++ b/graphics/src/widget/text_input.rs @@ -37,16 +37,9 @@ where fn measure_value(&self, value: &str, size: u16, font: Font) -> f32 { let backend = self.backend(); - let (mut width, _) = + let (width, _) = backend.measure(value, f32::from(size), font, Size::INFINITY); - let spaces_around = value.len() - value.trim().len(); - - if spaces_around > 0 { - let space_width = backend.space_width(size as f32); - width += spaces_around as f32 * space_width; - } - width } -- cgit From 2ca7e3c4b0cb293adebf9a9bf9a26191069d495d Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 28 May 2020 01:37:59 +0200 Subject: Write documentation for `iced_graphics` --- graphics/src/antialiasing.rs | 3 + graphics/src/backend.rs | 22 +++++++ graphics/src/font.rs | 11 ++++ graphics/src/font/source.rs | 8 +++ graphics/src/layer.rs | 115 ++++++++++++++++++++++++++++++++++- graphics/src/lib.rs | 10 +++ graphics/src/renderer.rs | 18 +++++- graphics/src/triangle.rs | 5 ++ graphics/src/viewport.rs | 22 +++++++ graphics/src/widget/column.rs | 1 + graphics/src/widget/image.rs | 1 + graphics/src/widget/row.rs | 1 + graphics/src/widget/svg.rs | 1 + graphics/src/window.rs | 1 + graphics/src/window/gl_compositor.rs | 41 +++++++++++++ 15 files changed, 256 insertions(+), 4 deletions(-) (limited to 'graphics/src') diff --git a/graphics/src/antialiasing.rs b/graphics/src/antialiasing.rs index f92b3692..34d94711 100644 --- a/graphics/src/antialiasing.rs +++ b/graphics/src/antialiasing.rs @@ -12,6 +12,9 @@ pub enum Antialiasing { } impl Antialiasing { + /// Returns the amount of samples of the [`Antialiasing`]. + /// + /// [`Antialiasing`]: enum.Antialiasing.html pub fn sample_count(self) -> u32 { match self { Antialiasing::MSAAx2 => 2, diff --git a/graphics/src/backend.rs b/graphics/src/backend.rs index ee4eca0a..83510311 100644 --- a/graphics/src/backend.rs +++ b/graphics/src/backend.rs @@ -1,15 +1,33 @@ +//! Write a graphics backend. use iced_native::image; use iced_native::svg; use iced_native::{Font, Size}; +/// The graphics backend of a [`Renderer`]. +/// +/// [`Renderer`]: ../struct.Renderer.html pub trait Backend { + /// Trims the measurements cache. + /// + /// This method is currently necessary to properly trim the text cache in + /// `iced_wgpu` and `iced_glow` because of limitations in the text rendering + /// pipeline. It will be removed in the future. fn trim_measurements(&mut self) {} } +/// A graphics backend that supports text rendering. pub trait Text { + /// The icon font of the backend. const ICON_FONT: Font; + + /// The `char` representing a ✔ icon in the [`ICON_FONT`]. + /// + /// [`ICON_FONT`]: #associatedconst.ICON_FONt const CHECKMARK_ICON: char; + /// Measures the text contents with the given size and font, + /// returning the size of a laid out paragraph that fits in the provided + /// bounds. fn measure( &self, contents: &str, @@ -19,10 +37,14 @@ pub trait Text { ) -> (f32, f32); } +/// A graphics backend that supports image rendering. pub trait Image { + /// Returns the dimensions of the provided image. fn dimensions(&self, handle: &image::Handle) -> (u32, u32); } +/// A graphics backend that supports SVG rendering. pub trait Svg { + /// Returns the viewport dimensions of the provided SVG. fn viewport_dimensions(&self, handle: &svg::Handle) -> (u32, u32); } diff --git a/graphics/src/font.rs b/graphics/src/font.rs index a490e609..bcc28857 100644 --- a/graphics/src/font.rs +++ b/graphics/src/font.rs @@ -1,22 +1,33 @@ +//! Find system fonts or use the built-in ones. #[cfg(feature = "font-source")] mod source; #[cfg(feature = "font-source")] +#[cfg_attr(docsrs, doc(cfg(feature = "font-source")))] pub use source::Source; #[cfg(feature = "font-source")] +#[cfg_attr(docsrs, doc(cfg(feature = "font-source")))] pub use font_kit::{ error::SelectionError as LoadError, family_name::FamilyName as Family, }; +/// A built-in fallback font, for convenience. #[cfg(feature = "font-fallback")] +#[cfg_attr(docsrs, doc(cfg(feature = "font-fallback")))] pub const FALLBACK: &[u8] = include_bytes!("../fonts/Lato-Regular.ttf"); +/// A built-in icon font, for convenience. #[cfg(feature = "font-icons")] +#[cfg_attr(docsrs, doc(cfg(feature = "font-icons")))] pub const ICONS: iced_native::Font = iced_native::Font::External { name: "iced_wgpu icons", bytes: include_bytes!("../fonts/Icons.ttf"), }; +/// The `char` representing a ✔ icon in the built-in [`ICONS`] font. +/// +/// [`ICONS`]: const.ICONS.html #[cfg(feature = "font-icons")] +#[cfg_attr(docsrs, doc(cfg(feature = "font-icons")))] pub const CHECKMARK_ICON: char = '\u{F00C}'; diff --git a/graphics/src/font/source.rs b/graphics/src/font/source.rs index 6855aa93..917291ff 100644 --- a/graphics/src/font/source.rs +++ b/graphics/src/font/source.rs @@ -1,16 +1,24 @@ use crate::font::{Family, LoadError}; +/// A font source that can find and load system fonts. +#[allow(missing_debug_implementations)] pub struct Source { raw: font_kit::source::SystemSource, } impl Source { + /// Creates a new [`Source`]. + /// + /// [`Source`]: struct.Source.html pub fn new() -> Self { Source { raw: font_kit::source::SystemSource::new(), } } + /// Finds and loads a font matching the set of provided family priorities. + /// + /// [`Source`]: struct.Source.html pub fn load(&self, families: &[Family]) -> Result, LoadError> { let font = self.raw.select_best_match( families, diff --git a/graphics/src/layer.rs b/graphics/src/layer.rs index 59b792f0..6aca738e 100644 --- a/graphics/src/layer.rs +++ b/graphics/src/layer.rs @@ -1,3 +1,4 @@ +//! Organize rendering primitives into a flattened list of layers. use crate::image; use crate::svg; use crate::triangle; @@ -6,16 +7,39 @@ use crate::{ Vector, VerticalAlignment, Viewport, }; +/// A group of primitives that should be clipped together. #[derive(Debug, Clone)] pub struct Layer<'a> { + /// The clipping bounds of the [`Layer`]. + /// + /// [`Layer`]: struct.Layer.html pub bounds: Rectangle, + + /// The quads of the [`Layer`]. + /// + /// [`Layer`]: struct.Layer.html pub quads: Vec, + + /// The triangle meshes of the [`Layer`]. + /// + /// [`Layer`]: struct.Layer.html pub meshes: Vec>, + + /// The text of the [`Layer`]. + /// + /// [`Layer`]: struct.Layer.html pub text: Vec>, + + /// The images of the [`Layer`]. + /// + /// [`Layer`]: struct.Layer.html pub images: Vec, } impl<'a> Layer<'a> { + /// Creates a new [`Layer`] with the given clipping bounds. + /// + /// [`Layer`]: struct.Layer.html pub fn new(bounds: Rectangle) -> Self { Self { bounds, @@ -26,6 +50,11 @@ impl<'a> Layer<'a> { } } + /// Creates a new [`Layer`] for the provided overlay text. + /// + /// This can be useful for displaying debug information. + /// + /// [`Layer`]: struct.Layer.html pub fn overlay(lines: &'a [impl AsRef], viewport: &Viewport) -> Self { let mut overlay = Layer::new(Rectangle::with_size(viewport.logical_size())); @@ -56,6 +85,10 @@ impl<'a> Layer<'a> { overlay } + /// Distributes the given [`Primitive`] and generates a list of layers based + /// on its contents. + /// + /// [`Primitive`]: ../enum.Primitive.html pub fn generate( primitive: &'a Primitive, viewport: &Viewport, @@ -119,7 +152,7 @@ impl<'a> Layer<'a> { bounds.x + translation.x, bounds.y + translation.y, ], - scale: [bounds.width, bounds.height], + size: [bounds.width, bounds.height], color: match background { Background::Color(color) => color.into_linear(), }, @@ -203,46 +236,124 @@ impl<'a> Layer<'a> { } } +/// A colored rectangle with a border. +/// +/// This type can be directly uploaded to GPU memory. #[derive(Debug, Clone, Copy)] #[repr(C)] pub struct Quad { + /// The position of the [`Quad`]. + /// + /// [`Quad`]: struct.Quad.html pub position: [f32; 2], - pub scale: [f32; 2], + + /// The size of the [`Quad`]. + /// + /// [`Quad`]: struct.Quad.html + pub size: [f32; 2], + + /// The color of the [`Quad`], in __linear RGB__. + /// + /// [`Quad`]: struct.Quad.html pub color: [f32; 4], + + /// The border color of the [`Quad`], in __linear RGB__. + /// + /// [`Quad`]: struct.Quad.html pub border_color: [f32; 4], + + /// The border radius of the [`Quad`]. + /// + /// [`Quad`]: struct.Quad.html pub border_radius: f32, + + /// The border width of the [`Quad`]. + /// + /// [`Quad`]: struct.Quad.html pub border_width: f32, } +/// A mesh of triangles. #[derive(Debug, Clone, Copy)] pub struct Mesh<'a> { + /// The origin of the vertices of the [`Mesh`]. + /// + /// [`Mesh`]: struct.Mesh.html pub origin: Point, + + /// The vertex and index buffers of the [`Mesh`]. + /// + /// [`Mesh`]: struct.Mesh.html pub buffers: &'a triangle::Mesh2D, + + /// The clipping bounds of the [`Mesh`]. + /// + /// [`Mesh`]: struct.Mesh.html pub clip_bounds: Rectangle, } +/// A paragraph of text. #[derive(Debug, Clone, Copy)] pub struct Text<'a> { + /// The content of the [`Text`]. + /// + /// [`Text`]: struct.Text.html pub content: &'a str, + + /// The layout bounds of the [`Text`]. + /// + /// [`Text`]: struct.Text.html pub bounds: Rectangle, + + /// The color of the [`Text`], in __linear RGB_. + /// + /// [`Text`]: struct.Text.html pub color: [f32; 4], + + /// The size of the [`Text`]. + /// + /// [`Text`]: struct.Text.html pub size: f32, + + /// The font of the [`Text`]. + /// + /// [`Text`]: struct.Text.html pub font: Font, + + /// The horizontal alignment of the [`Text`]. + /// + /// [`Text`]: struct.Text.html pub horizontal_alignment: HorizontalAlignment, + + /// The vertical alignment of the [`Text`]. + /// + /// [`Text`]: struct.Text.html pub vertical_alignment: VerticalAlignment, } +/// A raster or vector image. #[derive(Debug, Clone)] pub enum Image { + /// A raster image. Raster { + /// The handle of a raster image. handle: image::Handle, + + /// The bounds of the image. bounds: Rectangle, }, + /// A vector image. Vector { + /// The handle of a vector image. handle: svg::Handle, + + /// The bounds of the image. bounds: Rectangle, }, } +#[allow(unsafe_code)] unsafe impl bytemuck::Zeroable for Quad {} + +#[allow(unsafe_code)] unsafe impl bytemuck::Pod for Quad {} diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs index 2de9d21c..b6dda132 100644 --- a/graphics/src/lib.rs +++ b/graphics/src/lib.rs @@ -1,3 +1,13 @@ +//! A bunch of backend-agnostic types that can be leveraged to build a renderer +//! for [`iced`]. +//! +//! [`iced`]: https://github.com/hecrj/iced +#![deny(missing_docs)] +#![deny(missing_debug_implementations)] +#![deny(unused_results)] +#![deny(unsafe_code)] +#![forbid(rust_2018_idioms)] +#![cfg_attr(docsrs, feature(doc_cfg))] mod antialiasing; mod defaults; mod primitive; diff --git a/graphics/src/renderer.rs b/graphics/src/renderer.rs index f16e04b1..c9360f3a 100644 --- a/graphics/src/renderer.rs +++ b/graphics/src/renderer.rs @@ -3,19 +3,33 @@ use iced_native::layout::{self, Layout}; use iced_native::mouse; use iced_native::{Background, Color, Element, Point, Widget}; -pub struct Renderer { +/// A backend-agnostic renderer that supports all the built-in widgets. +#[derive(Debug)] +pub struct Renderer { backend: B, } -impl Renderer { +impl Renderer { + /// Creates a new [`Renderer`] from the given [`Backend`]. + /// + /// [`Renderer`]: struct.Renderer.html + /// [`Backend`]: backend/trait.Backend.html pub fn new(backend: B) -> Self { Self { backend } } + /// Returns a reference to the [`Backend`] of the [`Renderer`]. + /// + /// [`Renderer`]: struct.Renderer.html + /// [`Backend`]: backend/trait.Backend.html pub fn backend(&self) -> &B { &self.backend } + /// Returns a mutable reference to the [`Backend`] of the [`Renderer`]. + /// + /// [`Renderer`]: struct.Renderer.html + /// [`Backend`]: backend/trait.Backend.html pub fn backend_mut(&mut self) -> &mut B { &mut self.backend } diff --git a/graphics/src/triangle.rs b/graphics/src/triangle.rs index 474f69b8..ce879ffc 100644 --- a/graphics/src/triangle.rs +++ b/graphics/src/triangle.rs @@ -1,3 +1,5 @@ +//! Draw geometry using meshes of triangles. + /// A set of [`Vertex2D`] and indices representing a list of triangles. /// /// [`Vertex2D`]: struct.Vertex2D.html @@ -23,5 +25,8 @@ pub struct Vertex2D { pub color: [f32; 4], } +#[allow(unsafe_code)] unsafe impl bytemuck::Zeroable for Vertex2D {} + +#[allow(unsafe_code)] unsafe impl bytemuck::Pod for Vertex2D {} diff --git a/graphics/src/viewport.rs b/graphics/src/viewport.rs index 745ef339..66122e6d 100644 --- a/graphics/src/viewport.rs +++ b/graphics/src/viewport.rs @@ -26,22 +26,44 @@ impl Viewport { } } + /// Returns the physical size of the [`Viewport`]. + /// + /// [`Viewport`]: struct.Viewport.html pub fn physical_size(&self) -> Size { self.physical_size } + /// Returns the physical width of the [`Viewport`]. + /// + /// [`Viewport`]: struct.Viewport.html + pub fn physical_width(&self) -> u32 { + self.physical_size.height + } + + /// Returns the physical height of the [`Viewport`]. + /// + /// [`Viewport`]: struct.Viewport.html pub fn physical_height(&self) -> u32 { self.physical_size.height } + /// Returns the logical size of the [`Viewport`]. + /// + /// [`Viewport`]: struct.Viewport.html pub fn logical_size(&self) -> Size { self.logical_size } + /// Returns the scale factor of the [`Viewport`]. + /// + /// [`Viewport`]: struct.Viewport.html pub fn scale_factor(&self) -> f64 { self.scale_factor } + /// Returns the projection transformation of the [`Viewport`]. + /// + /// [`Viewport`]: struct.Viewport.html pub fn projection(&self) -> Transformation { self.projection } diff --git a/graphics/src/widget/column.rs b/graphics/src/widget/column.rs index 9183d2ee..6c7235c7 100644 --- a/graphics/src/widget/column.rs +++ b/graphics/src/widget/column.rs @@ -3,6 +3,7 @@ use iced_native::column; use iced_native::mouse; use iced_native::{Element, Layout, Point}; +/// A container that distributes its contents vertically. pub type Column<'a, Message, Backend> = iced_native::Column<'a, Message, Renderer>; diff --git a/graphics/src/widget/image.rs b/graphics/src/widget/image.rs index ea49febe..30f446e8 100644 --- a/graphics/src/widget/image.rs +++ b/graphics/src/widget/image.rs @@ -1,3 +1,4 @@ +//! Display images in your user interface. use crate::backend::{self, Backend}; use crate::{Primitive, Renderer}; use iced_native::image; diff --git a/graphics/src/widget/row.rs b/graphics/src/widget/row.rs index 9865d0de..4c1dbadc 100644 --- a/graphics/src/widget/row.rs +++ b/graphics/src/widget/row.rs @@ -3,6 +3,7 @@ use iced_native::mouse; use iced_native::row; use iced_native::{Element, Layout, Point}; +/// A container that distributes its contents horizontally. pub type Row<'a, Message, Backend> = iced_native::Row<'a, Message, Renderer>; diff --git a/graphics/src/widget/svg.rs b/graphics/src/widget/svg.rs index 8c681478..8b5ed66a 100644 --- a/graphics/src/widget/svg.rs +++ b/graphics/src/widget/svg.rs @@ -1,3 +1,4 @@ +//! Display vector graphics in your application. use crate::backend::{self, Backend}; use crate::{Primitive, Renderer}; use iced_native::{mouse, svg, Layout}; diff --git a/graphics/src/window.rs b/graphics/src/window.rs index 380efb8c..3e74db5f 100644 --- a/graphics/src/window.rs +++ b/graphics/src/window.rs @@ -1,3 +1,4 @@ +//! Draw graphics to window surfaces. mod compositor; #[cfg(feature = "opengl")] diff --git a/graphics/src/window/gl_compositor.rs b/graphics/src/window/gl_compositor.rs index aea898e3..542213b5 100644 --- a/graphics/src/window/gl_compositor.rs +++ b/graphics/src/window/gl_compositor.rs @@ -3,19 +3,60 @@ use iced_native::mouse; use core::ffi::c_void; +/// A basic OpenGL compositor. +/// +/// A compositor is responsible for initializing a renderer and managing window +/// surfaces. +/// +/// For now, this compositor only deals with a single global surface +/// for drawing. However, the trait will most likely change in the near future +/// to handle multiple surfaces at once. +/// +/// If you implement an OpenGL renderer, you can implement this trait to ease +/// integration with existing windowing shells, like `iced_glutin`. pub trait GLCompositor: Sized { + /// The renderer of the [`Compositor`]. + /// + /// This should point to your renderer type, which could be a type alias + /// of the [`Renderer`] provided in this crate with with a specific + /// [`Backend`]. + /// + /// [`Compositor`]: trait.Compositor.html + /// [`Renderer`]: ../struct.Renderer.html + /// [`Backend`]: ../backend/trait.Backend.html type Renderer: iced_native::Renderer; + + /// The settings of the [`Compositor`]. + /// + /// It's up to you to decide the configuration supported by your renderer! type Settings: Default; + /// Creates a new [`Compositor`] and [`Renderer`] with the given + /// [`Settings`] and an OpenGL address loader function. + /// + /// [`Compositor`]: trait.Compositor.html + /// [`Renderer`]: #associatedtype.Renderer + /// [`Backend`]: ../backend/trait.Backend.html + #[allow(unsafe_code)] unsafe fn new( settings: Self::Settings, loader_function: impl FnMut(&str) -> *const c_void, ) -> (Self, Self::Renderer); + /// Returns the amount of samples that should be used when configuring + /// an OpenGL context for this [`Compositor`]. + /// + /// [`Compositor`]: trait.Compositor.html fn sample_count(settings: &Self::Settings) -> u32; + /// Resizes the viewport of the [`Compositor`]. + /// + /// [`Compositor`]: trait.Compositor.html fn resize_viewport(&mut self, physical_size: Size); + /// Draws the provided output with the given [`Renderer`]. + /// + /// [`Compositor`]: trait.Compositor.html fn draw>( &mut self, renderer: &mut Self::Renderer, -- cgit