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.
147 lines
4.9 KiB
Rust
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),
|
|
);
|
|
}
|
|
}
|