Skip to main content

ftui_runtime/
lens.rs

1//! Bidirectional lenses for state-widget binding.
2//!
3//! A lens focuses on a part of a larger structure, providing both read
4//! (view) and write (set) access with algebraic guarantees:
5//!
6//! - **GetPut**: Setting the value you just read is a no-op.
7//! - **PutGet**: Reading after a set returns the value you set.
8//!
9//! # Usage
10//!
11//! ```
12//! use ftui_runtime::lens::{Lens, field_lens, compose};
13//!
14//! #[derive(Clone, Debug, PartialEq)]
15//! struct Config { volume: u8, brightness: u8 }
16//!
17//! let volume = field_lens(
18//!     |c: &Config| c.volume,
19//!     |c: &mut Config, v| c.volume = v,
20//! );
21//!
22//! let mut config = Config { volume: 50, brightness: 100 };
23//! assert_eq!(volume.view(&config), 50);
24//!
25//! volume.set(&mut config, 75);
26//! assert_eq!(config.volume, 75);
27//! ```
28//!
29//! # Composition
30//!
31//! Lenses compose for nested state access:
32//!
33//! ```
34//! use ftui_runtime::lens::{Lens, field_lens, compose};
35//!
36//! #[derive(Clone, Debug, PartialEq)]
37//! struct App { settings: Settings }
38//!
39//! #[derive(Clone, Debug, PartialEq)]
40//! struct Settings { font_size: u16 }
41//!
42//! let settings = field_lens(
43//!     |a: &App| a.settings.clone(),
44//!     |a: &mut App, s| a.settings = s,
45//! );
46//! let font_size = field_lens(
47//!     |s: &Settings| s.font_size,
48//!     |s: &mut Settings, v| s.font_size = v,
49//! );
50//!
51//! let app_font_size = compose(settings, font_size);
52//!
53//! let mut app = App { settings: Settings { font_size: 14 } };
54//! assert_eq!(app_font_size.view(&app), 14);
55//!
56//! app_font_size.set(&mut app, 18);
57//! assert_eq!(app.settings.font_size, 18);
58//! ```
59
60/// A bidirectional lens focusing on part `A` of a whole `S`.
61///
62/// # Laws
63///
64/// A well-behaved lens satisfies:
65/// 1. **GetPut**: `lens.set(s, lens.view(s))` leaves `s` unchanged.
66/// 2. **PutGet**: After `lens.set(s, a)`, `lens.view(s)` returns `a`.
67pub trait Lens<S, A> {
68    /// View the focused part.
69    fn view(&self, whole: &S) -> A;
70
71    /// Set the focused part, mutating the whole in place.
72    fn set(&self, whole: &mut S, part: A);
73
74    /// Modify the focused part with a function.
75    fn over(&self, whole: &mut S, f: impl FnOnce(A) -> A)
76    where
77        A: Clone,
78    {
79        let current = self.view(whole);
80        self.set(whole, f(current));
81    }
82
83    /// Compose this lens with an inner lens to focus deeper.
84    fn then<B, L2: Lens<A, B>>(self, inner: L2) -> Composed<Self, L2, A>
85    where
86        Self: Sized,
87        A: Clone,
88    {
89        Composed {
90            outer: self,
91            inner,
92            _mid: std::marker::PhantomData,
93        }
94    }
95}
96
97/// A lens built from getter and setter closures.
98pub struct FieldLens<G, P> {
99    getter: G,
100    putter: P,
101}
102
103impl<S, A, G, P> Lens<S, A> for FieldLens<G, P>
104where
105    G: Fn(&S) -> A,
106    P: Fn(&mut S, A),
107{
108    fn view(&self, whole: &S) -> A {
109        (self.getter)(whole)
110    }
111
112    fn set(&self, whole: &mut S, part: A) {
113        (self.putter)(whole, part);
114    }
115}
116
117/// Create a lens from getter and setter functions.
118///
119/// # Example
120///
121/// ```
122/// use ftui_runtime::lens::{Lens, field_lens};
123///
124/// struct Point { x: f64, y: f64 }
125///
126/// let x_lens = field_lens(|p: &Point| p.x, |p: &mut Point, v| p.x = v);
127///
128/// let mut p = Point { x: 1.0, y: 2.0 };
129/// assert_eq!(x_lens.view(&p), 1.0);
130/// x_lens.set(&mut p, 3.0);
131/// assert_eq!(p.x, 3.0);
132/// ```
133pub fn field_lens<S, A>(
134    getter: impl Fn(&S) -> A + 'static,
135    setter: impl Fn(&mut S, A) + 'static,
136) -> FieldLens<impl Fn(&S) -> A, impl Fn(&mut S, A)> {
137    FieldLens {
138        getter,
139        putter: setter,
140    }
141}
142
143/// Composed lens: outer ∘ inner.
144///
145/// First uses `outer` to access an intermediate value, then `inner` to
146/// access the final target within it.
147pub struct Composed<L1, L2, B> {
148    outer: L1,
149    inner: L2,
150    _mid: std::marker::PhantomData<B>,
151}
152
153impl<S, B, A, L1, L2> Lens<S, A> for Composed<L1, L2, B>
154where
155    L1: Lens<S, B>,
156    L2: Lens<B, A>,
157    B: Clone,
158{
159    fn view(&self, whole: &S) -> A {
160        let mid = self.outer.view(whole);
161        self.inner.view(&mid)
162    }
163
164    fn set(&self, whole: &mut S, part: A) {
165        let mut mid = self.outer.view(whole);
166        self.inner.set(&mut mid, part);
167        self.outer.set(whole, mid);
168    }
169}
170
171/// Compose two lenses: `outer` then `inner`.
172///
173/// The resulting lens focuses on `inner`'s target through `outer`'s target.
174pub fn compose<S, B, A, L1, L2>(outer: L1, inner: L2) -> Composed<L1, L2, B>
175where
176    L1: Lens<S, B>,
177    L2: Lens<B, A>,
178    B: Clone,
179{
180    Composed {
181        outer,
182        inner,
183        _mid: std::marker::PhantomData,
184    }
185}
186
187/// Identity lens that focuses on the whole value.
188pub struct Identity;
189
190impl<S: Clone> Lens<S, S> for Identity {
191    fn view(&self, whole: &S) -> S {
192        whole.clone()
193    }
194
195    fn set(&self, whole: &mut S, part: S) {
196        *whole = part;
197    }
198}
199
200/// Lens into the first element of a tuple.
201pub struct Fst;
202
203impl<A: Clone, B: Clone> Lens<(A, B), A> for Fst {
204    fn view(&self, whole: &(A, B)) -> A {
205        whole.0.clone()
206    }
207
208    fn set(&self, whole: &mut (A, B), part: A) {
209        whole.0 = part;
210    }
211}
212
213/// Lens into the second element of a tuple.
214pub struct Snd;
215
216impl<A: Clone, B: Clone> Lens<(A, B), B> for Snd {
217    fn view(&self, whole: &(A, B)) -> B {
218        whole.1.clone()
219    }
220
221    fn set(&self, whole: &mut (A, B), part: B) {
222        whole.1 = part;
223    }
224}
225
226/// Lens into a Vec element by index.
227///
228/// # Panics
229///
230/// Panics if the index is out of bounds.
231pub struct AtIndex {
232    index: usize,
233}
234
235impl AtIndex {
236    pub fn new(index: usize) -> Self {
237        Self { index }
238    }
239}
240
241impl<T: Clone> Lens<Vec<T>, T> for AtIndex {
242    fn view(&self, whole: &Vec<T>) -> T {
243        whole[self.index].clone()
244    }
245
246    fn set(&self, whole: &mut Vec<T>, part: T) {
247        whole[self.index] = part;
248    }
249}
250
251/// Create a lens that focuses on a Vec element by index.
252pub fn at_index(index: usize) -> AtIndex {
253    AtIndex::new(index)
254}
255
256/// A prism for optional focusing (lens that may fail to view).
257///
258/// Unlike a lens, a prism's `preview` returns `Option<A>` — the target
259/// may not exist. Useful for enum variants and optional fields.
260pub trait Prism<S, A> {
261    /// Try to view the focused part. Returns `None` if the prism doesn't match.
262    fn preview(&self, whole: &S) -> Option<A>;
263
264    /// Set the focused part if the prism matches. Returns true if applied.
265    fn set_if(&self, whole: &mut S, part: A) -> bool;
266}
267
268/// Prism into an `Option<T>` value.
269pub struct SomePrism;
270
271impl<T: Clone> Prism<Option<T>, T> for SomePrism {
272    fn preview(&self, whole: &Option<T>) -> Option<T> {
273        whole.clone()
274    }
275
276    fn set_if(&self, whole: &mut Option<T>, part: T) -> bool {
277        if whole.is_some() {
278            *whole = Some(part);
279            true
280        } else {
281            false
282        }
283    }
284}
285
286#[cfg(test)]
287mod tests {
288    use super::*;
289
290    #[derive(Clone, Debug, PartialEq)]
291    struct Point {
292        x: f64,
293        y: f64,
294    }
295
296    #[derive(Clone, Debug, PartialEq)]
297    struct Line {
298        start: Point,
299        end: Point,
300    }
301
302    fn x_lens() -> FieldLens<impl Fn(&Point) -> f64, impl Fn(&mut Point, f64)> {
303        field_lens(|p: &Point| p.x, |p: &mut Point, v| p.x = v)
304    }
305
306    fn y_lens() -> FieldLens<impl Fn(&Point) -> f64, impl Fn(&mut Point, f64)> {
307        field_lens(|p: &Point| p.y, |p: &mut Point, v| p.y = v)
308    }
309
310    fn start_lens() -> FieldLens<impl Fn(&Line) -> Point, impl Fn(&mut Line, Point)> {
311        field_lens(|l: &Line| l.start.clone(), |l: &mut Line, p| l.start = p)
312    }
313
314    fn end_lens() -> FieldLens<impl Fn(&Line) -> Point, impl Fn(&mut Line, Point)> {
315        field_lens(|l: &Line| l.end.clone(), |l: &mut Line, p| l.end = p)
316    }
317
318    // ── Basic lens operations ───────────────────────────────────────
319
320    #[test]
321    fn view_reads_field() {
322        let lens = x_lens();
323        let p = Point { x: 3.0, y: 4.0 };
324        assert_eq!(lens.view(&p), 3.0);
325    }
326
327    #[test]
328    fn set_writes_field() {
329        let lens = x_lens();
330        let mut p = Point { x: 3.0, y: 4.0 };
331        lens.set(&mut p, 5.0);
332        assert_eq!(p.x, 5.0);
333        assert_eq!(p.y, 4.0); // y unchanged
334    }
335
336    #[test]
337    fn over_modifies_with_function() {
338        let lens = x_lens();
339        let mut p = Point { x: 3.0, y: 4.0 };
340        lens.over(&mut p, |x| x * 2.0);
341        assert_eq!(p.x, 6.0);
342    }
343
344    // ── Lens laws ───────────────────────────────────────────────────
345
346    #[test]
347    fn law_get_put() {
348        // Setting what you got is a no-op
349        let lens = x_lens();
350        let mut p = Point { x: 3.0, y: 4.0 };
351        let original = p.clone();
352        let viewed = lens.view(&p);
353        lens.set(&mut p, viewed);
354        assert_eq!(p, original);
355    }
356
357    #[test]
358    fn law_put_get() {
359        // Getting after a set returns the set value
360        let lens = x_lens();
361        let mut p = Point { x: 3.0, y: 4.0 };
362        lens.set(&mut p, 99.0);
363        assert_eq!(lens.view(&p), 99.0);
364    }
365
366    #[test]
367    fn law_put_put() {
368        // Setting twice is the same as setting once with the second value
369        let lens = x_lens();
370        let mut p1 = Point { x: 3.0, y: 4.0 };
371        let mut p2 = p1.clone();
372
373        lens.set(&mut p1, 10.0);
374        lens.set(&mut p1, 20.0);
375
376        lens.set(&mut p2, 20.0);
377
378        assert_eq!(p1, p2);
379    }
380
381    // ── Composition ─────────────────────────────────────────────────
382
383    #[test]
384    fn compose_view() {
385        let start_x = compose(start_lens(), x_lens());
386        let line = Line {
387            start: Point { x: 1.0, y: 2.0 },
388            end: Point { x: 3.0, y: 4.0 },
389        };
390        assert_eq!(start_x.view(&line), 1.0);
391    }
392
393    #[test]
394    fn compose_set() {
395        let start_x = compose(start_lens(), x_lens());
396        let mut line = Line {
397            start: Point { x: 1.0, y: 2.0 },
398            end: Point { x: 3.0, y: 4.0 },
399        };
400        start_x.set(&mut line, 99.0);
401        assert_eq!(line.start.x, 99.0);
402        assert_eq!(line.start.y, 2.0); // y unchanged
403        assert_eq!(line.end.x, 3.0); // end unchanged
404    }
405
406    #[test]
407    fn compose_laws_hold() {
408        let start_x = compose(start_lens(), x_lens());
409        let mut line = Line {
410            start: Point { x: 1.0, y: 2.0 },
411            end: Point { x: 3.0, y: 4.0 },
412        };
413
414        // GetPut
415        let original = line.clone();
416        let v = start_x.view(&line);
417        start_x.set(&mut line, v);
418        assert_eq!(line, original);
419
420        // PutGet
421        start_x.set(&mut line, 42.0);
422        assert_eq!(start_x.view(&line), 42.0);
423    }
424
425    #[test]
426    fn compose_end_y() {
427        let end_y = compose(end_lens(), y_lens());
428        let mut line = Line {
429            start: Point { x: 1.0, y: 2.0 },
430            end: Point { x: 3.0, y: 4.0 },
431        };
432        assert_eq!(end_y.view(&line), 4.0);
433        end_y.set(&mut line, 100.0);
434        assert_eq!(line.end.y, 100.0);
435    }
436
437    // ── Identity lens ───────────────────────────────────────────────
438
439    #[test]
440    fn identity_view() {
441        let p = Point { x: 1.0, y: 2.0 };
442        assert_eq!(Identity.view(&p), p);
443    }
444
445    #[test]
446    fn identity_set() {
447        let mut p = Point { x: 1.0, y: 2.0 };
448        let new = Point { x: 5.0, y: 6.0 };
449        Identity.set(&mut p, new.clone());
450        assert_eq!(p, new);
451    }
452
453    // ── Tuple lenses ────────────────────────────────────────────────
454
455    #[test]
456    fn fst_lens() {
457        let mut pair = (10u32, "hello");
458        assert_eq!(Fst.view(&pair), 10);
459        Fst.set(&mut pair, 20);
460        assert_eq!(pair, (20, "hello"));
461    }
462
463    #[test]
464    fn snd_lens() {
465        let mut pair = (10u32, 20u32);
466        assert_eq!(Snd.view(&pair), 20);
467        Snd.set(&mut pair, 30);
468        assert_eq!(pair, (10, 30));
469    }
470
471    // ── AtIndex lens ────────────────────────────────────────────────
472
473    #[test]
474    fn at_index_view() {
475        let v = vec![10, 20, 30];
476        assert_eq!(at_index(1).view(&v), 20);
477    }
478
479    #[test]
480    fn at_index_set() {
481        let mut v = vec![10, 20, 30];
482        at_index(1).set(&mut v, 99);
483        assert_eq!(v, vec![10, 99, 30]);
484    }
485
486    #[test]
487    #[should_panic]
488    fn at_index_out_of_bounds() {
489        let v = vec![1, 2, 3];
490        at_index(5).view(&v);
491    }
492
493    // ── Prism ───────────────────────────────────────────────────────
494
495    #[test]
496    fn some_prism_preview() {
497        let opt = Some(42);
498        assert_eq!(SomePrism.preview(&opt), Some(42));
499
500        let none: Option<i32> = None;
501        assert_eq!(SomePrism.preview(&none), None);
502    }
503
504    #[test]
505    fn some_prism_set_if_some() {
506        let mut opt = Some(42);
507        assert!(SomePrism.set_if(&mut opt, 99));
508        assert_eq!(opt, Some(99));
509    }
510
511    #[test]
512    fn some_prism_set_if_none() {
513        let mut opt: Option<i32> = None;
514        assert!(!SomePrism.set_if(&mut opt, 99));
515        assert_eq!(opt, None);
516    }
517
518    // ── Composed lens with over ─────────────────────────────────────
519
520    #[test]
521    fn composed_over() {
522        let start_x = compose(start_lens(), x_lens());
523        let mut line = Line {
524            start: Point { x: 10.0, y: 20.0 },
525            end: Point { x: 30.0, y: 40.0 },
526        };
527        start_x.over(&mut line, |x| x + 5.0);
528        assert_eq!(line.start.x, 15.0);
529    }
530
531    // ── Method chaining with then() ─────────────────────────────────
532
533    #[test]
534    fn then_composition() {
535        let start_x = start_lens().then(x_lens());
536        let line = Line {
537            start: Point { x: 7.0, y: 8.0 },
538            end: Point { x: 9.0, y: 10.0 },
539        };
540        assert_eq!(start_x.view(&line), 7.0);
541    }
542}