diff --git a/crates/moon-ui-components/src/input/input.rs b/crates/moon-ui-components/src/input/input.rs index ac23645..2ebeddd 100644 --- a/crates/moon-ui-components/src/input/input.rs +++ b/crates/moon-ui-components/src/input/input.rs @@ -2,9 +2,9 @@ use std::rc::Rc; use gpui::prelude::FluentBuilder as _; use gpui::{ - AnyElement, App, DefiniteLength, Edges, EdgesRefinement, Entity, Hsla, InteractiveElement as _, - IntoElement, MouseButton, ParentElement as _, Pixels, RenderOnce, StyleRefinement, Styled, - TextAlign, Window, div, px, relative, + AnyElement, App, DefiniteLength, Edges, EdgesRefinement, ElementId, Entity, Hsla, + InteractiveElement as _, IntoElement, MouseButton, ParentElement as _, Pixels, RenderOnce, + StyleRefinement, Styled, TextAlign, Window, div, px, relative, }; use crate::Sizable; @@ -104,6 +104,8 @@ impl MoonInputMetrics { #[derive(IntoElement)] pub struct Input { state: Entity, + /// The caller's name for the root element; `None` keys it by the state entity. + id: Option, style: StyleRefinement, size: Size, prefix: Option, @@ -148,6 +150,7 @@ impl Input { pub fn new(state: &Entity) -> Self { Self { state: state.clone(), + id: None, size: Size::default(), style: StyleRefinement::default(), prefix: None, @@ -166,6 +169,17 @@ impl Input { } } + /// Name the input's root element. + /// + /// Without it the root is keyed by the state entity, a number that differs between launches, + /// so anything that addresses controls by their element path — an inspector, a scripted + /// test — cannot tell one field from another. The caller's id must be unique among the + /// input's siblings: two siblings with one id share their hover and scroll state. + pub fn id(mut self, id: impl Into) -> Self { + self.id = Some(id.into()); + self + } + pub fn prefix(mut self, prefix: impl IntoElement) -> Self { self.prefix = Some(prefix.into_any_element()); self @@ -357,8 +371,11 @@ impl RenderOnce for Input { && state.mode.is_single_line(); let has_suffix = suffix.is_some() || state.loading || self.mask_toggle || show_clear_button; + let root_id = self + .id + .unwrap_or_else(|| ("input", self.state.entity_id()).into()); div() - .id(("input", self.state.entity_id())) + .id(root_id) .flex() .key_context(crate::input::CONTEXT) .track_focus(&state.focus_handle.clone()) diff --git a/crates/moon-ui-components/src/input/number_input.rs b/crates/moon-ui-components/src/input/number_input.rs index 589b3ed..56cc55d 100644 --- a/crates/moon-ui-components/src/input/number_input.rs +++ b/crates/moon-ui-components/src/input/number_input.rs @@ -3,7 +3,9 @@ use std::rc::Rc; use crate::theme::ActiveTheme; use gpui::Corners; use gpui::Window; -use gpui::{AnyElement, App, Context, Edges, Entity, EventEmitter, FocusHandle, Focusable}; +use gpui::{ + AnyElement, App, Context, Edges, ElementId, Entity, EventEmitter, FocusHandle, Focusable, +}; use gpui::{ InteractiveElement, IntoElement, KeyBinding, ParentElement, RenderOnce, SharedString, StyleRefinement, Styled, TextAlign, actions, prelude::FluentBuilder as _, @@ -27,6 +29,8 @@ pub fn init(cx: &mut App) { #[derive(IntoElement)] pub struct NumberInput { state: Entity, + /// The caller's name for the root element; `None` keys it by the state entity. + id: Option, placeholder: SharedString, size: Size, prefix: Option, @@ -41,6 +45,7 @@ impl NumberInput { pub fn new(state: &Entity) -> Self { Self { state: state.clone(), + id: None, size: Size::default(), placeholder: SharedString::default(), prefix: None, @@ -51,6 +56,13 @@ impl NumberInput { } } + /// Name the number input's root element, for the same reason as [`Input::id`]: the default is + /// keyed by the state entity, which an element path cannot tell apart between launches. + pub fn id(mut self, id: impl Into) -> Self { + self.id = Some(id.into()); + self + } + /// Set the placeholder text of the number input. pub fn placeholder(mut self, placeholder: impl Into) -> Self { self.placeholder = placeholder.into(); @@ -292,8 +304,12 @@ impl RenderOnce for NumberInput { }); } + let root_id = self + .id + .clone() + .unwrap_or_else(|| ("number-input", self.state.entity_id()).into()); h_flex() - .id(("number-input", self.state.entity_id())) + .id(root_id) .key_context(CONTEXT) .on_action(window.listener_for(&self.state, InputState::on_action_increment)) .on_action(window.listener_for(&self.state, InputState::on_action_decrement)) diff --git a/crates/moon-ui-components/src/moon/input.rs b/crates/moon-ui-components/src/moon/input.rs index 4fcc93e..290e909 100644 --- a/crates/moon-ui-components/src/moon/input.rs +++ b/crates/moon-ui-components/src/moon/input.rs @@ -251,7 +251,10 @@ impl RenderOnce for MoonInput { MoonInputSize::Normal } }); + // The caller's id names the element as well as the state: kept only as a state key, it left + // every field of a form under the input's own internal ids, indistinguishable by path. let mut input = Input::new(&state) + .id(self.id.clone()) .with_size(size_for(size)) .disabled(self.disabled) .cleanable(self.cleanable) diff --git a/crates/moon-ui-components/src/moon/number_input.rs b/crates/moon-ui-components/src/moon/number_input.rs index 4d3828f..e6246d4 100644 --- a/crates/moon-ui-components/src/moon/number_input.rs +++ b/crates/moon-ui-components/src/moon/number_input.rs @@ -364,7 +364,9 @@ impl RenderOnce for MoonNumberInput { tokens.ui(metrics.pad_x), tokens.ui(4.0), ); + // The caller's id names the element too, as `MoonInput` does: see `NumberInput::id`. let mut input = NumberInput::new(&state) + .id(self.id.clone()) .with_size(base_size_for(tier)) .disabled(self.disabled) .min_w(px(min_w)); diff --git a/docs/component-mirror-baseline.json b/docs/component-mirror-baseline.json index 03ef088..9bb7107 100644 --- a/docs/component-mirror-baseline.json +++ b/docs/component-mirror-baseline.json @@ -437,7 +437,7 @@ "local_paths": [ "input" ], - "local_hash": "52d0cc9943a90f3c", + "local_hash": "4c1232652526f5a8", "local_files": [ { "path": "input/blink_cursor.rs", @@ -511,8 +511,8 @@ }, { "path": "input/input.rs", - "hash": "4ebe2e7550fade39", - "bytes": 21801 + "hash": "3b614fdff94c34a7", + "bytes": 22572 }, { "path": "input/lsp/code_actions.rs", @@ -571,8 +571,8 @@ }, { "path": "input/number_input.rs", - "hash": "9adbb67bd30a203c", - "bytes": 15815 + "hash": "9865642e5e90ed7c", + "bytes": 16385 }, { "path": "input/otp_input.rs", @@ -1512,7 +1512,7 @@ "local_paths": [ "input" ], - "local_hash": "52d0cc9943a90f3c", + "local_hash": "4c1232652526f5a8", "local_files": [ { "path": "input/blink_cursor.rs", @@ -1586,8 +1586,8 @@ }, { "path": "input/input.rs", - "hash": "4ebe2e7550fade39", - "bytes": 21801 + "hash": "3b614fdff94c34a7", + "bytes": 22572 }, { "path": "input/lsp/code_actions.rs", @@ -1646,8 +1646,8 @@ }, { "path": "input/number_input.rs", - "hash": "9adbb67bd30a203c", - "bytes": 15815 + "hash": "9865642e5e90ed7c", + "bytes": 16385 }, { "path": "input/otp_input.rs",