fui-rs 0.2.6

Retained Rust UI for native desktop and WebAssembly applications
Documentation
use crate::bindings::ui;
use crate::ffi::{PositionType, Unit};
use crate::node::{flex_box, portal, FlexBox, Node, NodeHandle};
use crate::theme::current_theme;
use std::cell::RefCell;

const STANDARD_FOCUS_RING_WIDTH: f32 = 2.0;
const STANDARD_FOCUS_RING_OUTSET: f32 = 2.0;
const TRANSPARENT: u32 = 0x00000000;

#[derive(Clone, Copy)]
struct FocusAdornerStyle {
    top_left_radius: f32,
    top_right_radius: f32,
    bottom_right_radius: f32,
    bottom_left_radius: f32,
}

#[derive(Clone, Copy)]
struct FocusAdornerRect {
    x: f32,
    y: f32,
    width: f32,
    height: f32,
}

struct FocusAdornerState {
    host_root: Option<FlexBox>,
    ring_node: Option<FlexBox>,
    active_owner: Option<NodeHandle>,
    active_style: Option<FocusAdornerStyle>,
    attached: bool,
    last_host_x: f32,
    last_host_y: f32,
    last_host_width: f32,
    last_host_height: f32,
    last_ring_x: f32,
    last_ring_y: f32,
    last_ring_width: f32,
    last_ring_height: f32,
    last_color: u32,
    last_top_left_radius: f32,
    last_top_right_radius: f32,
    last_bottom_right_radius: f32,
    last_bottom_left_radius: f32,
}

impl FocusAdornerState {
    fn new() -> Self {
        Self {
            host_root: None,
            ring_node: None,
            active_owner: None,
            active_style: None,
            attached: false,
            last_host_x: f32::NAN,
            last_host_y: f32::NAN,
            last_host_width: f32::NAN,
            last_host_height: f32::NAN,
            last_ring_x: f32::NAN,
            last_ring_y: f32::NAN,
            last_ring_width: f32::NAN,
            last_ring_height: f32::NAN,
            last_color: 0,
            last_top_left_radius: f32::NAN,
            last_top_right_radius: f32::NAN,
            last_bottom_right_radius: f32::NAN,
            last_bottom_left_radius: f32::NAN,
        }
    }

    fn reset_cached_geometry(&mut self) {
        self.last_host_x = f32::NAN;
        self.last_host_y = f32::NAN;
        self.last_host_width = f32::NAN;
        self.last_host_height = f32::NAN;
        self.last_ring_x = f32::NAN;
        self.last_ring_y = f32::NAN;
        self.last_ring_width = f32::NAN;
        self.last_ring_height = f32::NAN;
        self.last_color = 0;
        self.last_top_left_radius = f32::NAN;
        self.last_top_right_radius = f32::NAN;
        self.last_bottom_right_radius = f32::NAN;
        self.last_bottom_left_radius = f32::NAN;
    }
}

thread_local! {
    static FOCUS_ADORNER_STATE: RefCell<FocusAdornerState> =
        RefCell::new(FocusAdornerState::new());
}

pub(crate) fn create_default_host() -> FlexBox {
    FOCUS_ADORNER_STATE.with(|slot| {
        let mut state = slot.borrow_mut();
        if let Some(existing_host) = state.host_root.as_ref() {
            return existing_host.clone();
        }
        let ring_node = flex_box();
        ring_node
            .position_type(PositionType::Absolute)
            .bg_color(TRANSPARENT)
            .border(STANDARD_FOCUS_RING_WIDTH, TRANSPARENT);
        let host_root = portal();
        host_root
            .position_type(PositionType::Absolute)
            .position(0.0, 0.0)
            .width(0.0, Unit::Pixel)
            .height(0.0, Unit::Pixel)
            .clip_to_bounds(false);
        state.host_root = Some(host_root.clone());
        state.ring_node = Some(ring_node);
        host_root
    })
}

pub(crate) fn clear() {
    hide();
    FOCUS_ADORNER_STATE.with(|slot| {
        let mut state = slot.borrow_mut();
        state.active_owner = None;
        state.active_style = None;
    });
}

pub(crate) fn show_standard<T: Node>(owner: &T, corner_radius: f32) {
    show_standard_corners(
        owner,
        corner_radius,
        corner_radius,
        corner_radius,
        corner_radius,
    );
}

pub(crate) fn show_standard_corners<T: Node>(owner: &T, tl: f32, tr: f32, br: f32, bl: f32) {
    FOCUS_ADORNER_STATE.with(|slot| {
        let mut state = slot.borrow_mut();
        state.active_owner = Some(owner.handle());
        state.active_style = Some(FocusAdornerStyle {
            top_left_radius: tl + STANDARD_FOCUS_RING_OUTSET,
            top_right_radius: tr + STANDARD_FOCUS_RING_OUTSET,
            bottom_right_radius: br + STANDARD_FOCUS_RING_OUTSET,
            bottom_left_radius: bl + STANDARD_FOCUS_RING_OUTSET,
        });
    });
    sync();
}

pub(crate) fn hide_owner<T: Node>(owner: &T) {
    hide_owner_handle(owner.handle());
}

pub(crate) fn handle_owner_destroyed(owner: NodeHandle) {
    hide_owner_handle(owner);
}

pub(crate) fn refresh_after_commit() -> bool {
    sync()
}

