Watch
1
0
Fork
You've already forked souveraine
0
souveraine/src/ui/widgets/dropdown.rs
Fimeg 85aa1f77de tuie: replace hand-rolled widgets with verbatim tuie-demo copies
14 widgets now match the demo source exactly (import paths rewritten):
accordion, button, checkbox, counter, flat_button, focus_pane,
global_chords, horizontal_rule, link, page_layout, point_picker,
progress_bar, radio_group, segmented_control.

Theme simplified: accent color is now Color::YELLOW, which
apply_atmosphere resolves through harmonious to the agent's
current primary — no thread-local, no RGB arithmetic.

Added chord_macro + axis2d deps matching tuie-demo's Cargo.toml.

All 27 broken call sites (field_grid, settings, cockpit,
dropdown, model_picker, text_editor) migrated to demo APIs:
FlatButton::new().child(x), Button::new().children([x]),
Checkbox::new(label).set_checked(b), Counter::new(),
PointPicker::new().point(...), etc.
2026-06-04 18:24:12 -04:00

147 lines
4.9 KiB
Rust

//! Dropdown select — a compact trigger that opens a floating popup menu.
//!
//! The trigger renders `value ▾` and, on activation, opens a tuie [`Popup`]
//! anchored below it containing one [`FlatButton`] per option. Selecting an
//! option emits `ChangeEvent<usize>` from the dropdown's own id (so a parent
//! can map it back to a field) and closes the popup. Best for medium-length
//! enums; very long lists (model catalogs) are better served by a full picker.
//!
//! [`FlatButton`]: crate::ui::widgets::flat_button::FlatButton
use std::cell::Cell;
use std::rc::Rc;
use tuie::prelude::*;
use tuie::input::key::Key;
use tuie::input::mouse::MouseButton;
use tuie::input::trigger::Trigger;
use crate::ui::widgets::focus_pane::FocusPane;
use crate::ui::widgets::flat_button::FlatButton;
/// Self-contained popup host: forwards its subtree's events to a closure. This
/// is how the selected option travels back out of the (separately-rooted)
/// popup widget tree.
struct MenuHost {
root: Box<Pane>,
on_event: Box<dyn Fn(&WidgetEvent)>,
}
impl MenuHost {
fn new(root: Box<Pane>, on_event: impl Fn(&WidgetEvent) + 'static) -> Box<Self> {
Box::new(Self { root, on_event: Box::new(on_event) })
}
}
impl DelegateWidget for MenuHost {
tuie::delegate_widget!(root);
fn after_on_event(&mut self, event: &mut WidgetEvent) {
(self.on_event)(event);
}
}
/// Compact select control that opens a popup menu of options.
pub struct Dropdown {
trigger: Box<FocusPane>,
labels: Vec<String>,
selected: usize,
accent: Color,
}
impl DelegateWidget for Dropdown {
tuie::delegate_widget!(trigger);
fn override_on_input(&mut self, queue: &mut InputQueue) -> InputResult {
let Some(event) = queue.next() else {
return InputResult::Rejected;
};
match &event.chord.trigger {
Trigger::Key(Key::Enter) => {
self.open_menu();
}
Trigger::MouseDown(MouseButton::Left) => {
tuie::focus_widget(self.get_id());
}
Trigger::MouseUp(MouseButton::Left) => {
let size = self.get_rect_size();
let inside = Axis2D::all(|a| {
event.mouse_pos[a] >= 0 && event.mouse_pos[a] < size[a] as i32
});
if inside {
self.open_menu();
}
}
_ => return InputResult::Rejected,
}
InputResult::Handled
}
fn override_is_focusable(&self) -> bool {
true
}
}
impl Dropdown {
/// Creates a dropdown over `labels` with `selected` initially chosen.
pub fn new(labels: &[&str], selected: usize) -> Box<Self> {
let label = labels.get(selected).copied().unwrap_or("");
let text = Text::new().content(format!(" {label} \u{25be}"));
let mut trigger = FocusPane::new();
trigger.add_child(text as Box<dyn Widget>);
Box::new(Self {
trigger,
labels: labels.iter().map(|s| s.to_string()).collect(),
selected,
accent: Color::YELLOW,
})
}
/// Returns the currently selected index.
pub fn get_selected(&self) -> usize {
self.selected
}
fn open_menu(&self) {
let dd_id = self.get_id();
let accent = self.accent;
let mut row_ids: Vec<WidgetId> = Vec::new();
let mut list = Pane::new().vertical().gap(0);
for (i, label) in self.labels.iter().enumerate() {
let mut id = WidgetId::EMPTY;
let marker = if i == self.selected { "\u{203a}" } else { " " };
let row = FlatButton::new()
.child(Text::new().content(format!(" {marker} {label} ")))
.id(&mut id);
row_ids.push(id.untyped());
list = list.children([row as Box<dyn Widget>]);
}
let body = Pane::new()
.vertical()
.bordered()
.border_style(Style::new().fg(accent).dim())
.style(Style::new().bg(Color::grey256(3)))
.max_height(14)
.children([list as Box<dyn Widget>]);
let popup_slot: Rc<Cell<Option<WidgetId>>> = Rc::new(Cell::new(None));
let slot = popup_slot.clone();
let host = MenuHost::new(body, move |event| {
if event.of::<ClickEvent>() {
if let Some(idx) = row_ids.iter().position(|&id| event.source == id) {
tuie::emit(dd_id, ChangeEvent(idx));
if let Some(pid) = slot.get() {
tuie::close_popup(pid);
}
}
}
});
popup_slot.set(Some(host.get_id().untyped()));
tuie::open_popup(
Popup::new(host as Box<dyn Widget>)
.placement(Placement::side(Direction2D::Down, Sign::Positive, Align::Start))
.dismissible(true),
);
}
}