fui-rs 0.2.6

Retained Rust UI for native desktop and WebAssembly applications
Documentation
use super::*;
use crate::node::WeakFlexBox;
use crate::Border;

#[derive(Clone, Copy)]
struct ResolvedProgressBarSizing {
    length: f32,
    thickness: f32,
}

fn resolve_sizing(sizing: Option<ProgressBarSizing>) -> ResolvedProgressBarSizing {
    let sizing = sizing.unwrap_or_default();
    ResolvedProgressBarSizing {
        length: if sizing.has_length() {
            sizing.length_px()
        } else {
            PROGRESS_LENGTH
        },
        thickness: if sizing.has_thickness() {
            sizing.thickness_px()
        } else {
            PROGRESS_THICKNESS
        },
    }
}

#[derive(Clone)]
pub struct ProgressBar {
    root: FlexBox,
    fill: FlexBox,
    min: Rc<Cell<f32>>,
    max: Rc<Cell<f32>>,
    value: Rc<Cell<f32>>,
    orientation: Rc<Cell<Orientation>>,
    sizing_value: Rc<Cell<Option<ProgressBarSizing>>>,
    colors_value: Rc<Cell<Option<ProgressBarColors>>>,
    weak_root: WeakFlexBox,
    weak_fill: WeakFlexBox,
}

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

impl ProgressBar {
    pub fn new() -> Self {
        let root = flex_box();
        let fill = flex_box();
        root.semantic_orientation(Orientation::Horizontal)
            .semantic_value_range(0.0, 0.0, 100.0)
            .child(&fill);
        let control = Self {
            weak_root: root.downgrade(),
            weak_fill: fill.downgrade(),
            root,
            fill,
            min: Rc::new(Cell::new(0.0)),
            max: Rc::new(Cell::new(100.0)),
            value: Rc::new(Cell::new(0.0)),
            orientation: Rc::new(Cell::new(Orientation::Horizontal)),
            sizing_value: Rc::new(Cell::new(None)),
            colors_value: Rc::new(Cell::new(None)),
        };
        control.install_visual_subscriptions();
        control.set_value(0.0);
        control.handle_theme_changed();
        control
    }

    pub fn min(&self, value: f32) -> &Self {
        self.min.set(value);
        if self.max.get() < value {
            self.max.set(value);
        }
        self.set_value(self.value.get());
        self
    }

    pub fn max(&self, value: f32) -> &Self {
        self.max.set(value);
        if self.min.get() > value {
            self.min.set(value);
        }
        self.set_value(self.value.get());
        self
    }

    pub fn value(&self, value: f32) -> &Self {
        self.set_value(value);
        self
    }

    pub fn length(&self, value: f32) -> &Self {
        let value = clamp_direct_dimension("length", value);
        let current = self.sizing_value.get().unwrap_or_default();
        let mut sizing = ProgressBarSizing::new().length(value);
        if current.has_thickness() {
            sizing = sizing.thickness(current.thickness_px());
        }
        self.sizing_value.set(Some(sizing));
        self.sync_geometry();
        self
    }

    pub fn thickness(&self, value: f32) -> &Self {
        let value = clamp_direct_dimension("thickness", value);
        let current = self.sizing_value.get().unwrap_or_default();
        let mut sizing = ProgressBarSizing::new().thickness(value);
        if current.has_length() {
            sizing = sizing.length(current.length_px());
        }
        self.sizing_value.set(Some(sizing));
        self.sync_geometry();
        self.sync_visual_state();
        self
    }

    pub fn orientation(&self, orientation: Orientation) -> &Self {
        let orientation = if orientation == Orientation::Vertical {
            Orientation::Vertical
        } else {
            Orientation::Horizontal
        };
        self.orientation.set(orientation);
        self.root.semantic_orientation(orientation);
        self.sync_geometry();
        self
    }

    pub fn sizing(&self, sizing: ProgressBarSizing) -> &Self {
        self.sizing_value.set(Some(sizing));
        self.sync_geometry();
        self.sync_visual_state();
        self
    }

    pub fn clear_sizing(&self) -> &Self {
        self.sizing_value.set(None);
        self.sync_geometry();
        self.sync_visual_state();
        self
    }

    pub fn colors(&self, colors: ProgressBarColors) -> &Self {
        self.colors_value.set(Some(colors));
        self.sync_visual_state();
        self
    }

    pub fn clear_colors(&self) -> &Self {
        self.colors_value.set(None);
        self.sync_visual_state();
        self
    }

    pub fn corner_radius(&self, radius: f32) -> &Self {
        self.root.corner_radius(radius);
        self.fill.corner_radius(radius);
        self
    }

    pub fn current_value(&self) -> f32 {
        self.value.get()
    }

    fn install_visual_subscriptions(&self) {
        let weak_root = self.weak_root.clone();
        let weak_fill = self.weak_fill.clone();
        let sizing_value = self.sizing_value.clone();
        let colors_value = self.colors_value.clone();
        let guard = subscribe(move |_theme| {
            let Some(root) = weak_root.upgrade() else {
                return;
            };
            let Some(fill) = weak_fill.upgrade() else {
                return;
            };
            sync_progress_visual_state(
                &root,
                &fill,
                resolve_sizing(sizing_value.get()),
                colors_value.get(),
            );
        });
        self.root
            .retained_node_ref()
            .retain_attachment(Rc::new(guard));
    }

