fui-rs 0.2.3-alpha2

Retained-mode Rust UI SDK for EffinDOM
Documentation
use super::*;
use crate::logger;
use std::cell::{Cell, RefCell};
use std::rc::{Rc, Weak};

#[derive(Clone)]
pub(crate) struct RadioGroupEventTarget {
    radios: Rc<RefCell<Vec<RadioButton>>>,
    selected_index: Rc<Cell<i32>>,
    changed: Rc<RefCell<Option<RadioGroupChangedCallback>>>,
}

impl RadioGroupEventTarget {
    pub(crate) fn select_radio_handle(&self, radio_handle: u64, focus: bool) {
        let index = self.index_of_radio_handle(radio_handle);
        if index >= 0 {
            self.select_index_internal(index, focus, true, true);
        }
    }

    pub(crate) fn move_selection_from_handle(&self, radio_handle: u64, delta: i32) {
        let start_index = self.index_of_radio_handle(radio_handle);
        if start_index < 0 || self.radios.borrow().is_empty() {
            return;
        }
        let next_index = self.find_enabled_index(start_index, delta);
        if next_index >= 0 {
            self.select_index_internal(next_index, true, true, true);
        }
    }

    pub(crate) fn select_first_enabled(&self, focus: bool) {
        let next_index = self.find_boundary_index(true);
        if next_index >= 0 {
            self.select_index_internal(next_index, focus, true, true);
        }
    }

    pub(crate) fn select_last_enabled(&self, focus: bool) {
        let next_index = self.find_boundary_index(false);
        if next_index >= 0 {
            self.select_index_internal(next_index, focus, true, true);
        }
    }

    pub(crate) fn select_index(&self, index: i32) {
        if index == -1 {
            let previous_index = self.selected_index.get();
            let changed = previous_index != -1;
            if let Some(previous) = self.radio_at(previous_index) {
                previous.update_checked(false, true, false);
            }
            self.selected_index.set(-1);
            if changed {
                self.emit_changed(String::new());
            }
            return;
        }
        let radio_count = self.radios.borrow().len() as i32;
        if radio_count == 0 {
            if index != -1 {
                logger::warn(
                    "Layout",
                    &format!(
                        "RadioGroup.select_index() received {index} before any radios were added."
                    ),
                );
            }
            return;
        }
        let clamped_index = if index < 0 {
            0
        } else if index >= radio_count {
            radio_count - 1
        } else {
            index
        };
        if clamped_index != index {
            logger::warn(
                "Layout",
                &format!(
                    "RadioGroup.select_index() received {index}; clamping to {clamped_index}."
                ),
            );
        }
        self.select_index_internal(clamped_index, false, true, false);
    }

    fn select_index_internal(&self, index: i32, focus: bool, emit: bool, announce: bool) {
        let Some(radio) = self.radio_at(index) else {
            return;
        };
        if !radio.is_enabled() {
            return;
        }
        let previous_index = self.selected_index.get();
        if previous_index == index {
            if focus {
                radio.focus_now();
            }
            return;
        }
        if let Some(previous) = self.radio_at(previous_index) {
            previous.update_checked(false, emit, false);
        }
        self.selected_index.set(index);
        radio.update_checked(true, emit, announce);
        if focus {
            radio.focus_now();
        }
        if emit {
            self.emit_changed(radio.value().to_string());
        }
    }

    fn emit_changed(&self, value: String) {
        if let Some(callback) = self.changed.borrow().clone() {
            callback(RadioGroupChangedEventArgs { value });
        }
    }

    fn index_of_radio_handle(&self, target_handle: u64) -> i32 {
        self.radios
            .borrow()
            .iter()
            .position(|radio| radio.retained_node_ref().handle().raw() == target_handle)
            .map(|index| index as i32)
            .unwrap_or(-1)
    }

    fn find_enabled_index(&self, start_index: i32, delta: i32) -> i32 {
        let radios = self.radios.borrow();
        if radios.is_empty() {
            return -1;
        }
        let len = radios.len() as i32;
        let mut cursor = start_index;
        for _ in 0..len {
            cursor += delta;
            if cursor < 0 {
                cursor = len - 1;
            } else if cursor >= len {
                cursor = 0;
            }
            if radios[cursor as usize].is_enabled() {
                return cursor;
            }
        }
        -1
    }

