yuno 0.1.0

A declarative UI layout and rendering framework powered by Skia.
#[allow(deprecated)]
use skia_safe::gradient_shader::GradientShaderColors;

#[allow(deprecated)]
use skia_safe::{
    Canvas, Color, Color4f, Font, Paint, Path, PathBuilder, Point, RRect, Rect, TileMode,
    gradient_shader,
};
use std::cell::RefCell;
use std::rc::Rc;

use crate::drawing::{Drawable, Plan};
use crate::layouts::{EdgeLayoutElement, Edges, HasPlanner, Layout};
use crate::story_board::{StoryBoard, easing};

pub struct VolumeBar {
    // Layout binding
    planner: RefCell<Option<Rc<dyn Layout>>>,

    font: Font,
    vol_state: RefCell<StoryBoard>,
    volume: RefCell<f32>,

    bg_paint: Paint,
    border_paint: Paint,
    bar_rect: RRect,

    icon_paint: Paint,
    speaker_path: Path,

    wave_paint: Paint,
    wave_rect: Rect,

    bg_track: Paint,
    bg_track_rrect: RRect,

    thumb_paint: Paint,
    text_paint: Paint,
}

impl HasPlanner for VolumeBar {
    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 VolumeBar {
    fn get_horizontal_plan(&self) -> Plan {
        Plan::Fit(46.0)
    }

    fn get_vertical_plan(&self) -> Plan {
        Plan::Fit(220.0)
    }

    fn draw(&self, canvas: &Canvas) -> anyhow::Result<()> {
        let slide_offset = {
            let vol_state = self.vol_state.borrow();
            f64::from(&*vol_state) as f32
        };

        // If the bar has slid completely out of view, cull the draw call
        if slide_offset >= 119.0 {
            return Ok(());
        }

        // Fetch the definitive drawing area assigned by the parent layout
        let rect = self.get_planned_drawing_area(canvas);

        let bar_w = 46.0;
        let bar_h = 220.0;

        // Isolate canvas state.
        // We translate the canvas origin to the layout-provided Rect top-left corner,
        // PLUS our internal animation offset. This allows the internal drawing logic
        // to continue using (0, 0) as its local origin.
        canvas.save();
        canvas.translate(Point::new(rect.left + slide_offset, rect.top));

        canvas.draw_rrect(self.bar_rect, &self.bg_paint);
        canvas.draw_rrect(self.bar_rect, &self.border_paint);

        canvas.draw_path(&self.speaker_path, &self.icon_paint);

        let current_volume = *self.volume.borrow();

        if current_volume > 0.01 {
            canvas.draw_arc(self.wave_rect, -45.0, 90.0, false, &self.wave_paint);
        }

        let track_w = 6.0;
        let track_x = (bar_w - track_w) / 2.0;
        let track_y_start = 50.0;
        let track_y_end = bar_h - 40.0;
        let track_h = track_y_end - track_y_start;

        canvas.draw_rrect(self.bg_track_rrect, &self.bg_track);

        if current_volume > 0.0 {
            let fill_h = track_h * current_volume.clamp(0.0, 1.0);
            let fill_y = track_y_end - fill_h;

            let colors = [
                Color::from_argb(255, 90, 160, 255),
                Color::from_argb(255, 66, 133, 244),
            ];

            #[allow(deprecated)]
            let shader = gradient_shader::linear(
                (Point::new(0.0, fill_y), Point::new(0.0, track_y_end)),
                GradientShaderColors::Colors(&colors),
                None,
                TileMode::Clamp,
                None,
                None,
            );

            let mut active_track = Paint::default();
            active_track.set_anti_alias(true);
            if let Some(s) = shader {
                active_track.set_shader(s);
            } else {
                active_track.set_color4f(Color4f::new(0.26, 0.52, 0.96, 1.0), None);
            }

            canvas.draw_rrect(
                RRect::new_rect_xy(
                    Rect::from_xywh(track_x, fill_y, track_w, fill_h),
                    track_w / 2.0,
                    track_w / 2.0,
                ),
                &active_track,
            );

            canvas.draw_circle(Point::new(bar_w / 2.0, fill_y), 6.0, &self.thumb_paint);
        }

        let vol_percent = (current_volume.clamp(0.0, 1.0) * 100.0).round() as i32;
        let text = format!("{}", vol_percent);
        let (text_width, _) = self.font.measure_str(&text, Some(&self.text_paint));

        let text_x = (bar_w - text_width) / 2.0;
        let text_y = bar_h - 16.0;
        canvas.draw_str(
            &text,
            Point::new(text_x, text_y),
            &self.font,
            &self.text_paint,
        );

        // Restore canvas state to avoid polluting subsequent draw calls from other elements
        canvas.restore();

        Ok(())
    }
}

impl VolumeBar {
    pub fn new(font: &Font, volume: f32) -> Rc<Self> {
        let bar_w = 46.0;
        let bar_h = 220.0;

        let mut bg_paint = Paint::default();
        bg_paint
            .set_anti_alias(true)
            .set_color4f(Color4f::new(0.08, 0.08, 0.1, 0.75), None);

        let mut border_paint = Paint::default();
        border_paint
            .set_anti_alias(true)
            .set_style(skia_safe::paint::Style::Stroke)
            .set_stroke_width(1.5)
            .set_color4f(Color4f::new(1.0, 1.0, 1.0, 0.12), None);

        let bar_rect = RRect::new_rect_xy(Rect::from_xywh(0.0, 0.0, bar_w, bar_h), 14.0, 14.0);

        let icon_y = 28.0;
        let icon_center_x = bar_w / 2.0;

        let mut icon_paint = Paint::default();
        icon_paint
            .set_anti_alias(true)
            .set_color4f(Color4f::new(0.65, 0.65, 0.7, 1.0), None);

        let mut builder = PathBuilder::new();
        builder.move_to(Point::new(icon_center_x - 4.0, icon_y - 3.0));
        builder.line_to(Point::new(icon_center_x, icon_y - 3.0));
        builder.line_to(Point::new(icon_center_x + 5.0, icon_y - 7.0));
        builder.line_to(Point::new(icon_center_x + 5.0, icon_y + 7.0));
        builder.line_to(Point::new(icon_center_x, icon_y + 3.0));
        builder.line_to(Point::new(icon_center_x - 4.0, icon_y + 3.0));
        builder.close();
        let speaker_path = builder.detach();

        let mut wave_paint = Paint::default();
        wave_paint
            .set_anti_alias(true)
            .set_style(skia_safe::paint::Style::Stroke)
            .set_stroke_width(1.5)
            .set_color4f(Color4f::new(0.65, 0.65, 0.7, 1.0), None);

        let wave_rect = Rect::from_xywh(icon_center_x - 4.0, icon_y - 5.0, 14.0, 10.0);

        let track_w = 6.0;
        let track_x = (bar_w - track_w) / 2.0;
        let track_y_start = 50.0;
        let track_h = (bar_h - 40.0) - track_y_start;

        let mut bg_track = Paint::default();
        bg_track
            .set_anti_alias(true)
            .set_color4f(Color4f::new(1.0, 1.0, 1.0, 0.2), None);

        let bg_track_rrect = RRect::new_rect_xy(
            Rect::from_xywh(track_x, track_y_start, track_w, track_h),
            track_w / 2.0,
            track_w / 2.0,
        );

        let mut thumb_paint = Paint::default();
        thumb_paint
            .set_anti_alias(true)
            .set_color4f(Color4f::new(1.0, 1.0, 1.0, 1.0), None);

        let mut text_font = font.clone();
        text_font.set_size(13.0);

        let mut text_paint = Paint::default();
        text_paint
            .set_anti_alias(true)
            .set_color4f(Color4f::new(0.95, 0.95, 0.95, 1.0), None);

        Rc::new(Self {
            planner: RefCell::new(None),
            font: text_font,
            vol_state: RefCell::new(StoryBoard::init(
                120f64,
                0f64,
                3500f64,
                false,
                easing::cubic_fly_hover_out,
            )),
            volume: RefCell::new(volume),
            bg_paint,
            border_paint,
            bar_rect,
            icon_paint,
            speaker_path,
            wave_paint,
            wave_rect,
            bg_track,
            bg_track_rrect,
            thumb_paint,
            text_paint,
        })
    }

