use super::*;
pub(super) struct DashboardState {
pub(super) endpoint: String,
pub(super) gateway: ReadyPayload,
pub(super) clients: Vec<ClientStatus>,
pub(super) current_client_id: Option<String>,
pub(super) selected_client_id: Option<String>,
pub(super) selected_session_id: Option<String>,
pub(super) device_list: ListState,
pub(super) chat_list: ListState,
pub(super) focus: DashboardFocus,
pub(super) pending_unpair: Option<(String, String)>,
pub(super) profile: Option<ProfileSnapshot>,
pub(super) pending_open: Option<(String, String)>,
pub(super) overlay: Option<CapabilityOverlay>,
pub(super) error: Option<String>,
}
pub(super) struct CapabilityOverlay {
pub(super) session_id: String,
pub(super) widgets: Vec<((String, String), FrontendWidget)>,
pub(super) widget_list: ListState,
pub(super) open: Option<(String, String)>,
pub(super) option_list: ListState,
pub(super) action_index: usize,
pub(super) input: Option<ActionInput>,
}
pub(super) struct ActionInput {
pub(super) op: Op,
pub(super) text: String,
pub(super) cursor: usize,
}
impl CapabilityOverlay {
pub(super) fn from_session(payload: SessionReadyPayload) -> Self {
let session_id = payload.session.session_id;
let widgets = payload
.contributions
.into_iter()
.flat_map(|contribution| {
contribution.widgets.into_iter().filter_map(move |item| {
(item.slot == FrontendSlot::Navigation)
.then(|| ((contribution.capability.clone(), item.id.clone()), item))
})
})
.collect();
let mut overlay = Self {
session_id,
widgets,
widget_list: ListState::default(),
open: None,
option_list: ListState::default(),
action_index: 0,
input: None,
};
for widget in payload.widgets {
overlay.apply(FrontendEvent::Widget {
capability: widget.capability,
item: widget.item,
});
}
overlay.sync_selection();
overlay
}
pub(super) fn apply(&mut self, event: FrontendEvent) {
match event {
FrontendEvent::Widget { capability, item } => {
let key = (capability, item.id.clone());
if item.slot == FrontendSlot::Navigation {
if let Some((_, widget)) = self
.widgets
.iter_mut()
.find(|(candidate, _)| candidate == &key)
{
*widget = item;
} else {
self.widgets.push((key, item));
}
} else {
self.widgets.retain(|(candidate, _)| candidate != &key);
}
}
FrontendEvent::RemoveWidget { capability, id } => {
let key = (capability, id);
self.widgets.retain(|(candidate, _)| candidate != &key);
}
_ => return,
}
self.sync_selection();
}
pub(super) fn sync_selection(&mut self) {
let selected = self
.widgets
.len()
.checked_sub(1)
.map(|last| self.widget_list.selected().unwrap_or_default().min(last));
self.widget_list.select(selected);
if self
.open
.as_ref()
.is_some_and(|key| self.widget(key).is_none())
{
self.open = None;
}
let options = self
.open_widget()
.and_then(|widget| match widget.content.as_ref() {
Some(FrontendWidgetContent::Picker { options, .. }) => Some(options.len()),
Some(FrontendWidgetContent::ActionList { items, .. }) => Some(items.len()),
_ => None,
})
.unwrap_or_default();
self.option_list.select(
options
.checked_sub(1)
.map(|last| self.option_list.selected().unwrap_or_default().min(last)),
);
self.action_index = self
.selected_action_list_item()
.and_then(|item| item.actions.len().checked_sub(1))
.map_or(0, |last| self.action_index.min(last));
}
pub(super) fn selected_key(&self) -> Option<(String, String)> {
self.widgets
.get(self.widget_list.selected()?)
.map(|(key, _)| key)
.cloned()
}
pub(super) fn open_widget(&self) -> Option<&FrontendWidget> {
self.widget(self.open.as_ref()?)
}
pub(super) fn widget(&self, key: &(String, String)) -> Option<&FrontendWidget> {
self.widgets
.iter()
.find(|(candidate, _)| candidate == key)
.map(|(_, widget)| widget)
}
pub(super) fn selected_action_list_item(&self) -> Option<&FrontendActionListItem> {
let FrontendWidgetContent::ActionList { items, .. } =
self.open_widget()?.content.as_ref()?
else {
return None;
};
items.get(self.option_list.selected()?)
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub(super) enum DashboardFocus {
Devices,
Chats,
}