    fn handle_theme_changed(&self) {
        self.sync_visual_state();
        self.sync_geometry();
    }

    fn set_value(&self, value: f32) {
        self.value.set(value.clamp(self.min.get(), self.max.get()));
        self.sync_geometry();
    }

    fn sync_geometry(&self) {
        sync_progress_geometry(
            &self.root,
            &self.fill,
            self.min.get(),
            self.max.get(),
            self.value.get(),
            self.orientation.get(),
            resolve_sizing(self.sizing_value.get()),
        );
    }

    fn sync_visual_state(&self) {
        sync_progress_visual_state(
            &self.root,
            &self.fill,
            resolve_sizing(self.sizing_value.get()),
            self.colors_value.get(),
        );
    }
}

#[derive(Clone)]
struct ProgressBarThemeTarget {
    root: WeakFlexBox,
    fill: WeakFlexBox,
    min: Rc<Cell<f32>>,
    max: Rc<Cell<f32>>,
    value: Rc<Cell<f32>>,
    orientation: Rc<Cell<Orientation>>,
    sizing_value: Rc<Cell<Option<ProgressBarSizing>>>,
    colors_value: Rc<Cell<Option<ProgressBarColors>>>,
}

impl ProgressBarThemeTarget {
    fn upgrade(&self) -> Option<ProgressBar> {
        Some(ProgressBar {
            root: self.root.upgrade()?,
            fill: self.fill.upgrade()?,
            min: self.min.clone(),
            max: self.max.clone(),
            value: self.value.clone(),
            orientation: self.orientation.clone(),
            sizing_value: self.sizing_value.clone(),
            colors_value: self.colors_value.clone(),
            weak_root: self.root.clone(),
            weak_fill: self.fill.clone(),
        })
    }
}

fn clamp_direct_dimension(method: &str, value: f32) -> f32 {
    if value > 0.0 {
        return value;
    }
    crate::logger::warn(
        "Layout",
        &format!("ProgressBar.{method}() received {value}; clamping to 1.0."),
    );
    1.0
}

impl Node for ProgressBar {
    fn retained_node_ref(&self) -> NodeRef {
        self.root.retained_node_ref()
    }

    fn build_self(&self) {
        self.sync_visual_state();
        self.sync_geometry();
        self.root.build_self();
    }
}

impl HasFlexBoxRoot for ProgressBar {
    fn flex_box_root(&self) -> &FlexBox {
        &self.root
    }
}

impl ThemeBindable for ProgressBar {
    fn theme_binding_node(&self) -> NodeRef {
        self.root.retained_node_ref()
    }

    fn weak_theme_target(&self) -> Box<dyn Fn() -> Option<Self>> {
        let target = ProgressBarThemeTarget {
            root: self.weak_root.clone(),
            fill: self.weak_fill.clone(),
            min: self.min.clone(),
            max: self.max.clone(),
            value: self.value.clone(),
            orientation: self.orientation.clone(),
            sizing_value: self.sizing_value.clone(),
            colors_value: self.colors_value.clone(),
        };
        Box::new(move || target.upgrade())
    }
}

fn sync_progress_geometry(
    root: &FlexBox,
    fill: &FlexBox,
    min: f32,
    max: f32,
    current_value: f32,
    orientation: Orientation,
    sizing: ResolvedProgressBarSizing,
) {
    let range = max - min;
    let fraction = if range > 0.0 {
        ((current_value - min) / range).clamp(0.0, 1.0)
    } else {
        0.0
    };
    let fill_length = sizing.length * fraction;
    if orientation == Orientation::Vertical {
        root.flex_direction(FlexDirection::Column)
            .justify_content(JustifyContent::End)
            .width(sizing.thickness, Unit::Pixel)
            .height(sizing.length, Unit::Pixel);
        fill.width(sizing.thickness, Unit::Pixel)
            .height(fill_length, Unit::Pixel);
    } else {
        root.flex_direction(FlexDirection::Row)
            .justify_content(JustifyContent::Start)
            .width(sizing.length, Unit::Pixel)
            .height(sizing.thickness, Unit::Pixel);
        fill.width(fill_length, Unit::Pixel)
            .height(sizing.thickness, Unit::Pixel);
    }
    root.semantic_value_range(current_value, min, max)
        .default_semantic_label(format!(
            "Progress bar, value {}, range {} to {}",
            current_value, min, max
        ));
}

fn sync_progress_visual_state(
    root: &FlexBox,
    fill: &FlexBox,
    sizing: ResolvedProgressBarSizing,
    colors: Option<ProgressBarColors>,
) {
    let theme = current_theme();
    let track_color = colors
        .filter(ProgressBarColors::has_track)
        .map(|colors| colors.track_color())
        .unwrap_or(theme.colors.scrollbar_track);
    let fill_color = colors
        .filter(ProgressBarColors::has_fill)
        .map(|colors| colors.fill_color())
        .unwrap_or(theme.colors.accent);
    root.apply_presenter_style(
        crate::PresenterHostStyle::new()
            .background(track_color)
            .border(Border::solid(1.0, theme.colors.border))
            .corners(crate::Corners::all(sizing.thickness * 0.5)),
    );
    let corners = root
        .resolved_host_style()
        .corners
        .unwrap_or_else(|| crate::Corners::all(sizing.thickness * 0.5));
    fill.corners(
        corners.top_left,
        corners.top_right,
        corners.bottom_right,
        corners.bottom_left,
    )
    .bg_color(fill_color);
}