* feat: add Ellipsize enum * chore: API changes needed for ellipsize Decided not to change "layout()" function for now to avoid breaking the interface. For now. * chore: shape ellipsis * feat: Ellipsize::Start Since it can only have 1 line, it's easier to implement. * DROPME: temporarily change rich-text for testing * test(ellipsize): Testing Ellipsize::Start Long text in small buffer should produce ellipsis glyphs * fix: do not need font_system anymore We moved ellipsis shaping elsewhere so no need to pass font_system to layout function (which also was recreating a new one in the tests every time making them take forever). * feat: Ellipsize::End * improv(ellipsize): use a single ellipsis shape * improv: Ellipsie::End && Wrap::None There is no need to layout the whole line if it's not going to fit. * fix: mixed bidi text when Ellipsize::End && Wrap::None * chore: clean up and simplify when line.RTL==span.RTL * fix(ellipsize): last word is not (word_count -1) if iter().rev() * refactor(layout): extract the layout algorithm to make it more readable * improv(ellipsize): Ellipsize::Start && Wrap::None we iterate in reverse and only layout what's going to be visible * Revert: delete the previous approach of post processing ellipsis * doc: explain the interaction between Ellipsize and Wrap * chore: lower the scope * feat: Ellipsize the last line of a paragraph For now only the number of lines is supported * fix: clear ellipsized field on visual lines This was causing ellipsis to show on random lines * chore: remove old tests will add better tests soon * chore: clean up changes from previous attempt * fix: consider the ellipsis width when doing alignment * feat(ellipsize): add `Height` limit to `Ellipsize` * fix: ellipsize the start of the last line correctly * fix: ellipsize at the start of mixed bidi lines * feat: Ellipsize::Middle * fix: consider ellipsize::middle when calculating alignment correction * refactor: improve readability * refactor: deduplicate "fit_glyphs" * refactor: combine backward and forward layout into one (wip) * fix: Backward works in the unified layout_spans * chore: clean up * fix: Ellipsize::Middle * fix: handle large words in bidi boundaries * chore: clean up and some refactoring * fix: ellipsis is now the same level as the surrounding text * fix: try to fit more when ellipsizing::middle * improv: ellipsis now have the same level as the neighbors This makes ellipsized RTL text inside a LTR line more readable. before: Hello سلام...خوبی؟ Hello خولی؟...سلام * fix: some extra words were being rendered in Ellipsize::Middle This was causing the last word (if it's not the same level as the rest) to be rendered outside the buffer. * test: a few test cases for ellipsize * fix: assign the correct byte range to ellipsis this should fix the panic when selecting or clicking on or near the ellipsis in the editor.
1520 lines
53 KiB
Rust
1520 lines
53 KiB
Rust
// SPDX-License-Identifier: MIT OR Apache-2.0
|
|
|
|
#[cfg(not(feature = "std"))]
|
|
use alloc::{string::String, vec::Vec};
|
|
|
|
use core::{cmp, fmt};
|
|
|
|
#[cfg(not(feature = "std"))]
|
|
use core_maths::CoreFloat;
|
|
use unicode_segmentation::UnicodeSegmentation;
|
|
|
|
use crate::{
|
|
Affinity, Align, Attrs, AttrsList, BidiParagraphs, BorrowedWithFontSystem, BufferLine, Color,
|
|
Cursor, Ellipsize, FontSystem, Hinting, LayoutCursor, LayoutGlyph, LayoutLine, LineEnding,
|
|
LineIter, Motion, Renderer, Scroll, ShapeLine, Shaping, Wrap,
|
|
};
|
|
|
|
/// A line of visible text for rendering
|
|
#[derive(Debug)]
|
|
pub struct LayoutRun<'a> {
|
|
/// The index of the original text line
|
|
pub line_i: usize,
|
|
/// The original text line
|
|
pub text: &'a str,
|
|
/// True if the original paragraph direction is RTL
|
|
pub rtl: bool,
|
|
/// The array of layout glyphs to draw
|
|
pub glyphs: &'a [LayoutGlyph],
|
|
/// Y offset to baseline of line
|
|
pub line_y: f32,
|
|
/// Y offset to top of line
|
|
pub line_top: f32,
|
|
/// Y offset to next line
|
|
pub line_height: f32,
|
|
/// Width of line
|
|
pub line_w: f32,
|
|
}
|
|
|
|
impl LayoutRun<'_> {
|
|
/// Return the pixel span `Some((x_left, x_width))` of the highlighted area between `cursor_start`
|
|
/// and `cursor_end` within this run, or None if the cursor range does not intersect this run.
|
|
/// This may return widths of zero if `cursor_start == cursor_end`, if the run is empty, or if the
|
|
/// region's left start boundary is the same as the cursor's end boundary or vice versa.
|
|
#[allow(clippy::missing_panics_doc)]
|
|
pub fn highlight(&self, cursor_start: Cursor, cursor_end: Cursor) -> Option<(f32, f32)> {
|
|
let mut x_start = None;
|
|
let mut x_end = None;
|
|
let rtl_factor = if self.rtl { 1. } else { 0. };
|
|
let ltr_factor = 1. - rtl_factor;
|
|
for glyph in self.glyphs {
|
|
let cursor = self.cursor_from_glyph_left(glyph);
|
|
if cursor >= cursor_start && cursor <= cursor_end {
|
|
if x_start.is_none() {
|
|
x_start = Some(glyph.x + glyph.w.mul_add(rtl_factor, 0.0));
|
|
}
|
|
x_end = Some(glyph.x + glyph.w.mul_add(rtl_factor, 0.0));
|
|
}
|
|
let cursor = self.cursor_from_glyph_right(glyph);
|
|
if cursor >= cursor_start && cursor <= cursor_end {
|
|
if x_start.is_none() {
|
|
x_start = Some(glyph.x + glyph.w.mul_add(ltr_factor, 0.0));
|
|
}
|
|
x_end = Some(glyph.x + glyph.w.mul_add(ltr_factor, 0.0));
|
|
}
|
|
}
|
|
x_start.map(|x_start| {
|
|
let x_end = x_end.expect("end of cursor not found");
|
|
let (x_start, x_end) = if x_start < x_end {
|
|
(x_start, x_end)
|
|
} else {
|
|
(x_end, x_start)
|
|
};
|
|
(x_start, x_end - x_start)
|
|
})
|
|
}
|
|
|
|
const fn cursor_from_glyph_left(&self, glyph: &LayoutGlyph) -> Cursor {
|
|
if self.rtl {
|
|
Cursor::new_with_affinity(self.line_i, glyph.end, Affinity::Before)
|
|
} else {
|
|
Cursor::new_with_affinity(self.line_i, glyph.start, Affinity::After)
|
|
}
|
|
}
|
|
|
|
const fn cursor_from_glyph_right(&self, glyph: &LayoutGlyph) -> Cursor {
|
|
if self.rtl {
|
|
Cursor::new_with_affinity(self.line_i, glyph.start, Affinity::After)
|
|
} else {
|
|
Cursor::new_with_affinity(self.line_i, glyph.end, Affinity::Before)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// An iterator of visible text lines, see [`LayoutRun`]
|
|
#[derive(Debug)]
|
|
pub struct LayoutRunIter<'b> {
|
|
buffer: &'b Buffer,
|
|
line_i: usize,
|
|
layout_i: usize,
|
|
total_height: f32,
|
|
line_top: f32,
|
|
}
|
|
|
|
impl<'b> LayoutRunIter<'b> {
|
|
pub const fn new(buffer: &'b Buffer) -> Self {
|
|
Self {
|
|
buffer,
|
|
line_i: buffer.scroll.line,
|
|
layout_i: 0,
|
|
total_height: 0.0,
|
|
line_top: 0.0,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'b> Iterator for LayoutRunIter<'b> {
|
|
type Item = LayoutRun<'b>;
|
|
|
|
fn next(&mut self) -> Option<Self::Item> {
|
|
while let Some(line) = self.buffer.lines.get(self.line_i) {
|
|
let shape = line.shape_opt()?;
|
|
let layout = line.layout_opt()?;
|
|
while let Some(layout_line) = layout.get(self.layout_i) {
|
|
self.layout_i += 1;
|
|
|
|
let line_height = layout_line
|
|
.line_height_opt
|
|
.unwrap_or(self.buffer.metrics.line_height);
|
|
self.total_height += line_height;
|
|
|
|
let line_top = self.line_top - self.buffer.scroll.vertical;
|
|
let glyph_height = layout_line.max_ascent + layout_line.max_descent;
|
|
let centering_offset = (line_height - glyph_height) / 2.0;
|
|
let line_y = line_top + centering_offset + layout_line.max_ascent;
|
|
if let Some(height) = self.buffer.height_opt {
|
|
if line_y - layout_line.max_ascent > height {
|
|
return None;
|
|
}
|
|
}
|
|
self.line_top += line_height;
|
|
if line_y + layout_line.max_descent < 0.0 {
|
|
continue;
|
|
}
|
|
|
|
return Some(LayoutRun {
|
|
line_i: self.line_i,
|
|
text: line.text(),
|
|
rtl: shape.rtl,
|
|
glyphs: &layout_line.glyphs,
|
|
line_y,
|
|
line_top,
|
|
line_height,
|
|
line_w: layout_line.w,
|
|
});
|
|
}
|
|
self.line_i += 1;
|
|
self.layout_i = 0;
|
|
}
|
|
|
|
None
|
|
}
|
|
}
|
|
|
|
/// Metrics of text
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
|
pub struct Metrics {
|
|
/// Font size in pixels
|
|
pub font_size: f32,
|
|
/// Line height in pixels
|
|
pub line_height: f32,
|
|
}
|
|
|
|
impl Metrics {
|
|
/// Create metrics with given font size and line height
|
|
pub const fn new(font_size: f32, line_height: f32) -> Self {
|
|
Self {
|
|
font_size,
|
|
line_height,
|
|
}
|
|
}
|
|
|
|
/// Create metrics with given font size and calculate line height using relative scale
|
|
pub fn relative(font_size: f32, line_height_scale: f32) -> Self {
|
|
Self {
|
|
font_size,
|
|
line_height: font_size * line_height_scale,
|
|
}
|
|
}
|
|
|
|
/// Scale font size and line height
|
|
pub fn scale(self, scale: f32) -> Self {
|
|
Self {
|
|
font_size: self.font_size * scale,
|
|
line_height: self.line_height * scale,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for Metrics {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
write!(f, "{}px / {}px", self.font_size, self.line_height)
|
|
}
|
|
}
|
|
|
|
/// A buffer of text that is shaped and laid out
|
|
#[derive(Debug)]
|
|
pub struct Buffer {
|
|
/// [`BufferLine`]s (or paragraphs) of text in the buffer
|
|
pub lines: Vec<BufferLine>,
|
|
metrics: Metrics,
|
|
width_opt: Option<f32>,
|
|
height_opt: Option<f32>,
|
|
scroll: Scroll,
|
|
/// True if a redraw is requires. Set to false after processing
|
|
redraw: bool,
|
|
wrap: Wrap,
|
|
ellipsize: Ellipsize,
|
|
monospace_width: Option<f32>,
|
|
tab_width: u16,
|
|
hinting: Hinting,
|
|
}
|
|
|
|
impl Clone for Buffer {
|
|
fn clone(&self) -> Self {
|
|
Self {
|
|
lines: self.lines.clone(),
|
|
metrics: self.metrics,
|
|
width_opt: self.width_opt,
|
|
height_opt: self.height_opt,
|
|
scroll: self.scroll,
|
|
redraw: self.redraw,
|
|
wrap: self.wrap,
|
|
ellipsize: self.ellipsize,
|
|
monospace_width: self.monospace_width,
|
|
tab_width: self.tab_width,
|
|
hinting: self.hinting,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Buffer {
|
|
/// Create an empty [`Buffer`] with the provided [`Metrics`].
|
|
/// This is useful for initializing a [`Buffer`] without a [`FontSystem`].
|
|
///
|
|
/// You must populate the [`Buffer`] with at least one [`BufferLine`] before shaping and layout,
|
|
/// for example by calling [`Buffer::set_text`].
|
|
///
|
|
/// If you have a [`FontSystem`] in scope, you should use [`Buffer::new`] instead.
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// Will panic if `metrics.line_height` is zero.
|
|
pub fn new_empty(metrics: Metrics) -> Self {
|
|
assert_ne!(metrics.line_height, 0.0, "line height cannot be 0");
|
|
Self {
|
|
lines: Vec::new(),
|
|
metrics,
|
|
width_opt: None,
|
|
height_opt: None,
|
|
scroll: Scroll::default(),
|
|
redraw: false,
|
|
wrap: Wrap::WordOrGlyph,
|
|
ellipsize: Ellipsize::None,
|
|
monospace_width: None,
|
|
tab_width: 8,
|
|
hinting: Hinting::default(),
|
|
}
|
|
}
|
|
|
|
/// Create a new [`Buffer`] with the provided [`FontSystem`] and [`Metrics`]
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// Will panic if `metrics.line_height` is zero.
|
|
pub fn new(font_system: &mut FontSystem, metrics: Metrics) -> Self {
|
|
let mut buffer = Self::new_empty(metrics);
|
|
buffer.set_text(font_system, "", &Attrs::new(), Shaping::Advanced, None);
|
|
buffer
|
|
}
|
|
|
|
/// Mutably borrows the buffer together with an [`FontSystem`] for more convenient methods
|
|
pub fn borrow_with<'a>(
|
|
&'a mut self,
|
|
font_system: &'a mut FontSystem,
|
|
) -> BorrowedWithFontSystem<'a, Self> {
|
|
BorrowedWithFontSystem {
|
|
inner: self,
|
|
font_system,
|
|
}
|
|
}
|
|
|
|
fn relayout(&mut self, font_system: &mut FontSystem) {
|
|
#[cfg(all(feature = "std", not(target_arch = "wasm32")))]
|
|
let instant = std::time::Instant::now();
|
|
|
|
for line in &mut self.lines {
|
|
if line.shape_opt().is_some() {
|
|
line.reset_layout();
|
|
line.layout(
|
|
font_system,
|
|
self.metrics.font_size,
|
|
self.width_opt,
|
|
self.wrap,
|
|
self.ellipsize,
|
|
self.monospace_width,
|
|
self.tab_width,
|
|
self.hinting,
|
|
);
|
|
}
|
|
}
|
|
|
|
self.redraw = true;
|
|
|
|
#[cfg(all(feature = "std", not(target_arch = "wasm32")))]
|
|
log::debug!("relayout: {:?}", instant.elapsed());
|
|
}
|
|
|
|
/// Shape lines until cursor, also scrolling to include cursor in view
|
|
#[allow(clippy::missing_panics_doc)]
|
|
pub fn shape_until_cursor(
|
|
&mut self,
|
|
font_system: &mut FontSystem,
|
|
cursor: Cursor,
|
|
prune: bool,
|
|
) {
|
|
let metrics = self.metrics;
|
|
let old_scroll = self.scroll;
|
|
|
|
let layout_cursor = self
|
|
.layout_cursor(font_system, cursor)
|
|
.expect("shape_until_cursor invalid cursor");
|
|
|
|
let mut layout_y = 0.0;
|
|
let mut total_height = {
|
|
let layout = self
|
|
.line_layout(font_system, layout_cursor.line)
|
|
.expect("shape_until_cursor failed to scroll forwards");
|
|
(0..layout_cursor.layout).for_each(|layout_i| {
|
|
layout_y += layout[layout_i]
|
|
.line_height_opt
|
|
.unwrap_or(metrics.line_height);
|
|
});
|
|
layout_y
|
|
+ layout[layout_cursor.layout]
|
|
.line_height_opt
|
|
.unwrap_or(metrics.line_height)
|
|
};
|
|
|
|
if self.scroll.line > layout_cursor.line
|
|
|| (self.scroll.line == layout_cursor.line && self.scroll.vertical > layout_y)
|
|
{
|
|
// Adjust scroll backwards if cursor is before it
|
|
self.scroll.line = layout_cursor.line;
|
|
self.scroll.vertical = layout_y;
|
|
} else if let Some(height) = self.height_opt {
|
|
// Adjust scroll forwards if cursor is after it
|
|
let mut line_i = layout_cursor.line;
|
|
if line_i <= self.scroll.line {
|
|
// This is a single line that may wrap
|
|
if total_height > height + self.scroll.vertical {
|
|
self.scroll.vertical = total_height - height;
|
|
}
|
|
} else {
|
|
while line_i > self.scroll.line {
|
|
line_i -= 1;
|
|
let layout = self
|
|
.line_layout(font_system, line_i)
|
|
.expect("shape_until_cursor failed to scroll forwards");
|
|
for layout_line in layout {
|
|
total_height += layout_line.line_height_opt.unwrap_or(metrics.line_height);
|
|
}
|
|
if total_height > height + self.scroll.vertical {
|
|
self.scroll.line = line_i;
|
|
self.scroll.vertical = total_height - height;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if old_scroll != self.scroll {
|
|
self.redraw = true;
|
|
}
|
|
|
|
self.shape_until_scroll(font_system, prune);
|
|
|
|
// Adjust horizontal scroll to include cursor
|
|
if let Some(layout_cursor) = self.layout_cursor(font_system, cursor) {
|
|
if let Some(layout_lines) = self.line_layout(font_system, layout_cursor.line) {
|
|
if let Some(layout_line) = layout_lines.get(layout_cursor.layout) {
|
|
let (x_min, x_max) = layout_line
|
|
.glyphs
|
|
.get(layout_cursor.glyph)
|
|
.or_else(|| layout_line.glyphs.last())
|
|
.map_or((0.0, 0.0), |glyph| {
|
|
//TODO: use code from cursor_glyph_opt?
|
|
let x_a = glyph.x;
|
|
let x_b = glyph.x + glyph.w;
|
|
(x_a.min(x_b), x_a.max(x_b))
|
|
});
|
|
if x_min < self.scroll.horizontal {
|
|
self.scroll.horizontal = x_min;
|
|
self.redraw = true;
|
|
}
|
|
if let Some(width) = self.width_opt {
|
|
if x_max > self.scroll.horizontal + width {
|
|
self.scroll.horizontal = x_max - width;
|
|
self.redraw = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Shape lines until scroll
|
|
#[allow(clippy::missing_panics_doc)]
|
|
pub fn shape_until_scroll(&mut self, font_system: &mut FontSystem, prune: bool) {
|
|
let metrics = self.metrics;
|
|
let old_scroll = self.scroll;
|
|
|
|
loop {
|
|
// Adjust scroll.layout to be positive by moving scroll.line backwards
|
|
while self.scroll.vertical < 0.0 {
|
|
if self.scroll.line > 0 {
|
|
let line_i = self.scroll.line - 1;
|
|
if let Some(layout) = self.line_layout(font_system, line_i) {
|
|
let mut layout_height = 0.0;
|
|
for layout_line in layout {
|
|
layout_height +=
|
|
layout_line.line_height_opt.unwrap_or(metrics.line_height);
|
|
}
|
|
self.scroll.line = line_i;
|
|
self.scroll.vertical += layout_height;
|
|
} else {
|
|
// If layout is missing, just assume line height
|
|
self.scroll.line = line_i;
|
|
self.scroll.vertical += metrics.line_height;
|
|
}
|
|
} else {
|
|
self.scroll.vertical = 0.0;
|
|
break;
|
|
}
|
|
}
|
|
|
|
let scroll_start = self.scroll.vertical;
|
|
let scroll_end = scroll_start + self.height_opt.unwrap_or(f32::INFINITY);
|
|
|
|
let mut total_height = 0.0;
|
|
for line_i in 0..self.lines.len() {
|
|
if line_i < self.scroll.line {
|
|
if prune {
|
|
self.lines[line_i].reset_shaping();
|
|
}
|
|
continue;
|
|
}
|
|
if total_height > scroll_end {
|
|
if prune {
|
|
self.lines[line_i].reset_shaping();
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
|
|
let mut layout_height = 0.0;
|
|
let layout = self
|
|
.line_layout(font_system, line_i)
|
|
.expect("shape_until_scroll invalid line");
|
|
for layout_line in layout {
|
|
let line_height = layout_line.line_height_opt.unwrap_or(metrics.line_height);
|
|
layout_height += line_height;
|
|
total_height += line_height;
|
|
}
|
|
|
|
// Adjust scroll.vertical to be smaller by moving scroll.line forwards
|
|
if line_i == self.scroll.line && layout_height <= self.scroll.vertical {
|
|
self.scroll.line += 1;
|
|
self.scroll.vertical -= layout_height;
|
|
}
|
|
}
|
|
|
|
if total_height < scroll_end && self.scroll.line > 0 {
|
|
// Need to scroll up to stay inside of buffer
|
|
self.scroll.vertical -= scroll_end - total_height;
|
|
} else {
|
|
// Done adjusting scroll
|
|
break;
|
|
}
|
|
}
|
|
|
|
if old_scroll != self.scroll {
|
|
self.redraw = true;
|
|
}
|
|
}
|
|
|
|
/// Convert a [`Cursor`] to a [`LayoutCursor`]
|
|
pub fn layout_cursor(
|
|
&mut self,
|
|
font_system: &mut FontSystem,
|
|
cursor: Cursor,
|
|
) -> Option<LayoutCursor> {
|
|
let layout = self.line_layout(font_system, cursor.line)?;
|
|
for (layout_i, layout_line) in layout.iter().enumerate() {
|
|
for (glyph_i, glyph) in layout_line.glyphs.iter().enumerate() {
|
|
let cursor_end =
|
|
Cursor::new_with_affinity(cursor.line, glyph.end, Affinity::Before);
|
|
let cursor_start =
|
|
Cursor::new_with_affinity(cursor.line, glyph.start, Affinity::After);
|
|
let (cursor_left, cursor_right) = if glyph.level.is_ltr() {
|
|
(cursor_start, cursor_end)
|
|
} else {
|
|
(cursor_end, cursor_start)
|
|
};
|
|
if cursor == cursor_left {
|
|
return Some(LayoutCursor::new(cursor.line, layout_i, glyph_i));
|
|
}
|
|
if cursor == cursor_right {
|
|
return Some(LayoutCursor::new(cursor.line, layout_i, glyph_i + 1));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fall back to start of line
|
|
//TODO: should this be the end of the line?
|
|
Some(LayoutCursor::new(cursor.line, 0, 0))
|
|
}
|
|
|
|
/// Shape the provided line index and return the result
|
|
pub fn line_shape(
|
|
&mut self,
|
|
font_system: &mut FontSystem,
|
|
line_i: usize,
|
|
) -> Option<&ShapeLine> {
|
|
let line = self.lines.get_mut(line_i)?;
|
|
Some(line.shape(font_system, self.tab_width))
|
|
}
|
|
|
|
/// Lay out the provided line index and return the result
|
|
pub fn line_layout(
|
|
&mut self,
|
|
font_system: &mut FontSystem,
|
|
line_i: usize,
|
|
) -> Option<&[LayoutLine]> {
|
|
let line = self.lines.get_mut(line_i)?;
|
|
Some(line.layout(
|
|
font_system,
|
|
self.metrics.font_size,
|
|
self.width_opt,
|
|
self.wrap,
|
|
self.ellipsize,
|
|
self.monospace_width,
|
|
self.tab_width,
|
|
self.hinting,
|
|
))
|
|
}
|
|
|
|
/// Get the current [`Metrics`]
|
|
pub const fn metrics(&self) -> Metrics {
|
|
self.metrics
|
|
}
|
|
|
|
/// Set the current [`Metrics`]
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// Will panic if `metrics.font_size` is zero.
|
|
pub fn set_metrics(&mut self, font_system: &mut FontSystem, metrics: Metrics) {
|
|
self.set_metrics_and_size(font_system, metrics, self.width_opt, self.height_opt);
|
|
}
|
|
|
|
/// Get the current [`Hinting`] strategy.
|
|
pub const fn hinting(&self) -> Hinting {
|
|
self.hinting
|
|
}
|
|
|
|
/// Set the current [`Hinting`] strategy.
|
|
pub fn set_hinting(&mut self, font_system: &mut FontSystem, hinting: Hinting) {
|
|
if hinting != self.hinting {
|
|
self.hinting = hinting;
|
|
self.relayout(font_system);
|
|
self.shape_until_scroll(font_system, false);
|
|
}
|
|
}
|
|
|
|
/// Get the current [`Wrap`]
|
|
pub const fn wrap(&self) -> Wrap {
|
|
self.wrap
|
|
}
|
|
|
|
/// Set the current [`Wrap`]
|
|
pub fn set_wrap(&mut self, font_system: &mut FontSystem, wrap: Wrap) {
|
|
if wrap != self.wrap {
|
|
self.wrap = wrap;
|
|
self.relayout(font_system);
|
|
self.shape_until_scroll(font_system, false);
|
|
}
|
|
}
|
|
|
|
/// Get the current [`Ellipsize`]
|
|
pub const fn ellipsize(&self) -> Ellipsize {
|
|
self.ellipsize
|
|
}
|
|
|
|
/// Set the current [`Ellipsize`]
|
|
pub fn set_ellipsize(&mut self, font_system: &mut FontSystem, ellipsize: Ellipsize) {
|
|
if ellipsize != self.ellipsize {
|
|
self.ellipsize = ellipsize;
|
|
self.relayout(font_system);
|
|
self.shape_until_scroll(font_system, false);
|
|
}
|
|
}
|
|
|
|
/// Get the current `monospace_width`
|
|
pub const fn monospace_width(&self) -> Option<f32> {
|
|
self.monospace_width
|
|
}
|
|
|
|
/// Set monospace width monospace glyphs should be resized to match. `None` means don't resize
|
|
pub fn set_monospace_width(
|
|
&mut self,
|
|
font_system: &mut FontSystem,
|
|
monospace_width: Option<f32>,
|
|
) {
|
|
if monospace_width != self.monospace_width {
|
|
self.monospace_width = monospace_width;
|
|
self.relayout(font_system);
|
|
self.shape_until_scroll(font_system, false);
|
|
}
|
|
}
|
|
|
|
/// Get the current `tab_width`
|
|
pub const fn tab_width(&self) -> u16 {
|
|
self.tab_width
|
|
}
|
|
|
|
/// Set tab width (number of spaces between tab stops)
|
|
pub fn set_tab_width(&mut self, font_system: &mut FontSystem, tab_width: u16) {
|
|
// A tab width of 0 is not allowed
|
|
if tab_width == 0 {
|
|
return;
|
|
}
|
|
if tab_width != self.tab_width {
|
|
self.tab_width = tab_width;
|
|
// Shaping must be reset when tab width is changed
|
|
for line in &mut self.lines {
|
|
if line.shape_opt().is_some() && line.text().contains('\t') {
|
|
line.reset_shaping();
|
|
}
|
|
}
|
|
self.redraw = true;
|
|
self.shape_until_scroll(font_system, false);
|
|
}
|
|
}
|
|
|
|
/// Get the current buffer dimensions (width, height)
|
|
pub const fn size(&self) -> (Option<f32>, Option<f32>) {
|
|
(self.width_opt, self.height_opt)
|
|
}
|
|
|
|
/// Set the current buffer dimensions
|
|
pub fn set_size(
|
|
&mut self,
|
|
font_system: &mut FontSystem,
|
|
width_opt: Option<f32>,
|
|
height_opt: Option<f32>,
|
|
) {
|
|
self.set_metrics_and_size(font_system, self.metrics, width_opt, height_opt);
|
|
}
|
|
|
|
/// Set the current [`Metrics`] and buffer dimensions at the same time
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// Will panic if `metrics.font_size` is zero.
|
|
pub fn set_metrics_and_size(
|
|
&mut self,
|
|
font_system: &mut FontSystem,
|
|
metrics: Metrics,
|
|
width_opt: Option<f32>,
|
|
height_opt: Option<f32>,
|
|
) {
|
|
let clamped_width_opt = width_opt.map(|width| width.max(0.0));
|
|
let clamped_height_opt = height_opt.map(|height| height.max(0.0));
|
|
|
|
if metrics != self.metrics
|
|
|| clamped_width_opt != self.width_opt
|
|
|| clamped_height_opt != self.height_opt
|
|
{
|
|
assert_ne!(metrics.font_size, 0.0, "font size cannot be 0");
|
|
self.metrics = metrics;
|
|
self.width_opt = clamped_width_opt;
|
|
self.height_opt = clamped_height_opt;
|
|
self.relayout(font_system);
|
|
self.shape_until_scroll(font_system, false);
|
|
}
|
|
}
|
|
|
|
/// Get the current scroll location
|
|
pub const fn scroll(&self) -> Scroll {
|
|
self.scroll
|
|
}
|
|
|
|
/// Set the current scroll location
|
|
pub fn set_scroll(&mut self, scroll: Scroll) {
|
|
if scroll != self.scroll {
|
|
self.scroll = scroll;
|
|
self.redraw = true;
|
|
}
|
|
}
|
|
|
|
/// Set text of buffer, using provided attributes for each line by default
|
|
pub fn set_text(
|
|
&mut self,
|
|
font_system: &mut FontSystem,
|
|
text: &str,
|
|
attrs: &Attrs,
|
|
shaping: Shaping,
|
|
alignment: Option<Align>,
|
|
) {
|
|
self.lines.clear();
|
|
for (range, ending) in LineIter::new(text) {
|
|
self.lines.push(BufferLine::new(
|
|
&text[range],
|
|
ending,
|
|
AttrsList::new(attrs),
|
|
shaping,
|
|
));
|
|
}
|
|
|
|
// Ensure there is an ending line with no line ending
|
|
if self
|
|
.lines
|
|
.last()
|
|
.map(|line| line.ending())
|
|
.unwrap_or_default()
|
|
!= LineEnding::None
|
|
{
|
|
self.lines.push(BufferLine::new(
|
|
"",
|
|
LineEnding::None,
|
|
AttrsList::new(attrs),
|
|
shaping,
|
|
));
|
|
}
|
|
|
|
if alignment.is_some() {
|
|
self.lines.iter_mut().for_each(|line| {
|
|
line.set_align(alignment);
|
|
});
|
|
}
|
|
|
|
self.scroll = Scroll::default();
|
|
self.shape_until_scroll(font_system, false);
|
|
}
|
|
|
|
/// Set text of buffer, using an iterator of styled spans (pairs of text and attributes)
|
|
///
|
|
/// ```
|
|
/// # use cosmic_text::{Attrs, Buffer, Family, FontSystem, Metrics, Shaping};
|
|
/// # let mut font_system = FontSystem::new();
|
|
/// let mut buffer = Buffer::new_empty(Metrics::new(32.0, 44.0));
|
|
/// let attrs = Attrs::new().family(Family::Serif);
|
|
/// buffer.set_rich_text(
|
|
/// &mut font_system,
|
|
/// [
|
|
/// ("hello, ", attrs.clone()),
|
|
/// ("cosmic\ntext", attrs.clone().family(Family::Monospace)),
|
|
/// ],
|
|
/// &attrs,
|
|
/// Shaping::Advanced,
|
|
/// None,
|
|
/// );
|
|
/// ```
|
|
pub fn set_rich_text<'r, 's, I>(
|
|
&mut self,
|
|
font_system: &mut FontSystem,
|
|
spans: I,
|
|
default_attrs: &Attrs,
|
|
shaping: Shaping,
|
|
alignment: Option<Align>,
|
|
) where
|
|
I: IntoIterator<Item = (&'s str, Attrs<'r>)>,
|
|
{
|
|
let mut end = 0;
|
|
// TODO: find a way to cache this string and vec for reuse
|
|
let (string, spans_data): (String, Vec<_>) = spans
|
|
.into_iter()
|
|
.map(|(s, attrs)| {
|
|
let start = end;
|
|
end += s.len();
|
|
(s, (attrs, start..end))
|
|
})
|
|
.unzip();
|
|
|
|
let mut spans_iter = spans_data.into_iter();
|
|
let mut maybe_span = spans_iter.next();
|
|
|
|
// split the string into lines, as ranges
|
|
let string_start = string.as_ptr() as usize;
|
|
let mut lines_iter = BidiParagraphs::new(&string).map(|line: &str| {
|
|
let start = line.as_ptr() as usize - string_start;
|
|
let end = start + line.len();
|
|
start..end
|
|
});
|
|
let mut maybe_line = lines_iter.next();
|
|
//TODO: set this based on information from spans
|
|
let line_ending = LineEnding::default();
|
|
|
|
let mut line_count = 0;
|
|
let mut attrs_list = self
|
|
.lines
|
|
.get_mut(line_count)
|
|
.map_or_else(|| AttrsList::new(&Attrs::new()), BufferLine::reclaim_attrs)
|
|
.reset(default_attrs);
|
|
let mut line_string = self
|
|
.lines
|
|
.get_mut(line_count)
|
|
.map(BufferLine::reclaim_text)
|
|
.unwrap_or_default();
|
|
|
|
loop {
|
|
let (Some(line_range), Some((attrs, span_range))) = (&maybe_line, &maybe_span) else {
|
|
// this is reached only if this text is empty
|
|
if self.lines.len() == line_count {
|
|
self.lines.push(BufferLine::empty());
|
|
}
|
|
self.lines[line_count].reset_new(
|
|
String::new(),
|
|
line_ending,
|
|
AttrsList::new(default_attrs),
|
|
shaping,
|
|
);
|
|
line_count += 1;
|
|
break;
|
|
};
|
|
|
|
// start..end is the intersection of this line and this span
|
|
let start = line_range.start.max(span_range.start);
|
|
let end = line_range.end.min(span_range.end);
|
|
if start < end {
|
|
let text = &string[start..end];
|
|
let text_start = line_string.len();
|
|
line_string.push_str(text);
|
|
let text_end = line_string.len();
|
|
// Only add attrs if they don't match the defaults
|
|
if *attrs != attrs_list.defaults() {
|
|
attrs_list.add_span(text_start..text_end, attrs);
|
|
}
|
|
}
|
|
|
|
// we know that at the end of a line,
|
|
// span text's end index is always >= line text's end index
|
|
// so if this span ends before this line ends,
|
|
// there is another span in this line.
|
|
// otherwise, we move on to the next line.
|
|
if span_range.end < line_range.end {
|
|
maybe_span = spans_iter.next();
|
|
} else {
|
|
maybe_line = lines_iter.next();
|
|
if maybe_line.is_some() {
|
|
// finalize this line and start a new line
|
|
let next_attrs_list = self
|
|
.lines
|
|
.get_mut(line_count + 1)
|
|
.map_or_else(|| AttrsList::new(&Attrs::new()), BufferLine::reclaim_attrs)
|
|
.reset(default_attrs);
|
|
let next_line_string = self
|
|
.lines
|
|
.get_mut(line_count + 1)
|
|
.map(BufferLine::reclaim_text)
|
|
.unwrap_or_default();
|
|
let prev_attrs_list = core::mem::replace(&mut attrs_list, next_attrs_list);
|
|
let prev_line_string = core::mem::replace(&mut line_string, next_line_string);
|
|
if self.lines.len() == line_count {
|
|
self.lines.push(BufferLine::empty());
|
|
}
|
|
self.lines[line_count].reset_new(
|
|
prev_line_string,
|
|
line_ending,
|
|
prev_attrs_list,
|
|
shaping,
|
|
);
|
|
line_count += 1;
|
|
} else {
|
|
// finalize the final line
|
|
if self.lines.len() == line_count {
|
|
self.lines.push(BufferLine::empty());
|
|
}
|
|
self.lines[line_count].reset_new(line_string, line_ending, attrs_list, shaping);
|
|
line_count += 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Discard excess lines now that we have reused as much of the existing allocations as possible.
|
|
self.lines.truncate(line_count);
|
|
|
|
self.lines.iter_mut().for_each(|line| {
|
|
line.set_align(alignment);
|
|
});
|
|
|
|
self.scroll = Scroll::default();
|
|
|
|
self.shape_until_scroll(font_system, false);
|
|
}
|
|
|
|
/// True if a redraw is needed
|
|
pub const fn redraw(&self) -> bool {
|
|
self.redraw
|
|
}
|
|
|
|
/// Set redraw needed flag
|
|
pub fn set_redraw(&mut self, redraw: bool) {
|
|
self.redraw = redraw;
|
|
}
|
|
|
|
/// Get the visible layout runs for rendering and other tasks
|
|
pub fn layout_runs(&self) -> LayoutRunIter<'_> {
|
|
LayoutRunIter::new(self)
|
|
}
|
|
|
|
/// Convert x, y position to Cursor (hit detection)
|
|
pub fn hit(&self, x: f32, y: f32) -> Option<Cursor> {
|
|
#[cfg(all(feature = "std", not(target_arch = "wasm32")))]
|
|
let instant = std::time::Instant::now();
|
|
|
|
let mut new_cursor_opt = None;
|
|
|
|
let mut runs = self.layout_runs().peekable();
|
|
let mut first_run = true;
|
|
while let Some(run) = runs.next() {
|
|
let line_top = run.line_top;
|
|
let line_height = run.line_height;
|
|
|
|
if first_run && y < line_top {
|
|
first_run = false;
|
|
let new_cursor = Cursor::new(run.line_i, 0);
|
|
new_cursor_opt = Some(new_cursor);
|
|
} else if y >= line_top && y < line_top + line_height {
|
|
let mut new_cursor_glyph = run.glyphs.len();
|
|
let mut new_cursor_char = 0;
|
|
let mut new_cursor_affinity = Affinity::After;
|
|
|
|
let mut first_glyph = true;
|
|
|
|
'hit: for (glyph_i, glyph) in run.glyphs.iter().enumerate() {
|
|
if first_glyph {
|
|
first_glyph = false;
|
|
if (run.rtl && x > glyph.x) || (!run.rtl && x < 0.0) {
|
|
new_cursor_glyph = 0;
|
|
new_cursor_char = 0;
|
|
}
|
|
}
|
|
if x >= glyph.x && x <= glyph.x + glyph.w {
|
|
new_cursor_glyph = glyph_i;
|
|
|
|
let cluster = &run.text[glyph.start..glyph.end];
|
|
let total = cluster.grapheme_indices(true).count();
|
|
let mut egc_x = glyph.x;
|
|
let egc_w = glyph.w / (total as f32);
|
|
for (egc_i, egc) in cluster.grapheme_indices(true) {
|
|
if x >= egc_x && x <= egc_x + egc_w {
|
|
new_cursor_char = egc_i;
|
|
|
|
let right_half = x >= egc_x + egc_w / 2.0;
|
|
if right_half != glyph.level.is_rtl() {
|
|
// If clicking on last half of glyph, move cursor past glyph
|
|
new_cursor_char += egc.len();
|
|
new_cursor_affinity = Affinity::Before;
|
|
}
|
|
break 'hit;
|
|
}
|
|
egc_x += egc_w;
|
|
}
|
|
|
|
let right_half = x >= glyph.x + glyph.w / 2.0;
|
|
if right_half != glyph.level.is_rtl() {
|
|
// If clicking on last half of glyph, move cursor past glyph
|
|
new_cursor_char = cluster.len();
|
|
new_cursor_affinity = Affinity::Before;
|
|
}
|
|
break 'hit;
|
|
}
|
|
}
|
|
|
|
let mut new_cursor = Cursor::new(run.line_i, 0);
|
|
|
|
match run.glyphs.get(new_cursor_glyph) {
|
|
Some(glyph) => {
|
|
// Position at glyph
|
|
new_cursor.index = glyph.start + new_cursor_char;
|
|
new_cursor.affinity = new_cursor_affinity;
|
|
}
|
|
None => {
|
|
if let Some(glyph) = run.glyphs.last() {
|
|
// Position at end of line
|
|
new_cursor.index = glyph.end;
|
|
new_cursor.affinity = Affinity::Before;
|
|
}
|
|
}
|
|
}
|
|
|
|
new_cursor_opt = Some(new_cursor);
|
|
|
|
break;
|
|
} else if runs.peek().is_none() && y > run.line_y {
|
|
let mut new_cursor = Cursor::new(run.line_i, 0);
|
|
if let Some(glyph) = run.glyphs.last() {
|
|
new_cursor = run.cursor_from_glyph_right(glyph);
|
|
}
|
|
new_cursor_opt = Some(new_cursor);
|
|
}
|
|
}
|
|
|
|
#[cfg(all(feature = "std", not(target_arch = "wasm32")))]
|
|
log::trace!("click({}, {}): {:?}", x, y, instant.elapsed());
|
|
|
|
new_cursor_opt
|
|
}
|
|
|
|
/// Apply a [`Motion`] to a [`Cursor`]
|
|
pub fn cursor_motion(
|
|
&mut self,
|
|
font_system: &mut FontSystem,
|
|
mut cursor: Cursor,
|
|
mut cursor_x_opt: Option<i32>,
|
|
motion: Motion,
|
|
) -> Option<(Cursor, Option<i32>)> {
|
|
match motion {
|
|
Motion::LayoutCursor(layout_cursor) => {
|
|
let layout = self.line_layout(font_system, layout_cursor.line)?;
|
|
|
|
let layout_line = match layout.get(layout_cursor.layout) {
|
|
Some(some) => some,
|
|
None => match layout.last() {
|
|
Some(some) => some,
|
|
None => {
|
|
return None;
|
|
}
|
|
},
|
|
};
|
|
|
|
let (new_index, new_affinity) =
|
|
layout_line.glyphs.get(layout_cursor.glyph).map_or_else(
|
|
|| {
|
|
layout_line
|
|
.glyphs
|
|
.last()
|
|
.map_or((0, Affinity::After), |glyph| (glyph.end, Affinity::Before))
|
|
},
|
|
|glyph| (glyph.start, Affinity::After),
|
|
);
|
|
|
|
if cursor.line != layout_cursor.line
|
|
|| cursor.index != new_index
|
|
|| cursor.affinity != new_affinity
|
|
{
|
|
cursor.line = layout_cursor.line;
|
|
cursor.index = new_index;
|
|
cursor.affinity = new_affinity;
|
|
}
|
|
}
|
|
Motion::Previous => {
|
|
let line = self.lines.get(cursor.line)?;
|
|
if cursor.index > 0 {
|
|
// Find previous character index
|
|
let mut prev_index = 0;
|
|
for (i, _) in line.text().grapheme_indices(true) {
|
|
if i < cursor.index {
|
|
prev_index = i;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
cursor.index = prev_index;
|
|
cursor.affinity = Affinity::After;
|
|
} else if cursor.line > 0 {
|
|
cursor.line -= 1;
|
|
cursor.index = self.lines.get(cursor.line)?.text().len();
|
|
cursor.affinity = Affinity::After;
|
|
}
|
|
cursor_x_opt = None;
|
|
}
|
|
Motion::Next => {
|
|
let line = self.lines.get(cursor.line)?;
|
|
if cursor.index < line.text().len() {
|
|
for (i, c) in line.text().grapheme_indices(true) {
|
|
if i == cursor.index {
|
|
cursor.index += c.len();
|
|
cursor.affinity = Affinity::Before;
|
|
break;
|
|
}
|
|
}
|
|
} else if cursor.line + 1 < self.lines.len() {
|
|
cursor.line += 1;
|
|
cursor.index = 0;
|
|
cursor.affinity = Affinity::Before;
|
|
}
|
|
cursor_x_opt = None;
|
|
}
|
|
Motion::Left => {
|
|
let rtl_opt = self
|
|
.line_shape(font_system, cursor.line)
|
|
.map(|shape| shape.rtl);
|
|
if let Some(rtl) = rtl_opt {
|
|
if rtl {
|
|
(cursor, cursor_x_opt) =
|
|
self.cursor_motion(font_system, cursor, cursor_x_opt, Motion::Next)?;
|
|
} else {
|
|
(cursor, cursor_x_opt) = self.cursor_motion(
|
|
font_system,
|
|
cursor,
|
|
cursor_x_opt,
|
|
Motion::Previous,
|
|
)?;
|
|
}
|
|
}
|
|
}
|
|
Motion::Right => {
|
|
let rtl_opt = self
|
|
.line_shape(font_system, cursor.line)
|
|
.map(|shape| shape.rtl);
|
|
if let Some(rtl) = rtl_opt {
|
|
if rtl {
|
|
(cursor, cursor_x_opt) = self.cursor_motion(
|
|
font_system,
|
|
cursor,
|
|
cursor_x_opt,
|
|
Motion::Previous,
|
|
)?;
|
|
} else {
|
|
(cursor, cursor_x_opt) =
|
|
self.cursor_motion(font_system, cursor, cursor_x_opt, Motion::Next)?;
|
|
}
|
|
}
|
|
}
|
|
Motion::Up => {
|
|
let mut layout_cursor = self.layout_cursor(font_system, cursor)?;
|
|
|
|
if cursor_x_opt.is_none() {
|
|
cursor_x_opt = Some(
|
|
layout_cursor.glyph as i32, //TODO: glyph x position
|
|
);
|
|
}
|
|
|
|
if layout_cursor.layout > 0 {
|
|
layout_cursor.layout -= 1;
|
|
} else if layout_cursor.line > 0 {
|
|
layout_cursor.line -= 1;
|
|
layout_cursor.layout = usize::MAX;
|
|
}
|
|
|
|
if let Some(cursor_x) = cursor_x_opt {
|
|
layout_cursor.glyph = cursor_x as usize; //TODO: glyph x position
|
|
}
|
|
|
|
(cursor, cursor_x_opt) = self.cursor_motion(
|
|
font_system,
|
|
cursor,
|
|
cursor_x_opt,
|
|
Motion::LayoutCursor(layout_cursor),
|
|
)?;
|
|
}
|
|
Motion::Down => {
|
|
let mut layout_cursor = self.layout_cursor(font_system, cursor)?;
|
|
|
|
let layout_len = self.line_layout(font_system, layout_cursor.line)?.len();
|
|
|
|
if cursor_x_opt.is_none() {
|
|
cursor_x_opt = Some(
|
|
layout_cursor.glyph as i32, //TODO: glyph x position
|
|
);
|
|
}
|
|
|
|
if layout_cursor.layout + 1 < layout_len {
|
|
layout_cursor.layout += 1;
|
|
} else if layout_cursor.line + 1 < self.lines.len() {
|
|
layout_cursor.line += 1;
|
|
layout_cursor.layout = 0;
|
|
}
|
|
|
|
if let Some(cursor_x) = cursor_x_opt {
|
|
layout_cursor.glyph = cursor_x as usize; //TODO: glyph x position
|
|
}
|
|
|
|
(cursor, cursor_x_opt) = self.cursor_motion(
|
|
font_system,
|
|
cursor,
|
|
cursor_x_opt,
|
|
Motion::LayoutCursor(layout_cursor),
|
|
)?;
|
|
}
|
|
Motion::Home => {
|
|
cursor.index = 0;
|
|
cursor_x_opt = None;
|
|
}
|
|
Motion::SoftHome => {
|
|
let line = self.lines.get(cursor.line)?;
|
|
cursor.index = line
|
|
.text()
|
|
.char_indices()
|
|
.find_map(|(i, c)| if c.is_whitespace() { None } else { Some(i) })
|
|
.unwrap_or(0);
|
|
cursor_x_opt = None;
|
|
}
|
|
Motion::End => {
|
|
let line = self.lines.get(cursor.line)?;
|
|
cursor.index = line.text().len();
|
|
cursor_x_opt = None;
|
|
}
|
|
Motion::ParagraphStart => {
|
|
cursor.index = 0;
|
|
cursor_x_opt = None;
|
|
}
|
|
Motion::ParagraphEnd => {
|
|
cursor.index = self.lines.get(cursor.line)?.text().len();
|
|
cursor_x_opt = None;
|
|
}
|
|
Motion::PageUp => {
|
|
if let Some(height) = self.height_opt {
|
|
(cursor, cursor_x_opt) = self.cursor_motion(
|
|
font_system,
|
|
cursor,
|
|
cursor_x_opt,
|
|
Motion::Vertical(-height as i32),
|
|
)?;
|
|
}
|
|
}
|
|
Motion::PageDown => {
|
|
if let Some(height) = self.height_opt {
|
|
(cursor, cursor_x_opt) = self.cursor_motion(
|
|
font_system,
|
|
cursor,
|
|
cursor_x_opt,
|
|
Motion::Vertical(height as i32),
|
|
)?;
|
|
}
|
|
}
|
|
Motion::Vertical(px) => {
|
|
// TODO more efficient, use layout run line height
|
|
let lines = px / self.metrics().line_height as i32;
|
|
match lines.cmp(&0) {
|
|
cmp::Ordering::Less => {
|
|
for _ in 0..-lines {
|
|
(cursor, cursor_x_opt) =
|
|
self.cursor_motion(font_system, cursor, cursor_x_opt, Motion::Up)?;
|
|
}
|
|
}
|
|
cmp::Ordering::Greater => {
|
|
for _ in 0..lines {
|
|
(cursor, cursor_x_opt) = self.cursor_motion(
|
|
font_system,
|
|
cursor,
|
|
cursor_x_opt,
|
|
Motion::Down,
|
|
)?;
|
|
}
|
|
}
|
|
cmp::Ordering::Equal => {}
|
|
}
|
|
}
|
|
Motion::PreviousWord => {
|
|
let line = self.lines.get(cursor.line)?;
|
|
if cursor.index > 0 {
|
|
cursor.index = line
|
|
.text()
|
|
.unicode_word_indices()
|
|
.rev()
|
|
.map(|(i, _)| i)
|
|
.find(|&i| i < cursor.index)
|
|
.unwrap_or(0);
|
|
} else if cursor.line > 0 {
|
|
cursor.line -= 1;
|
|
cursor.index = self.lines.get(cursor.line)?.text().len();
|
|
}
|
|
cursor_x_opt = None;
|
|
}
|
|
Motion::NextWord => {
|
|
let line = self.lines.get(cursor.line)?;
|
|
if cursor.index < line.text().len() {
|
|
cursor.index = line
|
|
.text()
|
|
.unicode_word_indices()
|
|
.map(|(i, word)| i + word.len())
|
|
.find(|&i| i > cursor.index)
|
|
.unwrap_or_else(|| line.text().len());
|
|
} else if cursor.line + 1 < self.lines.len() {
|
|
cursor.line += 1;
|
|
cursor.index = 0;
|
|
}
|
|
cursor_x_opt = None;
|
|
}
|
|
Motion::LeftWord => {
|
|
let rtl_opt = self
|
|
.line_shape(font_system, cursor.line)
|
|
.map(|shape| shape.rtl);
|
|
if let Some(rtl) = rtl_opt {
|
|
if rtl {
|
|
(cursor, cursor_x_opt) = self.cursor_motion(
|
|
font_system,
|
|
cursor,
|
|
cursor_x_opt,
|
|
Motion::NextWord,
|
|
)?;
|
|
} else {
|
|
(cursor, cursor_x_opt) = self.cursor_motion(
|
|
font_system,
|
|
cursor,
|
|
cursor_x_opt,
|
|
Motion::PreviousWord,
|
|
)?;
|
|
}
|
|
}
|
|
}
|
|
Motion::RightWord => {
|
|
let rtl_opt = self
|
|
.line_shape(font_system, cursor.line)
|
|
.map(|shape| shape.rtl);
|
|
if let Some(rtl) = rtl_opt {
|
|
if rtl {
|
|
(cursor, cursor_x_opt) = self.cursor_motion(
|
|
font_system,
|
|
cursor,
|
|
cursor_x_opt,
|
|
Motion::PreviousWord,
|
|
)?;
|
|
} else {
|
|
(cursor, cursor_x_opt) = self.cursor_motion(
|
|
font_system,
|
|
cursor,
|
|
cursor_x_opt,
|
|
Motion::NextWord,
|
|
)?;
|
|
}
|
|
}
|
|
}
|
|
Motion::BufferStart => {
|
|
cursor.line = 0;
|
|
cursor.index = 0;
|
|
cursor_x_opt = None;
|
|
}
|
|
Motion::BufferEnd => {
|
|
cursor.line = self.lines.len().saturating_sub(1);
|
|
cursor.index = self.lines.get(cursor.line)?.text().len();
|
|
cursor_x_opt = None;
|
|
}
|
|
Motion::GotoLine(line) => {
|
|
let mut layout_cursor = self.layout_cursor(font_system, cursor)?;
|
|
layout_cursor.line = line;
|
|
(cursor, cursor_x_opt) = self.cursor_motion(
|
|
font_system,
|
|
cursor,
|
|
cursor_x_opt,
|
|
Motion::LayoutCursor(layout_cursor),
|
|
)?;
|
|
}
|
|
}
|
|
Some((cursor, cursor_x_opt))
|
|
}
|
|
|
|
/// Draw the buffer
|
|
#[cfg(feature = "swash")]
|
|
pub fn draw<F>(
|
|
&self,
|
|
font_system: &mut FontSystem,
|
|
cache: &mut crate::SwashCache,
|
|
color: Color,
|
|
callback: F,
|
|
) where
|
|
F: FnMut(i32, i32, u32, u32, Color),
|
|
{
|
|
let mut renderer = crate::LegacyRenderer {
|
|
font_system,
|
|
cache,
|
|
callback,
|
|
};
|
|
self.render(&mut renderer, color);
|
|
}
|
|
|
|
pub fn render<R: Renderer>(&self, renderer: &mut R, color: Color) {
|
|
for run in self.layout_runs() {
|
|
for glyph in run.glyphs {
|
|
let physical_glyph = glyph.physical((0., run.line_y), 1.0);
|
|
let glyph_color = glyph.color_opt.map_or(color, |some| some);
|
|
renderer.glyph(physical_glyph, glyph_color);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl BorrowedWithFontSystem<'_, Buffer> {
|
|
/// Shape lines until cursor, also scrolling to include cursor in view
|
|
pub fn shape_until_cursor(&mut self, cursor: Cursor, prune: bool) {
|
|
self.inner
|
|
.shape_until_cursor(self.font_system, cursor, prune);
|
|
}
|
|
|
|
/// Shape lines until scroll
|
|
pub fn shape_until_scroll(&mut self, prune: bool) {
|
|
self.inner.shape_until_scroll(self.font_system, prune);
|
|
}
|
|
|
|
/// Shape the provided line index and return the result
|
|
pub fn line_shape(&mut self, line_i: usize) -> Option<&ShapeLine> {
|
|
self.inner.line_shape(self.font_system, line_i)
|
|
}
|
|
|
|
/// Lay out the provided line index and return the result
|
|
pub fn line_layout(&mut self, line_i: usize) -> Option<&[LayoutLine]> {
|
|
self.inner.line_layout(self.font_system, line_i)
|
|
}
|
|
|
|
/// Set the current [`Metrics`]
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// Will panic if `metrics.font_size` is zero.
|
|
pub fn set_metrics(&mut self, metrics: Metrics) {
|
|
self.inner.set_metrics(self.font_system, metrics);
|
|
}
|
|
|
|
/// Set the current [`Wrap`]
|
|
pub fn set_wrap(&mut self, wrap: Wrap) {
|
|
self.inner.set_wrap(self.font_system, wrap);
|
|
}
|
|
|
|
/// Set the current [`Ellipsize`]
|
|
pub fn set_ellipsize(&mut self, ellipsize: Ellipsize) {
|
|
self.inner.set_ellipsize(self.font_system, ellipsize);
|
|
}
|
|
|
|
/// Set the current buffer dimensions
|
|
pub fn set_size(&mut self, width_opt: Option<f32>, height_opt: Option<f32>) {
|
|
self.inner.set_size(self.font_system, width_opt, height_opt);
|
|
}
|
|
|
|
/// Set the current [`Metrics`] and buffer dimensions at the same time
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// Will panic if `metrics.font_size` is zero.
|
|
pub fn set_metrics_and_size(
|
|
&mut self,
|
|
metrics: Metrics,
|
|
width_opt: Option<f32>,
|
|
height_opt: Option<f32>,
|
|
) {
|
|
self.inner
|
|
.set_metrics_and_size(self.font_system, metrics, width_opt, height_opt);
|
|
}
|
|
|
|
/// Set tab width (number of spaces between tab stops)
|
|
pub fn set_tab_width(&mut self, tab_width: u16) {
|
|
self.inner.set_tab_width(self.font_system, tab_width);
|
|
}
|
|
|
|
/// Set text of buffer, using provided attributes for each line by default
|
|
pub fn set_text(
|
|
&mut self,
|
|
text: &str,
|
|
attrs: &Attrs,
|
|
shaping: Shaping,
|
|
alignment: Option<Align>,
|
|
) {
|
|
self.inner
|
|
.set_text(self.font_system, text, attrs, shaping, alignment);
|
|
}
|
|
|
|
/// Set text of buffer, using an iterator of styled spans (pairs of text and attributes)
|
|
///
|
|
/// ```
|
|
/// # use cosmic_text::{Attrs, Buffer, Family, FontSystem, Metrics, Shaping};
|
|
/// # let mut font_system = FontSystem::new();
|
|
/// let mut buffer = Buffer::new_empty(Metrics::new(32.0, 44.0));
|
|
/// let attrs = Attrs::new().family(Family::Serif);
|
|
/// buffer.set_rich_text(
|
|
/// &mut font_system,
|
|
/// [
|
|
/// ("hello, ", attrs.clone()),
|
|
/// ("cosmic\ntext", attrs.clone().family(Family::Monospace)),
|
|
/// ],
|
|
/// &attrs,
|
|
/// Shaping::Advanced,
|
|
/// None,
|
|
/// );
|
|
/// ```
|
|
pub fn set_rich_text<'r, 's, I>(
|
|
&mut self,
|
|
spans: I,
|
|
default_attrs: &Attrs,
|
|
shaping: Shaping,
|
|
alignment: Option<Align>,
|
|
) where
|
|
I: IntoIterator<Item = (&'s str, Attrs<'r>)>,
|
|
{
|
|
self.inner
|
|
.set_rich_text(self.font_system, spans, default_attrs, shaping, alignment);
|
|
}
|
|
|
|
/// Apply a [`Motion`] to a [`Cursor`]
|
|
pub fn cursor_motion(
|
|
&mut self,
|
|
cursor: Cursor,
|
|
cursor_x_opt: Option<i32>,
|
|
motion: Motion,
|
|
) -> Option<(Cursor, Option<i32>)> {
|
|
self.inner
|
|
.cursor_motion(self.font_system, cursor, cursor_x_opt, motion)
|
|
}
|
|
|
|
/// Draw the buffer
|
|
#[cfg(feature = "swash")]
|
|
pub fn draw<F>(&mut self, cache: &mut crate::SwashCache, color: Color, f: F)
|
|
where
|
|
F: FnMut(i32, i32, u32, u32, Color),
|
|
{
|
|
self.inner.draw(self.font_system, cache, color, f);
|
|
}
|
|
}
|