
See https://zed.dev/channel/gpui-536 Fixes https://github.com/zed-industries/zed/issues/9010 Fixes https://github.com/zed-industries/zed/issues/8883 Fixes https://github.com/zed-industries/zed/issues/8640 Fixes https://github.com/zed-industries/zed/issues/8598 Fixes https://github.com/zed-industries/zed/issues/8579 Fixes https://github.com/zed-industries/zed/issues/8363 Fixes https://github.com/zed-industries/zed/issues/8207 ### Problem After transitioning Zed to GPUI 2, we started noticing that interacting with the mouse on many UI elements would lead to a pretty annoying flicker. The main issue with the old approach was that hover state was calculated based on the previous frame. That is, when computing whether a given element was hovered in the current frame, we would use information about the same element in the previous frame. However, inspecting the previous frame tells us very little about what should be hovered in the current frame, as elements in the current frame may have changed significantly. ### Solution This pull request's main contribution is the introduction of a new `after_layout` phase when redrawing the window. The key idea is that we'll give every element a chance to register a hitbox (see `ElementContext::insert_hitbox`) before painting anything. Then, during the `paint` phase, elements can determine whether they're the topmost and draw their hover state accordingly. We are also removing the ability to give an arbitrary z-index to elements. Instead, we will follow the much simpler painter's algorithm. That is, an element that gets painted after will be drawn on top of an element that got painted earlier. Elements can still escape their current "stacking context" by using the new `ElementContext::defer_draw` method (see `Overlay` for an example). Elements drawn using this method will still be logically considered as being children of their original parent (for keybinding, focus and cache invalidation purposes) but their layout and paint passes will be deferred until the currently-drawn element is done. With these changes we also reworked geometry batching within the `Scene`. The new approach uses an AABB tree to determine geometry occlusion, which allows the GPU to render non-overlapping geometry in parallel. ### Performance Performance is slightly better than on `main` even though this new approach is more correct and we're maintaining an extra data structure (the AABB tree).  Release Notes: - Fixed a bug that was causing popovers to flicker. --------- Co-authored-by: Nathan Sobo <nathan@zed.dev> Co-authored-by: Thorsten <thorsten@zed.dev>
513 lines
16 KiB
Rust
513 lines
16 KiB
Rust
use crate::{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,
|
|
};
|
|
|
|
/// 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)]
|
|
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
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
if prev_ch == ' ' && 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,
|
|
position: Point<Pixels>,
|
|
line_height: Pixels,
|
|
) -> Option<usize> {
|
|
let wrapped_line_ix = (position.y / line_height) as usize;
|
|
|
|
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 None;
|
|
};
|
|
let run = &self.unwrapped_layout.runs[line_start_boundary.run_ix];
|
|
run.glyphs[line_start_boundary.glyph_ix].position.x
|
|
} else {
|
|
Pixels::ZERO
|
|
};
|
|
|
|
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];
|
|
run.glyphs[next_wrap_boundary.glyph_ix].position.x
|
|
} else {
|
|
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_end_x {
|
|
None
|
|
} else {
|
|
self.unwrapped_layout
|
|
.index_for_x(position_in_unwrapped_line.x)
|
|
}
|
|
}
|
|
}
|
|
|
|
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();
|
|
|
|
for key in &previous_frame.used_lines[range.start.lines_index..range.end.lines_index] {
|
|
if let Some((key, line)) = previous_frame.lines.remove_entry(key) {
|
|
current_frame.lines.insert(key, line);
|
|
}
|
|
current_frame.used_lines.push(key.clone());
|
|
}
|
|
|
|
for key in &previous_frame.used_wrapped_lines
|
|
[range.start.wrapped_lines_index..range.end.wrapped_lines_index]
|
|
{
|
|
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 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
|
|
}
|
|
}
|