Skip to main content

oxiui_core/
response.rs

1//! Response structs returned by the extended [`UiCtx`](crate::UiCtx) widgets.
2//!
3//! ## The `supported` contract
4//!
5//! Every extended widget method on [`UiCtx`](crate::UiCtx) (`text_input`,
6//! `checkbox`, `slider`, …) ships with a **default implementation that returns
7//! a response whose `supported` field is `false`**. An adapter that has not
8//! overridden a given widget therefore reports `supported == false` to the
9//! caller, instead of silently drawing nothing and lying about success. Callers
10//! can branch on `supported` to fall back gracefully (e.g. render their own
11//! control, or warn). Each response type carries a zero/identity state for its
12//! payload alongside the flag, and an [`unsupported`](CheckboxResponse::unsupported)
13//! constructor the defaults use.
14
15/// Result of a [`text_area`](crate::UiCtx::text_area) widget.
16///
17/// Mirrors [`TextInputResponse`] but adds a `cursor_pos` field that reports the
18/// line/column of the caret inside the multi-line buffer.
19#[derive(Clone, Debug, Default, PartialEq)]
20pub struct TextAreaResponse {
21    /// Whether the text changed this frame.
22    pub changed: bool,
23    /// The current full text contents (lines separated by `'\n'`).
24    pub text: String,
25    /// Whether the active adapter actually rendered this widget.
26    pub supported: bool,
27    /// Whether this text area currently has keyboard focus.
28    pub focused: bool,
29    /// `(line, column)` zero-based caret position within the buffer.
30    pub cursor_pos: (usize, usize),
31}
32
33impl TextAreaResponse {
34    /// The "not implemented by this adapter" response: empty text, not changed,
35    /// `supported = false`.
36    pub fn unsupported() -> Self {
37        Self {
38            changed: false,
39            text: String::new(),
40            supported: false,
41            focused: false,
42            cursor_pos: (0, 0),
43        }
44    }
45
46    /// A supported response carrying the current `text`, `changed` flag, and
47    /// caret position.
48    ///
49    /// `focused` defaults to `false`. Use
50    /// [`TextAreaResponse::supported_focused`] if accurate focus state is
51    /// available.
52    pub fn supported(text: impl Into<String>, changed: bool, cursor_pos: (usize, usize)) -> Self {
53        Self {
54            changed,
55            text: text.into(),
56            supported: true,
57            focused: false,
58            cursor_pos,
59        }
60    }
61
62    /// A supported response with an explicit `focused` state.
63    pub fn supported_focused(
64        text: impl Into<String>,
65        changed: bool,
66        cursor_pos: (usize, usize),
67        focused: bool,
68    ) -> Self {
69        Self {
70            changed,
71            text: text.into(),
72            supported: true,
73            focused,
74            cursor_pos,
75        }
76    }
77}
78
79/// Result of a [`text_input`](crate::UiCtx::text_input) widget.
80#[derive(Clone, Debug, Default, PartialEq)]
81pub struct TextInputResponse {
82    /// Whether the text changed this frame.
83    pub changed: bool,
84    /// The current text contents.
85    pub text: String,
86    /// Whether the active adapter actually rendered this widget.
87    pub supported: bool,
88    /// Whether this text input currently has keyboard focus.
89    ///
90    /// # Headless-approximate note
91    ///
92    /// This field is a best-effort approximation. Backends that lack a real
93    /// focus mechanism (e.g. headless state-machine contexts) always report
94    /// `false`. Backends that wire iced focus events may set this accurately
95    /// in a future milestone.
96    pub focused: bool,
97}
98
99impl TextInputResponse {
100    /// The "not implemented by this adapter" response: empty text, not changed,
101    /// `supported = false`.
102    pub fn unsupported() -> Self {
103        Self {
104            changed: false,
105            text: String::new(),
106            supported: false,
107            focused: false,
108        }
109    }
110
111    /// A supported response carrying the current `text` and `changed` flag.
112    ///
113    /// `focused` defaults to `false` (headless-approximate). Use
114    /// [`TextInputResponse::supported_focused`] if you have accurate focus state.
115    pub fn supported(text: impl Into<String>, changed: bool) -> Self {
116        Self {
117            changed,
118            text: text.into(),
119            supported: true,
120            focused: false,
121        }
122    }
123
124    /// A supported response with an explicit `focused` state.
125    pub fn supported_focused(text: impl Into<String>, changed: bool, focused: bool) -> Self {
126        Self {
127            changed,
128            text: text.into(),
129            supported: true,
130            focused,
131        }
132    }
133}
134
135/// Result of a [`checkbox`](crate::UiCtx::checkbox) widget.
136#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
137pub struct CheckboxResponse {
138    /// Whether the checked state toggled this frame.
139    pub changed: bool,
140    /// The current checked state.
141    pub checked: bool,
142    /// Whether the active adapter actually rendered this widget.
143    pub supported: bool,
144}
145
146impl CheckboxResponse {
147    /// The "not implemented by this adapter" response.
148    pub fn unsupported() -> Self {
149        Self {
150            changed: false,
151            checked: false,
152            supported: false,
153        }
154    }
155
156    /// A supported response carrying the current `checked` and `changed` flags.
157    pub fn supported(checked: bool, changed: bool) -> Self {
158        Self {
159            changed,
160            checked,
161            supported: true,
162        }
163    }
164}
165
166/// Result of a [`slider`](crate::UiCtx::slider) widget.
167#[derive(Clone, Copy, Debug, Default, PartialEq)]
168pub struct SliderResponse {
169    /// Whether the value changed this frame.
170    pub changed: bool,
171    /// The current value.
172    pub value: f64,
173    /// Whether the active adapter actually rendered this widget.
174    pub supported: bool,
175}
176
177impl SliderResponse {
178    /// The "not implemented by this adapter" response: value `0.0`, not changed.
179    pub fn unsupported() -> Self {
180        Self {
181            changed: false,
182            value: 0.0,
183            supported: false,
184        }
185    }
186
187    /// A supported response carrying the current `value` and `changed` flag.
188    pub fn supported(value: f64, changed: bool) -> Self {
189        Self {
190            changed,
191            value,
192            supported: true,
193        }
194    }
195}
196
197/// Result of a [`dropdown`](crate::UiCtx::dropdown) widget.
198#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
199pub struct DropdownResponse {
200    /// Whether the selection changed this frame.
201    pub changed: bool,
202    /// The index of the currently selected option.
203    pub selected: usize,
204    /// Whether the active adapter actually rendered this widget.
205    pub supported: bool,
206}
207
208impl DropdownResponse {
209    /// The "not implemented by this adapter" response: selection `0`, not changed.
210    pub fn unsupported() -> Self {
211        Self {
212            changed: false,
213            selected: 0,
214            supported: false,
215        }
216    }
217
218    /// A supported response carrying the current `selected` and `changed` flag.
219    pub fn supported(selected: usize, changed: bool) -> Self {
220        Self {
221            changed,
222            selected,
223            supported: true,
224        }
225    }
226}
227
228/// A generic response for widgets with no interaction payload
229/// (`separator`, `spacer`, `image`, `tooltip`, `popup`, `modal`, `scroll_area`).
230///
231/// Carries only whether the adapter rendered it.
232#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
233pub struct WidgetResponse {
234    /// Whether the active adapter actually rendered this widget.
235    pub supported: bool,
236}
237
238impl WidgetResponse {
239    /// The "not implemented by this adapter" response (`supported = false`).
240    pub fn unsupported() -> Self {
241        Self { supported: false }
242    }
243
244    /// A response indicating the widget was rendered (`supported = true`).
245    pub fn supported() -> Self {
246        Self { supported: true }
247    }
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253
254    #[test]
255    fn unsupported_constructors_set_flag_false() {
256        assert!(!TextInputResponse::unsupported().supported);
257        assert!(!TextAreaResponse::unsupported().supported);
258        assert!(!CheckboxResponse::unsupported().supported);
259        assert!(!SliderResponse::unsupported().supported);
260        assert!(!DropdownResponse::unsupported().supported);
261        assert!(!WidgetResponse::unsupported().supported);
262    }
263
264    #[test]
265    fn unsupported_payloads_are_zeroed() {
266        assert_eq!(TextInputResponse::unsupported().text, "");
267        assert!(!TextInputResponse::unsupported().changed);
268        let ta = TextAreaResponse::unsupported();
269        assert_eq!(ta.text, "");
270        assert!(!ta.changed);
271        assert_eq!(ta.cursor_pos, (0, 0));
272        assert!(!CheckboxResponse::unsupported().checked);
273        assert_eq!(SliderResponse::unsupported().value, 0.0);
274        assert_eq!(DropdownResponse::unsupported().selected, 0);
275    }
276
277    #[test]
278    fn supported_constructors_carry_payload() {
279        let t = TextInputResponse::supported("hi", true);
280        assert!(t.supported && t.changed && t.text == "hi");
281        let ta = TextAreaResponse::supported("hello\nworld", true, (1, 3));
282        assert!(ta.supported && ta.changed);
283        assert_eq!(ta.text, "hello\nworld");
284        assert_eq!(ta.cursor_pos, (1, 3));
285        let taf = TextAreaResponse::supported_focused("txt", false, (0, 1), true);
286        assert!(taf.supported && taf.focused);
287        let c = CheckboxResponse::supported(true, true);
288        assert!(c.supported && c.checked && c.changed);
289        let s = SliderResponse::supported(0.5, false);
290        assert!(s.supported && !s.changed && (s.value - 0.5).abs() < 1e-9);
291        let d = DropdownResponse::supported(3, true);
292        assert!(d.supported && d.changed && d.selected == 3);
293        assert!(WidgetResponse::supported().supported);
294    }
295
296    #[test]
297    fn defaults_match_unsupported_for_flag() {
298        // Derived Default leaves supported=false, matching unsupported()'s flag.
299        assert_eq!(
300            CheckboxResponse::default().supported,
301            CheckboxResponse::unsupported().supported
302        );
303        assert_eq!(WidgetResponse::default(), WidgetResponse::unsupported());
304    }
305
306    // TextAreaResponse-specific tests
307    #[test]
308    fn text_area_response_supported_false_by_default() {
309        let r = TextAreaResponse::default();
310        assert!(!r.supported);
311        assert_eq!(r.cursor_pos, (0, 0));
312    }
313
314    #[test]
315    fn text_area_response_cursor_pos_preserved() {
316        let r = TextAreaResponse::supported("line1\nline2", false, (1, 4));
317        assert_eq!(r.cursor_pos, (1, 4));
318    }
319
320    #[test]
321    fn text_area_response_focused_flag() {
322        let unfocused = TextAreaResponse::supported("x", false, (0, 0));
323        assert!(!unfocused.focused);
324        let focused = TextAreaResponse::supported_focused("x", false, (0, 0), true);
325        assert!(focused.focused);
326    }
327}