diff --git a/crates/buiy/src/lib.rs b/crates/buiy/src/lib.rs index 6cfd418..07813f0 100644 --- a/crates/buiy/src/lib.rs +++ b/crates/buiy/src/lib.rs @@ -39,7 +39,7 @@ pub use buiy_core::{ }, theme::{Theme, UserPreferences, default_light_theme}, }; -pub use buiy_widgets::{Button, OnPress, WidgetsPlugin}; +pub use buiy_widgets::{Button, OnPress, TextInput, WidgetsPlugin}; // `buiy_core::render::ExtractedDraws` is intentionally NOT re-exported at // the crate root: it is a render-world resource only, populated during the diff --git a/crates/buiy_core/src/text/edit/input.rs b/crates/buiy_core/src/text/edit/input.rs index 3c25fcd..a63839b 100644 --- a/crates/buiy_core/src/text/edit/input.rs +++ b/crates/buiy_core/src/text/edit/input.rs @@ -21,6 +21,14 @@ use super::undo::{GroupKind, UndoUnit}; #[derive(Message, Debug, Clone, Copy, PartialEq, Eq)] pub struct TextChanged(pub Entity); +/// Emitted when a single-line editor is submitted (editing-and-ime § 11 row +/// `EditSubmitted`, § 3.3). Born from `EditCommand::Submit` — the focused +/// single-line Enter. Payload: the entity (the value is read via the +/// component, per the § 11 contract). This FINALIZES the § 11 taxonomy +/// (the host-facing surface of E2's internal `EditOutcome.submitted` flag). +#[derive(Message, Debug, Clone, Copy, PartialEq, Eq)] +pub struct EditSubmitted(pub Entity); + /// What one `apply` did, so the system can emit the right Messages. `value` /// changes drive `TextChanged`; `submitted` drives the internal Submit path /// (E6 turns it into the host-facing `EditSubmitted`). @@ -461,6 +469,7 @@ pub fn apply_keyboard_edits( mut changed: MessageWriter, mut undone: MessageWriter, mut redone: MessageWriter, + mut submitted: MessageWriter, ) { // No input infrastructure (no `InputPlugin` and no manual seed) ⇒ the // `KeyboardInput` Message and the `ButtonInput` resource are @@ -544,6 +553,7 @@ pub fn apply_keyboard_edits( let mut font_system = fonts.lock(); let mut any_value_change = false; let mut any_reshape = false; + let mut any_submit = false; for command in commands { // Capture the group BEFORE applying, for the undo/redo Messages. let was_undo = command == EditCommand::Undo; @@ -562,6 +572,7 @@ pub fn apply_keyboard_edits( let outcome = state.apply_tracked(&mut font_system, command, &mut ctx); any_value_change |= outcome.value_changed; any_reshape |= outcome.reshaped; + any_submit |= outcome.submitted; if was_undo && outcome.value_changed && let Some(g) = group_before_undo @@ -597,4 +608,7 @@ pub fn apply_keyboard_edits( if any_value_change { changed.write(TextChanged(entity)); } + if any_submit { + submitted.write(EditSubmitted(entity)); + } } diff --git a/crates/buiy_core/src/text/edit/lifecycle.rs b/crates/buiy_core/src/text/edit/lifecycle.rs new file mode 100644 index 0000000..a53ba67 --- /dev/null +++ b/crates/buiy_core/src/text/edit/lifecycle.rs @@ -0,0 +1,100 @@ +//! E6 — focus lifecycle (editing-and-ime § 10). One render-prep system, +//! `focus_lifecycle`, detects the `FocusedEntity` transition (gain / loss) +//! via a `Local>` previous-value compare (no transition +//! detector existed before E6) and runs the spec § 10 edges: +//! +//! - on GAIN of a non-`Disabled` editor: the blink phase resets (the user +//! just acted — the focused caret is solid-on for one half-period, web +//! parity); +//! - on LOSS of an editor: the open undo group seals (`seal()`), and any live +//! preedit is removed (wiring E5's deferred focus-loss removal — E5's +//! `apply_ime` only removes on `Ime::Disabled`, never on a bare focus +//! change). The **selection / buffer is RETAINED** (we never touch +//! `SelectionVisual` — re-focus restores it, web parity). +//! +//! **Caret visibility is NOT touched here (M1).** `write_caret_blink` is the +//! single focus-aware owner of `CaretVisual.visible` (it forces a non-focused +//! editor caret hidden and blinks the focused one). `focus_lifecycle` only +//! resets the blink ORIGIN on gain. +//! +//! `ime_enabled` is also NOT handled here: E5's `write_ime_window` already +//! decides it from focus + markers alone every frame (`ime.rs` enable_q). +//! +//! **The M1 dirty-mark.** `remove_preedit` does direct BufferLine surgery and +//! does NOT invalidate intrinsics or Taffy-dirty the node (verified `ime.rs` — +//! the removal there relies on `apply_ime`'s own dirty-mark). On a bare focus +//! change there is no `apply_ime` pass, so `focus_lifecycle` MUST do the same +//! dirty-mark itself after removing the preedit, or the orphaned preedit glyphs +//! persist a frame: `invalidate_intrinsics()` + +//! `tree.mark_dirty_for_entity(entity)` (the `apply_keyboard_edits` / +//! `apply_ime` seam). +//! +//! It names only the pure-data cosmic types via the facade accessors +//! (`remove_preedit` locks the `SharedFontSystem`; `seal` names none), so it +//! stays inside the `text::edit` facade. + +use bevy::prelude::*; + +use super::state::{Disabled, TextEditState}; +use crate::FocusedEntity; +use crate::layout::LayoutTree; +use crate::text::SharedFontSystem; + +/// Render-prep: react to focus gain / loss for editor entities (§ 10). Runs in +/// the `.after(BuiySet::Input).before(write_caret_blink)` window alongside the +/// E3 caret writer and E5 IME window writer. +/// +/// `Local>` holds last frame's focused entity — the canonical +/// transition detector E6 introduces (the codebase had none). Option params +/// (`focused`, `tree`) follow the inert-harness discipline — a bare +/// `BuiyTextPlugin` without `FocusPlugin` / `LayoutPlugin` no-ops instead of +/// panicking at param validation (the `apply_ime` precedent). +#[allow(clippy::type_complexity)] +pub fn focus_lifecycle( + time: Res