fui-rs 0.2.3

Retained-mode Rust UI SDK for EffinDOM
Documentation
use crate::bindings::ui;
use crate::event;
use crate::node::{NodeHandle, NodeRef};
use std::cell::RefCell;

const BOUNDS_TOLERANCE: f32 = 0.5;

thread_local! {
    static SCROLL_VIEWS: RefCell<Vec<NodeHandle>> = const { RefCell::new(Vec::new()) };
    static SELECTED_SCROLL_VIEW: RefCell<Option<NodeHandle>> = const { RefCell::new(None) };
    static SELECTED_BRANCH_ROOT: RefCell<Option<NodeHandle>> = const { RefCell::new(None) };
}

fn get_bounds(handle: NodeHandle) -> Option<[f32; 4]> {
    ui::get_bounds(handle.raw())
}

fn is_visible(bounds: [f32; 4]) -> bool {
    let viewport_width = ui::get_viewport_width();
    let viewport_height = ui::get_viewport_height();
    bounds[0] + bounds[2] > 0.0
        && bounds[1] + bounds[3] > 0.0
        && bounds[0] < viewport_width
        && bounds[1] < viewport_height
}

fn get_usable_visible_bounds(handle: NodeHandle) -> Option<[f32; 4]> {
    let bounds = get_bounds(handle)?;
    if bounds[2] <= BOUNDS_TOLERANCE || bounds[3] <= BOUNDS_TOLERANCE || !is_visible(bounds) {
        return None;
    }
    Some(bounds)
}

fn contains_point(bounds: [f32; 4], x: f32, y: f32) -> bool {
    x >= bounds[0] && x <= bounds[0] + bounds[2] && y >= bounds[1] && y <= bounds[1] + bounds[3]
}

fn area(bounds: [f32; 4]) -> f32 {
    bounds[2] * bounds[3]
}

fn distance_squared(bounds: [f32; 4], x: f32, y: f32) -> f32 {
    let delta_x = if x < bounds[0] {
        bounds[0] - x
    } else {
        let right = bounds[0] + bounds[2];
        if x > right {
            x - right
        } else {
            0.0
        }
    };
    let delta_y = if y < bounds[1] {
        bounds[1] - y
    } else {
        let bottom = bounds[1] + bounds[3];
        if y > bottom {
            y - bottom
        } else {
            0.0
        }
    };
    (delta_x * delta_x) + (delta_y * delta_y)
}

fn is_better_point_candidate(
    candidate_bounds: [f32; 4],
    candidate_contains_point: bool,
    best_bounds: [f32; 4],
    best_contains_point: bool,
    point_x: f32,
    point_y: f32,
) -> bool {
    if candidate_contains_point != best_contains_point {
        return candidate_contains_point;
    }
    if candidate_contains_point {
        let candidate_area = area(candidate_bounds);
        let best_area = area(best_bounds);
        if candidate_area + BOUNDS_TOLERANCE < best_area {
            return true;
        }
        if best_area + BOUNDS_TOLERANCE < candidate_area {
            return false;
        }
    } else {
        let candidate_distance = distance_squared(candidate_bounds, point_x, point_y);
        let best_distance = distance_squared(best_bounds, point_x, point_y);
        if candidate_distance + BOUNDS_TOLERANCE < best_distance {
            return true;
        }
        if best_distance + BOUNDS_TOLERANCE < candidate_distance {
            return false;
        }
    }
    if candidate_bounds[1] + BOUNDS_TOLERANCE < best_bounds[1] {
        return true;
    }
    if best_bounds[1] + BOUNDS_TOLERANCE < candidate_bounds[1] {
        return false;
    }
    candidate_bounds[0] < best_bounds[0]
}

fn is_better_default_candidate(candidate_bounds: [f32; 4], best_bounds: [f32; 4]) -> bool {
    if candidate_bounds[1] + BOUNDS_TOLERANCE < best_bounds[1] {
        return true;
    }
    if best_bounds[1] + BOUNDS_TOLERANCE < candidate_bounds[1] {
        return false;
    }
    candidate_bounds[0] < best_bounds[0]
}

fn is_descendant_of(node: NodeRef, ancestor: NodeRef) -> bool {
    let mut current = Some(node);
    while let Some(node) = current {
        if node.handle() == ancestor.handle() {
            return true;
        }
        current = node.parent();
    }
    false
}

