Files
oak-gpui/crates/gpui/src/text_system/line_layout.rs
T
Kirill Bulatov d0bafce86b Allow splitting the terminal panel (#21238)
Closes https://github.com/zed-industries/zed/issues/4351


![it_splits](https://github.com/user-attachments/assets/40de03c9-2173-4441-ba96-8e91537956e0)

Applies the same splitting mechanism, as Zed's central pane has, to the
terminal panel.
Similar navigation, splitting and (de)serialization capabilities are
supported.

Notable caveats:
* zooming keeps the terminal splits' ratio, rather expanding the
terminal pane
* on macOs, central panel is split with `cmd-k up/down/etc.` but `cmd-k`
is a "standard" terminal clearing keybinding on macOS, so terminal panel
splitting is done via `ctrl-k up/down/etc.`
* task terminals are "split" into regular terminals, and also not
persisted (same as currently in the terminal)

Seems ok for the initial version, we can revisit and polish things
later.

Release Notes:

- Added the ability to split the terminal panel
2024-11-27 20:22:39 +02:00

597 lines
18 KiB
Rust

use crate::{point, px, FontId, GlyphId, Pixels, PlatformTextSystem, Point, Size};
use collections::FxHashMap;
use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard};
use smallvec::SmallVec;
use std::{
borrow::Borrow,
hash::{Hash, Hasher},
ops::Range,
sync::Arc,
};
use super::LineWrapper;
/// A laid out and styled line of text
#[derive(Default, Debug)]
pub struct LineLayout {
/// The font size for this line
pub font_size: Pixels,
/// The width of the line
pub width: Pixels,
/// The ascent of the line
pub ascent: Pixels,
/// The descent of the line
pub descent: Pixels,
/// The shaped runs that make up this line
pub runs: Vec<ShapedRun>,
/// The length of the line in utf-8 bytes
pub len: usize,
}
/// A run of text that has been shaped .
#[derive(Debug, Clone)]
pub struct ShapedRun {
/// The font id for this run
pub font_id: FontId,
/// The glyphs that make up this run
pub glyphs: SmallVec<[ShapedGlyph; 8]>,
}
/// A single glyph, ready to paint.
#[derive(Clone, Debug)]
pub struct ShapedGlyph {
/// The ID for this glyph, as determined by the text system.
pub id: GlyphId,
/// The position of this glyph in its containing line.
pub position: Point<Pixels>,
/// The index of this glyph in the original text.
pub index: usize,
/// Whether this glyph is an emoji
pub is_emoji: bool,
}
impl LineLayout {
/// The index for the character at the given x coordinate
pub fn index_for_x(&self, x: Pixels) -> Option<usize> {
if x >= self.width {
None
} else {
for run in self.runs.iter().rev() {
for glyph in run.glyphs.iter().rev() {
if glyph.position.x <= x {
return Some(glyph.index);
}
}
}
Some(0)
}
}
/// closest_index_for_x returns the character boundary closest to the given x coordinate
/// (e.g. to handle aligning up/down arrow keys)
pub fn closest_index_for_x(&self, x: Pixels) -> usize {
let mut prev_index = 0;
let mut prev_x = px(0.);
for run in self.runs.iter() {
for glyph in run.glyphs.iter() {
if glyph.position.x >= x {
if glyph.position.x - x < x - prev_x {
return glyph.index;
} else {
return prev_index;
}
}
prev_index = glyph.index;
prev_x = glyph.position.x;
}
}
self.len
}
/// The x position of the character at the given index
pub fn x_for_index(&self, index: usize) -> Pixels {
for run in &self.runs {
for glyph in &run.glyphs {
if glyph.index >= index {
return glyph.position.x;
}
}
}
self.width
}
/// The corresponding Font at the given index
pub fn font_id_for_index(&self, index: usize) -> Option<FontId> {
for run in &self.runs {
for glyph in &run.glyphs {
if glyph.index >= index {
return Some(run.font_id);
}
}
}
None
}
fn compute_wrap_boundaries(
&self,
text: &str,
wrap_width: Pixels,
) -> SmallVec<[WrapBoundary; 1]> {
let mut boundaries = SmallVec::new();
let mut first_non_whitespace_ix = None;
let mut last_candidate_ix = None;
let mut last_candidate_x = px(0.);
let mut last_boundary = WrapBoundary {
run_ix: 0,
glyph_ix: 0,
};
let mut last_boundary_x = px(0.);
let mut prev_ch = '\0';
let mut glyphs = self
.runs
.iter()
.enumerate()
.flat_map(move |(run_ix, run)| {
run.glyphs.iter().enumerate().map(move |(glyph_ix, glyph)| {
let character = text[glyph.index..].chars().next().unwrap();
(
WrapBoundary { run_ix, glyph_ix },
character,
glyph.position.x,
)
})
})
.peekable();
while let Some((boundary, ch, x)) = glyphs.next() {
if ch == '\n' {
continue;
}
// Here is very similar to `LineWrapper::wrap_line` to determine text wrapping,
// but there are some differences, so we have to duplicate the code here.
if LineWrapper::is_word_char(ch) {
if prev_ch == ' ' && ch != ' ' && first_non_whitespace_ix.is_some() {
last_candidate_ix = Some(boundary);
last_candidate_x = x;
}
} else {
if ch != ' ' && first_non_whitespace_ix.is_some() {
last_candidate_ix = Some(boundary);
last_candidate_x = x;
}
}
if ch != ' ' && first_non_whitespace_ix.is_none() {
first_non_whitespace_ix = Some(boundary);
}
let next_x = glyphs.peek().map_or(self.width, |(_, _, x)| *x);
let width = next_x - last_boundary_x;
if width > wrap_width && boundary > last_boundary {
if let Some(last_candidate_ix) = last_candidate_ix.take() {
last_boundary = last_candidate_ix;
last_boundary_x = last_candidate_x;
} else {
last_boundary = boundary;
last_boundary_x = x;
}
boundaries.push(last_boundary);
}
prev_ch = ch;
}
boundaries
}
}
/// A line of text that has been wrapped to fit a given width
#[derive(Default, Debug)]
pub struct WrappedLineLayout {
/// The line layout, pre-wrapping.
pub unwrapped_layout: Arc<LineLayout>,
/// The boundaries at which the line was wrapped
pub wrap_boundaries: SmallVec<[WrapBoundary; 1]>,
/// The width of the line, if it was wrapped
pub wrap_width: Option<Pixels>,
}
/// A boundary at which a line was wrapped
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct WrapBoundary {
/// The index in the run just before the line was wrapped
pub run_ix: usize,
/// The index of the glyph just before the line was wrapped
pub glyph_ix: usize,
}
impl WrappedLineLayout {
/// The length of the underlying text, in utf8 bytes.
#[allow(clippy::len_without_is_empty)]
pub fn len(&self) -> usize {
self.unwrapped_layout.len
}
/// The width of this line, in pixels, whether or not it was wrapped.
pub fn width(&self) -> Pixels {
self.wrap_width
.unwrap_or(Pixels::MAX)
.min(self.unwrapped_layout.width)
}
/// The size of the whole wrapped text, for the given line_height.
/// can span multiple lines if there are multiple wrap boundaries.
pub fn size(&self, line_height: Pixels) -> Size<Pixels> {
Size {
width: self.width(),
height: line_height * (self.wrap_boundaries.len() + 1),
}
}
/// The ascent of a line in this layout
pub fn ascent(&self) -> Pixels {
self.unwrapped_layout.ascent
}
/// The descent of a line in this layout
pub fn descent(&self) -> Pixels {
self.unwrapped_layout.descent
}
/// The wrap boundaries in this layout
pub fn wrap_boundaries(&self) -> &[WrapBoundary] {
&self.wrap_boundaries
}
/// The font size of this layout
pub fn font_size(&self) -> Pixels {
self.unwrapped_layout.font_size
}
/// The runs in this layout, sans wrapping
pub fn runs(&self) -> &[ShapedRun] {
&self.unwrapped_layout.runs
}
/// The index corresponding to a given position in this layout for the given line height.
pub fn index_for_position(
&self,
mut position: Point<Pixels>,
line_height: Pixels,
) -> Result<usize, usize> {
let wrapped_line_ix = (position.y / line_height) as usize;
let wrapped_line_start_index;
let wrapped_line_start_x;
if wrapped_line_ix > 0 {
let Some(line_start_boundary) = self.wrap_boundaries.get(wrapped_line_ix - 1) else {
return Err(0);
};
let run = &self.unwrapped_layout.runs[line_start_boundary.run_ix];
let glyph = &run.glyphs[line_start_boundary.glyph_ix];
wrapped_line_start_index = glyph.index;
wrapped_line_start_x = glyph.position.x;
} else {
wrapped_line_start_index = 0;
wrapped_line_start_x = Pixels::ZERO;
};
let wrapped_line_end_index;
let wrapped_line_end_x;
if wrapped_line_ix < self.wrap_boundaries.len() {
let next_wrap_boundary_ix = wrapped_line_ix;
let next_wrap_boundary = self.wrap_boundaries[next_wrap_boundary_ix];
let run = &self.unwrapped_layout.runs[next_wrap_boundary.run_ix];
let glyph = &run.glyphs[next_wrap_boundary.glyph_ix];
wrapped_line_end_index = glyph.index;
wrapped_line_end_x = glyph.position.x;
} else {
wrapped_line_end_index = self.unwrapped_layout.len;
wrapped_line_end_x = self.unwrapped_layout.width;
};
let mut position_in_unwrapped_line = position;
position_in_unwrapped_line.x += wrapped_line_start_x;
if position_in_unwrapped_line.x < wrapped_line_start_x {
Err(wrapped_line_start_index)
} else if position_in_unwrapped_line.x >= wrapped_line_end_x {
Err(wrapped_line_end_index)
} else {
Ok(self
.unwrapped_layout
.index_for_x(position_in_unwrapped_line.x)
.unwrap())
}
}
/// Returns the pixel position for the given byte index.
pub fn position_for_index(&self, index: usize, line_height: Pixels) -> Option<Point<Pixels>> {
let mut line_start_ix = 0;
let mut line_end_indices = self
.wrap_boundaries
.iter()
.map(|wrap_boundary| {
let run = &self.unwrapped_layout.runs[wrap_boundary.run_ix];
let glyph = &run.glyphs[wrap_boundary.glyph_ix];
glyph.index
})
.chain([self.len()])
.enumerate();
for (ix, line_end_ix) in line_end_indices {
let line_y = ix as f32 * line_height;
if index < line_start_ix {
break;
} else if index > line_end_ix {
line_start_ix = line_end_ix;
continue;
} else {
let line_start_x = self.unwrapped_layout.x_for_index(line_start_ix);
let x = self.unwrapped_layout.x_for_index(index) - line_start_x;
return Some(point(x, line_y));
}
}
None
}
}
pub(crate) struct LineLayoutCache {
previous_frame: Mutex<FrameCache>,
current_frame: RwLock<FrameCache>,
platform_text_system: Arc<dyn PlatformTextSystem>,
}
#[derive(Default)]
struct FrameCache {
lines: FxHashMap<Arc<CacheKey>, Arc<LineLayout>>,
wrapped_lines: FxHashMap<Arc<CacheKey>, Arc<WrappedLineLayout>>,
used_lines: Vec<Arc<CacheKey>>,
used_wrapped_lines: Vec<Arc<CacheKey>>,
}
#[derive(Clone, Default)]
pub(crate) struct LineLayoutIndex {
lines_index: usize,
wrapped_lines_index: usize,
}
impl LineLayoutCache {
pub fn new(platform_text_system: Arc<dyn PlatformTextSystem>) -> Self {
Self {
previous_frame: Mutex::default(),
current_frame: RwLock::default(),
platform_text_system,
}
}
pub fn layout_index(&self) -> LineLayoutIndex {
let frame = self.current_frame.read();
LineLayoutIndex {
lines_index: frame.used_lines.len(),
wrapped_lines_index: frame.used_wrapped_lines.len(),
}
}
pub fn reuse_layouts(&self, range: Range<LineLayoutIndex>) {
let mut previous_frame = &mut *self.previous_frame.lock();
let mut current_frame = &mut *self.current_frame.write();
if let Some(cached_keys) = previous_frame
.used_lines
.get(range.start.lines_index..range.end.lines_index)
{
for key in cached_keys {
if let Some((key, line)) = previous_frame.lines.remove_entry(key) {
current_frame.lines.insert(key, line);
}
current_frame.used_lines.push(key.clone());
}
}
if let Some(cached_keys) = previous_frame
.used_wrapped_lines
.get(range.start.wrapped_lines_index..range.end.wrapped_lines_index)
{
for key in cached_keys {
if let Some((key, line)) = previous_frame.wrapped_lines.remove_entry(key) {
current_frame.wrapped_lines.insert(key, line);
}
current_frame.used_wrapped_lines.push(key.clone());
}
}
}
pub fn truncate_layouts(&self, index: LineLayoutIndex) {
let mut current_frame = &mut *self.current_frame.write();
current_frame.used_lines.truncate(index.lines_index);
current_frame
.used_wrapped_lines
.truncate(index.wrapped_lines_index);
}
pub fn finish_frame(&self) {
let mut prev_frame = self.previous_frame.lock();
let mut curr_frame = self.current_frame.write();
std::mem::swap(&mut *prev_frame, &mut *curr_frame);
curr_frame.lines.clear();
curr_frame.wrapped_lines.clear();
curr_frame.used_lines.clear();
curr_frame.used_wrapped_lines.clear();
}
pub fn layout_wrapped_line(
&self,
text: &str,
font_size: Pixels,
runs: &[FontRun],
wrap_width: Option<Pixels>,
) -> Arc<WrappedLineLayout> {
let key = &CacheKeyRef {
text,
font_size,
runs,
wrap_width,
} as &dyn AsCacheKeyRef;
let current_frame = self.current_frame.upgradable_read();
if let Some(layout) = current_frame.wrapped_lines.get(key) {
return layout.clone();
}
let previous_frame_entry = self.previous_frame.lock().wrapped_lines.remove_entry(key);
if let Some((key, layout)) = previous_frame_entry {
let mut current_frame = RwLockUpgradableReadGuard::upgrade(current_frame);
current_frame
.wrapped_lines
.insert(key.clone(), layout.clone());
current_frame.used_wrapped_lines.push(key);
layout
} else {
drop(current_frame);
let unwrapped_layout = self.layout_line(text, font_size, runs);
let wrap_boundaries = if let Some(wrap_width) = wrap_width {
unwrapped_layout.compute_wrap_boundaries(text.as_ref(), wrap_width)
} else {
SmallVec::new()
};
let layout = Arc::new(WrappedLineLayout {
unwrapped_layout,
wrap_boundaries,
wrap_width,
});
let key = Arc::new(CacheKey {
text: text.into(),
font_size,
runs: SmallVec::from(runs),
wrap_width,
});
let mut current_frame = self.current_frame.write();
current_frame
.wrapped_lines
.insert(key.clone(), layout.clone());
current_frame.used_wrapped_lines.push(key);
layout
}
}
pub fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> Arc<LineLayout> {
let key = &CacheKeyRef {
text,
font_size,
runs,
wrap_width: None,
} as &dyn AsCacheKeyRef;
let current_frame = self.current_frame.upgradable_read();
if let Some(layout) = current_frame.lines.get(key) {
return layout.clone();
}
let mut current_frame = RwLockUpgradableReadGuard::upgrade(current_frame);
if let Some((key, layout)) = self.previous_frame.lock().lines.remove_entry(key) {
current_frame.lines.insert(key.clone(), layout.clone());
current_frame.used_lines.push(key);
layout
} else {
let layout = Arc::new(self.platform_text_system.layout_line(text, font_size, runs));
let key = Arc::new(CacheKey {
text: text.into(),
font_size,
runs: SmallVec::from(runs),
wrap_width: None,
});
current_frame.lines.insert(key.clone(), layout.clone());
current_frame.used_lines.push(key);
layout
}
}
}
/// A run of text with a single font.
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub struct FontRun {
pub(crate) len: usize,
pub(crate) font_id: FontId,
}
trait AsCacheKeyRef {
fn as_cache_key_ref(&self) -> CacheKeyRef;
}
#[derive(Clone, Debug, Eq)]
struct CacheKey {
text: String,
font_size: Pixels,
runs: SmallVec<[FontRun; 1]>,
wrap_width: Option<Pixels>,
}
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
struct CacheKeyRef<'a> {
text: &'a str,
font_size: Pixels,
runs: &'a [FontRun],
wrap_width: Option<Pixels>,
}
impl<'a> PartialEq for (dyn AsCacheKeyRef + 'a) {
fn eq(&self, other: &dyn AsCacheKeyRef) -> bool {
self.as_cache_key_ref() == other.as_cache_key_ref()
}
}
impl<'a> Eq for (dyn AsCacheKeyRef + 'a) {}
impl<'a> Hash for (dyn AsCacheKeyRef + 'a) {
fn hash<H: Hasher>(&self, state: &mut H) {
self.as_cache_key_ref().hash(state)
}
}
impl AsCacheKeyRef for CacheKey {
fn as_cache_key_ref(&self) -> CacheKeyRef {
CacheKeyRef {
text: &self.text,
font_size: self.font_size,
runs: self.runs.as_slice(),
wrap_width: self.wrap_width,
}
}
}
impl PartialEq for CacheKey {
fn eq(&self, other: &Self) -> bool {
self.as_cache_key_ref().eq(&other.as_cache_key_ref())
}
}
impl Hash for CacheKey {
fn hash<H: Hasher>(&self, state: &mut H) {
self.as_cache_key_ref().hash(state);
}
}
impl<'a> Borrow<dyn AsCacheKeyRef + 'a> for Arc<CacheKey> {
fn borrow(&self) -> &(dyn AsCacheKeyRef + 'a) {
self.as_ref() as &dyn AsCacheKeyRef
}
}
impl<'a> AsCacheKeyRef for CacheKeyRef<'a> {
fn as_cache_key_ref(&self) -> CacheKeyRef {
*self
}
}