yuno 0.2.0

Multimedia UI layout and rendering framework powered by Skia.
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
use crate::drawing::{Drawable, Plan};
use crate::layouts::{HasPlanner, Layout};
use skia_safe::{Canvas, Font, Paint, Path, Point, RRect};
use std::cell::RefCell;
use std::rc::Rc;

pub struct RoundedRectangle {
    w: RefCell<Plan>,
    h: RefCell<Plan>,

    x_rad: RefCell<f32>,
    y_rad: RefCell<f32>,

    p: Paint,
    planner: RefCell<Option<Rc<dyn Layout>>>,
    enabled: RefCell<bool>,
}

impl HasPlanner for RoundedRectangle {
    fn planner(&self) -> &RefCell<Option<Rc<dyn Layout>>> {
        &self.planner
    }

    fn set_planner(&self, p: Option<Rc<dyn Layout>>) {
        *self.planner.borrow_mut() = p;
    }
}

impl Drawable for RoundedRectangle {
    fn draw(&self, canvas: &Canvas) -> anyhow::Result<()> {
        // Fetch the absolute drawing region allocated by the parent layout
        let planned_area = self.get_planned_drawing_area(canvas);

        // Create a Skia Rounded Rect directly using the resolved absolute bounds
        let rrect = RRect::new_rect_xy(planned_area, *self.x_rad.borrow(), *self.y_rad.borrow());

        // Draw it onto the canvas without unnecessary translation/state modification
        canvas.draw_rrect(rrect, &self.p);

        Ok(())
    }

    fn get_horizontal_plan(&self) -> Plan {
        *self.w.borrow()
    }

    fn get_vertical_plan(&self) -> Plan {
        *self.h.borrow()
    }

    fn replan(&self) {}

    fn is_enabled(&self) -> bool {
        *self.enabled.borrow()
    }

    fn set_enabled(&self, e: bool) {
        *self.enabled.borrow_mut() = e
    }

    fn as_drawable(&self) -> &dyn Drawable {
        self
    }
}

impl RoundedRectangle {
    /// Creates a new RoundedRectangle instance.
    /// If `w` or `h` is `None`, it means this element expands to match the parent constraints.
    pub fn new(w: Plan, h: Plan, x_rad: f32, y_rad: f32, paint: Paint) -> Rc<Self> {
        Rc::new(Self {
            w: RefCell::new(w),
            h: RefCell::new(h),
            x_rad: RefCell::new(x_rad),
            y_rad: RefCell::new(y_rad),
            p: paint,
            planner: RefCell::new(None),
            enabled: RefCell::new(true),
        })
    }

    pub fn set_w_plan(&self, w: Plan) {
        *self.w.borrow_mut() = w;
    }

    pub fn set_h_plan(&self, h: Plan) {
        *self.h.borrow_mut() = h;
    }

    pub fn set_x_rad(&self, x_rad: f32) {
        *self.x_rad.borrow_mut() = x_rad;
    }

    pub fn set_y_rad(&self, y_rad: f32) {
        *self.y_rad.borrow_mut() = y_rad;
    }
}

// ==========================================
// PathShape
// ==========================================

pub struct PathShape {
    path: RefCell<Path>,
    w: RefCell<f32>,
    h: RefCell<f32>,

    p: Paint,
    planner: RefCell<Option<Rc<dyn Layout>>>,
    enabled: RefCell<bool>,
}

impl HasPlanner for PathShape {
    fn planner(&self) -> &RefCell<Option<Rc<dyn Layout>>> {
        &self.planner
    }

    fn set_planner(&self, p: Option<Rc<dyn Layout>>) {
        *self.planner.borrow_mut() = p;
    }
}

impl Drawable for PathShape {
    fn draw(&self, canvas: &Canvas) -> anyhow::Result<()> {
        // Fetch the absolute destination region allocated by the parent planner
        let planned_area = self.get_planned_drawing_area(canvas);

        let dest_w = planned_area.width();
        let dest_h = planned_area.height();

        let w = *self.w.borrow();
        let h = *self.h.borrow();

        // Prevent division by zero if the planner allocates an empty or invalid area
        if w <= 0.0 || h <= 0.0 || dest_w <= 0.0 || dest_h <= 0.0 {
            return Ok(());
        }

        // Calculate the horizontal and vertical scale factors to fit the planned area
        let scale_x = dest_w / w;
        let scale_y = dest_h / h;

        canvas.save();

        // 1. Shift the canvas origin to the top-left corner of the planned layout area
        canvas.translate(Point::new(planned_area.left, planned_area.top));

        // 2. Scale the local coordinate space (0, 0, w, h) into the target destination size
        canvas.scale((scale_x, scale_y));

        // Draw the path using the transformed canvas context
        canvas.draw_path(&self.path.borrow(), &self.p);

        canvas.restore();

        Ok(())
    }

