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.
This commit is contained in:
ZacharyZhang-NY committed 2026-09-26 15:17:45 -04:00
1 parent e3038647c8
commit 3d9cf1008e
6 files changed
+122 -52

No files matched your search

+39 -34
View File
@@ -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();
})
}
+2
View File
@@ -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;
+30 -3
View File
@@ -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
View File
@@ -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() {