fn hide_owner_handle(owner: NodeHandle) {
    let should_hide = FOCUS_ADORNER_STATE.with(|slot| slot.borrow().active_owner == Some(owner));
    if !should_hide {
        return;
    }
    FOCUS_ADORNER_STATE.with(|slot| {
        let mut state = slot.borrow_mut();
        state.active_owner = None;
        state.active_style = None;
    });
    hide();
}

fn sync() -> bool {
    let owner = FOCUS_ADORNER_STATE.with(|slot| slot.borrow().active_owner);
    let style = FOCUS_ADORNER_STATE.with(|slot| slot.borrow().active_style);
    let (Some(owner), Some(style)) = (owner, style) else {
        return hide();
    };
    let (Some(host_root), Some(ring_node)) = FOCUS_ADORNER_STATE.with(|slot| {
        let state = slot.borrow();
        (state.host_root.clone(), state.ring_node.clone())
    }) else {
        return false;
    };
    if owner == NodeHandle::INVALID || host_root.handle() == NodeHandle::INVALID {
        return hide();
    }
    let Some(ring_rect) = resolve_ring_rect(owner) else {
        return hide();
    };
    let Some(visible_rect) = resolve_visible_rect(ring_rect) else {
        return hide();
    };
    let color = current_theme().colors.focus_ring;
    let mut changed = false;
    FOCUS_ADORNER_STATE.with(|slot| {
        let mut state = slot.borrow_mut();
        if !state.attached {
            host_root.child(&ring_node);
            state.attached = true;
            changed = true;
        }
        let relative_ring_x = ring_rect.x - visible_rect.x;
        let relative_ring_y = ring_rect.y - visible_rect.y;
        if visible_rect.x != state.last_host_x
            || visible_rect.y != state.last_host_y
            || visible_rect.width != state.last_host_width
            || visible_rect.height != state.last_host_height
            || relative_ring_x != state.last_ring_x
            || relative_ring_y != state.last_ring_y
            || ring_rect.width != state.last_ring_width
            || ring_rect.height != state.last_ring_height
        {
            host_root.position(visible_rect.x, visible_rect.y);
            host_root.width(visible_rect.width, Unit::Pixel);
            host_root.height(visible_rect.height, Unit::Pixel);
            ring_node.position(relative_ring_x, relative_ring_y);
            ring_node.width(ring_rect.width, Unit::Pixel);
            ring_node.height(ring_rect.height, Unit::Pixel);
            state.last_host_x = visible_rect.x;
            state.last_host_y = visible_rect.y;
            state.last_host_width = visible_rect.width;
            state.last_host_height = visible_rect.height;
            state.last_ring_x = relative_ring_x;
            state.last_ring_y = relative_ring_y;
            state.last_ring_width = ring_rect.width;
            state.last_ring_height = ring_rect.height;
            changed = true;
        }
        if color != state.last_color
            || style.top_left_radius != state.last_top_left_radius
            || style.top_right_radius != state.last_top_right_radius
            || style.bottom_right_radius != state.last_bottom_right_radius
            || style.bottom_left_radius != state.last_bottom_left_radius
        {
            ring_node.corners(
                style.top_left_radius,
                style.top_right_radius,
                style.bottom_right_radius,
                style.bottom_left_radius,
            );
            ring_node.border(STANDARD_FOCUS_RING_WIDTH, color);
            state.last_color = color;
            state.last_top_left_radius = style.top_left_radius;
            state.last_top_right_radius = style.top_right_radius;
            state.last_bottom_right_radius = style.bottom_right_radius;
            state.last_bottom_left_radius = style.bottom_left_radius;
            changed = true;
        }
    });
    changed
}

fn hide() -> bool {
    FOCUS_ADORNER_STATE.with(|slot| {
        let mut state = slot.borrow_mut();
        let (Some(host_root), Some(ring_node)) = (state.host_root.clone(), state.ring_node.clone())
        else {
            state.attached = false;
            state.reset_cached_geometry();
            return false;
        };
        if !state.attached {
            state.attached = false;
            state.reset_cached_geometry();
            return false;
        }
        host_root.remove_child(&ring_node);
        host_root.position(0.0, 0.0);
        host_root.width(0.0, Unit::Pixel);
        host_root.height(0.0, Unit::Pixel);
        state.attached = false;
        state.reset_cached_geometry();
        true
    })
}

fn resolve_ring_rect(owner: NodeHandle) -> Option<FocusAdornerRect> {
    let bounds = ui::get_bounds(owner.raw())?;
    Some(FocusAdornerRect {
        x: bounds[0] - STANDARD_FOCUS_RING_OUTSET,
        y: bounds[1] - STANDARD_FOCUS_RING_OUTSET,
        width: bounds[2] + (STANDARD_FOCUS_RING_OUTSET * 2.0),
        height: bounds[3] + (STANDARD_FOCUS_RING_OUTSET * 2.0),
    })
}

fn resolve_visible_rect(ring_rect: FocusAdornerRect) -> Option<FocusAdornerRect> {
    let min_x = ring_rect.x.max(0.0);
    let min_y = ring_rect.y.max(0.0);
    let max_x = (ring_rect.x + ring_rect.width).min(ui::get_viewport_width());
    let max_y = (ring_rect.y + ring_rect.height).min(ui::get_viewport_height());
    let width = max_x - min_x;
    let height = max_y - min_y;
    if width <= 0.0 || height <= 0.0 {
        return None;
    }
    Some(FocusAdornerRect {
        x: min_x,
        y: min_y,
        width,
        height,
    })
}