Canvas · Graphs and boards: a field hands focus back on Enter and Escape
writing::begin records the owner's focus, the mind map's or the tool layer's, through forms::Editing::returning. The field keeps its words on Enter's release and drops them on Escape, and both hand focus back, so the map's arrows and the board's Backspace work at once after a write.
This commit is contained in:
1 parent
538c3ed93f
commit
31bd483dc5
8 files changed
+81
-12
No files matched your search
@@ -170,6 +170,7 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
|
|||||||
- A ScrubInput shows its number in about 140px beside a short label. In a narrower column a pair hides it, so a long label takes a row of its own (`canvas::TypographyPanel`).
|
- A ScrubInput shows its number in about 140px beside a short label. In a narrower column a pair hides it, so a long label takes a row of its own (`canvas::TypographyPanel`).
|
||||||
- Design panels edit a whole value and hand the owner a changed copy through `canvas::panel::editing`. A swatch that opens a picker is `canvas::panel::color_well`.
|
- Design panels edit a whole value and hand the owner a changed copy through `canvas::panel::editing`. A swatch that opens a picker is `canvas::panel::color_well`.
|
||||||
- A press that stops propagation also stops the ancestors' focus on press. A layer that takes keys focuses itself in its own press handler (`canvas::ToolLayer`, `canvas::MindMap`), and a field over it stops its own presses.
|
- A press that stops propagation also stops the ancestors' focus on press. A layer that takes keys focuses itself in its own press handler (`canvas::ToolLayer`, `canvas::MindMap`), and a field over it stops its own presses.
|
||||||
|
- A field opened over a component hands focus back to that component on Enter and Escape, through `forms::Editing`'s `returning` and `give_back` (`canvas::writing`); a blur keeps the words and leaves focus where it went.
|
||||||
- A focus ring on a box whose children measure themselves goes on an overlay child: a border insets the children and shifts what they measure (`canvas::ToolLayer`).
|
- A focus ring on a box whose children measure themselves goes on an overlay child: a border insets the children and shifts what they measure (`canvas::ToolLayer`).
|
||||||
- A menu of times goes through `mail::times`: each row names its day and hour, a moment comes once (tomorrow is next Monday on a Sunday), and "Pick a time…" opens one dialog that waits for a time ahead of now (`ScheduleSend`, `SnoozePicker`).
|
- A menu of times goes through `mail::times`: each row names its day and hour, a moment comes once (tomorrow is next Monday on a Sunday), and "Pick a time…" opens one dialog that waits for a time ahead of now (`ScheduleSend`, `SnoozePicker`).
|
||||||
- A list whose cursor a field drives marks the cursor only while the field has focus; at rest its first row would look hovered (`mail::LabelPicker`). Its rows are option rows, and `forms::marked_row` leads one with a mark such as a label's hue.
|
- A list whose cursor a field drives marks the cursor only while the field has focus; at rest its first row would look hovered (`mail::LabelPicker`). Its rows are option rows, and `forms::marked_row` leads one with a mark such as a label's hue.
|
||||||
|
|||||||
@@ -379,3 +379,4 @@ Per-component lines live in `tasks/`. Tags there:
|
|||||||
| T32b | 2 | PASS | |
|
| T32b | 2 | PASS | |
|
||||||
| T32c | 1 | PASS | |
|
| T32c | 1 | PASS | |
|
||||||
| T32d | 1 | FAIL | Tab in a mind map's rename field added a topic; the replaced wire, the connector released on its own shape and a deleted shape's links went untested |
|
| T32d | 1 | FAIL | Tab in a mind map's rename field added a topic; the replaced wire, the connector released on its own shape and a deleted shape's links went untested |
|
||||||
|
| T32d | 2 | FAIL | Enter or Escape in a canvas field handed focus nowhere, so the map's and the layer's keys went dead until the next press |
|
||||||
@@ -125,15 +125,27 @@ impl RenderOnce for MindMap {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
let rename = {
|
let rename = {
|
||||||
let (editing, placed, on_rename) =
|
let (editing, placed, on_rename, home) = (
|
||||||
(editing.clone(), placed.clone(), self.on_rename.clone());
|
editing.clone(),
|
||||||
|
placed.clone(),
|
||||||
|
self.on_rename.clone(),
|
||||||
|
focus.clone(),
|
||||||
|
);
|
||||||
move |key: &SharedString, window: &mut Window, cx: &mut App| {
|
move |key: &SharedString, window: &mut Window, cx: &mut App| {
|
||||||
let topic = placed
|
let topic = placed
|
||||||
.iter()
|
.iter()
|
||||||
.find(|topic| topic.key == *key)
|
.find(|topic| topic.key == *key)
|
||||||
.expect("a placed topic");
|
.expect("a placed topic");
|
||||||
let words = topic.text.to_string();
|
let words = topic.text.to_string();
|
||||||
writing::begin(&editing, key.clone(), words, on_rename.clone(), window, cx);
|
writing::begin(
|
||||||
|
&editing,
|
||||||
|
key.clone(),
|
||||||
|
words,
|
||||||
|
on_rename.clone(),
|
||||||
|
home.clone(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let rename = Rc::new(rename);
|
let rename = Rc::new(rename);
|
||||||
|
|||||||
+4
-2
@@ -1,4 +1,4 @@
|
|||||||
use gpui::{App, Entity, SharedString, Window};
|
use gpui::{App, Entity, FocusHandle, SharedString, Window};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
edit::{Tool, hit},
|
edit::{Tool, hit},
|
||||||
@@ -11,7 +11,7 @@ use super::{
|
|||||||
};
|
};
|
||||||
use crate::forms::Editing;
|
use crate::forms::Editing;
|
||||||
|
|
||||||
/// A press on a tool layer: the tool, where it landed in canvas units and in view pixels, how many presses it counts, whether Shift is held, and a handle's side.
|
/// A press on a tool layer: the tool, where it landed in canvas units and in view pixels, how many presses it counts, whether Shift is held, a handle's side, and the layer's focus, which a field hands back.
|
||||||
pub(super) struct Press {
|
pub(super) struct Press {
|
||||||
pub tool: Tool,
|
pub tool: Tool,
|
||||||
pub at: (f32, f32),
|
pub at: (f32, f32),
|
||||||
@@ -19,6 +19,7 @@ pub(super) struct Press {
|
|||||||
pub count: usize,
|
pub count: usize,
|
||||||
pub shift: bool,
|
pub shift: bool,
|
||||||
pub side: f32,
|
pub side: f32,
|
||||||
|
pub back: FocusHandle,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The gesture a press sets going, after what it asks the owner at once: a selection, a dropped text or note, an ended path, or words to rewrite.
|
/// The gesture a press sets going, after what it asks the owner at once: a selection, a dropped text or note, an ended path, or words to rewrite.
|
||||||
@@ -68,6 +69,7 @@ pub(super) fn pressed(
|
|||||||
shape.key.clone(),
|
shape.key.clone(),
|
||||||
words,
|
words,
|
||||||
handlers.on_text.clone(),
|
handlers.on_text.clone(),
|
||||||
|
press.back.clone(),
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -207,3 +207,41 @@ fn a_link_released_on_its_own_shape_links_nothing(cx: &mut TestAppContext) {
|
|||||||
drag((50.0, 50.0), (60.0, 70.0), cx);
|
drag((50.0, 50.0), (60.0, 70.0), cx);
|
||||||
assert!(said(&host, cx).is_empty());
|
assert!(said(&host, cx).is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn enter_and_escape_in_a_topic_hand_the_keys_back_to_the_map(cx: &mut TestAppContext) {
|
||||||
|
let (host, cx) = stage(map, cx);
|
||||||
|
press(276.0, 126.0, 1, cx);
|
||||||
|
tap("f2", cx);
|
||||||
|
cx.simulate_input("Web");
|
||||||
|
tap("enter", cx);
|
||||||
|
tap("down", cx);
|
||||||
|
tap("f2", cx);
|
||||||
|
cx.simulate_input("Gone");
|
||||||
|
tap("escape", cx);
|
||||||
|
tap("down", cx);
|
||||||
|
assert_eq!(
|
||||||
|
said(&host, cx),
|
||||||
|
[
|
||||||
|
"select Some(\"a\")",
|
||||||
|
"rename a Web",
|
||||||
|
"select Some(\"b\")",
|
||||||
|
"select Some(\"b\")",
|
||||||
|
],
|
||||||
|
"the owner keeps a selected, so each Down steps from a"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn after_writing_on_a_shape_its_board_still_takes_backspace(cx: &mut TestAppContext) {
|
||||||
|
let (host, cx) = stage(board, cx);
|
||||||
|
press(250.0, 50.0, 1, cx);
|
||||||
|
press(250.0, 50.0, 2, cx);
|
||||||
|
cx.simulate_input("Idea");
|
||||||
|
tap("enter", cx);
|
||||||
|
tap("backspace", cx);
|
||||||
|
assert_eq!(
|
||||||
|
said(&host, cx),
|
||||||
|
["shapes [\"a\", \"b\"] links 1", "shapes [\"a\"] links 0"]
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -355,6 +355,7 @@ impl RenderOnce for ToolLayer {
|
|||||||
count: event.click_count,
|
count: event.click_count,
|
||||||
shift: event.modifiers.shift,
|
shift: event.modifiers.shift,
|
||||||
side,
|
side,
|
||||||
|
back: held_focus.clone(),
|
||||||
};
|
};
|
||||||
let gesture = pressed_with(
|
let gesture = pressed_with(
|
||||||
&press,
|
&press,
|
||||||
|
|||||||
+19
-5
@@ -1,19 +1,20 @@
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, Entity, InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels,
|
AnyElement, App, Entity, FocusHandle, InteractiveElement, IntoElement, MouseButton,
|
||||||
SharedString, Styled, Window, div,
|
ParentElement, Pixels, SharedString, Styled, Window, div,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{gesture::OnPair, view::Frame};
|
use super::{gesture::OnPair, view::Frame};
|
||||||
use crate::forms::{Editing, Input};
|
use crate::forms::{Editing, Input};
|
||||||
|
|
||||||
/// Opens a field on the words of what `key` names. Enter or leaving hands them to the owner; Escape drops them.
|
/// Opens a field on the words of what `key` names. Enter or leaving hands them to the owner; Escape drops them. Enter and Escape hand focus back to `back`.
|
||||||
pub(super) fn begin(
|
pub(super) fn begin(
|
||||||
editing: &Entity<Editing>,
|
editing: &Entity<Editing>,
|
||||||
key: SharedString,
|
key: SharedString,
|
||||||
words: String,
|
words: String,
|
||||||
on_text: Option<OnPair>,
|
on_text: Option<OnPair>,
|
||||||
|
back: FocusHandle,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) {
|
) {
|
||||||
@@ -29,12 +30,13 @@ pub(super) fn begin(
|
|||||||
));
|
));
|
||||||
});
|
});
|
||||||
Editing::begin(editing, words, window, cx);
|
Editing::begin(editing, words, window, cx);
|
||||||
|
editing.update(cx, |editing, _| editing.returning(back));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The open field over `frame`, in view pixels.
|
/// The open field over `frame`, in view pixels.
|
||||||
pub(super) fn field(editing: &Entity<Editing>, frame: Frame, cx: &App) -> Option<AnyElement> {
|
pub(super) fn field(editing: &Entity<Editing>, frame: Frame, cx: &App) -> Option<AnyElement> {
|
||||||
let field = editing.read(cx).field()?;
|
let field = editing.read(cx).field()?;
|
||||||
let closing = editing.clone();
|
let (closing, keeping) = (editing.clone(), editing.clone());
|
||||||
Some(
|
Some(
|
||||||
div()
|
div()
|
||||||
.absolute()
|
.absolute()
|
||||||
@@ -45,7 +47,19 @@ pub(super) fn field(editing: &Entity<Editing>, frame: Frame, cx: &App) -> Option
|
|||||||
.on_key_down(move |event, window, cx| {
|
.on_key_down(move |event, window, cx| {
|
||||||
if event.keystroke.key == "escape" {
|
if event.keystroke.key == "escape" {
|
||||||
cx.stop_propagation();
|
cx.stop_propagation();
|
||||||
closing.update(cx, |editing, cx| editing.finish(true, window, cx));
|
closing.update(cx, |editing, cx| {
|
||||||
|
editing.finish(true, window, cx);
|
||||||
|
editing.give_back(window);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on_key_up(move |event, window, cx| {
|
||||||
|
if event.keystroke.key == "enter" {
|
||||||
|
cx.stop_propagation();
|
||||||
|
keeping.update(cx, |editing, cx| {
|
||||||
|
editing.finish(false, window, cx);
|
||||||
|
editing.give_back(window);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.child(Input::new(&field))
|
.child(Input::new(&field))
|
||||||
|
|||||||
+2
-2
@@ -54,11 +54,11 @@ impl Editing {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Focus goes back here after Enter or Escape.
|
/// Focus goes back here after Enter or Escape.
|
||||||
fn returning(&mut self, back: FocusHandle) {
|
pub(crate) fn returning(&mut self, back: FocusHandle) {
|
||||||
self.back = Some(back);
|
self.back = Some(back);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn give_back(&mut self, window: &mut Window) {
|
pub(crate) fn give_back(&mut self, window: &mut Window) {
|
||||||
if let Some(back) = self.back.take() {
|
if let Some(back) = self.back.take() {
|
||||||
log::info!("inline edit: focus handed back");
|
log::info!("inline edit: focus handed back");
|
||||||
window.focus(&back);
|
window.focus(&back);
|
||||||
|
|||||||
Reference in new issue
Block a user