diff --git a/.DS_Store b/.DS_Store index 6ff9b1766..25aea550c 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/crates/vizia_core/Cargo.toml b/crates/vizia_core/Cargo.toml index adae7b189..8f1d0ba52 100644 --- a/crates/vizia_core/Cargo.toml +++ b/crates/vizia_core/Cargo.toml @@ -23,7 +23,8 @@ vizia_window = { path = "../vizia_window" } vizia_style = { path = "../vizia_style"} accesskit = "0.12.0" -femtovg = "0.8.2" +# femtovg = "0.8.2" +skia-safe = { version = "0.71", features = ["textlayout"] } image = { version = "0.24.8", default-features = false, features = ["png"] } # inherited from femtovg # morphorm = {path = "../../../morphorm" } morphorm = {git = "https://github.com/vizia/morphorm.git", branch = "auto-min-size2"} @@ -39,8 +40,6 @@ copypasta = {version = "0.10.1", optional = true, default-features = false } instant = "0.1.12" chrono = "0.4.34" hashbrown = "0.14.3" -cosmic-text = { git="https://github.com/pop-os/cosmic-text", rev="19b4d8336e34073bb51b83578d3d803c8c953787" } -swash = "0.1.12" log = "0.4.20" indexmap = "2.2.3" qfilter = "0.1.6" diff --git a/crates/vizia_core/resources/fonts/FiraCode-Regular.ttf b/crates/vizia_core/resources/fonts/FiraCode-Regular.ttf deleted file mode 100644 index b8a44d2db..000000000 Binary files a/crates/vizia_core/resources/fonts/FiraCode-Regular.ttf and /dev/null differ diff --git a/crates/vizia_core/resources/fonts/FiraCode.ttf b/crates/vizia_core/resources/fonts/FiraCode.ttf new file mode 100644 index 000000000..5655ed514 Binary files /dev/null and b/crates/vizia_core/resources/fonts/FiraCode.ttf differ diff --git a/crates/vizia_core/resources/fonts/Roboto-Bold.ttf b/crates/vizia_core/resources/fonts/Roboto-Bold.ttf deleted file mode 100644 index aaf374d2c..000000000 Binary files a/crates/vizia_core/resources/fonts/Roboto-Bold.ttf and /dev/null differ diff --git a/crates/vizia_core/resources/fonts/Roboto-Italic.ttf b/crates/vizia_core/resources/fonts/Roboto-Italic.ttf deleted file mode 100644 index 6682d17b0..000000000 Binary files a/crates/vizia_core/resources/fonts/Roboto-Italic.ttf and /dev/null differ diff --git a/crates/vizia_core/resources/fonts/Roboto-Regular.ttf b/crates/vizia_core/resources/fonts/Roboto-Regular.ttf deleted file mode 100644 index 3e6e2e761..000000000 Binary files a/crates/vizia_core/resources/fonts/Roboto-Regular.ttf and /dev/null differ diff --git a/crates/vizia_core/resources/fonts/RobotoFlex.ttf b/crates/vizia_core/resources/fonts/RobotoFlex.ttf new file mode 100644 index 000000000..6f8db7ea0 Binary files /dev/null and b/crates/vizia_core/resources/fonts/RobotoFlex.ttf differ diff --git a/crates/vizia_core/resources/fonts/tabler-icons.ttf b/crates/vizia_core/resources/fonts/tabler-icons.ttf index 4560f6545..7855ecca5 100644 Binary files a/crates/vizia_core/resources/fonts/tabler-icons.ttf and b/crates/vizia_core/resources/fonts/tabler-icons.ttf differ diff --git a/crates/vizia_core/src/animation/interpolator.rs b/crates/vizia_core/src/animation/interpolator.rs index c8d3fc1c1..74f8cb4cf 100644 --- a/crates/vizia_core/src/animation/interpolator.rs +++ b/crates/vizia_core/src/animation/interpolator.rs @@ -5,7 +5,7 @@ use vizia_style::{ LinearGradient, Opacity, PercentageOrNumber, Rect, Scale, Transform, Translate, RGBA, }; -use femtovg::Transform2D; +// use femtovg::Transform2D; use crate::style::ImageOrGradient; @@ -200,18 +200,18 @@ impl Interpolator for Transform { } // TODO: Split this into interpolated matrices for translation, rotation, scale, and skew -impl Interpolator for Transform2D { - fn interpolate(start: &Self, end: &Self, t: f32) -> Self { - let mut transform = *start; - transform[0] = f32::interpolate(&start[0], &end[0], t); - transform[1] = f32::interpolate(&start[1], &end[1], t); - transform[2] = f32::interpolate(&start[2], &end[2], t); - transform[3] = f32::interpolate(&start[3], &end[3], t); - transform[4] = f32::interpolate(&start[4], &end[4], t); - transform[5] = f32::interpolate(&start[5], &end[5], t); - transform - } -} +// impl Interpolator for Transform2D { +// fn interpolate(start: &Self, end: &Self, t: f32) -> Self { +// let mut transform = *start; +// transform[0] = f32::interpolate(&start[0], &end[0], t); +// transform[1] = f32::interpolate(&start[1], &end[1], t); +// transform[2] = f32::interpolate(&start[2], &end[2], t); +// transform[3] = f32::interpolate(&start[3], &end[3], t); +// transform[4] = f32::interpolate(&start[4], &end[4], t); +// transform[5] = f32::interpolate(&start[5], &end[5], t); +// transform +// } +// } impl Interpolator for Vec { fn interpolate(start: &Self, end: &Self, t: f32) -> Self { diff --git a/crates/vizia_core/src/binding/data.rs b/crates/vizia_core/src/binding/data.rs index 91bc4b1a6..76e85706c 100644 --- a/crates/vizia_core/src/binding/data.rs +++ b/crates/vizia_core/src/binding/data.rs @@ -71,6 +71,9 @@ impl_data_simple!(Entity); impl_data_simple!(Localized); impl_data_simple!(Length); impl_data_simple!(KeyChord); +impl_data_simple!(FamilyOwned); +impl_data_simple!(FontWeight); +impl_data_simple!(TextAlign); impl Data for &'static str { fn same(&self, other: &Self) -> bool { diff --git a/crates/vizia_core/src/binding/res.rs b/crates/vizia_core/src/binding/res.rs index 7229cb798..5563296fb 100644 --- a/crates/vizia_core/src/binding/res.rs +++ b/crates/vizia_core/src/binding/res.rs @@ -84,16 +84,18 @@ impl_res_simple!(Overflow); impl_res_simple!(LengthValue); impl_res_simple!(FontWeight); impl_res_simple!(FontWeightKeyword); -impl_res_simple!(FontStyle); +impl_res_simple!(FontSlant); impl_res_simple!(BorderCornerShape); impl_res_simple!(Angle); impl_res_simple!(TextAlign); +impl_res_simple!(TextOverflow); +impl_res_simple!(LineClamp); impl_res_clone!(BoxShadow); impl_res_clone!(LinearGradientBuilder); impl_res_clone!(BoxShadowBuilder); impl_res_clone!(Filter); impl_res_simple!(Opacity); -impl_res_simple!(FontStretch); +impl_res_simple!(FontWidth); impl_res_clone!(Translate); impl_res_clone!(Scale); impl_res_clone!(Position); diff --git a/crates/vizia_core/src/cache.rs b/crates/vizia_core/src/cache.rs index dabf60e66..485dd4fd8 100644 --- a/crates/vizia_core/src/cache.rs +++ b/crates/vizia_core/src/cache.rs @@ -2,7 +2,6 @@ //! results. The main type here is CachedData, usually accessed via `cx.cache`. use crate::prelude::*; -use femtovg::ImageId; use vizia_storage::SparseSet; #[derive(Debug, Default, Clone, Copy, PartialEq)] @@ -19,9 +18,9 @@ pub(crate) struct Pos { pub struct CachedData { pub(crate) bounds: SparseSet, pub(crate) relative_position: SparseSet, - pub(crate) shadow_images: SparseSet>>, - pub(crate) filter_image: SparseSet>, - pub(crate) screenshot_image: SparseSet>, + // pub(crate) shadow_images: SparseSet>>, + // pub(crate) filter_image: SparseSet>, + // pub(crate) screenshot_image: SparseSet>, pub(crate) geo_changed: SparseSet, } @@ -35,9 +34,9 @@ impl CachedData { pub(crate) fn remove(&mut self, entity: Entity) { self.bounds.remove(entity); self.relative_position.remove(entity); - self.shadow_images.remove(entity); - self.filter_image.remove(entity); - self.screenshot_image.remove(entity); + // self.shadow_images.remove(entity); + // self.filter_image.remove(entity); + // self.screenshot_image.remove(entity); self.geo_changed.remove(entity); } diff --git a/crates/vizia_core/src/context/backend.rs b/crates/vizia_core/src/context/backend.rs index f4078b794..135142434 100644 --- a/crates/vizia_core/src/context/backend.rs +++ b/crates/vizia_core/src/context/backend.rs @@ -1,13 +1,13 @@ use std::any::Any; -use femtovg::{renderer::OpenGl, Canvas}; +use skia_safe::Surface; use vizia_window::WindowDescription; use super::EventProxy; use crate::events::EventManager; use crate::{cache::CachedData, prelude::*, systems::*}; -pub use crate::text::cosmic::TextConfig; +pub use crate::text::text_context::TextConfig; #[cfg(feature = "clipboard")] use copypasta::ClipboardProvider; @@ -27,9 +27,9 @@ impl<'a> BackendContext<'a> { } /// Helper function for mutating the state of the root window. - pub fn mutate_window(&mut self, f: F) { - if let Some(window_event_handler) = self.0.views.remove(&Entity::root()) { - if let Some(window) = window_event_handler.downcast_ref::() { + pub fn mutate_window(&mut self, f: F) { + if let Some(mut window_event_handler) = self.0.views.remove(&Entity::root()) { + if let Some(window) = window_event_handler.downcast_mut::() { f(self, window); } @@ -62,6 +62,10 @@ impl<'a> BackendContext<'a> { self.0.focused } + pub fn get_surface_mut(&mut self, entity: Entity) -> Option<&mut Surface> { + self.0.canvases.get_mut(&entity) + } + /// The window's size in logical pixels, before /// [`user_scale_factor()`][Self::user_scale_factor()] gets applied to it. If this value changed /// during a frame then the window will be resized and a [`WindowEvent::GeometryChanged`] will be @@ -82,22 +86,12 @@ impl<'a> BackendContext<'a> { pub fn add_main_window( &mut self, window_description: &WindowDescription, - mut canvas: Canvas, + surface: Surface, dpi_factor: f32, ) { let physical_width = window_description.inner_size.width as f32 * dpi_factor; let physical_height = window_description.inner_size.height as f32 * dpi_factor; - // Scale factor is set to 1.0 here because scaling is applied prior to rendering - canvas.set_size(physical_width as u32, physical_height as u32, 1.0); - canvas.clear_rect( - 0, - 0, - physical_width as u32, - physical_height as u32, - femtovg::Color::rgba(0, 0, 0, 0), - ); - self.0.style.dpi_factor = dpi_factor as f64; self.0.cache.set_width(Entity::root(), physical_width); @@ -117,7 +111,7 @@ impl<'a> BackendContext<'a> { self.0.style.pseudo_classes.insert(Entity::root(), PseudoClassFlags::OVER); self.0.style.restyle.insert(Entity::root()).unwrap(); self.0.style.reaccess.insert(Entity::root()).unwrap(); - self.0.canvases.insert(Entity::root(), canvas); + self.0.canvases.insert(Entity::root(), surface); } /// Returns a reference to the [`Environment`] model. diff --git a/crates/vizia_core/src/context/draw.rs b/crates/vizia_core/src/context/draw.rs index e56bbf57f..3fd32843f 100644 --- a/crates/vizia_core/src/context/draw.rs +++ b/crates/vizia_core/src/context/draw.rs @@ -1,18 +1,20 @@ -use femtovg::{ImageId, Transform2D}; +use skia_safe::gradient_shader::GradientShaderColors; +use skia_safe::{ + BlurStyle, ClipOp, MaskFilter, Paint, PaintStyle, Path, PathDirection, Point, RRect, Rect, + Shader, TileMode, +}; +// use femtovg::{ImageId, Transform2D}; use std::any::{Any, TypeId}; use hashbrown::HashMap; -use crate::animation::Interpolator; use crate::cache::CachedData; use crate::events::ViewHandler; use crate::model::ModelDataStore; use crate::prelude::*; -use crate::resource::{ImageOrId, ResourceManager}; +use crate::resource::ResourceManager; use crate::text::{TextConfig, TextContext}; -use crate::vg::{Paint, Path}; use vizia_input::MouseState; -use vizia_style::LengthPercentageOrAuto; /// A context used when drawing a view. /// @@ -134,13 +136,11 @@ impl<'a> DrawContext<'a> { } /// Returns the bounding box of the clip region of the current view. - pub fn clip_region(&self) -> BoundingBox { + pub fn clip_region(&self) -> Option { let bounds = self.bounds(); let overflowx = self.style.overflowx.get(self.current).copied().unwrap_or_default(); let overflowy = self.style.overflowy.get(self.current).copied().unwrap_or_default(); - // let root_bounds = self.cache.get_bounds(Entity::root()); - let scale = self.scale_factor(); let clip_bounds = self @@ -158,11 +158,10 @@ impl<'a> DrawContext<'a> { }) .unwrap_or(bounds); - let root_bounds: BoundingBox = - BoundingBox { x: -f32::MAX / 2.0, y: -f32::MAX / 2.0, w: f32::MAX, h: f32::MAX }; + let root_bounds = self.cache.get_bounds(Entity::root()); - match (overflowx, overflowy) { - (Overflow::Visible, Overflow::Visible) => root_bounds, + let clip_bounds = match (overflowx, overflowy) { + (Overflow::Visible, Overflow::Visible) => return None, (Overflow::Hidden, Overflow::Visible) => { let left = clip_bounds.left(); let right = clip_bounds.right(); @@ -178,71 +177,74 @@ impl<'a> DrawContext<'a> { BoundingBox::from_min_max(left, top, right, bottom) } (Overflow::Hidden, Overflow::Hidden) => clip_bounds, - } + _ => return None, + }; + + Some(self.build_path(clip_bounds)) } /// Returns the 2D transform of the current view. - pub fn transform(&self) -> Transform2D { - let mut transform = Transform2D::identity(); - - let bounds = self.bounds(); - let scale_factor = self.scale_factor(); - - // Apply transform origin. - let mut origin = self - .style - .transform_origin - .get(self.current) - .map(|transform_origin| { - let mut origin = Transform2D::new_translation(bounds.left(), bounds.top()); - let offset = transform_origin.as_transform(bounds, scale_factor); - origin.premultiply(&offset); - origin - }) - .unwrap_or(Transform2D::new_translation(bounds.center().0, bounds.center().1)); - transform.premultiply(&origin); - origin.inverse(); - - // Apply translation. - if let Some(translate) = self.style.translate.get(self.current) { - transform.premultiply(&translate.as_transform(bounds, scale_factor)); - } - - // Apply rotation. - if let Some(rotate) = self.style.rotate.get(self.current) { - transform.premultiply(&rotate.as_transform(bounds, scale_factor)); - } - - // Apply scaling. - if let Some(scale) = self.style.scale.get(self.current) { - transform.premultiply(&scale.as_transform(bounds, scale_factor)); - } - - // Apply transform functions. - if let Some(transforms) = self.style.transform.get(self.current) { - // Check if the transform is currently animating - // Get the animation state - // Manually interpolate the value to get the overall transform for the current frame - if let Some(animation_state) = self.style.transform.get_active_animation(self.current) { - if let Some(start) = animation_state.keyframes.first() { - if let Some(end) = animation_state.keyframes.last() { - let start_transform = start.value.as_transform(bounds, scale_factor); - let end_transform = end.value.as_transform(bounds, scale_factor); - let t = animation_state.t; - let animated_transform = - Transform2D::interpolate(&start_transform, &end_transform, t); - transform.premultiply(&animated_transform); - } - } - } else { - transform.premultiply(&transforms.as_transform(bounds, scale_factor)); - } - } - - transform.premultiply(&origin); - - transform - } + // pub fn transform(&self) -> Transform2D { + // let mut transform = Transform2D::identity(); + + // let bounds = self.bounds(); + // let scale_factor = self.scale_factor(); + + // // Apply transform origin. + // let mut origin = self + // .style + // .transform_origin + // .get(self.current) + // .map(|transform_origin| { + // let mut origin = Transform2D::new_translation(bounds.left(), bounds.top()); + // let offset = transform_origin.as_transform(bounds, scale_factor); + // origin.premultiply(&offset); + // origin + // }) + // .unwrap_or(Transform2D::new_translation(bounds.center().0, bounds.center().1)); + // transform.premultiply(&origin); + // origin.inverse(); + + // // Apply translation. + // if let Some(translate) = self.style.translate.get(self.current) { + // transform.premultiply(&translate.as_transform(bounds, scale_factor)); + // } + + // // Apply rotation. + // if let Some(rotate) = self.style.rotate.get(self.current) { + // transform.premultiply(&rotate.as_transform(bounds, scale_factor)); + // } + + // // Apply scaling. + // if let Some(scale) = self.style.scale.get(self.current) { + // transform.premultiply(&scale.as_transform(bounds, scale_factor)); + // } + + // // Apply transform functions. + // if let Some(transforms) = self.style.transform.get(self.current) { + // // Check if the transform is currently animating + // // Get the animation state + // // Manually interpolate the value to get the overall transform for the current frame + // if let Some(animation_state) = self.style.transform.get_active_animation(self.current) { + // if let Some(start) = animation_state.keyframes.first() { + // if let Some(end) = animation_state.keyframes.last() { + // let start_transform = start.value.as_transform(bounds, scale_factor); + // let end_transform = end.value.as_transform(bounds, scale_factor); + // let t = animation_state.t; + // let animated_transform = + // Transform2D::interpolate(&start_transform, &end_transform, t); + // transform.premultiply(&animated_transform); + // } + // } + // } else { + // transform.premultiply(&transforms.as_transform(bounds, scale_factor)); + // } + // } + + // transform.premultiply(&origin); + + // transform + // } /// Returns the visibility of the current view. pub fn visibility(&self) -> Option { @@ -265,12 +267,24 @@ impl<'a> DrawContext<'a> { } /// Returns the font-size of the current view in physical pixels. - pub fn font_size(&self, entity: Entity) -> f32 { + pub fn font_size(&self) -> f32 { self.logical_to_physical( - self.style.font_size.get(entity).copied().map(|f| f.0).unwrap_or(16.0), + self.style.font_size.get(self.current).copied().map(|f| f.0).unwrap_or(16.0), ) } + pub fn font_weight(&self) -> FontWeight { + self.style.font_weight.get(self.current).copied().unwrap_or_default() + } + + pub fn font_width(&self) -> FontWidth { + self.style.font_width.get(self.current).copied().unwrap_or_default() + } + + pub fn font_slant(&self) -> FontSlant { + self.style.font_slant.get(self.current).copied().unwrap_or_default() + } + /// Function to convert logical points to physical pixels. pub fn logical_to_physical(&self, logical: f32) -> f32 { self.style.logical_to_physical(logical) @@ -373,8 +387,16 @@ impl<'a> DrawContext<'a> { self.style.text_wrap.get(self.current).copied().unwrap_or(true) } - pub fn text_align(&self) -> Option { - self.style.text_align.get(self.current).copied() + pub fn text_align(&self) -> TextAlign { + self.style.text_align.get(self.current).copied().unwrap_or_default() + } + + pub fn text_overflow(&self) -> TextOverflow { + self.style.text_overflow.get(self.current).copied().unwrap_or_default() + } + + pub fn line_clamp(&self) -> Option { + self.style.line_clamp.get(self.current).copied().map(|lc| lc.0 as usize) } pub fn box_shadows(&self) -> Option<&Vec> { @@ -394,12 +416,10 @@ impl<'a> DrawContext<'a> { } /// Get the vector path of the current view. - pub fn build_path(&mut self) -> Path { + pub fn build_path(&self, bounds: BoundingBox) -> Path { // Length proportional to radius of a cubic bezier handle for 90deg arcs. const KAPPA90: f32 = 0.552_284_8; - let bounds = self.bounds(); - let border_width = self.border_width(); let border_top_left_radius = self.border_top_left_radius(); @@ -421,7 +441,11 @@ impl<'a> DrawContext<'a> { && border_top_left_radius == bounds.h / 2.0 && border_top_right_radius == bounds.h / 2.0 { - path.circle(bounds.center().0, bounds.center().1, bounds.w / 2.0 - border_width / 2.0); + path.add_circle( + bounds.center(), + bounds.w / 2.0 - border_width / 2.0, + PathDirection::CW, + ); } else { let x = bounds.x + border_width / 2.0; let y = bounds.y + border_width / 2.0; @@ -442,71 +466,59 @@ impl<'a> DrawContext<'a> { let rx_tl = border_top_left_radius.min(halfw) * w.signum(); let ry_tl = border_top_left_radius.min(halfh) * h.signum(); - path.move_to(x, y + ry_tl); - path.line_to(x, y + h - ry_bl); + path.move_to((x, y + ry_tl)); + path.line_to((x, y + h - ry_bl)); if border_bottom_left_radius != 0.0 { if border_bottom_left_shape == BorderCornerShape::Round { - path.bezier_to( - x, - y + h - ry_bl * (1.0 - KAPPA90), - x + rx_bl * (1.0 - KAPPA90), - y + h, - x + rx_bl, - y + h, + path.cubic_to( + (x, y + h - ry_bl * (1.0 - KAPPA90)), + (x + rx_bl * (1.0 - KAPPA90), y + h), + (x + rx_bl, y + h), ); } else { - path.line_to(x + rx_bl, y + h); + path.line_to((x + rx_bl, y + h)); } } - path.line_to(x + w - rx_br, y + h); + path.line_to((x + w - rx_br, y + h)); if border_bottom_right_radius != 0.0 { if border_bottom_right_shape == BorderCornerShape::Round { - path.bezier_to( - x + w - rx_br * (1.0 - KAPPA90), - y + h, - x + w, - y + h - ry_br * (1.0 - KAPPA90), - x + w, - y + h - ry_br, + path.cubic_to( + (x + w - rx_br * (1.0 - KAPPA90), y + h), + (x + w, y + h - ry_br * (1.0 - KAPPA90)), + (x + w, y + h - ry_br), ); } else { - path.line_to(x + w, y + h - ry_br); + path.line_to((x + w, y + h - ry_br)); } } - path.line_to(x + w, y + ry_tr); + path.line_to((x + w, y + ry_tr)); if border_top_right_radius != 0.0 { if border_top_right_shape == BorderCornerShape::Round { - path.bezier_to( - x + w, - y + ry_tr * (1.0 - KAPPA90), - x + w - rx_tr * (1.0 - KAPPA90), - y, - x + w - rx_tr, - y, + path.cubic_to( + (x + w, y + ry_tr * (1.0 - KAPPA90)), + (x + w - rx_tr * (1.0 - KAPPA90), y), + (x + w - rx_tr, y), ); } else { - path.line_to(x + w - rx_tr, y); + path.line_to((x + w - rx_tr, y)); } } - path.line_to(x + rx_tl, y); + path.line_to((x + rx_tl, y)); if border_top_left_radius != 0.0 { if border_top_left_shape == BorderCornerShape::Round { - path.bezier_to( - x + rx_tl * (1.0 - KAPPA90), - y, - x, - y + ry_tl * (1.0 - KAPPA90), - x, - y + ry_tl, + path.cubic_to( + (x + rx_tl * (1.0 - KAPPA90), y), + (x, y + ry_tl * (1.0 - KAPPA90)), + (x, y + ry_tl), ); } else { - path.line_to(x, y + ry_tl); + path.line_to((x, y + ry_tl)); } } @@ -516,219 +528,231 @@ impl<'a> DrawContext<'a> { path } - /// Draw backdrop filters for the current view. - pub fn draw_backdrop_filter(&mut self, canvas: &mut Canvas, path: &mut Path) { - let window_width = self.cache.get_width(Entity::root()); - let window_height = self.cache.get_height(Entity::root()); - let bounds = self.bounds(); - - let blur_radius = self.backdrop_filter().map(|filter| match filter { - Filter::Blur(r) => r.to_px().unwrap_or_default(), - }); - - if let Some(blur_radius) = blur_radius { - let sigma = blur_radius / 2.0; - - let filter_image = - self.cache.filter_image.get(self.current).cloned().unwrap_or_default(); - - fn create_images(canvas: &mut Canvas, w: usize, h: usize) -> (ImageId, ImageId) { - ( - canvas - .create_image_empty( - w, - h, - femtovg::PixelFormat::Rgba8, - femtovg::ImageFlags::FLIP_Y | femtovg::ImageFlags::PREMULTIPLIED, - ) - .unwrap(), - canvas - .create_image_empty( - w, - h, - femtovg::PixelFormat::Rgba8, - femtovg::ImageFlags::FLIP_Y | femtovg::ImageFlags::PREMULTIPLIED, - ) - .unwrap(), - ) - } - - let (source, target) = match filter_image { - Some((s, t)) => { - let image_size = canvas.image_size(s).unwrap(); - if image_size.0 != bounds.w as usize || image_size.1 != bounds.h as usize { - canvas.delete_image(s); - canvas.delete_image(t); - - create_images(canvas, bounds.w as usize, bounds.h as usize) - } else { - (s, t) - } - } - - None => create_images(canvas, bounds.w as usize, bounds.h as usize), - }; - - self.cache.filter_image.insert(self.current, Some((source, target))); - - // TODO: Cache these - let screenshot = canvas.screenshot().unwrap(); - - let screenshot_image = - self.cache.screenshot_image.get(self.current).cloned().unwrap_or_default(); - - let screenshot_image_id = if let Some(s) = screenshot_image { - let image_size = canvas.image_size(s).unwrap(); - if image_size.0 != screenshot.width() || image_size.1 != screenshot.height() { - canvas.delete_image(s); - canvas.create_image(screenshot.as_ref(), femtovg::ImageFlags::empty()).unwrap() - } else { - canvas - .update_image(s, screenshot.as_ref(), 0, 0) - .expect("Failed to update image"); - s - } - } else { - canvas.create_image(screenshot.as_ref(), femtovg::ImageFlags::empty()).unwrap() - }; - - self.cache.screenshot_image.insert(self.current, Some(screenshot_image_id)); - - // Draw canvas to source image - canvas.save(); - canvas.set_render_target(femtovg::RenderTarget::Image(source)); - canvas.reset_scissor(); - canvas.reset_transform(); - canvas.clear_rect( - 0, - 0, - bounds.w as u32, - bounds.h as u32, - femtovg::Color::rgba(0, 0, 0, 0), - ); - let mut p = femtovg::Path::new(); - p.rect(0.0, 0.0, bounds.w, bounds.h); - canvas.fill_path( - &p, - &Paint::image( - screenshot_image_id, - -bounds.x, - -bounds.y, - window_width, - window_height, - 0.0, - 1.0, - ), - ); - - let blurred_image = if blur_radius > 0.0 { - canvas.filter_image(target, femtovg::ImageFilter::GaussianBlur { sigma }, source); - target - } else { - source - }; - canvas.restore(); - canvas.set_render_target(femtovg::RenderTarget::Screen); - - canvas.fill_path( - path, - &Paint::image(blurred_image, bounds.x, bounds.y, bounds.w, bounds.h, 0.0, 1.0), - ); - } - } - /// Draw background color or background image (including gradients) for the current view. - pub fn draw_background(&mut self, canvas: &mut Canvas, path: &mut Path) { + pub fn draw_background(&mut self, canvas: &Canvas, path: &Path) { let background_color = self.background_color(); - let paint = Paint::color(background_color.into()); - canvas.fill_path(path, &paint); + if background_color.a() != 0 { + let mut paint = Paint::default(); + paint.set_color(skia_safe::Color::from_argb( + background_color.a(), + background_color.r(), + background_color.g(), + background_color.b(), + )); + paint.set_anti_alias(true); + canvas.draw_path(path, &paint); + } self.draw_background_images(canvas, path); } - pub fn draw_text_and_selection(&mut self, canvas: &mut Canvas) { - if self.text_context.has_buffer(self.current) { - let mut bounds = self.bounds(); - let border_width = self.border_width(); - - bounds = bounds.shrink(border_width); - - let child_left = self.child_left(); - let child_right = self.child_right(); - let child_top = self.child_top(); - let child_bottom = self.child_bottom(); - - // shrink the bounding box based on pixel values - let left = child_left.to_px(self.bounds().w, 0.0); - let right = child_right.to_px(self.bounds().w, 0.0); - let top = child_top.to_px(self.bounds().h, 0.0); - let bottom = child_bottom.to_px(self.bounds().h, 0.0); - - bounds = bounds.shrink_sides(left, top, right, bottom); - - // Draw text - - let mut justify_x = match (child_left, child_right) { - (Stretch(left), Stretch(right)) => { - if left + right == 0.0 { - 0.5 - } else { - left / (left + right) - } - } - (Stretch(_), _) => 1.0, - _ => 0.0, - }; - - if let Some(text_align) = self.text_align() { - justify_x = match text_align { - TextAlign::Left => 0.0, - TextAlign::Right => 1.0, - TextAlign::Center => 0.5, - _ => 0.0, - }; - } - - let justify_y = match (child_top, child_bottom) { - (Stretch(top), Stretch(bottom)) => { - if top + bottom == 0.0 { - 0.5 - } else { - top / (top + bottom) - } - } - (Stretch(_), _) => 1.0, - _ => 0.0, - }; - - // let origin_x = box_x + box_w * justify_x; - // let origin_y = box_y + (box_h * justify_y).round(); - - // let justify_x = 0.0; - // let justify_y = 0.0; - // let origin_x = box_x; - // let origin_y = box_y; - - self.text_context.sync_styles(self.current, self.style); - - self.draw_text_selection(canvas, bounds, (justify_x, justify_y)); - self.draw_text_caret(canvas, bounds, (justify_x, justify_y), 1.0); - self.draw_text(canvas, bounds, (justify_x, justify_y)); - } - } + // /// Draw backdrop filters for the current view. + // pub fn draw_backdrop_filter(&mut self, canvas: &mut Canvas, path: &mut Path) { + // let window_width = self.cache.get_width(Entity::root()); + // let window_height = self.cache.get_height(Entity::root()); + // let bounds = self.bounds(); + + // let blur_radius = self.backdrop_filter().map(|filter| match filter { + // Filter::Blur(r) => r.to_px().unwrap_or_default(), + // }); + + // if let Some(blur_radius) = blur_radius { + // let sigma = blur_radius / 2.0; + + // let filter_image = + // self.cache.filter_image.get(self.current).cloned().unwrap_or_default(); + + // fn create_images(canvas: &mut Canvas, w: usize, h: usize) -> (ImageId, ImageId) { + // ( + // canvas + // .create_image_empty( + // w, + // h, + // femtovg::PixelFormat::Rgba8, + // femtovg::ImageFlags::FLIP_Y | femtovg::ImageFlags::PREMULTIPLIED, + // ) + // .unwrap(), + // canvas + // .create_image_empty( + // w, + // h, + // femtovg::PixelFormat::Rgba8, + // femtovg::ImageFlags::FLIP_Y | femtovg::ImageFlags::PREMULTIPLIED, + // ) + // .unwrap(), + // ) + // } + + // let (source, target) = match filter_image { + // Some((s, t)) => { + // let image_size = canvas.image_size(s).unwrap(); + // if image_size.0 != bounds.w as usize || image_size.1 != bounds.h as usize { + // canvas.delete_image(s); + // canvas.delete_image(t); + + // create_images(canvas, bounds.w as usize, bounds.h as usize) + // } else { + // (s, t) + // } + // } + + // None => create_images(canvas, bounds.w as usize, bounds.h as usize), + // }; + + // self.cache.filter_image.insert(self.current, Some((source, target))); + + // // TODO: Cache these + // let screenshot = canvas.screenshot().unwrap(); + + // let screenshot_image = + // self.cache.screenshot_image.get(self.current).cloned().unwrap_or_default(); + + // let screenshot_image_id = if let Some(s) = screenshot_image { + // let image_size = canvas.image_size(s).unwrap(); + // if image_size.0 != screenshot.width() || image_size.1 != screenshot.height() { + // canvas.delete_image(s); + // canvas.create_image(screenshot.as_ref(), femtovg::ImageFlags::empty()).unwrap() + // } else { + // canvas + // .update_image(s, screenshot.as_ref(), 0, 0) + // .expect("Failed to update image"); + // s + // } + // } else { + // canvas.create_image(screenshot.as_ref(), femtovg::ImageFlags::empty()).unwrap() + // }; + + // self.cache.screenshot_image.insert(self.current, Some(screenshot_image_id)); + + // // Draw canvas to source image + // canvas.save(); + // canvas.set_render_target(femtovg::RenderTarget::Image(source)); + // canvas.reset_scissor(); + // canvas.reset_transform(); + // canvas.clear_rect( + // 0, + // 0, + // bounds.w as u32, + // bounds.h as u32, + // femtovg::Color::rgba(0, 0, 0, 0), + // ); + // let mut p = femtovg::Path::new(); + // p.rect(0.0, 0.0, bounds.w, bounds.h); + // canvas.fill_path( + // &p, + // &Paint::image( + // screenshot_image_id, + // -bounds.x, + // -bounds.y, + // window_width, + // window_height, + // 0.0, + // 1.0, + // ), + // ); + + // let blurred_image = if blur_radius > 0.0 { + // canvas.filter_image(target, femtovg::ImageFilter::GaussianBlur { sigma }, source); + // target + // } else { + // source + // }; + // canvas.restore(); + // canvas.set_render_target(femtovg::RenderTarget::Screen); + + // canvas.fill_path( + // path, + // &Paint::image(blurred_image, bounds.x, bounds.y, bounds.w, bounds.h, 0.0, 1.0), + // ); + // } + // } + + // pub fn draw_text_and_selection(&mut self, canvas: &mut Canvas) { + // if self.text_context.has_buffer(self.current) { + // let mut bounds = self.bounds(); + // let border_width = self.border_width(); + + // bounds = bounds.shrink(border_width); + + // let child_left = self.child_left(); + // let child_right = self.child_right(); + // let child_top = self.child_top(); + // let child_bottom = self.child_bottom(); + + // // shrink the bounding box based on pixel values + // let left = child_left.to_px(self.bounds().w, 0.0); + // let right = child_right.to_px(self.bounds().w, 0.0); + // let top = child_top.to_px(self.bounds().h, 0.0); + // let bottom = child_bottom.to_px(self.bounds().h, 0.0); + + // bounds = bounds.shrink_sides(left, top, right, bottom); + + // // Draw text + + // let mut justify_x = match (child_left, child_right) { + // (Stretch(left), Stretch(right)) => { + // if left + right == 0.0 { + // 0.5 + // } else { + // left / (left + right) + // } + // } + // (Stretch(_), _) => 1.0, + // _ => 0.0, + // }; + + // if let Some(text_align) = self.text_align() { + // justify_x = match text_align { + // TextAlign::Left => 0.0, + // TextAlign::Right => 1.0, + // TextAlign::Center => 0.5, + // _ => 0.0, + // }; + // } + + // let justify_y = match (child_top, child_bottom) { + // (Stretch(top), Stretch(bottom)) => { + // if top + bottom == 0.0 { + // 0.5 + // } else { + // top / (top + bottom) + // } + // } + // (Stretch(_), _) => 1.0, + // _ => 0.0, + // }; + + // // let origin_x = box_x + box_w * justify_x; + // // let origin_y = box_y + (box_h * justify_y).round(); + + // // let justify_x = 0.0; + // // let justify_y = 0.0; + // // let origin_x = box_x; + // // let origin_y = box_y; + + // self.text_context.sync_styles(self.current, self.style); + + // self.draw_text_selection(canvas, bounds, (justify_x, justify_y)); + // self.draw_text_caret(canvas, bounds, (justify_x, justify_y), 1.0); + // self.draw_text(canvas, bounds, (justify_x, justify_y)); + // } + // } /// Draw the border of the current view. - pub fn draw_border(&mut self, canvas: &mut Canvas, path: &mut Path) { + pub fn draw_border(&mut self, canvas: &Canvas, path: &Path) { let border_color = self.border_color(); let border_width = self.border_width(); - let mut paint = Paint::color(border_color.into()); - paint.set_line_width(border_width); - canvas.stroke_path(path, &paint); + let mut paint = Paint::default(); + paint.set_style(PaintStyle::Stroke); + paint.set_color(border_color); + paint.set_stroke_width(border_width); + paint.set_anti_alias(true); + canvas.draw_path(path, &paint); } /// Draw the outline of the current view. - pub fn draw_outline(&mut self, canvas: &mut Canvas) { + pub fn draw_outline(&mut self, canvas: &Canvas) { let bounds = self.bounds(); let border_top_left_radius = self.border_top_left_radius(); @@ -740,343 +764,245 @@ impl<'a> DrawContext<'a> { let outline_offset = self.outline_offset(); let outline_color = self.outline_color(); - let mut outline_path = Path::new(); + // let mut outline_path = Path::new(); let half_outline_width = outline_width / 2.0; - outline_path.rounded_rect_varying( - bounds.x - half_outline_width - outline_offset, - bounds.y - half_outline_width - outline_offset, - bounds.w + outline_width + 2.0 * outline_offset, - bounds.h + outline_width + 2.0 * outline_offset, - border_top_left_radius * 1.5, - border_top_right_radius * 1.5, - border_bottom_right_radius * 1.5, - border_bottom_left_radius * 1.5, + let outline_path = RRect::new_rect_radii( + Rect::new( + bounds.x - half_outline_width - outline_offset, + bounds.y - half_outline_width - outline_offset, + bounds.x - half_outline_width - outline_offset + + bounds.w + + outline_width + + 2.0 * outline_offset, + bounds.y - half_outline_width - outline_offset + + bounds.h + + outline_width + + 2.0 * outline_offset, + ), + &[ + Point::new(border_top_left_radius * 1.5, border_top_left_radius * 1.5), + Point::new(border_top_right_radius * 1.5, border_top_left_radius * 1.5), + Point::new(border_top_left_radius * 1.5, border_top_left_radius * 1.5), + Point::new(border_top_left_radius * 1.5, border_top_left_radius * 1.5), + ], ); - let mut outline_paint = Paint::color(outline_color.into()); - outline_paint.set_line_width(outline_width); - canvas.stroke_path(&outline_path, &outline_paint); - } - - /// Draw inset box shadows for the current view. - pub fn draw_inset_box_shadows(&mut self, canvas: &mut Canvas, path: &mut Path) { - if let Some(box_shadows) = self.box_shadows() { - if box_shadows.is_empty() { - return; - } - - let mut shadow_images = - self.cache.shadow_images.get(self.current).cloned().unwrap_or_default(); - - if shadow_images.len() < box_shadows.len() { - shadow_images.resize(box_shadows.len(), None); - } else { - let excess = shadow_images.split_off(box_shadows.len()); - for (s, t) in excess.into_iter().flatten() { - canvas.delete_image(s); - canvas.delete_image(t); - } - } - - for (index, box_shadow) in - box_shadows.iter().enumerate().rev().filter(|(_, shadow)| shadow.inset) - { - let color = box_shadow.color.unwrap_or_default(); - let x_offset = box_shadow.x_offset.to_px().unwrap_or(0.0) * self.scale_factor(); - let y_offset = box_shadow.y_offset.to_px().unwrap_or(0.0) * self.scale_factor(); - let spread_radius = - box_shadow.spread_radius.as_ref().and_then(|l| l.to_px()).unwrap_or(0.0) - * self.scale_factor(); - - let blur_radius = - box_shadow.blur_radius.as_ref().and_then(|br| br.to_px()).unwrap_or(0.0); - let sigma = blur_radius / 2.0; - let d = (sigma * 5.0).ceil() + 2.0 * spread_radius + 20.0; - let bounds = self.bounds(); - - let (source, target) = - shadow_images[index].map(|(s, t)| (Some(s), Some(t))).unwrap_or((None, None)); - - fn create_images(canvas: &mut Canvas, w: usize, h: usize) -> (ImageId, ImageId) { - ( - canvas - .create_image_empty( - w, - h, - femtovg::PixelFormat::Rgba8, - femtovg::ImageFlags::FLIP_Y | femtovg::ImageFlags::PREMULTIPLIED, - ) - .unwrap(), - canvas - .create_image_empty( - w, - h, - femtovg::PixelFormat::Rgba8, - femtovg::ImageFlags::FLIP_Y | femtovg::ImageFlags::PREMULTIPLIED, - ) - .unwrap(), - ) - } - - let (source, target) = match (source, target) { - (Some(s), Some(t)) => { - if canvas.image_size(s).unwrap().0 != (bounds.w + d) as usize { - canvas.delete_image(s); - canvas.delete_image(t); - - create_images(canvas, (bounds.w + d) as usize, (bounds.h + d) as usize) - } else { - (s, t) - } - } - - (None, None) => { - create_images(canvas, (bounds.w + d) as usize, (bounds.h + d) as usize) - } - - _ => unreachable!(), - }; - - shadow_images[index] = Some((source, target)); - - canvas.save(); - canvas.set_render_target(femtovg::RenderTarget::Image(source)); - canvas.reset_scissor(); - canvas.reset_transform(); - canvas.clear_rect( - 0, - 0, - (bounds.w + d) as u32, - (bounds.h + d) as u32, - femtovg::Color::rgba(0, 0, 0, 0), - ); - - let scalex = 1.0 - (2.0 * spread_radius / bounds.w); - let scaley = 1.0 - (2.0 * spread_radius / bounds.h); - canvas.translate( - (-bounds.x - bounds.w / 2.0) * scalex, - (-bounds.y - bounds.h / 2.0) * scaley, - ); - canvas.scale(scalex, scaley); - canvas.translate( - (bounds.w / 2.0 + d / 2.0) / scalex, - (bounds.h / 2.0 + d / 2.0) / scaley, - ); - let paint = Paint::color(color.into()); - let mut shadow_path = path.clone(); - shadow_path.rect( - bounds.x - d / 2.0, - bounds.y - d / 2.0, - bounds.w + d, - bounds.h + d, - ); - - shadow_path.solidity(femtovg::Solidity::Hole); - canvas.fill_path(&shadow_path, &paint); - canvas.restore(); - - let target_image = if blur_radius > 0.0 { - canvas.filter_image( - target, - femtovg::ImageFilter::GaussianBlur { sigma }, - source, - ); - target - } else { - source - }; - - canvas.set_render_target(femtovg::RenderTarget::Screen); - canvas.save(); - - let paint = Paint::image( - target_image, - bounds.x - d / 2.0 + x_offset - 1.5, - bounds.y - d / 2.0 + y_offset - 1.5, - bounds.w + d + 3.0, - bounds.h + d + 3.0, - 0f32, - 1f32, - ); - - canvas.fill_path(path, &paint); - - canvas.restore(); - } - self.cache.shadow_images.insert(self.current, shadow_images); - } + // outline_path.rounded_rect_varying( + // border_top_left_radius * 1.5, + // border_top_right_radius * 1.5, + // border_bottom_right_radius * 1.5, + // border_bottom_left_radius * 1.5, + // ); + let mut outline_paint = Paint::default(); + outline_paint.set_color(outline_color); + outline_paint.set_stroke_width(outline_width); + outline_paint.set_style(PaintStyle::Stroke); + outline_paint.set_anti_alias(true); + canvas.draw_rrect(&outline_path, &outline_paint); } + // /// Draw inset box shadows for the current view. + // pub fn draw_inset_box_shadows(&mut self, canvas: &mut Canvas, path: &mut Path) { + // if let Some(box_shadows) = self.box_shadows() { + // if box_shadows.is_empty() { + // return; + // } + + // let mut shadow_images = + // self.cache.shadow_images.get(self.current).cloned().unwrap_or_default(); + + // if shadow_images.len() < box_shadows.len() { + // shadow_images.resize(box_shadows.len(), None); + // } else { + // let excess = shadow_images.split_off(box_shadows.len()); + // for (s, t) in excess.into_iter().flatten() { + // canvas.delete_image(s); + // canvas.delete_image(t); + // } + // } + + // for (index, box_shadow) in + // box_shadows.iter().enumerate().rev().filter(|(_, shadow)| shadow.inset) + // { + // let color = box_shadow.color.unwrap_or_default(); + // let x_offset = box_shadow.x_offset.to_px().unwrap_or(0.0) * self.scale_factor(); + // let y_offset = box_shadow.y_offset.to_px().unwrap_or(0.0) * self.scale_factor(); + // let spread_radius = + // box_shadow.spread_radius.as_ref().and_then(|l| l.to_px()).unwrap_or(0.0) + // * self.scale_factor(); + + // let blur_radius = + // box_shadow.blur_radius.as_ref().and_then(|br| br.to_px()).unwrap_or(0.0); + // let sigma = blur_radius / 2.0; + // let d = (sigma * 5.0).ceil() + 2.0 * spread_radius + 20.0; + + // let bounds = self.bounds(); + + // let (source, target) = + // shadow_images[index].map(|(s, t)| (Some(s), Some(t))).unwrap_or((None, None)); + + // fn create_images(canvas: &mut Canvas, w: usize, h: usize) -> (ImageId, ImageId) { + // ( + // canvas + // .create_image_empty( + // w, + // h, + // femtovg::PixelFormat::Rgba8, + // femtovg::ImageFlags::FLIP_Y | femtovg::ImageFlags::PREMULTIPLIED, + // ) + // .unwrap(), + // canvas + // .create_image_empty( + // w, + // h, + // femtovg::PixelFormat::Rgba8, + // femtovg::ImageFlags::FLIP_Y | femtovg::ImageFlags::PREMULTIPLIED, + // ) + // .unwrap(), + // ) + // } + + // let (source, target) = match (source, target) { + // (Some(s), Some(t)) => { + // if canvas.image_size(s).unwrap().0 != (bounds.w + d) as usize { + // canvas.delete_image(s); + // canvas.delete_image(t); + + // create_images(canvas, (bounds.w + d) as usize, (bounds.h + d) as usize) + // } else { + // (s, t) + // } + // } + + // (None, None) => { + // create_images(canvas, (bounds.w + d) as usize, (bounds.h + d) as usize) + // } + + // _ => unreachable!(), + // }; + + // shadow_images[index] = Some((source, target)); + + // canvas.save(); + // canvas.set_render_target(femtovg::RenderTarget::Image(source)); + // canvas.reset_scissor(); + // canvas.reset_transform(); + // canvas.clear_rect( + // 0, + // 0, + // (bounds.w + d) as u32, + // (bounds.h + d) as u32, + // femtovg::Color::rgba(0, 0, 0, 0), + // ); + + // let scalex = 1.0 - (2.0 * spread_radius / bounds.w); + // let scaley = 1.0 - (2.0 * spread_radius / bounds.h); + // canvas.translate( + // (-bounds.x - bounds.w / 2.0) * scalex, + // (-bounds.y - bounds.h / 2.0) * scaley, + // ); + // canvas.scale(scalex, scaley); + // canvas.translate( + // (bounds.w / 2.0 + d / 2.0) / scalex, + // (bounds.h / 2.0 + d / 2.0) / scaley, + // ); + // let paint = Paint::color(color.into()); + // let mut shadow_path = path.clone(); + // shadow_path.rect( + // bounds.x - d / 2.0, + // bounds.y - d / 2.0, + // bounds.w + d, + // bounds.h + d, + // ); + + // shadow_path.solidity(femtovg::Solidity::Hole); + // canvas.fill_path(&shadow_path, &paint); + // canvas.restore(); + + // let target_image = if blur_radius > 0.0 { + // canvas.filter_image( + // target, + // femtovg::ImageFilter::GaussianBlur { sigma }, + // source, + // ); + // target + // } else { + // source + // }; + + // canvas.set_render_target(femtovg::RenderTarget::Screen); + // canvas.save(); + + // let paint = Paint::image( + // target_image, + // bounds.x - d / 2.0 + x_offset - 1.5, + // bounds.y - d / 2.0 + y_offset - 1.5, + // bounds.w + d + 3.0, + // bounds.h + d + 3.0, + // 0f32, + // 1f32, + // ); + + // canvas.fill_path(path, &paint); + + // canvas.restore(); + // } + // self.cache.shadow_images.insert(self.current, shadow_images); + // } + // } + /// Draw non-inset box shadows for the current view. - pub fn draw_shadows(&mut self, canvas: &mut Canvas, path: &mut Path) { + pub fn draw_shadows(&mut self, canvas: &Canvas, path: &Path) { if let Some(box_shadows) = self.box_shadows() { if box_shadows.is_empty() { return; } - let mut shadow_images = - self.cache.shadow_images.get(self.current).cloned().unwrap_or_default(); - - if shadow_images.len() < box_shadows.len() { - shadow_images.resize(box_shadows.len(), None); - } else { - let excess = shadow_images.split_off(box_shadows.len()); - for (s, t) in excess.into_iter().flatten() { - canvas.delete_image(s); - canvas.delete_image(t); - } - } - let opacity = self.opacity(); for (index, box_shadow) in box_shadows.iter().enumerate().rev().filter(|(_, shadow)| !shadow.inset) { - let color = box_shadow.color.unwrap_or_default(); - let color = Color::rgba( - color.r(), - color.g(), - color.b(), - (opacity * color.a() as f32) as u8, - ); - let x_offset = box_shadow.x_offset.to_px().unwrap_or(0.0) * self.scale_factor(); - let y_offset = box_shadow.y_offset.to_px().unwrap_or(0.0) * self.scale_factor(); + let shadow_color = box_shadow.color.unwrap_or_default(); + + let shadow_x_offset = + box_shadow.x_offset.to_px().unwrap_or(0.0) * self.scale_factor(); + let shadow_y_offset = + box_shadow.y_offset.to_px().unwrap_or(0.0) * self.scale_factor(); let spread_radius = box_shadow.spread_radius.as_ref().and_then(|l| l.to_px()).unwrap_or(0.0) * self.scale_factor(); let blur_radius = box_shadow.blur_radius.as_ref().and_then(|br| br.to_px()).unwrap_or(0.0); - let sigma = blur_radius / 2.0; - let d = (sigma * 5.0).ceil() + 2.0 * spread_radius; - - let bounds = self.bounds(); - - let (source, target) = - shadow_images[index].map(|(s, t)| (Some(s), Some(t))).unwrap_or((None, None)); - - fn create_images(canvas: &mut Canvas, w: usize, h: usize) -> (ImageId, ImageId) { - ( - canvas - .create_image_empty( - w, - h, - femtovg::PixelFormat::Rgba8, - femtovg::ImageFlags::FLIP_Y | femtovg::ImageFlags::PREMULTIPLIED, - ) - .unwrap(), - canvas - .create_image_empty( - w, - h, - femtovg::PixelFormat::Rgba8, - femtovg::ImageFlags::FLIP_Y | femtovg::ImageFlags::PREMULTIPLIED, - ) - .unwrap(), - ) - } - let (source, target) = match (source, target) { - (Some(s), Some(t)) => { - let image_size = canvas.image_size(s).unwrap(); - if image_size.0 != (bounds.w + d) as usize - || image_size.1 != (bounds.h + d) as usize - { - canvas.delete_image(s); - canvas.delete_image(t); - - create_images(canvas, (bounds.w + d) as usize, (bounds.h + d) as usize) - } else { - (s, t) - } - } - - (None, None) => { - create_images(canvas, (bounds.w + d) as usize, (bounds.h + d) as usize) - } - - _ => unreachable!(), - }; + let mut shadow_paint = Paint::default(); + let mut shadow_path = path.clone(); - shadow_images[index] = Some((source, target)); + shadow_paint.set_color(shadow_color); - canvas.save(); - canvas.set_render_target(femtovg::RenderTarget::Image(source)); - canvas.reset_scissor(); - canvas.reset_transform(); - canvas.clear_rect( - 0, - 0, - (bounds.w + d) as u32, - (bounds.h + d) as u32, - femtovg::Color::rgba(0, 0, 0, 0), - ); - - let scalex = 1.0 + (2.0 * spread_radius / bounds.w); - let scaley = 1.0 + (2.0 * spread_radius / bounds.h); - canvas.translate( - (-bounds.x - bounds.w / 2.0) * scalex, - (-bounds.y - bounds.h / 2.0) * scaley, - ); - canvas.scale(scalex, scaley); - canvas.translate( - (bounds.w / 2.0 + d / 2.0) / scalex, - (bounds.h / 2.0 + d / 2.0) / scaley, - ); - let paint = Paint::color(color.into()); - canvas.fill_path(&path.clone(), &paint); - canvas.restore(); + if blur_radius > 0.0 { + shadow_paint.set_mask_filter(MaskFilter::blur( + BlurStyle::Normal, + blur_radius / 2.0, + false, + )); + } - let target_image = if blur_radius > 0.0 { - canvas.filter_image( - target, - femtovg::ImageFilter::GaussianBlur { sigma }, - source, - ); - target - } else { - source - }; + shadow_path.offset((shadow_x_offset, shadow_y_offset)); - canvas.set_render_target(femtovg::RenderTarget::Screen); canvas.save(); - canvas.translate(x_offset, y_offset); - let mut shadow_path = Path::new(); - shadow_path.rect( - bounds.x - d / 2.0, - bounds.y - d / 2.0, - bounds.w + d, - bounds.h + d, - ); - - canvas.fill_path( - &shadow_path, - &Paint::image( - target_image, - bounds.x - d / 2.0, - bounds.y - d / 2.0, - bounds.w + d, - bounds.h + d, - 0f32, - 1f32, - ), - ); - + canvas.clip_path(&path, ClipOp::Difference, true); + canvas.draw_path(&shadow_path, &shadow_paint); canvas.restore(); } - self.cache.shadow_images.insert(self.current, shadow_images); } } /// Draw background images (including gradients) for the current view. - fn draw_background_images(&self, canvas: &mut Canvas, path: &mut Path) { + fn draw_background_images(&self, canvas: &Canvas, path: &Path) { let bounds = self.bounds(); - let parent = self.tree.get_layout_parent(self.current).unwrap_or(Entity::root()); - - let parent_width = self.cache.get_width(parent); - let parent_height = self.cache.get_height(parent); - if let Some(images) = self.background_images() { let image_sizes = self.background_size(); @@ -1084,57 +1010,81 @@ impl<'a> DrawContext<'a> { match image { ImageOrGradient::Gradient(gradient) => match gradient { Gradient::Linear(linear_gradient) => { - let (start_x, start_y, end_x, end_y, parent_length) = - match linear_gradient.direction { - LineDirection::Horizontal(horizontal_keyword) => { - match horizontal_keyword { - HorizontalPositionKeyword::Left => { - (bounds.w, 0.0, 0.0, 0.0, parent_width) - } - - HorizontalPositionKeyword::Right => { - (0.0, 0.0, bounds.w, 0.0, parent_width) - } - } + let (start, end, parent_length) = match linear_gradient.direction { + LineDirection::Horizontal(horizontal_keyword) => { + match horizontal_keyword { + HorizontalPositionKeyword::Left => ( + bounds.center_right(), + bounds.center_left(), + bounds.width(), + ), + + HorizontalPositionKeyword::Right => ( + bounds.center_left(), + bounds.center_right(), + bounds.width(), + ), } + } - LineDirection::Vertical(vertical_keyword) => { - match vertical_keyword { - VerticalPositionKeyword::Top => { - (0.0, bounds.h, 0.0, 0.0, parent_height) - } - - VerticalPositionKeyword::Bottom => { - (0.0, 0.0, 0.0, bounds.h, parent_height) - } - } + LineDirection::Vertical(vertical_keyword) => match vertical_keyword + { + VerticalPositionKeyword::Top => ( + bounds.center_bottom(), + bounds.center_top(), + bounds.height(), + ), + + VerticalPositionKeyword::Bottom => ( + bounds.center_top(), + bounds.center_bottom(), + bounds.height(), + ), + }, + + LineDirection::Corner { horizontal, vertical } => { + match (horizontal, vertical) { + ( + HorizontalPositionKeyword::Right, + VerticalPositionKeyword::Bottom, + ) => ( + bounds.top_left(), + bounds.bottom_right(), + bounds.diagonal(), + ), + + ( + HorizontalPositionKeyword::Right, + VerticalPositionKeyword::Top, + ) => ( + bounds.bottom_left(), + bounds.top_right(), + bounds.diagonal(), + ), + + _ => (bounds.top_left(), bounds.bottom_right(), 0.0), } + } - LineDirection::Corner { horizontal, vertical } => { - match (horizontal, vertical) { - ( - HorizontalPositionKeyword::Right, - VerticalPositionKeyword::Bottom, - ) => (0.0, 0.0, bounds.w, bounds.h, parent_width), + LineDirection::Angle(angle) => { + let angle_rad = angle.to_radians(); + let start_x = + bounds.x + ((angle_rad.sin() * bounds.w) - bounds.w) / -2.0; + let end_x = + bounds.x + ((angle_rad.sin() * bounds.w) + bounds.w) / 2.0; + let start_y = + bounds.y + ((angle_rad.cos() * bounds.h) + bounds.h) / 2.0; + let end_y = + bounds.y + ((angle_rad.cos() * bounds.h) - bounds.h) / -2.0; - _ => (0.0, 0.0, 0.0, 0.0, 0.0), - } - } + let x = (end_x - start_x).abs(); + let y = (end_y - start_y).abs(); - LineDirection::Angle(angle) => { - let angle_rad = angle.to_radians(); - let start_x = - ((angle_rad.sin() * bounds.w) - bounds.w) / -2.0; - let end_x = ((angle_rad.sin() * bounds.w) + bounds.w) / 2.0; - let start_y = - ((angle_rad.cos() * bounds.h) + bounds.h) / 2.0; - let end_y = - ((angle_rad.cos() * bounds.h) - bounds.h) / -2.0; - - // TODO: Figure out what the parent length should be. - (start_x, start_y, end_x, end_y, parent_width) - } - }; + let dist = (x * x + y * y).sqrt(); + + ((start_x, start_y), (end_x, end_y), dist) + } + }; let num_stops = linear_gradient.stops.len(); @@ -1149,8 +1099,7 @@ impl<'a> DrawContext<'a> { } else { index as f32 / (num_stops - 1) as f32 }; - let col: femtovg::Color = stop.color.into(); - (pos, col) + (pos, skia_safe::Color::from(stop.color)) }) .collect::>(); @@ -1168,15 +1117,22 @@ impl<'a> DrawContext<'a> { } } - let paint = Paint::linear_gradient_stops( - bounds.x + start_x, - bounds.y + start_y, - bounds.x + end_x, - bounds.y + end_y, - stops, + let (offsets, colors): (Vec, Vec) = + stops.into_iter().unzip(); + + let shader = Shader::linear_gradient( + (Point::from(start), Point::from(end)), + GradientShaderColors::Colors(&colors[..]), + Some(&offsets[..]), + TileMode::Clamp, + None, + None, ); - canvas.fill_path(path, &paint); + let mut paint = Paint::default(); + paint.set_shader(shader); + + canvas.draw_path(path, &paint); } Gradient::Radial(radial_gradient) => { @@ -1188,13 +1144,13 @@ impl<'a> DrawContext<'a> { .enumerate() .map(|(index, stop)| { let pos = if let Some(pos) = &stop.position { - pos.to_pixels(parent_width, self.scale_factor()) - / parent_width + pos.to_pixels(bounds.width(), self.scale_factor()) + / bounds.width() } else { index as f32 / (num_stops - 1) as f32 }; - let col: femtovg::Color = stop.color.into(); - (pos, col) + + (pos, skia_safe::Color::from(stop.color)) }) .collect::>(); @@ -1211,155 +1167,173 @@ impl<'a> DrawContext<'a> { stops.push((1.0, last.1)); } } - let paint = Paint::radial_gradient_stops( - bounds.center().0, - bounds.center().1, - 0.0, + + let (offsets, colors): (Vec, Vec) = + stops.into_iter().unzip(); + + let shader = Shader::radial_gradient( + Point::from(bounds.center()), bounds.w.max(bounds.h), - stops, + GradientShaderColors::Colors(&colors[..]), + Some(&offsets[..]), + TileMode::Clamp, + None, + None, ); - canvas.fill_path(path, &paint); + let mut paint = Paint::default(); + paint.set_shader(shader); + + canvas.draw_path(path, &paint); } _ => {} }, - ImageOrGradient::Image(image_name) => { - if let Some(image) = self.resource_manager.images.get(image_name) { - match image.image { - ImageOrId::Id(id, dim) => { - let (width, height) = - if let Some(background_size) = image_sizes.get(index) { - match background_size { - BackgroundSize::Explicit { width, height } => { - let w = match width { - LengthPercentageOrAuto::LengthPercentage( - length, - ) => length - .to_pixels(bounds.w, self.scale_factor()), - LengthPercentageOrAuto::Auto => dim.0 as f32, - }; - - let h = match height { - LengthPercentageOrAuto::LengthPercentage( - length, - ) => length - .to_pixels(bounds.h, self.scale_factor()), - LengthPercentageOrAuto::Auto => dim.1 as f32, - }; - - (w, h) - } - - BackgroundSize::Contain => { - let image_ratio = dim.0 as f32 / dim.1 as f32; - let container_ratio = bounds.w / bounds.h; - - let (w, h) = if image_ratio > container_ratio { - (bounds.w, bounds.w / image_ratio) - } else { - (bounds.h * image_ratio, bounds.h) - }; - - (w, h) - } - - BackgroundSize::Cover => { - let image_ratio = dim.0 as f32 / dim.1 as f32; - let container_ratio = bounds.w / bounds.h; - - let (w, h) = if image_ratio < container_ratio { - (bounds.w, bounds.w / image_ratio) - } else { - (bounds.h * image_ratio, bounds.h) - }; - - (w, h) - } - } - } else { - (dim.0 as f32, dim.1 as f32) - }; - - let paint = Paint::image( - id, bounds.x, bounds.y, width, height, 0.0, 1.0, - ); - - canvas.fill_path(path, &paint); - } - - _ => {} - } - } - } + // ImageOrGradient::Image(image_name) => { + // if let Some(image) = self.resource_manager.images.get(image_name) { + // match image.image { + // ImageOrId::Id(id, dim) => { + // let (width, height) = + // if let Some(background_size) = image_sizes.get(index) { + // match background_size { + // BackgroundSize::Explicit { width, height } => { + // let w = match width { + // LengthPercentageOrAuto::LengthPercentage( + // length, + // ) => length + // .to_pixels(bounds.w, self.scale_factor()), + // LengthPercentageOrAuto::Auto => dim.0 as f32, + // }; + + // let h = match height { + // LengthPercentageOrAuto::LengthPercentage( + // length, + // ) => length + // .to_pixels(bounds.h, self.scale_factor()), + // LengthPercentageOrAuto::Auto => dim.1 as f32, + // }; + + // (w, h) + // } + + // BackgroundSize::Contain => { + // let image_ratio = dim.0 as f32 / dim.1 as f32; + // let container_ratio = bounds.w / bounds.h; + + // let (w, h) = if image_ratio > container_ratio { + // (bounds.w, bounds.w / image_ratio) + // } else { + // (bounds.h * image_ratio, bounds.h) + // }; + + // (w, h) + // } + + // BackgroundSize::Cover => { + // let image_ratio = dim.0 as f32 / dim.1 as f32; + // let container_ratio = bounds.w / bounds.h; + + // let (w, h) = if image_ratio < container_ratio { + // (bounds.w, bounds.w / image_ratio) + // } else { + // (bounds.h * image_ratio, bounds.h) + // }; + + // (w, h) + // } + // } + // } else { + // (dim.0 as f32, dim.1 as f32) + // }; + + // let paint = Paint::image( + // id, bounds.x, bounds.y, width, height, 0.0, 1.0, + // ); + + // canvas.fill_path(path, &paint); + // } + + // _ => {} + // } + // } + // } + _ => {} } } } } /// Draw any text for the current view. - pub fn draw_text(&mut self, canvas: &mut Canvas, bounds: BoundingBox, justify: (f32, f32)) { - if let Ok(draw_commands) = - self.text_context.fill_to_cmds(canvas, self.current, bounds, justify, *self.text_config) - { - let opacity = self.opacity(); - for (color, cmds) in draw_commands.into_iter() { - let font_color = Color::rgba( - color.r(), - color.g(), - color.b(), - (color.a() as f32 * opacity) as u8, - ); - let temp_paint = Paint::color(femtovg::Color::rgba( - font_color.r(), - font_color.g(), - font_color.b(), - font_color.a(), - )); - canvas.draw_glyph_commands(cmds, &temp_paint, 1.0); - } - } - } + pub fn draw_text(&mut self, canvas: &Canvas) { + if let Some(paragraph) = self.text_context.text_paragraphs.get(self.current) { + let bounds = self.bounds(); + let padding_left = self.child_left().to_px(bounds.width(), 0.0); + + let mut vertical_flex_sum = 0.0; + + let mut padding_top = match self.child_top() { + Units::Pixels(val) => val, + Units::Stretch(val) => { + vertical_flex_sum += val; + 0.0 + } + _ => 0.0, + }; + + let padding_bottom = match self.child_bottom() { + Units::Pixels(val) => val, + Units::Stretch(val) => { + vertical_flex_sum += val; + 0.0 + } + _ => 0.0, + }; - /// Draw the selection box for the text of the current view. - pub fn draw_text_selection( - &mut self, - canvas: &mut Canvas, - bounds: BoundingBox, - justify: (f32, f32), - ) { - let selections = self.text_context.layout_selection(self.current, bounds, justify); - if !selections.is_empty() { - let mut path = Path::new(); - for (x, y, w, h) in selections { - path.rect(x, y, w, h); + let vertical_free_space = + bounds.height() - paragraph.height() as f32 - padding_top - padding_bottom; + + if let Units::Stretch(val) = self.child_top() { + padding_top = (vertical_free_space * val / vertical_flex_sum).round() } - let selection_color = self.selection_color(); - canvas.fill_path(&path, &Paint::color(selection_color.into())); - } - } - /// Draw text caret for the current view. - pub fn draw_text_caret( - &mut self, - canvas: &mut Canvas, - bounds: BoundingBox, - justify: (f32, f32), - width: f32, - ) { - let caret_color = self.caret_color(); - if let Some((x, y, w, h)) = self.text_context.layout_caret( - self.current, - bounds, - justify, - self.logical_to_physical(width), - ) { - let mut path = Path::new(); - path.rect(x, y, w * self.scale_factor(), h); - canvas.fill_path(&path, &Paint::color(caret_color.into())); + // let mut paint = Paint::default(); + // paint.set_color(Color::green()); + // canvas.draw_rect( + // Rect::new( + // bounds.x + padding_left, + // bounds.y + padding_top, + // bounds.x + padding_left + paragraph.max_width(), + // bounds.y + padding_top + paragraph.height(), + // ), + // &paint, + // ); + + // println!("bounds.y {} padding_top: {} {}", bounds.y, padding_top, paragraph.height()); + + paragraph.paint(canvas, (bounds.x + padding_left, bounds.y + padding_top)); } + + // println!("{:?}", bounds.top_left()); } + + // /// Draw the selection box for the text of the current view. + // pub fn draw_text_selection( + // &mut self, + // canvas: &mut Canvas, + // bounds: BoundingBox, + // justify: (f32, f32), + // ) { + // let selections = self.text_context.layout_selection(self.current, bounds, justify); + // if !selections.is_empty() { + // let mut path = Path::new(); + // for (x, y, w, h) in selections { + // path.rect(x, y, w, h); + // } + // let selection_color = self.selection_color(); + // canvas.fill_path(&path, &Paint::color(selection_color.into())); + // } + // } } impl<'a> DataContext for DrawContext<'a> { diff --git a/crates/vizia_core/src/context/event.rs b/crates/vizia_core/src/context/event.rs index 319ac84e1..d13ef5dd9 100644 --- a/crates/vizia_core/src/context/event.rs +++ b/crates/vizia_core/src/context/event.rs @@ -4,7 +4,6 @@ use std::collections::{BinaryHeap, VecDeque}; use std::error::Error; use std::rc::Rc; -use femtovg::Transform2D; use hashbrown::{HashMap, HashSet}; use vizia_storage::{LayoutTreeIterator, TreeIterator}; @@ -294,67 +293,67 @@ impl<'a> EventContext<'a> { } /// Returns the transform of the current view. - pub fn transform(&self) -> Transform2D { - let mut transform = Transform2D::identity(); - - let bounds = self.bounds(); - let scale_factor = self.scale_factor(); - - // Apply transform origin. - let mut origin = self - .style - .transform_origin - .get(self.current) - .map(|transform_origin| { - let mut origin = Transform2D::new_translation(bounds.left(), bounds.top()); - let offset = transform_origin.as_transform(bounds, scale_factor); - origin.premultiply(&offset); - origin - }) - .unwrap_or(Transform2D::new_translation(bounds.center().0, bounds.center().1)); - transform.premultiply(&origin); - origin.inverse(); - - // Apply translation. - if let Some(translate) = self.style.translate.get(self.current) { - transform.premultiply(&translate.as_transform(bounds, scale_factor)); - } - - // Apply rotation. - if let Some(rotate) = self.style.rotate.get(self.current) { - transform.premultiply(&rotate.as_transform(bounds, scale_factor)); - } - - // Apply scaling. - if let Some(scale) = self.style.scale.get(self.current) { - transform.premultiply(&scale.as_transform(bounds, scale_factor)); - } - - // Apply transform functions. - if let Some(transforms) = self.style.transform.get(self.current) { - // Check if the transform is currently animating - // Get the animation state - // Manually interpolate the value to get the overall transform for the current frame - if let Some(animation_state) = self.style.transform.get_active_animation(self.current) { - if let Some(start) = animation_state.keyframes.first() { - if let Some(end) = animation_state.keyframes.last() { - let start_transform = start.value.as_transform(bounds, scale_factor); - let end_transform = end.value.as_transform(bounds, scale_factor); - let t = animation_state.t; - let animated_transform = - Transform2D::interpolate(&start_transform, &end_transform, t); - transform.premultiply(&animated_transform); - } - } - } else { - transform.premultiply(&transforms.as_transform(bounds, scale_factor)); - } - } - - transform.premultiply(&origin); - - transform - } + // pub fn transform(&self) -> Transform2D { + // let mut transform = Transform2D::identity(); + + // let bounds = self.bounds(); + // let scale_factor = self.scale_factor(); + + // // Apply transform origin. + // let mut origin = self + // .style + // .transform_origin + // .get(self.current) + // .map(|transform_origin| { + // let mut origin = Transform2D::new_translation(bounds.left(), bounds.top()); + // let offset = transform_origin.as_transform(bounds, scale_factor); + // origin.premultiply(&offset); + // origin + // }) + // .unwrap_or(Transform2D::new_translation(bounds.center().0, bounds.center().1)); + // transform.premultiply(&origin); + // origin.inverse(); + + // // Apply translation. + // if let Some(translate) = self.style.translate.get(self.current) { + // transform.premultiply(&translate.as_transform(bounds, scale_factor)); + // } + + // // Apply rotation. + // if let Some(rotate) = self.style.rotate.get(self.current) { + // transform.premultiply(&rotate.as_transform(bounds, scale_factor)); + // } + + // // Apply scaling. + // if let Some(scale) = self.style.scale.get(self.current) { + // transform.premultiply(&scale.as_transform(bounds, scale_factor)); + // } + + // // Apply transform functions. + // if let Some(transforms) = self.style.transform.get(self.current) { + // // Check if the transform is currently animating + // // Get the animation state + // // Manually interpolate the value to get the overall transform for the current frame + // if let Some(animation_state) = self.style.transform.get_active_animation(self.current) { + // if let Some(start) = animation_state.keyframes.first() { + // if let Some(end) = animation_state.keyframes.last() { + // let start_transform = start.value.as_transform(bounds, scale_factor); + // let end_transform = end.value.as_transform(bounds, scale_factor); + // let t = animation_state.t; + // let animated_transform = + // Transform2D::interpolate(&start_transform, &end_transform, t); + // transform.premultiply(&animated_transform); + // } + // } + // } else { + // transform.premultiply(&transforms.as_transform(bounds, scale_factor)); + // } + // } + + // transform.premultiply(&origin); + + // transform + // } /// Trigger an animation with the given id to play on the current view. pub fn play_animation(&mut self, anim_id: impl AnimId, duration: Duration) { @@ -1118,9 +1117,9 @@ impl<'a> EventContext<'a> { /// Sets the text of the current view. pub fn set_text(&mut self, text: &str) { - self.text_context.set_text(self.current, text); + // self.text_context.set_text(self.current, text); - self.style.needs_text_layout.insert(self.current, true); + self.style.needs_text_update(self.current); self.needs_relayout(); self.needs_redraw(); } diff --git a/crates/vizia_core/src/context/mod.rs b/crates/vizia_core/src/context/mod.rs index 738539ce6..57f93b168 100644 --- a/crates/vizia_core/src/context/mod.rs +++ b/crates/vizia_core/src/context/mod.rs @@ -9,6 +9,10 @@ mod proxy; mod resource; use log::debug; +use skia_safe::{ + textlayout::{FontCollection, TypefaceFontProvider}, + FontMgr, Surface, +}; use std::any::{Any, TypeId}; use std::cell::RefCell; use std::collections::{BinaryHeap, VecDeque}; @@ -20,8 +24,7 @@ use vizia_id::IdManager; use copypasta::ClipboardContext; #[cfg(feature = "clipboard")] use copypasta::{nop_clipboard::NopClipboardContext, ClipboardProvider}; -use cosmic_text::fontdb::Database; -use hashbrown::{hash_map::Entry, HashMap, HashSet}; +use hashbrown::{HashMap, HashSet}; pub use access::*; pub use draw::*; @@ -35,10 +38,9 @@ use crate::events::{TimedEvent, TimedEventHandle, TimerState, ViewHandler}; #[cfg(feature = "embedded_fonts")] use crate::fonts; -use crate::fonts::TABLER_ICONS; use crate::model::ModelDataStore; use crate::prelude::*; -use crate::resource::{ImageOrId, ResourceManager, StoredImage}; +use crate::resource::ResourceManager; use crate::text::{TextConfig, TextContext}; use vizia_input::MouseState; use vizia_storage::{ChildIterator, LayoutTreeIterator}; @@ -65,6 +67,7 @@ pub struct Context { pub(crate) entity_identifiers: HashMap, pub(crate) tree: Tree, pub(crate) current: Entity, + pub(crate) canvases: HashMap, pub(crate) views: Views, pub(crate) data: Models, pub(crate) bindings: Bindings, @@ -80,7 +83,6 @@ pub struct Context { pub(crate) style: Style, pub(crate) cache: CachedData, - pub(crate) canvases: HashMap, pub(crate) mouse: MouseState, pub(crate) modifiers: Modifiers, @@ -93,7 +95,7 @@ pub struct Context { pub(crate) resource_manager: ResourceManager, - pub(crate) text_context: TextContext, + pub text_context: TextContext, pub(crate) text_config: TextConfig, pub(crate) event_proxy: Option>, @@ -135,20 +137,17 @@ impl Context { let mut cache = CachedData::default(); cache.add(Entity::root()); - let mut db = Database::new(); - db.load_system_fonts(); + // // Add default fonts if the feature is enabled. + // #[cfg(feature = "embedded_fonts")] + // { + // db.load_font_data(Vec::from(fonts::ROBOTO_REGULAR)); + // db.load_font_data(Vec::from(fonts::ROBOTO_BOLD)); + // db.load_font_data(Vec::from(fonts::ROBOTO_ITALIC)); + // db.load_font_data(Vec::from(fonts::FIRACODE_REGULAR)) + // } - // Add default fonts if the feature is enabled. - #[cfg(feature = "embedded_fonts")] - { - db.load_font_data(Vec::from(fonts::ROBOTO_REGULAR)); - db.load_font_data(Vec::from(fonts::ROBOTO_BOLD)); - db.load_font_data(Vec::from(fonts::ROBOTO_ITALIC)); - db.load_font_data(Vec::from(fonts::FIRACODE_REGULAR)) - } - - // Add icon font - db.load_font_data(Vec::from(TABLER_ICONS)); + // // Add icon font + // db.load_font_data(Vec::from(TABLER_ICONS)); let mut result = Self { entity_manager: IdManager::new(), @@ -178,10 +177,51 @@ impl Context { focus_stack: Vec::new(), cursor_icon_locked: false, resource_manager: ResourceManager::new(), - text_context: TextContext::new_from_locale_and_db( - sys_locale::get_locale().unwrap_or_else(|| "en-US".to_owned()), - db, - ), + text_context: { + let default_font_manager = FontMgr::default(); + + TextContext { + font_collection: { + let mut font_collection = FontCollection::new(); + + #[cfg(feature = "embedded_fonts")] + { + let mut asset_provider = TypefaceFontProvider::new(); + + let ft_type = default_font_manager + .new_from_data(fonts::TABLER_ICONS, None) + .unwrap(); + asset_provider.register_typeface(ft_type, Some("tabler-icons")); + + let ft_type = + default_font_manager.new_from_data(fonts::FIRACODE, 0).unwrap(); + + asset_provider.register_typeface(ft_type, Some("Fira Code")); + + let ft_type = + default_font_manager.new_from_data(fonts::ROBOTO, 0).unwrap(); + + asset_provider.register_typeface(ft_type, Some("Roboto Flex")); + + let asset_font_manager: FontMgr = asset_provider.into(); + + font_collection.set_asset_font_manager(asset_font_manager.clone()); + } + + // for font in default_font_manager.family_names() { + // println!("{}", font); + // } + + font_collection + .set_default_font_manager(default_font_manager.clone(), "Roboto Flex"); + + font_collection + }, + text_bounds: Default::default(), + default_font_manager, + text_paragraphs: Default::default(), + } + }, text_config: TextConfig::default(), @@ -307,6 +347,10 @@ impl Context { if system_flags.contains(SystemFlags::RESTYLE) { self.needs_restyle(entity); } + + if system_flags.contains(SystemFlags::REFLOW) { + self.style.needs_text_update(entity); + } } /// Enables or disables PseudoClasses for the focus of an entity @@ -430,30 +474,30 @@ impl Context { self.captured = Entity::null(); } - // Remove any cached filter images associated with the entity. - if let Some(canvas) = self.canvases.get_mut(&Entity::root()) { - if let Some((s, t)) = self.cache.filter_image.get(*entity).cloned().flatten() { - canvas.delete_image(s); - canvas.delete_image(t); - } - } - - // Remove any cached screenshot images associated with the entity. - if let Some(canvas) = self.canvases.get_mut(&Entity::root()) { - if let Some(s) = self.cache.screenshot_image.get(*entity).cloned().flatten() { - canvas.delete_image(s); - } - } - - // Remove any cached shadow images associated with the entity. - if let Some(canvas) = self.canvases.get_mut(&Entity::root()) { - if let Some(shadows) = self.cache.shadow_images.get(*entity).cloned() { - for (s, t) in shadows.into_iter().flatten() { - canvas.delete_image(s); - canvas.delete_image(t); - } - } - } + // // Remove any cached filter images associated with the entity. + // if let Some(canvas) = self.canvases.get_mut(&Entity::root()) { + // if let Some((s, t)) = self.cache.filter_image.get(*entity).cloned().flatten() { + // canvas.delete_image(s); + // canvas.delete_image(t); + // } + // } + + // // Remove any cached screenshot images associated with the entity. + // if let Some(canvas) = self.canvases.get_mut(&Entity::root()) { + // if let Some(s) = self.cache.screenshot_image.get(*entity).cloned().flatten() { + // canvas.delete_image(s); + // } + // } + + // // Remove any cached shadow images associated with the entity. + // if let Some(canvas) = self.canvases.get_mut(&Entity::root()) { + // if let Some(shadows) = self.cache.shadow_images.get(*entity).cloned() { + // for (s, t) in shadows.into_iter().flatten() { + // canvas.delete_image(s); + // canvas.delete_image(t); + // } + // } + // } // Remove any map lenses associated with the entity. let ids = MAPS.with(|f| { @@ -495,8 +539,8 @@ impl Context { self.style.remove(*entity); self.data.remove(entity); self.views.remove(entity); - self.text_context.clear_buffer(*entity); - self.text_context.clear_bounds(*entity); + self.text_context.text_bounds.remove(*entity); + self.text_context.text_paragraphs.remove(*entity); self.entity_manager.destroy(*entity); } } @@ -546,15 +590,15 @@ impl Context { } pub fn add_font_mem(&mut self, data: impl AsRef<[u8]>) { - self.text_context.font_system().db_mut().load_font_data(data.as_ref().to_vec()); + // self.text_context.font_system().db_mut().load_font_data(data.as_ref().to_vec()); } /// Sets the global default font for the application. pub fn set_default_font(&mut self, names: &[&str]) { self.style.default_font = names .iter() - .map(|x| FamilyOwned::Name(x.to_string())) - .chain(std::iter::once(FamilyOwned::SansSerif)) + .map(|x| FamilyOwned::Named(x.to_string())) + .chain(std::iter::once(FamilyOwned::Generic(GenericFontFamily::SansSerif))) .collect(); } @@ -763,29 +807,29 @@ impl Context { image: image::DynamicImage, policy: ImageRetentionPolicy, ) { - match self.resource_manager.images.entry(path.to_string()) { - Entry::Occupied(mut occ) => { - occ.get_mut().image = ImageOrId::Image( - image, - femtovg::ImageFlags::REPEAT_X | femtovg::ImageFlags::REPEAT_Y, - ); - occ.get_mut().dirty = true; - occ.get_mut().retention_policy = policy; - } - Entry::Vacant(vac) => { - vac.insert(StoredImage { - image: ImageOrId::Image( - image, - femtovg::ImageFlags::REPEAT_X | femtovg::ImageFlags::REPEAT_Y, - ), - retention_policy: policy, - used: true, - dirty: false, - observers: HashSet::new(), - }); - } - } - self.style.needs_relayout(); + // match self.resource_manager.images.entry(path.to_string()) { + // Entry::Occupied(mut occ) => { + // occ.get_mut().image = ImageOrId::Image( + // image, + // femtovg::ImageFlags::REPEAT_X | femtovg::ImageFlags::REPEAT_Y, + // ); + // occ.get_mut().dirty = true; + // occ.get_mut().retention_policy = policy; + // } + // Entry::Vacant(vac) => { + // vac.insert(StoredImage { + // image: ImageOrId::Image( + // image, + // femtovg::ImageFlags::REPEAT_X | femtovg::ImageFlags::REPEAT_Y, + // ), + // retention_policy: policy, + // used: true, + // dirty: false, + // observers: HashSet::new(), + // }); + // } + // } + // self.style.needs_relayout(); } pub fn spawn(&self, target: F) diff --git a/crates/vizia_core/src/context/resource.rs b/crates/vizia_core/src/context/resource.rs index 67e44ea17..533bf566f 100644 --- a/crates/vizia_core/src/context/resource.rs +++ b/crates/vizia_core/src/context/resource.rs @@ -1,10 +1,11 @@ use hashbrown::{hash_map::Entry, HashSet}; +use skia_safe::Surface; use vizia_storage::Tree; use crate::{ entity::Entity, - resource::{ImageOrId, ImageRetentionPolicy, ResourceManager, StoredImage}, + resource::{ImageRetentionPolicy, ResourceManager, StoredImage}, style::Style, }; @@ -16,7 +17,7 @@ pub struct ResourceContext<'a> { pub(crate) current: Entity, pub(crate) event_proxy: &'a Option>, pub(crate) resource_manager: &'a mut ResourceManager, - pub(crate) canvases: &'a mut HashMap, + // pub(crate) canvases: &'a mut HashMap, pub(crate) style: &'a mut Style, pub(crate) tree: &'a Tree, } @@ -27,7 +28,7 @@ impl<'a> ResourceContext<'a> { current: cx.current, event_proxy: &cx.event_proxy, resource_manager: &mut cx.resource_manager, - canvases: &mut cx.canvases, + // canvases: &mut cx.canvases, style: &mut cx.style, tree: &cx.tree, } @@ -51,28 +52,28 @@ impl<'a> ResourceContext<'a> { image: image::DynamicImage, policy: ImageRetentionPolicy, ) { - match self.resource_manager.images.entry(path) { - Entry::Occupied(mut occ) => { - occ.get_mut().image = ImageOrId::Image( - image, - femtovg::ImageFlags::REPEAT_X | femtovg::ImageFlags::REPEAT_Y, - ); - occ.get_mut().dirty = true; - occ.get_mut().retention_policy = policy; - } - Entry::Vacant(vac) => { - vac.insert(StoredImage { - image: ImageOrId::Image( - image, - femtovg::ImageFlags::REPEAT_X | femtovg::ImageFlags::REPEAT_Y, - ), - retention_policy: policy, - used: true, - dirty: false, - observers: HashSet::new(), - }); - } - } - self.style.needs_relayout(); + // match self.resource_manager.images.entry(path) { + // Entry::Occupied(mut occ) => { + // occ.get_mut().image = ImageOrId::Image( + // image, + // femtovg::ImageFlags::REPEAT_X | femtovg::ImageFlags::REPEAT_Y, + // ); + // occ.get_mut().dirty = true; + // occ.get_mut().retention_policy = policy; + // } + // Entry::Vacant(vac) => { + // vac.insert(StoredImage { + // image: ImageOrId::Image( + // image, + // femtovg::ImageFlags::REPEAT_X | femtovg::ImageFlags::REPEAT_Y, + // ), + // retention_policy: policy, + // used: true, + // dirty: false, + // observers: HashSet::new(), + // }); + // } + // } + // self.style.needs_relayout(); } } diff --git a/crates/vizia_core/src/events/event_handler.rs b/crates/vizia_core/src/events/event_handler.rs index dcc5aa3e7..14acb5e9c 100644 --- a/crates/vizia_core/src/events/event_handler.rs +++ b/crates/vizia_core/src/events/event_handler.rs @@ -10,7 +10,7 @@ pub(crate) trait ViewHandler: Any { fn event(&mut self, cx: &mut EventContext, event: &mut Event); - fn draw(&self, cx: &mut DrawContext, canvas: &mut Canvas); + fn draw(&self, cx: &mut DrawContext, canvas: &Canvas); fn accessibility(&self, cx: &mut AccessContext, node: &mut AccessNode); diff --git a/crates/vizia_core/src/events/event_manager.rs b/crates/vizia_core/src/events/event_manager.rs index 7d5a5f705..23ca1f4ad 100644 --- a/crates/vizia_core/src/events/event_manager.rs +++ b/crates/vizia_core/src/events/event_manager.rs @@ -197,6 +197,8 @@ fn internal_state_updates(cx: &mut Context, window_event: &WindowEvent, meta: &m cx.mouse.cursorx = *x; cx.mouse.cursory = *y; + hover_system(cx); + mutate_direct_or_up(meta, cx.captured, cx.hovered, false); } @@ -205,7 +207,6 @@ fn internal_state_updates(cx: &mut Context, window_event: &WindowEvent, meta: &m // { // } - hover_system(cx); // if let Some(dropped_file) = cx.dropped_file.take() { // emit_direct_or_up( // cx, @@ -407,7 +408,7 @@ fn internal_state_updates(cx: &mut Context, window_event: &WindowEvent, meta: &m } } println!( - "{}{} {}{} [x: {} y: {} w: {} h: {}]", + "{}{} {}{} [x: {} y: {} w: {} h: {}] {:?}", indents(entity), entity, element_name, @@ -416,6 +417,19 @@ fn internal_state_updates(cx: &mut Context, window_event: &WindowEvent, meta: &m cache.get_bounds(entity).y, if w == f32::MAX { "inf".to_string() } else { w.to_string() }, if h == f32::MAX { "inf".to_string() } else { h.to_string() }, + cx.text_context.text_paragraphs.get(entity).map(|p| { + let mut r = p + .get_fonts() + .iter() + .map(|fi| { + (fi.text_range.clone(), fi.font.typeface().family_name()) + }) + .collect::>(); + + // r.pop(); + + r + }) ); } else if let Some(binding_name) = cx.bindings.get(&entity).map(|binding| format!("{:?}", binding)) @@ -474,18 +488,18 @@ fn internal_state_updates(cx: &mut Context, window_event: &WindowEvent, meta: &m if *code == Code::KeyT && cx.modifiers == Modifiers::CTRL | Modifiers::SHIFT | Modifiers::ALT { - debug!("Loaded font face info:"); - for face in cx.text_context.font_system().db().faces() { - debug!( - "family: {:?}\npost_script_name: {:?}\nstyle: {:?}\nweight: {:?}\nstretch: {:?}\nmonospaced: {:?}\n", - face.families, - face.post_script_name, - face.style, - face.weight, - face.stretch, - face.monospaced, - ); - } + // debug!("Loaded font face info:"); + // for face in cx.text_context.font_system().db().faces() { + // debug!( + // "family: {:?}\npost_script_name: {:?}\nstyle: {:?}\nweight: {:?}\nstretch: {:?}\nmonospaced: {:?}\n", + // face.families, + // face.post_script_name, + // face.style, + // face.weight, + // face.stretch, + // face.monospaced, + // ); + // } } if *code == Code::F5 { diff --git a/crates/vizia_core/src/fonts.rs b/crates/vizia_core/src/fonts.rs index e23ca3b0f..fbc51b663 100644 --- a/crates/vizia_core/src/fonts.rs +++ b/crates/vizia_core/src/fonts.rs @@ -1,6 +1,3 @@ -pub const ROBOTO_REGULAR: &[u8] = include_bytes!("../resources/fonts/Roboto-Regular.ttf"); -pub const ROBOTO_BOLD: &[u8] = include_bytes!("../resources/fonts/Roboto-Bold.ttf"); -pub const ROBOTO_ITALIC: &[u8] = include_bytes!("../resources/fonts/Roboto-Italic.ttf"); -pub const FIRACODE_REGULAR: &[u8] = include_bytes!("../resources/fonts/FiraCode-Regular.ttf"); - +pub const ROBOTO: &[u8] = include_bytes!("../resources/fonts/RobotoFlex.ttf"); +pub const FIRACODE: &[u8] = include_bytes!("../resources/fonts/FiraCode.ttf"); pub const TABLER_ICONS: &[u8] = include_bytes!("../resources/fonts/tabler-icons.ttf"); diff --git a/crates/vizia_core/src/icons.rs b/crates/vizia_core/src/icons.rs index 42efaa795..bf3b064a6 100644 --- a/crates/vizia_core/src/icons.rs +++ b/crates/vizia_core/src/icons.rs @@ -298,6 +298,10 @@ pub const ICON_BOX_MARGIN: &str = "\u{ee0b}"; pub const ICON_BOLD: &str = "\u{eb7b}"; pub const ICON_ITALIC: &str = "\u{eb93}"; pub const ICON_UNDERLINE: &str = "\u{eba2}"; +pub const ICON_ALIGN_LEFT: &str = "\u{ea09}"; +pub const ICON_ALIGN_CENTER: &str = "\u{ea07}"; +pub const ICON_ALIGN_RIGHT: &str = "\u{ea0a}"; +pub const ICON_ALIGN_JUSTIFIED: &str = "\u{ea08}"; pub const ICON_CODE: &str = "\u{ea77}"; pub const ICON_USER: &str = "\u{eb4d}"; diff --git a/crates/vizia_core/src/layout/bounds.rs b/crates/vizia_core/src/layout/bounds.rs index 564fd14c2..98656aa70 100644 --- a/crates/vizia_core/src/layout/bounds.rs +++ b/crates/vizia_core/src/layout/bounds.rs @@ -1,4 +1,4 @@ -use femtovg::Transform2D; +// use femtovg::Transform2D; /// Represents the axis-aligned bounding box of a view. #[derive(Clone, Copy, Debug, PartialEq)] @@ -97,25 +97,25 @@ impl BoundingBox { /// Bottom left point of bounds. #[inline(always)] pub fn bottom_left(&self) -> (f32, f32) { - (self.bottom(), self.left()) + (self.left(), self.bottom()) } /// Bottom right point of bounds. #[inline(always)] pub fn bottom_right(&self) -> (f32, f32) { - (self.bottom(), self.right()) + (self.right(), self.bottom()) } /// Top left point of bounds. #[inline(always)] pub fn top_left(&self) -> (f32, f32) { - (self.top(), self.left()) + (self.left(), self.top()) } /// Top right point of bounds. #[inline(always)] pub fn top_right(&self) -> (f32, f32) { - (self.top(), self.right()) + (self.right(), self.top()) } /// Shrinks by some `amount` in both directions and returns a new [`BoundingBox`]. @@ -228,11 +228,15 @@ impl BoundingBox { x_hit && y_hit } - pub fn transform(&self, transform: &Transform2D) -> Self { - let (tl, tt) = transform.transform_point(self.x, self.y); - let (tr, tb) = transform.transform_point(self.right(), self.bottom()); - BoundingBox::from_min_max(tl, tt, tr, tb) + pub fn diagonal(&self) -> f32 { + (self.width() * self.width() + self.height() * self.height()).sqrt() } + + // pub fn transform(&self, transform: &Transform2D) -> Self { + // let (tl, tt) = transform.transform_point(self.x, self.y); + // let (tr, tb) = transform.transform_point(self.right(), self.bottom()); + // BoundingBox::from_min_max(tl, tt, tr, tb) + // } } #[cfg(test)] diff --git a/crates/vizia_core/src/layout/cache.rs b/crates/vizia_core/src/layout/cache.rs index 01fefb877..b752b244e 100644 --- a/crates/vizia_core/src/layout/cache.rs +++ b/crates/vizia_core/src/layout/cache.rs @@ -44,11 +44,11 @@ impl Cache for CachedData { } if let Some(relative_position) = self.relative_position.get_mut(*node) { - if relative_position.x != posx { + if relative_position.x != posx.round() { geo_changed.set(GeoChanged::POSX_CHANGED, true); } - if relative_position.y != posy { + if relative_position.y != posy.round() { geo_changed.set(GeoChanged::POSY_CHANGED, true); } diff --git a/crates/vizia_core/src/layout/node.rs b/crates/vizia_core/src/layout/node.rs index 9dec74177..2c4e2748d 100644 --- a/crates/vizia_core/src/layout/node.rs +++ b/crates/vizia_core/src/layout/node.rs @@ -2,7 +2,7 @@ use morphorm::Node; use vizia_storage::MorphormChildIter; use crate::prelude::*; -use crate::resource::{ImageOrId, ResourceManager}; +use crate::resource::ResourceManager; use crate::text::TextContext; pub struct SubLayout<'a> { @@ -149,28 +149,29 @@ impl Node for Entity { width: Option, height: Option, ) -> Option<(f32, f32)> { - if sublayout.text_context.has_buffer(*self) { - // If the width is known use that, else use 0 for wrapping text or 999999 for non-wrapping text. - let max_width = if let Some(width) = width { - let child_left = - store.child_left.get(*self).cloned().unwrap_or_default().to_px(width, 0.0) - * store.scale_factor(); - let child_right = - store.child_right.get(*self).cloned().unwrap_or_default().to_px(width, 0.0) - * store.scale_factor(); - let border_width = store - .border_width - .get(*self) - .cloned() - .unwrap_or_default() - .to_pixels(0.0, store.scale_factor()); - - (width.ceil() - child_left - child_right - border_width - border_width) as i32 - } else if store.text_wrap.get(*self).copied().unwrap_or(true) { - 0 - } else { - 999999 - }; + if let Some(paragraph) = sublayout.text_context.text_paragraphs.get_mut(*self) { + // // If the width is known use that, else use 0 for wrapping text or 999999 for non-wrapping text. + // let max_width = if let Some(width) = width { + // let child_left = + // store.child_left.get(*self).cloned().unwrap_or_default().to_px(width, 0.0) + // * store.scale_factor(); + // let child_right = + // store.child_right.get(*self).cloned().unwrap_or_default().to_px(width, 0.0) + // * store.scale_factor(); + // let border_width = store + // .border_width + // .get(*self) + // .cloned() + // .unwrap_or_default() + // .to_pixels(0.0, store.scale_factor()); + + // width.ceil() - child_left - child_right - border_width - border_width + // } else { + // f32::MAX + // }; + + paragraph.layout(f32::MAX); + // println!("{}", paragraph.min_intrinsic_width()); let child_left = store.child_left.get(*self).copied().unwrap_or_default(); let child_right = store.child_right.get(*self).copied().unwrap_or_default(); @@ -180,10 +181,14 @@ impl Node for Entity { let mut child_space_x = 0.0; let mut child_space_y = 0.0; + let mut padding_left = 0.0; + let mut padding_top = 0.0; + // shrink the bounding box based on pixel values if let Pixels(val) = child_left { let val = val * store.scale_factor(); child_space_x += val; + padding_left += val; } if let Pixels(val) = child_right { let val = val * store.scale_factor(); @@ -192,6 +197,7 @@ impl Node for Entity { if let Pixels(val) = child_top { let val = val * store.scale_factor(); child_space_y += val; + padding_top += val; } if let Pixels(val) = child_bottom { let val = val * store.scale_factor(); @@ -208,62 +214,64 @@ impl Node for Entity { child_space_x += 2.0 * border_width; child_space_y += 2.0 * border_width; - sublayout.text_context.sync_styles(*self, store); - - let (text_width, mut text_height) = - sublayout.text_context.with_buffer(*self, |fs, buffer| { - buffer.set_size(fs, max_width as f32, f32::MAX); - - let w = buffer - .layout_runs() - .filter_map(|r| (!r.line_w.is_nan()).then_some(r.line_w)) - .max_by(|f1, f2| f1.partial_cmp(f2).unwrap()) - .unwrap_or_default(); - let lines = buffer.layout_runs().count(); + padding_left += border_width; + padding_top += border_width; - let h = lines as f32 * buffer.metrics().line_height; - (w, h) - }); - - if height.is_none() { - text_height = sublayout.text_context.with_buffer(*self, |fs, buffer| { - buffer.set_size(fs, text_width, f32::MAX); + let text_width = if store.text_wrap.get(*self).copied().unwrap_or(true) { + if let Some(width) = width { + width - child_space_x + } else { + paragraph.min_intrinsic_width().ceil() + } + } else if store.text_overflow.get(*self).is_some() { + if let Some(width) = width { + width - child_space_x + } else { + paragraph.max_intrinsic_width().ceil() + } + } else { + paragraph.max_intrinsic_width().ceil() + }; - let lines = buffer.layout_runs().count(); - lines as f32 * buffer.metrics().line_height - }); - } + paragraph.layout(text_width); - let height = - if let Some(height) = height { height } else { text_height + child_space_y }; - let width = if let Some(width) = width { width } else { text_width + child_space_x }; + let text_height = if let Some(height) = height { height } else { paragraph.height() }; - // Cache the text_width/ text_height in the text context so we can use it to compute transforms later - sublayout.text_context.set_bounds( + // // Cache the text_width/ text_height in the text context so we can use it to compute transforms later + sublayout.text_context.set_text_bounds( *self, - BoundingBox { w: text_width, h: text_height, ..Default::default() }, + BoundingBox { x: padding_left, y: padding_top, w: text_width, h: text_height }, ); - Some((width.ceil(), height.ceil())) + let width = + if let Some(width) = width { width } else { text_width.round() + child_space_x }; + + let height = if let Some(height) = height { + height + } else { + text_height.round() + child_space_y + }; + + Some((width, height)) } else if let Some(images) = store.background_image.get(*self) { let mut max_width = 0.0f32; let mut max_height = 0.0f32; - for image in images.iter() { - match image { - ImageOrGradient::Image(image_name) => { - if let Some(ImageOrId::Id(_, dim)) = sublayout - .resource_manager - .images - .get(image_name) - .map(|stored_img| &stored_img.image) - { - max_width = max_width.max(dim.0 as f32); - max_height = max_height.max(dim.1 as f32); - } - } - _ => {} - } - } + // for image in images.iter() { + // match image { + // ImageOrGradient::Image(image_name) => { + // if let Some(ImageOrId::Id(_, dim)) = sublayout + // .resource_manager + // .images + // .get(image_name) + // .map(|stored_img| &stored_img.image) + // { + // max_width = max_width.max(dim.0 as f32); + // max_height = max_height.max(dim.1 as f32); + // } + // } + // _ => {} + // } + // } let width = if let Some(width) = width { width } else { max_width }; let height = if let Some(height) = height { height } else { max_height }; diff --git a/crates/vizia_core/src/lib.rs b/crates/vizia_core/src/lib.rs index e5ed2a16c..e9a3bd4b8 100644 --- a/crates/vizia_core/src/lib.rs +++ b/crates/vizia_core/src/lib.rs @@ -35,7 +35,7 @@ mod storage; /// Contains types and functions used for custom drawing within views. This is a re-export of [femtovg](https://docs.rs/femtovg/latest/femtovg/). pub mod vg { - pub use femtovg::*; + pub use skia_safe::*; } /// Contains types and functions used for loading and manipulating images. This is a re-export of [image](https://docs.rs/image/latest/image/). @@ -54,7 +54,7 @@ pub mod backend { #[cfg(not(target_arch = "wasm32"))] pub use super::accessibility::IntoNode; pub use super::context::backend::BackendContext; - pub use super::text::cosmic::TextConfig; + pub use super::text::text_context::TextConfig; pub use vizia_window::WindowDescription; } @@ -88,10 +88,11 @@ pub mod prelude { }; pub use super::resource::ImageRetentionPolicy; pub use super::util::{IntoCssStr, CSS}; - pub use super::view::{Canvas, Handle, View}; + pub use super::view::{Handle, View}; pub use super::views::*; pub use super::window::{DropData, WindowEvent, WindowModifiers}; pub use accesskit::{Action, DefaultActionVerb, Live, Role}; + pub use skia_safe::Canvas; pub use vizia_derive::{Data, Lens}; pub use vizia_id::GenerationalId; pub use vizia_input::{Code, Key, KeyChord, Modifiers, MouseButton, MouseButtonState}; @@ -100,7 +101,6 @@ pub mod prelude { pub use super::style::*; - pub use cosmic_text::FamilyOwned; pub use instant::{Duration, Instant}; pub use morphorm::Units::*; pub use morphorm::{LayoutType, PositionType, Units}; diff --git a/crates/vizia_core/src/modifiers/style.rs b/crates/vizia_core/src/modifiers/style.rs index 12a820755..cb0a5e1b9 100644 --- a/crates/vizia_core/src/modifiers/style.rs +++ b/crates/vizia_core/src/modifiers/style.rs @@ -97,13 +97,13 @@ pub trait StyleModifiers: internal::Modifiable { abilities.set(Abilities::CHECKABLE, true); } - self.context().with_current(current, |cx| { - state.set_or_bind(cx, entity, |cx, val| { + self.context().with_current(current, move |cx| { + state.set_or_bind(cx, entity, move |cx, val| { let val = val.get(cx).into(); - if let Some(pseudo_classes) = cx.style.pseudo_classes.get_mut(cx.current) { + if let Some(pseudo_classes) = cx.style.pseudo_classes.get_mut(entity) { pseudo_classes.set(PseudoClassFlags::CHECKED, val); } - cx.needs_restyle(cx.current); + cx.needs_restyle(entity); }); }); diff --git a/crates/vizia_core/src/modifiers/text.rs b/crates/vizia_core/src/modifiers/text.rs index d8dde738b..d7eba07d1 100644 --- a/crates/vizia_core/src/modifiers/text.rs +++ b/crates/vizia_core/src/modifiers/text.rs @@ -11,9 +11,10 @@ pub trait TextModifiers: internal::Modifiable { value.set_or_bind(cx, entity, move |cx, val| { let cx: &mut EventContext<'_> = &mut EventContext::new_with_current(cx, entity); let text_data = val.get(cx).to_string_local(cx); - cx.text_context.set_text(entity, &text_data); + // cx.text_context.set_text(entity, &text_data); + cx.style.text.insert(entity, text_data); - cx.style.needs_text_layout.insert(entity, true); + cx.style.needs_text_update(entity); cx.needs_relayout(); cx.needs_redraw(); }); @@ -35,21 +36,21 @@ pub trait TextModifiers: internal::Modifiable { /// Sets the font weight that should be used by the view. font_weight, FontWeight, - SystemFlags::REDRAW + SystemFlags::REFLOW ); modifier!( /// Sets the font style that should be used by the view. - font_style, - FontStyle, - SystemFlags::REDRAW + font_slant, + FontSlant, + SystemFlags::REFLOW ); modifier!( /// Sets the font stretch that should be used by the view if the font supports it. - font_stretch, - FontStretch, - SystemFlags::REDRAW + font_width, + FontWidth, + SystemFlags::REFLOW ); /// Sets the text color of the view. @@ -59,6 +60,7 @@ pub trait TextModifiers: internal::Modifiable { self.context().with_current(current, move |cx| { value.set_or_bind(cx, entity, move |cx, v| { cx.style.font_color.insert(entity, v.get(cx).into()); + cx.style.needs_text_update(entity); cx.style.needs_redraw(); }); }); @@ -72,7 +74,7 @@ pub trait TextModifiers: internal::Modifiable { self.context().with_current(current, move |cx| { value.set_or_bind(cx, entity, move |cx, v| { cx.style.font_size.insert(cx.current, v.get(cx).into()); - cx.style.needs_text_layout.insert(cx.current, true); + cx.style.needs_text_update(entity); }); }); self @@ -103,7 +105,21 @@ pub trait TextModifiers: internal::Modifiable { /// Sets the horizontal alignment of text within the view. text_align, TextAlign, - SystemFlags::REDRAW + SystemFlags::REFLOW + ); + + modifier!( + /// Sets the text overflow. + text_overflow, + TextOverflow, + SystemFlags::REFLOW + ); + + modifier!( + /// Sets the max number of . + line_clamp, + LineClamp, + SystemFlags::REFLOW ); } diff --git a/crates/vizia_core/src/resource.rs b/crates/vizia_core/src/resource.rs index 9dce84bf1..c2777bc59 100644 --- a/crates/vizia_core/src/resource.rs +++ b/crates/vizia_core/src/resource.rs @@ -3,7 +3,7 @@ use crate::context::ResourceContext; use crate::entity::Entity; use crate::prelude::IntoCssStr; -use crate::view::Canvas; +// use crate::view::Canvas; use fluent_bundle::{FluentBundle, FluentResource}; use hashbrown::{HashMap, HashSet}; use image::GenericImageView; @@ -11,33 +11,33 @@ use std::borrow::Borrow; use unic_langid::LanguageIdentifier; pub(crate) struct StoredImage { - pub image: ImageOrId, + // pub image: ImageOrId, pub retention_policy: ImageRetentionPolicy, pub used: bool, pub dirty: bool, pub observers: HashSet, } -pub(crate) enum ImageOrId { - Image(image::DynamicImage, femtovg::ImageFlags), - Id(femtovg::ImageId, (u32, u32)), -} - -impl ImageOrId { - pub fn id(&mut self, canvas: &mut Canvas) -> femtovg::ImageId { - match self { - ImageOrId::Image(image, flags) => { - let image_ref: &image::DynamicImage = image.borrow(); - let res = canvas - .create_image(femtovg::ImageSource::try_from(image_ref).unwrap(), *flags) - .unwrap(); - *self = ImageOrId::Id(res, image.dimensions()); - res - } - ImageOrId::Id(i, _) => *i, - } - } -} +// pub(crate) enum ImageOrId { +// Image(image::DynamicImage, femtovg::ImageFlags), +// Id(femtovg::ImageId, (u32, u32)), +// } + +// impl ImageOrId { +// pub fn id(&mut self, canvas: &mut Canvas) -> femtovg::ImageId { +// match self { +// ImageOrId::Image(image, flags) => { +// let image_ref: &image::DynamicImage = image.borrow(); +// let res = canvas +// .create_image(femtovg::ImageSource::try_from(image_ref).unwrap(), *flags) +// .unwrap(); +// *self = ImageOrId::Id(res, image.dimensions()); +// res +// } +// ImageOrId::Id(i, _) => *i, +// } +// } +// } #[derive(Copy, Clone, PartialEq)] pub enum ImageRetentionPolicy { diff --git a/crates/vizia_core/src/style/mod.rs b/crates/vizia_core/src/style/mod.rs index 89936875c..b69efd55e 100644 --- a/crates/vizia_core/src/style/mod.rs +++ b/crates/vizia_core/src/style/mod.rs @@ -69,11 +69,11 @@ use crate::prelude::*; pub use vizia_style::{ Angle, BackgroundImage, BackgroundSize, BorderCornerShape, BoxShadow, ClipPath, Color, CssRule, - CursorIcon, Display, Filter, FontFamily, FontSize, FontStretch, FontStyle, FontWeight, - FontWeightKeyword, GenericFontFamily, Gradient, HorizontalPosition, HorizontalPositionKeyword, - Length, LengthOrPercentage, LengthValue, LineDirection, LinearGradient, Matrix, Opacity, - Overflow, PointerEvents, Position, Scale, TextAlign, Transform, Transition, Translate, - VerticalPosition, VerticalPositionKeyword, Visibility, RGBA, + CursorIcon, Display, Filter, FontFamily, FontSize, FontSlant, FontWeight, FontWeightKeyword, + FontWidth, GenericFontFamily, Gradient, HorizontalPosition, HorizontalPositionKeyword, Length, + LengthOrPercentage, LengthValue, LineClamp, LineDirection, LinearGradient, Matrix, Opacity, + Overflow, PointerEvents, Position, Scale, TextAlign, TextOverflow, Transform, Transition, + Translate, VerticalPosition, VerticalPositionKeyword, Visibility, RGBA, }; use vizia_style::{ @@ -86,8 +86,8 @@ pub(crate) use rule::Rule; mod pseudoclass; pub(crate) use pseudoclass::*; -mod transform; -pub(crate) use transform::*; +// mod transform; +// pub(crate) use transform::*; use crate::animation::{AnimationState, Interpolator, Keyframe, TimingFunction}; use crate::storage::animatable_set::AnimatableSet; @@ -144,6 +144,12 @@ pub enum ImageOrGradient { Gradient(Gradient), } +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum FamilyOwned { + Generic(GenericFontFamily), + Named(String), +} + /// Stores the style properties of all entities in the application. pub struct Style { pub(crate) rule_manager: IdManager, @@ -237,14 +243,17 @@ pub struct Style { pub(crate) box_shadow: AnimatableSet>, // Text & Font + pub text: SparseSet, pub(crate) text_wrap: StyleSet, + pub(crate) text_overflow: StyleSet, + pub(crate) line_clamp: StyleSet, pub(crate) text_align: StyleSet, pub(crate) font_family: StyleSet>, pub(crate) font_color: AnimatableSet, pub(crate) font_size: AnimatableSet, pub(crate) font_weight: StyleSet, - pub(crate) font_style: StyleSet, - pub(crate) font_stretch: StyleSet, + pub(crate) font_slant: StyleSet, + pub(crate) font_width: StyleSet, pub(crate) caret_color: AnimatableSet, pub(crate) selection_color: AnimatableSet, @@ -303,7 +312,8 @@ pub struct Style { // TODO: When we can do incremental updates on a per entity basis, change this to a bitflag // for layout, text layout, rendering, etc. to replace the above `needs_` members. - pub needs_text_layout: SparseSet, + pub text_construction: qfilter::Filter, + pub text_layout: qfilter::Filter, pub reaccess: qfilter::Filter, @@ -334,6 +344,7 @@ impl Default for Style { hidden: Default::default(), text_value: Default::default(), numeric_value: Default::default(), + text: Default::default(), display: Default::default(), visibility: Default::default(), opacity: Default::default(), @@ -365,13 +376,15 @@ impl Default for Style { background_size: Default::default(), box_shadow: Default::default(), text_wrap: Default::default(), + text_overflow: Default::default(), + line_clamp: Default::default(), text_align: Default::default(), font_family: Default::default(), font_color: Default::default(), font_size: Default::default(), font_weight: Default::default(), - font_style: Default::default(), - font_stretch: Default::default(), + font_slant: Default::default(), + font_width: Default::default(), caret_color: Default::default(), selection_color: Default::default(), cursor: Default::default(), @@ -404,7 +417,8 @@ impl Default for Style { max_bottom: Default::default(), system_flags: Default::default(), restyle: qfilter::Filter::new_resizeable(10000, 10000000, 0.01), - needs_text_layout: Default::default(), + text_construction: qfilter::Filter::new_resizeable(10000, 10000000, 0.01), + text_layout: qfilter::Filter::new_resizeable(10000, 10000000, 0.01), reaccess: qfilter::Filter::new_resizeable(10000, 10000000, 0.01), dpi_factor: Default::default(), } @@ -1473,14 +1487,8 @@ impl Style { font_family .iter() .map(|family| match family { - FontFamily::Named(name) => FamilyOwned::Name(name.to_string()), - FontFamily::Generic(generic) => match generic { - GenericFontFamily::Serif => FamilyOwned::Serif, - GenericFontFamily::SansSerif => FamilyOwned::SansSerif, - GenericFontFamily::Cursive => FamilyOwned::Cursive, - GenericFontFamily::Fantasy => FamilyOwned::Fantasy, - GenericFontFamily::Monospace => FamilyOwned::Monospace, - }, + FontFamily::Named(name) => FamilyOwned::Named(name.to_string()), + FontFamily::Generic(generic) => FamilyOwned::Generic(*generic), }) .collect::>(), ); @@ -1501,14 +1509,14 @@ impl Style { self.font_weight.insert_rule(rule_id, font_weight); } - // Font Style - Property::FontStyle(font_style) => { - self.font_style.insert_rule(rule_id, font_style); + // Font Slant + Property::FontSlant(font_slant) => { + self.font_slant.insert_rule(rule_id, font_slant); } - // Font Stretch - Property::FontStretch(font_stretch) => { - self.font_stretch.insert_rule(rule_id, font_stretch); + // Font Width + Property::FontWidth(font_width) => { + self.font_width.insert_rule(rule_id, font_width); } // Caret Color @@ -1640,7 +1648,12 @@ impl Style { Property::Custom(custom) => { warn!("Custom Property: {}", custom.name); } - + Property::TextOverflow(text_overflow) => { + self.text_overflow.insert_rule(rule_id, text_overflow); + } + Property::LineClamp(line_clamp) => { + self.line_clamp.insert_rule(rule_id, line_clamp); + } _ => {} } } @@ -1750,14 +1763,17 @@ impl Style { self.box_shadow.remove(entity); // Text and Font + self.text.remove(entity); self.text_wrap.remove(entity); + self.text_overflow.remove(entity); + self.line_clamp.remove(entity); self.text_align.remove(entity); self.font_family.remove(entity); self.font_color.remove(entity); self.font_size.remove(entity); self.font_weight.remove(entity); - self.font_style.remove(entity); - self.font_stretch.remove(entity); + self.font_slant.remove(entity); + self.font_width.remove(entity); self.caret_color.remove(entity); self.selection_color.remove(entity); @@ -1783,8 +1799,8 @@ impl Style { self.child_right.remove(entity); self.child_top.remove(entity); self.child_bottom.remove(entity); - self.col_between.remove(entity); self.row_between.remove(entity); + self.col_between.remove(entity); // Size self.width.remove(entity); @@ -1805,12 +1821,11 @@ impl Style { self.max_top.remove(entity); self.min_bottom.remove(entity); self.max_bottom.remove(entity); - - self.needs_text_layout.remove(entity); } pub fn needs_restyle(&mut self) { self.system_flags.set(SystemFlags::RESTYLE, true); + self.system_flags.set(SystemFlags::REDRAW, true); } pub fn needs_relayout(&mut self) { @@ -1825,6 +1840,15 @@ impl Style { self.reaccess.insert(entity).unwrap(); } + pub fn needs_text_update(&mut self, entity: Entity) { + self.text_construction.insert(entity).unwrap(); + self.text_layout.insert(entity).unwrap(); + } + + pub fn needs_text_layout(&mut self, entity: Entity) { + self.text_layout.insert(entity).unwrap(); + } + pub fn should_redraw(&mut self, f: F) { if self.system_flags.contains(SystemFlags::REDRAW) { f(); @@ -1927,10 +1951,12 @@ impl Style { // Text and Font self.text_wrap.clear_rules(); + self.text_overflow.clear_rules(); + self.line_clamp.clear_rules(); self.text_align.clear_rules(); self.font_family.clear_rules(); self.font_weight.clear_rules(); - self.font_style.clear_rules(); + self.font_slant.clear_rules(); self.font_color.clear_rules(); self.font_size.clear_rules(); self.selection_color.clear_rules(); diff --git a/crates/vizia_core/src/style/transform.rs b/crates/vizia_core/src/style/transform.rs index c4be8a514..7d108c832 100644 --- a/crates/vizia_core/src/style/transform.rs +++ b/crates/vizia_core/src/style/transform.rs @@ -1,4 +1,4 @@ -use femtovg::Transform2D; +// use femtovg::Transform2D; use vizia_style::{Angle, Scale, Transform, Translate}; use crate::layout::BoundingBox; diff --git a/crates/vizia_core/src/systems/animation.rs b/crates/vizia_core/src/systems/animation.rs index 9144e393d..285b52e3c 100644 --- a/crates/vizia_core/src/systems/animation.rs +++ b/crates/vizia_core/src/systems/animation.rs @@ -24,8 +24,6 @@ pub(crate) fn animation_system(cx: &mut Context) -> bool { | cx.style.background_size.tick(time) // Box Shadow | cx.style.box_shadow.tick(time) - // Font Color - | cx.style.font_color.tick(time) // Transform | cx.style.transform.tick(time) | cx.style.transform_origin.tick(time) @@ -39,12 +37,16 @@ pub(crate) fn animation_system(cx: &mut Context) -> bool { // Clip Path | cx.style.clip_path.tick(time); + let needs_reflow = + // Font Color + cx.style.font_color.tick(time) + // Font Size + | cx.style.font_size.tick(time); + // Properties which affect layout let needs_relayout = cx.style.display.tick(time) // Border Width | cx.style.border_width.tick(time) - // Font Size - | cx.style.font_size.tick(time) // Space | cx.style.left.tick(time) | cx.style.right.tick(time) @@ -84,5 +86,9 @@ pub(crate) fn animation_system(cx: &mut Context) -> bool { cx.style.system_flags.set(SystemFlags::REDRAW, true); } - needs_redraw | needs_relayout + if needs_reflow { + // + } + + needs_redraw | needs_relayout | needs_reflow } diff --git a/crates/vizia_core/src/systems/draw.rs b/crates/vizia_core/src/systems/draw.rs index bef5c51c4..4576b57cf 100644 --- a/crates/vizia_core/src/systems/draw.rs +++ b/crates/vizia_core/src/systems/draw.rs @@ -1,17 +1,16 @@ use crate::prelude::*; +use skia_safe::ClipOp; use std::cmp::Ordering; use std::collections::BinaryHeap; use vizia_storage::LayoutChildIterator; pub(crate) fn draw_system(cx: &mut Context) { - let canvas = cx.canvases.get_mut(&Entity::root()).unwrap(); + let canvas = cx.canvases.get_mut(&Entity::root()).unwrap().canvas(); cx.resource_manager.mark_images_unused(); - let window_width = cx.cache.get_width(Entity::root()); - let window_height = cx.cache.get_height(Entity::root()); + let clear_color = - cx.style.background_color.get(Entity::root()).cloned().unwrap_or(RGBA::TRANSPARENT.into()); - canvas.set_size(window_width as u32, window_height as u32, 1.0); - canvas.clear_rect(0, 0, window_width as u32, window_height as u32, clear_color.into()); + cx.style.background_color.get(Entity::root()).cloned().unwrap_or(Color::transparent()); + canvas.clear(clear_color); let mut queue = BinaryHeap::new(); queue.push(ZEntity { index: 0, entity: Entity::root(), opacity: 1.0, visible: true }); @@ -41,12 +40,12 @@ pub(crate) fn draw_system(cx: &mut Context) { canvas.restore(); } - canvas.flush(); + // canvas.flush(); } fn draw_entity( cx: &mut DrawContext, - canvas: &mut Canvas, + canvas: &Canvas, current_z: i32, queue: &mut BinaryHeap, visible: bool, @@ -68,11 +67,12 @@ fn draw_entity( canvas.save(); - canvas.set_transform(&cx.transform()); - - let clip_region = cx.clip_region(); + // canvas.set_transform(&cx.transform()); - canvas.intersect_scissor(clip_region.x, clip_region.y, clip_region.w, clip_region.h); + if let Some(clip_path) = cx.clip_region() { + canvas.clip_path(&clip_path, ClipOp::Intersect, true); + } + // canvas.intersect_scissor(clip_region.x, clip_region.y, clip_region.w, clip_region.h); let is_visible = match (visible, cx.visibility()) { (v, None) => v, diff --git a/crates/vizia_core/src/systems/hover.rs b/crates/vizia_core/src/systems/hover.rs index c5a9bea88..7b9b5997c 100644 --- a/crates/vizia_core/src/systems/hover.rs +++ b/crates/vizia_core/src/systems/hover.rs @@ -1,7 +1,7 @@ use std::{cmp::Ordering, collections::BinaryHeap}; use crate::prelude::*; -use femtovg::Transform2D; +// use femtovg::Transform2D; use log::debug; use vizia_storage::{LayoutChildIterator, LayoutParentIterator}; @@ -18,7 +18,7 @@ pub(crate) fn hover_system(cx: &mut Context) { cx.style.pointer_events.get(Entity::root()).copied().unwrap_or_default().into(); queue.push(ZEntity { index: 0, pointer_events, entity: Entity::root() }); let mut hovered = Entity::root(); - let transform = Transform2D::identity(); + // let transform = Transform2D::identity(); // let clip_bounds = cx.cache.get_bounds(Entity::root()); let clip_bounds: BoundingBox = BoundingBox { x: -f32::MAX / 2.0, y: -f32::MAX / 2.0, w: f32::MAX, h: f32::MAX }; @@ -31,7 +31,7 @@ pub(crate) fn hover_system(cx: &mut Context) { zentity.pointer_events, &mut queue, &mut hovered, - transform, + // transform, &clip_bounds, ); }); @@ -51,7 +51,7 @@ pub(crate) fn hover_system(cx: &mut Context) { if hovered != cx.hovered { // Useful for debugging - debug!( + println!( "Hover changed to {:?} parent: {:?}, view: {}, posx: {}, posy: {} width: {} height: {}", hovered, cx.tree.get_parent(hovered), @@ -88,7 +88,7 @@ fn hover_entity( parent_pointer_events: bool, queue: &mut BinaryHeap, hovered: &mut Entity, - parent_transform: Transform2D, + // parent_transform: Transform2D, clip_bounds: &BoundingBox, ) { // Skip if non-hoverable (will skip any descendants) @@ -136,17 +136,18 @@ fn hover_entity( return; } - let mut transform = parent_transform; + // let mut transform = parent_transform; - transform.premultiply(&cx.transform()); - - let mut t = transform; - t.inverse(); - let (tx, ty) = t.transform_point(cursorx, cursory); + // transform.premultiply(&cx.transform()); + // let mut t = transform; + // t.inverse(); + // let (tx, ty) = t.transform_point(cursorx, cursory); + let (tx, ty) = (cursorx, cursory); let clipping = clip_bounds.intersection(&cx.clip_region()); let b = bounds.intersection(&clipping); + // let b = bounds; if let Some(pseudo_classes) = cx.style.pseudo_classes.get_mut(cx.current) { pseudo_classes.set(PseudoClassFlags::HOVER, false); @@ -189,7 +190,7 @@ fn hover_entity( let child_iter = LayoutChildIterator::new(cx.tree, cx.current); for child in child_iter { cx.current = child; - hover_entity(cx, current_z, pointer_events, queue, hovered, transform, &clipping); + hover_entity(cx, current_z, pointer_events, queue, hovered, &clipping); } } diff --git a/crates/vizia_core/src/systems/image.rs b/crates/vizia_core/src/systems/image.rs index 2a984d478..3c424aaa6 100644 --- a/crates/vizia_core/src/systems/image.rs +++ b/crates/vizia_core/src/systems/image.rs @@ -1,6 +1,7 @@ -use crate::context::ResourceContext; -use crate::resource::StoredImage; -use crate::{prelude::*, resource::ImageOrId}; +use crate::context::{Context, ResourceContext}; +use crate::prelude::*; +use crate::resource::{ImageRetentionPolicy, StoredImage}; +use crate::style::ImageOrGradient; use hashbrown::HashSet; // Iterate the tree and load any images used by entities which aren't already loaded. Remove any images no longer being used. @@ -42,50 +43,50 @@ fn load_image(cx: &mut ResourceContext, entity: Entity, image_name: &str) { } fn try_load_image(cx: &mut ResourceContext, entity: Entity, image_name: &str) -> bool { - // Check if the image is already loaded - if let Some(image_store) = cx.resource_manager.images.get_mut(image_name) { - match &image_store.image { - // Image exists and is already loaded so just add this entity as an observer and mark image as used - ImageOrId::Id(_, _) => { - // TODO: check if the image is actually the same? - image_store.observers.insert(entity); - image_store.used = true; - } + // // Check if the image is already loaded + // if let Some(image_store) = cx.resource_manager.images.get_mut(image_name) { + // match &image_store.image { + // // Image exists and is already loaded so just add this entity as an observer and mark image as used + // ImageOrId::Id(_, _) => { + // // TODO: check if the image is actually the same? + // image_store.observers.insert(entity); + // image_store.used = true; + // } - // Image exists but isn't loaded yet - ImageOrId::Image(_, _) => { - if let Some(canvas) = cx.canvases.get_mut(&Entity::root()) { - // This loads the image and sets the image id - image_store.image.id(canvas); - image_store.used = true; - cx.style.needs_relayout(); - cx.style.needs_redraw(); - } - } - } + // // Image exists but isn't loaded yet + // ImageOrId::Image(_, _) => { + // if let Some(canvas) = cx.canvases.get_mut(&Entity::root()) { + // // This loads the image and sets the image id + // image_store.image.id(canvas); + // image_store.used = true; + // cx.style.needs_relayout(); + // cx.style.needs_redraw(); + // } + // } + // } - return true; - } else { - // Image doesn't exist yet so load and show placeholder image - // TODO: Add way to configure the placeholder image - cx.resource_manager.images.insert( - image_name.to_owned(), - StoredImage { - image: ImageOrId::Image( - image::load_from_memory_with_format( - include_bytes!("../../resources/images/broken_image.png"), - image::ImageFormat::Png, - ) - .unwrap(), - femtovg::ImageFlags::empty(), - ), - retention_policy: ImageRetentionPolicy::Forever, - used: true, - dirty: false, - observers: HashSet::new(), - }, - ); - } + // return true; + // } else { + // // Image doesn't exist yet so load and show placeholder image + // // TODO: Add way to configure the placeholder image + // cx.resource_manager.images.insert( + // image_name.to_owned(), + // StoredImage { + // image: ImageOrId::Image( + // image::load_from_memory_with_format( + // include_bytes!("../../resources/images/broken_image.png"), + // image::ImageFormat::Png, + // ) + // .unwrap(), + // femtovg::ImageFlags::empty(), + // ), + // retention_policy: ImageRetentionPolicy::Forever, + // used: true, + // dirty: false, + // observers: HashSet::new(), + // }, + // ); + // } false } diff --git a/crates/vizia_core/src/systems/layout.rs b/crates/vizia_core/src/systems/layout.rs index 67ba9d54f..0656bbfd8 100644 --- a/crates/vizia_core/src/systems/layout.rs +++ b/crates/vizia_core/src/systems/layout.rs @@ -3,12 +3,16 @@ use morphorm::Node; use crate::layout::node::SubLayout; use crate::prelude::*; +use super::{text_layout_system, text_system}; + /// Determines the size and position of views. /// TODO: Currently relayout is done on an entire tree rather than incrementally. /// Incremental relayout can be done by keeping a list of nodes that need relayout, /// and when a node undergoes relayout remove the descendants that have been processed from the list, /// then continue relayout on the remaining nodes in the list. pub(crate) fn layout_system(cx: &mut Context) { + text_system(cx); + if cx.style.system_flags.contains(SystemFlags::RELAYOUT) { // Perform layout on the whole tree. Entity::root().layout( @@ -27,85 +31,7 @@ pub(crate) fn layout_system(cx: &mut Context) { let cx = &mut EventContext::new(cx); for entity in cx.tree.into_iter() { - cx.current = entity; - if cx.text_context.has_buffer(entity) { - let auto_width = cx.style.width.get(entity).copied().unwrap_or_default().is_auto(); - let auto_height = - cx.style.height.get(entity).copied().unwrap_or_default().is_auto(); - if !auto_width && !auto_height { - let width = cx.cache.bounds.get(entity).unwrap().w; - let child_left = cx - .style - .child_left - .get(entity) - .cloned() - .unwrap_or_default() - .to_px(width, 0.0) - * cx.scale_factor(); - let child_right = cx - .style - .child_right - .get(entity) - .cloned() - .unwrap_or_default() - .to_px(width, 0.0) - * cx.scale_factor(); - let border_width = cx - .style - .border_width - .get(entity) - .cloned() - .unwrap_or_default() - .to_pixels(width, cx.scale_factor()); - let width = width.ceil() - child_left - child_right - 2.0 * border_width; - cx.text_context.sync_styles(entity, cx.style); - let (text_width, text_height) = - cx.text_context.with_buffer(entity, |fs, buf| { - buf.set_size(fs, width, f32::MAX); - let w = buf - .layout_runs() - .filter_map(|r| (!r.line_w.is_nan()).then_some(r.line_w)) - .max_by(|f1, f2| f1.partial_cmp(f2).unwrap()) - .unwrap_or_default(); - let h = buf.layout_runs().len() as f32 * buf.metrics().line_height; - (w, h) - }); - cx.text_context.set_bounds( - entity, - BoundingBox { w: text_width, h: text_height, ..Default::default() }, - ) - } else { - let width = cx.cache.bounds.get(entity).unwrap().w; - let child_left = cx - .style - .child_left - .get(entity) - .cloned() - .unwrap_or_default() - .to_px(width, 0.0) - * cx.scale_factor(); - let child_right = cx - .style - .child_right - .get(entity) - .cloned() - .unwrap_or_default() - .to_px(width, 0.0) - * cx.scale_factor(); - let border_width = cx - .style - .border_width - .get(entity) - .cloned() - .unwrap_or_default() - .to_pixels(width, cx.scale_factor()); - let width = width.ceil() - child_left - child_right - 2.0 * border_width; - - cx.text_context.with_buffer(entity, |fs, buffer| { - buffer.set_size(fs, width, f32::MAX); - }) - } - } + // if let Some(paragraph) = // Morphorm produces relative positions so convert to absolute. if let Some(parent) = cx.tree.get_layout_parent(entity) { @@ -119,6 +45,12 @@ pub(crate) fn layout_system(cx: &mut Context) { } if let Some(geo) = cx.cache.geo_changed.get(entity).copied() { + if !geo.is_empty() + // && cx.style.text.get(entity).is_some() + { + cx.style.needs_text_layout(entity); + } + // TODO: Use geo changed to determine whether an entity needs to be redrawn. if !geo.is_empty() { @@ -148,6 +80,8 @@ pub(crate) fn layout_system(cx: &mut Context) { cx.style.system_flags.set(SystemFlags::RELAYOUT, false); } + + text_layout_system(cx); } fn visit_entity(cx: &mut EventContext, entity: Entity, event: &mut Event) { diff --git a/crates/vizia_core/src/systems/mod.rs b/crates/vizia_core/src/systems/mod.rs index 0f839f0d1..01ffe806b 100644 --- a/crates/vizia_core/src/systems/mod.rs +++ b/crates/vizia_core/src/systems/mod.rs @@ -6,6 +6,7 @@ pub(crate) mod hover; pub(crate) mod image; pub(crate) mod layout; pub(crate) mod style; +pub(crate) mod text; pub(crate) use self::image::*; pub(crate) use accessibility::*; pub(crate) use animation::*; @@ -14,3 +15,4 @@ pub(crate) use draw::*; pub(crate) use hover::*; pub(crate) use layout::*; pub(crate) use style::*; +pub(crate) use text::*; diff --git a/crates/vizia_core/src/systems/style.rs b/crates/vizia_core/src/systems/style.rs index 62dd4bae2..5d8f912e1 100644 --- a/crates/vizia_core/src/systems/style.rs +++ b/crates/vizia_core/src/systems/style.rs @@ -243,7 +243,7 @@ pub(crate) fn inline_inheritance_system(cx: &mut Context) { cx.style.font_size.inherit_inline(entity, parent); cx.style.font_family.inherit_inline(entity, parent); cx.style.font_weight.inherit_inline(entity, parent); - cx.style.font_style.inherit_inline(entity, parent); + cx.style.font_slant.inherit_inline(entity, parent); cx.style.caret_color.inherit_inline(entity, parent); cx.style.selection_color.inherit_inline(entity, parent); } @@ -258,7 +258,7 @@ pub(crate) fn shared_inheritance_system(cx: &mut Context) { cx.style.font_size.inherit_shared(entity, parent); cx.style.font_family.inherit_shared(entity, parent); cx.style.font_weight.inherit_shared(entity, parent); - cx.style.font_style.inherit_shared(entity, parent); + cx.style.font_slant.inherit_shared(entity, parent); cx.style.caret_color.inherit_shared(entity, parent); cx.style.selection_color.inherit_shared(entity, parent); } @@ -268,6 +268,7 @@ pub(crate) fn shared_inheritance_system(cx: &mut Context) { fn link_style_data(style: &mut Style, entity: Entity, matched_rules: &[Rule]) { let mut should_relayout = false; let mut should_redraw = false; + let mut should_reflow = false; // Display if style.display.link(entity, matched_rules) { @@ -477,36 +478,53 @@ fn link_style_data(style: &mut Style, entity: Entity, matched_rules: &[Rule]) { // Font if style.font_color.link(entity, matched_rules) { should_redraw = true; + should_reflow = true; } if style.font_size.link(entity, matched_rules) { should_relayout = true; should_redraw = true; + should_reflow = true; } if style.font_family.link(entity, matched_rules) { should_relayout = true; should_redraw = true; + should_reflow = true; } if style.font_weight.link(entity, matched_rules) { should_redraw = true; should_relayout = true; + should_reflow = true; } - if style.font_style.link(entity, matched_rules) { + if style.font_slant.link(entity, matched_rules) { should_redraw = true; should_relayout = true; + should_reflow = true; } - if style.font_stretch.link(entity, matched_rules) { + if style.font_width.link(entity, matched_rules) { should_redraw = true; should_relayout = true; + should_reflow = true; } if style.text_wrap.link(entity, matched_rules) { should_redraw = true; should_relayout = true; + should_reflow = true; + } + + if style.text_overflow.link(entity, matched_rules) { + should_redraw = true; + should_reflow = true; + } + + if style.line_clamp.link(entity, matched_rules) { + should_redraw = true; + should_reflow = true; } if style.selection_color.link(entity, matched_rules) { @@ -589,6 +607,10 @@ fn link_style_data(style: &mut Style, entity: Entity, matched_rules: &[Rule]) { if should_redraw { style.system_flags.set(SystemFlags::REDRAW, true); } + + if should_reflow { + style.needs_text_update(entity); + } } /// Compute a list of matching style rules for a given entity. diff --git a/crates/vizia_core/src/systems/text.rs b/crates/vizia_core/src/systems/text.rs new file mode 100644 index 000000000..07cacd1c6 --- /dev/null +++ b/crates/vizia_core/src/systems/text.rs @@ -0,0 +1,162 @@ +use skia_safe::font_arguments::variation_position::Coordinate; +use skia_safe::font_arguments::VariationPosition; +use skia_safe::textlayout::{ + FontCollection, Paragraph, ParagraphBuilder, ParagraphStyle, TextStyle, +}; +use skia_safe::{FontArguments, FontStyle}; +use vizia_storage::LayoutTreeIterator; + +use crate::prelude::*; + +pub(crate) fn text_system(cx: &mut Context) { + let iterator = LayoutTreeIterator::full(&cx.tree); + for entity in iterator { + if !cx.style.text_construction.contains(entity) { + continue; + } + + if let Some(paragraph) = + build_paragraph(entity, &cx.style, cx.text_context.font_collection(), 1.0) + { + // println!("build text: {}", entity); + cx.text_context.text_paragraphs.insert(entity, paragraph); + cx.style.needs_text_layout(entity); + } + } + + cx.style.text_construction.clear(); +} + +pub(crate) fn text_layout_system(cx: &mut Context) { + let iterator = LayoutTreeIterator::full(&cx.tree); + for entity in iterator { + if !cx.style.text_layout.contains(entity) { + continue; + } + + // println!("layout text: {} {}", entity, cx.cache.get_bounds(entity),); + + if let Some(paragraph) = cx.text_context.text_paragraphs.get_mut(entity) { + let bounds = cx.cache.get_bounds(entity); + let padding_left = cx + .style + .child_left + .get(entity) + .copied() + .unwrap_or_default() + .to_px(bounds.width(), 0.0) + * cx.style.scale_factor(); + let padding_right = cx + .style + .child_right + .get(entity) + .copied() + .unwrap_or_default() + .to_px(bounds.width(), 0.0) + * cx.style.scale_factor(); + let text_bounds = cx + .text_context + .text_bounds + .get(entity) + .copied() + .unwrap_or(bounds.shrink_sides(padding_left, 0.0, padding_right, 0.0)); + + if cx.style.width.get(entity).copied().unwrap_or_default().is_auto() { + paragraph.layout(text_bounds.width()); + } else { + paragraph.layout(bounds.width() - padding_left - padding_right); + } + } + } + + cx.style.text_layout.clear(); +} + +pub fn build_paragraph( + entity: Entity, + style: &Style, + font_collection: &FontCollection, + opacity: f32, +) -> Option { + style.text.get(entity).map(|text| { + let mut paragraph_style = ParagraphStyle::default(); + if style.text_overflow.get(entity).copied().unwrap_or_default() == TextOverflow::Ellipsis { + paragraph_style.set_ellipsis("..."); + } + + paragraph_style.set_max_lines(style.line_clamp.get(entity).map(|lc| lc.0 as usize)); + let text_align = if let Some(text_align) = style.text_align.get(entity).copied() { + text_align + } else { + let child_left = style.child_left.get(entity).copied().unwrap_or_default(); + let child_right = style.child_right.get(entity).copied().unwrap_or_default(); + if matches!(child_left, Units::Stretch(_)) { + if matches!(child_right, Units::Stretch(_)) { + TextAlign::Center + } else { + TextAlign::Right + } + } else { + TextAlign::Left + } + }; + + paragraph_style.set_text_align(text_align.into()); + + let mut text_style = TextStyle::new(); + let font_weight = style.font_weight.get(entity).copied().unwrap_or_default(); + let font_width = style.font_width.get(entity).copied().unwrap_or_default(); + let font_slant = style.font_slant.get(entity).copied().unwrap_or_default(); + let font_style = FontStyle::new(font_weight.into(), font_width.into(), font_slant.into()); + text_style.set_font_style(font_style); + let font_families = style + .font_family + .get(entity) + .map(|families| { + families + .iter() + .map(|family| match family { + FamilyOwned::Generic(generic) => match generic { + _ => "Roboto Flex", + }, + + FamilyOwned::Named(name) => name.as_str(), + }) + .collect() + }) + .unwrap_or(vec!["Roboto Flex"]); + + text_style.set_font_families(&font_families); + + text_style.set_color( + style + .font_color + .get(entity) + .copied() + .map(|col| Color::rgba(col.r(), col.g(), col.b(), (opacity * col.a() as f32) as u8)) + .unwrap_or_default(), + ); + let font_size = style.font_size.get(entity).copied().map(|f| f.0).unwrap_or(16.0) + * style.scale_factor(); + text_style.set_font_size(font_size); + // text_style.add_font_feature("opsz", 1); + let coordinates = Box::new([ + Coordinate { axis: ('w', 'g', 'h', 't').into(), value: font_weight.0 as f32 }, + Coordinate { axis: ('o', 'p', 's', 'z').into(), value: font_size }, + ]); + let args = FontArguments::new(); + let pos = VariationPosition { coordinates: coordinates.as_ref() }; + let args = args.set_variation_design_position(pos); + text_style.set_font_arguments(&Some(args)); + + paragraph_style.turn_hinting_off(); + paragraph_style.set_text_style(&text_style); + let mut paragraph_builder = ParagraphBuilder::new(¶graph_style, font_collection); + + paragraph_builder.push_style(&text_style); + paragraph_builder.add_text(text.as_str()); + paragraph_builder.add_text("\u{200B}"); + + paragraph_builder.build() + }) +} diff --git a/crates/vizia_core/src/text/backspace.rs b/crates/vizia_core/src/text/backspace.rs new file mode 100644 index 000000000..05e5a2d88 --- /dev/null +++ b/crates/vizia_core/src/text/backspace.rs @@ -0,0 +1,412 @@ +use super::{EditableText, Selection}; + +#[allow(clippy::cognitive_complexity)] +fn backspace_offset(text: &impl EditableText, mut cursor: usize) -> usize { + #[derive(PartialEq)] + enum State { + Start, + Lf, + BeforeKeycap, + BeforeVsAndKeycap, + BeforeEmojiModifier, + BeforeVsAndEmojiModifier, + BeforeVs, + BeforeEmoji, + BeforeZwj, + BeforeVsAndZwj, + OddNumberedRis, + EvenNumberedRis, + InTagSequence, + Finished, + } + let mut state = State::Start; + + let mut delete_code_point_count = 0; + let mut last_seen_vs_code_point_count = 0; + + while state != State::Finished && cursor > 0 { + let code_point = text.prev_codepoint(cursor).unwrap_or('0'); + + match state { + State::Start => { + delete_code_point_count = 1; + if code_point == '\n' { + state = State::Lf; + } else if is_variation_selector(code_point) { + state = State::BeforeVs; + } else if is_regional_indicator_symbol(code_point) { + state = State::OddNumberedRis; + } else if is_emoji_modifier(code_point) { + state = State::BeforeEmojiModifier; + } else if is_emoji_combining_enclosing_keycap(code_point) { + state = State::BeforeKeycap; + } else if is_emoji(code_point) { + state = State::BeforeEmoji; + } else if is_emoji_cancel_tag(code_point) { + state = State::InTagSequence; + } else { + state = State::Finished; + } + } + State::Lf => { + if code_point == '\r' { + delete_code_point_count += 1; + } + state = State::Finished; + } + State::OddNumberedRis => { + if is_regional_indicator_symbol(code_point) { + delete_code_point_count += 1; + state = State::EvenNumberedRis + } else { + state = State::Finished + } + } + State::EvenNumberedRis => { + if is_regional_indicator_symbol(code_point) { + delete_code_point_count -= 1; + state = State::OddNumberedRis; + } else { + state = State::Finished; + } + } + State::BeforeKeycap => { + if is_variation_selector(code_point) { + last_seen_vs_code_point_count = 1; + state = State::BeforeVsAndKeycap; + } else { + if is_keycap_base(code_point) { + delete_code_point_count += 1; + } + state = State::Finished; + } + } + State::BeforeVsAndKeycap => { + if is_keycap_base(code_point) { + delete_code_point_count += last_seen_vs_code_point_count + 1; + } + state = State::Finished; + } + State::BeforeEmojiModifier => { + if is_variation_selector(code_point) { + last_seen_vs_code_point_count = 1; + state = State::BeforeVsAndEmojiModifier; + } else { + if is_emoji_modifier_base(code_point) { + delete_code_point_count += 1; + } + state = State::Finished; + } + } + State::BeforeVsAndEmojiModifier => { + if is_emoji_modifier_base(code_point) { + delete_code_point_count += last_seen_vs_code_point_count + 1; + } + state = State::Finished; + } + State::BeforeVs => { + if is_emoji(code_point) { + delete_code_point_count += 1; + state = State::BeforeEmoji; + } else { + if !is_variation_selector(code_point) { + //TODO: UCharacter.getCombiningClass(codePoint) == 0 + delete_code_point_count += 1; + } + state = State::Finished; + } + } + State::BeforeEmoji => { + if is_zwj(code_point) { + state = State::BeforeZwj; + } else { + state = State::Finished; + } + } + State::BeforeZwj => { + if is_emoji(code_point) { + delete_code_point_count += 2; + state = if is_emoji_modifier(code_point) { + State::BeforeEmojiModifier + } else { + State::BeforeEmoji + }; + } else if is_variation_selector(code_point) { + last_seen_vs_code_point_count = 1; + state = State::BeforeVsAndZwj; + } else { + state = State::Finished; + } + } + State::BeforeVsAndZwj => { + if is_emoji(code_point) { + delete_code_point_count += last_seen_vs_code_point_count + 2; + last_seen_vs_code_point_count = 0; + state = State::BeforeEmoji; + } else { + state = State::Finished; + } + } + State::InTagSequence => { + if is_tag_spec_char(code_point) { + delete_code_point_count += 1; + } else if is_emoji(code_point) { + delete_code_point_count += 1; + state = State::Finished; + } else { + delete_code_point_count = 1; + state = State::Finished; + } + } + State::Finished => { + break; + } + } + } + + for _ in 0..delete_code_point_count { + if let Some(offset) = text.prev_codepoint_offset(cursor) { + cursor = offset; + } + } + + cursor +} + +/// Calculate resulting offset for a backwards delete. +/// +/// This involves complicated logic to handle various special cases that +/// are unique to backspace. +#[allow(clippy::trivially_copy_pass_by_ref)] +pub fn offset_for_delete_backwards(region: &Selection, text: &impl EditableText) -> usize { + if !region.is_caret() { + region.min() + } else { + backspace_offset(text, region.active) + } +} + +pub fn is_variation_selector(c: char) -> bool { + (c >= '\u{FE00}' && c <= '\u{FE0F}') || (c >= '\u{E0100}' && c <= '\u{E01EF}') +} + +fn is_regional_indicator_symbol(c: char) -> bool { + c >= '\u{1F1E6}' && c <= '\u{1F1FF}' +} +fn is_emoji_modifier(c: char) -> bool { + c >= '\u{1F3FB}' && c <= '\u{1F3FF}' +} +fn is_emoji_combining_enclosing_keycap(c: char) -> bool { + c == '\u{20E3}' +} +fn is_emoji(c: char) -> bool { + is_in_asc_list(c, &EMOJI_TABLE, 0, EMOJI_TABLE.len() - 1) +} +fn is_emoji_modifier_base(c: char) -> bool { + is_in_asc_list(c, &EMOJI_MODIFIER_BASE_TABLE, 0, EMOJI_MODIFIER_BASE_TABLE.len() - 1) +} +fn is_tag_spec_char(c: char) -> bool { + '\u{E0020}' <= c && c <= '\u{E007E}' +} +fn is_emoji_cancel_tag(c: char) -> bool { + c == '\u{E007F}' +} +fn is_zwj(c: char) -> bool { + c == '\u{200D}' +} + +pub fn is_keycap_base(c: char) -> bool { + ('0'..='9').contains(&c) || c == '#' || c == '*' +} + +fn is_in_asc_list(c: T, list: &[T], start: usize, end: usize) -> bool { + if c == list[start] || c == list[end] { + return true; + } + if end - start <= 1 { + return false; + } + + let mid = (start + end) / 2; + + if c >= list[mid] { + is_in_asc_list(c, &list, mid, end) + } else { + is_in_asc_list(c, &list, start, mid) + } +} + +#[rustfmt::skip] +pub const EMOJI_TABLE: [char; 1250] = ['\u{23}', '\u{2A}', '\u{30}', '\u{31}', '\u{32}', '\u{33}', '\u{34}', +'\u{35}', '\u{36}', '\u{37}', '\u{38}', '\u{39}', '\u{A9}', '\u{AE}', '\u{203C}', +'\u{2049}', '\u{2122}', '\u{2139}', '\u{2194}', '\u{2195}', '\u{2196}', '\u{2197}', '\u{2198}', +'\u{2199}', '\u{21A9}', '\u{21AA}', '\u{231A}', '\u{231B}', '\u{2328}', '\u{23CF}', '\u{23E9}', +'\u{23EA}', '\u{23EB}', '\u{23EC}', '\u{23ED}', '\u{23EE}', '\u{23EF}', '\u{23F0}', '\u{23F1}', +'\u{23F2}', '\u{23F3}', '\u{23F8}', '\u{23F9}', '\u{23FA}', '\u{24C2}', '\u{25AA}', '\u{25AB}', +'\u{25B6}', '\u{25C0}', '\u{25FB}', '\u{25FC}', '\u{25FD}', '\u{25FE}', '\u{2600}', '\u{2601}', +'\u{2602}', '\u{2603}', '\u{2604}', '\u{260E}', '\u{2611}', '\u{2614}', '\u{2615}', '\u{2618}', +'\u{261D}', '\u{2620}', '\u{2622}', '\u{2623}', '\u{2626}', '\u{262A}', '\u{262E}', '\u{262F}', +'\u{2638}', '\u{2639}', '\u{263A}', '\u{2640}', '\u{2642}', '\u{2648}', '\u{2649}', '\u{264A}', +'\u{264B}', '\u{264C}', '\u{264D}', '\u{264E}', '\u{264F}', '\u{2650}', '\u{2651}', '\u{2652}', +'\u{2653}', '\u{265F}', '\u{2660}', '\u{2663}', '\u{2665}', '\u{2666}', '\u{2668}', '\u{267B}', +'\u{267E}', '\u{267F}', '\u{2692}', '\u{2693}', '\u{2694}', '\u{2695}', '\u{2696}', '\u{2697}', +'\u{2699}', '\u{269B}', '\u{269C}', '\u{26A0}', '\u{26A1}', '\u{26AA}', '\u{26AB}', '\u{26B0}', +'\u{26B1}', '\u{26BD}', '\u{26BE}', '\u{26C4}', '\u{26C5}', '\u{26C8}', '\u{26CE}', '\u{26CF}', +'\u{26D1}', '\u{26D3}', '\u{26D4}', '\u{26E9}', '\u{26EA}', '\u{26F0}', '\u{26F1}', '\u{26F2}', +'\u{26F3}', '\u{26F4}', '\u{26F5}', '\u{26F7}', '\u{26F8}', '\u{26F9}', '\u{26FA}', '\u{26FD}', +'\u{2702}', '\u{2705}', '\u{2708}', '\u{2709}', '\u{270A}', '\u{270B}', '\u{270C}', '\u{270D}', +'\u{270F}', '\u{2712}', '\u{2714}', '\u{2716}', '\u{271D}', '\u{2721}', '\u{2728}', '\u{2733}', +'\u{2734}', '\u{2744}', '\u{2747}', '\u{274C}', '\u{274E}', '\u{2753}', '\u{2754}', '\u{2755}', +'\u{2757}', '\u{2763}', '\u{2764}', '\u{2795}', '\u{2796}', '\u{2797}', '\u{27A1}', '\u{27B0}', +'\u{27BF}', '\u{2934}', '\u{2935}', '\u{2B05}', '\u{2B06}', '\u{2B07}', '\u{2B1B}', '\u{2B1C}', +'\u{2B50}', '\u{2B55}', '\u{3030}', '\u{303D}', '\u{3297}', '\u{3299}', '\u{1F004}', '\u{1F0CF}', +'\u{1F170}', '\u{1F171}', '\u{1F17E}', '\u{1F17F}', '\u{1F18E}', '\u{1F191}', '\u{1F192}', '\u{1F193}', +'\u{1F194}', '\u{1F195}', '\u{1F196}', '\u{1F197}', '\u{1F198}', '\u{1F199}', '\u{1F19A}', '\u{1F1E6}', +'\u{1F1E7}', '\u{1F1E8}', '\u{1F1E9}', '\u{1F1EA}', '\u{1F1EB}', '\u{1F1EC}', '\u{1F1ED}', '\u{1F1EE}', +'\u{1F1EF}', '\u{1F1F0}', '\u{1F1F1}', '\u{1F1F2}', '\u{1F1F3}', '\u{1F1F4}', '\u{1F1F5}', '\u{1F1F6}', +'\u{1F1F7}', '\u{1F1F8}', '\u{1F1F9}', '\u{1F1FA}', '\u{1F1FB}', '\u{1F1FC}', '\u{1F1FD}', '\u{1F1FE}', +'\u{1F1FF}', '\u{1F201}', '\u{1F202}', '\u{1F21A}', '\u{1F22F}', '\u{1F232}', '\u{1F233}', '\u{1F234}', +'\u{1F235}', '\u{1F236}', '\u{1F237}', '\u{1F238}', '\u{1F239}', '\u{1F23A}', '\u{1F250}', '\u{1F251}', +'\u{1F300}', '\u{1F301}', '\u{1F302}', '\u{1F303}', '\u{1F304}', '\u{1F305}', '\u{1F306}', '\u{1F307}', +'\u{1F308}', '\u{1F309}', '\u{1F30A}', '\u{1F30B}', '\u{1F30C}', '\u{1F30D}', '\u{1F30E}', '\u{1F30F}', +'\u{1F310}', '\u{1F311}', '\u{1F312}', '\u{1F313}', '\u{1F314}', '\u{1F315}', '\u{1F316}', '\u{1F317}', +'\u{1F318}', '\u{1F319}', '\u{1F31A}', '\u{1F31B}', '\u{1F31C}', '\u{1F31D}', '\u{1F31E}', '\u{1F31F}', +'\u{1F320}', '\u{1F321}', '\u{1F324}', '\u{1F325}', '\u{1F326}', '\u{1F327}', '\u{1F328}', '\u{1F329}', +'\u{1F32A}', '\u{1F32B}', '\u{1F32C}', '\u{1F32D}', '\u{1F32E}', '\u{1F32F}', '\u{1F330}', '\u{1F331}', +'\u{1F332}', '\u{1F333}', '\u{1F334}', '\u{1F335}', '\u{1F336}', '\u{1F337}', '\u{1F338}', '\u{1F339}', +'\u{1F33A}', '\u{1F33B}', '\u{1F33C}', '\u{1F33D}', '\u{1F33E}', '\u{1F33F}', '\u{1F340}', '\u{1F341}', +'\u{1F342}', '\u{1F343}', '\u{1F344}', '\u{1F345}', '\u{1F346}', '\u{1F347}', '\u{1F348}', '\u{1F349}', +'\u{1F34A}', '\u{1F34B}', '\u{1F34C}', '\u{1F34D}', '\u{1F34E}', '\u{1F34F}', '\u{1F350}', '\u{1F351}', +'\u{1F352}', '\u{1F353}', '\u{1F354}', '\u{1F355}', '\u{1F356}', '\u{1F357}', '\u{1F358}', '\u{1F359}', +'\u{1F35A}', '\u{1F35B}', '\u{1F35C}', '\u{1F35D}', '\u{1F35E}', '\u{1F35F}', '\u{1F360}', '\u{1F361}', +'\u{1F362}', '\u{1F363}', '\u{1F364}', '\u{1F365}', '\u{1F366}', '\u{1F367}', '\u{1F368}', '\u{1F369}', +'\u{1F36A}', '\u{1F36B}', '\u{1F36C}', '\u{1F36D}', '\u{1F36E}', '\u{1F36F}', '\u{1F370}', '\u{1F371}', +'\u{1F372}', '\u{1F373}', '\u{1F374}', '\u{1F375}', '\u{1F376}', '\u{1F377}', '\u{1F378}', '\u{1F379}', +'\u{1F37A}', '\u{1F37B}', '\u{1F37C}', '\u{1F37D}', '\u{1F37E}', '\u{1F37F}', '\u{1F380}', '\u{1F381}', +'\u{1F382}', '\u{1F383}', '\u{1F384}', '\u{1F385}', '\u{1F386}', '\u{1F387}', '\u{1F388}', '\u{1F389}', +'\u{1F38A}', '\u{1F38B}', '\u{1F38C}', '\u{1F38D}', '\u{1F38E}', '\u{1F38F}', '\u{1F390}', '\u{1F391}', +'\u{1F392}', '\u{1F393}', '\u{1F396}', '\u{1F397}', '\u{1F399}', '\u{1F39A}', '\u{1F39B}', '\u{1F39E}', +'\u{1F39F}', '\u{1F3A0}', '\u{1F3A1}', '\u{1F3A2}', '\u{1F3A3}', '\u{1F3A4}', '\u{1F3A5}', '\u{1F3A6}', +'\u{1F3A7}', '\u{1F3A8}', '\u{1F3A9}', '\u{1F3AA}', '\u{1F3AB}', '\u{1F3AC}', '\u{1F3AD}', '\u{1F3AE}', +'\u{1F3AF}', '\u{1F3B0}', '\u{1F3B1}', '\u{1F3B2}', '\u{1F3B3}', '\u{1F3B4}', '\u{1F3B5}', '\u{1F3B6}', +'\u{1F3B7}', '\u{1F3B8}', '\u{1F3B9}', '\u{1F3BA}', '\u{1F3BB}', '\u{1F3BC}', '\u{1F3BD}', '\u{1F3BE}', +'\u{1F3BF}', '\u{1F3C0}', '\u{1F3C1}', '\u{1F3C2}', '\u{1F3C3}', '\u{1F3C4}', '\u{1F3C5}', '\u{1F3C6}', +'\u{1F3C7}', '\u{1F3C8}', '\u{1F3C9}', '\u{1F3CA}', '\u{1F3CB}', '\u{1F3CC}', '\u{1F3CD}', '\u{1F3CE}', +'\u{1F3CF}', '\u{1F3D0}', '\u{1F3D1}', '\u{1F3D2}', '\u{1F3D3}', '\u{1F3D4}', '\u{1F3D5}', '\u{1F3D6}', +'\u{1F3D7}', '\u{1F3D8}', '\u{1F3D9}', '\u{1F3DA}', '\u{1F3DB}', '\u{1F3DC}', '\u{1F3DD}', '\u{1F3DE}', +'\u{1F3DF}', '\u{1F3E0}', '\u{1F3E1}', '\u{1F3E2}', '\u{1F3E3}', '\u{1F3E4}', '\u{1F3E5}', '\u{1F3E6}', +'\u{1F3E7}', '\u{1F3E8}', '\u{1F3E9}', '\u{1F3EA}', '\u{1F3EB}', '\u{1F3EC}', '\u{1F3ED}', '\u{1F3EE}', +'\u{1F3EF}', '\u{1F3F0}', '\u{1F3F3}', '\u{1F3F4}', '\u{1F3F5}', '\u{1F3F7}', '\u{1F3F8}', '\u{1F3F9}', +'\u{1F3FA}', '\u{1F3FB}', '\u{1F3FC}', '\u{1F3FD}', '\u{1F3FE}', '\u{1F3FF}', '\u{1F400}', '\u{1F401}', +'\u{1F402}', '\u{1F403}', '\u{1F404}', '\u{1F405}', '\u{1F406}', '\u{1F407}', '\u{1F408}', '\u{1F409}', +'\u{1F40A}', '\u{1F40B}', '\u{1F40C}', '\u{1F40D}', '\u{1F40E}', '\u{1F40F}', '\u{1F410}', '\u{1F411}', +'\u{1F412}', '\u{1F413}', '\u{1F414}', '\u{1F415}', '\u{1F416}', '\u{1F417}', '\u{1F418}', '\u{1F419}', +'\u{1F41A}', '\u{1F41B}', '\u{1F41C}', '\u{1F41D}', '\u{1F41E}', '\u{1F41F}', '\u{1F420}', '\u{1F421}', +'\u{1F422}', '\u{1F423}', '\u{1F424}', '\u{1F425}', '\u{1F426}', '\u{1F427}', '\u{1F428}', '\u{1F429}', +'\u{1F42A}', '\u{1F42B}', '\u{1F42C}', '\u{1F42D}', '\u{1F42E}', '\u{1F42F}', '\u{1F430}', '\u{1F431}', +'\u{1F432}', '\u{1F433}', '\u{1F434}', '\u{1F435}', '\u{1F436}', '\u{1F437}', '\u{1F438}', '\u{1F439}', +'\u{1F43A}', '\u{1F43B}', '\u{1F43C}', '\u{1F43D}', '\u{1F43E}', '\u{1F43F}', '\u{1F440}', '\u{1F441}', +'\u{1F442}', '\u{1F443}', '\u{1F444}', '\u{1F445}', '\u{1F446}', '\u{1F447}', '\u{1F448}', '\u{1F449}', +'\u{1F44A}', '\u{1F44B}', '\u{1F44C}', '\u{1F44D}', '\u{1F44E}', '\u{1F44F}', '\u{1F450}', '\u{1F451}', +'\u{1F452}', '\u{1F453}', '\u{1F454}', '\u{1F455}', '\u{1F456}', '\u{1F457}', '\u{1F458}', '\u{1F459}', +'\u{1F45A}', '\u{1F45B}', '\u{1F45C}', '\u{1F45D}', '\u{1F45E}', '\u{1F45F}', '\u{1F460}', '\u{1F461}', +'\u{1F462}', '\u{1F463}', '\u{1F464}', '\u{1F465}', '\u{1F466}', '\u{1F467}', '\u{1F468}', '\u{1F469}', +'\u{1F46A}', '\u{1F46B}', '\u{1F46C}', '\u{1F46D}', '\u{1F46E}', '\u{1F46F}', '\u{1F470}', '\u{1F471}', +'\u{1F472}', '\u{1F473}', '\u{1F474}', '\u{1F475}', '\u{1F476}', '\u{1F477}', '\u{1F478}', '\u{1F479}', +'\u{1F47A}', '\u{1F47B}', '\u{1F47C}', '\u{1F47D}', '\u{1F47E}', '\u{1F47F}', '\u{1F480}', '\u{1F481}', +'\u{1F482}', '\u{1F483}', '\u{1F484}', '\u{1F485}', '\u{1F486}', '\u{1F487}', '\u{1F488}', '\u{1F489}', +'\u{1F48A}', '\u{1F48B}', '\u{1F48C}', '\u{1F48D}', '\u{1F48E}', '\u{1F48F}', '\u{1F490}', '\u{1F491}', +'\u{1F492}', '\u{1F493}', '\u{1F494}', '\u{1F495}', '\u{1F496}', '\u{1F497}', '\u{1F498}', '\u{1F499}', +'\u{1F49A}', '\u{1F49B}', '\u{1F49C}', '\u{1F49D}', '\u{1F49E}', '\u{1F49F}', '\u{1F4A0}', '\u{1F4A1}', +'\u{1F4A2}', '\u{1F4A3}', '\u{1F4A4}', '\u{1F4A5}', '\u{1F4A6}', '\u{1F4A7}', '\u{1F4A8}', '\u{1F4A9}', +'\u{1F4AA}', '\u{1F4AB}', '\u{1F4AC}', '\u{1F4AD}', '\u{1F4AE}', '\u{1F4AF}', '\u{1F4B0}', '\u{1F4B1}', +'\u{1F4B2}', '\u{1F4B3}', '\u{1F4B4}', '\u{1F4B5}', '\u{1F4B6}', '\u{1F4B7}', '\u{1F4B8}', '\u{1F4B9}', +'\u{1F4BA}', '\u{1F4BB}', '\u{1F4BC}', '\u{1F4BD}', '\u{1F4BE}', '\u{1F4BF}', '\u{1F4C0}', '\u{1F4C1}', +'\u{1F4C2}', '\u{1F4C3}', '\u{1F4C4}', '\u{1F4C5}', '\u{1F4C6}', '\u{1F4C7}', '\u{1F4C8}', '\u{1F4C9}', +'\u{1F4CA}', '\u{1F4CB}', '\u{1F4CC}', '\u{1F4CD}', '\u{1F4CE}', '\u{1F4CF}', '\u{1F4D0}', '\u{1F4D1}', +'\u{1F4D2}', '\u{1F4D3}', '\u{1F4D4}', '\u{1F4D5}', '\u{1F4D6}', '\u{1F4D7}', '\u{1F4D8}', '\u{1F4D9}', +'\u{1F4DA}', '\u{1F4DB}', '\u{1F4DC}', '\u{1F4DD}', '\u{1F4DE}', '\u{1F4DF}', '\u{1F4E0}', '\u{1F4E1}', +'\u{1F4E2}', '\u{1F4E3}', '\u{1F4E4}', '\u{1F4E5}', '\u{1F4E6}', '\u{1F4E7}', '\u{1F4E8}', '\u{1F4E9}', +'\u{1F4EA}', '\u{1F4EB}', '\u{1F4EC}', '\u{1F4ED}', '\u{1F4EE}', '\u{1F4EF}', '\u{1F4F0}', '\u{1F4F1}', +'\u{1F4F2}', '\u{1F4F3}', '\u{1F4F4}', '\u{1F4F5}', '\u{1F4F6}', '\u{1F4F7}', '\u{1F4F8}', '\u{1F4F9}', +'\u{1F4FA}', '\u{1F4FB}', '\u{1F4FC}', '\u{1F4FD}', '\u{1F4FF}', '\u{1F500}', '\u{1F501}', '\u{1F502}', +'\u{1F503}', '\u{1F504}', '\u{1F505}', '\u{1F506}', '\u{1F507}', '\u{1F508}', '\u{1F509}', '\u{1F50A}', +'\u{1F50B}', '\u{1F50C}', '\u{1F50D}', '\u{1F50E}', '\u{1F50F}', '\u{1F510}', '\u{1F511}', '\u{1F512}', +'\u{1F513}', '\u{1F514}', '\u{1F515}', '\u{1F516}', '\u{1F517}', '\u{1F518}', '\u{1F519}', '\u{1F51A}', +'\u{1F51B}', '\u{1F51C}', '\u{1F51D}', '\u{1F51E}', '\u{1F51F}', '\u{1F520}', '\u{1F521}', '\u{1F522}', +'\u{1F523}', '\u{1F524}', '\u{1F525}', '\u{1F526}', '\u{1F527}', '\u{1F528}', '\u{1F529}', '\u{1F52A}', +'\u{1F52B}', '\u{1F52C}', '\u{1F52D}', '\u{1F52E}', '\u{1F52F}', '\u{1F530}', '\u{1F531}', '\u{1F532}', +'\u{1F533}', '\u{1F534}', '\u{1F535}', '\u{1F536}', '\u{1F537}', '\u{1F538}', '\u{1F539}', '\u{1F53A}', +'\u{1F53B}', '\u{1F53C}', '\u{1F53D}', '\u{1F549}', '\u{1F54A}', '\u{1F54B}', '\u{1F54C}', '\u{1F54D}', +'\u{1F54E}', '\u{1F550}', '\u{1F551}', '\u{1F552}', '\u{1F553}', '\u{1F554}', '\u{1F555}', '\u{1F556}', +'\u{1F557}', '\u{1F558}', '\u{1F559}', '\u{1F55A}', '\u{1F55B}', '\u{1F55C}', '\u{1F55D}', '\u{1F55E}', +'\u{1F55F}', '\u{1F560}', '\u{1F561}', '\u{1F562}', '\u{1F563}', '\u{1F564}', '\u{1F565}', '\u{1F566}', +'\u{1F567}', '\u{1F56F}', '\u{1F570}', '\u{1F573}', '\u{1F574}', '\u{1F575}', '\u{1F576}', '\u{1F577}', +'\u{1F578}', '\u{1F579}', '\u{1F57A}', '\u{1F587}', '\u{1F58A}', '\u{1F58B}', '\u{1F58C}', '\u{1F58D}', +'\u{1F590}', '\u{1F595}', '\u{1F596}', '\u{1F5A4}', '\u{1F5A5}', '\u{1F5A8}', '\u{1F5B1}', '\u{1F5B2}', +'\u{1F5BC}', '\u{1F5C2}', '\u{1F5C3}', '\u{1F5C4}', '\u{1F5D1}', '\u{1F5D2}', '\u{1F5D3}', '\u{1F5DC}', +'\u{1F5DD}', '\u{1F5DE}', '\u{1F5E1}', '\u{1F5E3}', '\u{1F5E8}', '\u{1F5EF}', '\u{1F5F3}', '\u{1F5FA}', +'\u{1F5FB}', '\u{1F5FC}', '\u{1F5FD}', '\u{1F5FE}', '\u{1F5FF}', '\u{1F600}', '\u{1F601}', '\u{1F602}', +'\u{1F603}', '\u{1F604}', '\u{1F605}', '\u{1F606}', '\u{1F607}', '\u{1F608}', '\u{1F609}', '\u{1F60A}', +'\u{1F60B}', '\u{1F60C}', '\u{1F60D}', '\u{1F60E}', '\u{1F60F}', '\u{1F610}', '\u{1F611}', '\u{1F612}', +'\u{1F613}', '\u{1F614}', '\u{1F615}', '\u{1F616}', '\u{1F617}', '\u{1F618}', '\u{1F619}', '\u{1F61A}', +'\u{1F61B}', '\u{1F61C}', '\u{1F61D}', '\u{1F61E}', '\u{1F61F}', '\u{1F620}', '\u{1F621}', '\u{1F622}', +'\u{1F623}', '\u{1F624}', '\u{1F625}', '\u{1F626}', '\u{1F627}', '\u{1F628}', '\u{1F629}', '\u{1F62A}', +'\u{1F62B}', '\u{1F62C}', '\u{1F62D}', '\u{1F62E}', '\u{1F62F}', '\u{1F630}', '\u{1F631}', '\u{1F632}', +'\u{1F633}', '\u{1F634}', '\u{1F635}', '\u{1F636}', '\u{1F637}', '\u{1F638}', '\u{1F639}', '\u{1F63A}', +'\u{1F63B}', '\u{1F63C}', '\u{1F63D}', '\u{1F63E}', '\u{1F63F}', '\u{1F640}', '\u{1F641}', '\u{1F642}', +'\u{1F643}', '\u{1F644}', '\u{1F645}', '\u{1F646}', '\u{1F647}', '\u{1F648}', '\u{1F649}', '\u{1F64A}', +'\u{1F64B}', '\u{1F64C}', '\u{1F64D}', '\u{1F64E}', '\u{1F64F}', '\u{1F680}', '\u{1F681}', '\u{1F682}', +'\u{1F683}', '\u{1F684}', '\u{1F685}', '\u{1F686}', '\u{1F687}', '\u{1F688}', '\u{1F689}', '\u{1F68A}', +'\u{1F68B}', '\u{1F68C}', '\u{1F68D}', '\u{1F68E}', '\u{1F68F}', '\u{1F690}', '\u{1F691}', '\u{1F692}', +'\u{1F693}', '\u{1F694}', '\u{1F695}', '\u{1F696}', '\u{1F697}', '\u{1F698}', '\u{1F699}', '\u{1F69A}', +'\u{1F69B}', '\u{1F69C}', '\u{1F69D}', '\u{1F69E}', '\u{1F69F}', '\u{1F6A0}', '\u{1F6A1}', '\u{1F6A2}', +'\u{1F6A3}', '\u{1F6A4}', '\u{1F6A5}', '\u{1F6A6}', '\u{1F6A7}', '\u{1F6A8}', '\u{1F6A9}', '\u{1F6AA}', +'\u{1F6AB}', '\u{1F6AC}', '\u{1F6AD}', '\u{1F6AE}', '\u{1F6AF}', '\u{1F6B0}', '\u{1F6B1}', '\u{1F6B2}', +'\u{1F6B3}', '\u{1F6B4}', '\u{1F6B5}', '\u{1F6B6}', '\u{1F6B7}', '\u{1F6B8}', '\u{1F6B9}', '\u{1F6BA}', +'\u{1F6BB}', '\u{1F6BC}', '\u{1F6BD}', '\u{1F6BE}', '\u{1F6BF}', '\u{1F6C0}', '\u{1F6C1}', '\u{1F6C2}', +'\u{1F6C3}', '\u{1F6C4}', '\u{1F6C5}', '\u{1F6CB}', '\u{1F6CC}', '\u{1F6CD}', '\u{1F6CE}', '\u{1F6CF}', +'\u{1F6D0}', '\u{1F6D1}', '\u{1F6D2}', '\u{1F6E0}', '\u{1F6E1}', '\u{1F6E2}', '\u{1F6E3}', '\u{1F6E4}', +'\u{1F6E5}', '\u{1F6E9}', '\u{1F6EB}', '\u{1F6EC}', '\u{1F6F0}', '\u{1F6F3}', '\u{1F6F4}', '\u{1F6F5}', +'\u{1F6F6}', '\u{1F6F7}', '\u{1F6F8}', '\u{1F6F9}', '\u{1F910}', '\u{1F911}', '\u{1F912}', '\u{1F913}', +'\u{1F914}', '\u{1F915}', '\u{1F916}', '\u{1F917}', '\u{1F918}', '\u{1F919}', '\u{1F91A}', '\u{1F91B}', +'\u{1F91C}', '\u{1F91D}', '\u{1F91E}', '\u{1F91F}', '\u{1F920}', '\u{1F921}', '\u{1F922}', '\u{1F923}', +'\u{1F924}', '\u{1F925}', '\u{1F926}', '\u{1F927}', '\u{1F928}', '\u{1F929}', '\u{1F92A}', '\u{1F92B}', +'\u{1F92C}', '\u{1F92D}', '\u{1F92E}', '\u{1F92F}', '\u{1F930}', '\u{1F931}', '\u{1F932}', '\u{1F933}', +'\u{1F934}', '\u{1F935}', '\u{1F936}', '\u{1F937}', '\u{1F938}', '\u{1F939}', '\u{1F93A}', '\u{1F93C}', +'\u{1F93D}', '\u{1F93E}', '\u{1F940}', '\u{1F941}', '\u{1F942}', '\u{1F943}', '\u{1F944}', '\u{1F945}', +'\u{1F947}', '\u{1F948}', '\u{1F949}', '\u{1F94A}', '\u{1F94B}', '\u{1F94C}', '\u{1F94D}', '\u{1F94E}', +'\u{1F94F}', '\u{1F950}', '\u{1F951}', '\u{1F952}', '\u{1F953}', '\u{1F954}', '\u{1F955}', '\u{1F956}', +'\u{1F957}', '\u{1F958}', '\u{1F959}', '\u{1F95A}', '\u{1F95B}', '\u{1F95C}', '\u{1F95D}', '\u{1F95E}', +'\u{1F95F}', '\u{1F960}', '\u{1F961}', '\u{1F962}', '\u{1F963}', '\u{1F964}', '\u{1F965}', '\u{1F966}', +'\u{1F967}', '\u{1F968}', '\u{1F969}', '\u{1F96A}', '\u{1F96B}', '\u{1F96C}', '\u{1F96D}', '\u{1F96E}', +'\u{1F96F}', '\u{1F970}', '\u{1F973}', '\u{1F974}', '\u{1F975}', '\u{1F976}', '\u{1F97A}', '\u{1F97C}', +'\u{1F97D}', '\u{1F97E}', '\u{1F97F}', '\u{1F980}', '\u{1F981}', '\u{1F982}', '\u{1F983}', '\u{1F984}', +'\u{1F985}', '\u{1F986}', '\u{1F987}', '\u{1F988}', '\u{1F989}', '\u{1F98A}', '\u{1F98B}', '\u{1F98C}', +'\u{1F98D}', '\u{1F98E}', '\u{1F98F}', '\u{1F990}', '\u{1F991}', '\u{1F992}', '\u{1F993}', '\u{1F994}', +'\u{1F995}', '\u{1F996}', '\u{1F997}', '\u{1F998}', '\u{1F999}', '\u{1F99A}', '\u{1F99B}', '\u{1F99C}', +'\u{1F99D}', '\u{1F99E}', '\u{1F99F}', '\u{1F9A0}', '\u{1F9A1}', '\u{1F9A2}', '\u{1F9B0}', '\u{1F9B1}', +'\u{1F9B2}', '\u{1F9B3}', '\u{1F9B4}', '\u{1F9B5}', '\u{1F9B6}', '\u{1F9B7}', '\u{1F9B8}', '\u{1F9B9}', +'\u{1F9C0}', '\u{1F9C1}', '\u{1F9C2}', '\u{1F9D0}', '\u{1F9D1}', '\u{1F9D2}', '\u{1F9D3}', '\u{1F9D4}', +'\u{1F9D5}', '\u{1F9D6}', '\u{1F9D7}', '\u{1F9D8}', '\u{1F9D9}', '\u{1F9DA}', '\u{1F9DB}', '\u{1F9DC}', +'\u{1F9DD}', '\u{1F9DE}', '\u{1F9DF}', '\u{1F9E0}', '\u{1F9E1}', '\u{1F9E2}', '\u{1F9E3}', '\u{1F9E4}', +'\u{1F9E5}', '\u{1F9E6}', '\u{1F9E7}', '\u{1F9E8}', '\u{1F9E9}', '\u{1F9EA}', '\u{1F9EB}', '\u{1F9EC}', +'\u{1F9ED}', '\u{1F9EE}', '\u{1F9EF}', '\u{1F9F0}', '\u{1F9F1}', '\u{1F9F2}', '\u{1F9F3}', '\u{1F9F4}', +'\u{1F9F5}', '\u{1F9F6}', '\u{1F9F7}', '\u{1F9F8}', '\u{1F9F9}', '\u{1F9FA}', '\u{1F9FB}', '\u{1F9FC}', +'\u{1F9FD}', '\u{1F9FE}', '\u{1F9FF}']; + +#[rustfmt::skip] +pub const EMOJI_MODIFIER_BASE_TABLE: [char; 106] = ['\u{261D}', '\u{26F9}', '\u{270A}', '\u{270B}', +'\u{270C}', '\u{270D}', '\u{1F385}', '\u{1F3C2}', '\u{1F3C3}', '\u{1F3C4}', '\u{1F3C7}', '\u{1F3CA}', +'\u{1F3CB}', '\u{1F3CC}', '\u{1F442}', '\u{1F443}', '\u{1F446}', '\u{1F447}', '\u{1F448}', '\u{1F449}', +'\u{1F44A}', '\u{1F44B}', '\u{1F44C}', '\u{1F44D}', '\u{1F44E}', '\u{1F44F}', '\u{1F450}', '\u{1F466}', +'\u{1F467}', '\u{1F468}', '\u{1F469}', '\u{1F46E}', '\u{1F470}', '\u{1F471}', '\u{1F472}', '\u{1F473}', +'\u{1F474}', '\u{1F475}', '\u{1F476}', '\u{1F477}', '\u{1F478}', '\u{1F47C}', '\u{1F481}', '\u{1F482}', +'\u{1F483}', '\u{1F485}', '\u{1F486}', '\u{1F487}', '\u{1F4AA}', '\u{1F574}', '\u{1F575}', '\u{1F57A}', +'\u{1F590}', '\u{1F595}', '\u{1F596}', '\u{1F645}', '\u{1F646}', '\u{1F647}', '\u{1F64B}', '\u{1F64C}', +'\u{1F64D}', '\u{1F64E}', '\u{1F64F}', '\u{1F6A3}', '\u{1F6B4}', '\u{1F6B5}', '\u{1F6B6}', '\u{1F6C0}', +'\u{1F6CC}', '\u{1F918}', '\u{1F919}', '\u{1F91A}', '\u{1F91B}', '\u{1F91C}', '\u{1F91E}', '\u{1F91F}', +'\u{1F926}', '\u{1F930}', '\u{1F931}', '\u{1F932}', '\u{1F933}', '\u{1F934}', '\u{1F935}', '\u{1F936}', +'\u{1F937}', '\u{1F938}', '\u{1F939}', '\u{1F93D}', '\u{1F93E}', '\u{1F9B5}', '\u{1F9B6}', '\u{1F9B8}', +'\u{1F9B9}', '\u{1F9D1}', '\u{1F9D2}', '\u{1F9D3}', '\u{1F9D4}', '\u{1F9D5}', '\u{1F9D6}', '\u{1F9D7}', +'\u{1F9D8}', '\u{1F9D9}', '\u{1F9DA}', '\u{1F9DB}', '\u{1F9DC}', '\u{1F9DD}']; diff --git a/crates/vizia_core/src/text/cosmic.rs b/crates/vizia_core/src/text/cosmic.rs deleted file mode 100644 index ab545bdd3..000000000 --- a/crates/vizia_core/src/text/cosmic.rs +++ /dev/null @@ -1,562 +0,0 @@ -use crate::entity::Entity; -use crate::layout::BoundingBox; -use crate::prelude::Color; -use crate::style::Style; -use cosmic_text::fontdb::Query; -use cosmic_text::{ - fontdb::Database, Attrs, AttrsList, Buffer, CacheKey, Color as FontColor, Edit, Editor, - FontSystem, Metrics, Weight, Wrap, -}; -use cosmic_text::{Align, Cursor, FamilyOwned, Selection, Shaping}; -use femtovg::imgref::{Img, ImgRef}; -use femtovg::rgb::RGBA8; -use femtovg::{ - Atlas, Canvas, DrawCommand, ErrorKind, GlyphDrawCommands, ImageFlags, ImageId, ImageSource, - Quad, Renderer, -}; -use hashbrown::HashMap; -use morphorm::Units; -use std::cmp::Ordering; -use swash::scale::image::Content; -use swash::scale::{Render, ScaleContext, Source, StrikeWith}; -use swash::zeno::{Format, Vector}; -use unicode_segmentation::UnicodeSegmentation; -use vizia_storage::SparseSet; -use vizia_style::{FontStretch, FontStyle, TextAlign}; - -const GLYPH_PADDING: u32 = 1; -const GLYPH_MARGIN: u32 = 1; -const TEXTURE_SIZE: usize = 512; - -#[derive(Default, Debug, Clone, Copy)] -pub struct TextConfig { - pub hint: bool, - pub subpixel: bool, -} - -pub struct TextContext { - font_system: FontSystem, - scale_context: ScaleContext, - rendered_glyphs: HashMap>, - glyph_textures: Vec, - buffers: HashMap, - bounds: SparseSet, -} - -impl TextContext { - #[allow(dead_code)] - pub(crate) fn font_system(&mut self) -> &mut FontSystem { - &mut self.font_system - } - - pub(crate) fn clear_buffer(&mut self, entity: Entity) { - self.buffers.remove(&entity); - } - - pub(crate) fn has_buffer(&self, entity: Entity) -> bool { - self.buffers.contains_key(&entity) - } - - pub(crate) fn set_text(&mut self, entity: Entity, text: &str) { - self.with_buffer(entity, |fs, buf| { - buf.set_text(fs, text, Attrs::new(), Shaping::Advanced); - }); - } - - pub(crate) fn with_editor( - &mut self, - entity: Entity, - f: impl FnOnce(&mut FontSystem, &mut Editor) -> O, - ) -> O { - let editor = self.buffers.entry(entity).or_insert_with(|| { - Editor::new(Buffer::new(&mut self.font_system, Metrics::new(18.0, 20.0))) - }); - - f(&mut self.font_system, editor) - } - - pub(crate) fn with_buffer( - &mut self, - entity: Entity, - f: impl FnOnce(&mut FontSystem, &mut Buffer) -> O, - ) -> O { - self.with_editor(entity, |fs, ed| f(fs, ed.buffer_mut())) - } - - pub(crate) fn set_bounds(&mut self, entity: Entity, size: BoundingBox) { - self.bounds.insert(entity, size); - } - - pub(crate) fn get_bounds(&self, entity: Entity) -> Option { - self.bounds.get(entity).copied() - } - - pub(crate) fn clear_bounds(&mut self, entity: Entity) { - if self.bounds.contains(entity) { - self.bounds.remove(entity).unwrap(); - } - } - - /// Sync the style data from vizia with the style attribites stored in cosmic-text buffers. - pub(crate) fn sync_styles(&mut self, entity: Entity, style: &Style) { - let (families, font_weight, font_style) = { - let families = style - .font_family - .get(entity) - .unwrap_or(&style.default_font) - .iter() - .map(|x| x.as_family()) - .collect::>(); - let query = Query { - families: families.as_slice(), - weight: Weight(style.font_weight.get(entity).copied().unwrap_or_default().into()), - stretch: style - .font_stretch - .get(entity) - .map(|stretch| match stretch { - FontStretch::UltraCondensed => cosmic_text::Stretch::UltraCondensed, - FontStretch::ExtraCondensed => cosmic_text::Stretch::ExtraCondensed, - FontStretch::Condensed => cosmic_text::Stretch::Condensed, - FontStretch::SemiCondensed => cosmic_text::Stretch::SemiCondensed, - FontStretch::Normal => cosmic_text::Stretch::Normal, - FontStretch::SemiExpanded => cosmic_text::Stretch::SemiExpanded, - FontStretch::Expanded => cosmic_text::Stretch::Expanded, - FontStretch::ExtraExpanded => cosmic_text::Stretch::ExtraExpanded, - FontStretch::UltraExpanded => cosmic_text::Stretch::UltraExpanded, - }) - .unwrap_or_default(), - style: style - .font_style - .get(entity) - .map(|style| match style { - FontStyle::Italic => cosmic_text::Style::Italic, - FontStyle::Normal => cosmic_text::Style::Normal, - FontStyle::Oblique => cosmic_text::Style::Oblique, - }) - .unwrap_or_default(), - }; - let id = self - .font_system - .db() - .query(&query) - .unwrap_or_else(|| panic!("Failed to find font: {:?}", query)); // TODO worst-case default handling - let info = self.font_system.db().face(id).unwrap(); - (info.families.clone(), info.weight, info.style) - }; - - let font_color = style.font_color.get(entity).copied().unwrap_or(Color::rgb(0, 0, 0)); - - let font_families = - families.into_iter().map(|(name, _)| FamilyOwned::Name(name)).collect::>(); - - let family = if let Some(font_family) = font_families.first() { - font_family.as_family() - } else { - style.default_font.first().unwrap().as_family() - }; - - let child_left = style.child_left.get(entity).copied().unwrap_or_default(); - let col_between = style.col_between.get(entity).copied().unwrap_or_default(); - let child_right = style.child_right.get(entity).copied().unwrap_or_default(); - - let width = style.width.get(entity).copied().unwrap_or_default(); - - let mut alignment = match (child_left, col_between, child_right) { - (Units::Stretch(_), _, Units::Stretch(_)) => Some(Align::Center), - - (Units::Stretch(_), _, _) => Some(Align::Right), - - (_, _, Units::Stretch(_)) => Some(Align::Left), - - (_, Units::Stretch(_), _) => Some(Align::Justified), - - _ => None, - }; - - if let Some(text_align) = style.text_align.get(entity).copied() { - alignment = match text_align { - TextAlign::Left => Some(Align::Left), - TextAlign::Right => Some(Align::Right), - TextAlign::Center => Some(Align::Center), - TextAlign::Justify => Some(Align::Justified), - _ => None, - }; - } - - if width.is_auto() { - alignment = None; - } - - self.with_buffer(entity, |fs, buf| { - let attrs = Attrs::new().family(family).weight(font_weight).style(font_style).color( - FontColor::rgba(font_color.r(), font_color.g(), font_color.b(), font_color.a()), - ); - - let wrap = if style.text_wrap.get(entity).copied().unwrap_or(true) { - Wrap::Word - } else { - Wrap::None - }; - buf.set_wrap(fs, wrap); - for line in buf.lines.iter_mut() { - // TODO spans - line.set_attrs_list(AttrsList::new(attrs)); - line.set_align(alignment); - } - let font_size = style.font_size.get(entity).copied().map(|f| f.0).unwrap_or(16.0) - * style.dpi_factor as f32; - // TODO configurable line spacing - buf.set_metrics(fs, Metrics::new(font_size, font_size * 1.2)); - // buf.set_size(fs, 200.0, 200.0); - // buf.shape_until_scroll(fs); - buf.shape_until(fs, i32::MAX); - }); - } - - /// Generate a series of canvas path operations to render the text of a particular entity. - pub(crate) fn fill_to_cmds( - &mut self, - canvas: &mut Canvas, - entity: Entity, - bounds: BoundingBox, - justify: (f32, f32), - config: TextConfig, - ) -> Result, ErrorKind> { - if !self.has_buffer(entity) { - return Ok(vec![]); - } - - let buffer = self.buffers.get_mut(&entity).unwrap().buffer_mut(); - - let mut alpha_cmd_map = HashMap::new(); - let mut color_cmd_map = HashMap::new(); - - let lines = buffer.layout_runs().filter(|run| run.line_w != 0.0).count(); - let total_height = lines as f32 * buffer.metrics().line_height; - for run in buffer.layout_runs() { - for glyph in run.glyphs { - let physical_glyph = glyph.physical( - (bounds.x, bounds.y + bounds.h * justify.1 - total_height * justify.1), - 1.0, - ); - let cache_key = physical_glyph.cache_key; - - // perform cache lookup for rendered glyph - let Some(rendered) = self.rendered_glyphs.entry(cache_key).or_insert_with(|| { - // ...or insert it - - // do the actual rasterization - let font = self - .font_system - .get_font(cache_key.font_id) - .expect("Somehow shaped a font that doesn't exist"); - let mut scaler = self - .scale_context - .builder(font.as_swash()) - .size(f32::from_bits(cache_key.font_size_bits)) - .hint(config.hint) - .build(); - let offset = - Vector::new(cache_key.x_bin.as_float(), cache_key.y_bin.as_float()); - let image = Render::new(&[ - Source::ColorOutline(0), - Source::ColorBitmap(StrikeWith::BestFit), - Source::Outline, - ]) - .format(if config.subpixel { Format::Subpixel } else { Format::Alpha }) - .offset(offset) - .render(&mut scaler, cache_key.glyph_id); - - // upload it to the GPU - image.map(|image| { - // pick an atlas texture for our glyph - let content_w = image.placement.width as usize; - let content_h = image.placement.height as usize; - let alloc_w = image.placement.width + (GLYPH_MARGIN + GLYPH_PADDING) * 2; - let alloc_h = image.placement.height + (GLYPH_MARGIN + GLYPH_PADDING) * 2; - let used_w = image.placement.width + GLYPH_PADDING * 2; - let used_h = image.placement.height + GLYPH_PADDING * 2; - let mut found = None; - for (texture_index, glyph_atlas) in - self.glyph_textures.iter_mut().enumerate() - { - if let Some((x, y)) = - glyph_atlas.atlas.add_rect(alloc_w as usize, alloc_h as usize) - { - found = Some((texture_index, x, y)); - break; - } - } - let (texture_index, atlas_alloc_x, atlas_alloc_y) = - found.unwrap_or_else(|| { - // if no atlas could fit the texture, make a new atlas tyvm - // TODO error handling - let mut atlas = Atlas::new(TEXTURE_SIZE, TEXTURE_SIZE); - let image_id = canvas - .create_image( - Img::new( - vec![ - RGBA8::new(0, 0, 0, 0); - TEXTURE_SIZE * TEXTURE_SIZE - ], - TEXTURE_SIZE, - TEXTURE_SIZE, - ) - .as_ref(), - ImageFlags::empty(), - ) - .unwrap(); - let texture_index = self.glyph_textures.len(); - let (x, y) = - atlas.add_rect(alloc_w as usize, alloc_h as usize).unwrap(); - self.glyph_textures.push(FontTexture { atlas, image_id }); - (texture_index, x, y) - }); - - let atlas_used_x = atlas_alloc_x as u32 + GLYPH_MARGIN; - let atlas_used_y = atlas_alloc_y as u32 + GLYPH_MARGIN; - let atlas_content_x = atlas_alloc_x as u32 + GLYPH_MARGIN + GLYPH_PADDING; - let atlas_content_y = atlas_alloc_y as u32 + GLYPH_MARGIN + GLYPH_PADDING; - - let mut src_buf = Vec::with_capacity(content_w * content_h); - match image.content { - Content::Mask => { - for chunk in image.data.chunks_exact(1) { - src_buf.push(RGBA8::new(chunk[0], 0, 0, 0)); - } - } - Content::Color | Content::SubpixelMask => { - for chunk in image.data.chunks_exact(4) { - src_buf - .push(RGBA8::new(chunk[0], chunk[1], chunk[2], chunk[3])); - } - } - } - canvas - .update_image::( - self.glyph_textures[texture_index].image_id, - ImgRef::new(&src_buf, content_w, content_h).into(), - atlas_content_x as usize, - atlas_content_y as usize, - ) - .unwrap(); - RenderedGlyph { - texture_index, - width: used_w, - height: used_h, - offset_x: image.placement.left, - offset_y: image.placement.top, - atlas_x: atlas_used_x, - atlas_y: atlas_used_y, - color_glyph: matches!(image.content, Content::Color), - } - }) - }) else { - continue; - }; - - let cmd_map = if rendered.color_glyph { - &mut color_cmd_map - } else { - alpha_cmd_map - .entry(glyph.color_opt.unwrap_or(FontColor::rgb(0, 0, 0))) - .or_insert_with(HashMap::default) - }; - - let cmd = cmd_map.entry(rendered.texture_index).or_insert_with(|| DrawCommand { - image_id: self.glyph_textures[rendered.texture_index].image_id, - quads: Vec::new(), - }); - - let mut q = Quad::default(); - let it = 1.0 / TEXTURE_SIZE as f32; - q.x0 = (physical_glyph.x + rendered.offset_x - GLYPH_PADDING as i32) as f32; - q.y0 = (physical_glyph.y - rendered.offset_y - GLYPH_PADDING as i32 - + run.line_y.round() as i32) as f32; - q.x1 = q.x0 + rendered.width as f32; - q.y1 = q.y0 + rendered.height as f32; - - q.s0 = rendered.atlas_x as f32 * it; - q.t0 = rendered.atlas_y as f32 * it; - q.s1 = (rendered.atlas_x + rendered.width) as f32 * it; - q.t1 = (rendered.atlas_y + rendered.height) as f32 * it; - - cmd.quads.push(q); - } - } - - if !alpha_cmd_map.is_empty() { - Ok(alpha_cmd_map - .into_iter() - .map(|(color, map)| { - ( - color, - GlyphDrawCommands { - alpha_glyphs: map.into_values().collect(), - color_glyphs: color_cmd_map.drain().map(|(_, cmd)| cmd).collect(), - }, - ) - }) - .collect()) - } else { - Ok(vec![( - FontColor(0), - GlyphDrawCommands { - alpha_glyphs: vec![], - color_glyphs: color_cmd_map.drain().map(|(_, cmd)| cmd).collect(), - }, - )]) - } - } - - pub(crate) fn layout_selection( - &mut self, - entity: Entity, - bounds: BoundingBox, - justify: (f32, f32), - ) -> Vec<(f32, f32, f32, f32)> { - self.with_editor(entity, |_, buf| { - let mut result = vec![]; - if let Selection::Normal(cursor_end) = buf.selection() { - let (cursor_start, cursor_end) = match buf.cursor().cmp(&cursor_end) { - Ordering::Less => (buf.cursor(), cursor_end), - Ordering::Greater => (cursor_end, buf.cursor()), - Ordering::Equal => return result, - }; - - let buffer = buf.buffer(); - let total_height = buffer.layout_runs().len() as f32 * buffer.metrics().line_height; - - for run in buffer.layout_runs() { - if let Some((x, w)) = run.highlight(cursor_start, cursor_end) { - let y = run.line_y - buffer.metrics().font_size; - let x = x + bounds.x; - - let y = y + bounds.y + bounds.h * justify.1 - total_height * justify.1; - result.push((x, y, w, buffer.metrics().line_height)); - } - } - } - result - }) - } - - pub(crate) fn layout_caret( - &mut self, - entity: Entity, - bounds: BoundingBox, - justify: (f32, f32), - width: f32, - ) -> Option<(f32, f32, f32, f32)> { - self.with_editor(entity, |_, buf| { - let buffer = buf.buffer(); - let total_height = buffer.layout_runs().len() as f32 * buffer.metrics().line_height; - - let position_y = bounds.y + bounds.h * justify.1 - total_height * justify.1; - - let line_height = buffer.metrics().line_height; - - for run in buffer.layout_runs() { - let line_i = run.line_i; - - let position_x = bounds.x; - - let cursor_glyph_opt = |cursor: &Cursor| -> Option<(usize, f32)> { - if cursor.line == line_i { - for (glyph_i, glyph) in run.glyphs.iter().enumerate() { - if cursor.index == glyph.start { - return Some((glyph_i, 0.0)); - } else if cursor.index > glyph.start && cursor.index < glyph.end { - // Guess x offset based on characters - let mut before = 0; - let mut total = 0; - - let cluster = &run.text[glyph.start..glyph.end]; - for (i, _) in cluster.grapheme_indices(true) { - if glyph.start + i < cursor.index { - before += 1; - } - total += 1; - } - - let offset = glyph.w * (before as f32) / (total as f32); - return Some((glyph_i, offset)); - } - } - match run.glyphs.last() { - Some(glyph) => { - if cursor.index == glyph.end { - return Some((run.glyphs.len(), 0.0)); - } - } - None => { - return Some((0, 0.0)); - } - } - } - None - }; - - if let Some((cursor_glyph, cursor_glyph_offset)) = cursor_glyph_opt(&buf.cursor()) { - let x = match run.glyphs.get(cursor_glyph) { - Some(glyph) => { - // Start of detected glyph - if glyph.level.is_rtl() { - glyph.x + glyph.w - cursor_glyph_offset - } else { - glyph.x + cursor_glyph_offset - } - } - None => match run.glyphs.last() { - Some(glyph) => { - // End of last glyph - if glyph.level.is_rtl() { - glyph.x - } else { - glyph.x + glyph.w - } - } - None => { - // Start of empty line - 0.0 - } - }, - }; - - return Some((position_x + x, position_y + run.line_top, width, line_height)); - } - } - None - }) - } -} - -impl TextContext { - pub(crate) fn new_from_locale_and_db(locale: String, font_db: Database) -> Self { - Self { - font_system: FontSystem::new_with_locale_and_db(locale, font_db), - scale_context: Default::default(), - rendered_glyphs: HashMap::default(), - glyph_textures: vec![], - buffers: HashMap::new(), - bounds: SparseSet::new(), - } - } -} - -pub(crate) struct FontTexture { - atlas: Atlas, - image_id: ImageId, -} - -#[derive(Copy, Clone, Debug)] -pub(crate) struct RenderedGlyph { - texture_index: usize, - width: u32, - height: u32, - offset_x: i32, - offset_y: i32, - atlas_x: u32, - atlas_y: u32, - color_glyph: bool, -} diff --git a/crates/vizia_core/src/text/editable_text.rs b/crates/vizia_core/src/text/editable_text.rs new file mode 100644 index 000000000..9a11f7db2 --- /dev/null +++ b/crates/vizia_core/src/text/editable_text.rs @@ -0,0 +1,212 @@ +use std::{borrow::Cow, ops::Range}; + +use unicode_segmentation::{GraphemeCursor, UnicodeSegmentation}; + +use crate::views::TextEvent; + +use super::{offset_for_delete_backwards, Movement, Selection, TextSelection}; + +pub trait TextData { + fn process_event(&mut self, event: TextEvent); +} + +pub struct TextboxData { + text: String, + selection: Selection, +} + +impl TextData for TextboxData { + fn process_event(&mut self, event: TextEvent) { + match event { + TextEvent::InsertText(text) => { + self.text.replace_range(self.selection.range(), text.as_str()); + } + + TextEvent::DeleteText(movement) => { + let del_range = if self.selection.is_caret() { + let del_offset = offset_for_delete_backwards(&self.selection, &self.text); + del_offset..self.selection.active + } else { + self.selection.range() + }; + + self.text.edit(del_range, ""); + } + + TextEvent::MoveCursor(movement, is_selection) => {} + + _ => {} + } + } +} + +pub trait EditableText: Sized { + /// Replace range with new text. + /// Can panic if supplied an invalid range. + fn edit(&mut self, range: Range, new: impl Into); + + /// Get slice of text at range. + fn slice(&self, range: Range) -> Option>; + + /// Get length of text (in bytes). + fn len(&self) -> usize; + + /// Get the previous word offset from the given offset, if it exists. + fn prev_word_offset(&self, offset: usize) -> Option; + + /// Get the next word offset from the given offset, if it exists. + fn next_word_offset(&self, offset: usize) -> Option; + + /// Get the next grapheme offset from the given offset, if it exists. + fn prev_grapheme_offset(&self, offset: usize) -> Option; + + /// Get the next grapheme offset from the given offset, if it exists. + fn next_grapheme_offset(&self, offset: usize) -> Option; + + /// Get the previous codepoint offset from the given offset, if it exists. + fn prev_codepoint_offset(&self, offset: usize) -> Option; + + /// Get the next codepoint offset from the given offset, if it exists. + fn next_codepoint_offset(&self, offset: usize) -> Option; + + fn prev_codepoint(&self, offset: usize) -> Option; + + /// Get the preceding line break offset from the given offset + fn preceding_line_break(&self, offset: usize) -> usize; + + /// Get the next line break offset from the given offset + fn next_line_break(&self, offset: usize) -> usize; + + /// Returns `true` if this text has 0 length. + fn is_empty(&self) -> bool; + + /// Construct an instance of this type from a `&str`. + fn from_str(s: &str) -> Self; +} + +impl EditableText for String { + fn edit(&mut self, range: Range, new: impl Into) { + self.replace_range(range, &new.into()); + } + + fn slice(&self, range: Range) -> Option> { + self.get(range).map(Cow::from) + } + + fn len(&self) -> usize { + self.len() + } + + fn prev_grapheme_offset(&self, from: usize) -> Option { + let mut c = GraphemeCursor::new(from, self.len(), true); + c.prev_boundary(self, 0).unwrap() + } + + fn next_grapheme_offset(&self, from: usize) -> Option { + let mut c = GraphemeCursor::new(from, self.len(), true); + c.next_boundary(self, 0).unwrap() + } + + fn prev_codepoint_offset(&self, current_pos: usize) -> Option { + if current_pos == 0 { + None + } else { + let mut len = 1; + while !self.is_char_boundary(current_pos - len) { + len += 1; + } + + Some(current_pos - len) + } + } + + fn next_codepoint_offset(&self, current_pos: usize) -> Option { + if current_pos == self.len() { + None + } else { + let b = self.as_bytes()[current_pos]; + Some(current_pos + len_utf8_from_first_byte(b)) + } + } + + fn prev_word_offset(&self, from: usize) -> Option { + let mut offset = from; + let mut passed_alphanumeric = false; + for prev_grapheme in self.get(0..from)?.graphemes(true).rev() { + let is_alphanumeric = prev_grapheme.chars().next()?.is_alphanumeric(); + if is_alphanumeric { + passed_alphanumeric = true; + } else if passed_alphanumeric { + return Some(offset); + } + offset -= prev_grapheme.len(); + } + None + } + + fn next_word_offset(&self, from: usize) -> Option { + let mut offset = from; + let mut passed_alphanumeric = false; + for next_grapheme in self.get(from..)?.graphemes(true) { + let is_alphanumeric = next_grapheme.chars().next()?.is_alphanumeric(); + if is_alphanumeric { + passed_alphanumeric = true; + } else if passed_alphanumeric { + return Some(offset); + } + offset += next_grapheme.len(); + } + Some(self.len()) + } + + fn is_empty(&self) -> bool { + self.is_empty() + } + + fn from_str(s: &str) -> Self { + s.to_string() + } + + fn preceding_line_break(&self, from: usize) -> usize { + let mut offset = from; + + for byte in self.get(0..from).unwrap_or("").bytes().rev() { + if byte == 0x0a { + return offset; + } + offset -= 1; + } + + 0 + } + + fn next_line_break(&self, from: usize) -> usize { + let mut offset = from; + + for char in self.get(from..).unwrap_or("").bytes() { + if char == 0x0a { + return offset; + } + offset += 1; + } + + self.len() + } + + fn prev_codepoint(&self, offset: usize) -> Option { + if let Some(prev) = self.prev_codepoint_offset(offset) { + self[prev..].chars().next() + } else { + None + } + } +} + +pub fn len_utf8_from_first_byte(b: u8) -> usize { + match b { + b if b < 0x80 => 1, + b if b < 0xe0 => 2, + b if b < 0xf0 => 3, + _ => 4, + } +} diff --git a/crates/vizia_core/src/text/mod.rs b/crates/vizia_core/src/text/mod.rs index 39ba859c7..b0dec0673 100644 --- a/crates/vizia_core/src/text/mod.rs +++ b/crates/vizia_core/src/text/mod.rs @@ -4,5 +4,14 @@ pub use movement::*; pub(crate) mod scrolling; pub(crate) use scrolling::*; -pub(crate) mod cosmic; -pub(crate) use cosmic::*; +pub(crate) mod text_context; +pub(crate) use text_context::*; + +pub mod editable_text; +pub use editable_text::*; + +pub mod selection; +pub use selection::*; + +pub mod backspace; +pub use backspace::*; diff --git a/crates/vizia_core/src/text/movement.rs b/crates/vizia_core/src/text/movement.rs index 21ee6c1cc..8c26afd92 100644 --- a/crates/vizia_core/src/text/movement.rs +++ b/crates/vizia_core/src/text/movement.rs @@ -1,3 +1,8 @@ +use log::warn; +use skia_safe::textlayout::Paragraph; + +use super::{EditableText, Selection}; + #[derive(Debug, Clone, Copy)] pub enum Direction { Left, @@ -6,6 +11,25 @@ pub enum Direction { Downstream, } +impl Direction { + /// Returns `true` if this direction is byte-wise backwards for + /// the provided [`WritingDirection`]. + /// + /// The provided direction *must not be* `WritingDirection::Natural`. + pub fn is_upstream_for_direction(self, direction: WritingDirection) -> bool { + assert!( + !matches!(direction, WritingDirection::Natural), + "writing direction must be resolved" + ); + match self { + Direction::Upstream => true, + Direction::Downstream => false, + Direction::Left => matches!(direction, WritingDirection::LeftToRight), + Direction::Right => matches!(direction, WritingDirection::RightToLeft), + } + } +} + #[derive(Debug, Clone, Copy)] pub enum Movement { Grapheme(Direction), @@ -16,6 +40,8 @@ pub enum Movement { LineStart, LineEnd, Vertical(VerticalMovement), + ParagraphStart, + ParagraphEnd, } #[derive(Debug, Clone, Copy)] @@ -27,3 +53,150 @@ pub enum VerticalMovement { DocumentStart, DocumentEnd, } + +#[derive(Debug, Clone, Copy)] +pub enum WritingDirection { + LeftToRight, + RightToLeft, + Natural, +} + +/// Compute the result of a [`Movement`] on a [`Selection`]. +/// +/// returns a new selection representing the state after the movement. +/// +/// If `modify` is true, only the 'active' edge (the `end`) of the selection +/// should be changed; this is the case when the user moves with the shift +/// key pressed. +pub fn apply_movement( + m: Movement, + s: Selection, + text: &T, + paragraph: &Paragraph, + modify: bool, +) -> Selection { + // let writing_direction = if crate::piet::util::first_strong_rtl(text.as_str()) { + // WritingDirection::RightToLeft + // } else { + // WritingDirection::LeftToRight + // }; + + println!("{}", modify); + + let writing_direction = WritingDirection::LeftToRight; + + let (offset, h_pos) = match m { + Movement::Grapheme(d) if d.is_upstream_for_direction(writing_direction) => { + if s.is_caret() || modify { + text.prev_grapheme_offset(s.active).map(|off| (off, None)).unwrap_or((0, s.h_pos)) + } else { + (s.min(), None) + } + } + Movement::Grapheme(_) => { + if s.is_caret() || modify { + text.next_grapheme_offset(s.active) + .map(|off| (off, None)) + .unwrap_or((s.active, s.h_pos)) + } else { + (s.max(), None) + } + } + Movement::Vertical(VerticalMovement::LineUp) => { + let cluster = paragraph.get_glyph_cluster_at(s.active).unwrap(); + let glyph_bounds = cluster.bounds; + let line = paragraph.get_line_number_at(s.active).unwrap(); + let h_pos = s.h_pos.unwrap_or(glyph_bounds.x()); + if line == 0 { + (0, Some(h_pos)) + } else { + let lm = paragraph.get_line_metrics_at(line).unwrap(); + let up_pos = paragraph + .get_closest_glyph_cluster_at((h_pos, glyph_bounds.y() - lm.height as f32)) + .unwrap(); + let s = if h_pos < up_pos.bounds.center_x() { + up_pos.text_range.start + } else { + up_pos.text_range.end + }; + // if up_pos.is_inside { + (s, Some(h_pos)) + // } else { + // // because we can't specify affinity, moving up when h_pos + // // is wider than both the current line and the previous line + // // can result in a cursor position at the visual start of the + // // current line; so we handle this as a special-case. + // let lm_prev = + // paragraph.get_line_metrics_at(line.saturating_sub(1)).unwrap(); + // let up_pos = lm_prev.end_excluding_whitespaces; + // (up_pos, Some(h_pos)) + // } + } + } + Movement::Vertical(VerticalMovement::LineDown) => { + let cluster = paragraph.get_glyph_cluster_at(s.active).unwrap(); + let h_pos = s.h_pos.unwrap_or(cluster.bounds.x()); + let line = paragraph.get_line_number_at(s.active).unwrap(); + if line == paragraph.line_number() - 1 { + (text.len(), Some(h_pos)) + } else { + let lm = paragraph.get_line_metrics_at(line).unwrap(); + // may not work correctly for point sizes below 1.0 + let y_below = lm.baseline - lm.ascent + lm.height + 1.0; + let down_pos = + paragraph.get_closest_glyph_cluster_at((h_pos, y_below as f32)).unwrap(); + let s = if h_pos < down_pos.bounds.center_x() { + down_pos.text_range.start + } else { + down_pos.text_range.end + }; + (s, Some(h_pos)) + } + } + Movement::Vertical(VerticalMovement::DocumentStart) => (0, None), + Movement::Vertical(VerticalMovement::DocumentEnd) => (text.len(), None), + + Movement::ParagraphStart => (text.preceding_line_break(s.active), None), + Movement::ParagraphEnd => (text.next_line_break(s.active), None), + + Movement::Line(d) => { + // let hit = layout.hit_test_text_position(s.active); + // let lm = layout.line_metric(hit.line).unwrap(); + // let offset = if d.is_upstream_for_direction(writing_direction) { + // lm.start_offset + // } else { + // lm.end_offset - lm.trailing_whitespace + // }; + // (offset, None) + todo!() + } + Movement::Word(d) if d.is_upstream_for_direction(writing_direction) => { + let offset = if s.is_caret() || modify { + text.prev_word_offset(s.active).unwrap_or(0) + } else { + s.min() + }; + (offset, None) + } + Movement::Word(_) => { + let offset = if s.is_caret() || modify { + text.next_word_offset(s.active).unwrap_or(s.active) + } else { + s.max() + }; + (offset, None) + } + + // These two are not handled; they require knowledge of the size + // of the viewport. + Movement::Vertical(VerticalMovement::PageDown) + | Movement::Vertical(VerticalMovement::PageUp) => (s.active, s.h_pos), + other => { + warn!("unhandled movement {:?}", other); + (s.anchor, s.h_pos) + } + }; + + let start = if modify { s.anchor } else { offset }; + Selection::new(start, offset).with_h_pos(h_pos) +} diff --git a/crates/vizia_core/src/text/selection.rs b/crates/vizia_core/src/text/selection.rs new file mode 100644 index 000000000..c60ce17d5 --- /dev/null +++ b/crates/vizia_core/src/text/selection.rs @@ -0,0 +1,57 @@ +use std::ops::Range; + +#[derive(Debug, Clone, Copy)] +pub struct Selection { + pub anchor: usize, + pub active: usize, + pub h_pos: Option, +} + +impl Selection { + pub fn new(anchor: usize, active: usize) -> Self { + Selection { anchor, active, h_pos: None } + } + + /// Construct a new selection from this selection, with the provided h_pos. + /// + /// # Note + /// + /// `h_pos` is used to track the *pixel* location of the cursor when moving + /// vertically; lines may have available cursor positions at different + /// positions, and arrowing down and then back up should always result + /// in a cursor at the original starting location; doing this correctly + /// requires tracking this state. + /// + /// You *probably* don't need to use this, unless you are implementing a new + /// text field, or otherwise implementing vertical cursor motion, in which + /// case you will want to set this during vertical motion if it is not + /// already set. + pub fn with_h_pos(mut self, h_pos: Option) -> Self { + self.h_pos = h_pos; + self + } + + pub fn caret(caret: usize) -> Self { + Selection { anchor: caret, active: caret, h_pos: None } + } + + pub fn min(&self) -> usize { + usize::min(self.anchor, self.active) + } + + pub fn max(&self) -> usize { + usize::max(self.anchor, self.active) + } + + pub fn range(&self) -> Range { + self.min()..self.max() + } + + pub fn is_caret(&self) -> bool { + self.min() == self.max() + } +} + +pub trait TextSelection: Sized {} + +impl TextSelection for Selection {} diff --git a/crates/vizia_core/src/text/text_context.rs b/crates/vizia_core/src/text/text_context.rs new file mode 100644 index 000000000..1dc5e2f38 --- /dev/null +++ b/crates/vizia_core/src/text/text_context.rs @@ -0,0 +1,29 @@ +use skia_safe::textlayout::Paragraph; +use skia_safe::{textlayout::FontCollection, FontMgr}; +use vizia_storage::SparseSet; + +use crate::{entity::Entity, layout::BoundingBox}; + +#[derive(Default, Debug, Clone, Copy)] +pub struct TextConfig { + pub hint: bool, + pub subpixel: bool, +} + +pub struct TextContext { + pub font_collection: FontCollection, + pub default_font_manager: FontMgr, + pub text_bounds: SparseSet, + pub text_paragraphs: SparseSet, +} + +impl TextContext { + #[allow(dead_code)] + pub(crate) fn font_collection(&self) -> &FontCollection { + &self.font_collection + } + + pub(crate) fn set_text_bounds(&mut self, entity: Entity, bounds: BoundingBox) { + self.text_bounds.insert(entity, bounds); + } +} diff --git a/crates/vizia_core/src/view.rs b/crates/vizia_core/src/view.rs index e35c62264..511850237 100644 --- a/crates/vizia_core/src/view.rs +++ b/crates/vizia_core/src/view.rs @@ -22,10 +22,10 @@ pub use handle::Handle; use crate::events::ViewHandler; use accesskit::{NodeBuilder, TreeUpdate}; -use femtovg::renderer::OpenGl; +// use femtovg::renderer::OpenGl; /// The canvas which all views draw to. -pub type Canvas = femtovg::Canvas; +// pub type Canvas = femtovg::Canvas; /// A view is any object which can be displayed on the screen. /// @@ -248,7 +248,7 @@ pub trait View: 'static + Sized { /// } /// } /// ``` - fn draw(&self, cx: &mut DrawContext, canvas: &mut Canvas) { + fn draw(&self, cx: &mut DrawContext, canvas: &Canvas) { let bounds = cx.bounds(); //Skip widgets with no width or no height @@ -256,21 +256,23 @@ pub trait View: 'static + Sized { return; } - let mut path = cx.build_path(); + let mut path = cx.build_path(bounds); cx.draw_shadows(canvas, &mut path); - cx.draw_backdrop_filter(canvas, &mut path); + // cx.draw_backdrop_filter(canvas, &mut path); cx.draw_background(canvas, &mut path); cx.draw_border(canvas, &mut path); - cx.draw_inset_box_shadows(canvas, &mut path); + // cx.draw_inset_box_shadows(canvas, &mut path); - cx.draw_outline(canvas); + // cx.draw_outline(canvas); - cx.draw_text_and_selection(canvas); + // cx.draw_text_and_selection(canvas); + + cx.draw_text(canvas); } #[allow(unused_variables)] @@ -289,7 +291,7 @@ where ::event(self, cx, event); } - fn draw(&self, cx: &mut DrawContext, canvas: &mut Canvas) { + fn draw(&self, cx: &mut DrawContext, canvas: &Canvas) { ::draw(self, cx, canvas); } diff --git a/crates/vizia_core/src/views/knob.rs b/crates/vizia_core/src/views/knob.rs index 04fae8805..178cd17f4 100644 --- a/crates/vizia_core/src/views/knob.rs +++ b/crates/vizia_core/src/views/knob.rs @@ -1,8 +1,9 @@ #![allow(dead_code)] #![allow(unused_imports)] #![allow(unused_variables)] -use femtovg::{LineCap, Paint, Path, Solidity}; +// use femtovg::{LineCap, Paint, Path, Solidity}; use morphorm::Units; +use skia_safe::{Paint, PaintCap, Path}; use crate::prelude::*; @@ -246,12 +247,12 @@ impl View for ArcTrack { Some("arctrack") } - fn draw(&self, cx: &mut DrawContext, canvas: &mut Canvas) { + fn draw(&self, cx: &mut DrawContext, canvas: &Canvas) { let opacity = cx.opacity(); - let foreground_color = cx.font_color().into(); + let foreground_color = cx.font_color(); - let background_color = cx.background_color().into(); + let background_color = cx.background_color(); let bounds = cx.bounds(); @@ -275,9 +276,10 @@ impl View for ArcTrack { // Draw the track arc let mut path = Path::new(); path.arc(centerx, centery, radius - span / 2.0, end, start, Solidity::Solid); - let mut paint = Paint::color(background_color); - paint.set_line_width(span); - paint.set_line_cap(LineCap::Round); + let mut paint = Paint::default(); + paint.set_color(background_color); + paint.set_stroke_width(span); + paint.set_stroke_cap(PaintCap::Round); canvas.stroke_path(&path, &paint); // Draw the active arc @@ -377,7 +379,7 @@ impl View for Ticks { fn element(&self) -> Option<&'static str> { Some("ticks") } - fn draw(&self, cx: &mut DrawContext, canvas: &mut Canvas) { + fn draw(&self, cx: &mut DrawContext, canvas: &Canvas) { let opacity = cx.opacity(); //let mut background_color: femtovg::Color = cx.current.get_background_color(cx).into(); // background_color.set_alphaf(background_color.a * opacity); diff --git a/crates/vizia_core/src/views/mod.rs b/crates/vizia_core/src/views/mod.rs index 0f4eb2993..9322409e2 100644 --- a/crates/vizia_core/src/views/mod.rs +++ b/crates/vizia_core/src/views/mod.rs @@ -13,7 +13,7 @@ mod dropdown; mod element; mod form; mod image; -mod knob; +// mod knob; mod label; mod list; mod menu; @@ -50,8 +50,7 @@ pub use divider::*; pub use dropdown::Dropdown; pub use element::Element; pub use form::{FormControl, FormGroup, FormPlacement}; -// pub use keybind::*; -pub use knob::{ArcTrack, Knob, KnobMode, TickKnob, Ticks}; +// pub use knob::{ArcTrack, Knob, KnobMode, TickKnob, Ticks}; pub use label::{Icon, Label}; pub use list::*; pub use menu::*; diff --git a/crates/vizia_core/src/views/popup.rs b/crates/vizia_core/src/views/popup.rs index 3f2cab5c9..e683b50ec 100644 --- a/crates/vizia_core/src/views/popup.rs +++ b/crates/vizia_core/src/views/popup.rs @@ -485,36 +485,36 @@ impl View for Arrow { fn element(&self) -> Option<&'static str> { Some("arrow") } - fn draw(&self, cx: &mut DrawContext, canvas: &mut Canvas) { + fn draw(&self, cx: &mut DrawContext, canvas: &Canvas) { let bounds = cx.bounds(); let mut path = vg::Path::new(); match Popup::placement.get(cx) { Placement::Bottom | Placement::BottomStart | Placement::BottomEnd => { - path.move_to(bounds.bottom_left().1, bounds.bottom_left().0); - path.line_to(bounds.center_top().0, bounds.center_top().1); - path.line_to(bounds.bottom_right().1, bounds.bottom_right().0); - path.line_to(bounds.bottom_left().1, bounds.bottom_left().0); + path.move_to(bounds.bottom_left()); + path.line_to(bounds.center_top()); + path.line_to(bounds.bottom_right()); + path.line_to(bounds.bottom_left()); } Placement::Top | Placement::TopStart | Placement::TopEnd => { - path.move_to(bounds.top_left().1, bounds.top_left().0); - path.line_to(bounds.center_bottom().0, bounds.center_bottom().1); - path.line_to(bounds.top_right().1, bounds.top_right().0); - path.line_to(bounds.top_left().1, bounds.top_left().0); + path.move_to(bounds.top_left()); + path.line_to(bounds.center_bottom()); + path.line_to(bounds.top_right()); + path.line_to(bounds.top_left()); } Placement::Left | Placement::LeftStart | Placement::LeftEnd => { - path.move_to(bounds.top_left().1, bounds.top_left().0); - path.line_to(bounds.center_right().0, bounds.center_right().1); - path.line_to(bounds.bottom_left().1, bounds.bottom_left().0); - path.line_to(bounds.top_left().1, bounds.top_left().0); + path.move_to(bounds.top_left()); + path.line_to(bounds.center_right()); + path.line_to(bounds.bottom_left()); + path.line_to(bounds.top_left()); } Placement::Right | Placement::RightStart | Placement::RightEnd => { - path.move_to(bounds.top_right().1, bounds.top_right().0); - path.line_to(bounds.center_left().0, bounds.center_left().1); - path.line_to(bounds.bottom_right().1, bounds.bottom_right().0); - path.line_to(bounds.top_right().1, bounds.top_right().0); + path.move_to(bounds.top_right()); + path.line_to(bounds.center_left()); + path.line_to(bounds.bottom_right()); + path.line_to(bounds.top_right()); } _ => {} @@ -522,7 +522,8 @@ impl View for Arrow { path.close(); let bg = cx.background_color(); - - canvas.fill_path(&path, &vg::Paint::color(bg.into())); + let mut paint = vg::Paint::default(); + paint.set_color(bg); + canvas.draw_path(&path, &paint); } } diff --git a/crates/vizia_core/src/views/textbox.rs b/crates/vizia_core/src/views/textbox.rs index 91e799104..0da639cfb 100644 --- a/crates/vizia_core/src/views/textbox.rs +++ b/crates/vizia_core/src/views/textbox.rs @@ -1,11 +1,14 @@ use crate::accessibility::IntoNode; -use crate::prelude::*; +use crate::{prelude::*, text}; -use crate::text::{enforce_text_bounds, ensure_visible, Direction, Movement}; +use crate::text::{ + apply_movement, enforce_text_bounds, ensure_visible, offset_for_delete_backwards, Direction, + EditableText, Movement, Selection, VerticalMovement, +}; use crate::views::scrollview::SCROLL_SENSITIVITY; use accesskit::{ActionData, ActionRequest, TextDirection, TextPosition, TextSelection}; -use cosmic_text::{Action, Cursor, Edit, Editor, FontSystem, Selection}; -use unicode_segmentation::UnicodeSegmentation; +use skia_safe::textlayout::{RectHeightStyle, RectWidthStyle}; +use skia_safe::{Paint, PaintStyle, Rect}; /// Events for modifying a textbox. pub enum TextEvent { @@ -71,6 +74,7 @@ pub struct Textbox { placeholder_shown: bool, show_caret: bool, caret_timer: Timer, + selection: Selection, } // Determines whether the enter key submits the text or inserts a new line. @@ -156,6 +160,7 @@ where placeholder_shown: true, show_caret: true, caret_timer, + selection: Selection::new(0, 0), } .build(cx, move |cx| { cx.add_listener(move |textbox: &mut Self, cx, event| { @@ -171,16 +176,16 @@ where }); }); - Binding::new(cx, lens, move |cx: &mut Context, text| { - Binding::new(cx, Self::edit, move |cx, edit| { - if !edit.get(cx) { - let text_str = text.get(cx).to_string(); - cx.emit(TextEvent::SelectAll); - cx.emit(TextEvent::InsertText(text_str)); - cx.emit(TextEvent::Scroll(0.0, 0.0)); - } - }) - }); + // Binding::new(cx, lens, move |cx: &mut Context, text| { + // Binding::new(cx, Self::edit, move |cx, edit| { + // if !edit.get(cx) { + // let text_str = text.get(cx).to_string(); + // cx.emit(TextEvent::SelectAll); + // cx.emit(TextEvent::InsertText(text_str)); + // cx.emit(TextEvent::Scroll(0.0, 0.0)); + // } + // }) + // }); Binding::new(cx, Self::placeholder, |cx, placeholder| { let placeholder_string = placeholder.get(cx); @@ -203,265 +208,89 @@ where .text_value(lens) .default_action_verb(DefaultActionVerb::Focus) .toggle_class("caret", Self::show_caret) + .text(lens) } - fn set_caret(&mut self, cx: &mut EventContext) { - // Calculate visible area for content and container - let mut text_bounds = cx.text_context.get_bounds(cx.current).unwrap_or_default(); - let mut bounds = cx.bounds(); - - let child_left = cx.style.child_left.get(cx.current).copied().unwrap_or_default(); - let child_top = cx.style.child_top.get(cx.current).copied().unwrap_or_default(); - let child_right = cx.style.child_right.get(cx.current).copied().unwrap_or_default(); - let child_bottom = cx.style.child_bottom.get(cx.current).copied().unwrap_or_default(); - - let logical_parent_width = cx.physical_to_logical(bounds.w); - let logical_parent_height = cx.physical_to_logical(bounds.h); - - let child_left = child_left.to_px(logical_parent_width, 0.0) * cx.scale_factor(); - let child_top = child_top.to_px(logical_parent_height, 0.0) * cx.scale_factor(); - let child_right = child_right.to_px(logical_parent_width, 0.0) * cx.scale_factor(); - let child_bottom = child_bottom.to_px(logical_parent_height, 0.0) * cx.scale_factor(); - - text_bounds.x = bounds.x; - text_bounds.y = bounds.y; - - bounds.h -= child_top + child_bottom; - bounds.w -= child_left + child_right; - - cx.text_context.sync_styles(cx.current, cx.style); + fn set_caret(&mut self, cx: &mut EventContext) {} - let (mut tx, mut ty) = self.transform; - - (tx, ty) = enforce_text_bounds(&text_bounds, &bounds, (tx, ty)); - - text_bounds.x += child_left; - text_bounds.y += child_top; - - // TODO justify???? - if let Some((x, y, _, h)) = - cx.text_context.layout_caret(cx.current, text_bounds, (0., 0.), 1.0 * cx.scale_factor()) - { - let caret_box = BoundingBox { x, y, w: 0.0, h }; - bounds.x += child_left; - bounds.y += child_top; - - (tx, ty) = ensure_visible(&caret_box, &bounds, (tx, ty)); + fn insert_text(&mut self, cx: &mut EventContext, txt: &str) { + if let Some(text) = cx.style.text.get_mut(cx.current) { + text.edit(self.selection.range(), txt); + self.selection = Selection::caret(self.selection.active + txt.len()); } - - self.transform = (tx.round(), ty.round()); - } - - fn insert_text(&mut self, cx: &mut EventContext, text: &str) { - cx.text_context.with_editor(cx.current, |_: &mut FontSystem, buf| { - buf.insert_string(text, None); - }); - cx.needs_relayout(); - cx.needs_redraw(); } fn delete_text(&mut self, cx: &mut EventContext, movement: Movement) { - let x = |_: &mut FontSystem, buf: &mut Editor| { - let no_selection = match (buf.cursor(), buf.selection()) { - (cursor, Selection::Normal(selection)) => cursor == selection, - (_, _) => true, + if let Some(text) = cx.style.text.get_mut(cx.current) { + let del_range = if self.selection.is_caret() { + let del_offset = offset_for_delete_backwards(&self.selection, text); + del_offset..self.selection.active + } else { + self.selection.range() }; - buf.delete_selection(); - no_selection - }; - - if cx.text_context.with_editor(cx.current, x) { - self.move_cursor(cx, movement, true); - cx.text_context.with_editor(cx.current, |_, buf| { - buf.delete_selection(); - }); + + self.selection = Selection::caret(del_range.start); + + text.edit(del_range, ""); } - cx.needs_relayout(); - cx.needs_redraw(); } - fn reset_text(&mut self, cx: &mut EventContext) { - self.select_all(cx); - cx.text_context.with_editor(cx.current, |_, buf| { - buf.delete_selection(); - }); - cx.needs_relayout(); - cx.needs_redraw(); - } + fn reset_text(&mut self, cx: &mut EventContext) {} fn move_cursor(&mut self, cx: &mut EventContext, movement: Movement, selection: bool) { - cx.text_context.with_editor(cx.current, |fs, buf| { - if selection { - if buf.selection() == Selection::None { - buf.set_selection(Selection::Normal(buf.cursor())); - } - } else { - buf.set_selection(Selection::None); + if let Some(text) = cx.style.text.get_mut(cx.current) { + if let Some(paragraph) = cx.text_context.text_paragraphs.get(cx.current) { + let new_selection = + apply_movement(movement, self.selection, text, paragraph, selection); + println!("{:?} {:?}", self.selection, new_selection); + self.selection = new_selection; + cx.needs_redraw(); } - - buf.action( - fs, - match movement { - Movement::Grapheme(Direction::Upstream) => Action::Previous, - Movement::Grapheme(Direction::Downstream) => Action::Next, - Movement::Grapheme(Direction::Left) => Action::Left, - Movement::Grapheme(Direction::Right) => Action::Right, - Movement::Word(Direction::Upstream) => Action::PreviousWord, - Movement::Word(Direction::Downstream) => Action::NextWord, - Movement::Word(Direction::Left) => Action::LeftWord, - Movement::Word(Direction::Right) => Action::RightWord, - Movement::Line(Direction::Upstream) => Action::Up, - Movement::Line(Direction::Downstream) => Action::Down, - Movement::LineStart => Action::Home, - Movement::LineEnd => Action::End, - Movement::Page(dir) => { - let parent = cx.current.parent(cx.tree).unwrap(); - let parent_bounds = *cx.cache.bounds.get(parent).unwrap(); - let sign = if let Direction::Upstream = dir { -1 } else { 1 }; - Action::Vertical(sign * parent_bounds.h as i32) - } - Movement::Body(Direction::Upstream) => Action::BufferStart, - Movement::Body(Direction::Downstream) => Action::BufferEnd, - _ => return, - }, - ); - }); - cx.needs_relayout(); - cx.needs_redraw(); + } } fn select_all(&mut self, cx: &mut EventContext) { - cx.text_context.with_editor(cx.current, |fs, buf| { - buf.action(fs, Action::BufferStart); - buf.set_selection(Selection::Normal(buf.cursor())); - buf.action(fs, Action::BufferEnd); - }); - cx.needs_redraw(); + if let Some(text) = cx.style.text.get(cx.current) { + self.selection.anchor = 0; + self.selection.active = text.len(); + } } - fn select_word(&mut self, cx: &mut EventContext) { - cx.text_context.with_editor(cx.current, |fs, buf| { - buf.action(fs, Action::PreviousWord); - buf.set_selection(Selection::Normal(buf.cursor())); - buf.action(fs, Action::NextWord); - }); - cx.needs_redraw(); - } + fn select_word(&mut self, cx: &mut EventContext) {} - fn select_paragraph(&mut self, cx: &mut EventContext) { - cx.text_context.with_editor(cx.current, |fs, buf| { - buf.action(fs, Action::ParagraphStart); - buf.set_selection(Selection::Normal(buf.cursor())); - buf.action(fs, Action::ParagraphEnd); - }); - cx.needs_redraw(); - } + fn select_paragraph(&mut self, cx: &mut EventContext) {} fn deselect(&mut self, cx: &mut EventContext) { - cx.text_context.with_editor(cx.current, |_, buf| { - buf.set_selection(Selection::None); - }); - cx.needs_redraw(); + self.selection = Selection::caret(self.selection.active); } /// These input coordinates should be physical coordinates, i.e. what the mouse events provide. /// The output text coordinates will also be physical, but relative to the top of the text /// glyphs, appropriate for passage to cosmic. fn coordinates_global_to_text(&self, cx: &mut EventContext, x: f32, y: f32) -> (f32, f32) { - let bounds = cx.bounds(); - - let child_left = cx.style.child_left.get(cx.current).copied().unwrap_or_default(); - let child_top = cx.style.child_top.get(cx.current).copied().unwrap_or_default(); - let _child_right = cx.style.child_right.get(cx.current).copied().unwrap_or_default(); - let child_bottom = cx.style.child_bottom.get(cx.current).copied().unwrap_or_default(); - - let justify_y = match (child_top, child_bottom) { - (Stretch(top), Stretch(bottom)) => { - if top + bottom == 0.0 { - 0.5 - } else { - top / (top + bottom) - } - } - (Stretch(_), _) => 1.0, - _ => 0.0, - }; - - let logical_parent_width = cx.physical_to_logical(bounds.w); - let logical_parent_height = cx.physical_to_logical(bounds.h); - - let child_left = child_left.to_px(logical_parent_width, 0.0) * cx.scale_factor(); - let child_top = child_top.to_px(logical_parent_height, 0.0) * cx.scale_factor(); - - let total_height = cx.text_context.with_buffer(cx.current, |_, buffer| { - buffer.layout_runs().len() as f32 * buffer.metrics().line_height - }); - - let x = x - bounds.x - self.transform.0 - child_left; - let y = y - self.transform.1 - bounds.y - (bounds.h - total_height) * justify_y - child_top; - - (x, y) + todo!() } /// This function takes window-global physical coordinates. - fn hit(&mut self, cx: &mut EventContext, x: f32, y: f32) { - let (x, y) = self.coordinates_global_to_text(cx, x, y); - cx.text_context.with_editor(cx.current, |fs, buf| { - buf.action(fs, Action::Click { x: x as i32, y: y as i32 }); - }); - cx.needs_redraw(); - } + fn hit(&mut self, cx: &mut EventContext, x: f32, y: f32) {} /// This function takes window-global physical coordinates. - fn drag(&mut self, cx: &mut EventContext, x: f32, y: f32) { - let (x, y) = self.coordinates_global_to_text(cx, x, y); - cx.text_context.with_editor(cx.current, |fs, buf| { - buf.action(fs, Action::Drag { x: x as i32, y: y as i32 }); - }); - cx.needs_redraw(); - } + fn drag(&mut self, cx: &mut EventContext, x: f32, y: f32) {} /// This function takes window-global physical dimensions. - fn scroll(&mut self, cx: &mut EventContext, x: f32, y: f32) { - let entity = cx.current; - let mut bounds = cx.cache.get_bounds(entity); - - let child_left = cx.style.child_left.get(cx.current).copied().unwrap_or_default(); - let child_top = cx.style.child_top.get(cx.current).copied().unwrap_or_default(); - let child_right = cx.style.child_right.get(cx.current).copied().unwrap_or_default(); - let child_bottom = cx.style.child_bottom.get(cx.current).copied().unwrap_or_default(); - - let logical_parent_width = cx.physical_to_logical(bounds.w); - let logical_parent_height = cx.physical_to_logical(bounds.h); - - let child_left = child_left.to_px(logical_parent_width, 0.0) * cx.scale_factor(); - let child_top = child_top.to_px(logical_parent_height, 0.0) * cx.scale_factor(); - let child_right = child_right.to_px(logical_parent_width, 0.0) * cx.scale_factor(); - let child_bottom = child_bottom.to_px(logical_parent_height, 0.0) * cx.scale_factor(); - - if let Some(mut text_bounds) = cx.text_context.get_bounds(entity) { - text_bounds.x = bounds.x; - text_bounds.y = bounds.y; - bounds.h -= child_top + child_bottom; - bounds.w -= child_left + child_right; - let (mut tx, mut ty) = self.transform; - tx += x * SCROLL_SENSITIVITY; - ty += y * SCROLL_SENSITIVITY; - (tx, ty) = enforce_text_bounds(&text_bounds, &bounds, (tx, ty)); - self.transform = (tx, ty); - cx.needs_redraw(); - } - } + fn scroll(&mut self, cx: &mut EventContext, x: f32, y: f32) {} #[allow(dead_code)] fn clone_selected(&self, cx: &mut EventContext) -> Option { - cx.text_context.with_editor(cx.current, |_, buf| buf.copy_selection()) + todo!() } fn clone_text(&self, cx: &mut EventContext) -> String { - cx.text_context.with_buffer(cx.current, |_, buf| { - buf.lines.iter().map(|line| line.text()).collect::>().join("\n") - }) + if let Some(text) = cx.style.text.get(cx.current) { + text.clone() + } else { + String::new() + } } fn reset_caret_timer(&mut self, cx: &mut EventContext) { @@ -471,6 +300,157 @@ where cx.start_timer(self.caret_timer); } } + + pub fn draw_selection(&self, cx: &mut DrawContext, canvas: &Canvas) { + if let Some(mut paragraph) = cx.text_context.text_paragraphs.get(cx.current) { + let cursor_rects = paragraph.get_rects_for_range( + self.selection.min()..self.selection.max(), + RectHeightStyle::Tight, + RectWidthStyle::Tight, + ); + + for cursor_rect in cursor_rects { + let bounds = cx.bounds(); + let padding_left = cx.child_left().to_px(bounds.width(), 0.0); + let padding_right = cx.child_right().to_px(bounds.width(), 0.0); + let text_bounds = cx + .text_context + .text_bounds + .get(cx.current) + .copied() + .unwrap_or(bounds.shrink_sides(padding_left, 0.0, padding_right, 0.0)); + // paragraph.layout(text_bounds.width().min(bounds.width())); + + let text_length = cx.style.text.get(cx.current).map(|txt| txt.len()).unwrap(); + + let lm = paragraph.get_actual_text_range(0, false); + // println!("{:?} {}", lm, self.selection.active); + + // let rng = if self.selection.active == 0 { + // (self.selection.active - 1)..self.selection.active + // } else { + // self.selection.active..self.selection.active + 1 + // }; + + let mut vertical_flex_sum = 0.0; + + let mut padding_top = match cx.child_top() { + Units::Pixels(val) => val, + Units::Stretch(val) => { + vertical_flex_sum += val; + 0.0 + } + _ => 0.0, + }; + + let padding_bottom = match cx.child_bottom() { + Units::Pixels(val) => val, + Units::Stretch(val) => { + vertical_flex_sum += val; + 0.0 + } + _ => 0.0, + }; + + let vertical_free_space = + bounds.height() - paragraph.height() as f32 - padding_top - padding_bottom; + + if let Units::Stretch(val) = cx.child_top() { + padding_top = (vertical_free_space * val / vertical_flex_sum).ceil() + } + + let x = bounds.x + padding_left + cursor_rect.rect.left; + let y = bounds.y + padding_top + cursor_rect.rect.top; + + let x2 = x + (cursor_rect.rect.right - cursor_rect.rect.left); + let y2 = y + (cursor_rect.rect.bottom - cursor_rect.rect.top); + + let mut paint = Paint::default(); + paint.set_anti_alias(true); + paint.set_style(PaintStyle::Fill); + paint.set_color(cx.selection_color()); + + canvas.draw_rect(Rect::new(x, y, x2, y2), &paint); + } + } + } + + /// Draw text caret for the current view. + pub fn draw_text_caret(&self, cx: &mut DrawContext, canvas: &Canvas) { + if let Some(mut paragraph) = cx.text_context.text_paragraphs.get(cx.current) { + let bounds = cx.bounds(); + let padding_left = cx.child_left().to_px(bounds.width(), 0.0); + let padding_right = cx.child_right().to_px(bounds.width(), 0.0); + let text_bounds = cx + .text_context + .text_bounds + .get(cx.current) + .copied() + .unwrap_or(bounds.shrink_sides(padding_left, 0.0, padding_right, 0.0)); + // paragraph.layout(text_bounds.width().min(bounds.width())); + + let text_length = cx.style.text.get(cx.current).map(|txt| txt.len()).unwrap(); + + let lm = paragraph.get_actual_text_range(0, false); + // println!("{:?} {}", lm, self.selection.active); + + // let rng = if self.selection.active == 0 { + // (self.selection.active - 1)..self.selection.active + // } else { + // self.selection.active..self.selection.active + 1 + // }; + + let rects = paragraph.get_rects_for_range( + self.selection.active..self.selection.active + 1, + RectHeightStyle::Tight, + RectWidthStyle::Tight, + ); + // println!("{:?}", rects); + + let cursor_rect = rects.first().unwrap(); + + let mut vertical_flex_sum = 0.0; + + let mut padding_top = match cx.child_top() { + Units::Pixels(val) => val, + Units::Stretch(val) => { + vertical_flex_sum += val; + 0.0 + } + _ => 0.0, + }; + + let padding_bottom = match cx.child_bottom() { + Units::Pixels(val) => val, + Units::Stretch(val) => { + vertical_flex_sum += val; + 0.0 + } + _ => 0.0, + }; + + let vertical_free_space = + bounds.height() - paragraph.height() as f32 - padding_top - padding_bottom; + + if let Units::Stretch(val) = cx.child_top() { + padding_top = (vertical_free_space * val / vertical_flex_sum).ceil() + } + + let x = bounds.x + padding_left + cursor_rect.rect.left; + let y = bounds.y + padding_top + cursor_rect.rect.top; + + let x2 = x + 2.0; + let y2 = y + (cursor_rect.rect.bottom - cursor_rect.rect.top); + + // println!("x {} {} {} {}", x, y, x2, y2); + let mut paint = Paint::default(); + paint.set_anti_alias(true); + paint.set_style(PaintStyle::Fill); + paint.set_color(cx.caret_color()); + + canvas.draw_rect(Rect::new(x, y, x2, y2), &paint); + } + } } impl<'a, L: Lens> Handle<'a, Textbox> { @@ -547,142 +527,142 @@ where let bounds = cx.bounds(); let node_id = node.node_id(); - cx.text_context.with_editor(cx.current, |_, editor| { - let mut selection = editor.selection(); - let cursor = editor.cursor(); - - let mut selection_active_line = node_id; - let mut selection_anchor_line = node_id; - let mut selection_active_cursor = 0; - let mut selection_anchor_cursor = 0; - - let mut current_cursor = 0; - let mut prev_line_index = std::usize::MAX; - - for (index, line) in editor.buffer().layout_runs().enumerate() { - let text = line.text; - - // We need a child node per line - let mut line_node = AccessNode::new_from_parent(node_id, index); - line_node.set_role(Role::InlineTextBox); - - let line_height = editor.buffer().metrics().line_height; - line_node.set_bounds(BoundingBox { - x: bounds.x, - y: bounds.y + line.line_y - editor.buffer().metrics().font_size, - w: line.line_w, - h: line_height, - }); - line_node.set_text_direction(if line.rtl { - TextDirection::RightToLeft - } else { - TextDirection::LeftToRight - }); - - let mut character_lengths = Vec::with_capacity(line.glyphs.len()); - let mut character_positions = Vec::with_capacity(line.glyphs.len()); - let mut character_widths = Vec::with_capacity(line.glyphs.len()); - - // Get the actual text in the line - let first_glyph_pos = - line.glyphs.first().map(|glyph| glyph.start).unwrap_or_default(); - let last_glyph_pos = line.glyphs.last().map(|glyph| glyph.end).unwrap_or_default(); - - let mut line_text = text[first_glyph_pos..last_glyph_pos].to_owned(); - - let word_lengths = - line_text.unicode_words().map(|word| word.len() as u8).collect::>(); - - let mut line_length = 0; - - for glyph in line.glyphs.iter() { - let length = (glyph.end - glyph.start) as u8; - - line_length += length as usize; - - let position = glyph.x; - let width = glyph.w; - - character_lengths.push(length); - character_positions.push(position); - character_widths.push(width); - } - - // Cosmic strips the newlines but accesskit needs them so we append them back in if line originally ended with a newline - // If the last glyph position is equal to the end of the buffer line then this layout run is the last one and ends in a newline. - if last_glyph_pos == line.text.len() { - line_text += "\n"; - character_lengths.push(1); - character_positions.push(line.line_w); - character_widths.push(0.0); - } - - // TODO: Might need to append any spaces that were stripped during layout. This can be done by - // figuring out if the start of the next line is greater than the end of the current line as long - // as the lines have the same `line_i`. This will require a peekable iterator loop. - - line_node.set_value(line_text.into_boxed_str()); - line_node.set_character_lengths(character_lengths.into_boxed_slice()); - line_node.set_character_positions(character_positions.into_boxed_slice()); - line_node.set_character_widths(character_widths.into_boxed_slice()); - line_node.set_word_lengths(word_lengths.into_boxed_slice()); - - if line.line_i != prev_line_index { - current_cursor = 0; - } - - if line.line_i == cursor.line { - if prev_line_index != line.line_i { - if cursor.index <= line_length { - selection_active_line = line_node.node_id(); - selection_active_cursor = cursor.index; - } - } else if cursor.index > current_cursor { - selection_active_line = line_node.node_id(); - selection_active_cursor = cursor.index - current_cursor; - } - } - - // Check if the current line contains the cursor or selection - // This is a mess because a line happens due to soft and hard breaks but - // the cursor and selected indices are relative to the lines caused by hard breaks only. - if selection == Selection::None { - selection = Selection::Normal(cursor); - } - if let Selection::Normal(selection) = selection { - if line.line_i == selection.line { - // A previous line index different to the current means that the current line follows a hard break - if prev_line_index != line.line_i { - if selection.index <= line_length { - selection_anchor_line = line_node.node_id(); - selection_anchor_cursor = selection.index; - } - } else if selection.index > current_cursor { - selection_anchor_line = line_node.node_id(); - selection_anchor_cursor = selection.index - current_cursor; - } - } - } - - node.add_child(line_node); - - current_cursor += line_length; - prev_line_index = line.line_i; - } - - node.set_text_selection(TextSelection { - anchor: TextPosition { - node: selection_anchor_line, - character_index: selection_anchor_cursor, - }, - focus: TextPosition { - node: selection_active_line, - character_index: selection_active_cursor, - }, - }); - - node.node_builder.set_default_action_verb(DefaultActionVerb::Focus); - }); + // cx.text_context.with_editor(cx.current, |_, editor| { + // let mut selection = editor.selection(); + // let cursor = editor.cursor(); + + // let mut selection_active_line = node_id; + // let mut selection_anchor_line = node_id; + // let mut selection_active_cursor = 0; + // let mut selection_anchor_cursor = 0; + + // let mut current_cursor = 0; + // let mut prev_line_index = std::usize::MAX; + + // for (index, line) in editor.buffer().layout_runs().enumerate() { + // let text = line.text; + + // // We need a child node per line + // let mut line_node = AccessNode::new_from_parent(node_id, index); + // line_node.set_role(Role::InlineTextBox); + + // let line_height = editor.buffer().metrics().line_height; + // line_node.set_bounds(BoundingBox { + // x: bounds.x, + // y: bounds.y + line.line_y - editor.buffer().metrics().font_size, + // w: line.line_w, + // h: line_height, + // }); + // line_node.set_text_direction(if line.rtl { + // TextDirection::RightToLeft + // } else { + // TextDirection::LeftToRight + // }); + + // let mut character_lengths = Vec::with_capacity(line.glyphs.len()); + // let mut character_positions = Vec::with_capacity(line.glyphs.len()); + // let mut character_widths = Vec::with_capacity(line.glyphs.len()); + + // // Get the actual text in the line + // let first_glyph_pos = + // line.glyphs.first().map(|glyph| glyph.start).unwrap_or_default(); + // let last_glyph_pos = line.glyphs.last().map(|glyph| glyph.end).unwrap_or_default(); + + // let mut line_text = text[first_glyph_pos..last_glyph_pos].to_owned(); + + // let word_lengths = + // line_text.unicode_words().map(|word| word.len() as u8).collect::>(); + + // let mut line_length = 0; + + // for glyph in line.glyphs.iter() { + // let length = (glyph.end - glyph.start) as u8; + + // line_length += length as usize; + + // let position = glyph.x; + // let width = glyph.w; + + // character_lengths.push(length); + // character_positions.push(position); + // character_widths.push(width); + // } + + // // Cosmic strips the newlines but accesskit needs them so we append them back in if line originally ended with a newline + // // If the last glyph position is equal to the end of the buffer line then this layout run is the last one and ends in a newline. + // if last_glyph_pos == line.text.len() { + // line_text += "\n"; + // character_lengths.push(1); + // character_positions.push(line.line_w); + // character_widths.push(0.0); + // } + + // // TODO: Might need to append any spaces that were stripped during layout. This can be done by + // // figuring out if the start of the next line is greater than the end of the current line as long + // // as the lines have the same `line_i`. This will require a peekable iterator loop. + + // line_node.set_value(line_text.into_boxed_str()); + // line_node.set_character_lengths(character_lengths.into_boxed_slice()); + // line_node.set_character_positions(character_positions.into_boxed_slice()); + // line_node.set_character_widths(character_widths.into_boxed_slice()); + // line_node.set_word_lengths(word_lengths.into_boxed_slice()); + + // if line.line_i != prev_line_index { + // current_cursor = 0; + // } + + // if line.line_i == cursor.line { + // if prev_line_index != line.line_i { + // if cursor.index <= line_length { + // selection_active_line = line_node.node_id(); + // selection_active_cursor = cursor.index; + // } + // } else if cursor.index > current_cursor { + // selection_active_line = line_node.node_id(); + // selection_active_cursor = cursor.index - current_cursor; + // } + // } + + // // Check if the current line contains the cursor or selection + // // This is a mess because a line happens due to soft and hard breaks but + // // the cursor and selected indices are relative to the lines caused by hard breaks only. + // // if selection == Selection::None { + // // selection = Selection::Normal(cursor); + // // } + // // if let Selection::Normal(selection) = selection { + // // if line.line_i == selection.line { + // // // A previous line index different to the current means that the current line follows a hard break + // // if prev_line_index != line.line_i { + // // if selection.index <= line_length { + // // selection_anchor_line = line_node.node_id(); + // // selection_anchor_cursor = selection.index; + // // } + // // } else if selection.index > current_cursor { + // // selection_anchor_line = line_node.node_id(); + // // selection_anchor_cursor = selection.index - current_cursor; + // // } + // // } + // // } + + // node.add_child(line_node); + + // current_cursor += line_length; + // prev_line_index = line.line_i; + // } + + // node.set_text_selection(TextSelection { + // anchor: TextPosition { + // node: selection_anchor_line, + // character_index: selection_anchor_cursor, + // }, + // focus: TextPosition { + // node: selection_active_line, + // character_index: selection_active_cursor, + // }, + // }); + + // node.node_builder.set_default_action_verb(DefaultActionVerb::Focus); + // }); } fn event(&mut self, cx: &mut EventContext, event: &mut Event) { @@ -821,7 +801,7 @@ where self.reset_caret_timer(cx); if self.kind != TextboxKind::SingleLine { cx.emit(TextEvent::MoveCursor( - Movement::Line(Direction::Upstream), + Movement::Vertical(VerticalMovement::LineUp), cx.modifiers.shift(), )); } @@ -831,7 +811,7 @@ where self.reset_caret_timer(cx); if self.kind != TextboxKind::SingleLine { cx.emit(TextEvent::MoveCursor( - Movement::Line(Direction::Downstream), + Movement::Vertical(VerticalMovement::LineDown), cx.modifiers.shift(), )); } @@ -950,51 +930,51 @@ where }) => { // TODO: This needs testing once I figure out how to trigger it with a screen reader. let node_id = cx.current.accesskit_id(); - cx.text_context.with_editor(cx.current, |_, editor| { - // let cursor_node = selection.focus.node; - let selection_node = selection.anchor.node; - - // let mut cursor_line_index = 0; - // let mut cursor_index = 0; - let mut selection_line_index = 0; - let mut selection_index = 0; - - let mut current_cursor = 0; - let mut prev_line_index = std::usize::MAX; - - for (index, line) in editor.buffer().layout_runs().enumerate() { - let line_node = AccessNode::new_from_parent(node_id, index); - // if line_node.node_id() == cursor_node { - // cursor_line_index = line.line_i; - // cursor_index = selection.focus.character_index + current_cursor; - // } - - if line_node.node_id() == selection_node { - selection_line_index = line.line_i; - selection_index = selection.anchor.character_index + current_cursor; - } + // cx.text_context.with_editor(cx.current, |_, editor| { + // // let cursor_node = selection.focus.node; + // let selection_node = selection.anchor.node; - if line.line_i != prev_line_index { - current_cursor = 0; - } + // // let mut cursor_line_index = 0; + // // let mut cursor_index = 0; + // let mut selection_line_index = 0; + // let mut selection_index = 0; - let first_glyph_pos = - line.glyphs.first().map(|glyph| glyph.start).unwrap_or_default(); - let last_glyph_pos = - line.glyphs.last().map(|glyph| glyph.end).unwrap_or_default(); + // let mut current_cursor = 0; + // let mut prev_line_index = std::usize::MAX; - let line_length = last_glyph_pos - first_glyph_pos; + // for (index, line) in editor.buffer().layout_runs().enumerate() { + // let line_node = AccessNode::new_from_parent(node_id, index); + // // if line_node.node_id() == cursor_node { + // // cursor_line_index = line.line_i; + // // cursor_index = selection.focus.character_index + current_cursor; + // // } - current_cursor += line_length; - prev_line_index = line.line_i; - } + // if line_node.node_id() == selection_node { + // selection_line_index = line.line_i; + // selection_index = selection.anchor.character_index + current_cursor; + // } - let selection_cursor = Cursor::new(selection_line_index, selection_index); - editor.set_selection(Selection::Normal(selection_cursor)); + // if line.line_i != prev_line_index { + // current_cursor = 0; + // } - // TODO: Either add a method to set the cursor by index to cosmic, - // or loop over an `Action` to move the cursor to the correct place. - }); + // let first_glyph_pos = + // line.glyphs.first().map(|glyph| glyph.start).unwrap_or_default(); + // let last_glyph_pos = + // line.glyphs.last().map(|glyph| glyph.end).unwrap_or_default(); + + // let line_length = last_glyph_pos - first_glyph_pos; + + // current_cursor += line_length; + // prev_line_index = line.line_i; + // } + + // let selection_cursor = Cursor::new(selection_line_index, selection_index); + // editor.set_selection(Selection::Normal(selection_cursor)); + + // // TODO: Either add a method to set the cursor by index to cosmic, + // // or loop over an `Action` to move the cursor to the correct place. + // }); // println!("Select some text: {:?}", selection); } @@ -1089,7 +1069,7 @@ where self.placeholder_shown = text.is_empty(); self.select_all(cx); - self.insert_text(cx, &text); + // self.insert_text(cx, &text); // self.set_caret(cx); if let Ok(value) = &text.parse::() { @@ -1116,7 +1096,7 @@ where self.placeholder_shown = text.is_empty(); self.select_all(cx); - self.insert_text(cx, &text); + // self.insert_text(cx, &text); // self.set_caret(cx); if let Ok(value) = &text.parse::() { @@ -1240,17 +1220,22 @@ where } // Use custom drawing for the textbox so a transform can be applied to just the text. - fn draw(&self, cx: &mut DrawContext, canvas: &mut Canvas) { - let mut path = cx.build_path(); + fn draw(&self, cx: &mut DrawContext, canvas: &Canvas) { + let mut path = cx.build_path(cx.bounds()); cx.draw_shadows(canvas, &mut path); - cx.draw_backdrop_filter(canvas, &mut path); + // cx.draw_backdrop_filter(canvas, &mut path); cx.draw_background(canvas, &mut path); cx.draw_border(canvas, &mut path); - cx.draw_inset_box_shadows(canvas, &mut path); - cx.draw_outline(canvas); - canvas.save(); + // cx.draw_inset_box_shadows(canvas, &mut path); + // cx.draw_outline(canvas); + // canvas.save(); // canvas.translate(self.transform.0, self.transform.1); - cx.draw_text_and_selection(canvas); - canvas.restore(); + // cx.draw_text_and_selection(canvas); + cx.draw_text(canvas); + if self.edit { + self.draw_selection(cx, canvas); + self.draw_text_caret(cx, canvas); + } + // canvas.restore(); } } diff --git a/crates/vizia_core/src/views/tooltip.rs b/crates/vizia_core/src/views/tooltip.rs index f46471861..179ddfee6 100644 --- a/crates/vizia_core/src/views/tooltip.rs +++ b/crates/vizia_core/src/views/tooltip.rs @@ -385,36 +385,36 @@ impl View for Arrow { fn element(&self) -> Option<&'static str> { Some("arrow") } - fn draw(&self, cx: &mut DrawContext, canvas: &mut Canvas) { + fn draw(&self, cx: &mut DrawContext, canvas: &Canvas) { let bounds = cx.bounds(); let mut path = vg::Path::new(); match Tooltip::placement.get(cx) { Placement::Bottom | Placement::BottomStart | Placement::BottomEnd => { - path.move_to(bounds.bottom_left().1, bounds.bottom_left().0); - path.line_to(bounds.center_top().0, bounds.center_top().1); - path.line_to(bounds.bottom_right().1, bounds.bottom_right().0); - path.line_to(bounds.bottom_left().1, bounds.bottom_left().0); + path.move_to(bounds.bottom_left()); + path.line_to(bounds.center_top()); + path.line_to(bounds.bottom_right()); + path.line_to(bounds.bottom_left()); } Placement::Top | Placement::TopStart | Placement::TopEnd => { - path.move_to(bounds.top_left().1, bounds.top_left().0); - path.line_to(bounds.center_bottom().0, bounds.center_bottom().1); - path.line_to(bounds.top_right().1, bounds.top_right().0); - path.line_to(bounds.top_left().1, bounds.top_left().0); + path.move_to(bounds.top_left()); + path.line_to(bounds.center_bottom()); + path.line_to(bounds.top_right()); + path.line_to(bounds.top_left()); } Placement::Left | Placement::LeftStart | Placement::LeftEnd => { - path.move_to(bounds.top_left().1, bounds.top_left().0); - path.line_to(bounds.center_right().0, bounds.center_right().1); - path.line_to(bounds.bottom_left().1, bounds.bottom_left().0); - path.line_to(bounds.top_left().1, bounds.top_left().0); + path.move_to(bounds.top_left()); + path.line_to(bounds.center_right()); + path.line_to(bounds.bottom_left()); + path.line_to(bounds.top_left()); } Placement::Right | Placement::RightStart | Placement::RightEnd => { - path.move_to(bounds.top_right().1, bounds.top_right().0); - path.line_to(bounds.center_left().0, bounds.center_left().1); - path.line_to(bounds.bottom_right().1, bounds.bottom_right().0); - path.line_to(bounds.top_right().1, bounds.top_right().0); + path.move_to(bounds.top_right()); + path.line_to(bounds.center_left()); + path.line_to(bounds.bottom_right()); + path.line_to(bounds.top_right()); } _ => {} @@ -423,6 +423,8 @@ impl View for Arrow { let bg = cx.background_color(); - canvas.fill_path(&path, &vg::Paint::color(bg.into())); + let mut paint = vg::Paint::default(); + paint.set_color(bg); + canvas.draw_path(&path, &paint); } } diff --git a/crates/vizia_core/src/views/virtual_list.rs b/crates/vizia_core/src/views/virtual_list.rs index 0f1119d0b..6f6d8c25b 100644 --- a/crates/vizia_core/src/views/virtual_list.rs +++ b/crates/vizia_core/src/views/virtual_list.rs @@ -154,7 +154,7 @@ impl VirtualList { impl View for VirtualList { fn element(&self) -> Option<&'static str> { - Some("virtual_list") + Some("virtual-list") } fn event(&mut self, cx: &mut EventContext, event: &mut Event) { diff --git a/crates/vizia_style/Cargo.toml b/crates/vizia_style/Cargo.toml index 8d61f3626..880f8c278 100644 --- a/crates/vizia_style/Cargo.toml +++ b/crates/vizia_style/Cargo.toml @@ -10,7 +10,8 @@ rust-version = "1.65" [dependencies] cssparser = "0.29.6" -femtovg = "0.8.2" +# femtovg = "0.8.2" +skia-safe = {version = "0.71", features = ["textlayout"] } selectors = { version = "0.23.0", path = "./selectors" } # morphorm = {path = "../../../morphorm" } morphorm = {git = "https://github.com/vizia/morphorm.git", branch = "auto-min-size2"} diff --git a/crates/vizia_style/src/property.rs b/crates/vizia_style/src/property.rs index ed76b5313..286fe6c61 100644 --- a/crates/vizia_style/src/property.rs +++ b/crates/vizia_style/src/property.rs @@ -1,10 +1,10 @@ use crate::{ define_property, Angle, BackgroundImage, BackgroundSize, Border, BorderCornerShape, BorderRadius, BorderWidth, BorderWidthValue, BoxShadow, ClipPath, Color, CursorIcon, - CustomParseError, CustomProperty, Display, Filter, FontFamily, FontSize, FontStretch, - FontStyle, FontWeight, LayoutType, LengthOrPercentage, Opacity, Outline, Overflow, Parse, - PointerEvents, Position, PositionType, Rect, Scale, TextAlign, Transform, Transition, - Translate, Units, UnparsedProperty, Visibility, + CustomParseError, CustomProperty, Display, Filter, FontFamily, FontSize, FontSlant, FontWeight, + FontWidth, LayoutType, LengthOrPercentage, LineClamp, Opacity, Outline, Overflow, Parse, + PointerEvents, Position, PositionType, Rect, Scale, TextAlign, TextOverflow, Transform, + Transition, Translate, Units, UnparsedProperty, Visibility, }; use cssparser::Parser; @@ -140,17 +140,19 @@ define_property! { "background-image": BackgroundImage(Vec>), "background-size": BackgroundSize(Vec), - // Font + // Text "font-size": FontSize(FontSize), "color": FontColor(Color), "font-family": FontFamily(Vec>), "font-weight": FontWeight(FontWeight), - "font-style": FontStyle(FontStyle), - "font-stretch": FontStretch(FontStretch), + "font-slant": FontSlant(FontSlant), + "font-width": FontWidth(FontWidth), "selection-color": SelectionColor(Color), // TODO: Remove this once we have the pseudoselector version. "caret-color": CaretColor(Color), "text-wrap": TextWrap(bool), "text-align": TextAlign(TextAlign), + "text-overflow": TextOverflow(TextOverflow), + "line-clamp": LineClamp(LineClamp), // Box Shadow "box-shadow": BoxShadow(Vec), diff --git a/crates/vizia_style/src/values/color.rs b/crates/vizia_style/src/values/color.rs index 8369d2eba..edd199356 100644 --- a/crates/vizia_style/src/values/color.rs +++ b/crates/vizia_style/src/values/color.rs @@ -796,15 +796,26 @@ fn hue(mut h: f32, m1: f32, m2: f32) -> f32 { m1 } -impl From for femtovg::Color { - fn from(src: Color) -> femtovg::Color { - femtovg::Color::rgba(src.r(), src.g(), src.b(), src.a()) +impl From for skia_safe::Color { + fn from(src: Color) -> skia_safe::Color { + skia_safe::Color::from_argb(src.a(), src.r(), src.g(), src.b()) } } -impl From for femtovg::Color { - fn from(src: RGBA) -> femtovg::Color { - femtovg::Color::rgba(src.r(), src.g(), src.b(), src.a()) +impl From for skia_safe::Color4f { + fn from(src: Color) -> Self { + skia_safe::Color4f { + r: src.r() as f32 / 255.0, + g: src.g() as f32 / 255.0, + b: src.b() as f32 / 255.0, + a: src.a() as f32 / 255.0, + } + } +} + +impl From for skia_safe::Color { + fn from(src: RGBA) -> skia_safe::Color { + skia_safe::Color::from_argb(src.a(), src.r(), src.g(), src.b()) } } diff --git a/crates/vizia_style/src/values/font_family.rs b/crates/vizia_style/src/values/font_family.rs index c457ca7ed..674ca3bcd 100644 --- a/crates/vizia_style/src/values/font_family.rs +++ b/crates/vizia_style/src/values/font_family.rs @@ -1,7 +1,7 @@ use crate::{macros::impl_parse, CustomParseError, Parse}; use cssparser::*; -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum GenericFontFamily { Serif, SansSerif, diff --git a/crates/vizia_style/src/values/font_size.rs b/crates/vizia_style/src/values/font_size.rs index 0ab65820f..da86f5aac 100644 --- a/crates/vizia_style/src/values/font_size.rs +++ b/crates/vizia_style/src/values/font_size.rs @@ -35,6 +35,18 @@ impl Default for FontSize { } } +impl From for FontSize { + fn from(number: u32) -> Self { + FontSize(number as f32) + } +} + +impl From for FontSize { + fn from(number: i32) -> Self { + FontSize(number as f32) + } +} + impl From for FontSize { fn from(number: f32) -> Self { FontSize(number) diff --git a/crates/vizia_style/src/values/font_slant.rs b/crates/vizia_style/src/values/font_slant.rs new file mode 100644 index 000000000..735955442 --- /dev/null +++ b/crates/vizia_style/src/values/font_slant.rs @@ -0,0 +1,28 @@ +use skia_safe::font_style::Slant; + +use crate::{macros::define_enum, Parse}; + +define_enum! { + /// A font style. + pub enum FontSlant { + "normal": Normal, + "italic": Italic, + "oblique": Oblique, + } +} + +impl Default for FontSlant { + fn default() -> Self { + FontSlant::Normal + } +} + +impl From for Slant { + fn from(value: FontSlant) -> Self { + match value { + FontSlant::Normal => Slant::Upright, + FontSlant::Italic => Slant::Italic, + FontSlant::Oblique => Slant::Oblique, + } + } +} diff --git a/crates/vizia_style/src/values/font_stretch.rs b/crates/vizia_style/src/values/font_stretch.rs deleted file mode 100644 index 93718cdd4..000000000 --- a/crates/vizia_style/src/values/font_stretch.rs +++ /dev/null @@ -1,66 +0,0 @@ -use crate::{CustomParseError, Ident, Parse, Percentage}; - -/// A font stretch value. -#[derive(Debug, Copy, Clone, PartialEq)] -pub enum FontStretch { - UltraCondensed, - ExtraCondensed, - Condensed, - SemiCondensed, - Normal, - SemiExpanded, - Expanded, - ExtraExpanded, - UltraExpanded, -} - -impl<'i> Parse<'i> for FontStretch { - fn parse<'t>( - input: &mut cssparser::Parser<'i, 't>, - ) -> Result>> { - let location = input.current_source_location(); - match input.try_parse(Ident::parse) { - Ok(ident) => match ident.0.as_ref() { - "ultra-condensed" => Ok(FontStretch::UltraCondensed), - "extra-condensed" => Ok(FontStretch::ExtraCondensed), - "condensed" => Ok(FontStretch::Condensed), - "semi-condensed" => Ok(FontStretch::SemiCondensed), - "normal" => Ok(FontStretch::Normal), - "semi-expanded" => Ok(FontStretch::SemiExpanded), - "expanded" => Ok(FontStretch::Expanded), - "extra-expanded" => Ok(FontStretch::ExtraExpanded), - "ultra-expanded" => Ok(FontStretch::UltraExpanded), - _ => Err(cssparser::ParseError { - kind: cssparser::ParseErrorKind::Custom(CustomParseError::InvalidValue), - location, - }), - }, - - Err(_) => input.try_parse(Percentage::parse).map(|val| val.into()), - } - } -} - -impl From for FontStretch { - fn from(p: Percentage) -> Self { - if p.0 >= 0.0 && p.0 <= 0.5625 { - FontStretch::UltraCondensed - } else if p.0 > 0.5625 && p.0 <= 0.6875 { - FontStretch::ExtraCondensed - } else if p.0 > 0.6875 && p.0 <= 0.7625 { - FontStretch::Condensed - } else if p.0 > 0.7625 && p.0 <= 0.8875 { - FontStretch::SemiCondensed - } else if p.0 > 0.8875 && p.0 <= 1.0125 { - FontStretch::Normal - } else if p.0 > 1.0125 && p.0 <= 1.1375 { - FontStretch::SemiExpanded - } else if p.0 > 1.1375 && p.0 <= 1.375 { - FontStretch::Expanded - } else if p.0 > 1.375 && p.0 <= 1.75 { - FontStretch::ExtraExpanded - } else { - FontStretch::UltraExpanded - } - } -} diff --git a/crates/vizia_style/src/values/font_style.rs b/crates/vizia_style/src/values/font_style.rs deleted file mode 100644 index b33323085..000000000 --- a/crates/vizia_style/src/values/font_style.rs +++ /dev/null @@ -1,10 +0,0 @@ -use crate::{macros::define_enum, Parse}; - -define_enum! { - /// A font style. - pub enum FontStyle { - "normal": Normal, - "italic": Italic, - "oblique": Oblique, - } -} diff --git a/crates/vizia_style/src/values/font_weight.rs b/crates/vizia_style/src/values/font_weight.rs index 0d323468e..5a230029c 100644 --- a/crates/vizia_style/src/values/font_weight.rs +++ b/crates/vizia_style/src/values/font_weight.rs @@ -1,4 +1,5 @@ use cssparser::*; +use skia_safe::font_style::Weight; use crate::{macros::impl_parse, FontWeightKeyword, Parse}; @@ -57,6 +58,12 @@ impl From for FontWeight { } } +impl From for FontWeight { + fn from(number: i32) -> Self { + FontWeight(number as u16) + } +} + impl From<&str> for FontWeight { fn from(s: &str) -> Self { let mut input = ParserInput::new(s); @@ -70,3 +77,9 @@ impl From for u16 { font_weight.0 } } + +impl From for Weight { + fn from(value: FontWeight) -> Self { + Weight::from(value.0 as i32) + } +} diff --git a/crates/vizia_style/src/values/font_width.rs b/crates/vizia_style/src/values/font_width.rs new file mode 100644 index 000000000..f1bc3d63d --- /dev/null +++ b/crates/vizia_style/src/values/font_width.rs @@ -0,0 +1,85 @@ +use skia_safe::font_style::Width; + +use crate::{CustomParseError, Ident, Parse, Percentage}; + +/// A font stretch value. +#[derive(Debug, Default, Copy, Clone, PartialEq)] +pub enum FontWidth { + UltraCondensed, + ExtraCondensed, + Condensed, + SemiCondensed, + #[default] + Normal, + SemiExpanded, + Expanded, + ExtraExpanded, + UltraExpanded, +} + +impl<'i> Parse<'i> for FontWidth { + fn parse<'t>( + input: &mut cssparser::Parser<'i, 't>, + ) -> Result>> { + let location = input.current_source_location(); + match input.try_parse(Ident::parse) { + Ok(ident) => match ident.0.as_ref() { + "ultra-condensed" => Ok(FontWidth::UltraCondensed), + "extra-condensed" => Ok(FontWidth::ExtraCondensed), + "condensed" => Ok(FontWidth::Condensed), + "semi-condensed" => Ok(FontWidth::SemiCondensed), + "normal" => Ok(FontWidth::Normal), + "semi-expanded" => Ok(FontWidth::SemiExpanded), + "expanded" => Ok(FontWidth::Expanded), + "extra-expanded" => Ok(FontWidth::ExtraExpanded), + "ultra-expanded" => Ok(FontWidth::UltraExpanded), + _ => Err(cssparser::ParseError { + kind: cssparser::ParseErrorKind::Custom(CustomParseError::InvalidValue), + location, + }), + }, + + Err(_) => input.try_parse(Percentage::parse).map(|val| val.into()), + } + } +} + +impl From for FontWidth { + fn from(p: Percentage) -> Self { + if p.0 >= 0.0 && p.0 <= 0.5625 { + FontWidth::UltraCondensed + } else if p.0 > 0.5625 && p.0 <= 0.6875 { + FontWidth::ExtraCondensed + } else if p.0 > 0.6875 && p.0 <= 0.7625 { + FontWidth::Condensed + } else if p.0 > 0.7625 && p.0 <= 0.8875 { + FontWidth::SemiCondensed + } else if p.0 > 0.8875 && p.0 <= 1.0125 { + FontWidth::Normal + } else if p.0 > 1.0125 && p.0 <= 1.1375 { + FontWidth::SemiExpanded + } else if p.0 > 1.1375 && p.0 <= 1.375 { + FontWidth::Expanded + } else if p.0 > 1.375 && p.0 <= 1.75 { + FontWidth::ExtraExpanded + } else { + FontWidth::UltraExpanded + } + } +} + +impl From for Width { + fn from(value: FontWidth) -> Self { + match value { + FontWidth::UltraCondensed => Width::ULTRA_CONDENSED, + FontWidth::ExtraCondensed => Width::EXTRA_CONDENSED, + FontWidth::Condensed => Width::CONDENSED, + FontWidth::SemiCondensed => Width::SEMI_CONDENSED, + FontWidth::Normal => Width::NORMAL, + FontWidth::SemiExpanded => Width::SEMI_EXPANDED, + FontWidth::Expanded => Width::EXPANDED, + FontWidth::ExtraExpanded => Width::EXTRA_EXPANDED, + FontWidth::UltraExpanded => Width::ULTRA_EXPANDED, + } + } +} diff --git a/crates/vizia_style/src/values/mod.rs b/crates/vizia_style/src/values/mod.rs index c529b394a..694e238ba 100644 --- a/crates/vizia_style/src/values/mod.rs +++ b/crates/vizia_style/src/values/mod.rs @@ -18,10 +18,10 @@ pub mod easing; pub mod font_family; pub mod font_size; pub mod font_size_keyword; -pub mod font_stretch; -pub mod font_style; +pub mod font_slant; pub mod font_weight; pub mod font_weight_keyword; +pub mod font_width; pub mod gradient; pub mod horizontal_position_keyword; pub mod image; @@ -72,10 +72,10 @@ pub use easing::*; pub use font_family::*; pub use font_size::*; pub use font_size_keyword::*; -pub use font_stretch::*; -pub use font_style::*; +pub use font_slant::*; pub use font_weight::*; pub use font_weight_keyword::*; +pub use font_width::*; pub use gradient::*; pub use horizontal_position_keyword::*; pub use image::*; diff --git a/crates/vizia_style/src/values/text_align.rs b/crates/vizia_style/src/values/text_align.rs index 82ba89950..1e0fccca6 100644 --- a/crates/vizia_style/src/values/text_align.rs +++ b/crates/vizia_style/src/values/text_align.rs @@ -19,3 +19,16 @@ define_enum! { "justify": Justify, } } + +impl From for skia_safe::textlayout::TextAlign { + fn from(value: TextAlign) -> Self { + match value { + TextAlign::Start => skia_safe::textlayout::TextAlign::Start, + TextAlign::End => skia_safe::textlayout::TextAlign::End, + TextAlign::Left => skia_safe::textlayout::TextAlign::Left, + TextAlign::Right => skia_safe::textlayout::TextAlign::Right, + TextAlign::Center => skia_safe::textlayout::TextAlign::Center, + TextAlign::Justify => skia_safe::textlayout::TextAlign::Justify, + } + } +} diff --git a/crates/vizia_style/src/values/text_overflow.rs b/crates/vizia_style/src/values/text_overflow.rs index 5f4b8488d..ed13c5064 100644 --- a/crates/vizia_style/src/values/text_overflow.rs +++ b/crates/vizia_style/src/values/text_overflow.rs @@ -1,4 +1,6 @@ -use crate::{define_enum, Parse}; +use cssparser::{Parser, ParserInput}; + +use crate::{define_enum, impl_parse, Parse}; define_enum! { /// Determines how overflowed content that is not displayed should be signaled to the user. @@ -9,3 +11,45 @@ define_enum! { "ellipsis": Ellipsis, } } + +impl Default for TextOverflow { + fn default() -> Self { + TextOverflow::Clip + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct LineClamp(pub u32); + +impl Default for LineClamp { + fn default() -> Self { + LineClamp(1) + } +} + +impl_parse! { + LineClamp, + try_parse { + u32, + } +} + +impl From<&str> for LineClamp { + fn from(s: &str) -> Self { + let mut input = ParserInput::new(s); + let mut parser = Parser::new(&mut input); + LineClamp::parse(&mut parser).unwrap_or_default() + } +} + +impl From for LineClamp { + fn from(number: u32) -> Self { + LineClamp(number) + } +} + +impl From for LineClamp { + fn from(number: i32) -> Self { + LineClamp(number as u32) + } +} diff --git a/crates/vizia_winit/Cargo.toml b/crates/vizia_winit/Cargo.toml index 7f74a0a21..e1d42c9e6 100644 --- a/crates/vizia_winit/Cargo.toml +++ b/crates/vizia_winit/Cargo.toml @@ -22,16 +22,18 @@ vizia_window = { path = "../vizia_window" } accesskit = "0.12" winit = { version = "0.29", default-features = false } -femtovg = "0.8.2" +# femtovg = "0.8.2" +skia-safe = {version = "0.71", features = ["gl"]} glutin = { version = "0.31", default-features = false, optional = true } copypasta = {version = "0.10", optional = true, default-features = false } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] accesskit_winit = { version = "0.18", optional = true } glutin = { version = "0.31", default-features = false } -femtovg = "0.8.2" +# femtovg = "0.8.2" glutin-winit = { version = "0.4", default-features = false, features = ["egl", "glx", "wgl"] } raw-window-handle = "0.5" +gl-rs = { package = "gl", version = "0.14.0" } [target.'cfg(target_arch = "wasm32")'.dependencies] wasm-bindgen = { version = "0.2" } diff --git a/crates/vizia_winit/src/application.rs b/crates/vizia_winit/src/application.rs index dd15cd7b0..9c856a39a 100644 --- a/crates/vizia_winit/src/application.rs +++ b/crates/vizia_winit/src/application.rs @@ -349,7 +349,7 @@ impl Application { .send_event(UserEvent::Event(Event::new(WindowEvent::Redraw))) .expect("Failed to send redraw event"); - cx.mutate_window(|_, window: &Window| { + cx.mutate_window(|_, window: &mut Window| { window.window().request_redraw(); }); } @@ -363,7 +363,7 @@ impl Application { } }); - cx.mutate_window(|cx, window: &Window| { + cx.mutate_window(|cx, window: &mut Window| { cx.style().should_redraw(|| { window.window().request_redraw(); }); @@ -381,7 +381,7 @@ impl Application { .expect("Failed to send event"); } - cx.mutate_window(|_, window: &Window| { + cx.mutate_window(|_, window: &mut Window| { if window.should_close { elwt.exit(); } @@ -399,7 +399,7 @@ impl Application { if main_events { // Redraw cx.draw(); - cx.mutate_window(|_, window: &Window| { + cx.mutate_window(|_, window: &mut Window| { // window.window().pre_present_notify(); window.swap_buffers(); }); @@ -409,7 +409,7 @@ impl Application { if is_initially_cloaked { is_initially_cloaked = false; cx.draw(); - cx.mutate_window(|_, window: &Window| { + cx.mutate_window(|_, window: &mut Window| { window.swap_buffers(); window.set_cloak(false); }); @@ -574,8 +574,10 @@ impl Application { } winit::event::WindowEvent::Resized(physical_size) => { - cx.mutate_window(|_, window: &Window| { - window.resize(physical_size); + cx.mutate_window(|cx, window: &mut Window| { + if let Some(surface) = cx.get_surface_mut(Entity::root()) { + window.resize(physical_size, surface); + } }); cx.set_window_size( @@ -600,7 +602,7 @@ impl Application { ))) .expect("Failed to send redraw event"); - cx.mutate_window(|_, window: &Window| { + cx.mutate_window(|_, window: &mut Window| { window.window().request_redraw(); }); } @@ -617,7 +619,7 @@ impl Application { } }); - cx.mutate_window(|_, window: &Window| { + cx.mutate_window(|_, window: &mut Window| { window.window().request_redraw(); }); } diff --git a/crates/vizia_winit/src/window.rs b/crates/vizia_winit/src/window.rs index 88bde57c7..2863f61d6 100644 --- a/crates/vizia_winit/src/window.rs +++ b/crates/vizia_winit/src/window.rs @@ -1,10 +1,14 @@ use crate::application::UserEvent; +use std::ffi::CString; #[cfg(not(target_arch = "wasm32"))] use std::num::NonZeroU32; use crate::convert::cursor_icon_to_cursor_icon; -use femtovg::{renderer::OpenGl, Canvas, Color}; +// use femtovg::{renderer::OpenGl, Canvas, Color}; +#[cfg(not(target_arch = "wasm32"))] +use gl_rs as gl; +use glutin::config::Config; #[cfg(not(target_arch = "wasm32"))] use glutin::surface::SwapInterval; #[cfg(not(target_arch = "wasm32"))] @@ -12,6 +16,8 @@ use glutin_winit::DisplayBuilder; #[cfg(not(target_arch = "wasm32"))] use raw_window_handle::HasRawWindowHandle; +use gl::types::*; + #[cfg(not(target_arch = "wasm32"))] use glutin::{ config::ConfigTemplateBuilder, @@ -21,6 +27,13 @@ use glutin::{ surface::{SurfaceAttributesBuilder, WindowSurface}, }; +use skia_safe::{ + gpu::{ + self, backend_render_targets, context_options, gl::FramebufferInfo, ContextOptions, + SurfaceOrigin, + }, + ColorType, Surface, +}; use vizia_core::backend::*; use vizia_core::prelude::*; use winit::event_loop::EventLoop; @@ -28,11 +41,15 @@ use winit::window::{CursorGrabMode, WindowBuilder, WindowLevel}; use winit::{dpi::*, window::WindowId}; pub struct Window { + gl_config: Config, + pub gl_surface: glutin::surface::Surface, + #[cfg(not(target_arch = "wasm32"))] pub id: WindowId, #[cfg(not(target_arch = "wasm32"))] - context: glutin::context::PossiblyCurrentContext, + pub gl_context: glutin::context::PossiblyCurrentContext, + + pub gr_context: skia_safe::gpu::DirectContext, #[cfg(not(target_arch = "wasm32"))] - surface: glutin::surface::Surface, window: winit::window::Window, pub should_close: bool, } @@ -134,7 +151,7 @@ impl Window { pub fn new( events_loop: &EventLoop, window_description: &WindowDescription, - ) -> (Self, Canvas) { + ) -> (Self, Surface) { let window_builder = WindowBuilder::new(); //Windows COM doesn't play nicely with winit's drag and drop right now @@ -196,55 +213,143 @@ impl Window { NonZeroU32::new(height.max(1)).unwrap(), ); - let surface = + let gl_surface = unsafe { gl_config.display().create_window_surface(&gl_config, &attrs).unwrap() }; - let gl_context = not_current_gl_context.take().unwrap().make_current(&surface).unwrap(); - - // Build the femtovg renderer - let renderer = unsafe { - OpenGl::new_from_function_cstr(|s| gl_display.get_proc_address(s) as *const _) - } - .expect("Cannot create renderer"); + let gl_context = not_current_gl_context.take().unwrap().make_current(&gl_surface).unwrap(); if window_description.vsync { - surface + gl_surface .set_swap_interval(&gl_context, SwapInterval::Wait(NonZeroU32::new(1).unwrap())) .expect("Failed to set vsync"); } - let mut canvas = Canvas::new(renderer).expect("Failed to create canvas"); + // Build the femtovg renderer + // let renderer = unsafe { + // OpenGl::new_from_function_cstr(|s| gl_display.get_proc_address(s) as *const _) + // } + // .expect("Cannot create renderer"); + + // let mut canvas = Canvas::new(renderer).expect("Failed to create canvas"); + + // let size = window.inner_size(); + // canvas.set_size(size.width, size.height, 1.0); + // canvas.clear_rect(0, 0, size.width, size.height, Color::rgb(255, 80, 80)); + + // Build skia renderer + gl::load_with(|s| { + gl_config.display().get_proc_address(CString::new(s).unwrap().as_c_str()) + }); + let interface = skia_safe::gpu::gl::Interface::new_load_with(|name| { + if name == "eglGetCurrentDisplay" { + return std::ptr::null(); + } + gl_config.display().get_proc_address(CString::new(name).unwrap().as_c_str()) + }) + .expect("Could not create interface"); - let size = window.inner_size(); - canvas.set_size(size.width, size.height, 1.0); - canvas.clear_rect(0, 0, size.width, size.height, Color::rgb(255, 80, 80)); + // https://github.com/rust-skia/rust-skia/issues/476 + let mut context_options = ContextOptions::new(); + context_options.skip_gl_error_checks = context_options::Enable::Yes; + + let mut gr_context = skia_safe::gpu::DirectContext::new_gl(interface, &context_options) + .expect("Could not create direct context"); + + let fb_info = { + let mut fboid: GLint = 0; + unsafe { gl::GetIntegerv(gl::FRAMEBUFFER_BINDING, &mut fboid) }; + + FramebufferInfo { + fboid: fboid.try_into().unwrap(), + format: skia_safe::gpu::gl::Format::RGBA8.into(), + ..Default::default() + } + }; + + let num_samples = gl_config.num_samples() as usize; + let stencil_size = gl_config.stencil_size() as usize; + + let surface = create_surface(&window, fb_info, &mut gr_context, num_samples, stencil_size); // Build our window - let win = - Window { id: window.id(), context: gl_context, surface, window, should_close: false }; + let win = Window { + gl_config, + id: window.id(), + gl_context, + gr_context, + gl_surface, + window, + should_close: false, + }; - (win, canvas) + (win, surface) } pub fn window(&self) -> &winit::window::Window { &self.window } - pub fn resize(&self, size: PhysicalSize) { + pub fn resize(&mut self, size: PhysicalSize, surface: &mut Surface) { + let fb_info = { + let mut fboid: GLint = 0; + unsafe { gl::GetIntegerv(gl::FRAMEBUFFER_BINDING, &mut fboid) }; + + FramebufferInfo { + fboid: fboid.try_into().unwrap(), + format: skia_safe::gpu::gl::Format::RGBA8.into(), + ..Default::default() + } + }; + + *surface = create_surface( + &self.window, + fb_info, + &mut self.gr_context, + self.gl_config.num_samples() as usize, + self.gl_config.stencil_size() as usize, + ); + if size.width != 0 && size.height != 0 { - self.surface.resize( - &self.context, + self.gl_surface.resize( + &self.gl_context, size.width.try_into().unwrap(), size.height.try_into().unwrap(), ); } } - pub fn swap_buffers(&self) { - self.surface.swap_buffers(&self.context).expect("Failed to swap buffers"); + pub fn swap_buffers(&mut self) { + self.gr_context.flush_and_submit(); + self.gl_surface.swap_buffers(&self.gl_context).expect("Failed to swap buffers"); } } +pub fn create_surface( + window: &winit::window::Window, + fb_info: FramebufferInfo, + gr_context: &mut skia_safe::gpu::DirectContext, + num_samples: usize, + stencil_size: usize, +) -> Surface { + let size = window.inner_size(); + let size = ( + size.width.try_into().expect("Could not convert width"), + size.height.try_into().expect("Could not convert height"), + ); + let backend_render_target = + backend_render_targets::make_gl(size, num_samples, stencil_size, fb_info); + + gpu::surfaces::wrap_backend_render_target( + gr_context, + &backend_render_target, + SurfaceOrigin::BottomLeft, + ColorType::RGBA8888, + None, + None, + ) + .expect("Could not create skia surface") +} + impl View for Window { fn event(&mut self, cx: &mut EventContext, event: &mut Event) { event.map(|window_event, _| match window_event { diff --git a/examples/style/clipping.rs b/examples/style/clipping.rs index a4c8f4273..12f31482b 100644 --- a/examples/style/clipping.rs +++ b/examples/style/clipping.rs @@ -3,6 +3,7 @@ use vizia::prelude::*; const STYLE: &str = r#" .container { size: 100px; + border-radius: 10px; background-color: rgb(200, 200, 200); } @@ -43,14 +44,12 @@ const STYLE: &str = r#" } .clipping { - size: 100%; - space: 0px; clip-path: inset(30px); overflow: hidden; } - .container:over .clipping { - clip-path: inset(10px); + .clipping:over { + clip-path: inset(0px); transition: clip-path 100ms; } "#; @@ -98,9 +97,10 @@ fn main() -> Result<(), ApplicationError> { .class("overflowy"); HStack::new(cx, |cx| { - Element::new(cx).class("clipping"); + Element::new(cx); }) - .class("container"); + .class("container") + .class("clipping"); }) .class("row"); }) diff --git a/examples/style/gradient.rs b/examples/style/gradient.rs index 7c6482c74..6f51f344f 100644 --- a/examples/style/gradient.rs +++ b/examples/style/gradient.rs @@ -7,7 +7,8 @@ const STYLE: &str = r#" } element { - size: 100px; + width: 200px; + height: 100px; background-color: rgb(200, 200, 200); } @@ -40,11 +41,13 @@ fn main() -> Result<(), ApplicationError> { // Element::new(cx).class("linear-gradient"); // Element::new(cx).class("grad2").width(Pixels(200.0)); - Element::new(cx).background_gradient( - LinearGradientBuilder::with_direction("to right") - .add_stop(Color::red()) - .add_stop(Color::blue()), - ); + Element::new(cx) + // .background_gradient( + // LinearGradientBuilder::with_direction("to top right") + // .add_stop(Color::red()) + // .add_stop(Color::blue()), + // ) + .class("grad2"); }) .title("Gradient") .run() diff --git a/examples/style/text.rs b/examples/style/text.rs index 288c63667..ebaa4caef 100644 --- a/examples/style/text.rs +++ b/examples/style/text.rs @@ -1,4 +1,7 @@ -use vizia::prelude::*; +use vizia::{ + icons::{ICON_ALIGN_CENTER, ICON_ALIGN_JUSTIFIED, ICON_ALIGN_LEFT, ICON_ALIGN_RIGHT}, + prelude::*, +}; const STYLE: &str = r#" .font_size { @@ -13,13 +16,19 @@ const STYLE: &str = r#" font-weight: bold; } - .font_style { - font-style: italic; + .font_slant { + font-slant: italic; } + .font_width { + font-width: condensed; + } - .font_stretch { - font-stretch: ultra-condensed; + .text_overflow { + text-overflow: ellipsis; + line-clamp: 1; + width: 1s; + child-space: 1s; } .caret_color:checked .textbox_content { @@ -32,29 +41,155 @@ const STYLE: &str = r#" #[derive(Lens)] pub struct AppData { text: String, + fonts: Vec, + weights: Vec<&'static str>, + sizes: Vec, + selected_font: String, + selected_weight: FontWeight, + selected_size: u32, + selected_align: TextAlign, } pub enum AppEvent { - SetText(String), + SetSelectedFont(String), + SetSelectedWeight(String), + SetSelectedSize(u32), + SetSelectedAlign(TextAlign), } -impl Model for AppData {} +impl Model for AppData { + fn event(&mut self, cx: &mut EventContext, event: &mut Event) { + event.map(|app_event, _| match app_event { + AppEvent::SetSelectedFont(font) => { + self.selected_font = font.clone(); + println!("{}", font); + } + + AppEvent::SetSelectedWeight(weight_keyword) => { + let weight: FontWeight = match weight_keyword.as_str() { + "Thin" => FontWeightKeyword::Thin, + "Extra Light" => FontWeightKeyword::ExtraLight, + "Light" => FontWeightKeyword::Light, + "Normal" => FontWeightKeyword::Normal, + "Medium" => FontWeightKeyword::Medium, + "Bold" => FontWeightKeyword::Bold, + "Semi Bold" => FontWeightKeyword::SemiBold, + "Extra Bold" => FontWeightKeyword::ExtraBold, + "Black" => FontWeightKeyword::Black, + _ => unreachable!(), + } + .into(); + + self.selected_weight = weight; + } + + AppEvent::SetSelectedSize(size) => { + self.selected_size = *size; + } + + AppEvent::SetSelectedAlign(align) => { + self.selected_align = *align; + } + }) + } +} fn main() -> Result<(), ApplicationError> { Application::new(|cx| { cx.add_stylesheet(STYLE).expect("Failed to add stylesheet"); - AppData { text: "This text is editable!".to_string() }.build(cx); - - Label::new(cx, "Font Size").class("font_size"); - Label::new(cx, "Font Color").class("font_color"); - Label::new(cx, "Font Weight").class("font_weight"); - Label::new(cx, "Font Style").class("font_style"); - Label::new(cx, "Font Stretch").class("font_stretch"); - Textbox::new(cx, AppData::text) - .on_edit(|cx, text| cx.emit(AppEvent::SetText(text))) - .width(Pixels(200.0)) - .class("caret_color"); + let mut fonts: Vec = cx.text_context.default_font_manager.family_names().collect(); + AppData { + text: "This text is editable!".to_string(), + fonts, + selected_font: String::from("Arial"), + selected_weight: FontWeight::from(FontWeightKeyword::Normal), + selected_size: 10, + selected_align: TextAlign::Left, + weights: vec![ + "Thin", + "Extra Light", + "Light", + "Normal", + "Medium", + "Bold", + "Semi Bold", + "Extra Bold", + "Black", + ], + sizes: vec![10, 11, 12, 13, 14, 15, 16, 20, 24, 32, 36, 40, 48, 64, 96, 128], + } + .build(cx); + + HStack::new(cx, |cx|{ + VStack::new(cx, |cx|{ + Label::new(cx, "This is some tester text which is sufficiently long as to wrap multiple lines if wrapping is enabled.") + .width(Pixels(200.0)) + .height(Auto) + .border_color(Color::gray()) + .border_width(Pixels(1.0)) + .font_family(AppData::selected_font.map(|font| vec![FamilyOwned::Named(font.clone())])) + .font_weight(AppData::selected_weight) + .font_size(AppData::selected_size) + .text_align(AppData::selected_align); + }).child_space(Stretch(1.0)); + + VStack::new(cx, |cx|{ + PickList::new(cx, AppData::fonts, true, move |index, label| { + label.font_family( + AppData::fonts.map(move |font| vec![FamilyOwned::Named(font[index].clone())]), + ) + }) + .width(Stretch(1.0)) + .on_item_select(|cx, item| cx.emit(AppEvent::SetSelectedFont(item.clone()))); + HStack::new(cx, |cx|{ + PickList::new(cx, AppData::weights, true, |i, l| l) + .width(Stretch(1.0)) + .on_item_select(|cx, item| cx.emit(AppEvent::SetSelectedWeight(item.to_owned()))); + + PickList::new(cx, AppData::sizes, true, |i, l| l) + .width(Stretch(1.0)) + .on_item_select(|cx, item| cx.emit(AppEvent::SetSelectedSize(item))); + + }).col_between(Pixels(4.0)).height(Auto); + + ButtonGroup::new(cx, |cx|{ + ToggleButton::new(cx, AppData::selected_align.map(|align| *align == TextAlign::Left), |cx|{ + Icon::new(cx, ICON_ALIGN_LEFT) + }) + .on_toggle(|cx| cx.emit(AppEvent::SetSelectedAlign(TextAlign::Left))); + ToggleButton::new(cx, AppData::selected_align.map(|align| *align == TextAlign::Center), |cx|{ + Icon::new(cx, ICON_ALIGN_CENTER) + }) + .on_toggle(|cx| cx.emit(AppEvent::SetSelectedAlign(TextAlign::Center))); + ToggleButton::new(cx, AppData::selected_align.map(|align| *align == TextAlign::Right), |cx|{ + Icon::new(cx, ICON_ALIGN_RIGHT) + }) + .on_toggle(|cx| cx.emit(AppEvent::SetSelectedAlign(TextAlign::Right))); + ToggleButton::new(cx, AppData::selected_align.map(|align| *align == TextAlign::Justify), |cx|{ + Icon::new(cx, ICON_ALIGN_JUSTIFIED) + }) + .on_toggle(|cx| cx.emit(AppEvent::SetSelectedAlign(TextAlign::Justify))); + + }); + }).width(Pixels(200.0)).box_shadow("-2px 0px 10px #22222255").child_space(Pixels(10.0)).row_between(Pixels(10.0)); + }); + + // VStack::new(cx, |cx| { + // Label::new(cx, "Font Size").class("font_size"); + // Label::new(cx, "Font Color").class("font_color"); + // Label::new(cx, "Font Weight").class("font_weight"); + // Label::new(cx, "Font Slant").class("font_slant"); + // Label::new(cx, "Font Width").class("font_width"); + // Label::new(cx, "Text Overflow").width(Stretch(1.0)).text_overflow(TextOverflow::Ellipsis).line_clamp(1).text_align(TextAlign::Center); + // }) + // .row_between(Pixels(10.0)) + // .child_space(Pixels(10.0)); + + // Textbox::new(cx, AppData::text) + // .on_edit(|cx, text| cx.emit(AppDataSetter::Text(text))) + // .width(Pixels(200.0)) + // .class("caret_color"); }) .run() } diff --git a/examples/text_layout.rs b/examples/text_layout.rs index dd97e4f06..6d9cdf255 100644 --- a/examples/text_layout.rs +++ b/examples/text_layout.rs @@ -70,7 +70,7 @@ fn main() -> Result<(), ApplicationError> { Element::new(cx).class("indicator"); }, |cx| { - alignment3(cx); + // alignment3(cx); }, ), @@ -80,7 +80,7 @@ fn main() -> Result<(), ApplicationError> { Element::new(cx).class("indicator"); }, |cx: &mut Context| { - alignment4(cx); + // alignment4(cx); }, ), @@ -434,265 +434,265 @@ fn alignment2(cx: &mut Context) { .child_space(Pixels(20.0)); } -fn alignment3(cx: &mut Context) { - HStack::new(cx, |cx| { - HStack::new(cx, |cx| { - Textbox::new(cx, AppData::text) - .text_wrap(false) - .size(Pixels(150.0)) - .child_space(Pixels(0.0)) - .child_right(Stretch(1.0)) - .background_color(Color::rgb(200, 100, 100)); - }) - .size(Auto) - .child_space(Pixels(10.0)) - .background_color(Color::rgb(100, 200, 100)); - - HStack::new(cx, |cx| { - Textbox::new(cx, AppData::text) - .text_wrap(false) - .size(Pixels(150.0)) - .child_top(Pixels(0.0)) - .child_left(Stretch(1.0)) - .child_right(Stretch(1.0)) - .background_color(Color::rgb(200, 100, 100)); - }) - .size(Auto) - .child_space(Pixels(10.0)) - .background_color(Color::rgb(100, 200, 100)); - - HStack::new(cx, |cx| { - Textbox::new(cx, AppData::text) - .size(Pixels(150.0)) - .child_top(Pixels(0.0)) - .child_left(Stretch(1.0)) - .child_right(Pixels(0.0)) - .background_color(Color::rgb(200, 100, 100)); - }) - .size(Auto) - .child_space(Pixels(10.0)) - .background_color(Color::rgb(100, 200, 100)); - }) - .size(Auto) - .col_between(Pixels(20.0)) - .child_space(Pixels(20.0)); - - HStack::new(cx, |cx| { - HStack::new(cx, |cx| { - Textbox::new(cx, AppData::text) - .text_wrap(false) - .size(Pixels(150.0)) - .child_left(Pixels(0.0)) - .child_top(Stretch(1.0)) - .child_bottom(Stretch(1.0)) - .child_right(Stretch(1.0)) - .background_color(Color::rgb(200, 100, 100)); - }) - .size(Auto) - .child_space(Pixels(10.0)) - .background_color(Color::rgb(100, 200, 100)); - - HStack::new(cx, |cx| { - Textbox::new(cx, AppData::text) - .text_wrap(false) - .size(Pixels(150.0)) - .child_space(Stretch(1.0)) - .background_color(Color::rgb(200, 100, 100)); - }) - .size(Auto) - .child_space(Pixels(10.0)) - .background_color(Color::rgb(100, 200, 100)); - - HStack::new(cx, |cx| { - Textbox::new(cx, AppData::text) - .size(Pixels(150.0)) - .child_top(Stretch(1.0)) - .child_bottom(Stretch(1.0)) - .child_left(Stretch(1.0)) - .child_right(Pixels(0.0)) - .background_color(Color::rgb(200, 100, 100)); - }) - .size(Auto) - .child_space(Pixels(10.0)) - .background_color(Color::rgb(100, 200, 100)); - }) - .size(Auto) - .col_between(Pixels(20.0)) - .child_space(Pixels(20.0)); - - HStack::new(cx, |cx| { - HStack::new(cx, |cx| { - Textbox::new(cx, AppData::text) - .text_wrap(false) - .size(Pixels(150.0)) - .child_space(Pixels(0.0)) - .child_top(Stretch(1.0)) - .child_right(Stretch(1.0)) - .background_color(Color::rgb(200, 100, 100)); - }) - .size(Auto) - .child_space(Pixels(10.0)) - .background_color(Color::rgb(100, 200, 100)); - - HStack::new(cx, |cx| { - Textbox::new(cx, AppData::text) - .text_wrap(false) - .size(Pixels(150.0)) - .child_space(Stretch(1.0)) - .child_bottom(Pixels(0.0)) - .background_color(Color::rgb(200, 100, 100)); - }) - .size(Auto) - .child_space(Pixels(10.0)) - .background_color(Color::rgb(100, 200, 100)); - - HStack::new(cx, |cx| { - Textbox::new(cx, AppData::text) - .size(Pixels(150.0)) - .child_top(Stretch(1.0)) - .child_bottom(Pixels(0.0)) - .child_left(Stretch(1.0)) - .child_right(Pixels(0.0)) - .background_color(Color::rgb(200, 100, 100)); - }) - .size(Auto) - .child_space(Pixels(10.0)) - .background_color(Color::rgb(100, 200, 100)); - }) - .size(Auto) - .col_between(Pixels(20.0)) - .child_space(Pixels(20.0)); -} - -fn alignment4(cx: &mut Context) { - HStack::new(cx, |cx| { - HStack::new(cx, |cx| { - Textbox::new(cx, AppData::text2) - .text_wrap(false) - .size(Pixels(150.0)) - .child_space(Pixels(0.0)) - .child_right(Stretch(1.0)) - .background_color(Color::rgb(200, 100, 100)); - }) - .size(Auto) - .child_space(Pixels(10.0)) - .background_color(Color::rgb(100, 200, 100)); - - HStack::new(cx, |cx| { - Textbox::new(cx, AppData::text2) - .text_wrap(false) - .size(Pixels(150.0)) - .child_top(Pixels(0.0)) - .child_left(Stretch(1.0)) - .child_right(Stretch(1.0)) - .background_color(Color::rgb(200, 100, 100)); - }) - .size(Auto) - .child_space(Pixels(10.0)) - .background_color(Color::rgb(100, 200, 100)); - - HStack::new(cx, |cx| { - Textbox::new(cx, AppData::text2) - .size(Pixels(150.0)) - .child_top(Pixels(0.0)) - .child_left(Stretch(1.0)) - .child_right(Pixels(0.0)) - .background_color(Color::rgb(200, 100, 100)); - }) - .size(Auto) - .child_space(Pixels(10.0)) - .background_color(Color::rgb(100, 200, 100)); - }) - .size(Auto) - .col_between(Pixels(20.0)) - .child_space(Pixels(20.0)); - - HStack::new(cx, |cx| { - HStack::new(cx, |cx| { - Textbox::new(cx, AppData::text2) - .text_wrap(false) - .size(Pixels(150.0)) - .child_left(Pixels(0.0)) - .child_top(Stretch(1.0)) - .child_bottom(Stretch(1.0)) - .child_right(Stretch(1.0)) - .background_color(Color::rgb(200, 100, 100)); - }) - .size(Auto) - .child_space(Pixels(10.0)) - .background_color(Color::rgb(100, 200, 100)); - - HStack::new(cx, |cx| { - Textbox::new(cx, AppData::text2) - .text_wrap(false) - .size(Pixels(150.0)) - .child_space(Stretch(1.0)) - .background_color(Color::rgb(200, 100, 100)); - }) - .size(Auto) - .child_space(Pixels(10.0)) - .background_color(Color::rgb(100, 200, 100)); - - HStack::new(cx, |cx| { - Textbox::new(cx, AppData::text2) - .size(Pixels(150.0)) - .child_top(Stretch(1.0)) - .child_bottom(Stretch(1.0)) - .child_left(Stretch(1.0)) - .child_right(Pixels(0.0)) - .background_color(Color::rgb(200, 100, 100)); - }) - .size(Auto) - .child_space(Pixels(10.0)) - .background_color(Color::rgb(100, 200, 100)); - }) - .size(Auto) - .col_between(Pixels(20.0)) - .child_space(Pixels(20.0)); - - HStack::new(cx, |cx| { - HStack::new(cx, |cx| { - Textbox::new(cx, AppData::text2) - .text_wrap(false) - .size(Pixels(150.0)) - .child_space(Pixels(0.0)) - .child_top(Stretch(1.0)) - .child_right(Stretch(1.0)) - .background_color(Color::rgb(200, 100, 100)); - }) - .size(Auto) - .child_space(Pixels(10.0)) - .background_color(Color::rgb(100, 200, 100)); - - HStack::new(cx, |cx| { - Textbox::new(cx, AppData::text2) - .text_wrap(false) - .size(Pixels(150.0)) - .child_space(Stretch(1.0)) - .child_bottom(Pixels(0.0)) - .background_color(Color::rgb(200, 100, 100)); - }) - .size(Auto) - .child_space(Pixels(10.0)) - .background_color(Color::rgb(100, 200, 100)); - - HStack::new(cx, |cx| { - Textbox::new(cx, AppData::text2) - .size(Pixels(150.0)) - .child_top(Stretch(1.0)) - .child_bottom(Pixels(0.0)) - .child_left(Stretch(1.0)) - .child_right(Pixels(0.0)) - .background_color(Color::rgb(200, 100, 100)); - }) - .size(Auto) - .child_space(Pixels(10.0)) - .background_color(Color::rgb(100, 200, 100)); - }) - .size(Auto) - .col_between(Pixels(20.0)) - .child_space(Pixels(20.0)); -} +// fn alignment3(cx: &mut Context) { +// HStack::new(cx, |cx| { +// HStack::new(cx, |cx| { +// Textbox::new(cx, AppData::text) +// .text_wrap(false) +// .size(Pixels(150.0)) +// .child_space(Pixels(0.0)) +// .child_right(Stretch(1.0)) +// .background_color(Color::rgb(200, 100, 100)); +// }) +// .size(Auto) +// .child_space(Pixels(10.0)) +// .background_color(Color::rgb(100, 200, 100)); + +// HStack::new(cx, |cx| { +// Textbox::new(cx, AppData::text) +// .text_wrap(false) +// .size(Pixels(150.0)) +// .child_top(Pixels(0.0)) +// .child_left(Stretch(1.0)) +// .child_right(Stretch(1.0)) +// .background_color(Color::rgb(200, 100, 100)); +// }) +// .size(Auto) +// .child_space(Pixels(10.0)) +// .background_color(Color::rgb(100, 200, 100)); + +// HStack::new(cx, |cx| { +// Textbox::new(cx, AppData::text) +// .size(Pixels(150.0)) +// .child_top(Pixels(0.0)) +// .child_left(Stretch(1.0)) +// .child_right(Pixels(0.0)) +// .background_color(Color::rgb(200, 100, 100)); +// }) +// .size(Auto) +// .child_space(Pixels(10.0)) +// .background_color(Color::rgb(100, 200, 100)); +// }) +// .size(Auto) +// .col_between(Pixels(20.0)) +// .child_space(Pixels(20.0)); + +// HStack::new(cx, |cx| { +// HStack::new(cx, |cx| { +// Textbox::new(cx, AppData::text) +// .text_wrap(false) +// .size(Pixels(150.0)) +// .child_left(Pixels(0.0)) +// .child_top(Stretch(1.0)) +// .child_bottom(Stretch(1.0)) +// .child_right(Stretch(1.0)) +// .background_color(Color::rgb(200, 100, 100)); +// }) +// .size(Auto) +// .child_space(Pixels(10.0)) +// .background_color(Color::rgb(100, 200, 100)); + +// HStack::new(cx, |cx| { +// Textbox::new(cx, AppData::text) +// .text_wrap(false) +// .size(Pixels(150.0)) +// .child_space(Stretch(1.0)) +// .background_color(Color::rgb(200, 100, 100)); +// }) +// .size(Auto) +// .child_space(Pixels(10.0)) +// .background_color(Color::rgb(100, 200, 100)); + +// HStack::new(cx, |cx| { +// Textbox::new(cx, AppData::text) +// .size(Pixels(150.0)) +// .child_top(Stretch(1.0)) +// .child_bottom(Stretch(1.0)) +// .child_left(Stretch(1.0)) +// .child_right(Pixels(0.0)) +// .background_color(Color::rgb(200, 100, 100)); +// }) +// .size(Auto) +// .child_space(Pixels(10.0)) +// .background_color(Color::rgb(100, 200, 100)); +// }) +// .size(Auto) +// .col_between(Pixels(20.0)) +// .child_space(Pixels(20.0)); + +// HStack::new(cx, |cx| { +// HStack::new(cx, |cx| { +// Textbox::new(cx, AppData::text) +// .text_wrap(false) +// .size(Pixels(150.0)) +// .child_space(Pixels(0.0)) +// .child_top(Stretch(1.0)) +// .child_right(Stretch(1.0)) +// .background_color(Color::rgb(200, 100, 100)); +// }) +// .size(Auto) +// .child_space(Pixels(10.0)) +// .background_color(Color::rgb(100, 200, 100)); + +// HStack::new(cx, |cx| { +// Textbox::new(cx, AppData::text) +// .text_wrap(false) +// .size(Pixels(150.0)) +// .child_space(Stretch(1.0)) +// .child_bottom(Pixels(0.0)) +// .background_color(Color::rgb(200, 100, 100)); +// }) +// .size(Auto) +// .child_space(Pixels(10.0)) +// .background_color(Color::rgb(100, 200, 100)); + +// HStack::new(cx, |cx| { +// Textbox::new(cx, AppData::text) +// .size(Pixels(150.0)) +// .child_top(Stretch(1.0)) +// .child_bottom(Pixels(0.0)) +// .child_left(Stretch(1.0)) +// .child_right(Pixels(0.0)) +// .background_color(Color::rgb(200, 100, 100)); +// }) +// .size(Auto) +// .child_space(Pixels(10.0)) +// .background_color(Color::rgb(100, 200, 100)); +// }) +// .size(Auto) +// .col_between(Pixels(20.0)) +// .child_space(Pixels(20.0)); +// } + +// fn alignment4(cx: &mut Context) { +// HStack::new(cx, |cx| { +// HStack::new(cx, |cx| { +// Textbox::new(cx, AppData::text2) +// .text_wrap(false) +// .size(Pixels(150.0)) +// .child_space(Pixels(0.0)) +// .child_right(Stretch(1.0)) +// .background_color(Color::rgb(200, 100, 100)); +// }) +// .size(Auto) +// .child_space(Pixels(10.0)) +// .background_color(Color::rgb(100, 200, 100)); + +// HStack::new(cx, |cx| { +// Textbox::new(cx, AppData::text2) +// .text_wrap(false) +// .size(Pixels(150.0)) +// .child_top(Pixels(0.0)) +// .child_left(Stretch(1.0)) +// .child_right(Stretch(1.0)) +// .background_color(Color::rgb(200, 100, 100)); +// }) +// .size(Auto) +// .child_space(Pixels(10.0)) +// .background_color(Color::rgb(100, 200, 100)); + +// HStack::new(cx, |cx| { +// Textbox::new(cx, AppData::text2) +// .size(Pixels(150.0)) +// .child_top(Pixels(0.0)) +// .child_left(Stretch(1.0)) +// .child_right(Pixels(0.0)) +// .background_color(Color::rgb(200, 100, 100)); +// }) +// .size(Auto) +// .child_space(Pixels(10.0)) +// .background_color(Color::rgb(100, 200, 100)); +// }) +// .size(Auto) +// .col_between(Pixels(20.0)) +// .child_space(Pixels(20.0)); + +// HStack::new(cx, |cx| { +// HStack::new(cx, |cx| { +// Textbox::new(cx, AppData::text2) +// .text_wrap(false) +// .size(Pixels(150.0)) +// .child_left(Pixels(0.0)) +// .child_top(Stretch(1.0)) +// .child_bottom(Stretch(1.0)) +// .child_right(Stretch(1.0)) +// .background_color(Color::rgb(200, 100, 100)); +// }) +// .size(Auto) +// .child_space(Pixels(10.0)) +// .background_color(Color::rgb(100, 200, 100)); + +// HStack::new(cx, |cx| { +// Textbox::new(cx, AppData::text2) +// .text_wrap(false) +// .size(Pixels(150.0)) +// .child_space(Stretch(1.0)) +// .background_color(Color::rgb(200, 100, 100)); +// }) +// .size(Auto) +// .child_space(Pixels(10.0)) +// .background_color(Color::rgb(100, 200, 100)); + +// HStack::new(cx, |cx| { +// Textbox::new(cx, AppData::text2) +// .size(Pixels(150.0)) +// .child_top(Stretch(1.0)) +// .child_bottom(Stretch(1.0)) +// .child_left(Stretch(1.0)) +// .child_right(Pixels(0.0)) +// .background_color(Color::rgb(200, 100, 100)); +// }) +// .size(Auto) +// .child_space(Pixels(10.0)) +// .background_color(Color::rgb(100, 200, 100)); +// }) +// .size(Auto) +// .col_between(Pixels(20.0)) +// .child_space(Pixels(20.0)); + +// HStack::new(cx, |cx| { +// HStack::new(cx, |cx| { +// Textbox::new(cx, AppData::text2) +// .text_wrap(false) +// .size(Pixels(150.0)) +// .child_space(Pixels(0.0)) +// .child_top(Stretch(1.0)) +// .child_right(Stretch(1.0)) +// .background_color(Color::rgb(200, 100, 100)); +// }) +// .size(Auto) +// .child_space(Pixels(10.0)) +// .background_color(Color::rgb(100, 200, 100)); + +// HStack::new(cx, |cx| { +// Textbox::new(cx, AppData::text2) +// .text_wrap(false) +// .size(Pixels(150.0)) +// .child_space(Stretch(1.0)) +// .child_bottom(Pixels(0.0)) +// .background_color(Color::rgb(200, 100, 100)); +// }) +// .size(Auto) +// .child_space(Pixels(10.0)) +// .background_color(Color::rgb(100, 200, 100)); + +// HStack::new(cx, |cx| { +// Textbox::new(cx, AppData::text2) +// .size(Pixels(150.0)) +// .child_top(Stretch(1.0)) +// .child_bottom(Pixels(0.0)) +// .child_left(Stretch(1.0)) +// .child_right(Pixels(0.0)) +// .background_color(Color::rgb(200, 100, 100)); +// }) +// .size(Auto) +// .child_space(Pixels(10.0)) +// .background_color(Color::rgb(100, 200, 100)); +// }) +// .size(Auto) +// .col_between(Pixels(20.0)) +// .child_space(Pixels(20.0)); +// } fn alignment5(cx: &mut Context) { HStack::new(cx, |cx| { diff --git a/examples/views/chip.rs b/examples/views/chip.rs index 4fde22747..fa44672a2 100644 --- a/examples/views/chip.rs +++ b/examples/views/chip.rs @@ -1,13 +1,11 @@ mod helpers; use helpers::*; use vizia::prelude::*; - #[derive(Clone, Lens)] struct AppData { chip: String, chips: Vec, } - impl Model for AppData { fn event(&mut self, _cx: &mut EventContext, event: &mut Event) { event.map(|app_event, _| match app_event { @@ -17,11 +15,9 @@ impl Model for AppData { }) } } - enum AppEvent { CloseChip(usize), } - fn main() -> Result<(), ApplicationError> { Application::new(|cx| { AppData { diff --git a/examples/views/helpers/mod.rs b/examples/views/helpers/mod.rs index 1e7532d9c..4f60b0006 100644 --- a/examples/views/helpers/mod.rs +++ b/examples/views/helpers/mod.rs @@ -66,12 +66,12 @@ impl ExamplePage { HStack::new(cx, |cx| { HStack::new(cx, |cx| { Switch::new(cx, ControlsData::disabled) - .on_toggle(|cx| cx.emit(ControlsEvent::ToggleDisabled)) - .tooltip(|cx| { - Tooltip::new(cx, |cx| { - Label::new(cx, "Toggle disabled"); - }) - }); + .on_toggle(|cx| cx.emit(ControlsEvent::ToggleDisabled)); + // .tooltip(|cx| { + // Tooltip::new(cx, |cx| { + // Label::new(cx, "Toggle disabled"); + // }) + // }); Label::new(cx, "Toggle Disabled"); }) .child_top(Stretch(1.0)) @@ -114,12 +114,12 @@ impl ExamplePage { HStack::new(cx, |cx| { HStack::new(cx, |cx| { Switch::new(cx, ControlsData::disabled) - .on_toggle(|cx| cx.emit(ControlsEvent::ToggleDisabled)) - .tooltip(|cx| { - Tooltip::new(cx, |cx| { - Label::new(cx, "Toggle disabled"); - }) - }); + .on_toggle(|cx| cx.emit(ControlsEvent::ToggleDisabled)); + // .tooltip(|cx| { + // Tooltip::new(cx, |cx| { + // Label::new(cx, "Toggle disabled"); + // }) + // }); Label::new(cx, "Toggle Disabled"); }) .child_top(Stretch(1.0)) @@ -149,11 +149,6 @@ impl ExamplePage { .disabled(ControlsData::disabled) .class("container") .entity(); - - // VStack::new(cx, |cx| { - // KeyBindView::new(cx, e); - // }) - // .background_color(Color::rgb(240, 240, 240)); }); }) } @@ -164,12 +159,12 @@ impl View for ExamplePage {} fn theme_selection_dropdown(cx: &mut Context) { PickList::new(cx, ControlsData::theme_options, ControlsData::selected_theme, true) .on_select(|cx, index| cx.emit(ControlsEvent::SetThemeMode(index))) - .width(Pixels(85.0)) - .tooltip(|cx| { - Tooltip::new(cx, |cx| { - Label::new(cx, "Select Theme Mode"); - }) - }); + .width(Pixels(85.0)); + // .tooltip(|cx| { + // Tooltip::new(cx, |cx| { + // Label::new(cx, "Select Theme Mode"); + // }) + // }); } pub fn setup_logging() -> Result<(), Box> { diff --git a/examples/views/label.rs b/examples/views/label.rs index e3e43d23f..17267a181 100644 --- a/examples/views/label.rs +++ b/examples/views/label.rs @@ -1,19 +1,16 @@ mod helpers; use helpers::*; use vizia::prelude::*; - #[derive(Lens)] pub struct AppData { text: String, value: f32, checked: bool, } - #[derive(Debug)] pub enum AppEvent { Toggle, } - impl Model for AppData { fn event(&mut self, _cx: &mut EventContext, event: &mut Event) { event.map(|app_event, _| match app_event { @@ -23,7 +20,6 @@ impl Model for AppData { }); } } - fn main() -> Result<(), ApplicationError> { Application::new(|cx| { AppData { @@ -47,7 +43,7 @@ fn main() -> Result<(), ApplicationError> { Label::new(cx, "Unless text wrapping is disabled.") .width(Pixels(200.0)) .text_wrap(false) - .font_style(FontStyle::Italic); + .font_slant(FontSlant::Italic); HStack::new(cx, |cx| { Checkbox::new(cx, AppData::checked) diff --git a/examples/widget_gallery/src/views/knob.rs b/examples/widget_gallery/src/views/knob.rs index f1cd60c54..41748a758 100644 --- a/examples/widget_gallery/src/views/knob.rs +++ b/examples/widget_gallery/src/views/knob.rs @@ -32,8 +32,8 @@ pub fn knob(cx: &mut Context) { DemoRegion::new( cx, |cx| { - Knob::new(cx, 0.5, KnobState::value, false) - .on_changing(|cx, val| cx.emit(KnobEvent::SetValue(val))); + // Knob::new(cx, 0.5, KnobState::value, false) + // .on_changing(|cx, val| cx.emit(KnobEvent::SetValue(val))); }, r#"Knob::new(cx, 0.5, KnobState::value, false) .on_changing(|cx, val| cx.emit(KnobEvent::SetValue(val)));"#,