eye_declare 0.4.3

Declarative inline TUI rendering library for Rust
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
use std::ops::{Deref, DerefMut};

use ratatui_core::{buffer::Buffer, layout::Rect};

use crate::element::Elements;
use crate::hooks::Hooks;
use crate::insets::Insets;
use crate::node::{Layout, WidthConstraint};

/// Implement [`ChildCollector`](crate::ChildCollector) for a component so it accepts slot children in
/// the `element!` macro.
///
/// Slot children are passed to the component's [`Component::children`] method as the
/// `slot` parameter. Layout containers like [`VStack`] and [`HStack`] use this to
/// accept arbitrary child elements.
///
/// # Example
///
/// ```ignore
/// #[derive(Default)]
/// struct Card { pub title: String }
///
/// impl Component for Card {
///     type State = ();
///     fn render(&self, area: Rect, buf: &mut Buffer, _: &()) { /* draw border */ }
///     fn children(&self, _: &(), slot: Option<Elements>) -> Option<Elements> {
///         slot // pass children through
///     }
/// }
///
/// impl_slot_children!(Card);
///
/// // Now Card can accept children:
/// element! {
///     Card(title: "My Card".into()) {
///         Spinner(label: "loading...")
///         "some text"
///     }
/// }
/// ```
#[macro_export]
macro_rules! impl_slot_children {
    ($t:ty) => {
        impl $crate::ChildCollector for $t {
            type Collector = $crate::Elements;
            type Output = $crate::ComponentWithSlot<$t>;

            fn finish(self, collector: $crate::Elements) -> $crate::ComponentWithSlot<$t> {
                $crate::ComponentWithSlot::new(self, collector)
            }
        }
    };
}

/// Result of handling an input event in a component's event handler.
///
/// Controls whether event propagation continues through the component
/// tree during either the capture or bubble phase.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EventResult {
    /// The event was handled by this component. Stops propagation.
    Consumed,
    /// The event was not handled. Propagation continues to the next node.
    Ignored,
}

/// Wrapper that automatically marks component state dirty on mutation.
///
/// The framework wraps each component's `State` in `Tracked<S>`.
/// Write access via [`DerefMut`] automatically marks the state dirty,
/// telling the framework this component needs to re-render.
/// Read access via [`Deref`] does not set the dirty flag.
///
/// # Usage in event handlers
///
/// Event handlers ([`Component::handle_event`], [`Component::handle_event_capture`])
/// receive `&mut Tracked<State>`. Writing to state through field access or
/// method calls goes through [`DerefMut`] and marks the component dirty:
///
/// ```ignore
/// fn handle_event(&self, event: &Event, state: &mut Tracked<Self::State>) -> EventResult {
///     state.text.push('a');  // DerefMut → marks dirty
///     EventResult::Consumed
/// }
/// ```
///
/// # Reading state without marking dirty
///
/// **Important:** on `&mut Tracked<S>`, Rust's auto-deref uses [`DerefMut`]
/// for *all* field access — even reads. This means `state.some_field` sets
/// dirty even when you only read the value. Use [`read()`](Tracked::read)
/// to get a shared reference that goes through [`Deref`] instead:
///
/// ```ignore
/// fn handle_event(&self, event: &Event, state: &mut Tracked<Self::State>) -> EventResult {
///     // state.mode would trigger DerefMut — use read() for a clean read
///     if state.read().mode == Mode::Insert {
///         state.text.push('a');  // DerefMut → marks dirty
///         EventResult::Consumed
///     } else {
///         EventResult::Ignored  // state stays clean
///     }
/// }
/// ```
///
/// # Usage with the imperative API
///
/// ```ignore
/// let id = renderer.push(Spinner::new("loading..."));
///
/// // DerefMut triggers dirty flag automatically
/// renderer.state_mut::<Spinner>(id).tick();
/// ```
pub struct Tracked<S> {
    inner: S,
    dirty: bool,
}

impl<S> Tracked<S> {
    /// Wrap a value in dirty-tracking. Starts dirty so the first render
    /// always happens.
    pub fn new(inner: S) -> Self {
        Self { inner, dirty: true }
    }

    /// Whether the inner value has been mutated since the last
    /// [`clear_dirty`](Tracked::clear_dirty) call.
    pub fn is_dirty(&self) -> bool {
        self.dirty
    }

    /// Reset the dirty flag. Called by the framework after rendering.
    pub fn clear_dirty(&mut self) {
        self.dirty = false;
    }

    /// Get a shared reference to the inner state without marking dirty.
    ///
    /// On `&mut Tracked<S>`, direct field access like `state.field` goes
    /// through [`DerefMut`], which unconditionally sets the dirty flag —
    /// even for reads. Use `state.read().field` instead to read state
    /// without triggering a re-render.
    ///
    /// This is especially useful in event handlers that conditionally
    /// modify state, or that read state to call methods using interior
    /// mutability (e.g., sending on a channel):
    ///
    /// ```ignore
    /// fn handle_event(&self, event: &Event, state: &mut Tracked<Self::State>) -> EventResult {
    ///     if let Some(tx) = &state.read().event_tx {
    ///         tx.send(MyEvent::KeyPressed).ok();
    ///     }
    ///     EventResult::Consumed
    /// }
    /// ```
    pub fn read(&self) -> &S {
        &self.inner
    }
}

