summaryrefslogtreecommitdiffstats
path: root/glow/src/renderer
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--glow/src/renderer.rs455
-rw-r--r--glow/src/renderer/widget.rs19
-rw-r--r--glow/src/renderer/widget/button.rs93
-rw-r--r--glow/src/renderer/widget/checkbox.rs63
-rw-r--r--glow/src/renderer/widget/column.rs34
-rw-r--r--glow/src/renderer/widget/container.rs48
-rw-r--r--glow/src/renderer/widget/image.rs22
-rw-r--r--glow/src/renderer/widget/pane_grid.rs93
-rw-r--r--glow/src/renderer/widget/progress_bar.rs54
-rw-r--r--glow/src/renderer/widget/radio.rs63
-rw-r--r--glow/src/renderer/widget/row.rs34
-rw-r--r--glow/src/renderer/widget/scrollable.rs125
-rw-r--r--glow/src/renderer/widget/slider.rs106
-rw-r--r--glow/src/renderer/widget/space.rs8
-rw-r--r--glow/src/renderer/widget/svg.rs22
-rw-r--r--glow/src/renderer/widget/text.rs61
-rw-r--r--glow/src/renderer/widget/text_input.rs261
17 files changed, 1561 insertions, 0 deletions
diff --git a/glow/src/renderer.rs b/glow/src/renderer.rs
new file mode 100644
index 00000000..40228a6b
--- /dev/null
+++ b/glow/src/renderer.rs
@@ -0,0 +1,455 @@
+use crate::{
+ quad, text, triangle, Defaults, Primitive, Quad, Settings, Transformation,
+ Viewport,
+};
+
+use iced_native::{
+ layout, mouse, Background, Color, Layout, Point, Rectangle, Vector, Widget,
+};
+
+mod widget;
+
+/// A [`glow`] renderer.
+///
+/// [`glow`]: https://github.com/grovesNL/glow
+#[derive(Debug)]
+pub struct Renderer {
+ quad_pipeline: quad::Pipeline,
+ text_pipeline: text::Pipeline,
+ triangle_pipeline: triangle::Pipeline,
+}
+
+struct Layer<'a> {
+ bounds: Rectangle<u32>,
+ quads: Vec<Quad>,
+ text: Vec<glow_glyph::Section<'a>>,
+ meshes: Vec<(Vector, Rectangle<u32>, &'a triangle::Mesh2D)>,
+}
+
+impl<'a> Layer<'a> {
+ pub fn new(bounds: Rectangle<u32>) -> Self {
+ Self {
+ bounds,
+ quads: Vec::new(),
+ text: Vec::new(),
+ meshes: Vec::new(),
+ }
+ }
+
+ pub fn intersection(&self, rectangle: Rectangle) -> Option<Rectangle<u32>> {
+ let layer_bounds: Rectangle<f32> = self.bounds.into();
+
+ layer_bounds.intersection(&rectangle).map(Into::into)
+ }
+}
+
+impl Renderer {
+ /// Creates a new [`Renderer`].
+ ///
+ /// [`Renderer`]: struct.Renderer.html
+ pub fn new(gl: &glow::Context, settings: Settings) -> Self {
+ let text_pipeline = text::Pipeline::new(gl, settings.default_font);
+ let quad_pipeline = quad::Pipeline::new(gl);
+ let triangle_pipeline =
+ triangle::Pipeline::new(gl, settings.antialiasing);
+
+ Self {
+ quad_pipeline,
+ text_pipeline,
+ triangle_pipeline,
+ }
+ }
+
+ /// Draws the provided primitives in the given [`Target`].
+ ///
+ /// The text provided as overlay will be renderer on top of the primitives.
+ /// This is useful for rendering debug information.
+ ///
+ /// [`Target`]: struct.Target.html
+ pub fn draw<T: AsRef<str>>(
+ &mut self,
+ gl: &glow::Context,
+ viewport: &Viewport,
+ (primitive, mouse_interaction): &(Primitive, mouse::Interaction),
+ scale_factor: f64,
+ overlay: &[T],
+ ) -> mouse::Interaction {
+ let (width, height) = viewport.dimensions();
+ let scale_factor = scale_factor as f32;
+ let transformation = viewport.transformation();
+
+ let mut layers = Vec::new();
+
+ layers.push(Layer::new(Rectangle {
+ x: 0,
+ y: 0,
+ width: (width as f32 / scale_factor).round() as u32,
+ height: (height as f32 / scale_factor).round() as u32,
+ }));
+
+ self.draw_primitive(Vector::new(0.0, 0.0), primitive, &mut layers);
+ self.draw_overlay(overlay, &mut layers);
+
+ for layer in layers {
+ self.flush(
+ gl,
+ viewport,
+ scale_factor,
+ transformation,
+ &layer,
+ width,
+ height,
+ );
+ }
+
+ *mouse_interaction
+ }
+
+ fn draw_primitive<'a>(
+ &mut self,
+ translation: Vector,
+ primitive: &'a Primitive,
+ layers: &mut Vec<Layer<'a>>,
+ ) {
+ match primitive {
+ Primitive::None => {}
+ Primitive::Group { primitives } => {
+ // TODO: Inspect a bit and regroup (?)
+ for primitive in primitives {
+ self.draw_primitive(translation, primitive, layers)
+ }
+ }
+ Primitive::Text {
+ content,
+ bounds,
+ size,
+ color,
+ font,
+ horizontal_alignment,
+ vertical_alignment,
+ } => {
+ let layer = layers.last_mut().unwrap();
+
+ layer.text.push(glow_glyph::Section {
+ text: &content,
+ screen_position: (
+ bounds.x + translation.x,
+ bounds.y + translation.y,
+ ),
+ bounds: (bounds.width, bounds.height),
+ scale: glow_glyph::Scale { x: *size, y: *size },
+ color: color.into_linear(),
+ font_id: self.text_pipeline.find_font(*font),
+ layout: glow_glyph::Layout::default()
+ .h_align(match horizontal_alignment {
+ iced_native::HorizontalAlignment::Left => {
+ glow_glyph::HorizontalAlign::Left
+ }
+ iced_native::HorizontalAlignment::Center => {
+ glow_glyph::HorizontalAlign::Center
+ }
+ iced_native::HorizontalAlignment::Right => {
+ glow_glyph::HorizontalAlign::Right
+ }
+ })
+ .v_align(match vertical_alignment {
+ iced_native::VerticalAlignment::Top => {
+ glow_glyph::VerticalAlign::Top
+ }
+ iced_native::VerticalAlignment::Center => {
+ glow_glyph::VerticalAlign::Center
+ }
+ iced_native::VerticalAlignment::Bottom => {
+ glow_glyph::VerticalAlign::Bottom
+ }
+ }),
+ ..Default::default()
+ })
+ }
+ 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 { size, buffers } => {
+ let layer = layers.last_mut().unwrap();
+
+ // Only draw visible content
+ if let Some(clip_bounds) = layer.intersection(Rectangle::new(
+ Point::new(translation.x, translation.y),
+ *size,
+ )) {
+ layer.meshes.push((
+ translation,
+ clip_bounds.into(),
+ buffers,
+ ));
+ }
+ }
+ 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.draw_primitive(
+ translation
+ - Vector::new(offset.x as f32, offset.y as f32),
+ content,
+ layers,
+ );
+ layers.push(new_layer);
+ }
+ }
+ Primitive::Translate {
+ translation: new_translation,
+ content,
+ } => {
+ self.draw_primitive(
+ translation + *new_translation,
+ &content,
+ layers,
+ );
+ }
+
+ Primitive::Cached { cache } => {
+ self.draw_primitive(translation, &cache, layers);
+ }
+
+ #[cfg(feature = "image")]
+ Primitive::Image { handle, bounds } => {
+ let layer = layers.last_mut().unwrap();
+
+ layer.images.push(Image {
+ handle: image::Handle::Raster(handle.clone()),
+ position: [
+ bounds.x + translation.x,
+ bounds.y + translation.y,
+ ],
+ size: [bounds.width, bounds.height],
+ });
+ }
+ #[cfg(not(feature = "image"))]
+ Primitive::Image { .. } => {}
+
+ #[cfg(feature = "svg")]
+ Primitive::Svg { handle, bounds } => {
+ let layer = layers.last_mut().unwrap();
+
+ layer.images.push(Image {
+ handle: image::Handle::Vector(handle.clone()),
+ position: [
+ bounds.x + translation.x,
+ bounds.y + translation.y,
+ ],
+ size: [bounds.width, bounds.height],
+ });
+ }
+ #[cfg(not(feature = "svg"))]
+ Primitive::Svg { .. } => {}
+ }
+ }
+
+ fn draw_overlay<'a, T: AsRef<str>>(
+ &mut self,
+ lines: &'a [T],
+ layers: &mut Vec<Layer<'a>>,
+ ) {
+ let first = layers.first().unwrap();
+ let mut overlay = Layer::new(first.bounds);
+
+ let font_id = self.text_pipeline.overlay_font();
+ let scale = glow_glyph::Scale { x: 20.0, y: 20.0 };
+
+ for (i, line) in lines.iter().enumerate() {
+ overlay.text.push(glow_glyph::Section {
+ text: line.as_ref(),
+ screen_position: (11.0, 11.0 + 25.0 * i as f32),
+ color: [0.9, 0.9, 0.9, 1.0],
+ scale,
+ font_id,
+ ..glow_glyph::Section::default()
+ });
+
+ overlay.text.push(glow_glyph::Section {
+ text: line.as_ref(),
+ screen_position: (10.0, 10.0 + 25.0 * i as f32),
+ color: [0.0, 0.0, 0.0, 1.0],
+ scale,
+ font_id,
+ ..glow_glyph::Section::default()
+ });
+ }
+
+ layers.push(overlay);
+ }
+
+ fn flush(
+ &mut self,
+ gl: &glow::Context,
+ viewport: &Viewport,
+ scale_factor: f32,
+ transformation: Transformation,
+ layer: &Layer<'_>,
+ target_width: u32,
+ target_height: u32,
+ ) {
+ let bounds = layer.bounds * scale_factor;
+
+ if !layer.quads.is_empty() {
+ self.quad_pipeline.draw(
+ gl,
+ viewport,
+ &layer.quads,
+ transformation,
+ scale_factor,
+ bounds,
+ );
+ }
+
+ if !layer.meshes.is_empty() {
+ let scaled = transformation
+ * Transformation::scale(scale_factor, scale_factor);
+
+ self.triangle_pipeline.draw(
+ gl,
+ target_width,
+ target_height,
+ scaled,
+ scale_factor,
+ &layer.meshes,
+ );
+ }
+
+ if !layer.text.is_empty() {
+ for text in layer.text.iter() {
+ // Target physical coordinates directly to avoid blurry text
+ let text = glow_glyph::Section {
+ // TODO: We `round` here to avoid rerasterizing text when
+ // its position changes slightly. This can make text feel a
+ // bit "jumpy". We may be able to do better once we improve
+ // our text rendering/caching pipeline.
+ screen_position: (
+ (text.screen_position.0 * scale_factor).round(),
+ (text.screen_position.1 * scale_factor).round(),
+ ),
+ // TODO: Fix precision issues with some scale factors.
+ //
+ // The `ceil` here can cause some words to render on the
+ // same line when they should not.
+ //
+ // Ideally, `wgpu_glyph` should be able to compute layout
+ // using logical positions, and then apply the proper
+ // scaling when rendering. This would ensure that both
+ // measuring and rendering follow the same layout rules.
+ bounds: (
+ (text.bounds.0 * scale_factor).ceil(),
+ (text.bounds.1 * scale_factor).ceil(),
+ ),
+ scale: glow_glyph::Scale {
+ x: text.scale.x * scale_factor,
+ y: text.scale.y * scale_factor,
+ },
+ ..*text
+ };
+
+ self.text_pipeline.queue(text);
+ }
+
+ self.text_pipeline.draw_queued(
+ gl,
+ transformation,
+ glow_glyph::Region {
+ x: bounds.x,
+ y: viewport.height()
+ - (bounds.y + bounds.height).min(viewport.height()),
+ width: bounds.width,
+ height: bounds.height,
+ },
+ );
+ }
+ }
+}
+
+impl iced_native::Renderer for Renderer {
+ type Output = (Primitive, mouse::Interaction);
+ type Defaults = Defaults;
+
+ fn layout<'a, Message>(
+ &mut self,
+ element: &iced_native::Element<'a, Message, Self>,
+ limits: &iced_native::layout::Limits,
+ ) -> iced_native::layout::Node {
+ let node = element.layout(self, limits);
+
+ self.text_pipeline.clear_measurement_cache();
+
+ node
+ }
+}
+
+impl layout::Debugger for Renderer {
+ fn explain<Message>(
+ &mut self,
+ defaults: &Defaults,
+ widget: &dyn Widget<Message, Self>,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ color: Color,
+ ) -> Self::Output {
+ let mut primitives = Vec::new();
+ let (primitive, cursor) =
+ widget.draw(self, defaults, layout, cursor_position);
+
+ explain_layout(layout, color, &mut primitives);
+ primitives.push(primitive);
+
+ (Primitive::Group { primitives }, cursor)
+ }
+}
+
+fn explain_layout(
+ layout: Layout<'_>,
+ color: Color,
+ primitives: &mut Vec<Primitive>,
+) {
+ 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/glow/src/renderer/widget.rs b/glow/src/renderer/widget.rs
new file mode 100644
index 00000000..37421fbe
--- /dev/null
+++ b/glow/src/renderer/widget.rs
@@ -0,0 +1,19 @@
+mod button;
+mod checkbox;
+mod column;
+mod container;
+mod pane_grid;
+mod progress_bar;
+mod radio;
+mod row;
+mod scrollable;
+mod slider;
+mod space;
+mod text;
+mod text_input;
+
+#[cfg(feature = "svg")]
+mod svg;
+
+#[cfg(feature = "image")]
+mod image;
diff --git a/glow/src/renderer/widget/button.rs b/glow/src/renderer/widget/button.rs
new file mode 100644
index 00000000..eb225038
--- /dev/null
+++ b/glow/src/renderer/widget/button.rs
@@ -0,0 +1,93 @@
+use crate::{button::StyleSheet, defaults, Defaults, Primitive, Renderer};
+use iced_native::{
+ mouse, Background, Color, Element, Layout, Point, Rectangle, Vector,
+};
+
+impl iced_native::button::Renderer for Renderer {
+ const DEFAULT_PADDING: u16 = 5;
+
+ type Style = Box<dyn StyleSheet>;
+
+ fn draw<Message>(
+ &mut self,
+ _defaults: &Defaults,
+ bounds: Rectangle,
+ cursor_position: Point,
+ is_disabled: bool,
+ is_pressed: bool,
+ style: &Box<dyn StyleSheet>,
+ 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/glow/src/renderer/widget/checkbox.rs b/glow/src/renderer/widget/checkbox.rs
new file mode 100644
index 00000000..0340bf62
--- /dev/null
+++ b/glow/src/renderer/widget/checkbox.rs
@@ -0,0 +1,63 @@
+use crate::{checkbox::StyleSheet, Primitive, Renderer};
+use iced_native::{
+ checkbox, mouse, HorizontalAlignment, Rectangle, VerticalAlignment,
+};
+
+impl checkbox::Renderer for Renderer {
+ type Style = Box<dyn StyleSheet>;
+
+ 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: crate::text::CHECKMARK_ICON.to_string(),
+ font: crate::text::BUILTIN_ICONS,
+ 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/glow/src/renderer/widget/column.rs b/glow/src/renderer/widget/column.rs
new file mode 100644
index 00000000..b853276d
--- /dev/null
+++ b/glow/src/renderer/widget/column.rs
@@ -0,0 +1,34 @@
+use crate::{Primitive, Renderer};
+use iced_native::{column, mouse, Element, Layout, Point};
+
+impl column::Renderer for Renderer {
+ fn draw<Message>(
+ &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/glow/src/renderer/widget/container.rs b/glow/src/renderer/widget/container.rs
new file mode 100644
index 00000000..30cc3f07
--- /dev/null
+++ b/glow/src/renderer/widget/container.rs
@@ -0,0 +1,48 @@
+use crate::{container, defaults, Defaults, Primitive, Renderer};
+use iced_native::{Background, Color, Element, Layout, Point, Rectangle};
+
+impl iced_native::container::Renderer for Renderer {
+ type Style = Box<dyn container::StyleSheet>;
+
+ fn draw<Message>(
+ &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/glow/src/renderer/widget/image.rs b/glow/src/renderer/widget/image.rs
new file mode 100644
index 00000000..c4c04984
--- /dev/null
+++ b/glow/src/renderer/widget/image.rs
@@ -0,0 +1,22 @@
+use crate::{Primitive, Renderer};
+use iced_native::{image, mouse, Layout};
+
+impl image::Renderer for Renderer {
+ fn dimensions(&self, handle: &image::Handle) -> (u32, u32) {
+ self.image_pipeline.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/glow/src/renderer/widget/pane_grid.rs b/glow/src/renderer/widget/pane_grid.rs
new file mode 100644
index 00000000..2253e4af
--- /dev/null
+++ b/glow/src/renderer/widget/pane_grid.rs
@@ -0,0 +1,93 @@
+use crate::{Primitive, Renderer};
+use iced_native::{
+ mouse,
+ pane_grid::{self, Axis, Pane},
+ Element, Layout, Point, Rectangle, Vector,
+};
+
+impl pane_grid::Renderer for Renderer {
+ fn draw<Message>(
+ &mut self,
+ defaults: &Self::Defaults,
+ content: &[(Pane, Element<'_, Message, Self>)],
+ dragging: Option<Pane>,
+ resizing: Option<Axis>,
+ 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/glow/src/renderer/widget/progress_bar.rs b/glow/src/renderer/widget/progress_bar.rs
new file mode 100644
index 00000000..2baeeb14
--- /dev/null
+++ b/glow/src/renderer/widget/progress_bar.rs
@@ -0,0 +1,54 @@
+use crate::{progress_bar::StyleSheet, Primitive, Renderer};
+use iced_native::{mouse, progress_bar, Color, Rectangle};
+
+impl progress_bar::Renderer for Renderer {
+ type Style = Box<dyn StyleSheet>;
+
+ const DEFAULT_HEIGHT: u16 = 30;
+
+ fn draw(
+ &self,
+ bounds: Rectangle,
+ range: std::ops::RangeInclusive<f32>,
+ 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/glow/src/renderer/widget/radio.rs b/glow/src/renderer/widget/radio.rs
new file mode 100644
index 00000000..cee0deb6
--- /dev/null
+++ b/glow/src/renderer/widget/radio.rs
@@ -0,0 +1,63 @@
+use crate::{radio::StyleSheet, 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 {
+ type Style = Box<dyn StyleSheet>;
+
+ 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/glow/src/renderer/widget/row.rs b/glow/src/renderer/widget/row.rs
new file mode 100644
index 00000000..d0b7ef09
--- /dev/null
+++ b/glow/src/renderer/widget/row.rs
@@ -0,0 +1,34 @@
+use crate::{Primitive, Renderer};
+use iced_native::{mouse, row, Element, Layout, Point};
+
+impl row::Renderer for Renderer {
+ fn draw<Message>(
+ &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/glow/src/renderer/widget/scrollable.rs b/glow/src/renderer/widget/scrollable.rs
new file mode 100644
index 00000000..8a400b82
--- /dev/null
+++ b/glow/src/renderer/widget/scrollable.rs
@@ -0,0 +1,125 @@
+use crate::{Primitive, Renderer};
+use iced_native::{mouse, scrollable, Background, Color, Rectangle, Vector};
+
+const SCROLLBAR_WIDTH: u16 = 10;
+const SCROLLBAR_MARGIN: u16 = 2;
+
+impl scrollable::Renderer for Renderer {
+ type Style = Box<dyn iced_style::scrollable::StyleSheet>;
+
+ fn scrollbar(
+ &self,
+ bounds: Rectangle,
+ content_bounds: Rectangle,
+ offset: u32,
+ ) -> Option<scrollable::Scrollbar> {
+ 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<scrollable::Scrollbar>,
+ 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/glow/src/renderer/widget/slider.rs b/glow/src/renderer/widget/slider.rs
new file mode 100644
index 00000000..220feace
--- /dev/null
+++ b/glow/src/renderer/widget/slider.rs
@@ -0,0 +1,106 @@
+use crate::{
+ slider::{HandleShape, StyleSheet},
+ Primitive, Renderer,
+};
+use iced_native::{mouse, slider, Background, Color, Point, Rectangle};
+
+const HANDLE_HEIGHT: f32 = 22.0;
+
+impl slider::Renderer for Renderer {
+ type Style = Box<dyn StyleSheet>;
+
+ fn height(&self) -> u32 {
+ 30
+ }
+
+ fn draw(
+ &mut self,
+ bounds: Rectangle,
+ cursor_position: Point,
+ range: std::ops::RangeInclusive<f32>,
+ 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/glow/src/renderer/widget/space.rs b/glow/src/renderer/widget/space.rs
new file mode 100644
index 00000000..225f7e6c
--- /dev/null
+++ b/glow/src/renderer/widget/space.rs
@@ -0,0 +1,8 @@
+use crate::{Primitive, Renderer};
+use iced_native::{mouse, space, Rectangle};
+
+impl space::Renderer for Renderer {
+ fn draw(&mut self, _bounds: Rectangle) -> Self::Output {
+ (Primitive::None, mouse::Interaction::default())
+ }
+}
diff --git a/glow/src/renderer/widget/svg.rs b/glow/src/renderer/widget/svg.rs
new file mode 100644
index 00000000..f6d6d0ba
--- /dev/null
+++ b/glow/src/renderer/widget/svg.rs
@@ -0,0 +1,22 @@
+use crate::{Primitive, Renderer};
+use iced_native::{mouse, svg, Layout};
+
+impl svg::Renderer for Renderer {
+ fn dimensions(&self, handle: &svg::Handle) -> (u32, u32) {
+ self.image_pipeline.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/glow/src/renderer/widget/text.rs b/glow/src/renderer/widget/text.rs
new file mode 100644
index 00000000..4605ed06
--- /dev/null
+++ b/glow/src/renderer/widget/text.rs
@@ -0,0 +1,61 @@
+use crate::{Primitive, Renderer};
+use iced_native::{
+ mouse, text, Color, Font, HorizontalAlignment, Rectangle, Size,
+ VerticalAlignment,
+};
+
+use std::f32;
+
+impl text::Renderer for Renderer {
+ type Font = Font;
+
+ const DEFAULT_SIZE: u16 = 20;
+
+ fn measure(
+ &self,
+ content: &str,
+ size: u16,
+ font: Font,
+ bounds: Size,
+ ) -> (f32, f32) {
+ self.text_pipeline
+ .measure(content, f32::from(size), font, bounds)
+ }
+
+ fn draw(
+ &mut self,
+ defaults: &Self::Defaults,
+ bounds: Rectangle,
+ content: &str,
+ size: u16,
+ font: Font,
+ color: Option<Color>,
+ 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/glow/src/renderer/widget/text_input.rs b/glow/src/renderer/widget/text_input.rs
new file mode 100644
index 00000000..57be6692
--- /dev/null
+++ b/glow/src/renderer/widget/text_input.rs
@@ -0,0 +1,261 @@
+use crate::{text_input::StyleSheet, 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 {
+ type Style = Box<dyn StyleSheet>;
+
+ fn default_size(&self) -> u16 {
+ // TODO: Make this configurable
+ 20
+ }
+
+ fn measure_value(&self, value: &str, size: u16, font: Font) -> f32 {
+ let (mut width, _) = self.text_pipeline.measure(
+ value,
+ f32::from(size),
+ font,
+ Size::INFINITY,
+ );
+
+ let spaces_around = value.len() - value.trim().len();
+
+ if spaces_around > 0 {
+ let space_width = self.text_pipeline.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) {
+ 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)
+}