use crossterm::event::KeyCode;
use rich_rs::{Console, ConsoleOptions, Renderable, Segment, Segments, Text};
use crate::event::{Action, Event, EventCtx};
use crate::message::*;
#[path = "toggle_option.rs"]
pub(crate) mod toggle_option;
use super::{
Widget, WidgetStyles,
helpers::{adjust_line_length_no_bg, empty_classes, fixed_height_from_constraints},
};
use toggle_option::OptionCursorState;
pub use toggle_option::{OptionId, OptionItem};
#[derive(Debug, Clone)]
pub struct OptionList {
items: Vec<OptionItem>,
cursor: OptionCursorState,
disabled: bool,
offset: usize,
focused: bool,
hovered: bool,
hovered_index: Option<usize>,
viewport_height: usize,
scroll_step: usize,
classes: Vec<String>,
focused_classes: Vec<String>,
styles: WidgetStyles,
}
impl OptionList {
pub fn new() -> Self {
Self {
items: Vec::new(),
cursor: OptionCursorState::default(),
disabled: false,
offset: 0,
focused: false,
hovered: false,
hovered_index: None,
viewport_height: 1,
scroll_step: 1,
classes: vec!["option-list".to_string()],
focused_classes: vec!["option-list".to_string(), "focused".to_string()],
styles: WidgetStyles::default(),
}
}
pub fn with_items(items: Vec<OptionItem>) -> Self {
let mut list = Self::new();
list.items = items;
list.cursor.set_highlighted(list.first_selectable());
list
}
pub fn scroll_step(mut self, step: usize) -> Self {
self.scroll_step = step.max(1);
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
if disabled {
self.focused = false;
}
self
}
pub fn add_option(&mut self, prompt: impl Into<String>, id: Option<OptionId>, disabled: bool) {
let was_empty = self.cursor.highlighted().is_none();
self.items.push(OptionItem::Option {
prompt: prompt.into(),
content: None,
id,
disabled,
});
if was_empty && !disabled {
self.cursor.set_highlighted(Some(self.items.len() - 1));
}
}
pub fn add_rich_option(
&mut self,
label: impl Into<String>,
content: Text,
id: Option<OptionId>,
disabled: bool,
) {
let was_empty = self.cursor.highlighted().is_none();
self.items.push(OptionItem::Option {
prompt: label.into(),
content: Some(content),
id,
disabled,
});
if was_empty && !disabled {
self.cursor.set_highlighted(Some(self.items.len() - 1));
}
}
pub fn add_separator(&mut self) {
self.items.push(OptionItem::Separator);
}
pub fn clear_options(&mut self) {
self.items.clear();
self.cursor.clear();
self.offset = 0;
self.hovered_index = None;
}
pub fn option_count(&self) -> usize {
self.items.len()
}
pub fn get_option(&self, index: usize) -> Option<&OptionItem> {
self.items.get(index)
}
pub fn highlighted(&self) -> Option<usize> {
self.cursor.highlighted()
}
pub fn hovered_index(&self) -> Option<usize> {
self.hovered_index
}
pub fn offset_for_click(&self) -> usize {
self.offset
}
pub fn set_highlighted(&mut self, index: usize) {
if index < self.items.len() && self.items[index].is_selectable() {
self.cursor.set_highlighted(Some(index));
self.ensure_visible();
}
}
pub fn clear_highlighted(&mut self) {
self.cursor.set_highlighted(None);
self.ensure_visible();
}
pub fn first_selectable_index(&self) -> Option<usize> {
self.first_selectable()
}
pub fn set_items(&mut self, items: Vec<OptionItem>) {
self.items = items;
self.cursor.set_highlighted(self.first_selectable());
self.offset = 0;
self.hovered_index = None;
self.ensure_visible();
}
fn visible_range(&self, viewport_height: usize) -> (usize, usize) {
let start = self.offset;
let end = (start + viewport_height).min(self.items.len());
(start, end)
}
fn render_rich_line(
&self,
content: &Text,
line_style: rich_rs::Style,
width: usize,
console: &Console,
options: &ConsoleOptions,
) -> Vec<Segment> {
let indent_width = 2usize;
let content_width = width.saturating_sub(indent_width);
let mut content_options = options.clone();
content_options.size = (content_width, 1);
content_options.max_width = content_width;
let rendered: Vec<Segment> = content
.render(console, &content_options)
.into_iter()
.collect();
let mut segments = Vec::with_capacity(rendered.len() + 1);
segments.push(Segment::styled(" ", line_style));
for seg in &rendered {
if seg.is_control() || seg.text.contains('\n') {
break; }
if seg.text.is_empty() {
continue;
}
let merged = line_style.combine(&seg.style.unwrap_or_default());
segments.push(Segment::styled(seg.text.clone(), merged));
}
adjust_line_length_no_bg(&segments, width)
}
fn first_selectable(&self) -> Option<usize> {
self.items.iter().position(|item| item.is_selectable())
}
fn last_selectable(&self) -> Option<usize> {
self.items
.iter()
.enumerate()
.rev()
.find(|(_, item)| item.is_selectable())
.map(|(i, _)| i)
}
fn selectable_count(&self) -> usize {
self.items
.iter()
.filter(|item| item.is_selectable())
.count()
}
fn max_offset(&self) -> usize {
self.items.len().saturating_sub(self.viewport_height.max(1))
}
fn clamp_offsets(&mut self) {
if self.items.is_empty() {
self.cursor.set_highlighted(None);
self.offset = 0;
self.hovered_index = None;
return;
}
self.offset = self.offset.min(self.max_offset());
if let Some(index) = self.hovered_index {
if index >= self.items.len() {
self.hovered_index = None;
}
}
}
fn ensure_visible(&mut self) {
self.clamp_offsets();
let Some(highlighted) = self.cursor.highlighted() else {
return;
};
let viewport = self.viewport_height.max(1);
if highlighted < self.offset {
self.offset = highlighted;
} else if highlighted >= self.offset + viewport {
self.offset = highlighted + 1 - viewport;
}
self.offset = self.offset.min(self.max_offset());
}
fn emit_highlighted(&self, ctx: &mut EventCtx) {
if let Some(index) = self.cursor.highlighted() {
ctx.post_message(Message::OptionHighlighted(OptionHighlighted { index }));
}
}
fn emit_selected(&self, ctx: &mut EventCtx) {
if let Some(index) = self.cursor.highlighted() {
ctx.post_message(Message::OptionSelected(OptionSelected { index }));
}
}
fn highlight_index(&mut self, index: usize, ctx: &mut EventCtx) {
if index >= self.items.len() {
return;
}
if !self.items[index].is_selectable() {
return;
}
let changed = self.cursor.highlighted() != Some(index);
self.cursor.set_highlighted(Some(index));
self.ensure_visible();
if changed {
self.emit_highlighted(ctx);
ctx.request_repaint();
}
}
fn move_highlight(&mut self, delta: isize, ctx: &mut EventCtx) {
if self.selectable_count() == 0 {
return;
}
if self.cursor.highlighted().is_none() {
let target = if delta.is_negative() {
self.last_selectable()
} else {
self.first_selectable()
};
if let Some(target) = target {
self.highlight_index(target, ctx);
}
return;
}
let current = self.cursor.highlighted().unwrap_or(0) as isize;
let max = (self.items.len() - 1) as isize;
let mut target = (current + delta).clamp(0, max) as usize;
let step: isize = if delta >= 0 { 1 } else { -1 };
while target < self.items.len() && !self.items[target].is_selectable() {
let next = target as isize + step;
if next < 0 || next > max {
return;
}
target = next as usize;
}
self.highlight_index(target, ctx);
}
fn page_step(&self) -> usize {
self.viewport_height.saturating_sub(1).max(1)
}
fn scroll_offset(&mut self, delta_rows: isize, ctx: &mut EventCtx) {
let before = self.offset;
if delta_rows.is_negative() {
self.offset = self.offset.saturating_sub(delta_rows.unsigned_abs());
} else {
self.offset = self.offset.saturating_add(delta_rows as usize);
}
self.offset = self.offset.min(self.max_offset());
if self.offset != before {
ctx.request_repaint();
ctx.set_handled();
}
}
fn confirm_selection(&mut self, ctx: &mut EventCtx) {
let Some(index) = self.cursor.highlighted() else {
return;
};
if !self.items[index].is_selectable() {
return;
}
self.emit_selected(ctx);
}
}
impl Widget for OptionList {
fn focusable(&self) -> bool {
!self.disabled
}
fn set_focus(&mut self, focused: bool) {
self.focused = focused && !self.disabled;
}
fn is_disabled(&self) -> bool {
self.disabled
}
fn has_focus(&self) -> bool {
self.focused
}
fn is_hovered(&self) -> bool {
self.hovered
}
fn set_hovered(&mut self, hovered: bool) {
self.hovered = hovered;
if !hovered {
self.hovered_index = None;
}
}
fn on_layout(&mut self, _width: u16, height: u16) {
self.viewport_height = usize::from(height).max(1);
self.ensure_visible();
}
fn on_event(&mut self, event: &Event, ctx: &mut EventCtx) {
if self.disabled {
return;
}
match event {
Event::MouseDown(mouse) if mouse.target == self.node_id() => {
let index = self.offset.saturating_add(mouse.y as usize);
if index < self.items.len() && self.items[index].is_selectable() {
self.highlight_index(index, ctx);
self.confirm_selection(ctx);
ctx.set_handled();
}
}
Event::Action(action) if self.focused => match action {
Action::ScrollUp => {
self.move_highlight(-1, ctx);
ctx.set_handled();
}
Action::ScrollDown => {
self.move_highlight(1, ctx);
ctx.set_handled();
}
Action::ScrollPageUp => {
if self.cursor.highlighted().is_none() {
if let Some(first) = self.first_selectable() {
self.highlight_index(first, ctx);
}
} else {
self.move_highlight(-(self.page_step() as isize), ctx);
}
ctx.set_handled();
}
Action::ScrollPageDown => {
if self.cursor.highlighted().is_none() {
if let Some(last) = self.last_selectable() {
self.highlight_index(last, ctx);
}
} else {
self.move_highlight(self.page_step() as isize, ctx);
}
ctx.set_handled();
}
_ => {}
},
Event::Key(key) if self.focused => match key.code {
KeyCode::Up => {
self.move_highlight(-1, ctx);
ctx.set_handled();
}
KeyCode::Down => {
self.move_highlight(1, ctx);
ctx.set_handled();
}
KeyCode::PageUp => {
if self.cursor.highlighted().is_none() {
if let Some(first) = self.first_selectable() {
self.highlight_index(first, ctx);
}
} else {
self.move_highlight(-(self.page_step() as isize), ctx);
}
ctx.set_handled();
}
KeyCode::PageDown => {
if self.cursor.highlighted().is_none() {
if let Some(last) = self.last_selectable() {
self.highlight_index(last, ctx);
}
} else {
self.move_highlight(self.page_step() as isize, ctx);
}
ctx.set_handled();
}
KeyCode::Home => {
if let Some(first) = self.first_selectable() {
self.highlight_index(first, ctx);
}
ctx.set_handled();
}
KeyCode::End => {
if let Some(last) = self.last_selectable() {
self.highlight_index(last, ctx);
}
ctx.set_handled();
}
KeyCode::Enter => {
self.confirm_selection(ctx);
ctx.set_handled();
}
_ => {}
},
Event::AppFocus(false) => {
if self.hovered || self.hovered_index.is_some() {
self.hovered = false;
self.hovered_index = None;
ctx.request_repaint();
}
}
_ => {}
}
}
fn on_mouse_move(&mut self, _x: u16, y: u16) -> bool {
if self.disabled {
return false;
}
if self.items.is_empty() {
return false;
}
let index = self.offset.saturating_add(y as usize);
let hovered = if index < self.items.len() && self.items[index].is_selectable() {
Some(index)
} else {
None
};
if hovered != self.hovered_index {
self.hovered_index = hovered;
return true;
}
false
}
fn on_mouse_scroll(&mut self, _delta_x: i32, delta_y: i32, ctx: &mut EventCtx) {
if self.disabled {
return;
}
if delta_y == 0 {
return;
}
self.scroll_offset(
delta_y.saturating_mul(self.scroll_step as i32) as isize,
ctx,
);
}
fn on_unmount(&mut self) {
self.hovered = false;
self.hovered_index = None;
}
fn render(&self, console: &Console, options: &ConsoleOptions) -> Segments {
let width = options.size.0.max(1);
let height = options.size.1.max(1);
let mut out = Segments::new();
let base_style = crate::css::resolve_component_style(self, &["option-list--option"])
.to_rich()
.unwrap_or_else(rich_rs::Style::new);
let (vis_start, vis_end) = self.visible_range(height);
for row in 0..height {
let index = vis_start + row;
if index >= vis_end {
let line = adjust_line_length_no_bg(&[], width);
out.extend(line);
} else if let Some(item) = self.items.get(index) {
match item {
OptionItem::Separator => {
let sep_style =
crate::css::resolve_component_style(self, &["option-list--separator"])
.to_rich()
.unwrap_or(base_style);
let line = adjust_line_length_no_bg(
&[Segment::styled("─".repeat(width), sep_style)],
width,
);
out.extend(line);
}
OptionItem::Option {
prompt,
content,
disabled,
..
} => {
let highlighted = self.cursor.highlighted() == Some(index);
let hovered = self.hovered_index == Some(index);
let mut classes = vec!["option-list--option"];
if highlighted {
classes.push("-highlighted");
}
if hovered && !highlighted {
classes.push("-hover");
}
if *disabled {
classes.push("-disabled");
}
if highlighted && self.focused {
classes.push("-focus");
}
let style = crate::css::resolve_component_style(self, &classes)
.to_rich()
.unwrap_or(base_style);
let line = if let Some(rich) = content {
self.render_rich_line(rich, style, width, console, options)
} else {
adjust_line_length_no_bg(
&[Segment::styled(format!(" {prompt}"), style)],
width,
)
};
out.extend(line);
}
}
} else {
let line = adjust_line_length_no_bg(&[], width);
out.extend(line);
}
if row + 1 < height {
out.push(Segment::line());
}
}
out
}
fn layout_height(&self) -> Option<usize> {
fixed_height_from_constraints(self.layout_constraints()).or(Some(self.items.len().max(1)))
}
fn content_width(&self) -> Option<usize> {
let content_width = self
.items
.iter()
.map(|item| match item {
OptionItem::Option {
prompt, content, ..
} => {
let text_width = if let Some(rich) = content {
rich.cell_len()
} else {
rich_rs::cell_len(prompt)
};
text_width.saturating_add(2) }
OptionItem::Separator => 3,
})
.max()
.unwrap_or(2)
.max(1);
let meta = crate::css::selector_meta_generic(self);
let resolved = crate::css::resolve_style(self, &meta);
let padding = resolved.effective_padding();
let (_, _, border_left, border_right) =
super::helpers::border_spacing_from_style(&resolved);
let chrome_lr =
usize::from(padding.left.saturating_add(padding.right)) + border_left + border_right;
Some(content_width.saturating_add(chrome_lr).max(1))
}
fn style_classes(&self) -> &[String] {
if self.focused {
&self.focused_classes
} else if self.classes.is_empty() {
empty_classes()
} else {
&self.classes
}
}
fn style_type(&self) -> &'static str {
"OptionList"
}
fn styles(&self) -> Option<&WidgetStyles> {
Some(&self.styles)
}
fn styles_mut(&mut self) -> Option<&mut WidgetStyles> {
Some(&mut self.styles)
}
}
impl Renderable for OptionList {
fn render(&self, console: &Console, options: &ConsoleOptions) -> Segments {
Widget::render(self, console, options)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::node_id::NodeId;
#[test]
fn option_list_navigation_skips_separators() {
let items = vec![
OptionItem::new("Alpha"),
OptionItem::Separator,
OptionItem::new("Beta"),
];
let mut list = OptionList::with_items(items);
list.set_focus(true);
list.on_layout(40, 10);
assert_eq!(list.highlighted(), Some(0));
let mut ctx = EventCtx::default();
list.move_highlight(1, &mut ctx);
assert_eq!(list.highlighted(), Some(2));
}
#[test]
fn option_list_navigation_skips_disabled() {
let items = vec![
OptionItem::new("Alpha"),
OptionItem::disabled("Bravo"),
OptionItem::new("Charlie"),
];
let mut list = OptionList::with_items(items);
list.set_focus(true);
list.on_layout(40, 10);
assert_eq!(list.highlighted(), Some(0));
let mut ctx = EventCtx::default();
list.move_highlight(1, &mut ctx);
assert_eq!(list.highlighted(), Some(2));
}
#[test]
fn option_list_home_end() {
let items = vec![
OptionItem::new("First"),
OptionItem::new("Middle"),
OptionItem::Separator,
OptionItem::new("Last"),
];
let mut list = OptionList::with_items(items);
list.set_focus(true);
list.on_layout(40, 10);
let mut ctx = EventCtx::default();
if let Some(last) = list.last_selectable() {
list.highlight_index(last, &mut ctx);
}
assert_eq!(list.highlighted(), Some(3));
if let Some(first) = list.first_selectable() {
list.highlight_index(first, &mut ctx);
}
assert_eq!(list.highlighted(), Some(0));
}
#[test]
fn option_list_confirm_emits_selected() {
let items = vec![OptionItem::new("Only")];
let mut list = OptionList::with_items(items);
list.set_focus(true);
list.on_layout(40, 10);
let mut ctx = EventCtx::default();
list.confirm_selection(&mut ctx);
let messages = ctx.take_messages();
assert!(messages.iter().any(|m| matches!(
m.message,
Message::OptionSelected(OptionSelected { index: 0 })
)));
}
#[test]
fn option_list_mouse_click_emits_highlighted_before_selected() {
let items = vec![OptionItem::new("Alpha"), OptionItem::new("Beta")];
let mut list = OptionList::with_items(items);
list.set_focus(true);
list.on_layout(40, 10);
let mut ctx = EventCtx::default();
list.on_event(
&Event::MouseDown(crate::event::MouseDownEvent {
target: NodeId::default(),
screen_x: 0,
screen_y: 1,
x: 0,
y: 1,
}),
&mut ctx,
);
assert!(ctx.handled());
let messages = ctx.take_messages();
let highlighted_pos = messages.iter().position(|m| {
matches!(
m.message,
Message::OptionHighlighted(OptionHighlighted { index: 1 })
)
});
let selected_pos = messages.iter().position(|m| {
matches!(
m.message,
Message::OptionSelected(OptionSelected { index: 1 })
)
});
assert!(
highlighted_pos.is_some() && selected_pos.is_some() && highlighted_pos < selected_pos
);
}
#[test]
fn option_list_clear_resets_state() {
let items = vec![OptionItem::new("A"), OptionItem::new("B")];
let mut list = OptionList::with_items(items);
list.set_highlighted(1);
list.clear_options();
assert_eq!(list.highlighted(), None);
assert_eq!(list.option_count(), 0);
}
#[test]
fn option_item_with_typed_id_round_trips() {
let item = OptionItem::with_id("Alpha", "alpha");
assert_eq!(item.string_id(), Some("alpha"));
}
#[test]
fn option_list_up_from_none_selects_last_enabled() {
let items = vec![
OptionItem::new("Alpha"),
OptionItem::disabled("Bravo"),
OptionItem::new("Charlie"),
];
let mut list = OptionList::with_items(items);
list.cursor.set_highlighted(None);
list.set_focus(true);
list.on_layout(40, 10);
let mut ctx = EventCtx::default();
list.move_highlight(-1, &mut ctx);
assert_eq!(list.highlighted(), Some(2));
}
#[test]
fn option_list_page_down_from_none_selects_last_enabled() {
let items = vec![
OptionItem::new("Alpha"),
OptionItem::disabled("Bravo"),
OptionItem::new("Charlie"),
];
let mut list = OptionList::with_items(items);
list.cursor.set_highlighted(None);
list.set_focus(true);
list.on_layout(40, 10);
let key = crate::keys::KeyEventData::from_crossterm(crossterm::event::KeyEvent::new(
KeyCode::PageDown,
crossterm::event::KeyModifiers::NONE,
));
let mut ctx = EventCtx::default();
list.on_event(&Event::Key(key), &mut ctx);
assert_eq!(list.highlighted(), Some(2));
assert!(ctx.handled());
}
#[test]
fn option_list_disabled_ignores_input() {
let items = vec![OptionItem::new("Alpha"), OptionItem::new("Beta")];
let mut list = OptionList::with_items(items).disabled(true);
list.set_focus(true);
list.on_layout(40, 10);
let before = list.highlighted();
let key = crate::keys::KeyEventData::from_crossterm(crossterm::event::KeyEvent::new(
KeyCode::Down,
crossterm::event::KeyModifiers::NONE,
));
let mut ctx = EventCtx::default();
list.on_event(&Event::Key(key), &mut ctx);
assert_eq!(list.highlighted(), before);
assert!(!ctx.handled());
assert!(!list.focusable());
}
#[test]
fn app_focus_loss_clears_hover_state() {
let items = vec![OptionItem::new("Alpha"), OptionItem::new("Beta")];
let mut list = OptionList::with_items(items);
list.set_hovered(true);
assert!(list.on_mouse_move(0, 0));
let mut ctx = EventCtx::default();
list.on_event(&Event::AppFocus(false), &mut ctx);
assert!(!list.is_hovered());
assert!(list.hovered_index.is_none());
assert!(ctx.repaint_requested());
}
#[test]
fn option_item_rich_stores_content() {
let content = rich_rs::Text::plain("Bold option");
let item = OptionItem::rich("Bold option", content);
assert!(item.content().is_some());
assert_eq!(item.prompt(), Some("Bold option"));
assert!(item.is_selectable());
}
#[test]
fn option_item_with_content_builder() {
let item = OptionItem::new("Plain").with_content(rich_rs::Text::plain("Rich"));
assert!(item.content().is_some());
assert_eq!(item.content().unwrap().plain_text(), "Rich");
}
#[test]
fn option_item_rich_with_id_stores_both() {
let content = rich_rs::Text::plain("Styled");
let item = OptionItem::rich_with_id("label", content, "my-id");
assert_eq!(item.string_id(), Some("my-id"));
assert!(item.content().is_some());
}
#[test]
fn option_list_add_rich_option() {
let mut list = OptionList::new();
list.add_rich_option(
"Bold",
rich_rs::Text::plain("Bold"),
Some(OptionId::new("b")),
false,
);
assert_eq!(list.option_count(), 1);
assert!(list.get_option(0).unwrap().content().is_some());
assert_eq!(list.highlighted(), Some(0));
}
#[test]
fn option_list_content_width_uses_rich_content() {
let content = rich_rs::Text::plain("Long rich content");
let items = vec![OptionItem::new("Short"), OptionItem::rich("Label", content)];
let list = OptionList::with_items(items);
let cw = list.content_width().unwrap();
assert_eq!(cw, 19);
}
#[test]
fn option_list_rich_render_produces_segments() {
let content = rich_rs::Text::plain("Hello");
let items = vec![OptionItem::rich("Hello", content)];
let list = OptionList::with_items(items);
let console = rich_rs::Console::new();
let options = rich_rs::ConsoleOptions {
size: (20, 1),
max_width: 20,
max_height: 1,
..Default::default()
};
let segments: Vec<_> = Widget::render(&list, &console, &options)
.into_iter()
.collect();
let text: String = segments.iter().map(|s| s.text.as_ref()).collect();
assert!(text.contains("Hello"));
}
#[test]
fn visible_range_clamps_to_item_count() {
let items = vec![OptionItem::new("A"), OptionItem::new("B")];
let list = OptionList::with_items(items);
let (start, end) = list.visible_range(10);
assert_eq!(start, 0);
assert_eq!(end, 2); }
#[test]
fn visible_range_respects_offset() {
let items: Vec<_> = (0..20)
.map(|i| OptionItem::new(format!("Item {i}")))
.collect();
let mut list = OptionList::with_items(items);
list.on_layout(40, 5);
list.set_highlighted(10);
let (start, end) = list.visible_range(5);
assert!(start <= 10);
assert!(end >= 10);
assert_eq!(end - start, 5);
}
#[test]
fn option_item_eq_ignores_rich_content() {
let a = OptionItem::new("Hello");
let b = OptionItem::new("Hello").with_content(rich_rs::Text::plain("Hello"));
assert_eq!(a, b);
}
fn make_node_id() -> NodeId {
use slotmap::SlotMap;
let mut sm: SlotMap<NodeId, ()> = SlotMap::new();
sm.insert(())
}
#[test]
fn mouse_click_with_dispatch_context_is_handled() {
use crate::runtime::dispatch_ctx::set_dispatch_recipient;
let items = vec![OptionItem::new("Alpha"), OptionItem::new("Beta")];
let mut list = OptionList::with_items(items);
list.set_focus(true);
list.on_layout(40, 10);
let id = make_node_id();
let _guard = set_dispatch_recipient(id);
let mut ctx = EventCtx::default();
list.on_event(
&Event::MouseDown(crate::event::MouseDownEvent {
target: id,
screen_x: 0,
screen_y: 1,
x: 0,
y: 1,
}),
&mut ctx,
);
assert!(ctx.handled());
}
#[test]
fn mouse_click_with_wrong_target_is_ignored() {
use crate::runtime::dispatch_ctx::set_dispatch_recipient;
use slotmap::SlotMap;
let items = vec![OptionItem::new("Alpha"), OptionItem::new("Beta")];
let mut list = OptionList::with_items(items);
list.set_focus(true);
list.on_layout(40, 10);
let mut sm: SlotMap<NodeId, ()> = SlotMap::new();
let my_id = sm.insert(());
let other_id = sm.insert(());
let _guard = set_dispatch_recipient(my_id);
let mut ctx = EventCtx::default();
list.on_event(
&Event::MouseDown(crate::event::MouseDownEvent {
target: other_id,
screen_x: 0,
screen_y: 1,
x: 0,
y: 1,
}),
&mut ctx,
);
assert!(!ctx.handled());
}
}