pub use super::render::register_input_coordinator_dropdown;
use super::render::{measure_flat, register_context_manager_dropdown};
use super::settings::DropdownSettings;
use super::state::DropdownState;
use super::types::{DropdownItem, DropdownRenderKind, DropdownView, DropdownViewKind, SubmenuWidth};
use crate::layout::docking::DockPanel;
use crate::input::core::coordinator::LayerId;
use crate::input::{Sense, WidgetKind};
use crate::layout::{CompositeKind, CompositeRegistration, DismissFrame, DispatchEvent, DropdownHandle, DropdownNode, EventBuilder, LayoutManager, LayoutNodeId, OverlayEntry, OverlayKind, WidgetNode};
use crate::render::RenderContext;
use crate::types::{OverflowMode, Rect, SizeMode, WidgetId};
pub struct ConsumeEventCtx {
pub cursor: (f64, f64),
pub frame_rect: Rect,
pub viewport: (f64, f64),
}
pub fn consume_event(
event: DispatchEvent,
state: &mut DropdownState,
host_id: &WidgetId,
_ctx: ConsumeEventCtx,
) -> Option<DispatchEvent> {
match event {
DispatchEvent::DropdownSubmenuToggle { ref dropdown, ref trigger_id } => {
if dropdown.id == *host_id {
if state.submenu_open.as_deref() == Some(trigger_id.as_str()) {
state.submenu_open = None;
} else {
state.submenu_open = Some(trigger_id.clone());
}
None
} else {
Some(event)
}
}
_ => Some(event),
}
}
pub fn register_layout_manager_dropdown<P: DockPanel>(
layout: &mut LayoutManager<P>,
render: &mut dyn RenderContext,
parent: LayoutNodeId,
slot_id: &str,
handle: &DropdownHandle,
overlay_rect: Rect,
anchor: Option<Rect>,
view: &mut DropdownView<'_>,
settings: &DropdownSettings,
kind: DropdownRenderKind,
) -> Option<DropdownNode> {
let id: WidgetId = handle.id.clone();
let mut state = layout.dropdowns_map_mut().remove(&id).unwrap_or_default();
layout.push_overlay(OverlayEntry {
id: slot_id.to_string(),
kind: OverlayKind::Dropdown,
rect: overlay_rect,
anchor,
});
let rect = overlay_rect;
let layer = LayerId::new("dropdown");
let z_order = layout.z_layers().dropdown as u32;
layout.push_dismiss_frame(DismissFrame {
z: z_order,
rect,
overlay_id: WidgetId(slot_id.to_owned()),
});
layout.ctx_mut().input.push_layer(layer.clone(), z_order, true);
let node_id = layout.tree_mut().add_widget(parent, WidgetNode { id: id.clone(), kind: WidgetKind::Dropdown, rect, sense: Sense::CLICK, label: None });
layout.dispatcher_mut().on_prefix(
format!("{}:item:", id.0),
EventBuilder::DropdownItem { handle: handle.clone() },
);
layout.dispatcher_mut().on_prefix(
format!("{}:sub-item:", id.0),
EventBuilder::DropdownItem { handle: handle.clone() },
);
layout.dispatcher_mut().on_prefix(
format!("{}:chev:submenu:", id.0),
EventBuilder::DropdownSubmenuToggleFromSuffix { handle: handle.clone() },
);
{
use crate::layout::ChevronStepDirection;
for (suffix, dir) in [
("chevron_up", ChevronStepDirection::Up),
("chevron_down", ChevronStepDirection::Down),
] {
let cid = WidgetId(format!("{}:{}", id.0, suffix));
layout.dispatcher_mut().on_exact(
format!("{}:{}", id.0, suffix),
EventBuilder::ChevronStep { chevron_id: cid, direction: dir },
);
}
}
state.sync_flat_hover_from_layout(layout, &id.0);
{
let main_prefix = format!("{}:item:", id.0);
let submenu_prefix = format!("{}:submenu:", id.0);
let chev_prefix = format!("{}:chev:submenu:", id.0);
let sub_prefix = format!("{}:sub-item:", id.0);
let hovered = layout.hovered_widget().map(|w| w.0.clone());
match hovered {
Some(h) if h.starts_with(&submenu_prefix) && !h.starts_with(&chev_prefix) => {
let rest = &h[submenu_prefix.len()..];
state.submenu_open = Some(rest.to_string());
}
Some(h) if h.starts_with(&chev_prefix) => {
}
Some(h) if h.starts_with(&sub_prefix) => {
}
Some(h) if h.starts_with(&main_prefix) => {
state.submenu_open = None;
}
_ => {}
}
}
register_context_manager_dropdown(
layout.ctx_mut(), render, id.clone(), rect, &mut state, view, settings, kind, &layer,
);
if let Some(win) = layout.last_window() {
let frame_top = rect.y;
let frame_bottom = rect.y + rect.height;
let win_bottom = win.y + win.height;
let win_top = win.y;
let clipped_top = frame_top < win_top;
let clipped_bottom = frame_bottom > win_bottom;
if clipped_top || clipped_bottom {
use crate::types::CompositeId;
let composite_id = CompositeId(id.clone());
let strip_h = 16.0_f64;
let visible_top = frame_top.max(win_top);
let visible_bottom = frame_bottom.min(win_bottom);
let up_rect = crate::types::Rect::new(rect.x, visible_top, rect.width, strip_h);
let dn_rect = crate::types::Rect::new(rect.x, visible_bottom - strip_h, rect.width, strip_h);
{
let coord = &mut layout.ctx_mut().input;
if clipped_top {
coord.register_child(
&composite_id,
format!("{}:chevron_up", id.0),
crate::input::WidgetKind::Button,
up_rect,
crate::input::Sense::CLICK | crate::input::Sense::HOVER,
);
}
if clipped_bottom {
coord.register_child(
&composite_id,
format!("{}:chevron_down", id.0),
crate::input::WidgetKind::Button,
dn_rect,
crate::input::Sense::CLICK | crate::input::Sense::HOVER,
);
}
}
use crate::ui::widgets::atomic::chevron::{
draw_chevron, settings::ChevronSettings,
types::{ChevronDirection, ChevronUseCase, ChevronView, ChevronVisualKind,
HitAreaPolicy, PlacementPolicy, VisibilityPolicy},
};
let bg = settings.theme.as_ref().bg();
let chev_settings = ChevronSettings::default();
if clipped_top {
render.set_fill_color(bg);
render.fill_rect(up_rect.x, up_rect.y, up_rect.width, up_rect.height);
let v = ChevronView {
direction: ChevronDirection::Up,
use_case: ChevronUseCase::PixelScrollStep,
visibility: VisibilityPolicy::WhenOverflow { has_more: true },
placement: PlacementPolicy::Overlay,
hit_area: HitAreaPolicy::Visual,
visual_kind: ChevronVisualKind::Stroked,
..Default::default()
};
draw_chevron(render, up_rect, &v, &chev_settings);
}
if clipped_bottom {
render.set_fill_color(bg);
render.fill_rect(dn_rect.x, dn_rect.y, dn_rect.width, dn_rect.height);
let v = ChevronView {
direction: ChevronDirection::Down,
use_case: ChevronUseCase::PixelScrollStep,
visibility: VisibilityPolicy::WhenOverflow { has_more: true },
placement: PlacementPolicy::Overlay,
hit_area: HitAreaPolicy::Visual,
visual_kind: ChevronVisualKind::Stroked,
..Default::default()
};
draw_chevron(render, dn_rect, &v, &chev_settings);
}
}
}
layout.push_composite_registration(CompositeRegistration {
kind: CompositeKind::Dropdown,
slot_id: slot_id.to_string(),
widget_id: id.clone(),
frame_rect: rect,
});
layout.dropdowns_map_mut().insert(id, state);
Some(DropdownNode(node_id))
}
pub fn open_dropdown_flat<'items, P: DockPanel>(
layout: &mut LayoutManager<P>,
render: &mut dyn RenderContext,
parent: LayoutNodeId,
slot_id: &str,
handle: &DropdownHandle,
items: &'items [DropdownItem<'items>],
settings: &DropdownSettings,
) -> Option<DropdownNode> {
let widget_id = &handle.id;
let (open, hovered_id, origin, anchor_rect, position_override) = {
let st = layout.dropdowns_map_mut().get(widget_id).map(|s| (
s.open,
s.hovered_id.clone(),
s.effective_origin(),
s.anchor_rect,
s.open_position_override,
));
match st {
Some(v) => v,
None => return None, }
};
if !open {
return None;
}
let (w, h) = measure_flat(items, settings);
let mut view = DropdownView {
anchor: anchor_rect,
position_override,
open: true,
kind: DropdownViewKind::Flat {
items,
hovered_id: hovered_id.as_deref(),
submenu_items: None,
submenu_hovered_id: None,
},
size_mode: SizeMode::AutoFit,
overflow: OverflowMode::Clip,
submenu_width: SubmenuWidth::Auto,
};
register_layout_manager_dropdown(
layout, render, parent,
slot_id, handle,
Rect::new(origin.0, origin.1, w, h),
None,
&mut view, settings,
DropdownRenderKind::Flat,
)
}
pub fn handle_dropdown_dismiss(
state: &DropdownState,
click_pos: (f64, f64),
main_rect: Rect,
submenu_rect: Option<Rect>,
) -> bool {
if !state.open {
return false;
}
let inside_main = main_rect.contains(click_pos.0, click_pos.1);
let inside_sub = submenu_rect
.map(|r| r.contains(click_pos.0, click_pos.1))
.unwrap_or(false);
!inside_main && !inside_sub
}
pub fn handle_dropdown_keyboard(
state: &mut DropdownState,
key: DropdownKey,
items: &[Option<&str>],
) -> DropdownKeyResult {
match key {
DropdownKey::Esc => {
state.close();
DropdownKeyResult::Close
}
DropdownKey::Enter => {
if let Some(ref id) = state.hovered_id {
DropdownKeyResult::Activate(id.clone())
} else {
DropdownKeyResult::None
}
}
DropdownKey::ArrowDown => {
let navigable: Vec<&str> = items.iter().filter_map(|o| *o).collect();
if navigable.is_empty() {
return DropdownKeyResult::None;
}
let next = match &state.hovered_id {
None => navigable[0].to_owned(),
Some(cur) => {
let pos = navigable.iter().position(|&s| s == cur.as_str());
let next_idx = pos.map(|i| (i + 1).min(navigable.len().saturating_sub(1))).unwrap_or(0);
navigable[next_idx].to_owned()
}
};
state.hovered_id = Some(next.clone());
DropdownKeyResult::Hovered(next)
}
DropdownKey::ArrowUp => {
let navigable: Vec<&str> = items.iter().filter_map(|o| *o).collect();
if navigable.is_empty() {
return DropdownKeyResult::None;
}
let next = match &state.hovered_id {
None => navigable[navigable.len().saturating_sub(1)].to_owned(),
Some(cur) => {
let pos = navigable.iter().position(|&s| s == cur.as_str());
let next_idx = pos.map(|i| i.saturating_sub(1)).unwrap_or(0);
navigable[next_idx].to_owned()
}
};
state.hovered_id = Some(next.clone());
DropdownKeyResult::Hovered(next)
}
DropdownKey::Tab => {
state.close();
DropdownKeyResult::Close
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DropdownKey {
ArrowDown,
ArrowUp,
Enter,
Esc,
Tab,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DropdownKeyResult {
Close,
Activate(String),
Hovered(String),
None,
}