    /// Adapts the control to the new EdgeLayout constraint semantics.
    /// Aligns the VolumeBar to the right edge with a margin, mapping all 4 constraints.
    pub fn to_layout_element(self: &Rc<Self>) -> EdgeLayoutElement {
        let width = match self.get_horizontal_plan() {
            Plan::Fit(w) => w,
            _ => panic!("Invalid Argument"),
        };
        let height = match self.get_vertical_plan() {
            Plan::Fit(h) => h,
            _ => panic!("Invalid Argument"),
        };

        let right_margin = 70.0;
        // As a default example, we peg the top to arbitrary 300.0 downwards.
        // In practice, an outside planner or the user handles precise constraint mapping.
        let top_margin = 300.0;

        EdgeLayoutElement {
            drawable: self.clone(),
            // Lock Left to Right edge to preserve fixed width scaling constraints
            l_edge: Edges::Right,
            l_margin: right_margin + width,
            t_edge: Edges::Top,
            t_margin: top_margin,
            r_edge: Edges::Right,
            r_margin: right_margin,
            b_edge: Edges::Top,
            b_margin: top_margin + height,
        }
    }

    pub fn set_vol(&self, v: f32) {
        *self.volume.borrow_mut() = v;
    }

    pub fn show(&self) {
        let mut vol_state = self.vol_state.borrow_mut();
        let ui_vol_p = vol_state.get_progress();
        if ui_vol_p > 0.12 && ui_vol_p < 0.88 {
            vol_state.seek(0.15);
        } else if ui_vol_p > 0.88 {
            vol_state.restart();
        }
    }
}