Generative: the embedding turns by drag past its edge; version diffs are tested
The turned embedding turns through gpui's drag, marked with its view, so a drag keeps turning past the plot's edge. What a chosen version is set against, and its heading, move into versions::compared, with a test. A hidden legend entry dims its key and not its ring, and an embedding refuses a group named twice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
98b1a6416c
commit
54e0384582
6 files changed
+122
-52
No files matched your search
@@ -301,3 +301,4 @@ Per-component lines live in `tasks/`. Tags there:
|
||||
| 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 |
|
||||
| T25e | 2 | PASS | |
|
||||
| T25f | 1 | FAIL | a drag on the turned embedding froze past the plot's edge; which version a diff sets against, and its heading, had no test |
|
||||
+9
-5
@@ -64,9 +64,6 @@ impl RenderOnce for ChartLegend {
|
||||
div()
|
||||
.id((self.id.clone(), format!("entry-{ix}")))
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_1p5()
|
||||
.when(hidden, |entry| entry.opacity(0.4))
|
||||
.when_some(on_toggle, |entry, on_toggle| {
|
||||
entry
|
||||
.px_1()
|
||||
@@ -81,8 +78,15 @@ impl RenderOnce for ChartLegend {
|
||||
})
|
||||
.on_click(move |_, window, cx| on_toggle(&key, window, cx))
|
||||
})
|
||||
.child(div().size(theme.status_dot()).rounded_full().bg(color))
|
||||
.child(name)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_1p5()
|
||||
.when(hidden, |key| key.opacity(0.4))
|
||||
.child(div().size(theme.status_dot()).rounded_full().bg(color))
|
||||
.child(name),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
+39
-34
@@ -1,12 +1,13 @@
|
||||
use std::{collections::HashSet, rc::Rc};
|
||||
|
||||
use gpui::{
|
||||
App, Bounds, ElementId, InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels,
|
||||
Point, RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window, canvas, div, fill,
|
||||
point, prelude::*, size, transparent_black,
|
||||
App, Bounds, DragMoveEvent, ElementId, EmptyView, EntityId, InteractiveElement, IntoElement,
|
||||
MouseButton, ParentElement, Pixels, Point, RenderOnce, SharedString,
|
||||
StatefulInteractiveElement, Styled, Window, canvas, div, fill, point, prelude::*, size,
|
||||
transparent_black,
|
||||
};
|
||||
|
||||
use super::lens::{Lens, START, STEP, radius, turned};
|
||||
use super::lens::{DRAG, Lens, START, STEP, radius, turned};
|
||||
use crate::{
|
||||
charts::{ChartLegend, ChartTooltip, anchored},
|
||||
primitives::{FocusRing, checked_ratio, tab_stop},
|
||||
@@ -22,18 +23,18 @@ pub struct Embedded {
|
||||
pub at: [f32; 3],
|
||||
}
|
||||
|
||||
/// What a drag turns the view for each pixel.
|
||||
const DRAG: f32 = 0.01;
|
||||
|
||||
/// A box's width and height in pixels.
|
||||
fn extent(bounds: Bounds<Pixels>) -> (f32, f32) {
|
||||
(f32::from(bounds.size.width), f32::from(bounds.size.height))
|
||||
}
|
||||
|
||||
/// A turning drag, marked with its view.
|
||||
struct Spin(EntityId);
|
||||
|
||||
/// The view's own state: its turn, where a drag was last, the point under the pointer, the groups the legend hid, and the plot's bounds.
|
||||
struct View {
|
||||
turn: (f32, f32),
|
||||
from: Option<Point<Pixels>>,
|
||||
from: Point<Pixels>,
|
||||
hovered: Option<usize>,
|
||||
hidden: HashSet<usize>,
|
||||
bounds: Bounds<Pixels>,
|
||||
@@ -59,6 +60,9 @@ impl EmbeddingVisualizer {
|
||||
) -> Self {
|
||||
let points: Vec<Embedded> = points.into_iter().collect();
|
||||
let groups: Vec<SharedString> = groups.into_iter().map(Into::into).collect();
|
||||
for (ix, name) in groups.iter().enumerate() {
|
||||
assert!(!groups[..ix].contains(name), "group {name} named twice");
|
||||
}
|
||||
for point in &points {
|
||||
assert!(
|
||||
point.group < groups.len(),
|
||||
@@ -100,7 +104,7 @@ impl RenderOnce for EmbeddingVisualizer {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let view = window.use_keyed_state((self.id.clone(), "view"), cx, |_, _| View {
|
||||
turn: START,
|
||||
from: None,
|
||||
from: Point::default(),
|
||||
hovered: None,
|
||||
hidden: HashSet::new(),
|
||||
bounds: Bounds::default(),
|
||||
@@ -158,7 +162,8 @@ impl RenderOnce for EmbeddingVisualizer {
|
||||
cx.notify();
|
||||
})
|
||||
});
|
||||
let (measure, moved, pressed, lifted, keys, left) = (
|
||||
let owner = view.entity_id();
|
||||
let (measure, moved, pressed, spun, keys, left) = (
|
||||
view.clone(),
|
||||
view.clone(),
|
||||
view.clone(),
|
||||
@@ -236,19 +241,10 @@ impl RenderOnce for EmbeddingVisualizer {
|
||||
)
|
||||
.children(tooltip)
|
||||
.on_mouse_move(move |event, _, cx| {
|
||||
if event.pressed_button.is_some() {
|
||||
return;
|
||||
}
|
||||
moved.update(cx, |view, cx| {
|
||||
if let (true, Some(from), Some(MouseButton::Left)) =
|
||||
(three, view.from, event.pressed_button)
|
||||
{
|
||||
let delta = event.position - from;
|
||||
view.turn = turned(
|
||||
view.turn,
|
||||
(f32::from(delta.x) * DRAG, f32::from(delta.y) * DRAG),
|
||||
);
|
||||
(view.from, view.hovered) = (Some(event.position), None);
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
let bounds = view.bounds;
|
||||
let at = (
|
||||
f32::from(event.position.x - bounds.left()),
|
||||
@@ -266,24 +262,33 @@ impl RenderOnce for EmbeddingVisualizer {
|
||||
}
|
||||
})
|
||||
})
|
||||
.on_mouse_down(MouseButton::Left, move |event, _, cx| {
|
||||
pressed.update(cx, |view, _| view.from = Some(event.position))
|
||||
})
|
||||
.on_mouse_up(MouseButton::Left, move |_, _, cx| {
|
||||
lifted.update(cx, |view, _| {
|
||||
if view.from.take().is_some() && three {
|
||||
log::info!(
|
||||
"embedding: turned to {:.2}, {:.2}",
|
||||
view.turn.0,
|
||||
view.turn.1
|
||||
);
|
||||
.when(three, |area| {
|
||||
area.on_mouse_down(MouseButton::Left, move |event, _, cx| {
|
||||
pressed.update(cx, |view, _| view.from = event.position)
|
||||
})
|
||||
.on_drag(Spin(owner), |_, _, _, cx| {
|
||||
log::info!("embedding: a drag turns the view");
|
||||
cx.new(|_| EmptyView)
|
||||
})
|
||||
.on_drag_move(move |event: &DragMoveEvent<Spin>, _, cx| {
|
||||
if event.drag(cx).0 != owner {
|
||||
return;
|
||||
}
|
||||
spun.update(cx, |view, cx| {
|
||||
let delta = event.event.position - view.from;
|
||||
view.turn = turned(
|
||||
view.turn,
|
||||
(f32::from(delta.x) * DRAG, f32::from(delta.y) * DRAG),
|
||||
);
|
||||
(view.from, view.hovered) = (event.event.position, None);
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
})
|
||||
.on_hover(move |inside, _, cx| {
|
||||
if !inside {
|
||||
left.update(cx, |view, cx| {
|
||||
(view.from, view.hovered) = (None, None);
|
||||
view.hovered = None;
|
||||
cx.notify();
|
||||
})
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ const EYE: f32 = 4.0;
|
||||
pub(crate) const START: (f32, f32) = (0.6, 0.35);
|
||||
/// What a key turns the view.
|
||||
pub(crate) const STEP: f32 = PI / 12.0;
|
||||
/// What a drag turns the view for each pixel.
|
||||
pub(crate) const DRAG: f32 = 0.01;
|
||||
/// Farthest the view tips up or down, short of looking straight along the upright axis.
|
||||
const TIP: f32 = 1.4;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use gpui::{
|
||||
AppContext, Bounds, Context, Entity, IntoElement, Modifiers, ParentElement, Pixels, Render,
|
||||
Styled, TestAppContext, VisualTestContext, Window, div, point, px,
|
||||
AppContext, Bounds, Context, Entity, IntoElement, Modifiers, MouseButton, ParentElement,
|
||||
Pixels, Render, Styled, TestAppContext, VisualTestContext, Window, div, point, px,
|
||||
};
|
||||
use jiff::Timestamp;
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::{
|
||||
forms::TextInput,
|
||||
generative::{
|
||||
Embedded, EmbeddingVisualizer, PromptPlayground, PromptVersion, PromptVersionHistory,
|
||||
lens::{Lens, START, STEP, radius, turned},
|
||||
lens::{DRAG, Lens, START, STEP, radius, turned},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -105,6 +105,33 @@ fn a_turned_view_takes_tab_and_spins_with_the_arrows(cx: &mut TestAppContext) {
|
||||
assert!((tip.origin - at).x.abs() < px(0.5) && (tip.origin - at).y.abs() < px(0.5));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_drag_keeps_turning_past_the_plots_edge(cx: &mut TestAppContext) {
|
||||
let cx = space(true, cx);
|
||||
let middle = cx
|
||||
.debug_bounds("embedding-area")
|
||||
.expect("the area draws")
|
||||
.center();
|
||||
let (none, down) = (Modifiers::none(), |y: f32| middle + point(px(0.0), px(y)));
|
||||
cx.simulate_mouse_down(middle, MouseButton::Left, none);
|
||||
cx.simulate_mouse_move(down(50.0), MouseButton::Left, none);
|
||||
cx.simulate_mouse_move(down(250.0), MouseButton::Left, none);
|
||||
cx.simulate_mouse_up(down(250.0), MouseButton::Left, none);
|
||||
settle(cx);
|
||||
let at = aim(A, Some(turned(START, (0.0, 250.0 * DRAG))), cx);
|
||||
hover(at, cx);
|
||||
let tip = cx
|
||||
.debug_bounds("embedding-tip")
|
||||
.expect("a is named where the whole drag turned it");
|
||||
assert!((tip.origin - at).x.abs() < px(0.5) && (tip.origin - at).y.abs() < px(0.5));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
#[should_panic(expected = "named twice")]
|
||||
fn a_group_named_twice_fails_loud() {
|
||||
EmbeddingVisualizer::new("space", points(), ["Docs", "Docs"]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
#[should_panic(expected = "sits outside -1 to 1")]
|
||||
fn a_point_past_the_cube_fails_loud() {
|
||||
|
||||
+41
-10
@@ -91,6 +91,19 @@ impl RenderOnce for PromptDiff {
|
||||
}
|
||||
}
|
||||
|
||||
/// What the chosen version is set against: a heading naming the version before it, and that version's text, empty for the first.
|
||||
pub(crate) fn compared(versions: &[PromptVersion], chosen: usize) -> (SharedString, SharedString) {
|
||||
let heading = match versions.len() - chosen {
|
||||
1 => "The first version".into(),
|
||||
place => format!("Changes from version {}", place - 1).into(),
|
||||
};
|
||||
let base = versions
|
||||
.get(chosen + 1)
|
||||
.map(|version| version.text.clone())
|
||||
.unwrap_or_default();
|
||||
(heading, base)
|
||||
}
|
||||
|
||||
/// A prompt as saved: its key, its words, who saved it, when, and a note on what changed.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct PromptVersion {
|
||||
@@ -163,11 +176,7 @@ impl RenderOnce for PromptVersionHistory {
|
||||
let theme = cx.theme();
|
||||
let colors = theme.colors.clone();
|
||||
let chosen = self.chosen;
|
||||
let before = self
|
||||
.versions
|
||||
.get(chosen + 1)
|
||||
.map(|version| version.text.clone())
|
||||
.unwrap_or_default();
|
||||
let (heading, before) = compared(&self.versions, chosen);
|
||||
let shown = &self.versions[chosen];
|
||||
let restore = self
|
||||
.on_restore
|
||||
@@ -182,10 +191,6 @@ impl RenderOnce for PromptVersionHistory {
|
||||
restore(chosen, window, cx)
|
||||
})
|
||||
});
|
||||
let heading = match self.versions.len() - chosen {
|
||||
1 => "The first version".to_string(),
|
||||
place => format!("Changes from version {}", place - 1),
|
||||
};
|
||||
let rows = self.versions.iter().zip(focuses).enumerate().map(
|
||||
|(ix, (version, (focus, focused)))| {
|
||||
let on = ix == chosen;
|
||||
@@ -297,7 +302,33 @@ impl RenderOnce for PromptVersionHistory {
|
||||
mod tests {
|
||||
use similar::ChangeTag;
|
||||
|
||||
use super::changes;
|
||||
use jiff::Timestamp;
|
||||
|
||||
use super::{PromptVersion, changes, compared};
|
||||
|
||||
#[test]
|
||||
fn a_version_compares_with_the_one_before_it() {
|
||||
let version = |n: usize| PromptVersion {
|
||||
key: format!("v{n}").into(),
|
||||
text: format!("text {n}").into(),
|
||||
author: "Mira".into(),
|
||||
at: Timestamp::UNIX_EPOCH,
|
||||
note: format!("note {n}").into(),
|
||||
};
|
||||
let versions: Vec<PromptVersion> = (1..=4).rev().map(version).collect();
|
||||
assert_eq!(
|
||||
compared(&versions, 2),
|
||||
("Changes from version 1".into(), "text 1".into())
|
||||
);
|
||||
assert_eq!(
|
||||
compared(&versions, 3),
|
||||
("The first version".into(), "".into())
|
||||
);
|
||||
assert_eq!(
|
||||
compared(&versions, 0),
|
||||
("Changes from version 3".into(), "text 3".into())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_change_keeps_words_and_marks_what_went_and_came() {
|
||||
|
||||
Reference in new issue
Block a user