mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-11 16:30:12 -06:00
Merge main into #2050
This commit is contained in:
@@ -260,6 +260,9 @@ fn vue_links_are_a_default_import_with_quoted_bindings() {
|
||||
ex(&mut s, "instance.setProperties", json!({"ids": [i], "properties": {"Title": "Say \"hi\""}}));
|
||||
ex(&mut s, "codeConnect.set", json!({"id": i, "language": "vue", "template": "<Card title=\"{title} {title}\" />"}));
|
||||
assert_eq!(code(&mut s, i, "vue"), "import Card from \"./Card.vue\";\n\n<Card title=\"Say "hi" Say "hi"\" />\n");
|
||||
// Literal text before the first placeholder keeps it inside the attribute value.
|
||||
ex(&mut s, "codeConnect.set", json!({"id": i, "language": "vue", "template": "<Card title=\"Say: {title}\" />"}));
|
||||
assert_eq!(code(&mut s, i, "vue"), "import Card from \"./Card.vue\";\n\n<Card title=\"Say: Say "hi"\" />\n");
|
||||
// Text after a completed tag is text, even when it contains `=`.
|
||||
ex(&mut s, "instance.setProperties", json!({"ids": [i], "properties": {"Title": "Buy"}}));
|
||||
ex(&mut s, "codeConnect.set", json!({"id": i, "language": "vue", "template": "<p>Total = {title}</p>"}));
|
||||
|
||||
@@ -288,41 +288,45 @@ fn pieces(t: &str) -> Vec<Piece<'_>> {
|
||||
checkable.then(|| parse(t).ok()).flatten().unwrap_or_else(|| vec![Piece::Text(t)])
|
||||
}
|
||||
|
||||
/// Whether `s` ends inside an unclosed opening tag (no `>` outside quotes since the last `<`).
|
||||
fn inside_open_tag(s: &str) -> bool {
|
||||
/// Whether `s` ends inside an unclosed opening tag (no `>` outside quotes since the last `<`), and
|
||||
/// the quote (with its byte offset) still open there.
|
||||
fn tag_state(s: &str) -> (bool, Option<(char, usize)>) {
|
||||
let mut in_tag = false;
|
||||
let mut quote = None;
|
||||
let mut chars = s.chars().peekable();
|
||||
while let Some(c) = chars.next() {
|
||||
let mut chars = s.char_indices().peekable();
|
||||
while let Some((i, c)) = chars.next() {
|
||||
match quote {
|
||||
Some(q) => {
|
||||
Some((q, _)) => {
|
||||
if c == q {
|
||||
quote = None;
|
||||
}
|
||||
}
|
||||
None => match c {
|
||||
'<' if !in_tag && chars.peek().is_some_and(|ch| ch.is_ascii_alphabetic() || matches!(*ch, '/' | '!' | '?')) => {
|
||||
'<' if !in_tag && chars.peek().is_some_and(|(_, ch)| ch.is_ascii_alphabetic() || matches!(*ch, '/' | '!' | '?')) => {
|
||||
in_tag = true;
|
||||
}
|
||||
'>' if in_tag => in_tag = false,
|
||||
'"' | '\'' if in_tag => quote = Some(c),
|
||||
'"' | '\'' if in_tag => quote = Some((c, i)),
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
in_tag
|
||||
(in_tag, quote)
|
||||
}
|
||||
|
||||
/// Where a placeholder sits in a Vue template: whether the attribute is bound, and the quote the
|
||||
/// template already opened (if any). `None` when it isn't in an attribute at all.
|
||||
fn vue_attribute(out: &str) -> Option<(bool, Option<char>)> {
|
||||
let end = out.trim_end();
|
||||
if !inside_open_tag(end) {
|
||||
let (in_tag, open) = tag_state(out);
|
||||
if !in_tag {
|
||||
return None;
|
||||
}
|
||||
let (after, quote) = match end.chars().last() {
|
||||
Some(q @ ('"' | '\'')) => (&end[..end.len() - q.len_utf8()], Some(q)),
|
||||
_ => (end, None),
|
||||
// A quote still open, with literal text after it (`title="Buy {x}"`), puts the placeholder in
|
||||
// that attribute's value as much as one directly after the quote does.
|
||||
let (after, quote) = match open {
|
||||
Some((q, at)) => (&out[..at], Some(q)),
|
||||
None => (end, None),
|
||||
};
|
||||
let after = after.trim_end();
|
||||
let eq = after.rfind('=')?;
|
||||
|
||||
@@ -1214,3 +1214,59 @@ fn sidebar_field_text_stays_in_place_on_focus() {
|
||||
assert!((before.center().y - after.center().y).abs() <= 0.5, "numeric={numeric}: before={before:?}, after={after:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// "You can also hover the input field itself and hold down the modifier key: Mac: ⌥ Option
|
||||
/// Windows: Alt" to scrub it (Adjust alignment, rotation, position, and dimensions). Alt is what
|
||||
/// makes the drag a scrub there, not the fine rate: a point moves a step, as on the label. Without
|
||||
/// Alt a drag on the field is no click, so it neither edits nor moves anything.
|
||||
#[test]
|
||||
fn alt_held_over_a_field_scrubs_it() {
|
||||
let (mut u, a, _) = setup();
|
||||
let (at, _) = field(&mut u, "X");
|
||||
let steps = undo_steps(&u);
|
||||
// Alt is held as the pointer arrives over the field, a frame before the press (the field takes
|
||||
// a drag only once it has seen Alt; egui assigns a press by the frame before it).
|
||||
let alt = Modifiers { alt: true, ..Modifiers::NONE };
|
||||
u.frame_with(vec![Event::PointerMoved(at)], alt);
|
||||
drag(&mut u, at, 25.0, alt);
|
||||
assert_eq!(geom(&u, a).0, 35.0, "a step a point: X from 10");
|
||||
assert_eq!(undo_steps(&u), steps + 1, "one undo step for the whole scrub");
|
||||
assert_eq!(field(&mut u, "X").1, "35", "the field shows it and isn't editing");
|
||||
let (at, _) = field(&mut u, "X");
|
||||
drag(&mut u, at, 25.0, Modifiers::NONE);
|
||||
assert_eq!(geom(&u, a).0, 35.0, "without Alt the field doesn't scrub");
|
||||
assert_eq!(undo_steps(&u), steps + 1);
|
||||
// Alt let go before the button still ends the scrub with its undo step.
|
||||
let (at, _) = field(&mut u, "X");
|
||||
let press = |pressed: bool, m: Modifiers, p: Pos2| Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed, modifiers: m };
|
||||
u.frame_with(vec![Event::PointerMoved(at)], alt);
|
||||
u.frame_with(vec![press(true, alt, at)], alt);
|
||||
for k in 1..=10 {
|
||||
u.frame_with(vec![Event::PointerMoved(pos2(at.x + 2.0 * k as f32, at.y))], alt);
|
||||
}
|
||||
let to = pos2(at.x + 20.0, at.y);
|
||||
u.frame_with(vec![Event::PointerMoved(to), press(false, Modifiers::NONE, to)], Modifiers::NONE);
|
||||
u.frame(vec![]);
|
||||
assert_eq!(geom(&u, a).0, 55.0, "twenty more");
|
||||
assert_eq!(undo_steps(&u), steps + 2, "committed when the button went up, without Alt");
|
||||
// Without Alt the field doesn't take the drag at all (a touch swipe over it is the panel's to
|
||||
// scroll): the press is a click of the field's, not a drag.
|
||||
let (at, _) = field(&mut u, "X");
|
||||
let field_id = Id::unique("pos_x").with("field");
|
||||
u.frame_with(vec![Event::PointerMoved(at)], Modifiers::NONE);
|
||||
u.frame_with(vec![press(true, Modifiers::NONE, at)], Modifiers::NONE);
|
||||
u.frame_with(vec![Event::PointerMoved(pos2(at.x + 10.0, at.y))], Modifiers::NONE);
|
||||
assert!(!u.ctx.is_being_dragged(field_id), "a plain press on the field isn't a drag of the field's");
|
||||
u.frame_with(vec![Event::PointerMoved(pos2(at.x + 10.0, at.y)), press(false, Modifiers::NONE, pos2(at.x + 10.0, at.y))], Modifiers::NONE);
|
||||
u.frame(vec![]);
|
||||
assert_eq!(geom(&u, a).0, 55.0, "nothing scrubbed");
|
||||
// With Alt held over it, the field does take the drag.
|
||||
let (at, _) = field(&mut u, "X");
|
||||
u.frame_with(vec![Event::PointerMoved(at)], alt);
|
||||
u.frame_with(vec![press(true, alt, at)], alt);
|
||||
u.frame_with(vec![Event::PointerMoved(pos2(at.x + 10.0, at.y))], alt);
|
||||
assert!(u.ctx.is_being_dragged(field_id), "with Alt the drag is the field's");
|
||||
u.frame_with(vec![Event::PointerMoved(pos2(at.x + 10.0, at.y)), press(false, alt, pos2(at.x + 10.0, at.y))], alt);
|
||||
u.frame(vec![]);
|
||||
assert_eq!(geom(&u, a).0, 65.0, "ten more");
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ pub(super) fn spacing_input(ui: &mut Ui, field: &SpacingField, value: &str, widt
|
||||
ui.ctx().set_cursor_icon(egui::CursorIcon::ResizeHorizontal);
|
||||
}
|
||||
let numeric = value.trim_end_matches('%').parse::<f64>().ok();
|
||||
match widgets::scrub(ui, field.id, &response, numeric, 1.0) {
|
||||
match widgets::scrub(ui, field.id, &response, numeric, 1.0, true) {
|
||||
Some(FieldEdit::Scrub(v, done)) => Some(SpacingEdit::Scrub(v, done)),
|
||||
_ => typed.map(SpacingEdit::Typed),
|
||||
}
|
||||
|
||||
@@ -132,8 +132,9 @@ pub fn scrubbed(from: f64, amount: f64, quantum: f64) -> f64 {
|
||||
}
|
||||
|
||||
/// The shared label-scrub gesture, including changing modifier rates and raw edge motion.
|
||||
/// Callers supply the hit area and apply live values until the final `Scrub(_, true)`.
|
||||
pub(crate) fn scrub(ui: &Ui, id: Id, response: &Response, value: Option<f64>, step: f64) -> Option<FieldEdit> {
|
||||
/// Callers supply the hit area and apply live values until the final `Scrub(_, true)`. `fine_on_alt`:
|
||||
/// Alt gives tenths (off where Alt is what makes the drag a scrub at all).
|
||||
pub(crate) fn scrub(ui: &Ui, id: Id, response: &Response, value: Option<f64>, step: f64, fine_on_alt: bool) -> Option<FieldEdit> {
|
||||
let v = value?;
|
||||
// Where the scrub began and how far it has taken the value (the modifiers can change on
|
||||
// the way: each stretch counts at its own rate).
|
||||
@@ -150,7 +151,7 @@ pub(crate) fn scrub(ui: &Ui, id: Id, response: &Response, value: Option<f64>, st
|
||||
// does too. Elsewhere the scrub just stops there.
|
||||
let raw = p.motion().map_or(0.0, |m| m.x / i.pixels_per_point);
|
||||
let at_edge = moved == 0.0 && p.hover_pos().is_none_or(|at| at.x <= 1.0 || at.x >= i.content_rect().max.x - 1.0);
|
||||
(f64::from(if at_edge { raw } else { moved }), i.modifiers.shift, i.modifiers.alt)
|
||||
(f64::from(if at_edge { raw } else { moved }), i.modifiers.shift, i.modifiers.alt && fine_on_alt)
|
||||
});
|
||||
// The frame the button goes up moves nothing: the scrub ends where the last one left it.
|
||||
if response.dragged() {
|
||||
@@ -265,7 +266,20 @@ fn number_field_impl(
|
||||
let edit_rect = Rect::from_min_max(pos2(rect.min.x + lead_w, rect.min.y), rect.max);
|
||||
let editing_id = id.with("editing");
|
||||
let mut editing: Option<String> = ui.data(|d| d.get_temp(editing_id));
|
||||
let field_resp = ui.interact(edit_rect, id.with("field"), Sense::click());
|
||||
// Alt held over the field scrubs it too, as the label always does: "hover the input field
|
||||
// itself and hold down the modifier key" (Adjust alignment, rotation, position, and
|
||||
// dimensions). The field senses drags only while Alt is held, or while a scrub begun that way
|
||||
// goes on (Alt released or not), so a plain press or a touch swipe on it stays the scroll
|
||||
// area's; egui assigns a press by the frame before it, so Alt is held over the field before
|
||||
// the press, as a hand does. Alt as the drag begins is what makes it a scrub.
|
||||
let field_id = id.with("field");
|
||||
let alt_now = ui.input(|i| i.modifiers.alt);
|
||||
let scrubbing_id = id.with("altscrub");
|
||||
let scrubbing = ui.data(|d| d.get_temp::<bool>(scrubbing_id)).unwrap_or(false);
|
||||
let field_sense = if editing.is_none() && (alt_now || scrubbing) { Sense::click_and_drag() } else { Sense::click() };
|
||||
let field_resp = ui.interact(edit_rect, field_id, field_sense);
|
||||
let alt_scrub = editing.is_none() && if field_resp.drag_started() { alt_now } else { scrubbing };
|
||||
ui.data_mut(|d| d.insert_temp(scrubbing_id, alt_scrub && field_resp.dragged() && !field_resp.drag_stopped()));
|
||||
a11y::number(&field_resp, lead.name(), value, suffix, empty.text());
|
||||
a11y::ring_around(ui, rect, &field_resp);
|
||||
// The label scrubs with the pointer; the keyboard stops at the value only.
|
||||
@@ -291,10 +305,13 @@ fn number_field_impl(
|
||||
}
|
||||
Lead::None | Lead::Named(_) => {}
|
||||
}
|
||||
if lead_resp.hovered() || lead_resp.dragged() {
|
||||
if lead_resp.hovered() || lead_resp.dragged() || (alt_now && editing.is_none() && field_resp.hovered()) || (alt_scrub && field_resp.dragged()) {
|
||||
ui.ctx().set_cursor_icon(egui::CursorIcon::ResizeHorizontal);
|
||||
}
|
||||
if let Some(edit) = scrub(ui, id, &lead_resp, value, step) {
|
||||
// On the field, Alt is what makes the drag a scrub, so it is not the fine rate there.
|
||||
if let Some(edit) =
|
||||
scrub(ui, id, &lead_resp, value, step, true).or_else(|| alt_scrub.then(|| scrub(ui, id, &field_resp, value, step, false)).flatten())
|
||||
{
|
||||
out = Some(Edit::All(edit));
|
||||
}
|
||||
let (key_step, key_big) = key_steps(ui.ctx(), suffix, step);
|
||||
@@ -340,7 +357,7 @@ fn number_field_impl(
|
||||
}
|
||||
// Typing on a field the keyboard reached (Tab) edits it: the editor takes the typed text.
|
||||
let typed_now = editing.is_none() && on_field && ui.input(|i| i.events.iter().any(|e| matches!(e, egui::Event::Text(_))));
|
||||
let started = editing.is_none() && (field_resp.clicked() || typed_now);
|
||||
let started = editing.is_none() && ((field_resp.clicked() && !alt_now) || typed_now);
|
||||
// The text the field would commit to no effect: what it showed when the edit began, or the
|
||||
// value the arrows last stepped it to.
|
||||
let kept_id = id.with("kept");
|
||||
|
||||
+3
-2
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user