yuno 0.2.1

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, ClipOp, Rect};
use std::any::Any;
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::rc::Rc;
use std::sync::atomic::{AtomicUsize, Ordering};

#[derive(Clone)]
pub struct ListLayoutElement {
    pub drawable: Rc<dyn Drawable>,
    pub index: usize,
}

pub struct ListLayout {
    pub selected: AtomicUsize,

    pub item_height: f32,
    pub padding_x: f32,
    pub padding_top: f32,
    pub padding_bottom: f32,

    pub childs: RefCell<BTreeMap<String, ListLayoutElement>>,
    pub planner: RefCell<Option<Rc<dyn Layout>>>,

    enabled: RefCell<bool>,
}

impl HasPlanner for ListLayout {
    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 ListLayout {
    fn draw(&self, canvas: &Canvas) -> anyhow::Result<()> {
        let area = self.get_planned_drawing_area(canvas);

        let clip_rect = Rect::from_xywh(
            area.left + self.padding_x,
            area.top + self.padding_top,
            (area.width() - self.padding_x * 2.0).max(0.0),
            (area.height() - self.padding_top - self.padding_bottom).max(0.0),
        );

        if clip_rect.is_empty() {
            return Ok(());
        }

        canvas.save();
        canvas.clip_rect(clip_rect, ClipOp::Intersect, true);

        let childs = self.childs.borrow();

        // Phase 1: Filter only enabled children and sort them to maintain visual order
        let mut ordered_childs: Vec<_> = childs
            .values()
            .filter(|e| e.drawable.is_enabled())
            .collect();
        ordered_childs.sort_by_key(|e| e.index);

        // Phase 2: Execute drawing only for enabled elements
        for element in ordered_childs {
            let child_rect = self.measure_child_internal(&*element.drawable, &area);
            if clip_rect.intersects(child_rect)
                && let Err(e) = element.drawable.draw(canvas)
            {
                println!(
                    "ListLayout render error on child {}: {:?}",
                    element.index, e
                );
            }
        }

        canvas.restore();
        Ok(())
    }

    fn get_horizontal_plan(&self) -> Plan {
        Plan::FillPositive
    }
    fn get_vertical_plan(&self) -> Plan {
        Plan::FillPositive
    }
    fn replan(&self) {
        // 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 ListLayout {
    fn measure_child(&self, c: &dyn Drawable, canvas: &Canvas) -> Rect {
        let parent_area = self.get_planned_drawing_area(canvas);
        self.measure_child_internal(c, &parent_area)
    }

    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 ListLayout {
    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);
            }
        });
    }
}

impl ListLayout {
    pub fn new(
        item_height: f32,
        padding_x: f32,
        padding_top: f32,
        padding_bottom: f32,
        is_enabled: bool,
    ) -> Rc<Self> {
        Rc::new(Self {
            selected: AtomicUsize::new(0),
            item_height,
            padding_x,
            padding_top,
            padding_bottom,
            childs: RefCell::new(BTreeMap::new()),
            planner: RefCell::new(None),
            enabled: RefCell::new(is_enabled),
        })
    }

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

        self
    }

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

    fn measure_child_internal(&self, c: &dyn Drawable, parent_area: &Rect) -> Rect {
        let childs = self.childs.borrow();

        // Filter out disabled children and sort by index to calculate visual gaps correctly
        let mut enabled_childs: Vec<_> = childs
            .values()
            .filter(|e| e.drawable.is_enabled())
            .collect();
        enabled_childs.sort_by_key(|e| e.index);

        let mut target_visual_index = None;
        let total_items = enabled_childs.len();

        let c_ptr = c as *const dyn Drawable as *const ();

        // Find the specific child's actual visual rank among the enabled siblings
        for (visual_index, element) in enabled_childs.iter().enumerate() {
            let elem_ptr = &*element.drawable as *const dyn Drawable as *const ();
            if std::ptr::eq(c_ptr, elem_ptr) {
                target_visual_index = Some(visual_index);
                break;
            }
        }

        if let Some(visual_index) = target_visual_index {
            let visible_height =
                (parent_area.height() - self.padding_top - self.padding_bottom).max(0.0);

            // Total height depends on enabled items only
            let total_height = total_items as f32 * self.item_height;

            let selected = self.selected.load(Ordering::Relaxed) as f32;
            let target_scroll_y =
                (selected * self.item_height) - (visible_height / 2.0) + (self.item_height / 2.0);
            let max_scroll = (total_height - visible_height).max(0.0);
            let scroll_y = target_scroll_y.clamp(0.0, max_scroll);

            // Compute Y relative to visual index, collapsing gaps left by disabled elements
            let item_y = (visual_index as f32) * self.item_height;
            let absolute_top = parent_area.top + self.padding_top + item_y - scroll_y;

            Rect::from_xywh(
                parent_area.left + self.padding_x,
                absolute_top,
                (parent_area.width() - self.padding_x * 2.0).max(0.0),
                self.item_height,
            )
        } else {
            // Disabled or unfound controls get a 0-size rect
            Rect::new(0.0, 0.0, 0.0, 0.0)
        }
    }
}