yuno 0.2.0

Multimedia UI layout and rendering framework powered by Skia.
use crate::drawing::{Drawable, Plan};
use crate::interacting::{Event, Interactive};
use crate::layouts::{HasPlanner, Layout};
use skia_safe::{Canvas, Rect};
use std::any::Any;
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 {
    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>>,

    enabled: RefCell<bool>,

    // Caches the dimensional plans to avoid recalculating every frame.
    cached_h_plan: RefCell<Option<Plan>>,
    cached_v_plan: RefCell<Option<Plan>>,
}

impl ColumnLayout {
    pub fn new(is_enabled: bool) -> Rc<Self> {
        Rc::new(Self {
            childs: RefCell::new(BTreeMap::new()),
            planner: RefCell::new(None),
            calculated_rects: RefCell::new(BTreeMap::new()),
            enabled: RefCell::new(is_enabled),
            cached_h_plan: RefCell::new(None),
            cached_v_plan: RefCell::new(None),
        })
    }

    pub fn add_child(self: Rc<Self>, id: &str, e: ColumnLayoutElement) -> Rc<Self> {
        e.drawable.set_planner(Some(self.clone()));
        self.childs.borrow_mut().insert(id.to_string(), e);
        self.replan();

        self
    }

    pub fn add_child_inplace(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);
            self.replan();
        }
    }

    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() {
            // Ignore disabled children in metrics calculation
            if !el.drawable.is_enabled() {
                continue;
            }

            match el.drawable.get_vertical_plan() {
                Plan::Fit(h) => total_fit_h += h,
                Plan::FillPositive => fill_count += 1,
                Plan::FillNegative => 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 (FillPositive > FillNegative)
        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() {
                // Skip bound calculation and cursor advancement for disabled children
                if !el.drawable.is_enabled() {
                    continue;
                }

                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::FillPositive => fill_allocated_h,
                    Plan::FillNegative => none_allocated_h,
                };

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

                // 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() {
            // Ignore disabled children during the rendering pass
            if !el.drawable.is_enabled() {
                continue;
            }

            if let Err(e) = el.drawable.draw(canvas) {
                println!("ColumnLayout draw error at child {}: {:?}", key, e);
            }
        }

        Ok(())
    }

    fn get_horizontal_plan(&self) -> Plan {
        if let Some(plan) = *self.cached_h_plan.borrow() {
            return plan;
        }

        let mut max_w = 0.0f32;
        let mut has_fill_positive = false;

        for el in self.childs.borrow().values() {
            if !el.drawable.is_enabled() {
                continue;
            }

            match el.drawable.get_horizontal_plan() {
                Plan::FillPositive => {
                    has_fill_positive = true;
                    break; // Immediate escalation
                }
                Plan::Fit(w) => {
                    max_w = max_w.max(w);
                }
                _ => {}
            }
        }

        let plan = if has_fill_positive {
            Plan::FillPositive
        } else {
            Plan::Fit(max_w)
        };

        *self.cached_h_plan.borrow_mut() = Some(plan);
        plan
    }

    fn get_vertical_plan(&self) -> Plan {
        if let Some(plan) = *self.cached_v_plan.borrow() {
            return plan;
        }

        let mut total_h = 0.0f32;
        let mut has_fill_positive = false;

        for el in self.childs.borrow().values() {
            if !el.drawable.is_enabled() {
                continue;
            }

            match el.drawable.get_vertical_plan() {
                Plan::FillPositive => {
                    has_fill_positive = true;
                    break; // Immediate escalation
                }
                Plan::Fit(h) => {
                    total_h += h;
                }
                _ => {}
            }
        }

        let plan = if has_fill_positive {
            Plan::FillPositive
        } else {
            Plan::Fit(total_h)
        };

        *self.cached_v_plan.borrow_mut() = Some(plan);
        plan
    }

    fn replan(&self) {
        // Invalidate self cache
        *self.cached_h_plan.borrow_mut() = None;
        *self.cached_v_plan.borrow_mut() = None;

        // Cascade replan signal to all children
        for el in self.childs.borrow().values() {
            el.drawable.replan();
        }
    }

    fn try_to_interactive(&self) -> Option<&dyn Interactive> {
        Some(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 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))
    }

    fn as_layout(&self) -> &dyn Layout {
        self
    }

    fn foreach_child(&self, f: &mut dyn FnMut(&dyn Drawable)) {
        let ch = self.childs.borrow();
        for i in ch.iter() {
            f(i.1.drawable.as_ref());
        }
    }
}

impl Interactive for ColumnLayout {
    fn handle_and_route(&self, e: &Event, u: &dyn Any) {
        self.foreach_child(&mut |x| {
            // Check if the child is enabled before attempting to route events
            if x.is_enabled()
                && let Some(i) = x.try_to_interactive()
            {
                i.handle_and_route(e, u);
            }
        });
    }
}