impl<S> Deref for Tracked<S> {
    type Target = S;

    fn deref(&self) -> &S {
        &self.inner
    }
}

impl<S> DerefMut for Tracked<S> {
    fn deref_mut(&mut self) -> &mut S {
        self.dirty = true;
        &mut self.inner
    }
}

/// A component that can render itself into a terminal region.
///
/// Most users should define components with the [`#[component]`](macro@crate::component)
/// and [`#[props]`](macro@crate::props) attribute macros rather than implementing
/// this trait directly:
///
/// ```ignore
/// #[props]
/// struct CardProps {
///     title: String,
///     #[default(true)]
///     visible: bool,
/// }
///
/// #[component(props = CardProps, children = Elements)]
/// fn card(props: &CardProps, children: Elements) -> Elements {
///     element! {
///         View(border: BorderType::Rounded, title: props.title.clone()) {
///             #(children)
///         }
///     }
/// }
/// ```
///
/// The `#[component]` macro generates an `impl Component` with an
/// [`update()`](Component::update) override that calls the function
/// once per cycle with real hooks and real children.
///
/// # Direct implementation
///
/// Implement this trait directly only for framework primitives that need
/// raw buffer access ([`Canvas`](crate::Canvas)) or layout chrome
/// ([`View`](crate::View)). Most methods are `#[doc(hidden)]` — they
/// exist for the framework and primitives, not for typical component
/// authors.
pub trait Component: Send + Sync + 'static {
    /// State type for this component. The framework wraps it in
    /// `Tracked<S>` for automatic dirty detection.
    type State: Send + Sync + Default + 'static;

    /// Return this component's props as `&dyn Any`.
    ///
    /// The default returns `self`, which is correct for all standard
    /// components (where the component struct IS the props). Data-children
    /// wrappers generated by `#[component]` override this to return the
    /// inner props struct instead. The framework passes the result to
    /// hook callbacks for typed downcast.
    #[doc(hidden)]
    fn props_as_any(&self) -> &dyn std::any::Any
    where
        Self: Sized,
    {
        self
    }

    /// Primitive: render into a buffer region. Only for hand-written
    /// leaf components (e.g., [`Canvas`](crate::Canvas)).
    /// `#[component]` functions return element trees instead.
    #[doc(hidden)]
    fn render(&self, _area: Rect, _buf: &mut Buffer, _state: &Self::State) {}

    /// Primitive: optional height hint to skip probe-render measurement.
    /// Only for hand-written leaf components.
    #[doc(hidden)]
    fn desired_height(&self, _width: u16, _state: &Self::State) -> Option<u16> {
        None
    }

    /// Capture-phase event handler (root → focused). Prefer
    /// [`Hooks::use_event_capture`](crate::Hooks::use_event_capture) in
    /// `#[component]` functions.
    #[doc(hidden)]
    fn handle_event_capture(
        &self,
        _event: &crossterm::event::Event,
        _state: &mut Tracked<Self::State>,
    ) -> EventResult {
        EventResult::Ignored
    }

    /// Bubble-phase event handler (focused → root). Prefer
    /// [`Hooks::use_event`](crate::Hooks::use_event) in `#[component]`
    /// functions.
    #[doc(hidden)]
    fn handle_event(
        &self,
        _event: &crossterm::event::Event,
        _state: &mut Tracked<Self::State>,
    ) -> EventResult {
        EventResult::Ignored
    }

    /// Whether this component can receive focus. Prefer
    /// [`Hooks::use_focusable`](crate::Hooks::use_focusable) in
    /// `#[component]` functions.
    #[doc(hidden)]
    fn is_focusable(&self, _state: &Self::State) -> bool {
        false
    }

    /// Cursor position when focused. Prefer
    /// [`Hooks::use_cursor`](crate::Hooks::use_cursor) in `#[component]`
    /// functions.
    #[doc(hidden)]
    fn cursor_position(&self, _area: Rect, _state: &Self::State) -> Option<(u16, u16)> {
        None
    }

    /// Provide initial state. Returns `None` to use `State::default()`.
    /// In `#[component]`, use `initial_state = expr` on the attribute instead.
    #[doc(hidden)]
    fn initial_state(&self) -> Option<Self::State> {
        None
    }

    /// Primitive: insets for child layout within the render area.
    /// Only for hand-written chrome components (e.g., [`View`](crate::View)).
    #[doc(hidden)]
    fn content_inset(&self, _state: &Self::State) -> Insets {
        Insets::ZERO
    }

    /// Layout direction for children. Prefer
    /// [`Hooks::use_layout`](crate::Hooks::use_layout) in `#[component]`
    /// functions.
    #[doc(hidden)]
    fn layout(&self) -> Layout {
        Layout::default()
    }

    /// Width constraint within a horizontal parent. Prefer
    /// [`Hooks::use_width_constraint`](crate::Hooks::use_width_constraint)
    /// in `#[component]` functions.
    #[doc(hidden)]
    fn width_constraint(&self) -> WidthConstraint {
        WidthConstraint::default()
    }

    /// Fallback: declare lifecycle effects. Called by the default
    /// [`update()`](Component::update) implementation. `#[component]`
    /// functions handle lifecycle and view in a single `update()` override.
    #[doc(hidden)]
    fn lifecycle(&self, _hooks: &mut Hooks<Self, Self::State>, _state: &Self::State)
    where
        Self: Sized,
    {
    }

    /// Fallback: return the element tree. Called by the default
    /// [`update()`](Component::update) implementation. `#[component]`
    /// functions handle lifecycle and view in a single `update()` override.
    #[doc(hidden)]
    fn view(&self, _state: &Self::State, children: Elements) -> Elements {
        children
    }

    /// Combined lifecycle and view in a single call.
    ///
    /// Called by the framework during reconciliation. Collects hooks
    /// and produces the element tree in one pass, so the component
    /// function runs exactly once per cycle.
    ///
    /// The default implementation calls [`lifecycle()`](Component::lifecycle)
    /// then [`view()`](Component::view) separately — correct for hand-written
    /// `impl Component` primitives. The `#[component]` macro overrides this
    /// to call the user function once with real hooks and real children.
    fn update(
        &self,
        hooks: &mut Hooks<Self, Self::State>,
        state: &Self::State,
        children: Elements,
    ) -> Elements
    where
        Self: Sized,
    {
        self.lifecycle(hooks, state);
        self.view(state, children)
    }
}