    fn get_horizontal_plan(&self) -> Plan {
        Plan::Fit(*self.w.borrow())
    }

    fn get_vertical_plan(&self) -> Plan {
        Plan::Fit(*self.h.borrow())
    }

    fn replan(&self) {}

    fn is_enabled(&self) -> bool {
        *self.enabled.borrow()
    }

    fn set_enabled(&self, e: bool) {
        *self.enabled.borrow_mut() = e;
    }

    fn as_drawable(&self) -> &dyn Drawable {
        self
    }
}

impl PathShape {
    /// Creates a new PathShape wrapper.
    /// The `path` parameter is assumed to be defined within the bounding box of `(0.0, 0.0, w, h)`.
    pub fn new(path: Path, w: f32, h: f32, paint: Paint) -> Rc<Self> {
        Rc::new(Self {
            path: RefCell::new(path),
            w: RefCell::new(w),
            h: RefCell::new(h),
            p: paint,
            planner: RefCell::new(None),
            enabled: RefCell::new(true),
        })
    }

    pub fn set_path(&self, path: Path) {
        *self.path.borrow_mut() = path;
    }

    pub fn set_w(&self, w: f32) {
        *self.w.borrow_mut() = w;
    }

    pub fn set_h(&self, h: f32) {
        *self.h.borrow_mut() = h;
    }
}

// ==========================================
// ArcShape
// ==========================================

pub struct ArcShape {
    w: RefCell<Plan>,
    h: RefCell<Plan>,

    start_angle: RefCell<f32>,
    sweep_angle: RefCell<f32>,

    p: Paint,
    planner: RefCell<Option<Rc<dyn Layout>>>,
    enabled: RefCell<bool>,
}

impl HasPlanner for ArcShape {
    fn planner(&self) -> &RefCell<Option<Rc<dyn Layout>>> {
        &self.planner
    }

    fn set_planner(&self, p: Option<Rc<dyn Layout>>) {
        *self.planner.borrow_mut() = p;
    }
}

impl Drawable for ArcShape {
    fn draw(&self, canvas: &Canvas) -> anyhow::Result<()> {
        // Fetch the absolute boundary allocated by the layout planner
        let planned_area = self.get_planned_drawing_area(canvas);

        // In Skia, an arc is defined by the bounding box of an oval,
        // the start angle (in degrees, 0 is right/east), and the sweep angle (clockwise).
        // The last parameter `use_center` defines whether the arc closes back to the center (forming a pie wedge).
        // We set it to false here for a clean curve/stroke; change to true if you need a pie chart segment.
        canvas.draw_arc(
            planned_area,
            *self.start_angle.borrow(),
            *self.sweep_angle.borrow(),
            false,
            &self.p,
        );

        Ok(())
    }

    fn get_horizontal_plan(&self) -> Plan {
        *self.w.borrow()
    }

    fn get_vertical_plan(&self) -> Plan {
        *self.h.borrow()
    }

    fn replan(&self) {}

    fn is_enabled(&self) -> bool {
        *self.enabled.borrow()
    }

    fn set_enabled(&self, e: bool) {
        *self.enabled.borrow_mut() = e;
    }

    fn as_drawable(&self) -> &dyn Drawable {
        self
    }
}

impl ArcShape {
    /// Creates a new ArcShape instance.
    /// If `w` or `h` is `None`, the arc's bounding box scales dynamically to fill the parent container.
    pub fn new(w: Plan, h: Plan, start_angle: f32, sweep_angle: f32, paint: Paint) -> Rc<Self> {
        Rc::new(Self {
            w: RefCell::new(w),
            h: RefCell::new(h),
            start_angle: RefCell::new(start_angle),
            sweep_angle: RefCell::new(sweep_angle),
            p: paint,
            planner: RefCell::new(None),
            enabled: RefCell::new(true),
        })
    }

    pub fn set_w_plan(&self, w: Plan) {
        *self.w.borrow_mut() = w;
    }

    pub fn set_h_plan(&self, h: Plan) {
        *self.h.borrow_mut() = h;
    }

