use compact_str::CompactString;
use crate::{
graphics::char_width,
layout::Rect,
tdom::node::{NodeArena, NodeId, NodeType},
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SelectOption {
pub value: CompactString,
pub label: CompactString,
pub group: Option<CompactString>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SelectRow {
Group(String),
Item(usize),
}
pub(crate) fn select_options(arena: &NodeArena, node_id: NodeId) -> Vec<SelectOption> {
let mut out = Vec::new();
let Some(node) = arena.get(node_id) else {
return out;
};
for &child_id in &node.children {
let child = &arena[child_id];
match child.node_type {
NodeType::HtmlOption => out.push(option_value(arena, child_id, None)),
NodeType::Optgroup => {
let group = child
.attributes
.get("label")
.cloned()
.map(CompactString::from);
for &option_id in &child.children {
if arena[option_id].node_type == NodeType::HtmlOption {
out.push(option_value(arena, option_id, group.clone()));
}
}
}
_ => {}
}
}
out
}
fn option_value(
arena: &NodeArena,
option_id: NodeId,
group: Option<CompactString>,
) -> SelectOption {
let child = &arena[option_id];
let label: CompactString = child
.children
.iter()
.filter_map(|&c| match &arena[c].node_type {
NodeType::Text => arena[c].data.as_deref(),
_ => None,
})
.collect();
let value = child
.attributes
.get("value")
.cloned()
.map(CompactString::from)
.unwrap_or_else(|| label.clone());
SelectOption {
value,
label,
group,
}
}
pub(crate) fn overlay_options(
arena: &NodeArena,
node_id: NodeId,
filter: Option<&str>,
) -> Vec<SelectOption> {
let Some(node) = arena.get(node_id) else {
return Vec::new();
};
if node.node_type == NodeType::Select {
return select_options(arena, node_id);
}
if node.node_type == NodeType::Input
&& let Some(list) = node.attributes.get("list")
{
return datalist_options(arena, list, filter.unwrap_or(""));
}
Vec::new()
}
fn datalist_options(arena: &NodeArena, list_id: &str, filter: &str) -> Vec<SelectOption> {
let f = filter.to_lowercase();
let mut out = Vec::new();
for (_, node) in arena.iter() {
if node.node_type != NodeType::Datalist || !node.get_id().is_some_and(|id| id == list_id) {
continue;
}
for &child_id in &node.children {
if arena[child_id].node_type != NodeType::HtmlOption {
continue;
}
let opt = option_value(arena, child_id, None);
if f.is_empty()
|| opt.value.to_lowercase().contains(&f)
|| opt.label.to_lowercase().contains(&f)
{
out.push(opt);
}
}
}
out
}
pub(crate) fn select_rows(options: &[SelectOption]) -> Vec<SelectRow> {
let mut rows = Vec::new();
let mut last_group: Option<&str> = None;
for (i, opt) in options.iter().enumerate() {
if opt.group.as_deref() != last_group {
if let Some(g) = opt.group.as_deref() {
rows.push(SelectRow::Group(g.to_string()));
}
last_group = opt.group.as_deref();
}
rows.push(SelectRow::Item(i));
}
rows
}
pub(crate) fn select_item_indices(rows: &[SelectRow]) -> Vec<usize> {
rows.iter()
.filter_map(|r| match r {
SelectRow::Item(i) => Some(*i),
SelectRow::Group(_) => None,
})
.collect()
}
pub(crate) fn select_overlay_rect(
options: &[SelectOption],
rows: &[SelectRow],
select_rect: Rect,
) -> Rect {
let text_w: usize = rows
.iter()
.map(|r| match r {
SelectRow::Group(g) => g.chars().map(char_width).sum::<usize>(),
SelectRow::Item(i) => options
.get(*i)
.map(|o| o.label.chars().map(char_width).sum::<usize>())
.unwrap_or(0),
})
.max()
.unwrap_or(0);
let width = select_rect.width.max(text_w.saturating_add(2)).max(1);
Rect::new(
select_rect.x,
select_rect.y.saturating_add(1),
width,
rows.len().max(1),
)
}