yuno 0.1.0

A declarative UI layout and rendering framework powered by Skia.
use crate::drawing::{Drawable, Plan};
use crate::layouts::{HasPlanner, Layout};
use skia_safe::{Canvas, Rect};
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::rc::Rc;

// Represents the horizontal alignment of a child within the Column,
// mirroring Jetpack Compose's horizontal alignment in a vertical layout.
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum ColumnHorizontalAlignment {
    Left,
    Center,
    Right,
}

#[derive(Clone)]
pub struct ColumnLayoutElement {
    pub drawable: Rc<dyn Drawable>,
    pub alignment: ColumnHorizontalAlignment,
}

pub struct ColumnLayout {
    w: RefCell<Option<f32>>,
    h: RefCell<Option<f32>>,
    pub childs: RefCell<BTreeMap<String, ColumnLayoutElement>>,
    pub planner: RefCell<Option<Rc<dyn Layout>>>,
    // Caches the calculated absolute bounds for each child during the pre-draw phase.
    // Keyed by the raw address of the drawable trait object.
    calculated_rects: RefCell<BTreeMap<usize, Rect>>,
}

impl Default for ColumnLayout {
    fn default() -> Self {
        Self::new()
    }
}

impl ColumnLayout {
    pub fn new() -> Self {
        Self {
            w: RefCell::new(None),
            h: RefCell::new(None),
            childs: RefCell::new(BTreeMap::new()),
            planner: RefCell::new(None),
            calculated_rects: RefCell::new(BTreeMap::new()),
        }
    }

    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 add_child(self: &Rc<Self>, id: &str, e: ColumnLayoutElement) {
        e.drawable.set_planner(Some(self.clone()));
        self.childs.borrow_mut().insert(id.to_string(), e);
    }

    pub fn remove_child(&self, id: &str) {
        if let Some(c) = self.lookup_child(id) {
            c.drawable.set_planner(None);
            self.childs.borrow_mut().remove(id);
        }
    }

    pub fn lookup_child(&self, id: &str) -> Option<ColumnLayoutElement> {
        self.childs.borrow().get(id).cloned()
    }
}

impl HasPlanner for ColumnLayout {
    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 ColumnLayout {
    fn draw(&self, canvas: &Canvas) -> anyhow::Result<()> {
        let parent_area = self.get_planned_drawing_area(canvas);
        let parent_w = parent_area.width();
        let parent_h = parent_area.height();

        // Skip rendering if the allocated container space is invalid or empty
        if parent_w <= 0.0 || parent_h <= 0.0 {
            return Ok(());
        }

        let childs_borrow = self.childs.borrow();

        // Phase 1: Pre-calculate total vertical space metrics
        let mut total_fit_h = 0.0;
        let mut fill_count = 0;
        let mut none_count = 0;

        for el in childs_borrow.values() {
            match el.drawable.get_vertical_plan() {
                Plan::Fit(h) => total_fit_h += h,
                Plan::Fill => fill_count += 1,
                Plan::None => none_count += 1,
            }
        }

        // Phase 2: Demand satisfaction verification
        // If Fit elements entirely consume or overflow the vertical space, fail fast and abort drawing
        if total_fit_h > parent_h {
            anyhow::bail!(
                "ColumnLayout Error: Total requested height of 'Fit' children ({}) exceeds available container height ({})",
                total_fit_h,
                parent_h
            );
        }

        let remaining_h = parent_h - total_fit_h;

        // Phase 3: Calculate dynamic heights based on priority (Fill > None)
        let fill_allocated_h = if fill_count > 0 {
            remaining_h / fill_count as f32
        } else {
            0.0
        };
        let none_allocated_h = if fill_count == 0 && none_count > 0 {
            remaining_h / none_count as f32
        } else {
            0.0
        };

        // Phase 4: Compute absolute bounds and store them into the cache
        // Wrapped in an explicit scope block to drop the mutable borrow of `calculated_rects` before sub-draw calls
        {
            let mut current_y = parent_area.top;
            let mut rects = self.calculated_rects.borrow_mut();
            rects.clear();

            for el in childs_borrow.values() {
                let target_ptr = el.drawable.as_ref() as *const _ as *const () as usize;

                let child_h = match el.drawable.get_vertical_plan() {
                    Plan::Fit(h) => h,
                    Plan::Fill => fill_allocated_h,
                    Plan::None => none_allocated_h,
                };

                let child_w = match el.drawable.get_horizontal_plan() {
                    Plan::Fit(w) => w,
                    _ => parent_w, // Expand to match parent width if Fill or None
                };

                // Horizontal alignment matching Jetpack Compose Column behavior
                let child_x = match el.alignment {
                    ColumnHorizontalAlignment::Left => parent_area.left,
                    ColumnHorizontalAlignment::Center => {
                        parent_area.left + (parent_w - child_w) / 2.0
                    }
                    ColumnHorizontalAlignment::Right => parent_area.right - child_w,
                };

                let child_rect = Rect::from_xywh(child_x, current_y, child_w, child_h);
                rects.insert(target_ptr, child_rect);

                // Move cursor forward vertically for the next sibling element
                current_y += child_h;
            }
        }

        // Phase 5: Execute chronological rendering based on BTreeMap key order
        for (key, el) in childs_borrow.iter() {
            if let Err(e) = el.drawable.draw(canvas) {
                println!("ColumnLayout draw error at child {}: {:?}", key, e);
            }
        }

        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 Layout for ColumnLayout {
    fn measure_child(&self, c: &dyn Drawable, _canvas: &Canvas) -> Rect {
        // Look up the pre-calculated rect inside the immutable shared borrow
        let target_ptr = c as *const _ as *const () as usize;
        self.calculated_rects
            .borrow()
            .get(&target_ptr)
            .cloned()
            .unwrap_or_else(|| Rect::new(0.0, 0.0, 0.0, 0.0))
    }
}