fn default_ordering_anchor(scroll_view: NodeRef) -> NodeRef {
    let mut anchor = scroll_view.clone();
    let mut cursor = scroll_view.parent();
    while let Some(current) = cursor {
        if current.is_scroll_view_for_routing() {
            break;
        }
        anchor = current.clone();
        cursor = current.parent();
    }
    anchor
}

fn append_unique_scroll_view(target: &mut Vec<NodeHandle>, handle: NodeHandle) {
    if target.contains(&handle) || get_usable_visible_bounds(handle).is_none() {
        return;
    }
    SCROLL_VIEWS.with(|scroll_views| {
        if scroll_views.borrow().contains(&handle) {
            target.push(handle);
        }
    });
}

fn append_default_ordered_candidate(target: &mut Vec<NodeHandle>, candidate: NodeHandle) {
    let candidate_bounds = match get_usable_visible_bounds(candidate) {
        Some(bounds) => bounds,
        None => return,
    };
    let Some(candidate_node) = event::resolve_node(candidate) else {
        return;
    };
    let candidate_anchor_bounds =
        match get_usable_visible_bounds(default_ordering_anchor(candidate_node).handle()) {
            Some(bounds) => bounds,
            None => return,
        };
    let mut insert_index = target.len();
    while insert_index > 0 {
        let current = target[insert_index - 1];
        let Some(current_node) = event::resolve_node(current) else {
            insert_index -= 1;
            continue;
        };
        let Some(current_anchor_bounds) =
            get_usable_visible_bounds(default_ordering_anchor(current_node).handle())
        else {
            insert_index -= 1;
            continue;
        };
        let Some(current_candidate_bounds) = get_usable_visible_bounds(current) else {
            insert_index -= 1;
            continue;
        };
        if is_better_default_candidate(candidate_anchor_bounds, current_anchor_bounds) {
            insert_index -= 1;
            continue;
        }
        if candidate_anchor_bounds == current_anchor_bounds
            && is_better_default_candidate(candidate_bounds, current_candidate_bounds)
        {
            insert_index -= 1;
            continue;
        }
        break;
    }
    target.push(candidate);
    for cursor in (insert_index + 1..target.len()).rev() {
        target[cursor] = target[cursor - 1];
    }
    target[insert_index] = candidate;
}

fn select_default_candidates_within(root: Option<NodeRef>) -> Vec<NodeHandle> {
    let mut ordered = Vec::new();
    let Some(root) = root else {
        return ordered;
    };
    SCROLL_VIEWS.with(|scroll_views| {
        for handle in scroll_views.borrow().iter().copied() {
            let Some(candidate) = event::resolve_node(handle) else {
                continue;
            };
            if !is_descendant_of(candidate, root.clone()) {
                continue;
            }
            append_default_ordered_candidate(&mut ordered, handle);
        }
    });
    ordered
}

fn find_nearest_descendant_scroll_branch(node: Option<NodeRef>) -> Option<NodeHandle> {
    let mut current = node;
    while let Some(node) = current {
        if node.is_scroll_view_for_routing() {
            return None;
        }
        if !select_default_candidates_within(Some(node.clone())).is_empty() {
            return Some(node.handle());
        }
        current = node.parent();
    }
    None
}

fn select_scroll_view_by_point(point_x: f32, point_y: f32) -> Option<NodeHandle> {
    let mut best_view = None;
    let mut best_bounds = None;
    let mut best_contains_point = false;
    SCROLL_VIEWS.with(|scroll_views| {
        for handle in scroll_views.borrow().iter().copied() {
            let Some(candidate_bounds) = get_usable_visible_bounds(handle) else {
                continue;
            };
            let candidate_contains_point = contains_point(candidate_bounds, point_x, point_y);
            if best_bounds.is_none()
                || is_better_point_candidate(
                    candidate_bounds,
                    candidate_contains_point,
                    best_bounds.unwrap_or([0.0; 4]),
                    best_contains_point,
                    point_x,
                    point_y,
                )
            {
                best_view = Some(handle);
                best_bounds = Some(candidate_bounds);
                best_contains_point = candidate_contains_point;
            }
        }
    });
    best_view
}

fn resolve_ancestor_scroll_view(node: Option<NodeRef>) -> Option<NodeHandle> {
    let mut current = node;
    while let Some(node) = current {
        if node.is_scroll_view_for_routing() {
            return Some(node.handle());
        }
        current = node.parent();
    }
    None
}