    pub fn set_start_angle(&self, start_angle: f32) {
        *self.start_angle.borrow_mut() = start_angle;
    }

    pub fn set_sweep_angle(&self, sweep_angle: f32) {
        *self.sweep_angle.borrow_mut() = sweep_angle;
    }
}

// ==========================================
// Text
// ==========================================

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum HorizontalAlign {
    Left,
    Center,
    Right,
}

pub struct Text {
    text: RefCell<String>,
    font: Font,
    p: Paint,
    align: HorizontalAlign,

    // Caches for the measured text dimensions
    cached_w: RefCell<f32>,
    cached_h: RefCell<f32>,

    planner: RefCell<Option<Rc<dyn Layout>>>,
    enabled: RefCell<bool>,
}

impl HasPlanner for Text {
    fn planner(&self) -> &RefCell<Option<Rc<dyn Layout>>> {
        &self.planner
    }

    fn set_planner(&self, p: Option<Rc<dyn Layout>>) {
        *self.planner.borrow_mut() = p;
    }
}

impl Drawable for Text {
    fn draw(&self, canvas: &Canvas) -> anyhow::Result<()> {
        let area = self.get_planned_drawing_area(canvas);
        let text_w = *self.cached_w.borrow();

        // 1. Calculate X position based on the horizontal alignment strategy
        let x = match self.align {
            HorizontalAlign::Left => area.left,
            HorizontalAlign::Center => area.left + (area.width() - text_w) / 2.0,
            HorizontalAlign::Right => area.right - text_w,
        };

        // 2. Calculate Y position for vertical centering
        // Skia's ascent is negative (above baseline) and descent is positive (below baseline).
        // The midpoint of the text bounding box relative to the baseline is (ascent + descent) / 2.0.
        let (_, metrics) = self.font.metrics();
        let center_y = area.top + area.height() / 2.0;
        let baseline_y = center_y - (metrics.ascent + metrics.descent) / 2.0;

        // 3. Draw the text at the computed baseline origin
        canvas.draw_str(
            &*self.text.borrow(),
            Point::new(x, baseline_y),
            &self.font,
            &self.p,
        );

        Ok(())
    }

    fn get_horizontal_plan(&self) -> Plan {
        Plan::Fit(*self.cached_w.borrow())
    }

    fn get_vertical_plan(&self) -> Plan {
        Plan::Fit(*self.cached_h.borrow())
    }

    fn replan(&self) {}

    fn is_enabled(&self) -> bool {
        *self.enabled.borrow()
    }

    fn set_enabled(&self, e: bool) {
        *self.enabled.borrow_mut() = e;
    }

    fn as_drawable(&self) -> &dyn Drawable {
        self
    }
}

impl Text {
    pub fn new(text: &str, font: Font, paint: Paint, align: HorizontalAlign) -> Rc<Self> {
        let instance = Self {
            text: RefCell::new(text.to_string()),
            font,
            p: paint,
            align,
            cached_w: RefCell::new(0.0),
            cached_h: RefCell::new(0.0),
            planner: RefCell::new(None),
            enabled: RefCell::new(true),
        };

        // Perform the initial dimension calculation
        instance.recalculate_metrics(text);

        Rc::new(instance)
    }

    /// Dynamically updates the text and forces a recalculation of the required dimensions.
    pub fn set_text(&self, new_text: &str) {
        if *self.text.borrow() == new_text {
            return; // Skip recalculation if the text hasn't changed
        }

        self.recalculate_metrics(new_text);
        *self.text.borrow_mut() = new_text.to_string();
    }

    /// Internal helper to calculate and cache the glyph widths and font height.
    fn recalculate_metrics(&self, text: &str) {
        // Measure exact width by summing up individual glyph widths
        let mut glyphs = vec![skia_safe::GlyphId::default(); text.len()];
        let actual_glyph_count = self.font.text_to_glyphs(text, &mut glyphs);
        glyphs.truncate(actual_glyph_count);

        let mut widths = vec![0.0f32; glyphs.len()];
        self.font.get_widths(&glyphs, &mut widths);

        let total_text_w: f32 = widths.iter().sum();
        *self.cached_w.borrow_mut() = total_text_w;

        // Measure exact height via font metrics (descent - ascent)
        let (_, metrics) = self.font.metrics();
        *self.cached_h.borrow_mut() = metrics.descent - metrics.ascent;
    }
}