    fn find_boundary_index(&self, first: bool) -> i32 {
        let radios = self.radios.borrow();
        if first {
            radios
                .iter()
                .position(|radio| radio.is_enabled())
                .map(|index| index as i32)
                .unwrap_or(-1)
        } else {
            radios
                .iter()
                .rposition(|radio| radio.is_enabled())
                .map(|index| index as i32)
                .unwrap_or(-1)
        }
    }

    fn radio_at(&self, index: i32) -> Option<RadioButton> {
        if index < 0 {
            return None;
        }
        self.radios.borrow().get(index as usize).cloned()
    }
}

#[derive(Clone)]
pub struct RadioGroup {
    root: FlexBox,
    event_target: Rc<RadioGroupEventTarget>,
}

impl RadioGroup {
    pub fn new() -> Self {
        let root = flex_box();
        root.semantic_role(SemanticRole::RadioGroup)
            .flex_direction(FlexDirection::Column);
        let event_target = Rc::new(RadioGroupEventTarget {
            radios: Rc::new(RefCell::new(Vec::new())),
            selected_index: Rc::new(Cell::new(-1)),
            changed: Rc::new(RefCell::new(None)),
        });
        root.retained_node_ref()
            .retain_attachment(event_target.clone());
        let control = Self { root, event_target };
        let selected_index = control.event_target.selected_index.clone();
        let restore_target = control.event_target.clone();
        control.persist_state(crate::persisted::persisted_value_adapter(
            "radio-group-selected-index",
            crate::persisted::PersistedInt32Codec,
            1,
            move || {
                let index = selected_index.get();
                if index >= 0 {
                    Some(index)
                } else {
                    None
                }
            },
            move |index| {
                restore_target.select_index(index);
            },
        ));
        control
    }

    pub fn add_radio(&self, radio: RadioButton) -> &Self {
        radio.bind_group(Rc::downgrade(&self.event_target));
        if self.selected_index() < 0 && radio.is_checked() {
            self.event_target
                .selected_index
                .set(self.event_target.radios.borrow().len() as i32);
        }
        self.root.child(&radio);
        self.event_target.radios.borrow_mut().push(radio);
        self
    }

    pub fn add_option(&self, value: impl Into<String>, label: impl Into<String>) -> RadioButton {
        let radio = RadioButton::with_label(value, label);
        self.add_radio(radio.clone());
        radio
    }

    pub fn add_options<I>(&self, radios: I) -> &Self
    where
        I: IntoIterator<Item = RadioButton>,
    {
        for radio in radios {
            self.add_radio(radio);
        }
        self
    }

    pub fn on_changed(&self, handler: impl Fn(RadioGroupChangedEventArgs) + 'static) -> &Self {
        *self.event_target.changed.borrow_mut() = Some(Rc::new(handler));
        self
    }

    pub fn selected_index(&self) -> i32 {
        self.event_target.selected_index.get()
    }

    pub fn selected_value(&self) -> String {
        self.event_target
            .radio_at(self.selected_index())
            .map(|radio| radio.value().to_string())
            .unwrap_or_default()
    }

    pub fn select_index(&self, index: i32) -> &Self {
        self.event_target.select_index(index);
        self
    }
}

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

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

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

impl crate::node::HasFlexBoxRoot for RadioGroup {
    fn flex_box_root(&self) -> &FlexBox {
        &self.root
    }
}

impl crate::node::ThemeBindable for RadioGroup {
    fn theme_binding_node(&self) -> NodeRef {
        self.root.retained_node_ref()
    }

    fn weak_theme_target(&self) -> Box<dyn Fn() -> Option<Self>> {
        let root = self.root.downgrade();
        let event_target = Rc::downgrade(&self.event_target);
        Box::new(move || {
            Some(Self {
                root: root.upgrade()?,
                event_target: event_target.upgrade()?,
            })
        })
    }
}

pub(crate) type WeakRadioGroupEventTarget = Weak<RadioGroupEventTarget>;