refactor(editor): centralize text input blink scheduling
This commit is contained in:
parent
a1918303de
commit
ea22bad6f6
|
|
@ -69,6 +69,7 @@ pub mod svg_import;
|
|||
pub mod svg_path_bounds;
|
||||
mod svg_path_data;
|
||||
pub mod text_edit;
|
||||
pub mod text_input_focus;
|
||||
pub mod theme_presets;
|
||||
pub mod tool;
|
||||
pub mod toolbar_state;
|
||||
|
|
|
|||
|
|
@ -294,6 +294,33 @@ mod tests {
|
|||
assert_eq!(a.tool, b.tool);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn active_text_input_prefers_canvas_text_edit_over_other_focus() {
|
||||
let mut s = EditorState::new();
|
||||
s.ui.property_focus = Some(crate::ui_draft::PropertyFocus::PositionX);
|
||||
s.ui.property_input.set_text("property");
|
||||
s.chat.focused = true;
|
||||
s.chat.input.set_text("chat");
|
||||
s.ui.text_editing = Some(crate::NodeId::new("text"));
|
||||
s.ui.text_edit_input.set_text("canvas");
|
||||
|
||||
assert_eq!(
|
||||
s.active_text_input().map(|input| input.text()),
|
||||
Some("canvas")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn active_text_input_mut_updates_the_focused_variable_row_input() {
|
||||
let mut s = EditorState::new();
|
||||
s.editor_ui.variable_row_focus = Some(crate::editor_ui_state::VariableRowFocus::String(0));
|
||||
s.editor_ui.variable_row_input.set_text("row");
|
||||
|
||||
s.active_text_input_mut().unwrap().insert_str("!", 0);
|
||||
|
||||
assert_eq!(s.editor_ui.variable_row_input.text(), "row!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replace_document_swaps_doc_but_preserves_editor_chrome() {
|
||||
let mut s = EditorState::new();
|
||||
|
|
|
|||
126
crates/op-editor-core/src/text_input_focus.rs
Normal file
126
crates/op-editor-core/src/text_input_focus.rs
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
//! Focused text-input resolver shared by hosts.
|
||||
//!
|
||||
//! The priority order mirrors the native host's historical caret
|
||||
//! wake-up branches. It is intentionally explicit: when multiple
|
||||
//! stale focus flags coexist, the first active site wins.
|
||||
|
||||
use crate::editor_ui_state::CloneField;
|
||||
use crate::state::EditorState;
|
||||
use jian_core::text_input::TextInputState;
|
||||
|
||||
impl EditorState {
|
||||
pub fn active_text_input(&self) -> Option<&TextInputState> {
|
||||
if self.ui.text_editing.is_some() {
|
||||
return Some(&self.ui.text_edit_input);
|
||||
}
|
||||
if let Some(rename) = &self.ui.layer_rename {
|
||||
return Some(&rename.input);
|
||||
}
|
||||
if self.ui.property_focus.is_some() {
|
||||
return Some(&self.ui.property_input);
|
||||
}
|
||||
if self.editor_ui.variables_theme_rename_axis.is_some()
|
||||
|| self.editor_ui.variables_variant_rename_value.is_some()
|
||||
{
|
||||
return Some(&self.editor_ui.variables_header_input);
|
||||
}
|
||||
if self.editor_ui.variable_row_focus.is_some() {
|
||||
return Some(&self.editor_ui.variable_row_input);
|
||||
}
|
||||
if self.editor_ui.agent_settings_open && self.editor_ui.agent_settings.focus.is_some() {
|
||||
return Some(&self.editor_ui.settings_input);
|
||||
}
|
||||
if self.editor_ui.chat_model_picker_open {
|
||||
return Some(&self.editor_ui.chat_model_picker_input);
|
||||
}
|
||||
if self.chat.focused {
|
||||
return Some(&self.chat.input);
|
||||
}
|
||||
|
||||
let git = &self.editor_ui.git_panel;
|
||||
if git.commit_focused {
|
||||
return Some(&git.commit_input);
|
||||
}
|
||||
if git.remote_focused {
|
||||
return Some(&git.remote_input);
|
||||
}
|
||||
if git.https_focused {
|
||||
return Some(&git.https_input);
|
||||
}
|
||||
if git.branch_create_focused {
|
||||
return Some(&git.branch_create_input);
|
||||
}
|
||||
if git.author_name_focused {
|
||||
return Some(&git.author_name_input);
|
||||
}
|
||||
if git.author_email_focused {
|
||||
return Some(&git.author_email_input);
|
||||
}
|
||||
if let Some(form) = &git.clone_form {
|
||||
return match form.focus {
|
||||
Some(CloneField::Url) => Some(&form.url_input),
|
||||
Some(CloneField::Dest) => Some(&form.dest_input),
|
||||
None => None,
|
||||
};
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn active_text_input_mut(&mut self) -> Option<&mut TextInputState> {
|
||||
if self.ui.text_editing.is_some() {
|
||||
return Some(&mut self.ui.text_edit_input);
|
||||
}
|
||||
if let Some(rename) = &mut self.ui.layer_rename {
|
||||
return Some(&mut rename.input);
|
||||
}
|
||||
if self.ui.property_focus.is_some() {
|
||||
return Some(&mut self.ui.property_input);
|
||||
}
|
||||
|
||||
let variables_header_active = self.editor_ui.variables_theme_rename_axis.is_some()
|
||||
|| self.editor_ui.variables_variant_rename_value.is_some();
|
||||
if variables_header_active {
|
||||
return Some(&mut self.editor_ui.variables_header_input);
|
||||
}
|
||||
if self.editor_ui.variable_row_focus.is_some() {
|
||||
return Some(&mut self.editor_ui.variable_row_input);
|
||||
}
|
||||
if self.editor_ui.agent_settings_open && self.editor_ui.agent_settings.focus.is_some() {
|
||||
return Some(&mut self.editor_ui.settings_input);
|
||||
}
|
||||
if self.editor_ui.chat_model_picker_open {
|
||||
return Some(&mut self.editor_ui.chat_model_picker_input);
|
||||
}
|
||||
if self.chat.focused {
|
||||
return Some(&mut self.chat.input);
|
||||
}
|
||||
|
||||
let git = &mut self.editor_ui.git_panel;
|
||||
if git.commit_focused {
|
||||
return Some(&mut git.commit_input);
|
||||
}
|
||||
if git.remote_focused {
|
||||
return Some(&mut git.remote_input);
|
||||
}
|
||||
if git.https_focused {
|
||||
return Some(&mut git.https_input);
|
||||
}
|
||||
if git.branch_create_focused {
|
||||
return Some(&mut git.branch_create_input);
|
||||
}
|
||||
if git.author_name_focused {
|
||||
return Some(&mut git.author_name_input);
|
||||
}
|
||||
if git.author_email_focused {
|
||||
return Some(&mut git.author_email_input);
|
||||
}
|
||||
if let Some(form) = &mut git.clone_form {
|
||||
return match form.focus {
|
||||
Some(CloneField::Url) => Some(&mut form.url_input),
|
||||
Some(CloneField::Dest) => Some(&mut form.dest_input),
|
||||
None => None,
|
||||
};
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
|
@ -116,11 +116,7 @@ pub struct PropertyPanel {
|
|||
pub caret_pos: usize,
|
||||
/// Whether Ctrl/Cmd+A selected the full focused draft.
|
||||
pub select_all: bool,
|
||||
/// Caret-blink anchor (ms since host start) for the focused
|
||||
/// input. Drives the same `jian_core::anim::blink_visible`
|
||||
/// helper the chat caret uses.
|
||||
pub caret_anchor_ms: u64,
|
||||
/// Host clock ms; paired with `caret_anchor_ms` for caret blink.
|
||||
/// Host clock ms for caret blink.
|
||||
pub now_ms: u64,
|
||||
/// Active flex-layout button.
|
||||
pub flex_layout: op_editor_core::FlexLayout,
|
||||
|
|
@ -371,7 +367,6 @@ impl PropertyPanel {
|
|||
state.ui.property_input.caret()
|
||||
},
|
||||
select_all: !is_multi && state.ui.property_input.is_select_all(),
|
||||
caret_anchor_ms: state.ui.property_caret_anchor_ms,
|
||||
now_ms,
|
||||
flex_layout,
|
||||
size_flags,
|
||||
|
|
@ -810,7 +805,6 @@ impl Widget for PropertyPanel {
|
|||
input: &self.input,
|
||||
caret: self.caret_pos,
|
||||
select_all: self.select_all,
|
||||
caret_anchor_ms: self.caret_anchor_ms,
|
||||
now_ms: self.now_ms,
|
||||
};
|
||||
let caps = self.capabilities();
|
||||
|
|
|
|||
|
|
@ -52,7 +52,6 @@ pub struct EditContext<'a> {
|
|||
pub caret: usize,
|
||||
/// Whether Ctrl/Cmd+A selected the focused draft.
|
||||
pub select_all: bool,
|
||||
pub caret_anchor_ms: u64,
|
||||
pub now_ms: u64,
|
||||
}
|
||||
|
||||
|
|
@ -156,17 +155,15 @@ impl<'a> EditContext<'a> {
|
|||
/// for editable surfaces (effect params) that don't key off a
|
||||
/// `PropertyFocus`.
|
||||
pub fn caret_blink_on(&self) -> bool {
|
||||
jian_core::anim::blink_visible(self.now_ms, self.caret_anchor_ms, 500)
|
||||
self.input.caret_visible(self.now_ms)
|
||||
}
|
||||
|
||||
/// Caret byte-offset for `focus` when it is the focused field
|
||||
/// and the blink is on — `None` otherwise. Drives caret paint;
|
||||
/// the offset is clamped into the draft so a stale value is safe.
|
||||
pub fn caret_at(&self, focus: PropertyFocus) -> Option<usize> {
|
||||
if self.focus == Some(focus)
|
||||
&& jian_core::anim::blink_visible(self.now_ms, self.caret_anchor_ms, 500)
|
||||
{
|
||||
Some(self.caret.min(self.draft.len()))
|
||||
if self.focus == Some(focus) && self.input.caret_visible(self.now_ms) {
|
||||
Some(self.input.caret().min(self.draft.len()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
|
|
|||
|
|
@ -141,6 +141,7 @@ pub struct VariablesPanel {
|
|||
add_menu_open: bool,
|
||||
search: String,
|
||||
search_focus: bool,
|
||||
search_input: jian_core::text_input::TextInputState,
|
||||
scroll: f32,
|
||||
/// Open `⋯` row menu, keyed by UNFILTERED row index.
|
||||
row_menu_open: Option<usize>,
|
||||
|
|
@ -149,7 +150,6 @@ pub struct VariablesPanel {
|
|||
editing_value_cell: Option<(usize, usize)>,
|
||||
header_input: jian_core::text_input::TextInputState,
|
||||
row_input: jian_core::text_input::TextInputState,
|
||||
caret_anchor_ms: u64,
|
||||
now_ms: u64,
|
||||
}
|
||||
|
||||
|
|
@ -228,6 +228,11 @@ impl VariablesPanel {
|
|||
.cloned()
|
||||
})
|
||||
.or_else(|| themes.first().map(|(axis, _)| axis.clone()));
|
||||
let mut search_input = jian_core::text_input::TextInputState::with_text(
|
||||
state.editor_ui.variables_search.clone(),
|
||||
);
|
||||
search_input.touch(state.ui.property_caret_anchor_ms);
|
||||
|
||||
Self {
|
||||
rows,
|
||||
total_rows,
|
||||
|
|
@ -245,6 +250,7 @@ impl VariablesPanel {
|
|||
add_menu_open: state.editor_ui.variables_add_menu_open,
|
||||
search: state.editor_ui.variables_search.clone(),
|
||||
search_focus: state.editor_ui.variables_search_focus,
|
||||
search_input,
|
||||
scroll: state.editor_ui.variables_scroll,
|
||||
row_menu_open: state.editor_ui.variables_row_menu,
|
||||
hover: state.editor_ui.variables_panel_hover,
|
||||
|
|
@ -265,7 +271,6 @@ impl VariablesPanel {
|
|||
}),
|
||||
header_input: state.editor_ui.variables_header_input.clone(),
|
||||
row_input: state.editor_ui.variable_row_input.clone(),
|
||||
caret_anchor_ms: state.ui.property_caret_anchor_ms,
|
||||
now_ms,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,9 +87,7 @@ fn paint_search_row(
|
|||
baseline_y,
|
||||
);
|
||||
}
|
||||
if panel.search_focus
|
||||
&& jian_core::anim::blink_visible(panel.now_ms, panel.caret_anchor_ms, 500)
|
||||
{
|
||||
if panel.search_focus && panel.search_input.caret_visible(panel.now_ms) {
|
||||
let caret_x = text_x
|
||||
+ if panel.search.is_empty() {
|
||||
0.0
|
||||
|
|
|
|||
|
|
@ -282,8 +282,8 @@ pub struct WidgetHostNative {
|
|||
/// Bumped past the highest sample id so new + sample nodes
|
||||
/// never collide on the same key.
|
||||
pub(in crate::widget_host) next_node_id: u64,
|
||||
/// Host-supplied frame timestamp in milliseconds. Drives the
|
||||
/// caret blink via `jian_core::anim::blink_visible`. The
|
||||
/// Host-supplied frame timestamp in milliseconds. Focused
|
||||
/// `TextInputState`s use this for caret blink. The
|
||||
/// inspector_window runner refreshes this once per
|
||||
/// `RedrawRequested` from a single `Instant` start anchor;
|
||||
/// any other host (mobile / browser) installs its own clock.
|
||||
|
|
@ -864,103 +864,14 @@ impl WidgetHostNative {
|
|||
/// Next millisecond at which the host should wake to repaint
|
||||
/// the caret blink phase. `None` = no animation pending.
|
||||
pub fn next_animation_deadline_ms(&self) -> Option<u64> {
|
||||
let ui = &self.editor_state.ui;
|
||||
if ui.text_editing.is_some() {
|
||||
return Some(ui.text_edit_input.next_blink_flip_ms(self.now_ms));
|
||||
if let Some(input) = self.editor_state.active_text_input() {
|
||||
return Some(input.next_blink_flip_ms(self.now_ms));
|
||||
}
|
||||
if let Some(rename) = &ui.layer_rename {
|
||||
return Some(rename.input.next_blink_flip_ms(self.now_ms));
|
||||
}
|
||||
if ui.property_focus.is_some() {
|
||||
return Some(jian_core::anim::next_blink_flip_ms(
|
||||
self.now_ms,
|
||||
ui.property_caret_anchor_ms,
|
||||
500,
|
||||
));
|
||||
}
|
||||
if self
|
||||
.editor_state
|
||||
.editor_ui
|
||||
.variables_theme_rename_axis
|
||||
.is_some()
|
||||
|| self
|
||||
.editor_state
|
||||
.editor_ui
|
||||
.variables_variant_rename_value
|
||||
.is_some()
|
||||
|| self.editor_state.editor_ui.variable_row_focus.is_some()
|
||||
{
|
||||
return Some(jian_core::anim::next_blink_flip_ms(
|
||||
self.now_ms,
|
||||
ui.property_caret_anchor_ms,
|
||||
500,
|
||||
));
|
||||
}
|
||||
if self.editor_state.editor_ui.agent_settings_open
|
||||
&& self.editor_state.editor_ui.agent_settings.focus.is_some()
|
||||
{
|
||||
return Some(
|
||||
self.editor_state
|
||||
.editor_ui
|
||||
.settings_input
|
||||
.next_blink_flip_ms(self.now_ms),
|
||||
);
|
||||
}
|
||||
if self.editor_state.editor_ui.chat_model_picker_open {
|
||||
return Some(
|
||||
self.editor_state
|
||||
.editor_ui
|
||||
.chat_model_picker_input
|
||||
.next_blink_flip_ms(self.now_ms),
|
||||
);
|
||||
}
|
||||
if self.editor_state.chat.focused {
|
||||
return Some(self.editor_state.chat.input.next_blink_flip_ms(self.now_ms));
|
||||
}
|
||||
// Git commit textarea caret — same 500 ms cadence the ready
|
||||
// panel paints at (`git_panel_ready.rs`). Without this wake the
|
||||
// window never repaints while the commit box is focused, so the
|
||||
// caret sits static instead of blinking.
|
||||
if self.editor_state.editor_ui.git_panel.commit_focused {
|
||||
return Some(
|
||||
self.editor_state
|
||||
.editor_ui
|
||||
.git_panel
|
||||
.commit_input
|
||||
.next_blink_flip_ms(self.now_ms),
|
||||
);
|
||||
}
|
||||
let git = &self.editor_state.editor_ui.git_panel;
|
||||
if git.remote_focused {
|
||||
return Some(git.remote_input.next_blink_flip_ms(self.now_ms));
|
||||
}
|
||||
if git.https_focused {
|
||||
return Some(git.https_input.next_blink_flip_ms(self.now_ms));
|
||||
}
|
||||
if git.branch_create_focused {
|
||||
return Some(git.branch_create_input.next_blink_flip_ms(self.now_ms));
|
||||
}
|
||||
if git.author_name_focused {
|
||||
return Some(git.author_name_input.next_blink_flip_ms(self.now_ms));
|
||||
}
|
||||
if git.author_email_focused {
|
||||
return Some(git.author_email_input.next_blink_flip_ms(self.now_ms));
|
||||
}
|
||||
// Clone-wizard field caret, and while a `git clone` runs — keep
|
||||
// the loop ticking so the caret blinks and `poll_git_clone_job`
|
||||
// drains the worker's result on a later frame.
|
||||
// While a `git clone` runs, keep the loop ticking so
|
||||
// `poll_git_clone_job` drains the worker's result later.
|
||||
if let Some(form) = &self.editor_state.editor_ui.git_panel.clone_form {
|
||||
match form.focus {
|
||||
Some(op_editor_core::CloneField::Url) => {
|
||||
return Some(form.url_input.next_blink_flip_ms(self.now_ms));
|
||||
}
|
||||
Some(op_editor_core::CloneField::Dest) => {
|
||||
return Some(form.dest_input.next_blink_flip_ms(self.now_ms));
|
||||
}
|
||||
None if form.cloning => {
|
||||
return Some(self.now_ms + jian_core::text_input::CARET_BLINK_PERIOD_MS);
|
||||
}
|
||||
None => {}
|
||||
if form.cloning {
|
||||
return Some(self.now_ms.saturating_add(100));
|
||||
}
|
||||
}
|
||||
None
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
use super::WidgetHostNative;
|
||||
use op_editor_core::agent_settings::{BuiltinAgentField, SettingsFocus};
|
||||
use op_editor_core::editor_ui_state::VariableRowFocus;
|
||||
|
||||
#[test]
|
||||
fn settings_input_uses_text_input_state_for_editing() {
|
||||
|
|
@ -28,3 +29,23 @@ fn settings_input_uses_text_input_state_for_editing() {
|
|||
assert_eq!(host.editor_state().editor_ui.settings_input.text(), "Z");
|
||||
assert_eq!(host.editor_state().editor_ui.settings_input.caret(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn next_animation_deadline_uses_focused_variable_row_input_anchor() {
|
||||
let mut host = WidgetHostNative::new();
|
||||
host.set_now_ms(1_300);
|
||||
{
|
||||
let state = host.editor_state_mut();
|
||||
state.editor_ui.variable_row_focus = Some(VariableRowFocus::String(0));
|
||||
state.editor_ui.variable_row_input.touch(1_250);
|
||||
state.ui.property_caret_anchor_ms = 0;
|
||||
}
|
||||
|
||||
let expected = host
|
||||
.editor_state()
|
||||
.editor_ui
|
||||
.variable_row_input
|
||||
.next_blink_flip_ms(1_300);
|
||||
|
||||
assert_eq!(host.next_animation_deadline_ms(), Some(expected));
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue