1525 lines
54 KiB
Rust
1525 lines
54 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);
|
|
}
|
|
} else if line_string.is_empty() && attrs.metrics_opt.is_some() {
|
|
// reset the attrs list with the span's attrs so the line height
|
|
// matches the span's font size rather than falling back to
|
|
// the buffer default
|
|
attrs_list = attrs_list.reset(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);
|
|
}
|
|
}
|