/// Vertical stack container.
///
/// Children are laid out top-to-bottom, each receiving the full
/// parent width. This is the default layout — [`VStack`] is mainly
/// used for explicit grouping or to anchor a keyed subtree.
///
/// ```ignore
/// element! {
///     VStack {
///         "Hello"
///         "World"
///     }
/// }
/// ```
#[derive(Debug, Default, Clone)]
pub struct VStack;

impl VStack {
    pub fn builder() -> Self {
        Self
    }

    pub fn build(self) -> Self {
        self
    }
}

#[eye_declare_macros::component(props = VStack, children = Elements, crate_path = crate)]
fn vstack(_props: &VStack, children: Elements) -> Elements {
    children
}

/// Horizontal stack container.
///
/// Children are laid out left-to-right. Use [`Column`] inside to
/// control individual child widths.
///
/// ```ignore
/// element! {
///     HStack {
///         Column(width: Fixed(10)) { "left" }
///         Column(width: Fill) { "right" }
///     }
/// }
/// ```
#[derive(Debug, Default, Clone)]
pub struct HStack;

impl HStack {
    pub fn builder() -> Self {
        Self
    }

    pub fn build(self) -> Self {
        self
    }
}

#[eye_declare_macros::component(props = HStack, children = Elements, crate_path = crate)]
fn hstack(_props: &HStack, hooks: &mut Hooks<HStack, ()>, children: Elements) -> Elements {
    hooks.use_layout(Layout::Horizontal);
    children
}

/// Width-constrained wrapper for use inside [`HStack`].
///
/// ```ignore
/// element! {
///     HStack {
///         Column(width: Fixed(10)) { "fixed" }
///         Column(width: Fill) { "flexible" }
///     }
/// }
/// ```
#[derive(Debug, Default, Clone, typed_builder::TypedBuilder)]
pub struct Column {
    #[builder(default, setter(into))]
    pub width: WidthConstraint,
}

#[eye_declare_macros::component(props = Column, children = Elements, crate_path = crate)]
fn column(props: &Column, hooks: &mut Hooks<Column, ()>, children: Elements) -> Elements {
    hooks.use_width_constraint(props.width);
    children
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn tracked_starts_dirty() {
        let t = Tracked::new(42u32);
        assert!(t.is_dirty());
    }

    #[test]
    fn tracked_deref_does_not_set_dirty() {
        let mut t = Tracked::new(42u32);
        t.clear_dirty();
        assert!(!t.is_dirty());

        // Read access via Deref
        let _val = *t;
        assert!(!t.is_dirty());
    }

    #[test]
    fn tracked_deref_mut_sets_dirty() {
        let mut t = Tracked::new(42u32);
        t.clear_dirty();
        assert!(!t.is_dirty());

        // Write access via DerefMut
        *t = 99;
        assert!(t.is_dirty());
    }

    #[test]
    fn tracked_clear_dirty_resets() {
        let mut t = Tracked::new(42u32);
        assert!(t.is_dirty());
        t.clear_dirty();
        assert!(!t.is_dirty());
    }
}