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.
429 lines
14 KiB
Rust
429 lines
14 KiB
Rust
use std::fmt;
|
||
|
||
use gpui::{
|
||
AbsoluteLength, AnyElement, App, BorderStyle, Bounds, Context, Corners, DefiniteLength,
|
||
DivInspectorState, Edges, FontWeight, Inspector, InteractiveElement, IntoElement, Length,
|
||
ParentElement, Pixels, StatefulInteractiveElement, StyleRefinement, Styled, TextAlign, TextRun,
|
||
Window, canvas, div, fill, point, quad, size, transparent_black,
|
||
};
|
||
|
||
use crate::{
|
||
buttons::{Button, ButtonVariant},
|
||
primitives::{FocusScope, raise},
|
||
theme::{ActiveTheme, ControlSize, Radius, TextSize},
|
||
typography::{Ellipsis, LEADING, literal, tabular},
|
||
};
|
||
|
||
// Chart hues Rust, Ochre, Green and Blue, as browsers color a box's layers.
|
||
const MARGIN: usize = 6;
|
||
const BORDER: usize = 2;
|
||
const PADDING: usize = 5;
|
||
const CONTENT: usize = 0;
|
||
|
||
/// One side of a box as written: pixels, `auto`, or a share of the parent's width.
|
||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||
enum Side {
|
||
Px(f32),
|
||
Auto,
|
||
Share(f32),
|
||
}
|
||
|
||
impl Side {
|
||
/// What the overlay draws: `auto` and shares only layout knows, so they draw nothing.
|
||
fn drawn(self) -> Pixels {
|
||
match self {
|
||
Side::Px(px) => Pixels::from(px),
|
||
Side::Auto | Side::Share(_) => Pixels::ZERO,
|
||
}
|
||
}
|
||
}
|
||
|
||
fn number(value: f32) -> String {
|
||
((value * 10.0).round() / 10.0).to_string()
|
||
}
|
||
|
||
impl fmt::Display for Side {
|
||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||
match self {
|
||
Side::Px(px) => write!(f, "{}", number(*px)),
|
||
Side::Auto => write!(f, "auto"),
|
||
Side::Share(share) => write!(f, "{}%", number(share * 100.0)),
|
||
}
|
||
}
|
||
}
|
||
|
||
/// A box's margin, border and padding, each top, right, bottom, left.
|
||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||
struct Layers {
|
||
margin: [Side; 4],
|
||
border: [Side; 4],
|
||
padding: [Side; 4],
|
||
}
|
||
|
||
/// The layers as the style writes them, rems at `rem`.
|
||
fn layers(style: &StyleRefinement, rem: Pixels) -> Layers {
|
||
let definite = |length: DefiniteLength| match length {
|
||
DefiniteLength::Absolute(length) => Side::Px(length.to_pixels(rem).into()),
|
||
DefiniteLength::Fraction(share) => Side::Share(share),
|
||
};
|
||
let margin = |length: Option<Length>| match length {
|
||
None => Side::Px(0.0),
|
||
Some(Length::Auto) => Side::Auto,
|
||
Some(Length::Definite(length)) => definite(length),
|
||
};
|
||
let padding = |length: Option<DefiniteLength>| length.map_or(Side::Px(0.0), definite);
|
||
let border = |length: Option<AbsoluteLength>| {
|
||
Side::Px(length.map_or(0.0, |length| length.to_pixels(rem).into()))
|
||
};
|
||
let (m, b, p) = (&style.margin, &style.border_widths, &style.padding);
|
||
Layers {
|
||
margin: [m.top, m.right, m.bottom, m.left].map(margin),
|
||
border: [b.top, b.right, b.bottom, b.left].map(border),
|
||
padding: [p.top, p.right, p.bottom, p.left].map(padding),
|
||
}
|
||
}
|
||
|
||
fn edges(sides: [Side; 4]) -> Edges<Pixels> {
|
||
let [top, right, bottom, left] = sides.map(Side::drawn);
|
||
Edges {
|
||
top,
|
||
right,
|
||
bottom,
|
||
left,
|
||
}
|
||
}
|
||
|
||
fn shrink(bounds: Bounds<Pixels>, by: Edges<Pixels>) -> Bounds<Pixels> {
|
||
let inner = bounds.extend(Edges {
|
||
top: -by.top,
|
||
right: -by.right,
|
||
bottom: -by.bottom,
|
||
left: -by.left,
|
||
});
|
||
Bounds::new(
|
||
inner.origin,
|
||
inner.size.max(&size(Pixels::ZERO, Pixels::ZERO)),
|
||
)
|
||
}
|
||
|
||
/// Paints the layers over the page at the box's bounds, with a tag that gives its size.
|
||
fn paint_layers(bounds: Bounds<Pixels>, layers: Layers, window: &mut Window, cx: &mut App) {
|
||
let theme = cx.theme();
|
||
let colors = &theme.colors;
|
||
let hue = |hue| colors.hue(hue, "inspector");
|
||
let band = |bounds, widths, hue| {
|
||
quad(
|
||
bounds,
|
||
Corners::default(),
|
||
transparent_black(),
|
||
widths,
|
||
colors.hue(hue, "inspector").alpha(0.4),
|
||
BorderStyle::Solid,
|
||
)
|
||
};
|
||
let (margin, border, padding) = (
|
||
edges(layers.margin),
|
||
edges(layers.border),
|
||
edges(layers.padding),
|
||
);
|
||
let (outer, inner) = (bounds.extend(margin), shrink(bounds, border));
|
||
window.paint_quad(band(outer, margin, MARGIN));
|
||
window.paint_quad(band(bounds, border, BORDER));
|
||
window.paint_quad(band(inner, padding, PADDING));
|
||
window.paint_quad(fill(shrink(inner, padding), hue(CONTENT).alpha(0.25)));
|
||
let text = theme.text_size(TextSize::Xs).to_pixels(window.rem_size());
|
||
let label = format!(
|
||
"{} × {}",
|
||
number(bounds.size.width.into()),
|
||
number(bounds.size.height.into())
|
||
);
|
||
let run = TextRun {
|
||
len: label.len(),
|
||
font: window.text_style().font(),
|
||
color: colors.bg,
|
||
background_color: None,
|
||
underline: None,
|
||
strikethrough: None,
|
||
};
|
||
let line = window
|
||
.text_system()
|
||
.shape_line(label.into(), text, &[run], None);
|
||
let (height, pad) = (text * LEADING, text / 2.0);
|
||
let top = (outer.top() - height).max(Pixels::ZERO);
|
||
let tag = Bounds::new(
|
||
point(outer.left(), top),
|
||
size(line.width + pad * 2.0, height),
|
||
);
|
||
let radius = theme.radius(Radius::Sm).to_pixels(window.rem_size());
|
||
window.paint_quad(fill(tag, colors.fg).corner_radii(radius));
|
||
if let Err(error) = line.paint(
|
||
point(tag.left() + pad, tag.top()),
|
||
height,
|
||
TextAlign::Left,
|
||
None,
|
||
window,
|
||
cx,
|
||
) {
|
||
log::error!("inspector: size tag did not paint: {error:#}");
|
||
}
|
||
}
|
||
|
||
/// One layer of the box model: its name, its four sides around what it holds.
|
||
fn ring(
|
||
name: &'static str,
|
||
sides: [Side; 4],
|
||
hue: usize,
|
||
inside: AnyElement,
|
||
cx: &App,
|
||
) -> AnyElement {
|
||
let theme = cx.theme();
|
||
let color = theme.colors.hue(hue, "inspector");
|
||
let value = |side: Side| tabular(div()).child(side.to_string());
|
||
let shown = sides.map(|side| side.to_string()).join("-");
|
||
div()
|
||
.debug_selector(move || format!("inspector-{name}-{shown}"))
|
||
.relative()
|
||
.flex()
|
||
.flex_col()
|
||
.items_center()
|
||
.gap_1()
|
||
.px_2()
|
||
.py_1()
|
||
.rounded(theme.radius(Radius::Sm))
|
||
.border_1()
|
||
.border_color(color)
|
||
.bg(color.alpha(0.12))
|
||
.child(
|
||
div()
|
||
.absolute()
|
||
.top_1()
|
||
.left_2()
|
||
.text_color(theme.colors.fg_muted)
|
||
.child(name),
|
||
)
|
||
.child(value(sides[0]))
|
||
.child(
|
||
div()
|
||
.flex()
|
||
.items_center()
|
||
.gap_2()
|
||
.child(value(sides[3]))
|
||
.child(inside)
|
||
.child(value(sides[1])),
|
||
)
|
||
.child(value(sides[2]))
|
||
.into_any_element()
|
||
}
|
||
|
||
/// A picked box's model, sizes and place, and its layers drawn over the page.
|
||
fn box_model(state: &DivInspectorState, window: &mut Window, cx: &mut App) -> AnyElement {
|
||
let (bounds, children) = (state.bounds, state.content_size);
|
||
let layers = layers(&state.base_style, window.rem_size());
|
||
let content = shrink(shrink(bounds, edges(layers.border)), edges(layers.padding)).size;
|
||
let pair = |w: Pixels, h: Pixels| format!("{} × {}", number(w.into()), number(h.into()));
|
||
let theme = cx.theme();
|
||
let fact = |name: &'static str, value: String| {
|
||
div()
|
||
.flex()
|
||
.justify_between()
|
||
.gap_2()
|
||
.child(div().text_color(theme.colors.fg_muted).child(name))
|
||
.child(tabular(div()).child(value))
|
||
};
|
||
let sized = format!(
|
||
"{}x{}",
|
||
number(bounds.size.width.into()),
|
||
number(bounds.size.height.into())
|
||
);
|
||
let core = div()
|
||
.debug_selector(move || {
|
||
format!(
|
||
"inspector-content-{}x{}",
|
||
number(content.width.into()),
|
||
number(content.height.into())
|
||
)
|
||
})
|
||
.px_2()
|
||
.py_1()
|
||
.rounded(theme.radius(Radius::Sm))
|
||
.border_1()
|
||
.border_color(theme.colors.hue(CONTENT, "inspector"))
|
||
.bg(theme.colors.hue(CONTENT, "inspector").alpha(0.12))
|
||
.child(tabular(div()).child(pair(content.width, content.height)))
|
||
.into_any_element();
|
||
let padded = ring("padding", layers.padding, PADDING, core, cx);
|
||
let bordered = ring("border", layers.border, BORDER, padded, cx);
|
||
let model = ring("margin", layers.margin, MARGIN, bordered, cx);
|
||
let theme = cx.theme();
|
||
div()
|
||
.flex()
|
||
.flex_col()
|
||
.gap_3()
|
||
.text_size(theme.text_size(TextSize::Xs))
|
||
// Raised, so the panel's scroll box does not clip it off the page.
|
||
.child(raise(
|
||
"ely-inspector-overlay",
|
||
canvas(
|
||
|_, _, _| {},
|
||
move |_, _, window, cx| paint_layers(bounds, layers, window, cx),
|
||
)
|
||
.absolute()
|
||
.top_0()
|
||
.left_0(),
|
||
))
|
||
.child(model)
|
||
.child(
|
||
div()
|
||
.debug_selector(move || format!("inspector-size-{sized}"))
|
||
.flex()
|
||
.flex_col()
|
||
.gap_1()
|
||
.child(fact(
|
||
"Place",
|
||
format!(
|
||
"{}, {}",
|
||
number(bounds.origin.x.into()),
|
||
number(bounds.origin.y.into())
|
||
),
|
||
))
|
||
.child(fact("Size", pair(bounds.size.width, bounds.size.height)))
|
||
.child(fact("Children", pair(children.width, children.height))),
|
||
)
|
||
.into_any_element()
|
||
}
|
||
|
||
/// The inspector's panel: how to pick, the picked box's source and ids, and its model.
|
||
fn panel(
|
||
inspector: &mut Inspector,
|
||
window: &mut Window,
|
||
cx: &mut Context<Inspector>,
|
||
) -> AnyElement {
|
||
let picking = inspector.is_picking();
|
||
if picking {
|
||
// gpui holds a pick on a press without a redraw, so the panel draws each frame while picking.
|
||
window.request_animation_frame();
|
||
}
|
||
let place = inspector.active_element_id().map(|id| {
|
||
let location = id.path.source_location;
|
||
let ids: Vec<String> = id.path.global_id.iter().map(ToString::to_string).collect();
|
||
(
|
||
format!("{}:{}", location.file(), location.line()),
|
||
ids[ids.len().saturating_sub(3)..].join(" › "),
|
||
)
|
||
});
|
||
let states = inspector.render_inspector_states(window, cx);
|
||
let focus = window
|
||
.use_keyed_state("ely-inspector-focus", cx, |_, cx| cx.focus_handle())
|
||
.read(cx)
|
||
.clone();
|
||
let owner = cx.entity();
|
||
let theme = cx.theme();
|
||
let colors = &theme.colors;
|
||
let hint = match picking {
|
||
true => {
|
||
"Hover picks a box, the wheel steps out to the boxes around it, and a press holds it."
|
||
}
|
||
false => "Held. Pick chooses another box.",
|
||
};
|
||
let body = div()
|
||
.id("ely-inspector")
|
||
.size_full()
|
||
.flex()
|
||
.flex_col()
|
||
.gap_4()
|
||
.p_4()
|
||
.overflow_y_scroll()
|
||
.bg(colors.surface)
|
||
.border_l_1()
|
||
.border_color(colors.border)
|
||
.text_color(colors.fg)
|
||
.text_size(theme.text_size(TextSize::Sm))
|
||
.child(
|
||
div()
|
||
.flex()
|
||
.items_center()
|
||
.justify_between()
|
||
.gap_2()
|
||
.child(div().font_weight(FontWeight::SEMIBOLD).child("Inspector"))
|
||
.children((!picking).then(|| {
|
||
div().debug_selector(|| "inspector-pick".into()).child(
|
||
Button::new("ely-inspector-pick", "Pick")
|
||
.variant(ButtonVariant::Ghost)
|
||
.size(ControlSize::Sm)
|
||
.on_click(move |_, window, cx| {
|
||
owner.update(cx, |inspector, _| inspector.start_picking());
|
||
log::info!("inspector: picking");
|
||
window.refresh();
|
||
}),
|
||
)
|
||
})),
|
||
)
|
||
.child(
|
||
div()
|
||
.debug_selector(move || format!("inspector-picking-{picking}"))
|
||
.text_color(colors.fg_muted)
|
||
.child(hint),
|
||
)
|
||
.children(place.map(|(source, ids)| {
|
||
div()
|
||
.flex()
|
||
.flex_col()
|
||
.gap_1()
|
||
.child(literal(div()).child(Ellipsis::new(source)))
|
||
.child(
|
||
literal(div())
|
||
.text_color(colors.fg_muted)
|
||
.child(Ellipsis::new(ids)),
|
||
)
|
||
}))
|
||
.children(states);
|
||
// A second root, out of the app's scope: it owns Tab itself.
|
||
FocusScope::new(&focus)
|
||
.size_full()
|
||
.child(body)
|
||
.into_any_element()
|
||
}
|
||
|
||
/// Draws gpui's inspector, which debug builds have, in Ely's look. `window.toggle_inspector` opens it at the window's right, and the page gives up its width. Hovering picks the box under the pointer, the wheel steps out to the boxes around it, and a press holds it; the picked box shows its margin, border, padding and size over the page, and its model in the panel.
|
||
pub fn install_inspector(cx: &mut App) {
|
||
cx.set_inspector_renderer(Box::new(panel));
|
||
cx.register_inspector_element(|_, _| {
|
||
|_, state: &DivInspectorState, window: &mut Window, cx: &mut App| {
|
||
box_model(state, window, cx)
|
||
}
|
||
});
|
||
log::info!("inspector: installed");
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use gpui::{StyleRefinement, Styled, div, px, relative, rems};
|
||
|
||
use super::{Side, layers};
|
||
|
||
#[test]
|
||
fn layers_read_as_written() {
|
||
let mut style = div().mt(px(4.0)).mx_auto().p(rems(0.5)).border_2();
|
||
let written = layers(style.style(), px(16.0));
|
||
assert_eq!(
|
||
written.margin,
|
||
[Side::Px(4.0), Side::Auto, Side::Px(0.0), Side::Auto]
|
||
);
|
||
assert_eq!(written.padding, [Side::Px(8.0); 4]);
|
||
assert_eq!(written.border, [Side::Px(2.0); 4]);
|
||
let bare = layers(&StyleRefinement::default(), px(16.0));
|
||
assert_eq!(
|
||
bare.border,
|
||
[Side::Px(0.0); 4],
|
||
"no border written, none read"
|
||
);
|
||
let mut share = StyleRefinement::default();
|
||
share.padding.left = Some(relative(0.25));
|
||
let shown = layers(&share, px(16.0))
|
||
.padding
|
||
.map(|side| side.to_string());
|
||
assert_eq!(shown, ["0", "0", "0", "25%"]);
|
||
assert_eq!(Side::Share(0.25).drawn(), px(0.0), "a share draws nothing");
|
||
}
|
||
}
|