use crate::primitives::text_edit::TextEdit;
use fresh_core::api::WidgetSpec;
use fresh_core::BufferId;
use std::collections::{HashMap, HashSet};
pub type PanelId = u64;
#[derive(Debug, Clone)]
pub struct HitArea {
pub widget_key: String,
pub widget_kind: &'static str,
pub buffer_row: u32,
pub byte_start: usize,
pub byte_end: usize,
pub payload: serde_json::Value,
pub event_type: &'static str,
}
#[derive(Debug, Clone, Default)]
pub enum WidgetInstanceState {
#[default]
None,
List {
scroll_offset: u32,
selected_index: i32,
},
Text {
editor: TextEdit,
scroll: u32,
completions: Vec<fresh_core::api::CompletionItem>,
completion_selected_index: usize,
completion_scroll_offset: u32,
},
Tree {
scroll_offset: u32,
selected_index: i32,
expanded_keys: HashSet<String>,
},
}
#[derive(Debug, Clone)]
pub struct WidgetPanelState {
pub buffer_id: BufferId,
pub spec: WidgetSpec,
pub hits: Vec<HitArea>,
pub instance_states: HashMap<String, WidgetInstanceState>,
pub focus_key: String,
pub tabbable: Vec<String>,
}
#[derive(Debug, Default)]
pub struct WidgetRegistry {
panels: HashMap<PanelId, WidgetPanelState>,
}
impl WidgetRegistry {
pub fn new() -> Self {
Self::default()
}
#[allow(clippy::too_many_arguments)]
pub fn mount(
&mut self,
panel_id: PanelId,
buffer_id: BufferId,
spec: WidgetSpec,
hits: Vec<HitArea>,
instance_states: HashMap<String, WidgetInstanceState>,
focus_key: String,
tabbable: Vec<String>,
) -> Option<WidgetPanelState> {
self.panels.insert(
panel_id,
WidgetPanelState {
buffer_id,
spec,
hits,
instance_states,
focus_key,
tabbable,
},
)
}
#[allow(clippy::result_unit_err)]
#[allow(clippy::too_many_arguments)]
pub fn update(
&mut self,
panel_id: PanelId,
spec: WidgetSpec,
hits: Vec<HitArea>,
instance_states: HashMap<String, WidgetInstanceState>,
focus_key: String,
tabbable: Vec<String>,
) -> Result<BufferId, ()> {
match self.panels.get_mut(&panel_id) {
Some(state) => {
state.spec = spec;
state.hits = hits;
state.instance_states = instance_states;
state.focus_key = focus_key;
state.tabbable = tabbable;
Ok(state.buffer_id)
}
None => Err(()),
}
}
pub fn instance_states(
&self,
panel_id: PanelId,
) -> Option<&HashMap<String, WidgetInstanceState>> {
self.panels.get(&panel_id).map(|s| &s.instance_states)
}
pub fn focus_key(&self, panel_id: PanelId) -> Option<&str> {
self.panels.get(&panel_id).map(|s| s.focus_key.as_str())
}
pub fn set_focus_key(&mut self, panel_id: PanelId, key: String) {
if let Some(state) = self.panels.get_mut(&panel_id) {
state.focus_key = key;
}
}
pub fn update_side_effects(
&mut self,
panel_id: PanelId,
hits: Vec<HitArea>,
instance_states: HashMap<String, WidgetInstanceState>,
focus_key: String,
tabbable: Vec<String>,
) -> Result<BufferId, ()> {
match self.panels.get_mut(&panel_id) {
Some(state) => {
state.hits = hits;
state.instance_states = instance_states;
state.focus_key = focus_key;
state.tabbable = tabbable;
Ok(state.buffer_id)
}
None => Err(()),
}
}
pub fn buffer_and_spec_ref(&self, panel_id: PanelId) -> Option<(BufferId, &WidgetSpec)> {
self.panels.get(&panel_id).map(|s| (s.buffer_id, &s.spec))
}
pub fn buffer_and_spec(&self, panel_id: PanelId) -> Option<(BufferId, WidgetSpec)> {
self.panels
.get(&panel_id)
.map(|s| (s.buffer_id, s.spec.clone()))
}
pub fn unmount(&mut self, panel_id: PanelId) -> Option<BufferId> {
self.panels.remove(&panel_id).map(|s| s.buffer_id)
}
pub fn get(&self, panel_id: PanelId) -> Option<&WidgetPanelState> {
self.panels.get(&panel_id)
}
pub fn get_mut(&mut self, panel_id: PanelId) -> Option<&mut WidgetPanelState> {
self.panels.get_mut(&panel_id)
}
pub fn panel_ids(&self) -> Vec<PanelId> {
self.panels.keys().copied().collect()
}
pub fn panels_for_buffer(&self, buffer_id: BufferId) -> Vec<PanelId> {
self.panels
.iter()
.filter(|(_, s)| s.buffer_id == buffer_id)
.map(|(pid, _)| *pid)
.collect()
}
pub fn hit_test(
&self,
buffer_id: BufferId,
row: u32,
col_byte: u32,
) -> Option<(PanelId, HitArea)> {
for (pid, state) in &self.panels {
if state.buffer_id != buffer_id {
continue;
}
for hit in &state.hits {
if hit.buffer_row == row
&& (col_byte as usize) >= hit.byte_start
&& (col_byte as usize) < hit.byte_end
{
return Some((*pid, hit.clone()));
}
}
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn empty_spec() -> WidgetSpec {
WidgetSpec::Col {
children: vec![],
key: None,
}
}
fn make_hit(row: u32, byte_start: usize, byte_end: usize, key: &str) -> HitArea {
HitArea {
widget_key: key.into(),
widget_kind: "button",
buffer_row: row,
byte_start,
byte_end,
payload: json!({}),
event_type: "activate",
}
}
#[test]
fn hit_test_finds_widget_inside_range() {
let mut reg = WidgetRegistry::new();
reg.mount(
42,
BufferId(7),
empty_spec(),
vec![make_hit(0, 0, 5, "a"), make_hit(0, 7, 12, "b")],
HashMap::new(),
String::new(),
Vec::new(),
);
let hit = reg.hit_test(BufferId(7), 0, 8).expect("inside b");
assert_eq!(hit.0, 42);
assert_eq!(hit.1.widget_key, "b");
}
#[test]
fn hit_test_returns_none_when_outside_range() {
let mut reg = WidgetRegistry::new();
reg.mount(
1,
BufferId(0),
empty_spec(),
vec![make_hit(0, 0, 5, "a")],
HashMap::new(),
String::new(),
Vec::new(),
);
assert!(
reg.hit_test(BufferId(0), 0, 5).is_none(),
"byte_end is exclusive"
);
assert!(reg.hit_test(BufferId(0), 0, 100).is_none());
assert!(reg.hit_test(BufferId(0), 1, 0).is_none(), "wrong row");
assert!(reg.hit_test(BufferId(99), 0, 0).is_none(), "wrong buffer");
}
#[test]
fn unmount_clears_hits() {
let mut reg = WidgetRegistry::new();
reg.mount(
5,
BufferId(2),
empty_spec(),
vec![make_hit(0, 0, 3, "x")],
HashMap::new(),
String::new(),
Vec::new(),
);
assert!(reg.hit_test(BufferId(2), 0, 1).is_some());
reg.unmount(5);
assert!(reg.hit_test(BufferId(2), 0, 1).is_none());
}
#[test]
fn update_replaces_hits() {
let mut reg = WidgetRegistry::new();
reg.mount(
5,
BufferId(2),
empty_spec(),
vec![make_hit(0, 0, 3, "old")],
HashMap::new(),
String::new(),
Vec::new(),
);
reg.update(
5,
empty_spec(),
vec![make_hit(1, 4, 9, "new")],
HashMap::new(),
String::new(),
Vec::new(),
)
.expect("mounted");
assert!(reg.hit_test(BufferId(2), 0, 1).is_none());
let hit = reg.hit_test(BufferId(2), 1, 5).unwrap();
assert_eq!(hit.1.widget_key, "new");
}
}