Charts · Navigation · Forms: band names widen a sideways chart's gutter; Pagination takes a size, speaks the locale and jumps to a typed page

- A horizontal chart's gutter grows to its widest band name, floored at the theme gutter and capped at half the chart; a name past the cap ends in an ellipsis (#10).
- Pagination's previous and next tooltips read i18n (English and zh-CN built in, app catalogs override); `.size(ControlSize)` sizes every control, Sm by default; `.jump()` adds a Go to field (#11).
- NumberInput keys the owner's value as shown: a draft survives a blur, a value set while focused lands when focus leaves, and an echo of its own commit is no new value. A ranged field holds room for its widest bound through `Input::least`.
This commit is contained in:
ZacharyZhang-NY committed 2026-10-06 13:29:09 -04:00
1 parent 2ce336895c
commit bd3772ad54
17 files changed
+438 -51

No files matched your search

+15 -5
View File
@@ -10,9 +10,12 @@ use super::{
};
use crate::{
theme::{ActiveTheme, Radius, TextSize},
typography::{LEADING, tabular, text_width},
typography::{Ellipsis, LEADING, tabular, text_width},
};
/// Room between a gutter's label and the plot.
pub(crate) const PAD: Rems = Rems(0.5);
/// A label centered under `x`, below the frame.
pub(crate) fn below(x: f32, frame: Rect, label: Div) -> Div {
div()
@@ -27,7 +30,7 @@ pub(crate) fn below(x: f32, frame: Rect, label: Div) -> Div {
}
/// A label centered on `y`, set right in the gutter.
pub(crate) fn beside(y: f32, gutter: Rems, label: Div) -> Div {
pub(crate) fn beside(y: f32, gutter: Pixels, label: Div) -> Div {
div()
.absolute()
.left_0()
@@ -37,7 +40,7 @@ pub(crate) fn beside(y: f32, gutter: Rems, label: Div) -> Div {
.flex()
.items_center()
.justify_end()
.pr_2()
.pr(PAD)
.child(label)
}
@@ -93,7 +96,7 @@ pub(crate) fn marks(
if plot.horizontal {
below(*at, rect, label)
} else {
beside(*at, sizes.gutter, label)
beside(*at, Pixels::from(rect.x), label)
}
})
.collect();
@@ -116,7 +119,14 @@ pub(crate) fn marks(
.filter(|(ix, _)| ix % every == 0)
.map(|(_, (at, label))| {
if plot.horizontal {
beside(*at, sizes.gutter, text(label.clone()))
let name = div()
.debug_selector(|| "chart-band".into())
.min_w_0()
.text_right()
.text_size(theme.text_size(TextSize::Xs))
.text_color(colors.fg_subtle)
.child(Ellipsis::new(label.clone()));
beside(*at, Pixels::from(rect.x), name)
} else {
below(*at, rect, text(label.clone()))
}
+1 -1
View File
@@ -135,7 +135,7 @@ impl RenderOnce for BurndownChart {
let levels: Vec<Div> = burn
.ticks
.iter()
.map(|(y, value)| beside(*y, sizes.gutter, label(compact(*value))))
.map(|(y, value)| beside(*y, sizes.gutter.to_pixels(rem), label(compact(*value))))
.collect();
let dates: Vec<Div> = marks(self.first, count as i32)
.into_iter()
+1 -1
View File
@@ -125,7 +125,7 @@ impl RenderOnce for CalendarHeatmap {
.expect("seven days hold every weekday");
beside(
top + side * (weekday as f32 + 0.5),
sizes.gutter,
sizes.gutter.to_pixels(rem),
text(date.strftime("%a").to_string()),
)
});
+17 -8
View File
@@ -6,7 +6,7 @@ use gpui::{
};
use super::{
axes::{anchored, marks},
axes::{PAD, anchored, marks},
geometry::{Geometry, Ink, Rect, frame},
glide::Glide,
paint::{Pen, measure, tint},
@@ -17,7 +17,8 @@ use super::{
use crate::{
buttons::{Button, ButtonVariant},
motion,
theme::{ActiveTheme, ControlSize, Radius},
theme::{ActiveTheme, ControlSize, Radius, TextSize},
typography::text_width,
};
pub(crate) type Format = Rc<dyn Fn(f64) -> String>;
@@ -127,12 +128,20 @@ pub(crate) fn plot(plot: Plot, base: Div, window: &mut Window, cx: &mut App) ->
.filter(|(_, to)| *to < count)
.unwrap_or((0, count.saturating_sub(1)));
let box_size = (f32::from(bounds.size.width), f32::from(bounds.size.height));
let rect = frame(
box_size,
pixels(sizes.gutter),
pixels(sizes.foot),
pixels(sizes.inset),
);
let gutter = if plot.horizontal {
let size = theme.text_size(TextSize::Xs).to_pixels(rem);
let widest = plot
.labels
.iter()
.map(|label| f32::from(text_width(label, size, window)))
.fold(0.0, f32::max);
(widest + pixels(PAD))
.max(pixels(sizes.gutter))
.min(box_size.0 / 2.0)
} else {
pixels(sizes.gutter)
};
let rect = frame(box_size, gutter, pixels(sizes.foot), pixels(sizes.inset));
let scene = Scene {
frame: rect,
values: &values,
+5 -1
View File
@@ -115,7 +115,11 @@ impl RenderOnce for RealtimeChart {
.whitespace_nowrap()
.text_size(theme.text_size(TextSize::Xs))
.text_color(colors.fg_subtle);
beside(scale.at(*tick), sizes.gutter, label.child(compact(*tick)))
beside(
scale.at(*tick),
sizes.gutter.to_pixels(rem),
label.child(compact(*tick)),
)
});
let (ink, levels) = (
tint(&colors, 0),
+43
View File
@@ -9,6 +9,7 @@ use gpui::{
};
use jiff::civil::date;
use super::axes::PAD;
use super::{
AreaChart, BarChart, BoxPlot, Bullet, BulletChart, CalendarHeatmap, ChartLegend, ChordDiagram,
FunnelChart, GanttChart, HeatmapChart, Histogram, LineChart, NetworkGraph, ParallelCoordinates,
@@ -330,3 +331,45 @@ fn every_chart_fills_a_column_its_block_measures_by_content(cx: &mut TestAppCont
"each chart spans the card inside its padding"
);
}
/// A sideways bar chart `wide` across with one band named `name`.
struct Sideways(String, f32);
impl Render for Sideways {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
div().w(px(self.1)).child(
BarChart::new("sideways", [self.0.clone()])
.series(Series::new("s", [3.0]))
.horizontal(),
)
}
}
#[gpui::test]
fn a_long_band_name_widens_the_gutter_to_half_the_chart(cx: &mut TestAppContext) {
cx.update(Theme::init);
let long = "x".repeat(200);
for (name, wide) in [
("claude-sonnet-4-5-20250929", 400.0),
(&long, 400.0),
(&long, 80.0),
] {
let (_, cx) = cx.add_window_view(|_, _| Sideways(name.to_string(), wide));
settle(cx);
let root = cx.debug_bounds("chart-root").expect("the chart draws");
let band = cx.debug_bounds("chart-band").expect("the band is named");
let half = root.left() + px(wide / 2.0);
let pad = cx.update(|window, _| PAD.to_pixels(window.rem_size()));
assert!(
band.left() >= root.left(),
"{wide}: the name's box starts in the chart"
);
assert!(
band.right() + pad <= half,
"{wide}: the gutter takes half at most"
);
if wide > 200.0 {
assert!(band.size.width > px(48.0), "past the theme gutter");
}
}
}
+1 -1
View File
@@ -371,7 +371,7 @@ impl RenderOnce for HeatmapChart {
let row_labels = self.rows.iter().enumerate().map(|(ix, (label, _))| {
beside(
rect.y + cell_h * (ix as f32 + 0.5),
sizes.gutter,
sizes.gutter.to_pixels(rem),
text(label.clone()),
)
});