diff --git a/examples/editor-libcosmic/Cargo.toml b/examples/editor-libcosmic/Cargo.toml index 2c8082f..3db022c 100644 --- a/examples/editor-libcosmic/Cargo.toml +++ b/examples/editor-libcosmic/Cargo.toml @@ -15,7 +15,7 @@ log = "0.4" [dependencies.libcosmic] git = "https://github.com/pop-os/libcosmic" -rev = "5fe44511" +rev = "2dde95ee" default-features = false features = ["wgpu", "winit"] #path = "../../../libcosmic" diff --git a/examples/editor-libcosmic/src/main.rs b/examples/editor-libcosmic/src/main.rs index 250bba7..974e97f 100644 --- a/examples/editor-libcosmic/src/main.rs +++ b/examples/editor-libcosmic/src/main.rs @@ -12,7 +12,7 @@ use cosmic::{ Element, }; use cosmic_text::{ - Attrs, AttrsList, Buffer, Edit, FontSystem, Metrics, SyntaxEditor, SyntaxSystem, Wrap, + Align, Attrs, AttrsList, Buffer, Edit, FontSystem, Metrics, SyntaxEditor, SyntaxSystem, Wrap, }; use std::{env, fs, path::PathBuf, sync::Mutex}; @@ -66,6 +66,7 @@ pub enum Message { Monospaced(bool), MetricsChanged(Metrics), WrapChanged(Wrap), + AlignmentChanged(Align), ThemeChanged(&'static str), } @@ -202,6 +203,10 @@ impl Application for Window { let mut editor = self.editor.lock().unwrap(); editor.buffer_mut().set_wrap(wrap); } + Message::AlignmentChanged(align) => { + let mut editor = self.editor.lock().unwrap(); + update_alignment(&mut *editor, align); + } Message::ThemeChanged(theme) => { self.theme = match theme { "Dark" => Theme::Dark, @@ -282,8 +287,24 @@ impl Application for Window { theme_picker, text("Font Size:"), font_size_picker, + ] + .align_items(Alignment::Center) + .spacing(8), + row![ text("Wrap:"), wrap_picker, + button(theme::Button::Text) + .icon(theme::Svg::Default, "format-justify-left", 20) + .on_press(Message::AlignmentChanged(Align::Left)), + button(theme::Button::Text) + .icon(theme::Svg::Symbolic, "format-justify-center", 20) + .on_press(Message::AlignmentChanged(Align::Center)), + button(theme::Button::Text) + .icon(theme::Svg::Symbolic, "format-justify-right", 20) + .on_press(Message::AlignmentChanged(Align::Right)), + button(theme::Button::Text) + .icon(theme::Svg::SymbolicLink, "format-justify-fill", 20) + .on_press(Message::AlignmentChanged(Align::Justified)), ] .align_items(Alignment::Center) .spacing(8), @@ -303,3 +324,21 @@ fn update_attrs<'a, T: Edit<'a>>(editor: &mut T, attrs: Attrs<'a>) { line.set_attrs_list(AttrsList::new(attrs)); }); } + +fn update_alignment<'a, T: Edit<'a>>(editor: &mut T, align: Align) { + let current_line = editor.cursor().line; + if let Some(select) = editor.select_opt() { + let (start, end) = match select.line.cmp(¤t_line) { + std::cmp::Ordering::Greater => (current_line, select.line), + std::cmp::Ordering::Less => (select.line, current_line), + std::cmp::Ordering::Equal => (current_line, current_line), + }; + editor.buffer_mut().lines.get_mut(start..=end).map(|lines| { + for line in lines.iter_mut() { + line.set_align(Some(align)); + } + }); + } else if let Some(line) = editor.buffer_mut().lines.get_mut(current_line) { + line.set_align(Some(align)); + } +} diff --git a/examples/editor-libcosmic/src/text.rs b/examples/editor-libcosmic/src/text.rs index 66a9c81..4b487d8 100644 --- a/examples/editor-libcosmic/src/text.rs +++ b/examples/editor-libcosmic/src/text.rs @@ -8,6 +8,7 @@ use cosmic::{ widget::{self, tree, Widget}, {Color, Element, Length, Point, Rectangle, Size}, }, + iced_winit::renderer::BorderRadius, theme::Theme, }; use cosmic_text::{Attrs, AttrsList, BufferLine, Metrics, SwashCache}; @@ -100,6 +101,7 @@ where self.metrics.font_size, limits.max().width as i32, self.line.wrap(), + self.line.align(), ); let mut width = 0; @@ -139,7 +141,7 @@ where renderer.fill_quad( renderer::Quad { bounds: layout.bounds(), - border_radius: 0.0, + border_radius: BorderRadius::default(), border_width: 0.0, border_color: Color::TRANSPARENT, }, @@ -160,7 +162,12 @@ where let shape = self.line.shape_opt().as_ref().unwrap(); //TODO: can we cache this? - let layout_lines = shape.layout(self.metrics.font_size, layout_w, self.line.wrap()); + let layout_lines = shape.layout( + self.metrics.font_size, + layout_w, + self.line.wrap(), + self.line.align(), + ); let mut cache = state.cache.lock().unwrap(); diff --git a/examples/editor-libcosmic/src/text_box.rs b/examples/editor-libcosmic/src/text_box.rs index 1faeff8..8a6b6fc 100644 --- a/examples/editor-libcosmic/src/text_box.rs +++ b/examples/editor-libcosmic/src/text_box.rs @@ -13,6 +13,7 @@ use cosmic::{ widget::{self, tree, Widget}, Padding, {Color, Element, Length, Point, Rectangle, Shell, Size}, }, + iced_winit::renderer::BorderRadius, theme::Theme, }; use cosmic_text::{Action, Edit, SwashCache}; @@ -142,7 +143,7 @@ where renderer.fill_quad( renderer::Quad { bounds: layout.bounds(), - border_radius: 0.0, + border_radius: BorderRadius::default(), border_width: 0.0, border_color: Color::TRANSPARENT, }, @@ -190,7 +191,7 @@ where + [self.padding.left as f32, self.padding.top as f32].into(), Size::new(w as f32, h as f32), ), - border_radius: 0.0, + border_radius: BorderRadius::default(), border_width: 0.0, border_color: Color::TRANSPARENT, }, diff --git a/src/buffer_line.rs b/src/buffer_line.rs index 428504c..c1f5b92 100644 --- a/src/buffer_line.rs +++ b/src/buffer_line.rs @@ -1,7 +1,7 @@ #[cfg(not(feature = "std"))] use alloc::{string::String, vec::Vec}; -use crate::{AttrsList, FontSystem, LayoutLine, ShapeLine, Wrap}; +use crate::{Align, AttrsList, FontSystem, LayoutLine, ShapeLine, Wrap}; /// A line (or paragraph) of text that is shaped and laid out pub struct BufferLine { @@ -9,6 +9,7 @@ pub struct BufferLine { text: String, attrs_list: AttrsList, wrap: Wrap, + align: Option, shape_opt: Option, layout_opt: Option>, } @@ -22,6 +23,7 @@ impl BufferLine { text: text.into(), attrs_list, wrap: Wrap::Word, + align: None, shape_opt: None, layout_opt: None, } @@ -87,7 +89,27 @@ impl BufferLine { pub fn set_wrap(&mut self, wrap: Wrap) -> bool { if wrap != self.wrap { self.wrap = wrap; - self.reset(); + self.reset_layout(); + true + } else { + false + } + } + + /// Get the Text alignment + pub fn align(&self) -> Option { + self.align + } + + /// Set the text alignment + /// + /// Will reset shape and layout if it differs from current alignment. + /// Setting to None will use `Align::Right` for RTL lines, and `Align::Left` for LTR lines. + /// Returns true if the line was reset + pub fn set_align(&mut self, align: Option) -> bool { + if align != self.align { + self.align = align; + self.reset_layout(); true } else { false @@ -168,8 +190,9 @@ impl BufferLine { ) -> &[LayoutLine] { if self.layout_opt.is_none() { self.wrap = wrap; + let align = self.align; let shape = self.shape(font_system); - let layout = shape.layout(font_size, width, wrap); + let layout = shape.layout(font_size, width, wrap, align); self.layout_opt = Some(layout); } self.layout_opt.as_ref().expect("layout not found") diff --git a/src/layout.rs b/src/layout.rs index 2625659..9ed6212 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -80,3 +80,23 @@ impl Display for Wrap { } } } + +/// Align or justify +#[derive(Debug, Eq, PartialEq, Clone, Copy)] +pub enum Align { + Left, + Right, + Center, + Justified, +} + +impl Display for Align { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::Left => write!(f, "Left"), + Self::Right => write!(f, "Right"), + Self::Center => write!(f, "Center"), + Self::Justified => write!(f, "Justified"), + } + } +} diff --git a/src/shape.rs b/src/shape.rs index 561224d..41e60f5 100644 --- a/src/shape.rs +++ b/src/shape.rs @@ -9,7 +9,7 @@ use unicode_script::{Script, UnicodeScript}; use unicode_segmentation::UnicodeSegmentation; use crate::fallback::FontFallbackIter; -use crate::{AttrsList, CacheKey, Color, Font, FontSystem, LayoutGlyph, LayoutLine, Wrap}; +use crate::{Align, AttrsList, CacheKey, Color, Font, FontSystem, LayoutGlyph, LayoutLine, Wrap}; fn shape_fallback( font: &Font, @@ -600,15 +600,36 @@ impl ShapeLine { runs } - pub fn layout(&self, font_size: i32, line_width: i32, wrap: Wrap) -> Vec { + pub fn layout( + &self, + font_size: i32, + line_width: i32, + wrap: Wrap, + align: Option, + ) -> Vec { let mut layout_lines = Vec::with_capacity(1); + let align = align.unwrap_or({ + if self.rtl { + Align::Right + } else { + Align::Left + } + }); + // This is used to create a visual line for empty lines (e.g. lines with only a ) let mut push_line = true; + #[derive(Default)] + struct VisualLine { + ranges: Vec, + spaces: u32, + w: f32, + } // For each visual line a list of (span index, and range of words in that span) // Note that a BiDi visual line could have multiple spans or parts of them - let mut vl_range_of_spans = Vec::with_capacity(1); + // let mut vl_range_of_spans = Vec::with_capacity(1); + let mut vl_range_of_spans: Vec = Vec::with_capacity(1); let start_x = if self.rtl { line_width as f32 } else { 0.0 }; let end_x = if self.rtl { 0.0 } else { line_width as f32 }; @@ -618,17 +639,21 @@ impl ShapeLine { // This would keep the maximum number of spans that would fit on a visual line // If one span is too large, this variable will hold the range of words inside that span // that fits on a line. - let mut current_visual_line: Vec = Vec::with_capacity(1); + // let mut current_visual_line: Vec = Vec::with_capacity(1); + let mut current_visual_line = VisualLine::default(); if wrap == Wrap::None { for (span_index, span) in self.spans.iter().enumerate() { - current_visual_line.push((span_index, (0, 0), (span.words.len(), 0))); + current_visual_line + .ranges + .push((span_index, (0, 0), (span.words.len(), 0))); } } else { let mut fit_x = line_width as f32; for (span_index, span) in self.spans.iter().enumerate() { let mut word_ranges = Vec::new(); let mut word_range_width = 0.; + let mut number_of_blanks = 0; // Create the word ranges that fits in a visual line if self.rtl != span.level.is_rtl() { @@ -640,6 +665,9 @@ impl ShapeLine { // fits fit_x -= word_size; word_range_width += word_size; + if word.blank { + number_of_blanks += 1; + } continue; } else if wrap == Wrap::Glyph { for (glyph_i, glyph) in word.glyphs.iter().enumerate().rev() { @@ -653,7 +681,9 @@ impl ShapeLine { (i, glyph_i + 1), fitting_start, word_range_width, + number_of_blanks, )); + number_of_blanks = 0; fit_x = line_width as f32 - glyph_size; word_range_width = glyph_size; fitting_start = (i, glyph_i + 1); @@ -661,8 +691,35 @@ impl ShapeLine { } } else { // Wrap::Word - word_ranges.push(((i + 1, 0), fitting_start, word_range_width)); - + let mut prev_word_width = None; + if word.blank && number_of_blanks > 0 { + // current word causing a wrap is a space so we ignore it + number_of_blanks -= 1; + } else if let Some(previous_word) = span.words.get(i - 1) { + // Current word causing a wrap is not whitespace, so we ignore the + // previous word if it's a whitespace + if previous_word.blank { + number_of_blanks -= 1; + prev_word_width = + Some(previous_word.x_advance * font_size as f32); + } + } + if let Some(width) = prev_word_width { + word_ranges.push(( + (i, 0), + fitting_start, + word_range_width - width, + number_of_blanks, + )); + } else { + word_ranges.push(( + (i + 1, 0), + fitting_start, + word_range_width, + number_of_blanks, + )); + } + number_of_blanks = 0; if word.blank { fit_x = line_width as f32; word_range_width = 0.; @@ -674,7 +731,7 @@ impl ShapeLine { } } } - word_ranges.push(((0, 0), fitting_start, word_range_width)); + word_ranges.push(((0, 0), fitting_start, word_range_width, number_of_blanks)); } else { // congruent direction let mut fitting_start = (0, 0); @@ -684,6 +741,9 @@ impl ShapeLine { // fits fit_x -= word_size; word_range_width += word_size; + if word.blank { + number_of_blanks += 1; + } continue; } else if wrap == Wrap::Glyph { for (glyph_i, glyph) in word.glyphs.iter().enumerate() { @@ -697,7 +757,9 @@ impl ShapeLine { fitting_start, (i, glyph_i), word_range_width, + number_of_blanks, )); + number_of_blanks = 0; fit_x = line_width as f32 - glyph_size; word_range_width = glyph_size; fitting_start = (i, glyph_i); @@ -705,7 +767,35 @@ impl ShapeLine { } } else { // Wrap::Word - word_ranges.push((fitting_start, (i, 0), word_range_width)); + let mut prev_word_width = None; + if word.blank && number_of_blanks > 0 { + // current word causing a wrap is a space so we ignore it + number_of_blanks -= 1; + } else if let Some(previous_word) = span.words.get(i - 1) { + // Current word causing a wrap is not whitespace, so we ignore the + // previous word if it's a whitespace + if previous_word.blank { + number_of_blanks -= 1; + prev_word_width = + Some(previous_word.x_advance * font_size as f32); + } + } + if let Some(width) = prev_word_width { + word_ranges.push(( + fitting_start, + (i - 1, 0), + word_range_width - width, + number_of_blanks, + )); + } else { + word_ranges.push(( + fitting_start, + (i, 0), + word_range_width, + number_of_blanks, + )); + } + number_of_blanks = 0; if word.blank { fit_x = line_width as f32; @@ -718,7 +808,12 @@ impl ShapeLine { } } } - word_ranges.push((fitting_start, (span.words.len(), 0), word_range_width)); + word_ranges.push(( + fitting_start, + (span.words.len(), 0), + word_range_width, + number_of_blanks, + )); } // Create a visual line @@ -726,6 +821,7 @@ impl ShapeLine { (starting_word, starting_glyph), (ending_word, ending_glyph), word_range_width, + number_of_blanks, ) in word_ranges { // To simplify the algorithm above, we might push empty ranges but we ignore them here @@ -740,27 +836,31 @@ impl ShapeLine { }; if fits { - current_visual_line.push(( + current_visual_line.ranges.push(( span_index, (starting_word, starting_glyph), (ending_word, ending_glyph), )); + current_visual_line.w += word_range_width; + current_visual_line.spaces += number_of_blanks; if self.rtl { x -= word_range_width; } else { x += word_range_width; } } else { - if !current_visual_line.is_empty() { + if !current_visual_line.ranges.is_empty() { vl_range_of_spans.push(current_visual_line); - current_visual_line = Vec::with_capacity(1); + current_visual_line = VisualLine::default(); x = start_x; } - current_visual_line.push(( + current_visual_line.ranges.push(( span_index, (starting_word, starting_glyph), (ending_word, ending_glyph), )); + current_visual_line.w += word_range_width; + current_visual_line.spaces += number_of_blanks; if self.rtl { x -= word_range_width; } else { @@ -769,7 +869,7 @@ impl ShapeLine { if word_range_width > line_width as f32 { // single word is bigger than line_width vl_range_of_spans.push(current_visual_line); - current_visual_line = Vec::with_capacity(1); + current_visual_line = VisualLine::default(); x = start_x; } } @@ -777,39 +877,57 @@ impl ShapeLine { } } - if !current_visual_line.is_empty() { + if !current_visual_line.ranges.is_empty() { vl_range_of_spans.push(current_visual_line); } // Create the LayoutLines using the ranges inside visual lines - for visual_line in &vl_range_of_spans { - let new_order = self.reorder(visual_line); + let number_of_visual_lines = vl_range_of_spans.len(); + for (index, visual_line) in vl_range_of_spans.iter().enumerate() { + let new_order = self.reorder(&visual_line.ranges); let mut glyphs = Vec::with_capacity(1); x = start_x; y = 0.; + let alignment_correction = match (align, self.rtl) { + (Align::Left, true) => line_width as f32 - visual_line.w, + (Align::Left, false) => 0., + (Align::Right, true) => 0., + (Align::Right, false) => line_width as f32 - visual_line.w, + (Align::Center, _) => (line_width as f32 - visual_line.w) / 2.0, + (Align::Justified, _) => { + // Don't justify the last line in a paragraph. + if visual_line.spaces > 0 && index != number_of_visual_lines - 1 { + (line_width as f32 - visual_line.w) / visual_line.spaces as f32 + } else { + 0. + } + } + }; if self.rtl { + if align != Align::Justified { + x -= alignment_correction; + } for range in new_order.iter().rev() { for ( span_index, (starting_word, starting_glyph), (ending_word, ending_glyph), - ) in visual_line[range.clone()].iter() + ) in visual_line.ranges[range.clone()].iter() { let span = &self.spans[*span_index]; if starting_word == ending_word { + let word_blank = span.words[*starting_word].blank; for glyph in span.words[*starting_word].glyphs [*starting_glyph..*ending_glyph] .iter() { let x_advance = font_size as f32 * glyph.x_advance; let y_advance = font_size as f32 * glyph.y_advance; - if self.rtl { - x -= x_advance; + x -= x_advance; + if word_blank && align == Align::Justified { + x -= alignment_correction; } glyphs.push(glyph.layout(font_size, x, y, span.level)); - if !self.rtl { - x += x_advance; - } y += y_advance; } } else { @@ -823,16 +941,15 @@ impl ShapeLine { (0, word.glyphs.len()) }; + let word_blank = word.blank; for glyph in &word.glyphs[g1..g2] { let x_advance = font_size as f32 * glyph.x_advance; let y_advance = font_size as f32 * glyph.y_advance; - if self.rtl { - x -= x_advance; + if word_blank && align == Align::Justified { + x -= alignment_correction; } + x -= x_advance; glyphs.push(glyph.layout(font_size, x, y, span.level)); - if !self.rtl { - x += x_advance; - } y += y_advance; } } @@ -841,27 +958,30 @@ impl ShapeLine { } } } else { + /* LTR */ + if align != Align::Justified { + x += alignment_correction; + } for range in new_order { for ( span_index, (starting_word, starting_glyph), (ending_word, ending_glyph), - ) in visual_line[range.clone()].iter() + ) in visual_line.ranges[range.clone()].iter() { let span = &self.spans[*span_index]; if starting_word == ending_word { + let word_blank = span.words[*starting_word].blank; for glyph in span.words[*starting_word].glyphs [*starting_glyph..*ending_glyph] .iter() { let x_advance = font_size as f32 * glyph.x_advance; let y_advance = font_size as f32 * glyph.y_advance; - if self.rtl { - x -= x_advance; - } glyphs.push(glyph.layout(font_size, x, y, span.level)); - if !self.rtl { - x += x_advance; + x += x_advance; + if word_blank && align == Align::Justified { + x += alignment_correction; } y += y_advance; } @@ -876,16 +996,15 @@ impl ShapeLine { (0, word.glyphs.len()) }; + let word_blank = word.blank; for glyph in &word.glyphs[g1..g2] { let x_advance = font_size as f32 * glyph.x_advance; let y_advance = font_size as f32 * glyph.y_advance; - if self.rtl { - x -= x_advance; - } glyphs.push(glyph.layout(font_size, x, y, span.level)); - if !self.rtl { - x += x_advance; + if word_blank && align == Align::Justified { + x += alignment_correction; } + x += x_advance; y += y_advance; } }