diff --git a/CHANGELOG.md b/CHANGELOG.md index 18b4318d..2021327d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ - **BREAKING**: `WindowManager::create` now returns an error enum instead of a string. - Added `Window::on_bin_focus_change` method. +- Added `Window::attach_intvl_hook` method. - Reduce usage of winit's event loop for event processing. - winit event loop tends to get backed up fairly easily causing many basalt systems to behave poorly. - Many `Bin` related events are now sent directly to the renderer improving latency. @@ -63,10 +64,11 @@ - **BREAKING** Remove method `toggle_hidden` and `set_hidden`. - Use `style_modify` instead. - **BEHAVIOR**: Rewrote radius code to be more circular. -- Added method `style_modify`. +- Added method `style_modify` & `style_modify_then`. - Added method `is_visible`. - Added method `style_update_batch`. - Added method `attach_intvl_hook`. +- Added method `text_body` which retrives a `TextBodyGuard` that can be used to modify the text body. - Fixed `children_recursive` returning self. - Fixed `children_recursive_with_self` returning self twice. - Fixed text alignment being incorrect with scale. @@ -76,6 +78,7 @@ - These callbacks are now called at the end of the update cycle instead of right away to ensure all other `Bin`'s post update state is up to date. - Replaced usage of `ArcSwap` with `RwLock` to improve consistency. - Improved `mouse_inside` performance to better handle high polling rate mice. +- `on_update_once` now takes `FnOnce` instead of `FnMut`. ## Changes to `BinPostUpdate` - **BREAKING**: Added `inner_bounds` & `outer_bound` fields to `BinPostUpdate`. @@ -188,6 +191,7 @@ - Added method `clear_bin_focus`. - Added method `on_bin_focus_change` to `InputBuilder`. - Changed how input is processed to better handle high polling rate devices. +- Fixed `on_hold` hooks not being removed after the associated `Bin` or `Window` drops. # Version 0.21.0 (May 12, 2024) diff --git a/Cargo.toml b/Cargo.toml index dbc92bd2..bccf43f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ foldhash = "0.1" vulkano = "0.35" vulkano-shaders = "0.35" vulkano-taskgraph = "0.35" +unicode-segmentation = "1" [dependencies.cosmic-text] version = "0.14" @@ -52,3 +53,4 @@ default = ["image_decode", "image_download"] style_validation_debug_on_drop = [] image_decode = ["dep:image"] image_download = ["image_decode", "dep:curl", "dep:url"] +deadlock_detection = ["parking_lot/deadlock_detection"] diff --git a/src/input/builder.rs b/src/input/builder.rs index 375a7ca7..41e3ab7c 100644 --- a/src/input/builder.rs +++ b/src/input/builder.rs @@ -383,6 +383,16 @@ impl<'a> InputHoldBuilder<'a> { } }); + match &self.parent.target { + InputHookTarget::Bin(bin) => { + bin.attach_intvl_hook(intvl_id); + }, + InputHookTarget::Window(window) => { + window.attach_intvl_hook(intvl_id); + }, + InputHookTarget::None => (), + } + self.parent.input.add_hook_with_id( input_hook_id, Hook { diff --git a/src/input/mod.rs b/src/input/mod.rs index fc3e0d5b..ddcc76c3 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -275,7 +275,7 @@ impl Input { /// .finish() /// .unwrap(); /// ``` - pub fn hook(&self) -> InputHookBuilder { + pub fn hook(&self) -> InputHookBuilder<'_> { InputHookBuilder::start(self) } diff --git a/src/interface/bin/mod.rs b/src/interface/bin/mod.rs index e56c7dbc..0c79daa4 100644 --- a/src/interface/bin/mod.rs +++ b/src/interface/bin/mod.rs @@ -1,5 +1,7 @@ pub mod style; -mod text_state; +pub mod text_body; +pub mod text_guard; +pub mod text_state; use std::any::Any; use std::collections::{BTreeMap, BTreeSet}; @@ -24,7 +26,7 @@ use crate::input::{ }; use crate::interface::{ BinStyle, BinStyleValidation, Color, DefaultFont, FloatWeight, Flow, ItfVertInfo, Opacity, - Position, UnitValue, Visibility, ZIndex, scale_verts, + Position, TextBodyGuard, UnitValue, Visibility, ZIndex, scale_verts, }; use crate::interval::{IntvlHookCtrl, IntvlHookID}; use crate::render::RendererMetricsLevel; @@ -73,6 +75,8 @@ pub struct BinPostUpdate { pub content_bounds: Option<[f32; 4]>, /// Optimal bounds of the content. Same as `optimal_inner_bounds` but with padding included. pub optimal_content_bounds: [f32; 4], + /// The offset of the content as result of scroll. + pub content_offset: [f32; 2], /// Target Extent (Generally Window Size) pub extent: [u32; 2], /// UI Scale Used @@ -106,6 +110,7 @@ enum InternalHookTy { enum InternalHookFn { Updated(Box, &BinPostUpdate) + Send + 'static>), + UpdatedOnce(Box, &BinPostUpdate) + Send + 'static>), ChildrenAdded(Box, &Vec>) + Send + 'static>), ChildrenRemoved(Box, &Vec>) + Send + 'static>), } @@ -326,9 +331,17 @@ impl Drop for Bin { self.basalt.interval_ref().remove(hook_id); } + let effects_siblings = self.style_inspect(|style| style.position == Position::Floating); + if let Some(parent) = self.parent() { - let mut parent_hrchy = parent.hrchy.write(); - parent_hrchy.children.remove(&self.id); + { + let mut parent_hrchy = parent.hrchy.write(); + parent_hrchy.children.remove(&self.id); + } + + if effects_siblings { + parent.trigger_children_update(); + } } if let Some(window) = self.window() { @@ -410,7 +423,7 @@ impl Bin { /// Return the parent of this `Bin`. pub fn parent(&self) -> Option> { self.hrchy - .read() + .read_recursive() .parent .as_ref() .and_then(|parent_wk| parent_wk.upgrade()) @@ -434,7 +447,7 @@ impl Bin { /// Return the children of this `Bin` pub fn children(&self) -> Vec> { self.hrchy - .read() + .read_recursive() .children .iter() .filter_map(|(_, child_wk)| child_wk.upgrade()) @@ -454,7 +467,7 @@ impl Bin { children.extend( child .hrchy - .read() + .read_recursive() .children .iter() .filter_map(|(_, child_wk)| child_wk.upgrade()), @@ -479,7 +492,7 @@ impl Bin { children.extend( child .hrchy - .read() + .read_recursive() .children .iter() .filter_map(|(_, child_wk)| child_wk.upgrade()), @@ -551,12 +564,12 @@ impl Bin { /// /// This is useful where it is only needed to inspect the style of the `Bin`. pub fn style(&self) -> Arc { - self.style.read().clone() + self.style.read_recursive().clone() } /// Obtain a copy of `BinStyle` of this `Bin`. pub fn style_copy(&self) -> BinStyle { - (**self.style.read()).clone() + (**self.style.read_recursive()).clone() } /// Inspect `BinStyle` by reference given a method. @@ -564,7 +577,7 @@ impl Bin { /// When inspecting a style where it is only needed for a short period of time, this method /// will avoid cloning an `Arc` in comparision to the `style` method. pub fn style_inspect T, T>(&self, mut method: F) -> T { - method(&self.style.read()) + method(&self.style.read_recursive()) } #[track_caller] @@ -598,6 +611,40 @@ impl Bin { output } + pub fn style_modify_then(self: &Arc, modify: M, then: T) + where + M: FnOnce(&mut BinStyle) -> A, + T: FnOnce(&Arc, &BinPostUpdate, A) + Send + 'static, + A: Send + 'static, + { + let mut style = self.style.write(); + let mut modified_style = (**style).clone(); + + let output = modify(&mut modified_style); + modified_style.validate(self).expect_valid(); + + let effects_siblings = + style.position == Position::Floating || modified_style.position == Position::Floating; + + self.initial.store(false, atomic::Ordering::SeqCst); + *style = Arc::new(modified_style); + drop(style); + + // NOTE: The style lock must not be held otherwise a deadlock may occur. + self.on_update_once(move |bin, bpu| then(bin, bpu, output)); + + if effects_siblings { + match self.parent() { + Some(parent) => parent.trigger_children_update(), + None => { + self.trigger_recursive_update(); + }, + } + } else { + self.trigger_recursive_update(); + } + } + /// Update the style of this `Bin`. /// /// ***Note:** If the style has a validation error, the style will not be updated.* @@ -685,6 +732,12 @@ impl Bin { } } + /// Obtain [`TextBodyGuard`](TextBodyGuard) which is used to inspect/modify the + /// [`TextBody`](crate::interface::TextBody). + pub fn text_body<'a>(self: &'a Arc) -> TextBodyGuard<'a> { + TextBodyGuard::new(self) + } + /// Check if this [`Bin`] is visible. /// /// **Note**: This does not check if the `Bin` is offscreen. @@ -776,12 +829,12 @@ impl Bin { /// Obtain the `BinPostUpdate` information this `Bin`. pub fn post_update(&self) -> BinPostUpdate { - self.post_update.read().clone() + self.post_update.read_recursive().clone() } /// Calculate the amount of vertical overflow. pub fn calc_vert_overflow(self: &Arc) -> f32 { - let self_bpu = self.post_update.read(); + let self_bpu = self.post_update.read_recursive(); let extent = [ self_bpu.tri[0] - self_bpu.tli[0], @@ -799,7 +852,7 @@ impl Bin { let mut overflow_b: f32 = 0.0; for child in self.children() { - let child_bpu = child.post_update.read(); + let child_bpu = child.post_update.read_recursive(); if child_bpu.floating { overflow_t = overflow_t.max( @@ -818,8 +871,14 @@ impl Bin { // TODO: This only includes the content of this bin. Should it include others? if let Some(content_bounds) = self_bpu.content_bounds { - overflow_t = overflow_t.max(self_bpu.optimal_content_bounds[2] - content_bounds[2]); - overflow_b = overflow_b.max(content_bounds[3] - self_bpu.optimal_content_bounds[3]); + overflow_t = overflow_t.max( + self_bpu.optimal_content_bounds[2] + - (content_bounds[2] - self_bpu.content_offset[1]), + ); + overflow_b = overflow_b.max( + (content_bounds[3] - self_bpu.content_offset[1]) + - self_bpu.optimal_content_bounds[3], + ); } overflow_t + overflow_b @@ -827,7 +886,7 @@ impl Bin { /// Calculate the amount of horizontal overflow. pub fn calc_hori_overflow(self: &Arc) -> f32 { - let self_bpu = self.post_update.read(); + let self_bpu = self.post_update.read_recursive(); let extent = [ self_bpu.tri[0] - self_bpu.tli[0], @@ -845,7 +904,7 @@ impl Bin { let mut overflow_r: f32 = 0.0; for child in self.children() { - let child_bpu = child.post_update.read(); + let child_bpu = child.post_update.read_recursive(); if child_bpu.floating { overflow_l = overflow_l.max( @@ -864,8 +923,14 @@ impl Bin { // TODO: This only includes the content of this bin. Should it include others? if let Some(content_bounds) = self_bpu.content_bounds { - overflow_l = overflow_l.max(self_bpu.optimal_content_bounds[0] - content_bounds[0]); - overflow_r = overflow_r.max(content_bounds[1] - self_bpu.optimal_content_bounds[1]); + overflow_l = overflow_l.max( + self_bpu.optimal_content_bounds[0] + - (content_bounds[0] - self_bpu.content_offset[0]), + ); + overflow_r = overflow_r.max( + (content_bounds[1] - self_bpu.content_offset[0]) + - self_bpu.optimal_content_bounds[1], + ); } overflow_l + overflow_r @@ -875,7 +940,7 @@ impl Bin { /// /// ***Note:** This does not check the window.* pub fn mouse_inside(&self, mouse_x: f32, mouse_y: f32) -> bool { - let post = self.post_update.read(); + let post = self.post_update.read_recursive(); if !post.visible { return false; @@ -903,21 +968,6 @@ impl Bin { } } - pub fn add_enter_text_events(self: &Arc) { - self.on_character(move |target, _, c| { - let this = target.into_bin().unwrap(); - let mut style = this.style_copy(); - - if style.text_body.spans.is_empty() { - style.text_body.spans.push(Default::default()); - } - - c.modify_string(&mut style.text_body.spans.last_mut().unwrap().text); - this.style_update(style).expect_valid(); - Default::default() - }); - } - pub fn add_drag_events(self: &Arc, target_op: Option>) { let window = match self.window() { Some(some) => some, @@ -1280,7 +1330,7 @@ impl Bin { } #[inline] - pub fn on_update_once, &BinPostUpdate) + Send + 'static>( + pub fn on_update_once, &BinPostUpdate) + Send + 'static>( self: &Arc, func: F, ) { @@ -1288,7 +1338,7 @@ impl Bin { .lock() .get_mut(&InternalHookTy::UpdatedOnce) .unwrap() - .push(InternalHookFn::Updated(Box::new(func))); + .push(InternalHookFn::UpdatedOnce(Box::new(func))); } fn call_children_added_hooks(self: &Arc, children: Vec>) { @@ -1635,7 +1685,7 @@ impl Bin { }; let top = - parent_plmt.tlwh[0] + y + padding_tblr[0] + margin_t - scroll_xy[1] - offset_y; + parent_plmt.tlwh[0] + y + padding_tblr[0] + margin_t + scroll_xy[1] - offset_y; let left = parent_plmt.tlwh[1] + x + padding_tblr[2] + margin_l + scroll_xy[0] - offset_x; @@ -1782,16 +1832,16 @@ impl Bin { }; let [left, width] = match (left_op, right_op, width_op) { - (Some(left), _, Some(width)) => [parent_plmt.tlwh[1] + left + scroll_xy[0], width], + (Some(left), _, Some(width)) => [parent_plmt.tlwh[1] + left - scroll_xy[0], width], (_, Some(right), Some(width)) => { [ - parent_plmt.tlwh[1] + parent_plmt.tlwh[2] - right - width + scroll_xy[0], + parent_plmt.tlwh[1] + parent_plmt.tlwh[2] - right - width - scroll_xy[0], width, ] }, (Some(left), Some(right), _) => { let left = parent_plmt.tlwh[1] + left + scroll_xy[0]; - let right = parent_plmt.tlwh[1] + parent_plmt.tlwh[2] - right + scroll_xy[0]; + let right = parent_plmt.tlwh[1] + parent_plmt.tlwh[2] - right - scroll_xy[0]; [left, right - left] }, _ => panic!("invalid style"), @@ -1917,7 +1967,7 @@ impl Bin { .unwrap() .drain(..) { - if let InternalHookFn::Updated(mut func) = hook_enum { + if let InternalHookFn::UpdatedOnce(func) = hook_enum { func(self, &bpu); } } @@ -1945,8 +1995,9 @@ impl Bin { // -- Obtain BinPostUpdate & Style --------------------------------------------------- // - let mut bpu = self.post_update.write(); let mut update_state = self.update_state.lock(); + let mut bpu = self.post_update.write(); + let style = self.style(); if let Some(metrics_state) = metrics_op.as_mut() { @@ -2013,6 +2064,7 @@ impl Bin { top + padding_t, top + height - padding_b, ], + content_offset: [-style.scroll_x, -style.scroll_y], extent: [ context.extent[0].trunc() as u32, context.extent[1].trunc() as u32, @@ -2108,8 +2160,14 @@ impl Bin { for (_, vertexes) in style.user_vertexes.iter() { for vertex in vertexes { - let x = left + vertex.x.px_width([width, height]).unwrap_or(0.0); - let y = top + vertex.y.px_height([width, height]).unwrap_or(0.0); + let x = left + + bpu.content_offset[0] + + vertex.x.px_width([width, height]).unwrap_or(0.0); + + let y = top + + bpu.content_offset[1] + + vertex.y.px_height([width, height]).unwrap_or(0.0); + bounds[0] = bounds[0].min(x); bounds[1] = bounds[1].max(x); bounds[2] = bounds[2].min(y); @@ -2129,8 +2187,8 @@ impl Bin { // Update text for up to date ImageKey's and bounds. let content_tlwh = [ - bpu.optimal_content_bounds[2], - bpu.optimal_content_bounds[0], + bpu.optimal_content_bounds[2] + bpu.content_offset[1], + bpu.optimal_content_bounds[0] + bpu.content_offset[0], bpu.optimal_content_bounds[1] - bpu.optimal_content_bounds[0], bpu.optimal_content_bounds[3] - bpu.optimal_content_bounds[2], ]; @@ -2650,8 +2708,14 @@ impl Bin { let ty = if image_key.is_invalid() { 0 } else { 100 }; for vertex in vertexes.iter() { - let x = left + vertex.x.px_width([width, height]).unwrap_or(0.0); - let y = top + vertex.y.px_height([width, height]).unwrap_or(0.0); + let x = left + + bpu.content_offset[0] + + vertex.x.px_width([width, height]).unwrap_or(0.0); + + let y = top + + bpu.content_offset[1] + + vertex.y.px_height([width, height]).unwrap_or(0.0); + bounds[0] = bounds[0].min(x); bounds[1] = bounds[1].max(x); bounds[2] = bounds[2].min(y); @@ -2680,8 +2744,8 @@ impl Bin { // -- Text -------------------------------------------------------------------------- // let content_tlwh = [ - bpu.optimal_content_bounds[2], - bpu.optimal_content_bounds[0], + bpu.optimal_content_bounds[2] + bpu.content_offset[1], + bpu.optimal_content_bounds[0] + bpu.content_offset[0], bpu.optimal_content_bounds[1] - bpu.optimal_content_bounds[0], bpu.optimal_content_bounds[3] - bpu.optimal_content_bounds[2], ]; @@ -2699,9 +2763,14 @@ impl Bin { }); } - update_state - .text - .output_vertexes(content_tlwh, content_z, opacity, &mut inner_vert_data); + update_state.text.output_vertexes( + content_tlwh, + content_z, + opacity, + &style.text_body, + &context, + &mut inner_vert_data, + ); if let Some(text_bounds) = update_state.text.bounds(content_tlwh) { match bpu.content_bounds.as_mut() { diff --git a/src/interface/bin/style.rs b/src/interface/bin/style.rs index 217f725d..0cb24faa 100644 --- a/src/interface/bin/style.rs +++ b/src/interface/bin/style.rs @@ -7,9 +7,7 @@ mod vko { use crate::NonExhaustive; use crate::image::ImageKey; -use crate::interface::{ - Bin, Color, Flow, FontFamily, FontStretch, FontStyle, FontWeight, Position, UnitValue, -}; +use crate::interface::{Bin, Color, Flow, Position, TextBody, UnitValue}; /// Z-Index behavior /// @@ -184,130 +182,6 @@ pub enum LineLimit { Fixed(usize), } -/// The text body of a `Bin`. -/// -/// Each [`BinStyle`](`BinStyle`) has a single `TextBody`. It can contain multiple -/// [`TextSpan`](`TextSpan`). -/// -/// The default values for `base_attrs` will inheirt those set with -/// [`Interface::set_default_font`](`crate::interface::Interface::set_default_font`). -#[derive(Debug, Clone, PartialEq)] -pub struct TextBody { - pub spans: Vec, - pub line_spacing: LineSpacing, - pub line_limit: LineLimit, - pub text_wrap: TextWrap, - pub vert_align: TextVertAlign, - pub hori_align: TextHoriAlign, - pub base_attrs: TextAttrs, - pub _ne: NonExhaustive, -} - -impl Default for TextBody { - fn default() -> Self { - Self { - spans: Vec::new(), - line_spacing: Default::default(), - line_limit: Default::default(), - text_wrap: Default::default(), - vert_align: Default::default(), - hori_align: Default::default(), - base_attrs: TextAttrs::default(), - _ne: NonExhaustive(()), - } - } -} - -impl From for TextBody -where - T: Into, -{ - fn from(from: T) -> Self { - Self { - spans: vec![TextSpan::from(from)], - ..Default::default() - } - } -} - -impl TextBody { - pub fn is_empty(&self) -> bool { - self.spans.is_empty() || self.spans.iter().all(|span| span.is_empty()) - } -} - -/// A span of text within `TextBody`. -/// -/// A span consist of the text and its text attributes. -/// -/// The default values for `attrs` will inheirt those set in -/// [`TextBody.base_attrs`](struct.TextBody.html#structfield.base_attrs). -#[derive(Debug, Clone, PartialEq)] -pub struct TextSpan { - pub text: String, - pub attrs: TextAttrs, - pub _ne: NonExhaustive, -} - -impl Default for TextSpan { - fn default() -> Self { - Self { - text: String::new(), - attrs: TextAttrs { - color: Default::default(), - ..Default::default() - }, - _ne: NonExhaustive(()), - } - } -} - -impl From for TextSpan -where - T: Into, -{ - fn from(from: T) -> Self { - Self { - text: from.into(), - ..Default::default() - } - } -} - -impl TextSpan { - pub fn is_empty(&self) -> bool { - self.text.is_empty() - } -} - -/// Attributes of text. -#[derive(Debug, Clone, PartialEq)] -pub struct TextAttrs { - pub color: Color, - pub height: UnitValue, - pub secret: bool, - pub font_family: FontFamily, - pub font_weight: FontWeight, - pub font_stretch: FontStretch, - pub font_style: FontStyle, - pub _ne: NonExhaustive, -} - -impl Default for TextAttrs { - fn default() -> Self { - Self { - color: Color::black(), - height: Default::default(), - secret: false, - font_family: Default::default(), - font_weight: Default::default(), - font_stretch: Default::default(), - font_style: Default::default(), - _ne: NonExhaustive(()), - } - } -} - /// A user defined vertex for [`Bin`](`Bin`) /// /// - `x` & `y` will be from the top-left on the inside of the `Bin`. diff --git a/src/interface/bin/text_body.rs b/src/interface/bin/text_body.rs new file mode 100644 index 00000000..353f5324 --- /dev/null +++ b/src/interface/bin/text_body.rs @@ -0,0 +1,342 @@ +use std::cmp::Ordering; +use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign}; + +use crate::NonExhaustive; +use crate::interface::{ + Color, FontFamily, FontStretch, FontStyle, FontWeight, LineLimit, LineSpacing, TextHoriAlign, + TextVertAlign, TextWrap, UnitValue, +}; + +/// Attributes of text. +#[derive(Debug, Clone, PartialEq)] +pub struct TextAttrs { + pub color: Color, + pub height: UnitValue, + pub secret: bool, + pub font_family: FontFamily, + pub font_weight: FontWeight, + pub font_stretch: FontStretch, + pub font_style: FontStyle, + pub _ne: NonExhaustive, +} + +impl Default for TextAttrs { + fn default() -> Self { + Self { + color: Color::black(), + height: Default::default(), + secret: false, + font_family: Default::default(), + font_weight: Default::default(), + font_stretch: Default::default(), + font_style: Default::default(), + _ne: NonExhaustive(()), + } + } +} + +/// A mask for [`TextAttrs`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct TextAttrsMask(u16); + +impl Default for TextAttrsMask { + fn default() -> Self { + Self::ALL + } +} + +impl TextAttrsMask { + pub const ALL: Self = TextAttrsMask(u16::max_value()); + pub const COLOR: Self = TextAttrsMask(0b1000000000000000); + pub const FONT_FAMILY: Self = TextAttrsMask(0b0001000000000000); + pub const FONT_STRETCH: Self = TextAttrsMask(0b0000010000000000); + pub const FONT_STYLE: Self = TextAttrsMask(0b0000001000000000); + pub const FONT_WEIGHT: Self = TextAttrsMask(0b0000100000000000); + pub const HEIGHT: Self = TextAttrsMask(0b0100000000000000); + pub const NONE: Self = TextAttrsMask(0); + pub const SECRET: Self = TextAttrsMask(0b0010000000000000); + + pub fn apply(self, src: &TextAttrs, dst: &mut TextAttrs) { + if self & Self::COLOR == Self::COLOR { + dst.color = src.color; + } + + if self & Self::HEIGHT == Self::HEIGHT { + dst.height = src.height; + } + + if self & Self::SECRET == Self::SECRET { + dst.secret = src.secret; + } + + if self & Self::FONT_FAMILY == Self::FONT_FAMILY { + dst.font_family = src.font_family.clone(); + } + + if self & Self::FONT_WEIGHT == Self::FONT_WEIGHT { + dst.font_weight = src.font_weight; + } + + if self & Self::FONT_STRETCH == Self::FONT_STRETCH { + dst.font_stretch = src.font_stretch; + } + + if self & Self::FONT_STYLE == Self::FONT_STYLE { + dst.font_style = src.font_style; + } + } +} + +impl BitAnd for TextAttrsMask { + type Output = Self; + + fn bitand(self, rhs: Self) -> Self::Output { + Self(self.0 & rhs.0) + } +} + +impl BitAndAssign for TextAttrsMask { + fn bitand_assign(&mut self, rhs: Self) { + *self = Self(self.0 & rhs.0); + } +} + +impl BitOr for TextAttrsMask { + type Output = Self; + + fn bitor(self, rhs: Self) -> Self::Output { + Self(self.0 | rhs.0) + } +} + +impl BitOrAssign for TextAttrsMask { + fn bitor_assign(&mut self, rhs: Self) { + *self = Self(self.0 | rhs.0); + } +} + +impl BitXor for TextAttrsMask { + type Output = Self; + + fn bitxor(self, rhs: Self) -> Self::Output { + Self(self.0 ^ rhs.0) + } +} + +impl BitXorAssign for TextAttrsMask { + fn bitxor_assign(&mut self, rhs: Self) { + *self = Self(self.0 ^ rhs.0); + } +} + +/// A span of text within `TextBody`. +/// +/// A span consist of the text and its text attributes. +/// +/// The default values for `attrs` will inheirt those set in +/// [`TextBody.base_attrs`](struct.TextBody.html#structfield.base_attrs). +#[derive(Debug, Clone, PartialEq)] +pub struct TextSpan { + pub text: String, + pub attrs: TextAttrs, + pub _ne: NonExhaustive, +} + +impl Default for TextSpan { + fn default() -> Self { + Self { + text: String::new(), + attrs: TextAttrs { + color: Default::default(), + ..Default::default() + }, + _ne: NonExhaustive(()), + } + } +} + +impl From for TextSpan +where + T: Into, +{ + fn from(from: T) -> Self { + Self { + text: from.into(), + ..Default::default() + } + } +} + +impl TextSpan { + pub fn is_empty(&self) -> bool { + self.text.is_empty() + } +} + +/// The text body of a `Bin`. +/// +/// Each [`BinStyle`](`crate::interface::BinStyle`) has a single `TextBody`. It can contain multiple +/// [`TextSpan`](`TextSpan`). +/// +/// The default values for `base_attrs` will inheirt those set with +/// [`Interface::set_default_font`](`crate::interface::Interface::set_default_font`). +#[derive(Debug, Clone, PartialEq)] +pub struct TextBody { + pub spans: Vec, + pub line_spacing: LineSpacing, + pub line_limit: LineLimit, + pub text_wrap: TextWrap, + pub vert_align: TextVertAlign, + pub hori_align: TextHoriAlign, + pub base_attrs: TextAttrs, + pub cursor: TextCursor, + pub cursor_color: Color, + pub selection: Option, + pub selection_color: Color, + pub _ne: NonExhaustive, +} + +impl Default for TextBody { + fn default() -> Self { + Self { + spans: Vec::new(), + line_spacing: Default::default(), + line_limit: Default::default(), + text_wrap: Default::default(), + vert_align: Default::default(), + hori_align: Default::default(), + base_attrs: TextAttrs::default(), + cursor: Default::default(), + cursor_color: Color::black(), + selection: None, + selection_color: Color::shex("4040ffc0"), + _ne: NonExhaustive(()), + } + } +} + +impl From for TextBody +where + T: Into, +{ + fn from(from: T) -> Self { + Self { + spans: vec![TextSpan::from(from)], + ..Default::default() + } + } +} + +/// A positioned cursor within [`TextBody`]. +/// +/// **Note:** May become invalid if the [`TextBody`] is modified. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PosTextCursor { + pub span: usize, + pub byte_s: usize, + pub byte_e: usize, + pub affinity: TextCursorAffinity, +} + +/// A cursor within [`TextBody`]. +/// +/// **Note:** May become invalid if the [`TextBody`] is modified. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub enum TextCursor { + #[default] + None, + Empty, + Position(PosTextCursor), +} + +impl TextCursor { + pub fn into_position(self) -> Option { + match self { + Self::Position(cursor) => Some(cursor), + _ => None, + } + } +} + +impl From for TextCursor { + fn from(cursor: PosTextCursor) -> TextCursor { + TextCursor::Position(cursor) + } +} + +impl PartialOrd for PosTextCursor { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for PosTextCursor { + fn cmp(&self, other: &Self) -> Ordering { + self.span.cmp(&other.span).then( + self.byte_s + .cmp(&other.byte_s) + .then(self.affinity.cmp(&other.affinity)), + ) + } +} + +/// The affinity of a text cursor. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TextCursorAffinity { + Before, + After, +} + +impl PartialOrd for TextCursorAffinity { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for TextCursorAffinity { + fn cmp(&self, other: &Self) -> Ordering { + match self { + Self::Before => { + match other { + Self::Before => Ordering::Equal, + Self::After => Ordering::Less, + } + }, + Self::After => { + match other { + Self::Before => Ordering::Greater, + Self::After => Ordering::Equal, + } + }, + } + } +} + +/// A text selection with a [`TextBody`]. +/// +/// A valid `TextSelection` must be constructed with two valid cursors and the start cursor must be +/// before the end cursor. The [`unordered`](`TextSelection::unordered`) method can be used for +/// constructing `TextSelection` with two cursors with unknown order. +/// +/// **Note:** May become invalid if the [`TextBody`] is modified. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct TextSelection { + pub start: PosTextCursor, + pub end: PosTextCursor, +} + +impl TextSelection { + pub fn unordered(a: PosTextCursor, b: PosTextCursor) -> Self { + if a > b { + Self { + start: b, + end: a, + } + } else { + Self { + start: a, + end: b, + } + } + } +} diff --git a/src/interface/bin/text_guard.rs b/src/interface/bin/text_guard.rs new file mode 100644 index 00000000..f403b4b1 --- /dev/null +++ b/src/interface/bin/text_guard.rs @@ -0,0 +1,2500 @@ +use std::cell::{Ref, RefCell, RefMut}; +use std::ops::{Deref, DerefMut}; +use std::sync::Arc; + +use parking_lot::{MutexGuard, RwLockUpgradableReadGuard}; +use unicode_segmentation::UnicodeSegmentation; + +use crate::interface::bin::{InternalHookFn, InternalHookTy, TextState, UpdateState}; +use crate::interface::{ + Bin, BinPostUpdate, BinStyle, BinStyleValidation, Color, DefaultFont, PosTextCursor, Position, + TextAttrs, TextAttrsMask, TextBody, TextCursor, TextCursorAffinity, TextSelection, TextSpan, +}; + +/// Used to inspect and/or modify the [`TextBody`](TextBody). +/// +/// **Warning:** While `TextBodyGuard` is in scope, methods that modify [`Bin`](Bin)'s +/// should not be used. Failure to do so, may result in potential deadlocks. Methods that +/// should be avoided include [`Bin::style_modify`](Bin::style_modify), +/// [`Bin::style_modify_then`](Bin::style_modify_then), [`Bin::style_update`](Bin::style_update), +/// [`Bin::set_visibility`](Bin::set_visibility), [`Bin::toggle_visibility`](Bin::toggle_visibility), etc. +/// `TextBodyGuard` provides methods to do so, see [`TextBodyGuard::style_modify`] and +/// [`TextBodyGuard::bin_on_update`]. +pub struct TextBodyGuard<'a> { + bin: &'a Arc, + text_state: RefCell>>, + style_state: RefCell>>, + tlwh: RefCell>, + default_font: RefCell>, + on_update: RefCell, &BinPostUpdate) + Send + 'static>>>, +} + +impl<'a> TextBodyGuard<'a> { + /// Check if [`TextBody`](TextBody) is empty. + pub fn is_empty(&self) -> bool { + let body = &self.style().text_body; + body.spans.is_empty() || body.spans.iter().all(|span| span.is_empty()) + } + + /// Check if the provided [`TextCursor`](TextCursor) is valid. + pub fn is_cursor_valid(&self, cursor: C) -> bool + where + C: Into, + { + let body = &self.style().text_body; + + let cursor = match cursor.into() { + TextCursor::Position(cursor) => cursor, + _ => return true, + }; + + if cursor.span >= body.spans.len() + || cursor.byte_s >= body.spans[cursor.span].text.len() + || cursor.byte_e > body.spans[cursor.span].text.len() + || cursor.byte_e <= cursor.byte_s + { + return false; + } + + if !body.spans[cursor.span].text.is_char_boundary(cursor.byte_s) { + return false; + } + + for (byte_i, c) in body.spans[cursor.span].text.char_indices() { + if byte_i == cursor.byte_s { + if c.len_utf8() != cursor.byte_e - cursor.byte_s { + return false; + } + + break; + } + } + + true + } + + /// Check if the provided [`TextCursor`](TextCursor)'s are equivalent. + /// + /// **Note:** will return `false` if either of the provided cursors are invalid. + pub fn are_cursors_equivalent(&self, a: TextCursor, b: TextCursor) -> bool { + if !self.is_cursor_valid(a) || !self.is_cursor_valid(b) { + return false; + } + + if a == b { + return true; + } + + let a = match a { + TextCursor::Empty | TextCursor::None => return false, + TextCursor::Position(cursor) => cursor, + }; + + let b = match b { + TextCursor::Empty | TextCursor::None => return false, + TextCursor::Position(cursor) => cursor, + }; + + if a.affinity == b.affinity { + // The affinities must be different + return false; + } + + let body = &self.style().text_body; + + match a.affinity { + TextCursorAffinity::Before => { + // B is after the character before A + if a.byte_s == 0 { + // A is at the start of the span + if a.span == 0 || b.span != a.span - 1 { + // B must be in the previous span + false + } else { + // B must be at the end of the previous span. + b.byte_e == body.spans[b.span].text.len() + } + } else { + // A isn't at the start of the span + if a.span != b.span { + // B must be within the same span + false + } else { + // A's byte_s should equal B's byte_e + a.byte_s == b.byte_e + } + } + }, + TextCursorAffinity::After => { + // Same as above, but A/B are reversed. + if b.byte_s == 0 { + if b.span == 0 || a.span != b.span - 1 { + false + } else { + a.byte_e == body.spans[a.span].text.len() + } + } else { + if b.span != a.span { + false + } else { + b.byte_s == a.byte_e + } + } + }, + } + } + + /// Check if the provided [`TextSelection`](TextSelection) is valid. + pub fn is_selection_valid(&self, selection: TextSelection) -> bool { + self.is_cursor_valid(selection.start) + && self.is_cursor_valid(selection.end) + && selection.start < selection.end + } + + /// Obtain the current displayed [`TextCursor`](TextCursor). + pub fn cursor(&self) -> TextCursor { + self.style().text_body.cursor + } + + /// Set the displayed [`TextCursor`](TextCursor). + pub fn set_cursor(&self, cursor: TextCursor) { + self.style_mut().text_body.cursor = cursor; + } + + /// Set the [`Color`](Color) of the displayed [`TextCursor`](TextCursor). + pub fn set_cursor_color(&self, color: Color) { + self.style_mut().text_body.cursor_color = color; + } + + /// Obtain a [`TextCursor`](TextCursor) given a phyiscal position. + /// + /// **Returns [`None`](TextCursor::None) if:** + /// - this `Bin` is currently not visible. + /// + /// **Returns [`Empty`](TextCursor::Empty) if:** + /// - this `Bin` has yet to update the text layout. + /// - this `Bin`'s `TextBody` is empty. + pub fn get_cursor(&self, mut position: [f32; 2]) -> TextCursor { + self.update_layout(); + let tlwh = self.tlwh(); + position[0] -= tlwh[1]; + position[1] -= tlwh[0]; + self.state().get_cursor(position) + } + + /// Obtain the line and column index of the provided [`TextCursor`](TextCursor). + /// + /// **Note:** When `as_displayed` is `true` wrapping is taken into account. + /// + /// **Returns `None` if:** + /// - the provided cursor is invalid. + /// - the provided cursor is [`Empty`](TextCursor::Empty) or [`None`](TextCursor::None). + pub fn cursor_line_column(&self, cursor: TextCursor, as_displayed: bool) -> Option<[usize; 2]> { + if as_displayed { + self.update_layout(); + self.state().cursor_line_column(cursor) + } else { + if !self.is_cursor_valid(cursor) { + return None; + } + + let cursor = match cursor { + TextCursor::None | TextCursor::Empty => return None, + TextCursor::Position(cursor) => cursor, + }; + + let body = &self.style().text_body; + let mut line_i = 0; + let mut col_i = 0; + + for span_i in 0..body.spans.len() { + if span_i < cursor.span { + for c in body.spans[span_i].text.chars() { + if c == '\n' { + line_i += 1; + col_i = 0; + } else { + col_i += 1; + } + } + } else { + debug_assert_eq!(span_i, cursor.span); + + for (byte_i, c) in body.spans[span_i].text.char_indices() { + if byte_i < cursor.byte_s { + if c == '\n' { + line_i += 1; + col_i = 0; + continue; + } else { + col_i += 1; + } + } else { + match cursor.affinity { + TextCursorAffinity::Before => { + return Some([line_i, col_i]); + }, + TextCursorAffinity::After => { + if c == '\n' { + return Some([line_i + 1, 0]); + } else { + return Some([line_i, col_i + 1]); + } + }, + } + } + } + } + } + + unreachable!() + } + } + + /// Obtain the bounding box of the provided [`TextCursor`](TextCursor). + /// + /// Format: `[MIN_X, MAX_X, MIN_Y, MAX_Y]`. + /// + /// **Returns `None` if:** + /// - the provided cursor is invalid. + /// - the provided cursor is [`None`](TextCursor::None). + pub fn cursor_bounds(&self, mut cursor: TextCursor) -> Option<[f32; 4]> { + if cursor == TextCursor::None { + return None; + } + + if cursor == TextCursor::Empty { + // Note: TextState::get_cursor_bounds doesn't check if the body is empty and assumes if + // the provided cursor is Empty the body is empty. + + cursor = match self.cursor_next(TextCursor::Empty) { + TextCursor::Empty => TextCursor::Empty, + TextCursor::None => return None, + text_cursor_position @ TextCursor::Position(_) => text_cursor_position, + }; + } + + self.update_layout(); + let tlwh = self.tlwh(); + let default_font = self.default_font(); + let style = self.style(); + + self.state() + .get_cursor_bounds(cursor, tlwh, &style.text_body, default_font.height) + .map(|(bounds, _)| bounds) + } + + /// Obtain the bounding box of the displayed line with the provided [`TextCursor`](TextCursor). + /// + /// Format: `[MIN_X, MAX_X, MIN_Y, MAX_Y]`. + /// + /// **Returns `None` if:** + /// - the provided cursor is invalid. + /// - the provided cursor is [`None`](TextCursor::None) or [`Empty`](TextCursor::Empty). + pub fn cursor_line_bounds(&self, cursor: TextCursor) -> Option<[f32; 4]> { + let line_i = self.cursor_line_column(cursor, true)?[0]; + let tlwh = self.tlwh(); + self.state().line_bounds(tlwh, line_i) + } + + /// Get the [`TextCursor`](TextCursor) one position to the left of the provided [`TextCursor`](TextCursor). + /// + /// **Returns [`None`](`TextCursor::None`) if:** + /// - the provided cursor is invalid. + /// - the provided cursor is [`None`](TextCursor::None) or [`Empty`](TextCursor::Empty). + /// - there isn't a valid cursor position before the one provided. + pub fn cursor_prev(&self, cursor: TextCursor) -> TextCursor { + let body = &self.style().text_body; + + let cursor = match cursor { + TextCursor::None | TextCursor::Empty => return TextCursor::None, + TextCursor::Position(cursor) => cursor, + }; + + if cursor.affinity == TextCursorAffinity::After { + if !self.is_cursor_valid(cursor) { + return TextCursor::None; + } + + return TextCursor::Position(PosTextCursor { + affinity: TextCursorAffinity::Before, + ..cursor + }); + } + + if cursor.span >= body.spans.len() + || cursor.byte_s >= body.spans[cursor.span].text.len() + || cursor.byte_e > body.spans[cursor.span].text.len() + || cursor.byte_e <= cursor.byte_s + { + return TextCursor::None; + } + + let mut is_next = false; + + for span_i in (0..=cursor.span).rev() { + if !is_next && span_i != cursor.span { + return TextCursor::None; + } + + for (byte_i, c) in body.spans[span_i].text.char_indices().rev() { + if is_next { + return TextCursor::Position(PosTextCursor { + span: span_i, + byte_s: byte_i, + byte_e: byte_i + c.len_utf8(), + affinity: TextCursorAffinity::Before, + }); + } + + if byte_i == cursor.byte_s { + if c.len_utf8() != cursor.byte_e - cursor.byte_s { + return TextCursor::None; + } + + is_next = true; + continue; + } + + if byte_i < cursor.byte_s { + return TextCursor::None; + } + } + } + + TextCursor::None + } + + /// Get the [`TextCursor`](TextCursor) one position to the right of the provided [`TextCursor`](TextCursor). + /// + /// **Returns [`None`](`TextCursor::None`) if:** + /// - the provided cursor is invalid. + /// - the provided cursor is `None`. + /// - there isn't a valid cursor position after the one provided. + /// + /// **Returns [`Empty`]('TextCursor::Empty`) if:** + /// - the provided cursor is [`Empty`](`TextCursor::Empty`) and the [`TextBody`](TextBody) is empty. + pub fn cursor_next(&self, cursor: TextCursor) -> TextCursor { + let body = &self.style().text_body; + + let cursor = match cursor { + TextCursor::None => return TextCursor::None, + TextCursor::Empty => { + for span_i in 0..body.spans.len() { + for (byte_i, c) in body.spans[span_i].text.char_indices() { + return TextCursor::Position(PosTextCursor { + span: span_i, + byte_s: byte_i, + byte_e: byte_i + c.len_utf8(), + affinity: TextCursorAffinity::After, + }); + } + } + + return TextCursor::Empty; + }, + TextCursor::Position(cursor) => cursor, + }; + + if cursor.affinity == TextCursorAffinity::Before { + if !self.is_cursor_valid(cursor) { + return TextCursor::None; + } + + return TextCursor::Position(PosTextCursor { + affinity: TextCursorAffinity::After, + ..cursor + }); + } + + if cursor.span >= body.spans.len() + || cursor.byte_s >= body.spans[cursor.span].text.len() + || cursor.byte_e > body.spans[cursor.span].text.len() + || cursor.byte_e <= cursor.byte_s + { + return TextCursor::None; + } + + let mut is_next = false; + + for span_i in cursor.span..body.spans.len() { + if !is_next && span_i != cursor.span { + return TextCursor::None; + } + + for (byte_i, c) in body.spans[span_i].text.char_indices() { + if is_next { + return TextCursor::Position(PosTextCursor { + span: span_i, + byte_s: byte_i, + byte_e: byte_i + c.len_utf8(), + affinity: TextCursorAffinity::After, + }); + } + + if byte_i == cursor.byte_s { + if c.len_utf8() != cursor.byte_e - cursor.byte_s { + return TextCursor::None; + } + + is_next = true; + continue; + } + + if byte_i > cursor.byte_s { + return TextCursor::None; + } + } + } + + TextCursor::None + } + + /// Get the [`TextCursor`](TextCursor) one line up from the provided [`TextCursor`](TextCursor). + /// + /// **Note:** When `as_displayed` is `true` wrapping is taken into account. + /// + /// **Returns [`None`](`TextCursor::None`) if:** + /// - the provided cursor is invalid. + /// - the provided cursor is `None`. + /// - there isn't a valid cursor position above the one provided. + pub fn cursor_up(&self, cursor: TextCursor, as_displayed: bool) -> TextCursor { + if as_displayed { + self.update_layout(); + self.state().cursor_up(cursor, &self.style().text_body) + } else { + let [mut line_i, mut col_i] = match self.cursor_line_column(cursor, false) { + Some(some) => some, + None => return TextCursor::None, + }; + + if line_i == 0 { + return TextCursor::None; + } + + line_i -= 1; + + let num_cols = match self.line_column_count(line_i, false) { + Some(some) => some, + None => unreachable!(), + }; + + col_i = col_i.min(num_cols); + self.line_column_cursor(line_i, col_i, false) + } + } + + /// Get the [`TextCursor`](TextCursor) one line down from the provided [`TextCursor`](TextCursor). + /// + /// **Note:** When `as_displayed` is `true` wrapping is taken into account. + /// + /// **Returns [`None`](`TextCursor::None`) if:** + /// - the provided cursor is invalid. + /// - the provided cursor is `None`. + /// - there isn't a valid cursor position below the one provided. + pub fn cursor_down(&self, cursor: TextCursor, as_displayed: bool) -> TextCursor { + if as_displayed { + self.update_layout(); + self.state().cursor_down(cursor, &self.style().text_body) + } else { + let [mut line_i, mut col_i] = match self.cursor_line_column(cursor, false) { + Some(some) => some, + None => return TextCursor::None, + }; + + line_i += 1; + + let num_cols = match self.line_column_count(line_i, false) { + Some(some) => some, + None => return TextCursor::None, + }; + + col_i = col_i.min(num_cols); + self.line_column_cursor(line_i, col_i, false) + } + } + + /// Get the [`TextCursor`](TextCursor) of the provided [`TextCursor`](TextCursor) offset by + /// the provided `line_offset`. + /// + /// **Notes:** + /// - When `as_displayed` is `true` wrapping is taken into account. + /// - `line_offset` will be clamped to a valid range. For example, if the provided cursor is on + /// line `2` and an offset of `-3` is provided, then this method will return a cursor on line `0`. + /// + /// **Returns [`None`](`TextCursor::None`) if:** + /// - the provided cursor is invalid. + /// - the provided cursor is `None`. + /// - there isn't a valid cursor position above/below the provided cursor. + pub fn cursor_line_offset( + &self, + cursor: TextCursor, + line_offset: isize, + as_displayed: bool, + ) -> TextCursor { + if line_offset == 0 { + TextCursor::None + } else if as_displayed { + self.update_layout(); + self.state() + .cursor_line_offset(cursor, &self.style().text_body, line_offset) + } else { + let [line_i, col_i] = match self.cursor_line_column(cursor, false) { + Some(some) => some, + None => return TextCursor::None, + }; + + let line_count = match self.line_count(false) { + Some(line_count) => line_count, + None => return TextCursor::None, + }; + + let t_line_i = + (line_i as isize + line_offset).clamp(0, line_count as isize - 1) as usize; + + if t_line_i == line_i { + return TextCursor::None; + } + + let col_count = self.line_column_count(t_line_i, false).unwrap(); + let t_col_i = col_i.min(col_count); + self.line_column_cursor(t_line_i, t_col_i, false) + } + } + + /// Insert a `char` after the provided [`TextCursor`](TextCursor). + /// + /// **Returns [`None`](`TextCursor::None`) if:** + /// - the provided cursor is invalid. + pub fn cursor_insert(&self, cursor: TextCursor, c: char) -> TextCursor { + match cursor { + TextCursor::None => TextCursor::None, + TextCursor::Empty => { + let body = &mut self.style_mut().text_body; + + if body.spans.is_empty() { + body.spans.push(Default::default()); + } + + body.spans[0].text.insert(0, c); + + TextCursor::Position(PosTextCursor { + span: 0, + byte_s: 0, + byte_e: c.len_utf8(), + affinity: TextCursorAffinity::After, + }) + }, + TextCursor::Position(mut cursor) => { + if !self.is_cursor_valid(cursor) { + return TextCursor::None; + } + + let body = &mut self.style_mut().text_body; + + if cursor.affinity == TextCursorAffinity::Before { + body.spans[cursor.span].text.insert(cursor.byte_s, c); + cursor.byte_e = cursor.byte_s + c.len_utf8(); + cursor.affinity = TextCursorAffinity::After; + TextCursor::Position(cursor) + } else { + body.spans[cursor.span].text.insert(cursor.byte_e, c); + cursor.byte_s = cursor.byte_e; + cursor.byte_e = cursor.byte_s + c.len_utf8(); + TextCursor::Position(cursor) + } + }, + } + } + + /// Insert a string after the provided [`TextCursor`](TextCursor). + /// + /// **Returns [`None`](`TextCursor::None`) if:** + /// - the provided cursor is invalid. + /// - the provided cursor is [`None`](`TextCursor::None`). + pub fn cursor_insert_str(&self, cursor: TextCursor, string: S) -> TextCursor + where + S: AsRef, + { + let string = string.as_ref(); + + if string.is_empty() { + return cursor; + } + + let [span_i, byte_i] = match cursor { + TextCursor::None => return TextCursor::None, + TextCursor::Empty => { + let body = &mut self.style_mut().text_body; + + if body.spans.is_empty() { + body.spans.push(Default::default()); + } + + [0, 0] + }, + TextCursor::Position(cursor) => { + if !self.is_cursor_valid(cursor) { + return TextCursor::None; + } + + match cursor.affinity { + TextCursorAffinity::Before => [cursor.span, cursor.byte_s], + TextCursorAffinity::After => [cursor.span, cursor.byte_e], + } + }, + }; + + let body = &mut self.style_mut().text_body; + body.spans[span_i].text.insert_str(byte_i, string); + let (mut byte_s, c) = string.char_indices().rev().next().unwrap(); + byte_s += byte_i; + let byte_e = byte_s + c.len_utf8(); + + TextCursor::Position(PosTextCursor { + span: span_i, + byte_s, + byte_e, + affinity: TextCursorAffinity::After, + }) + } + + /// Insert a collection of [`TextSpan`](TextSpan)'s after the provided [`TextCursor`](TextCursor). + /// + /// **Note:** This will merge adjacent spans with the same attributes. + /// + /// **Returns [`None`](`TextCursor::None`) if:** + /// - the provided cursor is invalid. + /// - the provided cursor is [`None`](`TextCursor::None`). + pub fn cursor_insert_spans(&self, cursor: TextCursor, spans: S) -> TextCursor + where + S: IntoIterator, + { + let [span_i, byte_i] = match cursor { + TextCursor::None => return TextCursor::None, + TextCursor::Empty => { + let body = &mut self.style_mut().text_body; + + if body.spans.is_empty() { + body.spans.push(Default::default()); + } + + [0, 0] + }, + TextCursor::Position(cursor) => { + if !self.is_cursor_valid(cursor) { + return TextCursor::None; + } + + match cursor.affinity { + TextCursorAffinity::Before => [cursor.span, cursor.byte_s], + TextCursorAffinity::After => [cursor.span, cursor.byte_e], + } + }, + }; + + let mut pos_c = PosTextCursor { + span: span_i, + byte_s: 0, + byte_e: byte_i, + affinity: TextCursorAffinity::After, + }; + + let body = &mut self.style_mut().text_body; + let mut is_empty = true; + + for span in spans.into_iter() { + if span.attrs == body.spans[pos_c.span].attrs { + // Same attributes as the current span, so merge. + + body.spans[pos_c.span] + .text + .insert_str(pos_c.byte_e, span.text.as_str()); + pos_c.byte_e += span.text.len(); + is_empty = false; + continue; + } + + if pos_c.span + 1 < body.spans.len() + && pos_c.byte_e == body.spans[pos_c.span].text.len() + && span.attrs == body.spans[pos_c.span + 1].attrs + { + // There is a span following this one, the cursor is at the end of the current span + // and the attributes are the same as the following span, so merge. + + body.spans[pos_c.span + 1] + .text + .insert_str(0, span.text.as_str()); + pos_c.span += 1; + pos_c.byte_e = span.text.len(); + is_empty = false; + continue; + } + + // Doesn't match either span, so insert. + + pos_c.span += 1; + pos_c.byte_e = span.text.len(); + body.spans.insert(pos_c.span + 1, span); + is_empty = false; + } + + if is_empty { cursor } else { pos_c.into() } + } + + /// Delete the `char` before the provided [`TextCursor`](TextCursor). + /// + /// **Note:** If deleletion empties the cursor's span the span will be removed. + /// + /// **Returns [`None`](`TextCursor::None`) if:** + /// - the provided cursor is invalid. + /// - the provided cursor is `None`. + /// + /// **Returns [`Empty`](`TextCursor::Empty`) if:** + /// - the `TextBody` is empty after the deletion. + pub fn cursor_delete(&self, cursor: TextCursor) -> TextCursor { + let rm_cursor = match self.cursor_prev(cursor) { + TextCursor::None => { + return match self.cursor_next(TextCursor::Empty) { + TextCursor::None => TextCursor::None, + TextCursor::Empty => TextCursor::Empty, + TextCursor::Position(mut cursor) => { + cursor.affinity = TextCursorAffinity::Before; + TextCursor::Position(cursor) + }, + }; + }, + TextCursor::Empty => unreachable!(), + TextCursor::Position(cursor) => cursor, + }; + + let mut ret_cursor = match self.cursor_prev(rm_cursor.into()) { + TextCursor::None => TextCursor::None, + TextCursor::Empty => unreachable!(), + TextCursor::Position(mut cursor) => { + cursor.affinity = TextCursorAffinity::After; + TextCursor::Position(cursor) + }, + }; + + { + let body = &mut self.style_mut().text_body; + body.spans[rm_cursor.span].text.remove(rm_cursor.byte_s); + + if body.spans[rm_cursor.span].text.is_empty() { + body.spans.remove(rm_cursor.span); + } + } + + if ret_cursor == TextCursor::None { + ret_cursor = match self.cursor_next(TextCursor::Empty) { + TextCursor::None => TextCursor::None, + TextCursor::Empty => TextCursor::Empty, + TextCursor::Position(mut cursor) => { + cursor.affinity = TextCursorAffinity::Before; + TextCursor::Position(cursor) + }, + }; + } + + ret_cursor + } + + /// Delete the word that the provided cursor is within. + /// + /// **Returns [`None`](TextCursor::None) if:** + /// - the provided cursor is invalid. + /// - the provided cursor is [`None`](TextCursor::None) or [`Empty`](TextCursor::Empty). + pub fn cursor_delete_word(&self, cursor: TextCursor) -> TextCursor { + let selection = match self.cursor_select_word(cursor) { + Some(some) => some, + None => return TextCursor::None, + }; + + self.selection_delete(selection) + } + + /// Delete the line that the provided [`TextCursor`](TextCursor) is within. + /// + /// **Note:** When `as_displayed` is `true` wrapping is taken into account. + /// + /// **Returns [`None`](TextCursor::None) if:** + /// - the provided cursor is invalid. + /// - the provided cursor is [`Empty`](TextCursor::Empty) or [`None`](TextCursor::None). + /// + /// **Returns [`Empty`](TextCursor::Empty) if:** + /// - the `TextBody` is empty after the deletion. + pub fn cursor_delete_line(&self, cursor: TextCursor, as_displayed: bool) -> TextCursor { + let selection = match self.cursor_select_line(cursor, as_displayed) { + Some(selection) => selection, + None => return TextCursor::None, + }; + + self.selection_delete(selection) + } + + /// Delete the span that the provided [`TextCursor`](TextCursor) is within. + /// + /// **Returns [`None`](TextCursor::None) if:** + /// - the provided cursor is invalid. + /// - the provided cursor is [`Empty`](TextCursor::Empty) or [`None`](TextCursor::None). + /// + /// **Returns [`Empty`](TextCursor::Empty) if:** + /// - the `TextBody` is empty after the deletion. + pub fn cursor_delete_span(&self, cursor: TextCursor) -> TextCursor { + let selection = match self.cursor_select_span(cursor) { + Some(selection) => selection, + None => return TextCursor::None, + }; + + self.selection_delete(selection) + } + + /// Get the [`TextCursor`](TextCursor) at the start of the word that the provided [`TextCursor`](TextCursor) is within. + /// + /// **Returns [`None`](TextCursor::None) if:** + /// - the provided cursor is invalid. + /// - the provided cursor is [`Empty`](TextCursor::Empty) or [`None`](TextCursor::None). + pub fn cursor_word_start(&self, cursor: TextCursor) -> TextCursor { + if !self.is_cursor_valid(cursor) { + return TextCursor::None; + } + + let cursor = match cursor { + TextCursor::None | TextCursor::Empty => return TextCursor::None, + TextCursor::Position(cursor) => cursor, + }; + + let body = &self.style().text_body; + let word_ranges = word_ranges(body, true); + + for range_i in (0..word_ranges.len()).rev() { + if cursor > word_ranges[range_i].start + || self.are_cursors_equivalent(cursor.into(), word_ranges[range_i].start.into()) + { + return word_ranges[range_i].start.into(); + } + } + + self.span_start(0) + } + + /// Get the [`TextCursor`](TextCursor) at the end of the word that the provided [`TextCursor`](TextCursor) is within. + /// + /// **Returns [`None`](TextCursor::None) if:** + /// - the provided cursor is invalid. + /// - the provided cursor is [`Empty`](TextCursor::Empty) or [`None`](TextCursor::None). + pub fn cursor_word_end(&self, cursor: TextCursor) -> TextCursor { + if !self.is_cursor_valid(cursor) { + return TextCursor::None; + } + + let cursor = match cursor { + TextCursor::None | TextCursor::Empty => return TextCursor::None, + TextCursor::Position(cursor) => cursor, + }; + + let body = &self.style().text_body; + let word_ranges = word_ranges(body, true); + + for range_i in 0..word_ranges.len() { + if cursor < word_ranges[range_i].end + || self.are_cursors_equivalent(cursor.into(), word_ranges[range_i].end.into()) + { + return word_ranges[range_i].end.into(); + } + } + + if body.spans.is_empty() { + TextCursor::None + } else { + self.span_end(body.spans.len() - 1) + } + } + + /// Get the [`TextCursor`](TextCursor) of the word that the provided [`TextCursor`](TextCursor) is within. + /// + /// **Returns [`None`](TextCursor::None) if:** + /// - the provided cursor is invalid. + /// - the provided cursor is [`Empty`](TextCursor::Empty) or [`None`](TextCursor::None). + pub fn cursor_select_word(&self, cursor: TextCursor) -> Option { + if !self.is_cursor_valid(cursor) { + return None; + } + + let cursor = match cursor { + TextCursor::None | TextCursor::Empty => return None, + TextCursor::Position(cursor) => cursor, + }; + + let body = &self.style().text_body; + let word_ranges = word_ranges(body, false); + + for range_i in 0..word_ranges.len() { + if word_ranges[range_i].is_whitespace { + if range_i == 0 { + if word_ranges.len() == 1 || cursor < word_ranges[range_i].end { + return Some(TextSelection { + start: word_ranges[range_i].start, + end: word_ranges[range_i].end, + }); + } + } else if range_i == word_ranges.len() - 1 { + if word_ranges.len() == 1 || cursor > word_ranges[range_i].start { + return Some(TextSelection { + start: word_ranges[range_i].start, + end: word_ranges[range_i].end, + }); + } + } else { + if cursor > word_ranges[range_i].start && cursor < word_ranges[range_i].end { + return Some(TextSelection { + start: word_ranges[range_i].start, + end: word_ranges[range_i].end, + }); + } + } + } else { + if (cursor > word_ranges[range_i].start + || self + .are_cursors_equivalent(cursor.into(), word_ranges[range_i].start.into())) + && (cursor < word_ranges[range_i].end + || self + .are_cursors_equivalent(cursor.into(), word_ranges[range_i].end.into())) + { + return Some(TextSelection { + start: word_ranges[range_i].start, + end: word_ranges[range_i].end, + }); + } + } + } + + // NOTE: Since whitespace isn't ignored, word_ranges *should* include everything, but, for + // the sake of robustness, return None instead. + None + } + + /// Get the [`TextCursor`](TextCursor) at the start of line of the provided [`TextCursor`](TextCursor). + /// + /// **Note:** When `as_displayed` is `true` wrapping is taken into account. + /// + /// **Returns [`None`](TextCursor::None) if:** + /// - the provided cursor is invalid. + /// - the provided cursor is [`Empty`](TextCursor::Empty) or [`None`](TextCursor::None). + pub fn cursor_line_start(&self, cursor: TextCursor, as_displayed: bool) -> TextCursor { + if as_displayed { + self.update_layout(); + + let line_i = match self.state().cursor_line_column(cursor) { + Some([line_i, _]) => line_i, + None => return TextCursor::None, + }; + + match self.state().select_line(line_i) { + Some(selection) => selection.start.into(), + None => TextCursor::None, + } + } else { + let cursor = match cursor { + TextCursor::Empty | TextCursor::None => return TextCursor::None, + TextCursor::Position(cursor) => cursor, + }; + + let body = &self.style().text_body; + let mut found_cursor = false; + let mut start_of_line = cursor; + + for span_i in (0..=cursor.span).rev() { + for (byte_i, c) in body.spans[span_i].text.char_indices().rev() { + if byte_i > cursor.byte_s { + continue; + } + + if !found_cursor { + if byte_i == cursor.byte_s { + if c == '\n' && cursor.affinity == TextCursorAffinity::After { + return cursor.into(); + } + + found_cursor = true; + } + } else { + if c == '\n' { + return PosTextCursor { + span: span_i, + byte_s: byte_i, + byte_e: byte_i + c.len_utf8(), + affinity: TextCursorAffinity::After, + } + .into(); + } else { + start_of_line = PosTextCursor { + span: span_i, + byte_s: byte_i, + byte_e: byte_i + c.len_utf8(), + affinity: TextCursorAffinity::Before, + }; + } + } + } + } + + start_of_line.into() + } + } + + /// Get the [`TextCursor`](TextCursor) at the end of line of the provided [`TextCursor`](TextCursor). + /// + /// **Note:** When `as_displayed` is `true` wrapping is taken into account. + /// + /// **Returns [`None`](TextCursor::None) if:** + /// - the provided cursor is invalid. + /// - the provided cursor is [`Empty`](TextCursor::Empty) or [`None`](TextCursor::None). + pub fn cursor_line_end(&self, cursor: TextCursor, as_displayed: bool) -> TextCursor { + if as_displayed { + self.update_layout(); + + let line_i = match self.state().cursor_line_column(cursor) { + Some([line_i, _]) => line_i, + None => return TextCursor::None, + }; + + match self.state().select_line(line_i) { + Some(selection) => selection.end.into(), + None => TextCursor::None, + } + } else { + let cursor = match cursor { + TextCursor::Empty | TextCursor::None => return TextCursor::None, + TextCursor::Position(cursor) => cursor, + }; + + let body = &self.style().text_body; + let mut found_cursor = false; + let mut end_of_line = cursor; + + for span_i in cursor.span..body.spans.len() { + for (byte_i, c) in body.spans[span_i].text.char_indices() { + if byte_i < cursor.byte_s { + continue; + } + + if !found_cursor { + if byte_i == cursor.byte_s { + if c == '\n' && cursor.affinity == TextCursorAffinity::Before { + return cursor.into(); + } + + found_cursor = true; + } + } else { + if c == '\n' { + return PosTextCursor { + span: span_i, + byte_s: byte_i, + byte_e: byte_i + c.len_utf8(), + affinity: TextCursorAffinity::Before, + } + .into(); + } else { + end_of_line = PosTextCursor { + span: span_i, + byte_s: byte_i, + byte_e: byte_i + c.len_utf8(), + affinity: TextCursorAffinity::After, + }; + } + } + } + } + + end_of_line.into() + } + } + + /// Get the [`TextSelection`](TextSelection) of the line that provided cursor is on. + /// + /// **Note:** When `as_displayed` is `true` wrapping is taken into account. + /// + /// **Returns `None` if:** + /// - the provided cursor is invalid. + /// - the provided cursor is [`Empty`](TextCursor::Empty) or [`None`](TextCursor::None). + pub fn cursor_select_line( + &self, + cursor: TextCursor, + as_displayed: bool, + ) -> Option { + if as_displayed { + self.state().cursor_select_line(cursor) + } else { + let start = match self.cursor_line_start(cursor, false) { + TextCursor::None => return None, + TextCursor::Empty => unreachable!(), + TextCursor::Position(cursor) => cursor, + }; + + let end = match self.cursor_line_end(cursor, false) { + TextCursor::None => return None, + TextCursor::Empty => unreachable!(), + TextCursor::Position(cursor) => cursor, + }; + + Some(TextSelection { + start, + end, + }) + } + } + + /// Get the [`TextCursor`](`TextCursor`) at the start of the span that the provided [`TextCursor`](`TextCursor`) is in. + /// + /// **Returns [`None`](TextCursor::None) if:** + /// - the provided cursor is invalid. + /// - the provided cursor is [`None`](TextCursor::None) or [`Empty`](TextCursor::Empty) + pub fn cursor_span_start(&self, cursor: TextCursor) -> TextCursor { + let body = &self.style().text_body; + + if !self.is_cursor_valid(cursor) { + return TextCursor::None; + } + + let cursor = match cursor { + TextCursor::Empty | TextCursor::None => return TextCursor::None, + TextCursor::Position(cursor) => cursor, + }; + + let byte_e = match body.spans[cursor.span].text.chars().next() { + Some(c) => c.len_utf8(), + None => { + // Note: is_cursor_valid ensures that byte_e <= byte_s, therefore; there should + // be at least one character in this span. + unreachable!() + }, + }; + + PosTextCursor { + span: cursor.span, + byte_s: 0, + byte_e, + affinity: TextCursorAffinity::Before, + } + .into() + } + + /// Get the [`TextCursor`](`TextCursor`) at the end of the span that the provided [`TextCursor`](`TextCursor`) is in. + /// + /// **Returns [`None`](TextCursor::None) if:** + /// - the provided cursor is invalid. + /// - the provided cursor is [`None`](TextCursor::None) or [`Empty`](TextCursor::Empty) + pub fn cursor_span_end(&self, cursor: TextCursor) -> TextCursor { + let body = &self.style().text_body; + + if !self.is_cursor_valid(cursor) { + return TextCursor::None; + } + + let cursor = match cursor { + TextCursor::Empty | TextCursor::None => return TextCursor::None, + TextCursor::Position(cursor) => cursor, + }; + + let [byte_s, byte_e] = match body.spans[cursor.span].text.char_indices().rev().next() { + Some((byte_s, c)) => [byte_s, byte_s + c.len_utf8()], + None => { + // Note: is_cursor_valid ensures that byte_e <= byte_s, therefore; there should + // be at least one character in this span. + unreachable!() + }, + }; + + PosTextCursor { + span: cursor.span, + byte_s, + byte_e, + affinity: TextCursorAffinity::Before, + } + .into() + } + + /// Get a [`TextSelection`][TextSelection] of the span that the provided [`TextCursor`](TextCursor) is in. + /// + /// **Returns `None` if:** + /// - the provided cursor is invalid. + /// - the provided cursor is [`None`](TextCursor::None) or [`Empty`](TextCursor::Empty) + pub fn cursor_select_span(&self, cursor: TextCursor) -> Option { + let start = match self.cursor_span_start(cursor) { + TextCursor::Empty | TextCursor::None => return None, + TextCursor::Position(cursor) => cursor, + }; + + let end = match self.cursor_span_end(cursor) { + TextCursor::Empty | TextCursor::None => return None, + TextCursor::Position(cursor) => cursor, + }; + + Some(TextSelection { + start, + end, + }) + } + + /// Get the [`TextCursor`](TextCursor) at the start of line with the provided index. + /// + /// **Note:** When `as_displayed` is `true` wrapping is taken into account. + /// + /// **Returns [`None`](TextCursor::None) if:** + /// - the text body is empty. + /// - the line index is invalid. + pub fn line_start(&self, line_i: usize, as_displayed: bool) -> TextCursor { + if as_displayed { + self.update_layout(); + + match self.state().select_line(line_i) { + Some(selection) => selection.start.into(), + None => TextCursor::None, + } + } else { + let body = &self.style().text_body; + let mut cur_line_i = 0; + + for (span_i, span) in body.spans.iter().enumerate() { + for (byte_i, c) in span.text.char_indices() { + if line_i == 0 { + return PosTextCursor { + span: span_i, + byte_s: byte_i, + byte_e: byte_i + c.len_utf8(), + affinity: TextCursorAffinity::Before, + } + .into(); + } + + if c == '\n' { + cur_line_i += 1; + + if cur_line_i == line_i { + return PosTextCursor { + span: span_i, + byte_s: byte_i, + byte_e: byte_i + c.len_utf8(), + affinity: TextCursorAffinity::After, + } + .into(); + } + } + } + } + + TextCursor::None + } + } + + /// Get the [`TextCursor`](TextCursor) at the end of line with the provided index. + /// + /// **Note:** When `as_displayed` is `true` wrapping is taken into account. + /// + /// **Returns [`None`](TextCursor::None) if:** + /// - the text body is empty. + /// - the line index is invalid. + pub fn line_end(&self, line_i: usize, as_displayed: bool) -> TextCursor { + if as_displayed { + self.update_layout(); + + match self.state().select_line(line_i) { + Some(selection) => selection.end.into(), + None => TextCursor::None, + } + } else { + let body = &self.style().text_body; + let mut cur_line_i = 0; + + for (span_i, span) in body.spans.iter().enumerate() { + for (byte_i, c) in span.text.char_indices() { + if c == '\n' { + if cur_line_i == line_i { + return PosTextCursor { + span: span_i, + byte_s: byte_i, + byte_e: byte_i + c.len_utf8(), + affinity: TextCursorAffinity::Before, + } + .into(); + } + + cur_line_i += 1; + } + } + } + + TextCursor::None + } + } + + /// Obtain the bounding box of the displayed line with the provided index. + /// + /// Format: `[MIN_X, MAX_X, MIN_Y, MAX_Y]`. + /// + /// **Returns `None` if:** + /// - the line index is invalid. + pub fn line_bounds(&self, line_i: usize) -> Option<[f32; 4]> { + self.update_layout(); + let tlwh = self.tlwh(); + self.state().line_bounds(tlwh, line_i) + } + + /// Count the number of lines within the [`TextBody`](TextBody). + /// + /// **Returns `None` if:** + /// - the text body is empty. + pub fn line_count(&self, as_displayed: bool) -> Option { + if as_displayed { + self.update_layout(); + self.state().line_count() + } else { + if self.is_empty() { + return None; + } + + let body = &self.style().text_body; + let mut count = 1; + + for span in body.spans.iter() { + for c in span.text.chars() { + if c == '\n' { + count += 1; + } + } + } + + Some(count) + } + } + + /// Count the number of columns of the line with the provided index. + /// + /// **Returns `None` if:** + /// - the text body is empty. + /// - the provided line index is invalid. + pub fn line_column_count(&self, line_i: usize, as_displayed: bool) -> Option { + if as_displayed { + self.update_layout(); + self.state().line_column_count(line_i) + } else { + let body = &self.style().text_body; + let mut cur_line_i = 0; + let mut count = 0; + + for span in body.spans.iter() { + for c in span.text.chars() { + if c == '\n' { + if cur_line_i == line_i { + break; + } else { + cur_line_i += 1; + } + } else if cur_line_i == line_i { + count += 1; + } + } + } + + Some(count) + } + } + + /// Obtain a [`TextCursor`](TextCursor) given a line and column index. + /// + /// **Note:** When `as_displayed` is `true` wrapping is taken into account. + /// + /// **Returns [`None`](TextCursor::None) if:** + /// - the text body is empty. + /// - the line or column index is invalid. + pub fn line_column_cursor( + &self, + line_i: usize, + col_i: usize, + as_displayed: bool, + ) -> TextCursor { + if as_displayed { + self.update_layout(); + self.state().line_column_cursor(line_i, col_i) + } else { + if self.is_empty() { + return TextCursor::None; + } + + let body = &self.style().text_body; + let mut cur_line_i = 0; + let mut cur_col_i = 0; + let mut fallback_op = None; + + for (span_i, span) in body.spans.iter().enumerate() { + for (byte_i, c) in span.text.char_indices() { + if cur_line_i == line_i && cur_col_i == col_i { + return PosTextCursor { + span: span_i, + byte_s: byte_i, + byte_e: byte_i + c.len_utf8(), + affinity: TextCursorAffinity::Before, + } + .into(); + } + + if c == '\n' { + cur_line_i += 1; + cur_col_i = 0; + } else { + cur_col_i += 1; + } + + if cur_line_i == line_i && cur_col_i == col_i { + fallback_op = Some(PosTextCursor { + span: span_i, + byte_s: byte_i, + byte_e: byte_i + c.len_utf8(), + affinity: TextCursorAffinity::After, + }); + } + } + } + + fallback_op + .map(|cursor| cursor.into()) + .unwrap_or(TextCursor::None) + } + } + + /// Get the [`TextCursor`](`TextCursor`) at the start of the span with the provided index. + /// + /// **Returns [`None`](TextCursor::None) if:** + /// - the provided span index is invalid. + pub fn span_start(&self, span_i: usize) -> TextCursor { + let body = &self.style().text_body; + + if span_i >= body.spans.len() { + return TextCursor::None; + } + + match body.spans[span_i].text.char_indices().next() { + Some((byte_i, c)) => { + PosTextCursor { + span: span_i, + byte_s: byte_i, + byte_e: byte_i + c.len_utf8(), + affinity: TextCursorAffinity::Before, + } + .into() + }, + None => TextCursor::None, + } + } + + /// Get the [`TextCursor`](`TextCursor`) at the end of the span with the provided index. + /// + /// **Returns [`None`](TextCursor::None) if:** + /// - the provided span index is invalid. + pub fn span_end(&self, span_i: usize) -> TextCursor { + let body = &self.style().text_body; + + if span_i >= body.spans.len() { + return TextCursor::None; + } + + match body.spans[span_i].text.char_indices().rev().next() { + Some((byte_i, c)) => { + PosTextCursor { + span: span_i, + byte_s: byte_i, + byte_e: byte_i + c.len_utf8(), + affinity: TextCursorAffinity::After, + } + .into() + }, + None => TextCursor::None, + } + } + + /// Count the total number of spans. + pub fn span_count(&self) -> usize { + self.style().text_body.spans.len() + } + + /// Set the [`TextAttrs`] of the span with the provided index. + /// + /// `mask` controls which attributes are set from the provided attrs. + /// + /// If `consolidate` is `true`, spans with the same attributes will be merged. + /// + /// If `preserve_cursors` is `true`, the current cursor & selection will be kept valid. + /// + /// **Note:** This is a no-op if that span's index is invalid. + pub fn span_apply_attrs( + &self, + span_i: usize, + attrs: &TextAttrs, + mask: TextAttrsMask, + consolidate: bool, + preserve_cursors: bool, + ) { + if span_i >= self.style().text_body.spans.len() { + return; + } + + let preserve_cursors = PreserveCursors::new(self, preserve_cursors); + self.cursors_invalidated(); + + { + let body = &mut self.style_mut().text_body; + mask.apply(attrs, &mut body.spans[span_i].attrs); + } + + if consolidate { + self.spans_consolidate(); + } + + preserve_cursors.restore(self); + } + + /// Consolidate spans that share [`TextAttrs`]. + pub fn spans_consolidate(&self) { + if self.style().text_body.spans.len() < 2 { + return; + } + + let body = &mut self.style_mut().text_body; + + for span_i in (1..body.spans.len()).rev() { + if body.spans[span_i - 1].attrs == body.spans[span_i].attrs { + let span = body.spans.remove(span_i); + body.spans[span_i - 1].text.push_str(&span.text); + } + } + } + + /// Obtain the current displayed [`TextSelection`](TextSelection). + pub fn selection(&self) -> Option { + self.style().text_body.selection + } + + /// Set the displayed [`TextSelection`](TextSelection). + pub fn set_selection(&self, selection: TextSelection) { + self.style_mut().text_body.selection = Some(selection); + } + + /// Clear the displayed [`TextSelection`](TextSelection). + pub fn clear_selection(&self) { + self.style_mut().text_body.selection = None; + } + + /// Set the [`Color`](Color) of the displayed [`TextSelection`](TextSelection). + pub fn set_selection_color(&self, color: Color) { + self.style_mut().text_body.selection_color = color; + } + + /// Get the [`TextSelection`](TextSelection) of the line with the provided index. + /// + /// **Note:** When `as_displayed` is `true` wrapping is taken into account. + pub fn select_line(&self, line_i: usize, as_displayed: bool) -> Option { + if as_displayed { + self.update_layout(); + self.state().select_line(line_i) + } else { + let start = match self.line_start(line_i, false) { + TextCursor::None => return None, + TextCursor::Empty => unreachable!(), + TextCursor::Position(cursor) => cursor, + }; + + let end = match self.line_end(line_i, false) { + TextCursor::None => return None, + TextCursor::Empty => unreachable!(), + TextCursor::Position(cursor) => cursor, + }; + + Some(TextSelection { + start, + end, + }) + } + } + + /// Get the [`TextSelection`](TextSelection) of the [`TextSpan`](TextSpan) with the provided index. + /// + /// **Returns `None` if:** + /// - the provided span index is invalid. + pub fn select_span(&self, span_i: usize) -> Option { + let start = match self.span_start(span_i) { + TextCursor::None => return None, + TextCursor::Empty => unreachable!(), + TextCursor::Position(cursor) => cursor, + }; + + let end = match self.span_end(span_i) { + TextCursor::None => return None, + TextCursor::Empty => unreachable!(), + TextCursor::Position(cursor) => cursor, + }; + + Some(TextSelection { + start, + end, + }) + } + + /// Get the [`TextSelection`](TextSelection) of the whole [`TextBody`](TextBody). + /// + /// **Returns `None` if:** + /// - the text body is empty. + pub fn select_all(&self) -> Option { + let span_count = self.span_count(); + + if span_count == 0 { + return None; + } + + let start = match self.span_start(0) { + TextCursor::None => return None, + TextCursor::Empty => unreachable!(), + TextCursor::Position(cursor) => cursor, + }; + + let end = match self.span_end(span_count - 1) { + TextCursor::None => return None, + TextCursor::Empty => unreachable!(), + TextCursor::Position(cursor) => cursor, + }; + + Some(TextSelection { + start, + end, + }) + } + + /// Obtain the selection's value as `String`. + /// + /// **Returns an empty `String` if:** + /// - The provided [`TextSelection`](TextSelection) is invalid. + pub fn selection_string(&self, selection: TextSelection) -> String { + self.selection_spans(selection) + .into_iter() + .map(|span| span.text) + .collect() + } + + /// Set the [`TextAttrs`] of the provided [`TextSelection`]. + /// + /// `mask` controls which attributes are set from the provided attrs. + /// + /// If `consolidate` is `true`, spans with the same attributes will be merged. + /// + /// If `preserve_cursors` is `true`, the current cursor & selection will be kept valid. + /// + /// **Note:** This is a no-op if the provided [`TextSelection`] is invalid. + pub fn selection_apply_attrs( + &self, + selection: TextSelection, + attrs: &TextAttrs, + mask: TextAttrsMask, + consolidate: bool, + preserve_cursors: bool, + ) { + if !self.is_selection_valid(selection) { + return; + } + + let preserve_cursors = PreserveCursors::new(self, preserve_cursors); + self.cursors_invalidated(); + + { + let body = &mut self.style_mut().text_body; + + for span_i in (selection.start.span..=selection.end.span).rev() { + let mut new_attrs = body.spans[span_i].attrs.clone(); + mask.apply(attrs, &mut new_attrs); + + if new_attrs == body.spans[span_i].attrs { + continue; + } + + if selection.start.span == selection.end.span { + if selection.start.byte_s == 0 + && selection.end.byte_e == body.spans[span_i].text.len() + { + mask.apply(attrs, &mut body.spans[span_i].attrs); + } else { + if selection.start.byte_s == 0 { + let text = body.spans[span_i].text.split_off(selection.end.byte_s); + + body.spans.insert( + span_i + 1, + TextSpan { + text, + attrs: new_attrs, + ..Default::default() + }, + ); + } else if selection.end.byte_e == body.spans[span_i].text.len() { + let text = body.spans[span_i].text.split_off(selection.end.byte_s); + let attrs = body.spans[span_i].attrs.clone(); + + body.spans.insert( + span_i + 1, + TextSpan { + text, + attrs, + ..Default::default() + }, + ); + + body.spans[span_i].attrs = new_attrs; + } else { + let t_before = + body.spans[span_i].text.split_off(selection.start.byte_s); + + let t_after = body.spans[span_i] + .text + .split_off(selection.end.byte_s - selection.start.byte_s); + + let attrs = body.spans[span_i].attrs.clone(); + + body.spans.insert( + span_i, + TextSpan { + text: t_before, + attrs: attrs.clone(), + ..Default::default() + }, + ); + + body.spans.insert( + span_i + 2, + TextSpan { + text: t_after, + attrs, + ..Default::default() + }, + ); + + body.spans[span_i + 1].attrs = new_attrs; + } + } + } else if span_i == selection.start.span { + if selection.start.byte_s == 0 { + mask.apply(attrs, &mut body.spans[span_i].attrs); + } else { + let text = body.spans[span_i].text.split_off(selection.start.byte_s); + + body.spans.insert( + span_i + 1, + TextSpan { + text, + attrs: new_attrs, + ..Default::default() + }, + ); + } + } else if span_i == selection.end.span { + if selection.end.byte_e == body.spans[span_i].text.len() { + mask.apply(attrs, &mut body.spans[span_i].attrs); + } else { + let text = body.spans[span_i].text.split_off(selection.end.byte_s); + let attrs = body.spans[span_i].attrs.clone(); + + body.spans.insert( + span_i + 1, + TextSpan { + text, + attrs, + ..Default::default() + }, + ); + + body.spans[span_i].attrs = new_attrs; + } + } + } + } + + if consolidate { + self.spans_consolidate(); + } + + preserve_cursors.restore(self); + } + + /// Obtain the selection's value as [`Vec`](TextSpan). + /// + /// **Returns an empty `Vec` if:** + /// - The provided [`TextSelection`](TextSelection) is invalid. + pub fn selection_spans(&self, selection: TextSelection) -> Vec { + let body = &self.style().text_body; + + let [s_span, s_byte, e_span, e_byte] = match self.selection_byte_range(selection) { + Some(some) => some, + None => return Vec::new(), + }; + + let mut spans = Vec::with_capacity(e_span - s_span + 1); + + for span_i in s_span..=e_span { + let span_s_byte_op = if span_i == s_span { + if s_byte == 0 { None } else { Some(s_byte) } + } else { + None + }; + + let mut span_e_byte_op = if span_i == e_span { + if e_byte == body.spans[span_i].text.len() { + None + } else { + Some(e_byte) + } + } else { + None + }; + + let mut sel_str = match span_s_byte_op { + Some(span_s_byte) => { + if let Some(span_e_byte) = span_e_byte_op.as_mut() { + *span_e_byte -= span_s_byte; + } + + body.spans[span_i].text.split_at(span_s_byte).1 + }, + None => body.spans[span_i].text.as_str(), + }; + + if let Some(span_e_byte) = span_e_byte_op { + sel_str = sel_str.split_at(span_e_byte).0; + } + + spans.push(TextSpan { + attrs: body.spans[span_i].attrs.clone(), + text: sel_str.into(), + ..Default::default() + }); + } + + spans + } + + /// Take the selection out of the [`TextBody`](TextBody) returning the value as `String`. + /// + /// **Note:** The returned [`TextCursor`](TextCursor) behaves the same as + /// [`selection_delete`](`TextBodyGuard::selection_delete`) + /// + /// **The returned `String` will be empty if:** + /// - the provided selection is invalid. + pub fn selection_take_string(&self, selection: TextSelection) -> (TextCursor, String) { + let (cursor, spans) = self.selection_take_spans(selection); + (cursor, spans.into_iter().map(|span| span.text).collect()) + } + + /// Take the selection out of the [`TextBody`](TextBody) returning the value as [`Vec`](TextSpan). + /// + /// **Note:** The returned [`TextCursor`](TextCursor) behaves the same as + /// [`selection_delete`](`TextBodyGuard::selection_delete`) + /// + /// **The returned [`Vec`](TextSpan) will be empty if:** + /// - the provided selection is invalid. + pub fn selection_take_spans(&self, selection: TextSelection) -> (TextCursor, Vec) { + let mut spans = Vec::with_capacity(selection.end.span - selection.start.span + 1); + let cursor = self.inner_selection_delete(selection, Some(&mut spans)); + (cursor, spans) + } + + /// Delete the provided ['TextSelection`](TextSelection). + /// + /// **Note:** If deleletion empties any span within the selection, the span will be removed. + /// + /// **Returns [`None`](`TextCursor::None`) if:** + /// - the provided selection is invalid. + /// + /// **Returns [`Empty`](`TextCursor::Empty`) if:** + /// - the [`TextBody`](TextBody) is empty after the deletion. + pub fn selection_delete(&self, selection: TextSelection) -> TextCursor { + self.inner_selection_delete(selection, None) + } + + fn inner_selection_delete( + &self, + selection: TextSelection, + mut spans_op: Option<&mut Vec>, + ) -> TextCursor { + if !self.is_selection_valid(selection) { + return TextCursor::None; + } + + let [s_span, s_byte, e_span, e_byte] = match self.selection_byte_range(selection) { + Some(some) => some, + None => return TextCursor::None, + }; + + let mut ret_cursor = match selection.start.affinity { + TextCursorAffinity::Before => { + match self.cursor_prev(selection.start.into()) { + TextCursor::None => TextCursor::None, + TextCursor::Empty => unreachable!(), + TextCursor::Position(mut cursor) => { + cursor.affinity = TextCursorAffinity::After; + cursor.into() + }, + } + }, + TextCursorAffinity::After => selection.start.into(), + }; + + { + let body = &mut self.style_mut().text_body; + let mut remove_spans = Vec::new(); + + for span_i in s_span..=e_span { + let span_s_byte = if span_i == s_span { s_byte } else { 0 }; + + let span_e_byte = if span_i == e_span { + e_byte + } else { + body.spans[span_i].text.len() + }; + + let text = body.spans[span_i] + .text + .drain(span_s_byte..span_e_byte) + .collect::(); + + if let Some(spans) = spans_op.as_mut() { + spans.push(TextSpan { + attrs: body.spans[span_i].attrs.clone(), + text, + ..Default::default() + }); + } + + if body.spans[span_i].text.is_empty() { + remove_spans.push(span_i); + } + } + + for span_i in remove_spans.into_iter().rev() { + body.spans.remove(span_i); + } + } + + if ret_cursor == TextCursor::None { + ret_cursor = match self.cursor_next(TextCursor::Empty) { + TextCursor::None | TextCursor::Empty => TextCursor::Empty, + TextCursor::Position(mut cursor) => { + cursor.affinity = TextCursorAffinity::Before; + cursor.into() + }, + }; + } + + ret_cursor + } + + fn selection_byte_range(&self, selection: TextSelection) -> Option<[usize; 4]> { + if !self.is_selection_valid(selection) { + return None; + } + + let [start_span, start_b] = match selection.start.affinity { + TextCursorAffinity::Before => [selection.start.span, selection.start.byte_s], + TextCursorAffinity::After => { + match self.cursor_next(selection.start.into()) { + TextCursor::None => return None, + TextCursor::Empty => unreachable!(), + TextCursor::Position(cursor) => [cursor.span, cursor.byte_s], + } + }, + }; + + let [end_span, end_b] = match selection.end.affinity { + TextCursorAffinity::Before => { + match self.cursor_prev(selection.end.into()) { + TextCursor::None => return None, + TextCursor::Empty => unreachable!(), + TextCursor::Position(cursor) => [cursor.span, cursor.byte_e], + } + }, + TextCursorAffinity::After => [selection.end.span, selection.end.byte_e], + }; + + Some([start_span, start_b, end_span, end_b]) + } + + fn update_layout(&self) { + if self.style().layout_stale { + let window = self.bin.window().expect("no associated window"); + + { + let style = self.style(); + let mut update_ctx = window.shared_update_ctx(); + + let tlwh = self.bin.calc_placement(&mut update_ctx).tlwh; + let padding_t = style.padding_t.px_height([tlwh[2], tlwh[3]]).unwrap_or(0.0); + let padding_b = style.padding_b.px_height([tlwh[2], tlwh[3]]).unwrap_or(0.0); + let padding_l = style.padding_l.px_width([tlwh[2], tlwh[3]]).unwrap_or(0.0); + let padding_r = style.padding_r.px_width([tlwh[2], tlwh[3]]).unwrap_or(0.0); + + let content_tlwh = [ + tlwh[0] + padding_t - style.scroll_y, + tlwh[1] + padding_l - style.scroll_x, + tlwh[2] - padding_r - padding_l, + tlwh[3] - padding_b - padding_t, + ]; + + let image_cache = window.basalt_ref().image_cache_ref(); + + self.state() + .update(content_tlwh, &style.text_body, &mut update_ctx, image_cache); + + *self.tlwh.borrow_mut() = Some(content_tlwh); + *self.default_font.borrow_mut() = Some(update_ctx.default_font.clone()); + } + + self.style_mut().layout_stale = false; + } + } + + /// Inspect the inner [`TextBody`](TextBody) with the provided method. + /// + /// **Warning:** A deadlock may occur if modifications are made to this + /// [`TextBodyGuard`](TextBodyGuard) or the parent [`Bin`](Bin) with the provided method. + pub fn inspect(&self, inspect: I) -> T + where + I: FnOnce(&TextBody) -> T, + { + inspect(&self.style().text_body) + } + + /// Modify the inner [`TextBody`](TextBody) with the provided method. + /// + /// **Warning:** A deadlock may occur if modifications are made to this + /// [`TextBodyGuard`](TextBodyGuard) or the parent [`Bin`](Bin) with the provided method. + pub fn modify(&self, modify: M) -> T + where + M: FnOnce(&mut TextBody) -> T, + { + modify(&mut self.style_mut().text_body) + } + + /// Modify the [`TextBody`](TextBody) parent ['BinStyle`](BinStyle). + /// + /// **Warning:** A deadlock may occur if modifications are made to this + /// [`TextBodyGuard`](TextBodyGuard) or the parent [`Bin`](Bin) with the provided method. + #[track_caller] + pub fn style_modify(&self, modify: M) -> Result + where + M: FnOnce(&mut BinStyle) -> T, + { + let mut style = self.style().clone(); + let user_ret = modify(&mut style); + let validation = style.validate(self.bin); + + if validation.errors_present() { + return Err(validation); + } + + **self.style_mut() = style; + Ok(user_ret) + } + + /// Modify the [`TextBody`](TextBody) parent ['BinStyle`](BinStyle). + /// + /// **Warning:** A deadlock may occur if modifications are made to this + /// [`TextBodyGuard`](TextBodyGuard) or the parent [`Bin`](Bin) with the provided method. + pub fn style_inspect(&self, inspect: I) -> T + where + I: FnOnce(&BinStyle) -> T, + { + inspect(&self.style()) + } + + /// This is equivlent to [`Bin::on_update_once`](Bin::on_update_once). + /// + /// Useful when having an up-to-date [`BinPostUpdate`](BinPostUpdate) is needed. + /// + /// Method is called at the end of a ui update cycle when everything is up-to-date. + /// + /// **Note:** If no modifications are made, the provided method won't be called. + pub fn bin_on_update(&self, updated: U) + where + U: FnOnce(&Arc, &BinPostUpdate) + Send + 'static, + { + self.on_update.borrow_mut().push(Box::new(updated)); + } + + /// Finish modifications. + /// + /// **Note:** This is automatically called when [`TextBodyGuard`](TextBodyGuard) is dropped. + #[track_caller] + pub fn finish(self) { + self.finish_inner(); + } + + #[track_caller] + fn finish_inner(&self) { + let StyleState { + guard: style_guard, + modified: modified_style_op, + .. + } = match self.style_state.borrow_mut().take() { + Some(style_state) => style_state, + None => return, + }; + + let modified_style = match modified_style_op { + Some(modified_style) => modified_style, + None => return, + }; + + modified_style.validate(self.bin).expect_valid(); + let mut effects_siblings = modified_style.position == Position::Floating; + let mut old_style = Arc::new(modified_style); + + { + let mut style_guard = RwLockUpgradableReadGuard::upgrade(style_guard); + std::mem::swap(&mut *style_guard, &mut old_style); + effects_siblings |= old_style.position == Position::Floating; + } + + { + let mut internal_hooks = self.bin.internal_hooks.lock(); + + let on_update_once = internal_hooks + .get_mut(&InternalHookTy::UpdatedOnce) + .unwrap(); + + for updated in self.on_update.borrow_mut().drain(..) { + on_update_once.push(InternalHookFn::UpdatedOnce(updated)); + } + } + + if effects_siblings && let Some(parent) = self.bin.parent() { + parent.trigger_children_update(); + } else { + self.bin.trigger_recursive_update(); + } + } + + fn cursors_invalidated(&self) { + if matches!(self.cursor(), TextCursor::Position(..)) { + self.set_cursor(TextCursor::None); + } + + self.clear_selection(); + } + + pub(crate) fn new(bin: &'a Arc) -> Self { + Self { + bin, + text_state: RefCell::new(None), + style_state: RefCell::new(None), + tlwh: RefCell::new(None), + default_font: RefCell::new(None), + on_update: RefCell::new(Vec::new()), + } + } + + #[track_caller] + fn state<'b>(&'b self) -> SomeRefMut<'b, TextStateGuard<'a>> { + if self.text_state.borrow().is_none() { + *self.text_state.borrow_mut() = Some(TextStateGuard { + inner: self.bin.update_state.lock(), + }); + } + + SomeRefMut { + inner: self.text_state.borrow_mut(), + } + } + + #[track_caller] + fn style(&self) -> SomeRef<'_, StyleState<'_>> { + if self.style_state.borrow().is_none() { + *self.style_state.borrow_mut() = Some(StyleState { + guard: self.bin.style.upgradable_read(), + modified: None, + layout_stale: false, + }); + } + + SomeRef { + inner: self.style_state.borrow(), + } + } + + #[track_caller] + fn style_mut<'b>(&'b self) -> SomeRefMut<'b, StyleState<'a>> { + if self.style_state.borrow().is_none() { + *self.style_state.borrow_mut() = Some(StyleState { + guard: self.bin.style.upgradable_read(), + modified: None, + layout_stale: true, + }); + } + + let mut style_state = self.style_state.borrow_mut(); + style_state.as_mut().unwrap().layout_stale = true; + + SomeRefMut { + inner: style_state, + } + } + + fn tlwh(&self) -> [f32; 4] { + if self.tlwh.borrow().is_none() { + let bpu = self.bin.post_update.read_recursive(); + + *self.tlwh.borrow_mut() = Some([ + bpu.optimal_content_bounds[2] + bpu.content_offset[1], + bpu.optimal_content_bounds[0] + bpu.content_offset[0], + bpu.optimal_content_bounds[1] - bpu.optimal_content_bounds[0], + bpu.optimal_content_bounds[3] - bpu.optimal_content_bounds[2], + ]); + } + + self.tlwh.borrow().unwrap() + } + + fn default_font(&self) -> SomeRef<'_, DefaultFont> { + if self.default_font.borrow().is_none() { + *self.default_font.borrow_mut() = + Some(self.bin.basalt_ref().interface_ref().default_font()); + } + + SomeRef { + inner: self.default_font.borrow(), + } + } +} + +impl<'a> Drop for TextBodyGuard<'a> { + #[track_caller] + fn drop(&mut self) { + self.finish_inner(); + } +} + +struct SomeRef<'a, T: Sized + 'a> { + inner: Ref<'a, Option>, +} + +impl Deref for SomeRef<'_, T> { + type Target = T; + + fn deref(&self) -> &T { + (*self.inner).as_ref().unwrap() + } +} + +struct SomeRefMut<'a, T: Sized + 'a> { + inner: RefMut<'a, Option>, +} + +impl Deref for SomeRefMut<'_, T> { + type Target = T; + + fn deref(&self) -> &T { + (*self.inner).as_ref().unwrap() + } +} + +impl DerefMut for SomeRefMut<'_, T> { + fn deref_mut(&mut self) -> &mut T { + (*self.inner).as_mut().unwrap() + } +} + +struct StyleState<'a> { + guard: RwLockUpgradableReadGuard<'a, Arc>, + modified: Option, + layout_stale: bool, +} + +impl Deref for StyleState<'_> { + type Target = BinStyle; + + fn deref(&self) -> &BinStyle { + if let Some(modified) = self.modified.as_ref() { + return modified; + } + + &**self.guard + } +} + +impl DerefMut for StyleState<'_> { + fn deref_mut(&mut self) -> &mut BinStyle { + if self.modified.is_none() { + self.modified = Some((**self.guard).clone()); + } + + self.modified.as_mut().unwrap() + } +} + +struct TextStateGuard<'a> { + inner: MutexGuard<'a, UpdateState>, +} + +impl Deref for TextStateGuard<'_> { + type Target = TextState; + + fn deref(&self) -> &TextState { + &self.inner.text + } +} + +impl DerefMut for TextStateGuard<'_> { + fn deref_mut(&mut self) -> &mut TextState { + &mut self.inner.text + } +} + +struct WordRange { + start: PosTextCursor, + end: PosTextCursor, + is_whitespace: bool, +} + +// TODO: In the future this should be removed and replaced with some means of lazily checking words. +// Checking the entire body's words is unnecessary and slow. +fn word_ranges(body: &TextBody, ignore_whitespace: bool) -> Vec { + let mut text = String::new(); + let mut span_ranges = Vec::new(); + + for span in body.spans.iter() { + let start = text.len(); + text.push_str(span.text.as_str()); + span_ranges.push(start..text.len()); + } + + let mut word_ranges = Vec::new(); + + for (word_offset, word) in text.split_word_bound_indices() { + let is_whitespace = word.chars().all(|c| c.is_whitespace()); + + if ignore_whitespace && is_whitespace { + continue; + } + + let (s_span_i, s_span_o) = span_ranges + .iter() + .enumerate() + .find_map(|(span_i, range)| { + if range.contains(&word_offset) { + Some((span_i, range.start)) + } else { + None + } + }) + .unwrap(); + + let (e_span_i, e_span_o) = span_ranges + .iter() + .enumerate() + .find_map(|(span_i, range)| { + if range.contains(&word_offset) { + Some((span_i, range.start)) + } else { + None + } + }) + .unwrap(); + + let s_byte_s = word_offset - s_span_o; + let s_byte_e = s_byte_s + word.chars().next().unwrap().len_utf8(); + let e_byte_e = (word_offset + word.len()) - e_span_o; + let e_byte_s = e_byte_e - word.chars().rev().next().unwrap().len_utf8(); + + word_ranges.push(WordRange { + start: PosTextCursor { + span: s_span_i, + byte_s: s_byte_s, + byte_e: s_byte_e, + affinity: TextCursorAffinity::Before, + }, + end: PosTextCursor { + span: e_span_i, + byte_s: e_byte_s, + byte_e: e_byte_e, + affinity: TextCursorAffinity::After, + }, + is_whitespace, + }); + } + + word_ranges +} + +struct PreserveCursors { + cursor_lc: Option<[usize; 2]>, + selection_lc: Option<[usize; 4]>, +} + +impl PreserveCursors { + fn new(tbg: &TextBodyGuard, preserve: bool) -> Self { + if preserve { + let cursor_lc = match tbg.cursor() { + TextCursor::Empty | TextCursor::None => None, + TextCursor::Position(cursor) => { + if tbg.is_cursor_valid(cursor) { + Some(tbg.cursor_line_column(cursor.into(), false).unwrap()) + } else { + tbg.set_cursor(TextCursor::None); + None + } + }, + }; + + let selection_lc = match tbg.selection() { + Some(selection) => { + if tbg.is_selection_valid(selection) { + let [s_line_i, s_col_i] = tbg + .cursor_line_column(selection.start.into(), false) + .unwrap(); + let [e_line_i, e_col_i] = + tbg.cursor_line_column(selection.end.into(), false).unwrap(); + Some([s_line_i, s_col_i, e_line_i, e_col_i]) + } else { + tbg.clear_selection(); + None + } + }, + None => None, + }; + + Self { + cursor_lc, + selection_lc, + } + } else { + Self { + cursor_lc: None, + selection_lc: None, + } + } + } + + fn restore(self, tbg: &TextBodyGuard) { + if let Some([line_i, col_i]) = self.cursor_lc { + tbg.set_cursor(tbg.line_column_cursor(line_i, col_i, false)); + } + + if let Some([s_line_i, s_col_i, e_line_i, e_col_i]) = self.selection_lc { + tbg.set_selection(TextSelection { + start: tbg + .line_column_cursor(s_line_i, s_col_i, false) + .into_position() + .unwrap(), + end: tbg + .line_column_cursor(e_line_i, e_col_i, false) + .into_position() + .unwrap(), + }); + } + } +} diff --git a/src/interface/bin/text_state.rs b/src/interface/bin/text_state.rs index 322c718c..85da21bc 100644 --- a/src/interface/bin/text_state.rs +++ b/src/interface/bin/text_state.rs @@ -1,3 +1,5 @@ +use std::cmp::Ordering; +use std::collections::BTreeMap; use std::ops::Range; use std::sync::Arc; @@ -8,7 +10,8 @@ use crate::image::{ }; use crate::interface::{ Color, FontFamily, FontStretch, FontStyle, FontWeight, ItfVertInfo, LineLimit, LineSpacing, - TextBody, TextHoriAlign, TextVertAlign, TextWrap, UnitValue, UpdateContext, + PosTextCursor, TextBody, TextCursor, TextCursorAffinity, TextHoriAlign, TextSelection, + TextVertAlign, TextWrap, UnitValue, UpdateContext, }; pub struct TextState { @@ -32,7 +35,7 @@ struct Layout { } impl Layout { - fn cosmic_attrs(&self) -> ct::Attrs { + fn cosmic_attrs(&self) -> ct::Attrs<'_> { ct::Attrs { color_opt: None, family: ct::Family::Serif, @@ -61,7 +64,13 @@ struct Span { } impl Span { - fn cosmic_attrs(&self, metadata: usize) -> ct::Attrs { + fn cosmic_attrs(&self, metadata: usize) -> ct::Attrs<'_> { + let mut font_features = ct::FontFeatures::default(); + + // TODO: Ligatures are disabled as they break selection. + font_features.disable(ct::FeatureTag::STANDARD_LIGATURES); + font_features.disable(ct::FeatureTag::CONTEXTUAL_LIGATURES); + ct::Attrs { color_opt: None, family: self.font_family.as_cosmic().unwrap(), @@ -78,16 +87,16 @@ impl Span { .into(), ), letter_spacing_opt: None, - font_features: Default::default(), + font_features, } } } +#[derive(Debug)] struct LayoutGlyph { span_i: usize, - // NOTE: Used with text editing? - #[allow(dead_code)] - line_i: usize, + byte_s: usize, + byte_e: usize, offset: [f32; 2], extent: [f32; 2], hitbox: [f32; 4], @@ -96,9 +105,14 @@ struct LayoutGlyph { vertex_type: i32, } +#[derive(Debug)] struct LayoutLine { + bounds: [f32; 4], + hitbox: [f32; 4], height: f32, glyphs: Range, + s_cursor: PosTextCursor, + e_cursor: PosTextCursor, } struct GlyphImageData { @@ -121,6 +135,427 @@ impl Default for TextState { } impl TextState { + pub fn get_cursor(&self, cursor_position: [f32; 2]) -> TextCursor { + let layout = match self.layout_op.as_ref() { + Some(layout) => layout, + None => return TextCursor::Empty, + }; + + if layout.lines.is_empty() { + return TextCursor::Empty; + } + + // Find the closest line to the cursor. + + let mut line_i_op = None; + let mut dist = 0.0; + + for (line_i, line) in layout.lines.iter().enumerate() { + // TODO: Use baseline instead of center? + let c = line.hitbox[2] + ((line.hitbox[3] - line.hitbox[2]) / 2.0); + let d = (cursor_position[1] - c).abs(); + + if line_i_op.is_none() { + line_i_op = Some(line_i); + dist = d; + continue; + } + + if d < dist { + line_i_op = Some(line_i); + dist = d; + } + } + + Self::get_cursor_on_line(layout, line_i_op.unwrap(), cursor_position[0]) + } + + fn get_cursor_on_line(layout: &Layout, line_i: usize, cursor_x: f32) -> TextCursor { + if line_i >= layout.lines.len() { + return TextCursor::None; + } + + let line = &layout.lines[line_i]; + let glyphs = &layout.glyphs[line.glyphs.clone()]; + + if glyphs.is_empty() { + return line.e_cursor.into(); + } + + if cursor_x < glyphs.first().unwrap().hitbox[0] { + // Cursor is to the left of the first glyph, use start of line + return line.s_cursor.into(); + } + + if cursor_x > glyphs.last().unwrap().hitbox[1] { + // Cursor is to the right of the last glyph, use end of line + return line.e_cursor.into(); + } + + let mut glyph_i_op = None; + let mut dist = 0.0; + let mut affinity = TextCursorAffinity::Before; + + for (i, glyph) in glyphs.iter().enumerate() { + let c = glyph.hitbox[0] + ((glyph.hitbox[1] - glyph.hitbox[0]) / 2.0); + let d = (cursor_x - c).abs(); + + let a = if cursor_x < c { + TextCursorAffinity::Before + } else { + TextCursorAffinity::After + }; + + if glyph_i_op.is_none() { + glyph_i_op = Some(i); + dist = d; + affinity = a; + continue; + } + + if d < dist { + glyph_i_op = Some(i); + dist = d; + affinity = a; + } + } + + let glyph = &glyphs[glyph_i_op.unwrap()]; + + PosTextCursor { + span: glyph.span_i, + byte_s: glyph.byte_s, + byte_e: glyph.byte_e, + affinity, + } + .into() + } + + pub fn cursor_up(&self, cursor: TextCursor, text_body: &TextBody) -> TextCursor { + self.cursor_line_offset(cursor, text_body, -1) + } + + pub fn cursor_down(&self, cursor: TextCursor, text_body: &TextBody) -> TextCursor { + self.cursor_line_offset(cursor, text_body, 1) + } + + pub fn cursor_line_offset( + &self, + cursor: TextCursor, + text_body: &TextBody, + line_offset: isize, + ) -> TextCursor { + if self.layout_op.is_none() || matches!(cursor, TextCursor::Empty | TextCursor::None) { + return TextCursor::None; + } + + // Note: Since it is known that TextCursor isn't Empty. + // - default_font_height doesn't need to be valid. + // - tlwh can be all zeros. + let ([min_x, max_x, _, _], line_i) = + match self.get_cursor_bounds(cursor, [0.0; 4], text_body, UnitValue::Pixels(0.0)) { + Some(some) => some, + None => return TextCursor::None, + }; + + let cursor_x = ((max_x - min_x) / 2.0) + min_x; + let layout = self.layout_op.as_ref().unwrap(); + + let t_line_i = + (line_i as isize + line_offset).clamp(0, layout.lines.len() as isize - 1) as usize; + + if t_line_i == line_i { + return TextCursor::None; + } + + Self::get_cursor_on_line(layout, t_line_i, cursor_x) + } + + pub fn get_cursor_bounds( + &self, + cursor: TextCursor, + tlwh: [f32; 4], + text_body: &TextBody, + default_font_height: UnitValue, + ) -> Option<([f32; 4], usize)> { + if cursor == TextCursor::None { + return None; + } + + if self.layout_op.is_none() || cursor == TextCursor::Empty { + let text_height = match text_body.base_attrs.height { + UnitValue::Undefined => default_font_height, + body_height => body_height, + } + .px_height([tlwh[2], tlwh[3]]) + .unwrap(); + + let line_height = match text_body.line_spacing { + LineSpacing::HeightMult(mult) => text_height * mult, + LineSpacing::HeightMultAdd(mult, add) => (text_height * mult) + add, + }; + + let [t, b] = match text_body.vert_align { + TextVertAlign::Top => [0.0, line_height], + TextVertAlign::Center => { + let center = tlwh[3] / 2.0; + let half_height = line_height / 2.0; + [center - half_height, center + half_height] + }, + TextVertAlign::Bottom => [tlwh[3] - line_height, tlwh[3]], + }; + + let [l, r] = match text_body.hori_align { + TextHoriAlign::Left => [0.0, 1.0], + TextHoriAlign::Center => { + let center = tlwh[2] / 2.0; + [center - 0.5, center + 0.5] + }, + TextHoriAlign::Right => [tlwh[2] - 1.0, tlwh[2]], + }; + + return Some(([l + tlwh[1], r + tlwh[1], t + tlwh[0], b + tlwh[0]], 0)); + } + + let layout = match self.layout_op.as_ref() { + Some(layout) => layout, + None => return None, + }; + + let cursor = match cursor { + TextCursor::None | TextCursor::Empty => unreachable!(), + TextCursor::Position(cursor) => cursor, + }; + + for (line_i, line) in layout.lines.iter().enumerate() { + if cursor > line.e_cursor { + // The cursor is past this line. + continue; + } + + if cursor < line.s_cursor { + // The cursor is before this line? + break; + } + + if line.glyphs.is_empty() { + // This line has no glyphs, so use the line's hitbox. + + let t = tlwh[0] + line.hitbox[2]; + let b = tlwh[0] + line.hitbox[3]; + let l = tlwh[1] + line.hitbox[0]; + let r = l + 1.0; + return Some(([l, r, t, b], line_i)); + } + + let first_glyph = &layout.glyphs[line.glyphs.start]; + + if match first_glyph.span_i.cmp(&cursor.span) { + Ordering::Less => false, + Ordering::Equal => { + match first_glyph.byte_s.cmp(&cursor.byte_s) { + Ordering::Less => false, + Ordering::Equal => false, + Ordering::Greater => true, + } + }, + Ordering::Greater => true, + } { + // Cursor is before the first glyph, use the left side of the line's bounding box. + + let t = tlwh[0] + line.hitbox[2]; + let b = tlwh[0] + line.hitbox[3]; + let l = tlwh[1] + line.hitbox[0]; + let r = l + 1.0; + return Some(([l, r, t, b], line_i)); + } + + let last_glyph = &layout.glyphs[line.glyphs.end - 1]; + + if match last_glyph.span_i.cmp(&cursor.span) { + Ordering::Less => true, + Ordering::Equal => { + match last_glyph.byte_s.cmp(&cursor.byte_s) { + Ordering::Less => true, + Ordering::Equal => false, + Ordering::Greater => false, + } + }, + Ordering::Greater => false, + } { + // Cursor is after the last glyph, use the right side of the line's bounding box. + + let t = tlwh[0] + line.hitbox[2]; + let b = tlwh[0] + line.hitbox[3]; + let r = tlwh[1] + line.hitbox[1]; + let l = r - 1.0; + return Some(([l, r, t, b], line_i)); + } + + // The cursor *should* have an associated glyph. + + for glyph in layout.glyphs[line.glyphs.clone()].iter() { + if glyph.span_i == cursor.span && glyph.byte_s == cursor.byte_s { + match cursor.affinity { + TextCursorAffinity::Before => { + let t = tlwh[0] + glyph.hitbox[2]; + let b = tlwh[0] + glyph.hitbox[3]; + let r = tlwh[1] + glyph.hitbox[0]; + let l = r - 1.0; + return Some(([l, r, t, b], line_i)); + }, + TextCursorAffinity::After => { + let t = tlwh[0] + glyph.hitbox[2]; + let b = tlwh[0] + glyph.hitbox[3]; + let l = tlwh[1] + glyph.hitbox[1]; + let r = l - 1.0; + return Some(([l, r, t, b], line_i)); + }, + }; + } + } + } + + // The cursor is probably invalid. + + None + } + + pub fn cursor_select_line(&self, cursor: TextCursor) -> Option { + let cursor = match cursor { + TextCursor::None | TextCursor::Empty => return None, + TextCursor::Position(cursor) => cursor, + }; + + let layout = match self.layout_op.as_ref() { + Some(layout) => layout, + None => return None, + }; + + for line in layout.lines.iter() { + if cursor > line.e_cursor { + continue; + } + + if cursor < line.s_cursor { + break; + } + + return Some(TextSelection { + start: line.s_cursor, + end: line.e_cursor, + }); + } + + None + } + + pub fn select_line(&self, line_i: usize) -> Option { + let layout = self.layout_op.as_ref()?; + let line = layout.lines.get(line_i)?; + + Some(TextSelection { + start: line.s_cursor, + end: line.e_cursor, + }) + } + + pub fn cursor_line_column(&self, cursor: TextCursor) -> Option<[usize; 2]> { + let cursor = match cursor { + TextCursor::None | TextCursor::Empty => return None, + TextCursor::Position(cursor) => cursor, + }; + + let layout = self.layout_op.as_ref()?; + + for line_i in 0..layout.lines.len() { + if cursor > layout.lines[line_i].e_cursor { + // cursor is past this line, continue to next + continue; + } + + if cursor < layout.lines[line_i].s_cursor { + // cursor is before this line?, use end of last line + + if line_i == 0 { + // on the first line? use start of line? + return Some([0, 0]); + } + + let glyph_range = layout.lines[line_i - 1].glyphs.clone(); + return Some([line_i - 1, glyph_range.end - glyph_range.start]); + } + + // cursor is on this line + + let mut col_i = 0; + let glyph_range = layout.lines[line_i].glyphs.clone(); + + for glyph_i in glyph_range { + if cursor.byte_s > layout.glyphs[glyph_i].byte_s { + col_i += 1; + } else { + break; + } + } + + return Some([line_i, col_i]); + } + + // cursor is past the last line?, use end of last line + + let glyph_range = layout.lines.last().unwrap().glyphs.clone(); + return Some([layout.lines.len() - 1, glyph_range.end - glyph_range.start]); + } + + pub fn line_column_cursor(&self, line_i: usize, col_i: usize) -> TextCursor { + let layout = match self.layout_op.as_ref() { + Some(layout) => layout, + None => return TextCursor::None, + }; + + let line = match layout.lines.get(line_i) { + Some(line) => line, + None => return TextCursor::None, + }; + + if line.glyphs.start + col_i >= line.glyphs.end { + return TextCursor::None; + } + + let glyph = &layout.glyphs[line.glyphs.start + col_i]; + + PosTextCursor { + span: glyph.span_i, + byte_s: glyph.byte_s, + byte_e: glyph.byte_e, + affinity: TextCursorAffinity::After, + } + .into() + } + + pub fn line_bounds(&self, tlwh: [f32; 4], line_i: usize) -> Option<[f32; 4]> { + let layout = self.layout_op.as_ref()?; + let line = layout.lines.get(line_i)?; + + Some([ + tlwh[0] + line.hitbox[2], + tlwh[0] + line.hitbox[3], + tlwh[1] + line.hitbox[0], + tlwh[1] + line.hitbox[1], + ]) + } + + pub fn line_count(&self) -> Option { + Some(self.layout_op.as_ref()?.lines.len()) + } + + pub fn line_column_count(&self, line_i: usize) -> Option { + let layout = self.layout_op.as_ref()?; + let line = layout.lines.get(line_i)?; + Some(line.glyphs.end - line.glyphs.start) + } + pub fn update( &mut self, tlwh: [f32; 4], @@ -128,7 +563,7 @@ impl TextState { context: &mut UpdateContext, image_cache: &Arc, ) { - if body.is_empty() { + if body.spans.is_empty() || body.spans.iter().all(|span| span.is_empty()) { self.buffer_op = None; self.layout_op = None; self.image_info_cache.clear(); @@ -435,58 +870,389 @@ impl TextState { None, ); + buffer.shape_until_scroll(&mut context.font_system, false); let mut layout_glyphs = Vec::new(); let mut layout_lines: Vec = Vec::new(); + let mut line_byte_mapping: Vec> = Vec::new(); + let mut line_byte = 0; - for (line_i, run) in buffer.layout_runs().enumerate() { - if let LineLimit::Fixed(line_limit) = layout.line_limit { - if line_i >= line_limit { - break; + for (span_i, span) in layout.spans.iter().enumerate() { + for (byte_i, c) in span.text.char_indices() { + if line_byte_mapping.is_empty() { + line_byte_mapping.push(BTreeMap::new()); + } + + line_byte_mapping + .last_mut() + .unwrap() + .insert(line_byte, [span_i, byte_i, byte_i + c.len_utf8()]); + + if c == '\n' { + line_byte_mapping.push(BTreeMap::new()); + line_byte = 0; + } else { + line_byte += c.len_utf8(); } } + } - for l_glyph in run.glyphs.iter() { - let p_glyph = l_glyph.physical((0.0, 0.0), self.layout_scale); - let span_i = l_glyph.metadata; - - layout_glyphs.push(LayoutGlyph { - span_i, - line_i, - offset: [ - p_glyph.x as f32 / self.layout_scale, - (p_glyph.y as f32 / self.layout_scale) + run.line_y, - ], - extent: [0.0; 2], - image_extent: [0.0; 2], - hitbox: [ - l_glyph.x, - l_glyph.x + l_glyph.w, - l_glyph.y - + run.line_top - + l_glyph - .line_height_opt - .map(|glyph_lh| run.line_height - glyph_lh) - .unwrap_or(0.0), - l_glyph.y + run.line_top + run.line_height, - ], - image_key: ImageKey::glyph(p_glyph.cache_key), - vertex_type: 0, - }); + // FIXME: There seems to be a bug with cosmic-text where an empty line will not output + // following a '\n' if it is the last line. + assert!(line_byte_mapping.len() >= buffer.lines.len()); + + let mut line_top = 0.0; + + for (buffer_i, buffer_line) in buffer.lines.iter().enumerate() { + let ct_layout_lines = buffer_line.layout_opt().unwrap(); + + for (layout_line_i, layout_line) in ct_layout_lines.iter().enumerate() { + if layout_line.glyphs.is_empty() { + // If the layout line is empty then there should only be one layout line in + // this buffer line. + debug_assert_eq!(ct_layout_lines.len(), 1); + + if line_byte_mapping[buffer_i].is_empty() { + // This is the last line in the body and it is empty and there should be + // at least one buffer line before this one. + debug_assert_eq!(buffer_i, buffer.lines.len() - 1); + + let [span, byte_s, byte_e] = + *line_byte_mapping[buffer_i - 1].last_entry().unwrap().get(); + + let se_cursor = PosTextCursor { + span, + byte_s, + byte_e, + affinity: TextCursorAffinity::After, + }; - match layout_lines.get_mut(line_i) { - Some(layout_line) => { - layout_line.glyphs.end += 1; - }, - None => { layout_lines.push(LayoutLine { - height: run.line_height, - glyphs: (layout_glyphs.len() - 1)..layout_glyphs.len(), + height: layout.spans[se_cursor.span].line_height, + glyphs: 0..0, + bounds: [0.0; 4], + hitbox: [0.0; 4], + s_cursor: se_cursor, + e_cursor: se_cursor, }); - }, + } else { + // This line only has a '\n' + debug_assert_eq!(line_byte_mapping[buffer_i].len(), 1); + + let [span, byte_s, byte_e] = + *line_byte_mapping[buffer_i].last_entry().unwrap().get(); + + let e_cursor = PosTextCursor { + span, + byte_s, + byte_e, + affinity: TextCursorAffinity::Before, + }; + + let s_cursor = if buffer_i > 0 { + // There is a buffer before this line use that as the start cursor. + + let [span, byte_s, byte_e] = + *line_byte_mapping[buffer_i - 1].last_entry().unwrap().get(); + + PosTextCursor { + span, + byte_s, + byte_e, + affinity: TextCursorAffinity::After, + } + } else { + e_cursor + }; + + layout_lines.push(LayoutLine { + height: layout.spans[e_cursor.span].line_height, + glyphs: 0..0, + bounds: [0.0; 4], + hitbox: [0.0; 4], + s_cursor, + e_cursor, + }); + } + + line_top += layout_lines.last().unwrap().height; + continue; + } + + debug_assert!(!layout_line.glyphs.is_empty()); + + let first_glyph_i = layout_glyphs.len(); + + for l_glyph in layout_line.glyphs.iter() { + let g_span_i = l_glyph.metadata; + let p_glyph = l_glyph.physical((0.0, 0.0), self.layout_scale); + let g_byte_s = line_byte_mapping[buffer_i][&l_glyph.start][1]; + let g_byte_e = g_byte_s + (l_glyph.end - l_glyph.start); + + layout_glyphs.push(LayoutGlyph { + span_i: g_span_i, + byte_s: g_byte_s, + byte_e: g_byte_e, + offset: [ + p_glyph.x as f32 / self.layout_scale, + p_glyph.y as f32 / self.layout_scale, + ], + extent: [0.0; 2], + image_extent: [0.0; 2], + hitbox: [ + l_glyph.x, + l_glyph.x + l_glyph.w, + l_glyph.y + line_top, + l_glyph.y + line_top, + ], + image_key: ImageKey::glyph(p_glyph.cache_key), + vertex_type: 0, + }); } + + let last_glyph_i = layout_glyphs.len() - 1; + + let s_cursor = if layout_line_i == 0 { + // This is the first layout line in this buffer line, so include + // the previous '\n' as the start cursor. + + if buffer_i == 0 { + // Use the start of this line as the start. + + let [span, byte_s, byte_e] = + *line_byte_mapping[buffer_i].first_entry().unwrap().get(); + + PosTextCursor { + span, + byte_s, + byte_e, + affinity: TextCursorAffinity::Before, + } + } else { + // There is one buffer line before this one use that one's '\n' as + // the start of this line. + let [span, byte_s, byte_e] = + *line_byte_mapping[buffer_i - 1].last_entry().unwrap().get(); + + PosTextCursor { + span, + byte_s, + byte_e, + affinity: TextCursorAffinity::After, + } + } + } else { + // This is not the first layout line in this buffer line, so it wrapped + // on a character or whitespace. + + let last_line = layout_lines.last_mut().unwrap(); + let first_glyph = &layout_glyphs[first_glyph_i]; + + if last_line.e_cursor.span == first_glyph.span_i { + // The two points are within the same span, this is easier... + + if last_line.e_cursor.byte_e == first_glyph.byte_s { + // Wrapped on a character, use this line's first glyph + + PosTextCursor { + span: first_glyph.span_i, + byte_s: first_glyph.byte_s, + byte_e: first_glyph.byte_e, + affinity: TextCursorAffinity::Before, + } + } else { + // Wrapped on whitespace, modify last line's cursor to include this + // white space and use that as the line ends. + + // TODO: This assumes the byte range between the two points is a single + // whitespace character. Could it be possible that it is multiple? + + last_line.e_cursor = PosTextCursor { + span: first_glyph.span_i, + byte_s: last_line.e_cursor.byte_e, + byte_e: first_glyph.byte_s, + affinity: TextCursorAffinity::Before, + }; + + PosTextCursor { + affinity: TextCursorAffinity::After, + ..last_line.e_cursor + } + } + } else { + // The two points go across span boundries, this is trickier... + + if last_line.e_cursor.byte_e + == body.spans[last_line.e_cursor.span].text.len() + { + // The last line included the entirety of the last span. + + if first_glyph.byte_s == 0 { + // The first glyph starts at zero, so we wrapped on a character. + // Use this line's first glyph. + + PosTextCursor { + span: first_glyph.span_i, + byte_s: first_glyph.byte_s, + byte_e: first_glyph.byte_e, + affinity: TextCursorAffinity::Before, + } + } else { + // The first glyph starts doesn't start at zero, so the span that + // the first glyph resides in contains the whitespace. Modify the + // last line's cursor to include the whitespace and use it as the + // the line ends. + + // TODO: See above TODO about the whitespace's validity. + + last_line.e_cursor = PosTextCursor { + span: first_glyph.span_i, + byte_s: 0, + byte_e: first_glyph.byte_s, + affinity: TextCursorAffinity::Before, + }; + + PosTextCursor { + affinity: TextCursorAffinity::After, + ..last_line.e_cursor + } + } + } else { + // The last line doesn't include the entirety of the last span. + + if first_glyph.byte_s == 0 { + // This line starts at the start of first glyph's span, the + // whitespace entirely resides in the previous span. Modify the + // last line's e_cursor to include the whitespace and use it as the + // start of this line. + + last_line.e_cursor = PosTextCursor { + byte_s: last_line.e_cursor.byte_e, + byte_e: body.spans[last_line.e_cursor.span].text.len(), + affinity: TextCursorAffinity::Before, + ..last_line.e_cursor + }; + + PosTextCursor { + affinity: TextCursorAffinity::After, + ..last_line.e_cursor + } + } else { + // TODO: The whitespace is multiple characters? Is this possible? + + // Modify the last line's e_cursor to include the start of this + // span's whitespace and use it as the start of the line. + + last_line.e_cursor = PosTextCursor { + span: first_glyph.span_i, + byte_s: 0, + byte_e: first_glyph.byte_s, + affinity: TextCursorAffinity::Before, + }; + + PosTextCursor { + affinity: TextCursorAffinity::After, + ..last_line.e_cursor + } + } + } + } + }; + + let e_cursor = if layout_line_i + 1 == ct_layout_lines.len() { + // This is the last layout line in this buffer line. + + let [span, byte_s, byte_e] = + *line_byte_mapping[buffer_i].last_entry().unwrap().get(); + let last_glyph = &layout_glyphs[last_glyph_i]; + + if last_glyph.span_i == span && last_glyph.byte_s == byte_s { + // The last glyph is at the end of this buffer line's bytes. Use the last + // glyph as the end. + + PosTextCursor { + span: last_glyph.span_i, + byte_s: last_glyph.byte_s, + byte_e: last_glyph.byte_e, + affinity: TextCursorAffinity::After, + } + } else { + // The last glyph isn't at the end of this buffer line's bytes. Use the end + // of the line's bytes. + + PosTextCursor { + span, + byte_s, + byte_e, + affinity: TextCursorAffinity::Before, + } + } + } else { + // This isn't the last layout line use the last glyph as the end for now. + // NOTE: the above s_cursor code modifies the e_cursor if need be. + + let last_glyph = &layout_glyphs[last_glyph_i]; + + PosTextCursor { + span: last_glyph.span_i, + byte_s: last_glyph.byte_s, + byte_e: last_glyph.byte_e, + affinity: TextCursorAffinity::After, + } + }; + + let mut line_height: f32 = 0.0; + + for span_i in + layout_glyphs[first_glyph_i].span_i..=layout_glyphs[last_glyph_i].span_i + { + line_height = line_height.max(layout.spans[span_i].line_height); + } + + let line_offset = line_top + + ((line_height - (layout_line.max_ascent + layout_line.max_descent)) / 2.0) + + layout_line.max_ascent; + + for glyph_i in first_glyph_i..=last_glyph_i { + let glyph = &mut layout_glyphs[glyph_i]; + glyph.offset[1] += line_offset; + glyph.hitbox[3] += line_height; + } + + layout_lines.push(LayoutLine { + height: line_height, + glyphs: first_glyph_i..(last_glyph_i + 1), + bounds: [0.0; 4], + hitbox: [0.0; 4], + s_cursor, + e_cursor, + }); + + line_top += line_height; } } + // FIXME: See above assert + if line_byte_mapping.len() > buffer.lines.len() { + let line_i = line_byte_mapping.len() - 2; + let [span, byte_s, byte_e] = *line_byte_mapping[line_i].last_entry().unwrap().get(); + + let cursor = PosTextCursor { + span, + byte_s, + byte_e, + affinity: TextCursorAffinity::After, + }; + + layout_lines.push(LayoutLine { + height: layout.spans[span].line_height, + glyphs: 0..0, + bounds: [0.0; 4], + hitbox: [0.0; 4], + s_cursor: cursor, + e_cursor: cursor, + }); + } + let mut image_keys = ImageSet::new(); for glyph in layout_glyphs.iter() { @@ -596,37 +1362,70 @@ impl TextState { glyph.hitbox[3] += vert_align_offset; } - for line in layout_lines.iter() { + let mut line_y_min = vert_align_offset; + + for line in layout_lines.iter_mut() { let mut line_x_mm = [f32::INFINITY, f32::NEG_INFINITY]; + let mut line_x_hb = [f32::INFINITY, f32::NEG_INFINITY]; for glyph_i in line.glyphs.clone() { line_x_mm[0] = line_x_mm[0].min(layout_glyphs[glyph_i].offset[0]); line_x_mm[1] = line_x_mm[1] .max(layout_glyphs[glyph_i].offset[0] + layout_glyphs[glyph_i].extent[0]); + line_x_hb[0] = line_x_hb[0].min(layout_glyphs[glyph_i].hitbox[0]); + line_x_hb[1] = line_x_hb[1].max(layout_glyphs[glyph_i].hitbox[1]); } - let line_width = line_x_mm[1] - line_x_mm[0]; + if line.glyphs.is_empty() { + line_x_mm = [0.0; 2]; + line_x_hb = [0.0; 2]; + } - let hori_align_offset = match if layout.text_wrap == TextWrap::Shift - && line_width > self.layout_size[0] - { - TextHoriAlign::Right - } else { - layout.hori_align - } { - TextHoriAlign::Left => -line_x_mm[0], - TextHoriAlign::Center => -line_x_mm[0] + ((self.layout_size[0] - line_width) / 2.0), - TextHoriAlign::Right => line_x_mm[0] + self.layout_size[0] - line_width, - }; + let line_width = line_x_mm[1] - line_x_mm[0]; - bounds[0] = bounds[0].min(line_x_mm[0] + hori_align_offset); - bounds[1] = bounds[1].max(line_x_mm[1] + hori_align_offset); + let hori_align_offset = + match if layout.text_wrap == TextWrap::Shift && line_width > self.layout_size[0] { + TextHoriAlign::Right + } else { + layout.hori_align + } { + /*TextHoriAlign::Left => -line_x_mm[0], + TextHoriAlign::Center => -line_x_mm[0] + ((self.layout_size[0] - line_width) / 2.0), + TextHoriAlign::Right => line_x_mm[0] + self.layout_size[0] - line_width,*/ + TextHoriAlign::Left => 0.0, + TextHoriAlign::Center => (self.layout_size[0] - line_width) / 2.0, + TextHoriAlign::Right => self.layout_size[0] - line_width - line_x_mm[0], + }; + + line_x_mm[0] += hori_align_offset; + line_x_mm[1] += hori_align_offset; + line_x_hb[0] += hori_align_offset; + line_x_hb[1] += hori_align_offset; + + bounds[0] = bounds[0].min(line_x_mm[0]); + bounds[1] = bounds[1].max(line_x_mm[1]); for glyph_i in line.glyphs.clone() { layout_glyphs[glyph_i].offset[0] += hori_align_offset; layout_glyphs[glyph_i].hitbox[0] += hori_align_offset; layout_glyphs[glyph_i].hitbox[1] += hori_align_offset; } + + line.bounds = [ + line_x_mm[0], + line_x_mm[1], + line_y_min, + line_y_min + line.height, + ]; + + line.hitbox = [ + line_x_hb[0], + line_x_hb[1], + line_y_min, + line_y_min + line.height, + ]; + + line_y_min += line.height; } layout.lines = layout_lines; @@ -648,70 +1447,212 @@ impl TextState { tlwh: [f32; 4], z: f32, opacity: f32, + text_body: &TextBody, + context: &UpdateContext, output: &mut ImageMap>, ) { let layout = match self.layout_op.as_ref() { Some(layout) => layout, - None => return, + None => { + if let Some(([l, r, t, b], _)) = self.get_cursor_bounds( + text_body.cursor, + tlwh, + text_body, + context.default_font.height, + ) { + output.try_insert_then( + &ImageKey::INVALID, + Vec::new, + |vertexes: &mut Vec| { + vertexes.extend( + [ + [r, t, z], + [l, t, z], + [l, b, z], + [r, t, z], + [l, b, z], + [r, b, z], + ] + .into_iter() + .map(|position| { + ItfVertInfo { + position, + coords: [0.0; 2], + color: text_body.cursor_color.rgbaf_array(), + ty: 0, + tex_i: 0, + } + }), + ); + }, + ); + } + + return; + }, }; for glyph in layout.glyphs.iter() { - if glyph.image_key.is_invalid() { - continue; + if !glyph.image_key.is_invalid() { + output.try_insert_then( + &glyph.image_key, + Vec::new, + |vertexes: &mut Vec| { + let t = ((tlwh[0] + glyph.offset[1]) * self.layout_scale).round() + / self.layout_scale; + let b = ((tlwh[0] + glyph.extent[1] + glyph.offset[1]) * self.layout_scale) + .round() + / self.layout_scale; + let l = ((tlwh[1] + glyph.offset[0]) * self.layout_scale).round() + / self.layout_scale; + let r = ((tlwh[1] + glyph.extent[0] + glyph.offset[0]) * self.layout_scale) + .round() + / self.layout_scale; + + let mut color = layout.spans[glyph.span_i].text_color; + color.a *= opacity; + let color = color.rgbaf_array(); + + vertexes.extend( + [ + ([r, t, z], [glyph.image_extent[0], 0.0]), + ([l, t, z], [0.0, 0.0]), + ([l, b, z], [0.0, glyph.image_extent[1]]), + ([r, t, z], [glyph.image_extent[0], 0.0]), + ([l, b, z], [0.0, glyph.image_extent[1]]), + ([r, b, z], glyph.image_extent), + ] + .into_iter() + .map(|(position, coords)| { + ItfVertInfo { + position, + coords, + color, + ty: glyph.vertex_type, + tex_i: 0, + } + }), + ); + }, + ); } - output.try_insert_then( - &glyph.image_key, - Vec::new, - |vertexes: &mut Vec| { - let t = ((tlwh[0] + glyph.offset[1]) * self.layout_scale).round() - / self.layout_scale; - let b = ((tlwh[0] + glyph.extent[1] + glyph.offset[1]) * self.layout_scale) - .round() - / self.layout_scale; - let l = ((tlwh[1] + glyph.offset[0]) * self.layout_scale).round() - / self.layout_scale; - let r = ((tlwh[1] + glyph.extent[0] + glyph.offset[0]) * self.layout_scale) - .round() - / self.layout_scale; - - let mut color = layout.spans[glyph.span_i].text_color; - color.a *= opacity; - let color = color.rgbaf_array(); + if let Some(selection) = text_body.selection.as_ref() { + if glyph.span_i < selection.start.span || glyph.span_i > selection.end.span { + continue; + } - vertexes.extend( - [ - ([r, t, z], [glyph.image_extent[0], 0.0]), - ([l, t, z], [0.0, 0.0]), - ([l, b, z], [0.0, glyph.image_extent[1]]), - ([r, t, z], [glyph.image_extent[0], 0.0]), - ([l, b, z], [0.0, glyph.image_extent[1]]), - ([r, b, z], glyph.image_extent), - ] - .into_iter() - .map(|(position, coords)| { - ItfVertInfo { - position, - coords, - color, - ty: glyph.vertex_type, - tex_i: 0, - } - }), - ); - }, - ); + if glyph.span_i == selection.start.span { + if glyph.byte_s < selection.start.byte_s { + continue; + } + + if glyph.byte_s == selection.start.byte_s + && selection.start.affinity == TextCursorAffinity::After + { + continue; + } + } + + if glyph.span_i == selection.end.span { + if glyph.byte_s > selection.end.byte_s { + continue; + } + + if glyph.byte_s == selection.end.byte_s + && selection.end.affinity == TextCursorAffinity::Before + { + continue; + } + } + + output.try_insert_then( + &ImageKey::INVALID, + Vec::new, + |vertexes: &mut Vec| { + let t = tlwh[0] + glyph.hitbox[2]; + let b = tlwh[0] + glyph.hitbox[3]; + let l = tlwh[1] + glyph.hitbox[0]; + let r = tlwh[1] + glyph.hitbox[1]; + + vertexes.extend( + [ + [r, t, z], + [l, t, z], + [l, b, z], + [r, t, z], + [l, b, z], + [r, b, z], + ] + .into_iter() + .map(|position| { + ItfVertInfo { + position, + coords: [0.0; 2], + color: text_body.selection_color.rgbaf_array(), + ty: 0, + tex_i: 0, + } + }), + ); + }, + ); + } + } - // Highlight test - /*output.try_insert_then( + if let Some(selection) = text_body.selection.as_ref() { + for (line_i, line) in layout.lines.iter().enumerate() { + if line_i + 1 == layout.lines.len() + || layout.lines[line_i + 1].s_cursor <= selection.start + || layout.lines[line_i + 1].s_cursor > selection.end + { + continue; + } + + let t = tlwh[0] + line.hitbox[2]; + let b = tlwh[0] + line.hitbox[3]; + let l = tlwh[1] + line.hitbox[1]; + let r = tlwh[1] + line.hitbox[1] + (line.height / 4.0).round(); + + output.try_insert_then( + &ImageKey::INVALID, + Vec::new, + |vertexes: &mut Vec| { + vertexes.extend( + [ + [r, t, z], + [l, t, z], + [l, b, z], + [r, t, z], + [l, b, z], + [r, b, z], + ] + .into_iter() + .map(|position| { + ItfVertInfo { + position, + coords: [0.0; 2], + color: text_body.selection_color.rgbaf_array(), + ty: 0, + tex_i: 0, + } + }), + ); + }, + ); + } + } + + if let Some(([l, r, t, b], _)) = self.get_cursor_bounds( + text_body.cursor, + tlwh, + text_body, + context.default_font.height, + ) { + output.try_insert_then( &ImageKey::INVALID, Vec::new, |vertexes: &mut Vec| { - let t = tlwh[0] + glyph.hitbox[2]; - let b = tlwh[0] + glyph.hitbox[3]; - let l = tlwh[1] + glyph.hitbox[0]; - let r = tlwh[1] + glyph.hitbox[1]; - vertexes.extend( [ [r, t, z], @@ -726,14 +1667,14 @@ impl TextState { ItfVertInfo { position, coords: [0.0; 2], - color: Color::shex("4040ffc0").rgbaf_array(), + color: text_body.cursor_color.rgbaf_array(), ty: 0, tex_i: 0, } }), ); }, - );*/ + ); } } diff --git a/src/interface/mod.rs b/src/interface/mod.rs index 4364a1af..75886550 100644 --- a/src/interface/mod.rs +++ b/src/interface/mod.rs @@ -20,8 +20,13 @@ pub(crate) use self::bin::UpdateContext; pub use self::bin::style::{ BackImageRegion, BinStyle, BinStyleError, BinStyleErrorType, BinStyleValidation, BinStyleWarn, BinStyleWarnType, BinVertex, FloatWeight, ImageEffect, LineLimit, LineSpacing, Opacity, - TextAttrs, TextBody, TextHoriAlign, TextSpan, TextVertAlign, TextWrap, Visibility, ZIndex, + TextHoriAlign, TextVertAlign, TextWrap, Visibility, ZIndex, }; +pub use self::bin::text_body::{ + PosTextCursor, TextAttrs, TextAttrsMask, TextBody, TextCursor, TextCursorAffinity, + TextSelection, TextSpan, +}; +pub use self::bin::text_guard::TextBodyGuard; pub use self::bin::{Bin, BinID, BinPostUpdate, OVDPerfMetrics}; pub use self::color::Color; pub use self::style::{Flow, FontFamily, FontStretch, FontStyle, FontWeight, Position, UnitValue}; diff --git a/src/interface/style.rs b/src/interface/style.rs index 83afd7f0..298ee666 100644 --- a/src/interface/style.rs +++ b/src/interface/style.rs @@ -137,7 +137,7 @@ pub enum FontFamily { } impl FontFamily { - pub(crate) fn as_cosmic(&self) -> Option { + pub(crate) fn as_cosmic(&self) -> Option> { match self { Self::Inheirt => None, Self::Serif => Some(cosmic_text::Family::Serif), diff --git a/src/lib.rs b/src/lib.rs index fe82ceae..c78502b4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -671,6 +671,31 @@ impl Basalt { basalt.interface.associate_basalt(basalt.clone()); basalt.window_manager.associate_basalt(basalt.clone()); + + #[cfg(feature = "deadlock_detection")] + { + use parking_lot::deadlock::check_deadlock; + + std::thread::spawn(move || { + loop { + for threads in check_deadlock().into_iter() { + println!("[Deadlock]"); + + for thread in threads { + println!(" Thread ID: {:#?}", thread.thread_id()); + let backtrace = format!("{:#?}", thread.backtrace()); + + for line in backtrace.lines() { + println!(" {}", line); + } + } + } + + std::thread::sleep(std::time::Duration::from_secs(5)); + } + }); + } + result_fn(Ok(basalt)); }); } diff --git a/src/render/worker/mod.rs b/src/render/worker/mod.rs index 111ee790..50d93666 100644 --- a/src/render/worker/mod.rs +++ b/src/render/worker/mod.rs @@ -312,7 +312,7 @@ impl Worker { .get(); let mut update_contexts = Vec::with_capacity(update_threads); - update_contexts.push(UpdateContext::from(&window)); + update_contexts.push(UpdateContext::from(&*window.shared_update_ctx())); let metrics_level = update_contexts[0].metrics_level; let current_extent = [ diff --git a/src/window/mod.rs b/src/window/mod.rs index 582d31fe..ddf04c31 100644 --- a/src/window/mod.rs +++ b/src/window/mod.rs @@ -394,7 +394,7 @@ impl WindowManager { self.send_event(WMEvent::AssociateBasalt(basalt)); } - pub(crate) fn request_draw(&self) -> DrawGuard { + pub(crate) fn request_draw(&self) -> DrawGuard<'_> { DrawGuard { inner: self.draw_lock.lock(), } diff --git a/src/window/window.rs b/src/window/window.rs index d3a8192b..2110323d 100644 --- a/src/window/window.rs +++ b/src/window/window.rs @@ -1,16 +1,20 @@ use std::any::Any; +use std::ops::{Deref, DerefMut}; use std::sync::atomic::{self, AtomicBool}; use std::sync::{Arc, Weak}; use std::time::Duration; +use cosmic_text::fontdb::Source as FontSource; use flume::{Receiver, Sender}; use foldhash::{HashMap, HashMapExt}; -use parking_lot::Mutex; +use parking_lot::{Mutex, MutexGuard}; use raw_window_handle::{ DisplayHandle, HandleError as RwhHandleError, HasDisplayHandle, HasWindowHandle, RawWindowHandle, WindowHandle, }; +use crate::interval::IntvlHookID; + mod winit { pub use winit::dpi::PhysicalSize; #[allow(unused_imports)] @@ -31,7 +35,7 @@ use crate::input::{ Char, InputEvent, InputHookCtrl, InputHookID, InputHookTarget, KeyCombo, LocalCursorState, LocalKeyState, WindowState, }; -use crate::interface::{Bin, BinID}; +use crate::interface::{Bin, BinID, UpdateContext}; use crate::render::{MSAA, RendererMetricsLevel, RendererPerfMetrics, VSync}; use crate::window::monitor::{FullScreenBehavior, FullScreenError, Monitor}; use crate::window::{WMEvent, WindowCreateError, WindowEvent, WindowID, WindowManager, WindowType}; @@ -51,6 +55,7 @@ pub struct Window { event_send: Sender, event_recv: Receiver, event_recv_acquired: AtomicBool, + shared_update_ctx: Mutex>, } struct State { @@ -66,6 +71,7 @@ struct State { on_metrics_update: Vec>, associated_bins: HashMap>, attached_input_hooks: Vec, + attached_intvl_hooks: Vec, keep_alive_objects: Vec>, } @@ -135,6 +141,7 @@ impl Window { interface_scale: basalt.config.window_default_scale, associated_bins: HashMap::new(), attached_input_hooks: Vec::new(), + attached_intvl_hooks: Vec::new(), keep_alive_objects: Vec::new(), }; @@ -150,6 +157,7 @@ impl Window { event_send, event_recv, event_recv_acquired: AtomicBool::new(false), + shared_update_ctx: Mutex::new(None), })) } @@ -798,7 +806,47 @@ impl Window { .store(false, atomic::Ordering::SeqCst); } + pub(crate) fn shared_update_ctx<'a>(self: &'a Arc) -> SharedUpdateCtx<'a> { + let mut ctx = SharedUpdateCtx { + inner: self.shared_update_ctx.lock(), + }; + + ctx.ready(self); + ctx + } + pub(crate) fn send_event(&self, event: WindowEvent) { + match &event { + WindowEvent::Resized { + width, + height, + } => { + if let Some(shared_update_ctx) = self.shared_update_ctx.lock().as_mut() { + shared_update_ctx.extent[0] = *width as f32; + shared_update_ctx.extent[1] = *height as f32; + } + }, + WindowEvent::ScaleChanged(scale) => { + if let Some(shared_update_ctx) = self.shared_update_ctx.lock().as_mut() { + shared_update_ctx.scale = *scale; + } + }, + WindowEvent::AddBinaryFont(binary_font) => { + if let Some(shared_update_ctx) = self.shared_update_ctx.lock().as_mut() { + shared_update_ctx + .font_system + .db_mut() + .load_font_source(FontSource::Binary(binary_font.clone())); + } + }, + WindowEvent::SetDefaultFont(default_font) => { + if let Some(shared_update_ctx) = self.shared_update_ctx.lock().as_mut() { + shared_update_ctx.default_font = default_font.clone(); + } + }, + _ => (), + } + if self.event_recv_acquired.load(atomic::Ordering::SeqCst) { self.event_send.send(event).unwrap(); } @@ -813,6 +861,12 @@ impl Window { self.state.lock().attached_input_hooks.push(hook); } + /// Attach an interval hook to this window. When the window closes, this hook will be + /// automatically removed from `Interval`. + pub fn attach_intvl_hook(&self, hook: IntvlHookID) { + self.state.lock().attached_intvl_hooks.push(hook); + } + pub fn on_press(self: &Arc, combo: C, method: F) -> InputHookID where F: FnMut(InputHookTarget, &WindowState, &LocalKeyState) -> InputHookCtrl + Send + 'static, @@ -977,20 +1031,54 @@ impl Window { impl Drop for Window { fn drop(&mut self) { - for hook_id in self.state.lock().attached_input_hooks.drain(..) { + let mut state = self.state.lock(); + + for hook_id in state.attached_input_hooks.drain(..) { self.basalt.input_ref().remove_hook(hook_id); } + + for hook_id in state.attached_intvl_hooks.drain(..) { + self.basalt.interval_ref().remove(hook_id); + } } } impl HasWindowHandle for Window { - fn window_handle(&self) -> Result { + fn window_handle(&self) -> Result, RwhHandleError> { self.inner.window_handle() } } impl HasDisplayHandle for Window { - fn display_handle(&self) -> Result { + fn display_handle(&self) -> Result, RwhHandleError> { self.inner.display_handle() } } + +pub(crate) struct SharedUpdateCtx<'a> { + inner: MutexGuard<'a, Option>, +} + +impl SharedUpdateCtx<'_> { + fn ready(&mut self, window: &Arc) { + if self.inner.is_none() { + *self.inner = Some(UpdateContext::from(window)); + } + + self.inner.as_mut().unwrap().placement_cache.clear(); + } +} + +impl Deref for SharedUpdateCtx<'_> { + type Target = UpdateContext; + + fn deref(&self) -> &Self::Target { + (*self.inner).as_ref().unwrap() + } +} + +impl DerefMut for SharedUpdateCtx<'_> { + fn deref_mut(&mut self) -> &mut Self::Target { + (*self.inner).as_mut().unwrap() + } +}