gpui and gpui_platform now come from Zed's repo by git at 1a28cff, the code gpui-pre 0.3.7 republishes, so the gallery can later build for the browser. gpui takes stacker, as 0.2.2 always did, and gpui_platform takes font-kit, without which macOS draws no text; runtime_shaders moves to gpui_platform. The API moved: focus, blur and Tab steps take cx, Corner is Anchor, shaped lines paint with an alignment, defer_draw takes a content mask, the inspector registers a factory, async updates return their value, and wrapped lines are shared, not cloned. Updates through a window context go through the window, so a closed window still ends them as it did. Where the new gpui behaves differently, Ely keeps its old intent. Holds and reveals under a resting pointer survive typing through gpui's input-modality-independent hover; previews the keyboard also moves keep the default, and the calendar's leave ignores a key press. Enter reaches a button inside the code editor, since gpui now arms a key click on the press. The event logger skips a lone modifier. A video and an image grid keep their shape in a flex column under taffy 0.13, as a picture does. AGENTS.md records the new stack and the rules this port changes; the rest of the old gpui and taffy rules are re-tested in T44c.
265 lines
8.7 KiB
Rust
265 lines
8.7 KiB
Rust
use gpui::{
|
|
Animation, AnimationExt, AnyElement, App, Bounds, ElementId, Entity, FontWeight,
|
|
InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels, RenderOnce, ScrollHandle,
|
|
SharedString, Styled, Window, canvas, div, point, prelude::*,
|
|
};
|
|
use smallvec::SmallVec;
|
|
|
|
use crate::{
|
|
motion::{self, Axis, Marker, glide, measure_item, measure_origin, slide},
|
|
theme::{ActiveTheme, ControlSize, Radius, TextSize},
|
|
};
|
|
|
|
/// Where a document's sections sit: shared by its anchors and its table of contents.
|
|
pub struct Sections {
|
|
scroll: ScrollHandle,
|
|
spots: Vec<(SharedString, Bounds<Pixels>)>,
|
|
}
|
|
|
|
impl Sections {
|
|
/// `scroll` tracks the container the document scrolls in.
|
|
pub fn new(scroll: ScrollHandle) -> Self {
|
|
Self {
|
|
scroll,
|
|
spots: Vec::new(),
|
|
}
|
|
}
|
|
|
|
fn spot(&self, value: &SharedString) -> Option<Bounds<Pixels>> {
|
|
self.spots
|
|
.iter()
|
|
.find(|(name, _)| name == value)
|
|
.map(|(_, bounds)| *bounds)
|
|
}
|
|
}
|
|
|
|
/// The last entry whose section starts above `line`: a quarter down the view, or its bottom at the end.
|
|
pub(crate) fn in_view(tops: &[Option<Pixels>], line: Pixels) -> usize {
|
|
tops.iter()
|
|
.rposition(|top| top.is_some_and(|top| top <= line))
|
|
.unwrap_or(0)
|
|
}
|
|
|
|
/// Marks where a section starts, so a table of contents can follow it and bring it into view.
|
|
#[derive(IntoElement)]
|
|
pub struct Anchor {
|
|
sections: Entity<Sections>,
|
|
value: SharedString,
|
|
children: SmallVec<[AnyElement; 2]>,
|
|
}
|
|
|
|
impl Anchor {
|
|
pub fn new(sections: &Entity<Sections>, value: impl Into<SharedString>) -> Self {
|
|
Self {
|
|
sections: sections.clone(),
|
|
value: value.into(),
|
|
children: SmallVec::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ParentElement for Anchor {
|
|
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
|
self.children.extend(elements);
|
|
}
|
|
}
|
|
|
|
impl RenderOnce for Anchor {
|
|
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
|
|
let (sections, value) = (self.sections, self.value);
|
|
div().relative().children(self.children).child(
|
|
canvas(
|
|
move |bounds, window, cx| {
|
|
if sections.read(cx).spot(&value) == Some(bounds) {
|
|
return;
|
|
}
|
|
sections.update(cx, |sections, cx| {
|
|
match sections.spots.iter_mut().find(|(name, _)| *name == value) {
|
|
Some(spot) => spot.1 = bounds,
|
|
None => sections.spots.push((value.clone(), bounds)),
|
|
}
|
|
cx.notify();
|
|
window.request_animation_frame();
|
|
});
|
|
},
|
|
|_, _, _, _| {},
|
|
)
|
|
.absolute()
|
|
.top_0()
|
|
.left_0()
|
|
.size_full(),
|
|
)
|
|
}
|
|
}
|
|
|
|
/// A document's headings. A line marks the section in view; a click brings a section to the top.
|
|
#[derive(IntoElement)]
|
|
pub struct TableOfContents {
|
|
id: ElementId,
|
|
sections: Entity<Sections>,
|
|
entries: Vec<(SharedString, SharedString, usize)>,
|
|
}
|
|
|
|
impl TableOfContents {
|
|
pub fn new(id: impl Into<ElementId>, sections: &Entity<Sections>) -> Self {
|
|
Self {
|
|
id: id.into(),
|
|
sections: sections.clone(),
|
|
entries: Vec::new(),
|
|
}
|
|
}
|
|
|
|
/// A heading: its anchor's value, its text, and how deep it sits, from 0.
|
|
pub fn entry(
|
|
mut self,
|
|
value: impl Into<SharedString>,
|
|
label: impl Into<SharedString>,
|
|
depth: usize,
|
|
) -> Self {
|
|
self.entries.push((value.into(), label.into(), depth));
|
|
self
|
|
}
|
|
}
|
|
|
|
impl RenderOnce for TableOfContents {
|
|
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
|
assert!(
|
|
!self.entries.is_empty(),
|
|
"table of contents {:?} is empty",
|
|
self.id
|
|
);
|
|
let (viewport, at_end, tops) = {
|
|
let sections = self.sections.read(cx);
|
|
let (scroll, viewport) = (§ions.scroll, sections.scroll.bounds());
|
|
let room = scroll.max_offset().y;
|
|
let at_end = room > Pixels::ZERO && -scroll.offset().y >= room;
|
|
let tops: Vec<Option<Pixels>> = self
|
|
.entries
|
|
.iter()
|
|
.map(|(value, _, _)| sections.spot(value).map(|spot| spot.top()))
|
|
.collect();
|
|
(viewport, at_end, tops)
|
|
};
|
|
let line = if at_end {
|
|
viewport.bottom()
|
|
} else {
|
|
viewport.top() + viewport.size.height / 4.0
|
|
};
|
|
let here = in_view(&tops, line);
|
|
log::debug!(
|
|
"table of contents {:?}: {here} in view, tops {tops:?} in {viewport:?}",
|
|
self.id
|
|
);
|
|
let values: Vec<SharedString> = self
|
|
.entries
|
|
.iter()
|
|
.map(|(value, ..)| value.clone())
|
|
.collect();
|
|
let (state, marker) = slide(
|
|
(self.id.clone(), "slide"),
|
|
&values,
|
|
&values[here],
|
|
window,
|
|
cx,
|
|
);
|
|
let theme = cx.theme();
|
|
let colors = &theme.colors;
|
|
let duration = motion::duration(motion::BASE, cx);
|
|
let line = marker.map(
|
|
|Marker {
|
|
from,
|
|
to,
|
|
generation,
|
|
}| {
|
|
div()
|
|
.absolute()
|
|
.left_0()
|
|
.w(theme.tab_indicator())
|
|
.rounded_full()
|
|
.bg(colors.accent)
|
|
.with_animation(
|
|
("toc-line", generation),
|
|
Animation::new(duration),
|
|
move |line, t| {
|
|
let (top, height) = glide(from, to, t);
|
|
line.top(top).h(height)
|
|
},
|
|
)
|
|
},
|
|
);
|
|
let rows = self
|
|
.entries
|
|
.into_iter()
|
|
.enumerate()
|
|
.map(|(ix, (value, label, depth))| {
|
|
let sections = self.sections.clone();
|
|
let on = ix == here;
|
|
let row = div()
|
|
.id(("toc", ix))
|
|
.relative()
|
|
.flex()
|
|
.items_center()
|
|
.min_h(theme.control_height(ControlSize::Sm))
|
|
.pr_2()
|
|
.rounded(theme.radius(Radius::Sm))
|
|
.text_size(theme.text_size(TextSize::Sm))
|
|
.text_color(if on { colors.fg } else { colors.fg_muted })
|
|
.when(on, |row| row.font_weight(FontWeight::MEDIUM))
|
|
.cursor_pointer()
|
|
.hover(|style| style.text_color(colors.fg))
|
|
.on_mouse_down(MouseButton::Left, move |_, window, cx| {
|
|
let (scroll, spot) = {
|
|
let sections = sections.read(cx);
|
|
(sections.scroll.clone(), sections.spot(&value))
|
|
};
|
|
let Some(spot) = spot else {
|
|
log::error!("table of contents: no anchor {value} has drawn");
|
|
return;
|
|
};
|
|
let offset = scroll.offset();
|
|
let lift = spot.top() - scroll.bounds().top();
|
|
scroll.set_offset(point(offset.x, offset.y - lift));
|
|
log::info!(
|
|
"table of contents: to {value}, lift {lift:?} from {:?} in {:?}",
|
|
offset.y,
|
|
scroll.bounds()
|
|
);
|
|
window.refresh();
|
|
})
|
|
.child(label)
|
|
.child(measure_item(state.clone(), ix, Axis::Vertical));
|
|
match depth {
|
|
0 => row.pl_3(),
|
|
1 => row.pl_6(),
|
|
_ => row.pl_8(),
|
|
}
|
|
});
|
|
div()
|
|
.id(self.id)
|
|
.relative()
|
|
.flex()
|
|
.flex_col()
|
|
.gap_0p5()
|
|
.border_l_1()
|
|
.border_color(colors.border)
|
|
.child(measure_origin(state.clone(), Axis::Vertical))
|
|
.children(line)
|
|
.children(rows)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use gpui::px;
|
|
|
|
use super::in_view;
|
|
|
|
#[test]
|
|
fn the_section_in_view_is_the_last_one_started() {
|
|
let tops = [Some(px(-400.0)), Some(px(-20.0)), Some(px(300.0)), None];
|
|
assert_eq!(in_view(&tops, px(100.0)), 1);
|
|
assert_eq!(in_view(&tops, px(-500.0)), 0);
|
|
assert_eq!(in_view(&tops, px(900.0)), 2);
|
|
}
|
|
}
|