Files
Ely-GPUI-Components/src/charts/tiles.rs
T
ZacharyZhang-NYandClaude Opus 5.5 94deeaed52 Port · Build: Zed's gpui at 1a28cff
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.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 20:30:17 -04:00

462 lines
16 KiB
Rust

use std::rc::Rc;
use gpui::{
App, Bounds, Div, ElementId, Entity, HoverListenerMode, Hsla, InteractiveElement, IntoElement,
MouseMoveEvent, ParentElement, Pixels, Refineable, RenderOnce, SharedString,
StatefulInteractiveElement, StyleRefinement, Styled, Window, canvas, div, fill,
};
use super::{
axes::{anchored, below, beside},
geometry::{Rect, frame},
layout::squarify,
paint::{measure, place, tint},
parts::ChartTooltip,
plot::Format,
scale::compact,
};
use crate::{
theme::{ActiveTheme, Radius, TextSize},
typography::{format, tabular},
};
/// A chart's own state when the pointer picks one part: its box and the part under the pointer.
#[derive(Default)]
pub(crate) struct Tiles {
pub bounds: Bounds<Pixels>,
pub hover: Option<usize>,
}
impl Tiles {
/// The part under the pointer, if there are still that many parts.
pub(crate) fn pointed(&self, count: usize) -> Option<usize> {
self.hover.filter(|ix| *ix < count)
}
}
/// Keeps `tiles` answering the pointer: `find` names the tile at a place in the box.
pub(crate) fn tracked(
root: Div,
id: ElementId,
tiles: &Entity<Tiles>,
find: impl Fn((f32, f32)) -> Option<usize> + 'static,
) -> gpui::Stateful<Div> {
let (moved, left) = (tiles.clone(), tiles.clone());
root.id(id)
.on_mouse_move(move |event: &MouseMoveEvent, _, cx| {
let offset = event.position - moved.read(cx).bounds.origin;
let next = find((f32::from(offset.x), f32::from(offset.y)));
moved.update(cx, |tiles, cx| {
if tiles.hover != next {
tiles.hover = next;
cx.notify();
}
})
})
.hover_listener_mode(HoverListenerMode::InputModalityIndependent)
.on_hover(move |inside, _, cx| {
if !*inside {
left.update(cx, |tiles, cx| {
tiles.hover = None;
cx.notify();
})
}
})
}
/// A key of five steps from none to most, between two words.
pub(crate) fn key(steps: [Hsla; 5], (less, more): (&'static str, &'static str), cx: &App) -> Div {
let theme = cx.theme();
div()
.flex()
.items_center()
.justify_end()
.gap_1()
.text_size(theme.text_size(TextSize::Xs))
.text_color(theme.colors.fg_subtle)
.child(less)
.children(steps.map(|step| {
div()
.size(theme.status_dot() * 1.5)
.rounded(theme.radius(Radius::Sm))
.bg(step)
}))
.child(more)
}
/// Parts of a whole as tiles whose areas compare, laid out close to square, largest first. Hover reads one.
#[derive(IntoElement)]
pub struct Treemap {
base: Div,
id: ElementId,
tiles: Vec<(SharedString, f64)>,
format: Format,
}
impl Treemap {
pub fn new(id: impl Into<ElementId>) -> Self {
Self {
base: div(),
id: id.into(),
tiles: Vec::new(),
format: Rc::new(compact),
}
}
pub fn tile(mut self, name: impl Into<SharedString>, value: f64) -> Self {
assert!(
value.is_finite() && value >= 0.0,
"a tile needs a value of zero or more"
);
self.tiles.push((name.into(), value));
self
}
pub fn format(mut self, format: impl Fn(f64) -> String + 'static) -> Self {
self.format = Rc::new(format);
self
}
}
impl Styled for Treemap {
fn style(&mut self) -> &mut StyleRefinement {
self.base.style()
}
}
impl RenderOnce for Treemap {
fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let tiles: Entity<Tiles> =
window.use_keyed_state((self.id.clone(), "tiles"), cx, |_, _| Tiles::default());
let (bounds, hover) = (
tiles.read(cx).bounds,
tiles.read(cx).pointed(self.tiles.len()),
);
let theme = cx.theme();
let (colors, sizes, rem) = (theme.colors.clone(), theme.chart(), window.rem_size());
let pixels = |length: gpui::Rems| f32::from(length.to_pixels(rem));
let mut order: Vec<usize> = (0..self.tiles.len()).collect();
order.sort_by(|a, b| self.tiles[*b].1.total_cmp(&self.tiles[*a].1));
let whole = Rect {
x: 0.0,
y: 0.0,
w: f32::from(bounds.size.width),
h: f32::from(bounds.size.height),
};
let laid = squarify(
&order.iter().map(|ix| self.tiles[*ix].1).collect::<Vec<_>>(),
whole,
);
let mut rects = vec![Rect::default(); self.tiles.len()];
let gap = pixels(sizes.stroke) / 2.0;
for (slot, ix) in order.iter().enumerate() {
let rect = laid[slot];
rects[*ix] = Rect {
x: rect.x + gap,
y: rect.y + gap,
w: (rect.w - gap * 2.0).max(0.0),
h: (rect.h - gap * 2.0).max(0.0),
};
}
let total: f64 = self.tiles.iter().map(|(_, value)| value).sum();
let (room, format) = (pixels(sizes.label), self.format.clone());
let labels: Vec<Div> = rects
.iter()
.zip(&self.tiles)
.filter(|(rect, _)| rect.w >= room && rect.h >= room / 2.0)
.map(|(rect, (name, value))| {
div()
.absolute()
.left(Pixels::from(rect.x))
.top(Pixels::from(rect.y))
.w(Pixels::from(rect.w))
.h(Pixels::from(rect.h))
.overflow_hidden()
.p_2()
.text_size(theme.text_size(TextSize::Xs))
.child(div().truncate().text_color(colors.fg).child(name.clone()))
.child(
tabular(div())
.text_color(colors.fg_muted)
.child(format(*value)),
)
})
.collect();
let tooltip = hover.map(|ix| {
let rect = rects[ix];
let share = if total > 0.0 {
self.tiles[ix].1 / total
} else {
0.0
};
let card = ChartTooltip::new(self.tiles[ix].0.clone())
.row(
Some(tint(&colors, ix)),
"Share",
format::percent(share, 1, false),
)
.row(None, "Value", format(self.tiles[ix].1));
let x = rect.x + rect.w;
anchored(
(x.min(whole.w), rect.y + rect.h / 2.0),
x > whole.w * 0.6,
card,
)
});
let (corner, hairline, painted, hit) = (
theme.radius(Radius::Sm).to_pixels(rem),
sizes.hairline,
rects.clone(),
rects,
);
let mut root = div()
.debug_selector(|| "chart-root".into())
.relative()
.h(sizes.height);
root.style().refine(self.base.style());
tracked(
root,
(self.id.clone(), "treemap").into(),
&tiles,
move |at| hit.iter().position(|rect| rect.contains(at)),
)
.child(
canvas(
|_, _, _| {},
move |bounds, _, window, _| {
for (ix, rect) in painted.iter().enumerate() {
let ink = tint(&colors, ix);
let strength = if hover == Some(ix) { 0.34 } else { 0.2 };
let tile = fill(place(bounds.origin, *rect), ink.opacity(strength))
.corner_radii(corner);
window.paint_quad(
tile.border_widths(hairline).border_color(ink.opacity(0.6)),
);
}
},
)
.absolute()
.inset_0(),
)
.children(labels)
.children(tooltip)
.child(measure(tiles, |tiles| &mut tiles.bounds))
}
}
/// A grid of values, a row per label down the side and a column per label along the bottom, each cell as strong as its value. Hover reads a cell.
#[derive(IntoElement)]
pub struct HeatmapChart {
base: Div,
id: ElementId,
columns: Vec<SharedString>,
rows: Vec<(SharedString, Vec<f64>)>,
format: Format,
}
impl HeatmapChart {
pub fn new(
id: impl Into<ElementId>,
columns: impl IntoIterator<Item = impl Into<SharedString>>,
) -> Self {
let columns = columns.into_iter().map(Into::into).collect();
Self {
base: div(),
id: id.into(),
columns,
rows: Vec::new(),
format: Rc::new(compact),
}
}
/// A row of values, one for each column.
pub fn row(
mut self,
label: impl Into<SharedString>,
values: impl IntoIterator<Item = f64>,
) -> Self {
let values: Vec<f64> = values.into_iter().collect();
assert_eq!(
values.len(),
self.columns.len(),
"a heatmap row needs a value per column"
);
assert!(
values.iter().all(|value| value.is_finite()),
"a heatmap needs finite values"
);
self.rows.push((label.into(), values));
self
}
pub fn format(mut self, format: impl Fn(f64) -> String + 'static) -> Self {
self.format = Rc::new(format);
self
}
}
impl Styled for HeatmapChart {
fn style(&mut self) -> &mut StyleRefinement {
self.base.style()
}
}
/// Five steps of one ink, faint to full, for a key.
fn steps(ink: Hsla) -> [Hsla; 5] {
[0.08, 0.3, 0.52, 0.76, 1.0].map(|strength| ink.opacity(strength))
}
impl RenderOnce for HeatmapChart {
fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let tiles: Entity<Tiles> =
window.use_keyed_state((self.id.clone(), "tiles"), cx, |_, _| Tiles::default());
let cells = self.rows.len() * self.columns.len();
let (bounds, hover) = (tiles.read(cx).bounds, tiles.read(cx).pointed(cells));
let theme = cx.theme();
let (colors, sizes, rem) = (theme.colors.clone(), theme.chart(), window.rem_size());
let pixels = |length: gpui::Rems| f32::from(length.to_pixels(rem));
let size = (f32::from(bounds.size.width), f32::from(bounds.size.height));
let rect = frame(
size,
pixels(sizes.gutter),
pixels(sizes.foot),
pixels(sizes.inset),
);
let (columns, rows) = (self.columns.len(), self.rows.len());
let (cell_w, cell_h) = (rect.w / columns.max(1) as f32, rect.h / rows.max(1) as f32);
let cell = |row: usize, column: usize| Rect {
x: rect.x + cell_w * column as f32,
y: rect.y + cell_h * row as f32,
w: cell_w,
h: cell_h,
};
let (low, high) = self
.rows
.iter()
.flat_map(|(_, values)| values)
.fold((f64::MAX, f64::MIN), |(low, high), value| {
(low.min(*value), high.max(*value))
});
let ink = tint(&colors, 0);
let strength = move |value: f64| {
if high > low {
0.08 + 0.92 * ((value - low) / (high - low)) as f32
} else {
0.5
}
};
let text = |content: SharedString| {
div()
.flex_none()
.whitespace_nowrap()
.text_size(theme.text_size(TextSize::Xs))
.text_color(colors.fg_subtle)
.child(content)
};
let every = ((self.columns.len() as f32 * pixels(sizes.label) / rect.w.max(1.0)).ceil()
as usize)
.max(1);
let column_labels = self
.columns
.iter()
.enumerate()
.filter(|(ix, _)| ix % every == 0)
.map(|(ix, label)| {
below(
rect.x + cell_w * (ix as f32 + 0.5),
rect,
text(label.clone()),
)
});
let row_labels = self.rows.iter().enumerate().map(|(ix, (label, _))| {
beside(
rect.y + cell_h * (ix as f32 + 0.5),
sizes.gutter,
text(label.clone()),
)
});
let format = self.format.clone();
let tooltip = hover.map(|at| {
let (row, column) = (at / columns, at % columns);
let place = cell(row, column);
let card =
ChartTooltip::new(format!("{} · {}", self.rows[row].0, self.columns[column])).row(
Some(ink.opacity(strength(self.rows[row].1[column]))),
"Value",
format(self.rows[row].1[column]),
);
anchored(
(place.x + place.w, place.y + place.h / 2.0),
place.x > size.0 * 0.6,
card,
)
});
let cells: Vec<(Rect, Hsla)> = self
.rows
.iter()
.enumerate()
.flat_map(|(row, (_, values))| {
values
.iter()
.enumerate()
.map(move |(column, value)| (row, column, *value))
})
.map(|(row, column, value)| (cell(row, column), ink.opacity(strength(value))))
.collect();
let (gap, corner, edge, hairline) = (
pixels(sizes.stroke) / 2.0,
theme.radius(Radius::Sm).to_pixels(rem),
colors.fg,
sizes.hairline,
);
let legend = key(steps(ink), ("Less", "More"), cx);
let mut root = div()
.debug_selector(|| "chart-root".into())
.relative()
.h(sizes.height);
root.style().refine(self.base.style());
let chart = tracked(
div().relative().flex_1().min_h_0(),
(self.id.clone(), "heatmap").into(),
&tiles,
move |at| {
(rect.contains(at) && rows > 0 && columns > 0).then(|| {
let (row, column) = (
((at.1 - rect.y) / cell_h) as usize,
((at.0 - rect.x) / cell_w) as usize,
);
row.min(rows - 1) * columns + column.min(columns - 1)
})
},
)
.child(
canvas(
|_, _, _| {},
move |bounds, _, window, _| {
for (ix, (cell, color)) in cells.iter().enumerate() {
let inner = Rect {
x: cell.x + gap,
y: cell.y + gap,
w: (cell.w - gap * 2.0).max(0.0),
h: (cell.h - gap * 2.0).max(0.0),
};
let quad = fill(place(bounds.origin, inner), *color).corner_radii(corner);
window.paint_quad(if hover == Some(ix) {
quad.border_widths(hairline).border_color(edge)
} else {
quad
});
}
},
)
.absolute()
.inset_0(),
)
.children(column_labels)
.children(row_labels)
.children(tooltip)
.child(measure(tiles, |tiles| &mut tiles.bounds));
root.flex().flex_col().gap_2().child(chart).child(legend)
}
}