From 1317488e9c11a13c1d311b63512208865ddf1e1c Mon Sep 17 00:00:00 2001 From: Marc Espin Date: Sun, 6 Oct 2024 10:32:00 +0200 Subject: [PATCH] refactor: Split core render functions (#937) * refactor: Split core render functions * lint --- crates/core/src/elements/label.rs | 6 +- crates/core/src/elements/paragraph.rs | 86 +----- crates/core/src/elements/rect.rs | 348 ++-------------------- crates/core/src/render/mod.rs | 2 + crates/core/src/render/skia_measurer.rs | 160 +--------- crates/core/src/render/utils/borders.rs | 237 +++++++++++++++ crates/core/src/render/utils/label.rs | 50 ++++ crates/core/src/render/utils/mod.rs | 9 + crates/core/src/render/utils/paragraph.rs | 204 +++++++++++++ crates/core/src/render/utils/shadows.rs | 77 +++++ crates/state/src/values/fill.rs | 25 +- 11 files changed, 638 insertions(+), 566 deletions(-) create mode 100644 crates/core/src/render/utils/borders.rs create mode 100644 crates/core/src/render/utils/label.rs create mode 100644 crates/core/src/render/utils/mod.rs create mode 100644 crates/core/src/render/utils/paragraph.rs create mode 100644 crates/core/src/render/utils/shadows.rs diff --git a/crates/core/src/elements/label.rs b/crates/core/src/elements/label.rs index 0c66114d2..186cf107d 100644 --- a/crates/core/src/elements/label.rs +++ b/crates/core/src/elements/label.rs @@ -11,9 +11,9 @@ use torin::prelude::{ }; use super::utils::ElementUtils; -use crate::prelude::{ - align_main_align_paragraph, - DioxusNode, +use crate::{ + prelude::DioxusNode, + render::align_main_align_paragraph, }; pub struct LabelElement; diff --git a/crates/core/src/elements/paragraph.rs b/crates/core/src/elements/paragraph.rs index 2b595aa21..05effff7e 100644 --- a/crates/core/src/elements/paragraph.rs +++ b/crates/core/src/elements/paragraph.rs @@ -31,12 +31,13 @@ use torin::{ use super::utils::ElementUtils; use crate::{ dom::DioxusNode, - prelude::{ - align_highlights_and_cursor_paragraph, + prelude::TextGroupMeasurement, + render::{ align_main_align_paragraph, - TextGroupMeasurement, + create_paragraph, + draw_cursor, + draw_cursor_highlights, }, - render::create_paragraph, }; pub struct ParagraphElement; @@ -230,80 +231,3 @@ impl ElementUtils for ParagraphElement { area } } - -fn draw_cursor_highlights( - area: &Area, - paragraph: &Paragraph, - canvas: &Canvas, - node_ref: &DioxusNode, -) -> Option<()> { - let node_cursor_state = &*node_ref.get::().unwrap(); - - let highlights = node_cursor_state.highlights.as_ref()?; - let highlight_color = node_cursor_state.highlight_color; - - for (from, to) in highlights.iter() { - let (from, to) = { - if from < to { - (from, to) - } else { - (to, from) - } - }; - let cursor_rects = paragraph.get_rects_for_range( - *from..*to, - RectHeightStyle::Tight, - RectWidthStyle::Tight, - ); - for cursor_rect in cursor_rects { - let rect = align_highlights_and_cursor_paragraph( - node_ref, - area, - paragraph, - &cursor_rect, - None, - ); - - let mut paint = Paint::default(); - paint.set_anti_alias(true); - paint.set_style(PaintStyle::Fill); - paint.set_color(highlight_color); - - canvas.draw_rect(rect, &paint); - } - } - - Some(()) -} - -fn draw_cursor( - area: &Area, - paragraph: &Paragraph, - canvas: &Canvas, - node_ref: &DioxusNode, -) -> Option<()> { - let node_cursor_state = &*node_ref.get::().unwrap(); - - let cursor = node_cursor_state.position?; - let cursor_color = node_cursor_state.color; - let cursor_position = cursor as usize; - - let cursor_rects = paragraph.get_rects_for_range( - cursor_position..cursor_position + 1, - RectHeightStyle::Tight, - RectWidthStyle::Tight, - ); - let cursor_rect = cursor_rects.first()?; - - let rect = - align_highlights_and_cursor_paragraph(node_ref, area, paragraph, cursor_rect, Some(1.0)); - - let mut paint = Paint::default(); - paint.set_anti_alias(true); - paint.set_style(PaintStyle::Fill); - paint.set_color(cursor_color); - - canvas.draw_rect(rect, &paint); - - Some(()) -} diff --git a/crates/core/src/elements/rect.rs b/crates/core/src/elements/rect.rs index 55bc235c0..b033ee415 100644 --- a/crates/core/src/elements/rect.rs +++ b/crates/core/src/elements/rect.rs @@ -1,10 +1,7 @@ use freya_engine::prelude::*; use freya_native_core::real_dom::NodeImmutable; use freya_node_state::{ - Border, - BorderAlignment, CanvasRunnerContext, - CornerRadius, Fill, ReferencesState, ShadowPosition, @@ -22,12 +19,15 @@ use torin::{ }; use super::utils::ElementUtils; -use crate::dom::DioxusNode; - -enum BorderShape { - DRRect(RRect, RRect), - Path(Path), -} +use crate::{ + dom::DioxusNode, + render::{ + border_shape, + render_border, + render_shadow, + BorderShape, + }, +}; pub struct RectElement; @@ -53,207 +53,6 @@ impl RectElement { ], ) } - - fn outer_border_path_corner_radius( - alignment: BorderAlignment, - corner_radius: f32, - width_1: f32, - width_2: f32, - ) -> f32 { - if alignment == BorderAlignment::Inner || corner_radius == 0.0 { - return corner_radius; - } - - let mut offset = if width_1 == 0.0 { - width_2 - } else if width_2 == 0.0 { - width_1 - } else { - width_1.min(width_2) - }; - - if alignment == BorderAlignment::Center { - offset *= 0.5; - } - - corner_radius + offset - } - - fn inner_border_path_corner_radius( - alignment: BorderAlignment, - corner_radius: f32, - width_1: f32, - width_2: f32, - ) -> f32 { - if alignment == BorderAlignment::Outer || corner_radius == 0.0 { - return corner_radius; - } - - let mut offset = if width_1 == 0.0 { - width_2 - } else if width_2 == 0.0 { - width_1 - } else { - width_1.min(width_2) - }; - - if alignment == BorderAlignment::Center { - offset *= 0.5; - } - - corner_radius - offset - } - - /// Returns a `Path` that will draw a [`Border`] around a base rectangle. - /// - /// We don't use Skia's stroking API here, since we might need different widths for each side. - fn border_shape( - base_rect: Rect, - base_corner_radius: CornerRadius, - border: &Border, - ) -> BorderShape { - let border_alignment = border.alignment; - let border_width = border.width; - - // First we create a path that is outset from the rect by a certain amount on each side. - // - // Let's call this the outer border path. - let (outer_rrect, outer_corner_radius) = { - // Calculuate the outer corner radius for the border. - let corner_radius = CornerRadius { - top_left: Self::outer_border_path_corner_radius( - border_alignment, - base_corner_radius.top_left, - border_width.top, - border_width.left, - ), - top_right: Self::outer_border_path_corner_radius( - border_alignment, - base_corner_radius.top_right, - border_width.top, - border_width.right, - ), - bottom_left: Self::outer_border_path_corner_radius( - border_alignment, - base_corner_radius.bottom_left, - border_width.bottom, - border_width.left, - ), - bottom_right: Self::outer_border_path_corner_radius( - border_alignment, - base_corner_radius.bottom_right, - border_width.bottom, - border_width.right, - ), - smoothing: base_corner_radius.smoothing, - }; - - let rrect = RRect::new_rect_radii( - { - let mut rect = base_rect; - let alignment_scale = match border_alignment { - BorderAlignment::Outer => 1.0, - BorderAlignment::Center => 0.5, - BorderAlignment::Inner => 0.0, - }; - - rect.left -= border_width.left * alignment_scale; - rect.top -= border_width.top * alignment_scale; - rect.right += border_width.right * alignment_scale; - rect.bottom += border_width.bottom * alignment_scale; - - rect - }, - &[ - (corner_radius.top_left, corner_radius.top_left).into(), - (corner_radius.top_right, corner_radius.top_right).into(), - (corner_radius.bottom_right, corner_radius.bottom_right).into(), - (corner_radius.bottom_left, corner_radius.bottom_left).into(), - ], - ); - - (rrect, corner_radius) - }; - - // After the outer path, we will then move to the inner bounds of the border. - let (inner_rrect, inner_corner_radius) = { - // Calculuate the inner corner radius for the border. - let corner_radius = CornerRadius { - top_left: Self::inner_border_path_corner_radius( - border_alignment, - base_corner_radius.top_left, - border_width.top, - border_width.left, - ), - top_right: Self::inner_border_path_corner_radius( - border_alignment, - base_corner_radius.top_right, - border_width.top, - border_width.right, - ), - bottom_left: Self::inner_border_path_corner_radius( - border_alignment, - base_corner_radius.bottom_left, - border_width.bottom, - border_width.left, - ), - bottom_right: Self::inner_border_path_corner_radius( - border_alignment, - base_corner_radius.bottom_right, - border_width.bottom, - border_width.right, - ), - smoothing: base_corner_radius.smoothing, - }; - - let rrect = RRect::new_rect_radii( - { - let mut rect = base_rect; - let alignment_scale = match border_alignment { - BorderAlignment::Outer => 0.0, - BorderAlignment::Center => 0.5, - BorderAlignment::Inner => 1.0, - }; - - rect.left += border_width.left * alignment_scale; - rect.top += border_width.top * alignment_scale; - rect.right -= border_width.right * alignment_scale; - rect.bottom -= border_width.bottom * alignment_scale; - - rect - }, - &[ - (corner_radius.top_left, corner_radius.top_left).into(), - (corner_radius.top_right, corner_radius.top_right).into(), - (corner_radius.bottom_right, corner_radius.bottom_right).into(), - (corner_radius.bottom_left, corner_radius.bottom_left).into(), - ], - ); - - (rrect, corner_radius) - }; - - if base_corner_radius.smoothing > 0.0 { - let mut path = Path::new(); - path.set_fill_type(PathFillType::EvenOdd); - - path.add_path( - &outer_corner_radius.smoothed_path(outer_rrect), - Point::new(outer_rrect.rect().x(), outer_rrect.rect().y()), - None, - ); - - path.add_path( - &inner_corner_radius.smoothed_path(inner_rrect), - Point::new(inner_rrect.rect().x(), inner_rrect.rect().y()), - None, - ); - - BorderShape::Path(path) - } else { - BorderShape::DRRect(outer_rrect, inner_rrect) - } - } } impl ElementUtils for RectElement { @@ -299,24 +98,12 @@ impl ElementUtils for RectElement { paint.set_anti_alias(true); paint.set_style(PaintStyle::Fill); - match &node_style.background { - Fill::Color(color) => { - paint.set_color(*color); - } - Fill::LinearGradient(gradient) => { - paint.set_shader(gradient.into_shader(area)); - } - Fill::RadialGradient(gradient) => { - paint.set_shader(gradient.into_shader(area)); - } - Fill::ConicGradient(gradient) => { - paint.set_shader(gradient.into_shader(area)); - } - } + node_style.background.apply_to_paint(&mut paint, area); let mut corner_radius = node_style.corner_radius; corner_radius.scale(scale_factor); + // Container let rounded_rect = RRect::new_rect_radii( Rect::new(area.min_x(), area.min_y(), area.max_x(), area.max_y()), &[ @@ -326,7 +113,6 @@ impl ElementUtils for RectElement { (corner_radius.bottom_left, corner_radius.bottom_left).into(), ], ); - if corner_radius.smoothing > 0.0 { path.add_path( &corner_radius.smoothed_path(rounded_rect), @@ -336,7 +122,6 @@ impl ElementUtils for RectElement { } else { path.add_rrect(rounded_rect, None); } - canvas.draw_path(&path, &paint); // Shadows @@ -344,77 +129,15 @@ impl ElementUtils for RectElement { if shadow.fill != Fill::Color(Color::TRANSPARENT) { shadow.scale(scale_factor); - let mut shadow_path = Path::new(); - let mut shadow_paint = Paint::default(); - shadow_paint.set_anti_alias(true); - - match &shadow.fill { - Fill::Color(color) => { - shadow_paint.set_color(*color); - } - Fill::LinearGradient(gradient) => { - shadow_paint.set_shader(gradient.into_shader(area)); - } - Fill::RadialGradient(gradient) => { - shadow_paint.set_shader(gradient.into_shader(area)); - } - Fill::ConicGradient(gradient) => { - shadow_paint.set_shader(gradient.into_shader(area)); - } - } - - // Shadows can be either outset or inset - // If they are outset, we fill a copy of the path outset by spread_radius, and blur it. - // Otherwise, we draw a stroke with the inner portion being spread_radius width, and the outer portion being blur_radius width. - let outset: Point = match shadow.position { - ShadowPosition::Normal => { - shadow_paint.set_style(PaintStyle::Fill); - (shadow.spread, shadow.spread).into() - } - ShadowPosition::Inset => { - shadow_paint.set_style(PaintStyle::Stroke); - shadow_paint.set_stroke_width(shadow.blur / 2.0 + shadow.spread); - (-shadow.spread / 2.0, -shadow.spread / 2.0).into() - } - }; - - // Apply gassuan blur to the copied path. - if shadow.blur > 0.0 { - shadow_paint.set_mask_filter(MaskFilter::blur( - BlurStyle::Normal, - shadow.blur / 2.0, - false, - )); - } - - // Add either the RRect or smoothed path based on whether smoothing is used. - if corner_radius.smoothing > 0.0 { - shadow_path.add_path( - &node_style - .corner_radius - .smoothed_path(rounded_rect.with_outset(outset)), - Point::new(area.min_x(), area.min_y()) - outset, - None, - ); - } else { - shadow_path.add_rrect(rounded_rect.with_outset(outset), None); - } - - // Offset our path by the shadow's x and y coordinates. - shadow_path.offset((shadow.x, shadow.y)); - - // Exclude the original path bounds from the shadow using a clip, then draw the shadow. - canvas.save(); - canvas.clip_path( - &path, - match shadow.position { - ShadowPosition::Normal => ClipOp::Difference, - ShadowPosition::Inset => ClipOp::Intersect, - }, - true, + render_shadow( + canvas, + node_style, + &mut path, + rounded_rect, + area, + shadow, + corner_radius, ); - canvas.draw_path(&shadow_path, &shadow_paint); - canvas.restore(); } } @@ -423,39 +146,12 @@ impl ElementUtils for RectElement { if border.is_visible() { border.scale(scale_factor); - // Create a new paint - let mut border_paint = Paint::default(); - border_paint.set_style(PaintStyle::Fill); - border_paint.set_anti_alias(true); - - match &border.fill { - Fill::Color(color) => { - border_paint.set_color(*color); - } - Fill::LinearGradient(gradient) => { - border_paint.set_shader(gradient.into_shader(area)); - } - Fill::RadialGradient(gradient) => { - border_paint.set_shader(gradient.into_shader(area)); - } - Fill::ConicGradient(gradient) => { - border_paint.set_shader(gradient.into_shader(area)); - } - } - - match Self::border_shape(*rounded_rect.rect(), corner_radius, &border) { - BorderShape::DRRect(outer, inner) => { - canvas.draw_drrect(outer, inner, &border_paint); - } - BorderShape::Path(path) => { - canvas.draw_path(&path, &border_paint); - } - } + render_border(canvas, rounded_rect, area, &border, corner_radius); } } + // Layout references let references = node_ref.get::().unwrap(); - if let Some(canvas_ref) = &references.canvas_ref { let mut ctx = CanvasRunnerContext { canvas, @@ -561,7 +257,7 @@ impl ElementUtils for RectElement { border.scale(scale_factor); let border_shape = - Self::border_shape(*rounded_rect.rect(), node_style.corner_radius, &border); + border_shape(*rounded_rect.rect(), node_style.corner_radius, &border); let border_bounds = match border_shape { BorderShape::DRRect(ref outer, _) => outer.bounds(), BorderShape::Path(ref path) => path.bounds(), diff --git a/crates/core/src/render/mod.rs b/crates/core/src/render/mod.rs index 81a0379a0..35d6d787c 100644 --- a/crates/core/src/render/mod.rs +++ b/crates/core/src/render/mod.rs @@ -1,8 +1,10 @@ pub mod compositor; pub mod pipeline; pub mod skia_measurer; +pub mod utils; mod wireframe_renderer; pub use compositor::*; pub use pipeline::*; pub use skia_measurer::*; +pub use utils::*; diff --git a/crates/core/src/render/skia_measurer.rs b/crates/core/src/render/skia_measurer.rs index 4eaaef5b0..b3a03a207 100644 --- a/crates/core/src/render/skia_measurer.rs +++ b/crates/core/src/render/skia_measurer.rs @@ -15,20 +15,18 @@ use freya_native_core::{ tags::TagName, NodeId, }; -use freya_node_state::{ - CursorState, - FontStyleState, - HighlightMode, - LayoutState, -}; +use freya_node_state::LayoutState; use torin::prelude::{ - Alignment, Area, LayoutMeasurer, Node, Size2D, }; +use super::{ + create_label, + create_paragraph, +}; use crate::dom::*; /// Provides Text measurements using Skia APIs like SkParagraph @@ -123,151 +121,3 @@ impl<'a> LayoutMeasurer for SkiaMeasurer<'a> { } } } - -pub fn create_label( - node: &DioxusNode, - area_size: &Size2D, - font_collection: &FontCollection, - default_font_family: &[String], - scale_factor: f32, -) -> Paragraph { - let font_style = &*node.get::().unwrap(); - - let mut paragraph_style = ParagraphStyle::default(); - paragraph_style.set_text_align(font_style.text_align); - paragraph_style.set_max_lines(font_style.max_lines); - paragraph_style.set_replace_tab_characters(true); - - if let Some(ellipsis) = font_style.text_overflow.get_ellipsis() { - paragraph_style.set_ellipsis(ellipsis); - } - - let text_style = font_style.text_style(default_font_family, scale_factor); - paragraph_style.set_text_style(&text_style); - - let mut paragraph_builder = ParagraphBuilder::new(¶graph_style, font_collection); - - for child in node.children() { - if let NodeType::Text(text) = &*child.node_type() { - paragraph_builder.add_text(text); - } - } - - let mut paragraph = paragraph_builder.build(); - paragraph.layout( - if font_style.max_lines == Some(1) && font_style.text_align == TextAlign::default() { - f32::MAX - } else { - area_size.width + 1.0 - }, - ); - - paragraph -} - -/// Align the Y axis of the highlights and cursor of a paragraph -pub fn align_highlights_and_cursor_paragraph( - node: &DioxusNode, - area: &Area, - paragraph: &Paragraph, - cursor_rect: &TextBox, - width: Option, -) -> Rect { - let cursor_state = node.get::().unwrap(); - - let left = area.min_x() + cursor_rect.rect.left; - let right = left + width.unwrap_or(cursor_rect.rect.right - cursor_rect.rect.left); - - match cursor_state.highlight_mode { - HighlightMode::Fit => { - let top = (area.min_y() - + align_main_align_paragraph(node, area, paragraph) - + cursor_rect.rect.top) - .clamp(area.min_y(), area.max_y()); - let bottom = (top + (cursor_rect.rect.bottom - cursor_rect.rect.top)) - .clamp(area.min_y(), area.max_y()); - - Rect::new(left, top, right, bottom) - } - HighlightMode::Expanded => { - let top = area.min_y(); - let bottom = area.max_y(); - - Rect::new(left, top, right, bottom) - } - } -} - -/// Align the main alignment of a paragraph -pub fn align_main_align_paragraph(node: &DioxusNode, area: &Area, paragraph: &Paragraph) -> f32 { - let layout = node.get::().unwrap(); - - match layout.main_alignment { - Alignment::Start => 0., - Alignment::Center => (area.height() / 2.0) - (paragraph.height() / 2.0), - Alignment::End => area.height() - paragraph.height(), - Alignment::SpaceBetween => 0., - Alignment::SpaceEvenly => 0., - Alignment::SpaceAround => 0., - } -} - -/// Compose a new SkParagraph -pub fn create_paragraph( - node: &DioxusNode, - area_size: &Size2D, - font_collection: &FontCollection, - is_rendering: bool, - default_font_family: &[String], - scale_factor: f32, -) -> Paragraph { - let font_style = &*node.get::().unwrap(); - - let mut paragraph_style = ParagraphStyle::default(); - paragraph_style.set_text_align(font_style.text_align); - paragraph_style.set_max_lines(font_style.max_lines); - paragraph_style.set_replace_tab_characters(true); - - if let Some(ellipsis) = font_style.text_overflow.get_ellipsis() { - paragraph_style.set_ellipsis(ellipsis); - } - - let mut paragraph_builder = ParagraphBuilder::new(¶graph_style, font_collection); - - let text_style = font_style.text_style(default_font_family, scale_factor); - paragraph_builder.push_style(&text_style); - - for text_span in node.children() { - if let NodeType::Element(ElementNode { - tag: TagName::Text, .. - }) = &*text_span.node_type() - { - let text_nodes = text_span.children(); - let text_node = *text_nodes.first().unwrap(); - let text_node_type = &*text_node.node_type(); - let font_style = text_span.get::().unwrap(); - let text_style = font_style.text_style(default_font_family, scale_factor); - paragraph_builder.push_style(&text_style); - - if let NodeType::Text(text) = text_node_type { - paragraph_builder.add_text(text); - } - } - } - - if is_rendering { - // This is very tricky, but it works! It allows freya to render the cursor at the end of a line. - paragraph_builder.add_text(" "); - } - - let mut paragraph = paragraph_builder.build(); - paragraph.layout( - if font_style.max_lines == Some(1) && font_style.text_align == TextAlign::default() { - f32::MAX - } else { - area_size.width + 1.0 - }, - ); - - paragraph -} diff --git a/crates/core/src/render/utils/borders.rs b/crates/core/src/render/utils/borders.rs new file mode 100644 index 000000000..3ced2b54c --- /dev/null +++ b/crates/core/src/render/utils/borders.rs @@ -0,0 +1,237 @@ +use freya_engine::prelude::*; +use freya_node_state::{ + Border, + BorderAlignment, + CornerRadius, +}; +use torin::prelude::Area; + +pub enum BorderShape { + DRRect(RRect, RRect), + Path(Path), +} + +pub fn render_border( + canvas: &Canvas, + rounded_rect: RRect, + area: Area, + border: &Border, + corner_radius: CornerRadius, +) { + // Create a new paint + let mut border_paint = Paint::default(); + border_paint.set_style(PaintStyle::Fill); + border_paint.set_anti_alias(true); + + border.fill.apply_to_paint(&mut border_paint, area); + + match border_shape(*rounded_rect.rect(), corner_radius, border) { + BorderShape::DRRect(outer, inner) => { + canvas.draw_drrect(outer, inner, &border_paint); + } + BorderShape::Path(path) => { + canvas.draw_path(&path, &border_paint); + } + } +} + +/// Returns a `Path` that will draw a [`Border`] around a base rectangle. +/// +/// We don't use Skia's stroking API here, since we might need different widths for each side. +pub fn border_shape( + base_rect: Rect, + base_corner_radius: CornerRadius, + border: &Border, +) -> BorderShape { + let border_alignment = border.alignment; + let border_width = border.width; + + // First we create a path that is outset from the rect by a certain amount on each side. + // + // Let's call this the outer border path. + let (outer_rrect, outer_corner_radius) = { + // Calculuate the outer corner radius for the border. + let corner_radius = CornerRadius { + top_left: outer_border_path_corner_radius( + border_alignment, + base_corner_radius.top_left, + border_width.top, + border_width.left, + ), + top_right: outer_border_path_corner_radius( + border_alignment, + base_corner_radius.top_right, + border_width.top, + border_width.right, + ), + bottom_left: outer_border_path_corner_radius( + border_alignment, + base_corner_radius.bottom_left, + border_width.bottom, + border_width.left, + ), + bottom_right: outer_border_path_corner_radius( + border_alignment, + base_corner_radius.bottom_right, + border_width.bottom, + border_width.right, + ), + smoothing: base_corner_radius.smoothing, + }; + + let rrect = RRect::new_rect_radii( + { + let mut rect = base_rect; + let alignment_scale = match border_alignment { + BorderAlignment::Outer => 1.0, + BorderAlignment::Center => 0.5, + BorderAlignment::Inner => 0.0, + }; + + rect.left -= border_width.left * alignment_scale; + rect.top -= border_width.top * alignment_scale; + rect.right += border_width.right * alignment_scale; + rect.bottom += border_width.bottom * alignment_scale; + + rect + }, + &[ + (corner_radius.top_left, corner_radius.top_left).into(), + (corner_radius.top_right, corner_radius.top_right).into(), + (corner_radius.bottom_right, corner_radius.bottom_right).into(), + (corner_radius.bottom_left, corner_radius.bottom_left).into(), + ], + ); + + (rrect, corner_radius) + }; + + // After the outer path, we will then move to the inner bounds of the border. + let (inner_rrect, inner_corner_radius) = { + // Calculuate the inner corner radius for the border. + let corner_radius = CornerRadius { + top_left: inner_border_path_corner_radius( + border_alignment, + base_corner_radius.top_left, + border_width.top, + border_width.left, + ), + top_right: inner_border_path_corner_radius( + border_alignment, + base_corner_radius.top_right, + border_width.top, + border_width.right, + ), + bottom_left: inner_border_path_corner_radius( + border_alignment, + base_corner_radius.bottom_left, + border_width.bottom, + border_width.left, + ), + bottom_right: inner_border_path_corner_radius( + border_alignment, + base_corner_radius.bottom_right, + border_width.bottom, + border_width.right, + ), + smoothing: base_corner_radius.smoothing, + }; + + let rrect = RRect::new_rect_radii( + { + let mut rect = base_rect; + let alignment_scale = match border_alignment { + BorderAlignment::Outer => 0.0, + BorderAlignment::Center => 0.5, + BorderAlignment::Inner => 1.0, + }; + + rect.left += border_width.left * alignment_scale; + rect.top += border_width.top * alignment_scale; + rect.right -= border_width.right * alignment_scale; + rect.bottom -= border_width.bottom * alignment_scale; + + rect + }, + &[ + (corner_radius.top_left, corner_radius.top_left).into(), + (corner_radius.top_right, corner_radius.top_right).into(), + (corner_radius.bottom_right, corner_radius.bottom_right).into(), + (corner_radius.bottom_left, corner_radius.bottom_left).into(), + ], + ); + + (rrect, corner_radius) + }; + + if base_corner_radius.smoothing > 0.0 { + let mut path = Path::new(); + path.set_fill_type(PathFillType::EvenOdd); + + path.add_path( + &outer_corner_radius.smoothed_path(outer_rrect), + Point::new(outer_rrect.rect().x(), outer_rrect.rect().y()), + None, + ); + + path.add_path( + &inner_corner_radius.smoothed_path(inner_rrect), + Point::new(inner_rrect.rect().x(), inner_rrect.rect().y()), + None, + ); + + BorderShape::Path(path) + } else { + BorderShape::DRRect(outer_rrect, inner_rrect) + } +} + +fn outer_border_path_corner_radius( + alignment: BorderAlignment, + corner_radius: f32, + width_1: f32, + width_2: f32, +) -> f32 { + if alignment == BorderAlignment::Inner || corner_radius == 0.0 { + return corner_radius; + } + + let mut offset = if width_1 == 0.0 { + width_2 + } else if width_2 == 0.0 { + width_1 + } else { + width_1.min(width_2) + }; + + if alignment == BorderAlignment::Center { + offset *= 0.5; + } + + corner_radius + offset +} + +fn inner_border_path_corner_radius( + alignment: BorderAlignment, + corner_radius: f32, + width_1: f32, + width_2: f32, +) -> f32 { + if alignment == BorderAlignment::Outer || corner_radius == 0.0 { + return corner_radius; + } + + let mut offset = if width_1 == 0.0 { + width_2 + } else if width_2 == 0.0 { + width_1 + } else { + width_1.min(width_2) + }; + + if alignment == BorderAlignment::Center { + offset *= 0.5; + } + + corner_radius - offset +} diff --git a/crates/core/src/render/utils/label.rs b/crates/core/src/render/utils/label.rs new file mode 100644 index 000000000..e1d1619b2 --- /dev/null +++ b/crates/core/src/render/utils/label.rs @@ -0,0 +1,50 @@ +use freya_engine::prelude::*; +use freya_native_core::{ + prelude::NodeType, + real_dom::NodeImmutable, +}; +use freya_node_state::FontStyleState; +use torin::prelude::Size2D; + +use crate::dom::*; + +pub fn create_label( + node: &DioxusNode, + area_size: &Size2D, + font_collection: &FontCollection, + default_font_family: &[String], + scale_factor: f32, +) -> Paragraph { + let font_style = &*node.get::().unwrap(); + + let mut paragraph_style = ParagraphStyle::default(); + paragraph_style.set_text_align(font_style.text_align); + paragraph_style.set_max_lines(font_style.max_lines); + paragraph_style.set_replace_tab_characters(true); + + if let Some(ellipsis) = font_style.text_overflow.get_ellipsis() { + paragraph_style.set_ellipsis(ellipsis); + } + + let text_style = font_style.text_style(default_font_family, scale_factor); + paragraph_style.set_text_style(&text_style); + + let mut paragraph_builder = ParagraphBuilder::new(¶graph_style, font_collection); + + for child in node.children() { + if let NodeType::Text(text) = &*child.node_type() { + paragraph_builder.add_text(text); + } + } + + let mut paragraph = paragraph_builder.build(); + paragraph.layout( + if font_style.max_lines == Some(1) && font_style.text_align == TextAlign::default() { + f32::MAX + } else { + area_size.width + 1.0 + }, + ); + + paragraph +} diff --git a/crates/core/src/render/utils/mod.rs b/crates/core/src/render/utils/mod.rs new file mode 100644 index 000000000..5a3a0fbf8 --- /dev/null +++ b/crates/core/src/render/utils/mod.rs @@ -0,0 +1,9 @@ +mod borders; +mod label; +mod paragraph; +mod shadows; + +pub use borders::*; +pub use label::*; +pub use paragraph::*; +pub use shadows::*; diff --git a/crates/core/src/render/utils/paragraph.rs b/crates/core/src/render/utils/paragraph.rs new file mode 100644 index 000000000..6e8754a28 --- /dev/null +++ b/crates/core/src/render/utils/paragraph.rs @@ -0,0 +1,204 @@ +use freya_engine::prelude::*; +use freya_native_core::{ + node::ElementNode, + prelude::NodeType, + real_dom::NodeImmutable, + tags::TagName, +}; +use freya_node_state::{ + CursorState, + FontStyleState, + HighlightMode, + LayoutState, +}; +use torin::prelude::{ + Alignment, + Area, + Size2D, +}; + +use crate::dom::DioxusNode; + +/// Compose a new SkParagraph +pub fn create_paragraph( + node: &DioxusNode, + area_size: &Size2D, + font_collection: &FontCollection, + is_rendering: bool, + default_font_family: &[String], + scale_factor: f32, +) -> Paragraph { + let font_style = &*node.get::().unwrap(); + + let mut paragraph_style = ParagraphStyle::default(); + paragraph_style.set_text_align(font_style.text_align); + paragraph_style.set_max_lines(font_style.max_lines); + paragraph_style.set_replace_tab_characters(true); + + if let Some(ellipsis) = font_style.text_overflow.get_ellipsis() { + paragraph_style.set_ellipsis(ellipsis); + } + + let mut paragraph_builder = ParagraphBuilder::new(¶graph_style, font_collection); + + let text_style = font_style.text_style(default_font_family, scale_factor); + paragraph_builder.push_style(&text_style); + + for text_span in node.children() { + if let NodeType::Element(ElementNode { + tag: TagName::Text, .. + }) = &*text_span.node_type() + { + let text_nodes = text_span.children(); + let text_node = *text_nodes.first().unwrap(); + let text_node_type = &*text_node.node_type(); + let font_style = text_span.get::().unwrap(); + let text_style = font_style.text_style(default_font_family, scale_factor); + paragraph_builder.push_style(&text_style); + + if let NodeType::Text(text) = text_node_type { + paragraph_builder.add_text(text); + } + } + } + + if is_rendering { + // This is very tricky, but it works! It allows freya to render the cursor at the end of a line. + paragraph_builder.add_text(" "); + } + + let mut paragraph = paragraph_builder.build(); + paragraph.layout( + if font_style.max_lines == Some(1) && font_style.text_align == TextAlign::default() { + f32::MAX + } else { + area_size.width + 1.0 + }, + ); + + paragraph +} + +pub fn draw_cursor_highlights( + area: &Area, + paragraph: &Paragraph, + canvas: &Canvas, + node_ref: &DioxusNode, +) -> Option<()> { + let node_cursor_state = &*node_ref.get::().unwrap(); + + let highlights = node_cursor_state.highlights.as_ref()?; + let highlight_color = node_cursor_state.highlight_color; + + for (from, to) in highlights.iter() { + let (from, to) = { + if from < to { + (from, to) + } else { + (to, from) + } + }; + let cursor_rects = paragraph.get_rects_for_range( + *from..*to, + RectHeightStyle::Tight, + RectWidthStyle::Tight, + ); + for cursor_rect in cursor_rects { + let rect = align_highlights_and_cursor_paragraph( + node_ref, + area, + paragraph, + &cursor_rect, + None, + ); + + let mut paint = Paint::default(); + paint.set_anti_alias(true); + paint.set_style(PaintStyle::Fill); + paint.set_color(highlight_color); + + canvas.draw_rect(rect, &paint); + } + } + + Some(()) +} + +pub fn draw_cursor( + area: &Area, + paragraph: &Paragraph, + canvas: &Canvas, + node_ref: &DioxusNode, +) -> Option<()> { + let node_cursor_state = &*node_ref.get::().unwrap(); + + let cursor = node_cursor_state.position?; + let cursor_color = node_cursor_state.color; + let cursor_position = cursor as usize; + + let cursor_rects = paragraph.get_rects_for_range( + cursor_position..cursor_position + 1, + RectHeightStyle::Tight, + RectWidthStyle::Tight, + ); + let cursor_rect = cursor_rects.first()?; + + let rect = + align_highlights_and_cursor_paragraph(node_ref, area, paragraph, cursor_rect, Some(1.0)); + + let mut paint = Paint::default(); + paint.set_anti_alias(true); + paint.set_style(PaintStyle::Fill); + paint.set_color(cursor_color); + + canvas.draw_rect(rect, &paint); + + Some(()) +} + +/// Align the Y axis of the highlights and cursor of a paragraph +pub fn align_highlights_and_cursor_paragraph( + node: &DioxusNode, + area: &Area, + paragraph: &Paragraph, + cursor_rect: &TextBox, + width: Option, +) -> Rect { + let cursor_state = node.get::().unwrap(); + + let left = area.min_x() + cursor_rect.rect.left; + let right = left + width.unwrap_or(cursor_rect.rect.right - cursor_rect.rect.left); + + match cursor_state.highlight_mode { + HighlightMode::Fit => { + let top = (area.min_y() + + align_main_align_paragraph(node, area, paragraph) + + cursor_rect.rect.top) + .clamp(area.min_y(), area.max_y()); + let bottom = (top + (cursor_rect.rect.bottom - cursor_rect.rect.top)) + .clamp(area.min_y(), area.max_y()); + + Rect::new(left, top, right, bottom) + } + HighlightMode::Expanded => { + let top = area.min_y(); + let bottom = area.max_y(); + + Rect::new(left, top, right, bottom) + } + } +} + +/// Align the main alignment of a paragraph +pub fn align_main_align_paragraph(node: &DioxusNode, area: &Area, paragraph: &Paragraph) -> f32 { + let layout = node.get::().unwrap(); + + match layout.main_alignment { + Alignment::Start => 0., + Alignment::Center => (area.height() / 2.0) - (paragraph.height() / 2.0), + Alignment::End => area.height() - paragraph.height(), + Alignment::SpaceBetween => 0., + Alignment::SpaceEvenly => 0., + Alignment::SpaceAround => 0., + } +} diff --git a/crates/core/src/render/utils/shadows.rs b/crates/core/src/render/utils/shadows.rs new file mode 100644 index 000000000..ea83c183e --- /dev/null +++ b/crates/core/src/render/utils/shadows.rs @@ -0,0 +1,77 @@ +use freya_engine::prelude::*; +use freya_node_state::{ + CornerRadius, + Shadow, + ShadowPosition, + StyleState, +}; +use torin::prelude::Area; + +pub fn render_shadow( + canvas: &Canvas, + node_style: &StyleState, + path: &mut Path, + rounded_rect: RRect, + area: Area, + shadow: Shadow, + corner_radius: CornerRadius, +) { + let mut shadow_path = Path::new(); + let mut shadow_paint = Paint::default(); + shadow_paint.set_anti_alias(true); + + shadow.fill.apply_to_paint(&mut shadow_paint, area); + + // Shadows can be either outset or inset + // If they are outset, we fill a copy of the path outset by spread_radius, and blur it. + // Otherwise, we draw a stroke with the inner portion being spread_radius width, and the outer portion being blur_radius width. + let outset: Point = match shadow.position { + ShadowPosition::Normal => { + shadow_paint.set_style(PaintStyle::Fill); + (shadow.spread, shadow.spread).into() + } + ShadowPosition::Inset => { + shadow_paint.set_style(PaintStyle::Stroke); + shadow_paint.set_stroke_width(shadow.blur / 2.0 + shadow.spread); + (-shadow.spread / 2.0, -shadow.spread / 2.0).into() + } + }; + + // Apply gassuan blur to the copied path. + if shadow.blur > 0.0 { + shadow_paint.set_mask_filter(MaskFilter::blur( + BlurStyle::Normal, + shadow.blur / 2.0, + false, + )); + } + + // Add either the RRect or smoothed path based on whether smoothing is used. + if corner_radius.smoothing > 0.0 { + shadow_path.add_path( + &node_style + .corner_radius + .smoothed_path(rounded_rect.with_outset(outset)), + Point::new(area.min_x(), area.min_y()) - outset, + None, + ); + } else { + shadow_path.add_rrect(rounded_rect.with_outset(outset), None); + } + + // Offset our path by the shadow's x and y coordinates. + shadow_path.offset((shadow.x, shadow.y)); + + // Exclude the original path bounds from the shadow using a clip, then draw the shadow. + canvas.save(); + canvas.clip_path( + path, + match shadow.position { + ShadowPosition::Normal => ClipOp::Difference, + ShadowPosition::Inset => ClipOp::Intersect, + }, + true, + ); + canvas.draw_path(&shadow_path, &shadow_paint); + canvas.restore(); +} diff --git a/crates/state/src/values/fill.rs b/crates/state/src/values/fill.rs index bf285bbf5..84556f517 100644 --- a/crates/state/src/values/fill.rs +++ b/crates/state/src/values/fill.rs @@ -1,6 +1,10 @@ use std::fmt; -use freya_engine::prelude::Color; +use freya_engine::prelude::{ + Color, + Paint, +}; +use torin::prelude::Area; use crate::{ ConicGradient, @@ -19,6 +23,25 @@ pub enum Fill { ConicGradient(ConicGradient), } +impl Fill { + pub fn apply_to_paint(&self, paint: &mut Paint, area: Area) { + match &self { + Fill::Color(color) => { + paint.set_color(*color); + } + Fill::LinearGradient(gradient) => { + paint.set_shader(gradient.into_shader(area)); + } + Fill::RadialGradient(gradient) => { + paint.set_shader(gradient.into_shader(area)); + } + Fill::ConicGradient(gradient) => { + paint.set_shader(gradient.into_shader(area)); + } + } + } +} + impl Default for Fill { fn default() -> Self { Self::Color(Color::default())