fn append_ancestor_scroll_view_fallbacks(view: Option<NodeHandle>, target: &mut Vec<NodeHandle>) {
    let Some(view) = view else {
        return;
    };
    let Some(view_node) = event::resolve_node(view) else {
        return;
    };
    let mut current = view_node.parent();
    while let Some(node) = current {
        if node.is_scroll_view_for_routing() {
            append_unique_scroll_view(target, node.handle());
        }
        current = node.parent();
    }
}

fn select_default_candidates() -> Vec<NodeHandle> {
    let mut ordered = Vec::new();
    SCROLL_VIEWS.with(|scroll_views| {
        for handle in scroll_views.borrow().iter().copied() {
            append_default_ordered_candidate(&mut ordered, handle);
        }
    });
    ordered
}

pub(crate) fn register_keyboard_scroll_node(node: &NodeRef) {
    if !node.is_scroll_view_for_routing() {
        return;
    }
    SCROLL_VIEWS.with(|scroll_views| {
        let mut scroll_views = scroll_views.borrow_mut();
        if !scroll_views.contains(&node.handle()) {
            scroll_views.push(node.handle());
        }
    });
}

pub(crate) fn unregister_keyboard_scroll_node(handle: NodeHandle) {
    SCROLL_VIEWS.with(|scroll_views| {
        scroll_views
            .borrow_mut()
            .retain(|candidate| *candidate != handle);
    });
    SELECTED_SCROLL_VIEW.with(|selected| {
        if *selected.borrow() == Some(handle) {
            selected.replace(None);
        }
    });
}

pub(crate) fn track_keyboard_scroll_pointer_up(target_node: Option<NodeRef>, x: f32, y: f32) {
    let selected_branch_root = target_node.clone().and_then(|node| {
        if node.is_scroll_view_for_routing() {
            Some(node.handle())
        } else {
            find_nearest_descendant_scroll_branch(Some(node))
        }
    });
    SELECTED_BRANCH_ROOT.with(|branch| branch.replace(selected_branch_root));

    if let Some(ancestor_scroll_view) = resolve_ancestor_scroll_view(target_node.clone()) {
        SELECTED_BRANCH_ROOT.with(|branch| {
            if branch.borrow().is_none() {
                branch.replace(Some(ancestor_scroll_view));
            }
        });
        SELECTED_SCROLL_VIEW.with(|selected| selected.replace(Some(ancestor_scroll_view)));
        return;
    }

    if let Some(branch_root) = SELECTED_BRANCH_ROOT.with(|branch| *branch.borrow()) {
        let branch_candidates = select_default_candidates_within(event::resolve_node(branch_root));
        if let Some(candidate) = branch_candidates.first().copied() {
            SELECTED_SCROLL_VIEW.with(|selected| selected.replace(Some(candidate)));
            return;
        }
    }

    let selected = select_scroll_view_by_point(x, y);
    SELECTED_SCROLL_VIEW.with(|slot| slot.replace(selected));
    if selected.is_none() {
        SELECTED_BRANCH_ROOT.with(|branch| branch.replace(None));
    }
}

pub(crate) fn get_keyboard_scroll_selected_candidate() -> Option<NodeHandle> {
    SELECTED_SCROLL_VIEW.with(|selected| *selected.borrow())
}

pub(crate) fn get_keyboard_scroll_fallback_candidates() -> Vec<NodeHandle> {
    let mut ordered = Vec::new();
    append_ancestor_scroll_view_fallbacks(get_keyboard_scroll_selected_candidate(), &mut ordered);
    if let Some(branch_root) = SELECTED_BRANCH_ROOT.with(|branch| *branch.borrow()) {
        for candidate in select_default_candidates_within(event::resolve_node(branch_root)) {
            if Some(candidate) != get_keyboard_scroll_selected_candidate() {
                append_unique_scroll_view(&mut ordered, candidate);
            }
        }
    }
    for candidate in select_default_candidates() {
        if Some(candidate) != get_keyboard_scroll_selected_candidate() {
            append_unique_scroll_view(&mut ordered, candidate);
        }
    }
    ordered
}

pub(crate) fn reset_keyboard_scroll_tracking() {
    SCROLL_VIEWS.with(|scroll_views| scroll_views.borrow_mut().clear());
    SELECTED_SCROLL_VIEW.with(|selected| selected.replace(None));
    SELECTED_BRANCH_ROOT.with(|branch| branch.replace(None));
}