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:
1 parent
2ce336895c
commit
bd3772ad54
17 files changed
+438
-51
No files matched your search
+15
-5
@@ -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()))
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
@@ -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()),
|
||||
)
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user