diff --git a/TASKS.md b/TASKS.md index db40620..e9867f2 100644 --- a/TASKS.md +++ b/TASKS.md @@ -299,3 +299,4 @@ Per-component lines live in `tasks/`. Tags there: | T25c | 2 | PASS | | | T25d | 1 | FAIL | the timeline's strip never scrolled, its child stretched to the box; a focused shot past the edge stayed hidden; many voice tags ran past a 280px row | | T25d | 2 | PASS | | +| T25e | 1 | FAIL | a description row never wrapped, so a 280px model card broke its facts mid-word; the fine-tune bar's share had no test | diff --git a/src/data_display/records.rs b/src/data_display/records.rs index e39ad43..653312e 100644 --- a/src/data_display/records.rs +++ b/src/data_display/records.rs @@ -9,7 +9,7 @@ use crate::{ theme::{ActiveTheme, ControlSize, TextSize}, }; -/// Labels and their values, one pair a row with the labels in a quiet column, or stacked with each label above its value. +/// Labels and their values, one pair a row with the labels in a quiet column, or stacked with each label above its value. A value short of room drops under its label. #[derive(IntoElement, Default)] pub struct DescriptionList { items: Vec<(SharedString, AnyElement)>, @@ -59,7 +59,9 @@ impl RenderOnce for DescriptionList { div() .flex() .when(stacked, |row| row.flex_col().gap_0p5()) - .when(!stacked, |row| row.items_baseline().gap_4()) + .when(!stacked, |row| { + row.flex_wrap().items_baseline().gap_x_4().gap_y_0p5() + }) .py_2() .when(lined && ix < last, |row| { row.border_b_1().border_color(colors.border) @@ -73,7 +75,8 @@ impl RenderOnce for DescriptionList { div() .flex() .flex_1() - .min_w_0() + .when(stacked, |value| value.min_w_0()) + .when(!stacked, |value| value.min_w(theme.label_width() * 0.5)) .text_color(colors.fg) .child(div().min_w_0().child(value)), ) diff --git a/src/data_display/tests.rs b/src/data_display/tests.rs index 6fb551c..8726ed1 100644 --- a/src/data_display/tests.rs +++ b/src/data_display/tests.rs @@ -5,11 +5,11 @@ use std::{ }; use gpui::{ - Context, IntoElement, Modifiers, ParentElement, Pixels, Render, Styled, TestAppContext, + Bounds, Context, IntoElement, Modifiers, ParentElement, Pixels, Render, Styled, TestAppContext, VisualTestContext, Window, div, point, px, }; -use super::{Carousel, PropertyGrid, PropertyGroup}; +use super::{Carousel, DescriptionList, PropertyGrid, PropertyGroup}; use crate::{primitives::Measure, theme::Theme}; /// A property grid whose drawn height the test reads. @@ -68,6 +68,54 @@ fn a_property_group_folds_from_its_header_and_stays_folded(cx: &mut TestAppConte assert_eq!(height.get(), open, "a second press opens it"); } +/// A one-row description list, this wide, that reports where the list and its value sit. +struct Facts(Pixels, Rc; 2]>>); + +impl Render for Facts { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + let (list, value) = (self.1.clone(), self.1.clone()); + let value = Measure::new("value", move |bounds, _, _| { + value.set([value.get()[0], bounds]) + }) + .child("Apache-2.0"); + div().w(self.0).child( + Measure::new("list", move |bounds, _, _| { + list.set([bounds, list.get()[1]]) + }) + .child(DescriptionList::new().item("License", value)), + ) + } +} + +#[gpui::test] +fn a_value_drops_below_its_label_when_the_row_runs_short(cx: &mut TestAppContext) { + cx.update(Theme::init); + let mut place = |width: f32| { + let seen = Rc::new(Cell::new([Bounds::default(); 2])); + let shared = seen.clone(); + let (_, cx) = cx.add_window_view(move |_, _| Facts(px(width), shared)); + settle(cx); + let [list, value] = seen.get(); + (value.left() - list.left(), value.top() - list.top()) + }; + let (wide_left, wide_top) = place(400.0); + let (narrow_left, narrow_top) = place(240.0); + assert_eq!( + wide_left, + px(176.0), + "wide, the value sits beside its label" + ); + assert_eq!( + narrow_left, + Pixels::ZERO, + "narrow, the value starts under its label" + ); + assert!( + narrow_top > wide_top, + "and a line lower: {wide_top:?} to {narrow_top:?}" + ); +} + /// A three-slide carousel, 200 wide, that turns every five seconds; each slide reports where it sits. struct Deck(Rc>); diff --git a/src/generative/tuning.rs b/src/generative/tuning.rs index c99a3ab..4b67f12 100644 --- a/src/generative/tuning.rs +++ b/src/generative/tuning.rs @@ -39,6 +39,16 @@ impl TunePhase { } } +/// A running job's bar, whole epochs and the share of the one under way, and the line under it. +pub(crate) fn progress(epoch: usize, epochs: usize, share: f32) -> (f32, String) { + let whole = ((epoch - 1) as f32 + share) / epochs as f32; + let line = format!( + "Epoch {epoch} of {epochs} · {}", + percent(f64::from(share), 0, false) + ); + (whole, line) +} + /// A fine-tuning job: its name, the base model and data it learns from, where it stands, how far it has come by epoch, and its loss on training and held-out data. Cancel while it waits or trains; Open once done. #[derive(IntoElement)] pub struct FineTuneJobCard { @@ -128,7 +138,7 @@ impl RenderOnce for FineTuneJobCard { }; let progress = match &self.phase { TunePhase::Running(epoch, epochs, share) => { - let whole = ((*epoch - 1) as f32 + share) / *epochs as f32; + let (whole, line) = progress(*epoch, *epochs, *share); Some( div() .flex() @@ -143,10 +153,7 @@ impl RenderOnce for FineTuneJobCard { tabular(div()) .text_size(theme.text_size(TextSize::Xs)) .text_color(colors.fg_muted) - .child(format!( - "Epoch {epoch} of {epochs} · {}", - percent(f64::from(*share), 0, false) - )), + .child(line), ) .into_any_element(), ) @@ -228,3 +235,17 @@ impl RenderOnce for FineTuneJobCard { .children(chart) } } + +#[cfg(test)] +mod tests { + use super::progress; + + #[test] + fn the_bar_counts_whole_epochs_and_the_share_of_the_one_under_way() { + let (whole, line) = progress(2, 3, 0.64); + assert!((whole - 0.5467).abs() < 1e-4, "{whole}"); + assert_eq!(line, "Epoch 2 of 3 · 64%"); + assert_eq!(progress(1, 1, 0.5), (0.5, "Epoch 1 of 1 · 50%".to_string())); + assert_eq!(progress(3, 3, 1.0).0, 1.0); + } +}