use crate::compat::{Cell, HashMap, Rc, RefCell, String, Vec};
use crate::core::{ObjectId, Rect};
use crate::signal::Signal1;
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub(crate) enum GroupKey {
Name(Rc<str>),
Parent(ObjectId),
Handle(ObjectId),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct GroupMember {
pub id: ObjectId,
pub rect: Rect,
pub enabled: bool,
}
#[derive(Default)]
struct Group {
members: Vec<GroupMember>,
selected: Option<ObjectId>,
sorted: Option<Vec<GroupMember>>,
}
thread_local! {
#[cfg_attr(alloc_frugal, allow(clippy::missing_const_for_thread_local))]
static GROUPS: RefCell<HashMap<GroupKey, Group>> = RefCell::new(HashMap::new());
#[cfg_attr(alloc_frugal, allow(clippy::missing_const_for_thread_local))]
static MEMBER_KEYS: RefCell<HashMap<ObjectId, GroupKey>> = RefCell::new(HashMap::new());
#[cfg_attr(alloc_frugal, allow(clippy::missing_const_for_thread_local))]
static MEMBERSHIP: RefCell<HashMap<ObjectId, GroupMembership>> = RefCell::new(HashMap::new());
#[cfg_attr(alloc_frugal, allow(clippy::missing_const_for_thread_local))]
static HUBS: RefCell<HashMap<GroupKey, GroupSignals>> = RefCell::new(HashMap::new());
static NEXT_HANDLE: Cell<ObjectId> = const { Cell::new(1) };
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct GroupMembership {
key: GroupKey,
id: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum MemberEvent {
Pressed,
Released,
Clicked,
Toggled,
}
#[derive(Clone, Default)]
struct GroupSignals {
pressed: Signal1<u64>,
released: Signal1<u64>,
clicked: Signal1<u64>,
toggled: Signal1<u64>,
}
impl GroupSignals {
fn emit(&self, event: MemberEvent, id: ObjectId) {
match event {
MemberEvent::Pressed => self.pressed.emit(id),
MemberEvent::Released => self.released.emit(id),
MemberEvent::Clicked => self.clicked.emit(id),
MemberEvent::Toggled => self.toggled.emit(id),
}
}
}
pub(crate) fn register(key: GroupKey, id: ObjectId, rect: Rect, enabled: bool) {
let member = GroupMember { id, rect, enabled };
let previous = MEMBER_KEYS
.try_with(|keys| keys.borrow_mut().insert(id, key.clone()))
.ok()
.flatten()
.filter(|previous| *previous != key);
if let Some(previous) = previous {
unregister(&previous, id);
}
let _ = GROUPS.try_with(|groups| {
let mut groups = groups.borrow_mut();
let group = groups.entry(key).or_default();
match group.members.iter_mut().find(|existing| existing.id == id) {
Some(existing) => {
if *existing != member {
*existing = member;
group.sorted = None;
}
}
None => {
group.members.push(member);
group.sorted = None;
}
}
});
}
pub(crate) fn unregister(key: &GroupKey, id: ObjectId) {
let _ = GROUPS.try_with(|groups| {
let mut groups = groups.borrow_mut();
let Some(group) = groups.get_mut(key) else {
return;
};
group.members.retain(|member| member.id != id);
group.sorted = None;
if group.members.iter().all(|member| member.id != id) && group.selected == Some(id) {
group.selected = None;
}
if group.members.is_empty() {
groups.remove(key);
}
});
let _ = MEMBER_KEYS.try_with(|keys| {
let mut keys = keys.borrow_mut();
if keys.get(&id) == Some(key) {
keys.remove(&id);
}
});
}
pub(crate) fn select(key: &GroupKey, id: ObjectId) -> Option<ObjectId> {
GROUPS
.try_with(|groups| {
let mut groups = groups.borrow_mut();
let group = groups.entry(key.clone()).or_default();
if group.selected == Some(id) {
return None;
}
let previous = group.selected.replace(id);
previous.filter(|previous| *previous != id)
})
.ok()
.flatten()
}
pub(crate) fn clear_selection(key: &GroupKey) -> Option<ObjectId> {
GROUPS
.try_with(|groups| groups.borrow_mut().get_mut(key).and_then(|group| group.selected.take()))
.ok()
.flatten()
}
pub(crate) fn selected(key: &GroupKey) -> Option<ObjectId> {
GROUPS
.try_with(|groups| groups.borrow().get(key).and_then(|group| group.selected))
.ok()
.flatten()
}
pub(crate) fn is_selected(key: &GroupKey, id: ObjectId) -> bool {
selected(key) == Some(id)
}
pub(crate) fn members(key: &GroupKey) -> Vec<GroupMember> {
GROUPS
.try_with(|groups| {
groups.borrow().get(key).map(|group| group.members.clone()).unwrap_or_default()
})
.unwrap_or_default()
}
pub(crate) fn next_enabled_member(key: &GroupKey, id: ObjectId, forward: bool) -> Option<ObjectId> {
GROUPS
.try_with(|groups| {
let mut groups = groups.borrow_mut();
let group = groups.get_mut(key)?;
if group.sorted.is_none() {
let mut sorted: Vec<GroupMember> =
group.members.iter().copied().filter(|member| member.enabled).collect();
sorted.sort_by_key(|member| (member.rect.y, member.rect.x));
group.sorted = Some(sorted);
}
let sorted = group.sorted.as_ref()?;
if sorted.len() < 2 {
return None;
}
let index = sorted.iter().position(|member| member.id == id).unwrap_or(0);
let next = if forward {
(index + 1) % sorted.len()
} else {
(index + sorted.len() - 1) % sorted.len()
};
Some(sorted[next].id)
})
.ok()
.flatten()
}
pub(crate) fn contains_member(key: &GroupKey, id: ObjectId) -> bool {
GROUPS
.try_with(|groups| {
groups
.borrow()
.get(key)
.is_some_and(|group| group.members.iter().any(|member| member.id == id))
})
.unwrap_or(false)
}
#[cfg(not(alloc_frugal))]
pub(crate) fn notify_checked(ids: &[ObjectId], checked: bool) {
use crate::widget::capability::properties_trait::widget_property_set;
use crate::widget::capability::types::CapabilityValue;
for id in ids {
let _ = crate::widget::runtime::with_widget_mut(*id, |widget| {
let _ = widget_property_set(widget, "checked", CapabilityValue::Bool(checked));
});
}
}
#[cfg(alloc_frugal)]
pub(crate) fn notify_checked(_ids: &[ObjectId], _checked: bool) {}
pub(crate) fn membership(id: ObjectId) -> Option<GroupKey> {
MEMBERSHIP.try_with(|m| m.borrow().get(&id).map(|entry| entry.key.clone())).ok().flatten()
}
pub(crate) fn notify_member_event(id: ObjectId, event: MemberEvent) {
let Some(key) = MEMBER_KEYS
.try_with(|keys| keys.borrow().get(&id).cloned())
.ok()
.flatten()
.or_else(|| membership(id))
else {
return;
};
let hub = HUBS.try_with(|hubs| hubs.borrow().get(&key).cloned()).ok().flatten();
if let Some(hub) = hub {
hub.emit(event, id);
}
}
pub(crate) fn leave_all(id: ObjectId) {
let _ = MEMBERSHIP.try_with(|m| m.borrow_mut().remove(&id));
let key = MEMBER_KEYS.try_with(|keys| keys.borrow_mut().remove(&id)).ok().flatten();
if let Some(key) = key {
unregister(&key, id);
}
}
fn hub_for(key: &GroupKey) -> GroupSignals {
HUBS.try_with(|hubs| hubs.borrow_mut().entry(key.clone()).or_default().clone())
.unwrap_or_default()
}
fn next_handle_identity() -> ObjectId {
NEXT_HANDLE
.try_with(|next| {
let value = next.get();
next.set(value.saturating_add(1));
value
})
.unwrap_or(1)
}
fn move_group(old_key: &GroupKey, new_key: &GroupKey) {
if old_key == new_key {
return;
}
let moved = GROUPS.try_with(|groups| {
let mut groups = groups.borrow_mut();
groups.remove(old_key)
});
if let Some(moved) = moved.ok().flatten() {
let _ = GROUPS.try_with(|groups| {
let mut groups = groups.borrow_mut();
let destination = groups.entry(new_key.clone()).or_default();
for member in moved.members {
match destination.members.iter_mut().find(|existing| existing.id == member.id) {
Some(existing) => *existing = member,
None => destination.members.push(member),
}
}
destination.sorted = None;
if destination.selected.is_none() {
destination.selected = moved.selected;
}
});
}
let moved_hub = HUBS.try_with(|hubs| {
let mut hubs = hubs.borrow_mut();
if hubs.contains_key(new_key) {
let _ = hubs.remove(old_key);
} else if let Some(hub) = hubs.remove(old_key) {
let _ = hubs.insert(new_key.clone(), hub);
}
});
let _ = moved_hub;
let _ = MEMBERSHIP.try_with(|m| {
for entry in m.borrow_mut().values_mut() {
if entry.key == *old_key {
entry.key = new_key.clone();
}
}
});
let _ = MEMBER_KEYS.try_with(|keys| {
for key in keys.borrow_mut().values_mut() {
if *key == *old_key {
*key = new_key.clone();
}
}
});
}
#[cfg(not(alloc_frugal))]
fn member_facts(id: ObjectId) -> Option<(Rect, bool, bool)> {
use crate::widget::capability::properties_trait::widget_property_get;
use crate::widget::capability::types::CapabilityValue;
crate::widget::runtime::with_widget_mut(id, |widget| {
let checked =
matches!(widget_property_get(widget, "checked"), Ok(CapabilityValue::Bool(true)));
(widget.geometry(), widget.is_enabled(), checked)
})
}
#[cfg(alloc_frugal)]
fn member_facts(_id: ObjectId) -> Option<(Rect, bool, bool)> {
Some((Rect::new(0, 0, 0, 0), true, false))
}
#[derive(Clone)]
pub struct ButtonGroup {
key: GroupKey,
signals: GroupSignals,
}
impl Default for ButtonGroup {
fn default() -> Self {
Self::new()
}
}
impl ButtonGroup {
pub fn new() -> Self {
let key = GroupKey::Handle(next_handle_identity());
Self { signals: hub_for(&key), key }
}
pub fn with_id(id: impl Into<String>) -> Self {
let name: String = id.into();
let key = GroupKey::Name(Rc::from(name.as_str()));
Self { signals: hub_for(&key), key }
}
pub fn id(&self) -> Option<&str> {
match &self.key {
GroupKey::Name(name) => Some(&**name),
GroupKey::Parent(_) | GroupKey::Handle(_) => None,
}
}
pub fn set_id(&mut self, id: impl Into<String>) {
let name: String = id.into();
let new_key = GroupKey::Name(Rc::from(name.as_str()));
if new_key == self.key {
return;
}
move_group(&self.key, &new_key);
self.key = new_key;
self.signals = hub_for(&self.key);
}
pub fn clear_id(&mut self) {
let new_key = GroupKey::Handle(next_handle_identity());
move_group(&self.key, &new_key);
self.key = new_key;
self.signals = hub_for(&self.key);
}
pub fn add_button(&mut self, button: ObjectId) -> bool {
self.add_button_with_id(button, button)
}
pub fn add_button_with_id(&mut self, button: ObjectId, id: u64) -> bool {
if let Some(previous) =
MEMBER_KEYS.try_with(|keys| keys.borrow().get(&button).cloned()).ok().flatten()
{
if previous != self.key {
unregister(&previous, button);
}
}
let _ = MEMBERSHIP.try_with(|m| {
let _ = m.borrow_mut().insert(button, GroupMembership { key: self.key.clone(), id });
});
let _ = hub_for(&self.key);
if let Some((rect, enabled, checked)) = member_facts(button) {
register(self.key.clone(), button, rect, enabled);
if checked {
select(&self.key, button);
}
}
true
}
pub fn remove_button(&mut self, button: ObjectId) -> bool {
let in_handle = MEMBERSHIP
.try_with(|m| m.borrow().get(&button).is_some_and(|entry| entry.key == self.key))
.unwrap_or(false);
if in_handle {
let _ = MEMBERSHIP.try_with(|m| m.borrow_mut().remove(&button));
}
let registered = MEMBER_KEYS
.try_with(|keys| keys.borrow().get(&button) == Some(&self.key))
.unwrap_or(false);
unregister(&self.key, button);
in_handle || registered
}
pub fn buttons(&self) -> Vec<ObjectId> {
let mut ids: Vec<ObjectId> =
members(&self.key).into_iter().map(|member| member.id).collect();
let extra = MEMBERSHIP.try_with(|m| {
let mut extra: Vec<ObjectId> = m
.borrow()
.iter()
.filter(|(id, entry)| entry.key == self.key && !ids.contains(*id))
.map(|(id, _)| *id)
.collect();
extra.sort_unstable();
extra
});
if let Ok(extra) = extra {
ids.extend(extra);
}
ids
}
pub fn contains(&self, button: ObjectId) -> bool {
contains_member(&self.key, button) || membership(button) == Some(self.key.clone())
}
pub fn checked_button(&self) -> Option<ObjectId> {
selected(&self.key)
}
pub fn checked_id(&self) -> Option<u64> {
let selected = selected(&self.key)?;
MEMBERSHIP
.try_with(|m| {
m.borrow()
.get(&selected)
.filter(|entry| entry.key == self.key)
.map(|entry| entry.id)
})
.ok()
.flatten()
.or(Some(selected))
}
pub fn set_checked_button(&mut self, button: ObjectId) -> Option<ObjectId> {
if !self.contains(button) {
return None;
}
let previous = select(&self.key, button);
if let Some(previous) = previous.filter(|previous| *previous != button) {
notify_checked(&[previous], false);
}
previous
}
pub fn button_pressed(&self) -> &Signal1<u64> {
&self.signals.pressed
}
pub fn button_released(&self) -> &Signal1<u64> {
&self.signals.released
}
pub fn button_clicked(&self) -> &Signal1<u64> {
&self.signals.clicked
}
pub fn button_toggled(&self) -> &Signal1<u64> {
&self.signals.toggled
}
}
#[cfg(test)]
pub(crate) fn clear() {
let _ = GROUPS.try_with(|groups| groups.borrow_mut().clear());
let _ = MEMBER_KEYS.try_with(|keys| keys.borrow_mut().clear());
let _ = MEMBERSHIP.try_with(|m| m.borrow_mut().clear());
let _ = HUBS.try_with(|hubs| hubs.borrow_mut().clear());
let _ = NEXT_HANDLE.try_with(|next| next.set(1));
}
#[cfg(test)]
mod tests {
use super::*;
fn key(name: &str) -> GroupKey {
GroupKey::Name(Rc::from(name))
}
#[test]
fn a_group_selects_one_member_at_a_time() {
clear();
register(key("g"), 1, Rect::new(0, 0, 10, 10), true);
register(key("g"), 2, Rect::new(0, 20, 10, 10), true);
assert_eq!(select(&key("g"), 1), None, "the first selection has no predecessor");
assert_eq!(select(&key("g"), 1), None, "a repeat selection is not a move");
assert_eq!(select(&key("g"), 2), Some(1), "the previous member is reported once");
assert!(is_selected(&key("g"), 2));
assert!(!is_selected(&key("g"), 1));
}
#[test]
fn the_group_disappears_when_its_last_member_leaves() {
clear();
register(key("g"), 7, Rect::new(0, 0, 4, 4), true);
select(&key("g"), 7);
unregister(&key("g"), 7);
assert_eq!(selected(&key("g")), None);
assert!(members(&key("g")).is_empty());
}
#[test]
fn a_geometry_refresh_replaces_the_snapshot() {
clear();
register(key("g"), 3, Rect::new(0, 0, 10, 10), true);
register(key("g"), 3, Rect::new(40, 50, 10, 10), false);
let members = members(&key("g"));
assert_eq!(members.len(), 1, "a refresh must not duplicate the member");
assert_eq!(members[0].rect, Rect::new(40, 50, 10, 10));
assert!(!members[0].enabled);
}
#[test]
fn parent_and_name_groups_are_distinct() {
clear();
register(GroupKey::Parent(1), 1, Rect::new(0, 0, 1, 1), true);
register(key("1"), 2, Rect::new(0, 0, 1, 1), true);
select(&key("1"), 2);
assert!(!is_selected(&GroupKey::Parent(1), 2));
}
#[test]
fn navigation_reads_the_cached_visual_order() {
clear();
register(key("g"), 1, Rect::new(0, 0, 10, 10), true);
register(key("g"), 2, Rect::new(0, 20, 10, 10), false);
register(key("g"), 3, Rect::new(0, 40, 10, 10), true);
assert_eq!(next_enabled_member(&key("g"), 1, true), Some(3), "disabled peers are skipped");
assert_eq!(next_enabled_member(&key("g"), 3, true), Some(1), "the walk wraps");
assert_eq!(next_enabled_member(&key("g"), 1, false), Some(3));
assert!(contains_member(&key("g"), 2));
assert!(!contains_member(&key("g"), 9));
}
}