T06f forms other: signature pad, code and JSON inputs, font, icon and emoji pickers, language, country and currency comboboxes

This commit is contained in:
ZacharyZhang-NY committed 2026-09-25 09:41:29 -04:00
1 parent ffd634dab6
commit 0ca1ff65c7
18 files changed
+1692 -24

No files matched your search

+281
View File
@@ -0,0 +1,281 @@
use std::rc::Rc;
use gpui::{
App, Bounds, Corners, DragMoveEvent, ElementId, EmptyView, EntityId, Hsla, InteractiveElement,
IntoElement, MouseButton, ParentElement, Path, PathBuilder, Pixels, Point, RenderOnce,
StatefulInteractiveElement, Styled, Window, canvas, div, fill, point, prelude::*, size,
};
use crate::{
buttons::{Button, ButtonVariant},
layout::seeded::use_seeded,
theme::{ActiveTheme, ControlSize, Radius, TextSize},
};
/// A pen stroke: points from the pad's top left corner.
pub type Stroke = Vec<Point<Pixels>>;
type OnStrokes = Rc<dyn Fn(&[Stroke], &mut Window, &mut App)>;
type Lift = Rc<dyn Fn(Point<Pixels>, &mut Window, &mut App)>;
struct Pen {
owner: EntityId,
}
/// Where the pad sits, and the stroke under the pen.
#[derive(Default)]
struct Drawing {
bounds: Bounds<Pixels>,
stroke: Stroke,
}
impl Drawing {
/// Adds `at` to the stroke under the pen, unless the pen has not moved.
fn reach(&mut self, at: Point<Pixels>) {
let at = local(self.bounds, at);
if self.stroke.last() != Some(&at) {
self.stroke.push(at);
}
}
}
/// `at` from the pad's corner, kept inside the pad.
fn local(bounds: Bounds<Pixels>, at: Point<Pixels>) -> Point<Pixels> {
point(
(at.x - bounds.left())
.max(Pixels::ZERO)
.min(bounds.size.width),
(at.y - bounds.top())
.max(Pixels::ZERO)
.min(bounds.size.height),
)
}
/// A line through `points`, bent through their midpoints so it reads smooth.
fn ink(points: &[Point<Pixels>], origin: Point<Pixels>, width: Pixels) -> Option<Path<Pixels>> {
let [first, .., last] = points else {
return None;
};
let at = |p: Point<Pixels>| point(origin.x + p.x, origin.y + p.y);
let mut path = PathBuilder::stroke(width);
path.move_to(at(*first));
for pair in points.windows(2) {
let middle = point((pair[0].x + pair[1].x) / 2.0, (pair[0].y + pair[1].y) / 2.0);
path.curve_to(at(middle), at(pair[0]));
}
path.line_to(at(*last));
Some(path.build().expect("a stroke is a simple open path"))
}
fn paint(
strokes: &[Stroke],
bounds: Bounds<Pixels>,
width: Pixels,
color: Hsla,
window: &mut Window,
) {
let dot = |at: Point<Pixels>| {
let center = point(bounds.left() + at.x, bounds.top() + at.y);
fill(Bounds::centered_at(center, size(width, width)), color)
.corner_radii(Corners::all(width / 2.0))
};
for stroke in strokes {
if let Some(path) = ink(stroke, bounds.origin, width) {
window.paint_path(path, color);
}
for end in [stroke.first(), stroke.last()].into_iter().flatten() {
window.paint_quad(dot(*end));
}
}
}
/// A pad to sign with the pointer. A stroke lands when the pen lifts; Clear wipes them all.
#[derive(IntoElement)]
pub struct SignaturePad {
id: ElementId,
strokes: Vec<Stroke>,
on_change: Option<OnStrokes>,
}
impl SignaturePad {
pub fn new(id: impl Into<ElementId>, strokes: impl IntoIterator<Item = Stroke>) -> Self {
Self {
id: id.into(),
strokes: strokes.into_iter().collect(),
on_change: None,
}
}
pub fn on_change(
mut self,
handler: impl Fn(&[Stroke], &mut Window, &mut App) + 'static,
) -> Self {
self.on_change = Some(Rc::new(handler));
self
}
}
impl RenderOnce for SignaturePad {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let strokes = use_seeded((self.id.clone(), "strokes"), self.strokes, window, cx);
let drawing =
window.use_keyed_state((self.id.clone(), "drawing"), cx, |_, _| Drawing::default());
let owner = drawing.entity_id();
let set: OnStrokes = {
let (id, strokes, on_change) = (self.id.clone(), strokes.clone(), self.on_change);
Rc::new(move |next, window, cx| {
log::info!("signature pad {id:?}: {} strokes", next.len());
strokes.update(cx, |strokes, cx| {
strokes.value = next.to_vec();
cx.notify();
});
if let Some(on_change) = &on_change {
on_change(next, window, cx);
}
})
};
let lift: Lift = {
let (strokes, drawing, set) = (strokes.clone(), drawing.clone(), set.clone());
Rc::new(move |at, window, cx| {
let stroke = drawing.update(cx, |drawing, _| {
if !drawing.stroke.is_empty() {
drawing.reach(at);
}
std::mem::take(&mut drawing.stroke)
});
if stroke.is_empty() {
return;
}
let mut next = strokes.read(cx).value.clone();
next.push(stroke);
set(&next, window, cx);
})
};
let theme = cx.theme();
let colors = &theme.colors;
let (height, width) = theme.signature();
let width = width.to_pixels(window.rem_size());
let empty = strokes.read(cx).value.is_empty();
let mut shown = strokes.read(cx).value.clone();
shown.push(drawing.read(cx).stroke.clone());
let (measure, press, start, pen) =
(drawing.clone(), drawing.clone(), drawing.clone(), drawing);
let (up, out) = (lift.clone(), lift);
let color = colors.fg;
let pad = div()
.id(self.id.clone())
.relative()
.w_full()
.h(height)
.rounded(theme.radius(Radius::Lg))
.border_1()
.border_color(colors.border_strong)
.bg(colors.surface)
.cursor_crosshair()
.child(
div()
.absolute()
.left_6()
.right_6()
.bottom_8()
.flex()
.items_end()
.gap_2()
.pb_1()
.border_b_1()
.border_dashed()
.border_color(colors.border_strong)
.text_size(theme.text_size(TextSize::Sm))
.text_color(colors.fg_subtle)
.child("×")
.when(empty, |line| line.child("Sign here")),
)
.child(
canvas(
move |bounds, _, cx| {
if measure.read(cx).bounds != bounds {
measure.update(cx, |drawing, _| drawing.bounds = bounds);
}
},
move |bounds, _, window, _| paint(&shown, bounds, width, color, window),
)
.absolute()
.size_full(),
)
.on_mouse_down(MouseButton::Left, move |event, _, cx| {
press.update(cx, |drawing, cx| {
drawing.stroke = vec![local(drawing.bounds, event.position)];
cx.notify();
})
})
.on_drag(Pen { owner }, move |_, _, window, cx| {
let at = window.mouse_position();
start.update(cx, |drawing, cx| {
drawing.reach(at);
cx.notify();
});
cx.new(|_| EmptyView)
})
.on_drag_move(move |event: &DragMoveEvent<Pen>, _, cx| {
if event.drag(cx).owner != owner {
return;
}
pen.update(cx, |drawing, cx| {
drawing.reach(event.event.position);
cx.notify();
})
})
.on_mouse_up(MouseButton::Left, move |event, window, cx| {
up(event.position, window, cx)
})
.on_mouse_up_out(MouseButton::Left, move |event, window, cx| {
out(event.position, window, cx)
});
div()
.flex()
.flex_col()
.items_end()
.gap_1()
.w_full()
.child(pad)
.child(
Button::new((self.id, "clear"), "Clear")
.size(ControlSize::Sm)
.variant(ButtonVariant::Ghost)
.disabled(empty)
.on_click(move |_, window, cx| set(&[], window, cx)),
)
}
}
#[cfg(test)]
mod tests {
use gpui::{Bounds, point, px, size};
use super::{ink, local};
#[test]
fn points_stay_inside_the_pad() {
let bounds = Bounds::new(point(px(10.0), px(20.0)), size(px(100.0), px(50.0)));
assert_eq!(
local(bounds, point(px(30.0), px(40.0))),
point(px(20.0), px(20.0))
);
assert_eq!(
local(bounds, point(px(-5.0), px(90.0))),
point(px(0.0), px(50.0))
);
}
#[test]
fn a_stroke_needs_two_points_for_a_line() {
let origin = point(px(0.0), px(0.0));
assert!(ink(&[point(px(1.0), px(1.0))], origin, px(2.0)).is_none());
let line = [
point(px(1.0), px(1.0)),
point(px(9.0), px(4.0)),
point(px(12.0), px(9.0)),
];
assert!(ink(&line, origin, px(2.0)).is_some());
}
}