yuno 0.1.1

A declarative UI layout and rendering framework powered by Skia.
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<Option<f32>>,
    h: RefCell<Option<f32>>,

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

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

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 {
        match *self.w.borrow() {
            Some(w) => Plan::Fit(w),
            None => Plan::Fill,
        }
    }

    fn get_vertical_plan(&self) -> Plan {
        match *self.h.borrow() {
            Some(h) => Plan::Fit(h),
            None => Plan::Fill,
        }
    }
}

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: Option<f32>, h: Option<f32>, 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),
        })
    }

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

    pub fn set_h(&self, h: Option<f32>) {
        *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;
    }
}

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

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

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())
    }
}

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),
        })
    }

    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;
    }
}

pub struct ArcShape {
    w: RefCell<Option<f32>>,
    h: RefCell<Option<f32>>,

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

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

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 {
        match *self.w.borrow() {
            Some(w) => Plan::Fit(w),
            None => Plan::Fill,
        }
    }

    fn get_vertical_plan(&self) -> Plan {
        match *self.h.borrow() {
            Some(h) => Plan::Fit(h),
            None => Plan::Fill,
        }
    }
}

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: Option<f32>,
        h: Option<f32>,
        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),
        })
    }

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

    pub fn set_h(&self, h: Option<f32>) {
        *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;
    }
}

#[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>>>,
}

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())
    }
}

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),
        };

        // 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;
    }
}