use std::sync::mpsc;
use std::time::Instant;
use crate::draw::grid_width;
use crate::items::UiItem;
use crate::loader::{PressAction, UiMsg, load_menu_soft};
use crate::threader::FrameCtx;
use crate::widgets::{ItemState, TickCtx, item_state_for, tick_item};
#[derive(Default, Clone)]
pub struct NavState {
pub focused_id: Option<String>,
pub nav_focus: Option<usize>,
pub consumes_nav: bool,
}
impl NavState {
pub fn clear(&mut self) {
*self = Self::default();
}
}
pub struct Root {
pub items: Vec<(UiItem, ItemState)>,
}
impl Root {
pub(crate) const fn new() -> Self {
Self { items: Vec::new() }
}
pub(crate) fn add(&mut self, item: UiItem) {
let state = item_state_for(&item);
self.items.push((item, state));
}
}
pub fn check_messages(ctx: &mut FrameCtx) {
while let Ok(msg) = ctx.persistent.rx.try_recv() {
match msg {
UiMsg::SwitchRoot(name) => {
ctx.persistent.active_root = Some(name.clone());
ctx.persistent.nav = NavState::default();
ctx.persistent
.pending_actions
.push(crate::api::PaneAction::SwitchRoot(name));
}
UiMsg::SwitchTabPage { tab_id, page } => {
ctx.persistent.tab_pages.insert(tab_id, page);
}
UiMsg::Quit => {
ctx.persistent
.pending_actions
.push(crate::api::PaneAction::Quit);
}
UiMsg::Reload => {
let (Some(device), Some(queue)) =
(ctx.persistent.device.clone(), ctx.persistent.queue.clone())
else {
continue;
};
if let Ok(new_menu) = load_menu_soft(&ctx.persistent.ron_path) {
let tx = ctx.persistent.tx.clone();
let prev_active = ctx.persistent.active_root.clone();
let crate::threader::Persistent {
renderer, tex_reg, ..
} = &mut *ctx.persistent;
let (Some(renderer), Some(tex_reg)) = (renderer.as_mut(), tex_reg.as_mut())
else {
continue;
};
let (
new_styles,
new_roots,
new_active,
_textured_id,
new_default_style,
new_style_map,
) = crate::api::init_and_build(
renderer, &device, &queue, &new_menu, &tx, tex_reg,
);
ctx.persistent.styles = new_styles;
ctx.persistent.style_map = new_style_map;
ctx.persistent.active_root = prev_active
.filter(|name| new_roots.iter().any(|(k, _)| k == name))
.or(new_active);
ctx.persistent.roots = new_roots;
ctx.persistent.default_style = new_default_style;
ctx.persistent.nav = NavState::default();
ctx.persistent.clear_color = new_menu.clear_color;
}
}
UiMsg::Toast {
message,
duration,
x,
y,
width,
height,
} => {
crate::api::push_toast_into(ctx.persistent, message, duration, x, y, width, height);
}
other => {
if let Some(action) = crate::api::msg_to_action(other) {
ctx.persistent.pending_actions.push(action);
}
}
}
}
}
pub fn tick_dt(ctx: &mut FrameCtx) {
let now = Instant::now();
ctx.frame.dt = now
.duration_since(ctx.frame.last_tick)
.as_secs_f32()
.min(0.1);
ctx.frame.last_tick = now;
}
pub fn draw_background(ctx: &mut FrameCtx) {
let Some(ref bg) = ctx.persistent.background else {
return;
};
let (shader, tex, w, h) = (bg.shader, bg.texture, bg.width, bg.height);
let pw = ctx.frame.pw;
let ph = ctx.frame.ph;
let gw = grid_width(pw, ph);
let img_ar = w as f32 / h as f32;
let scr_ar = pw / ph;
let (draw_w, draw_h) = if img_ar > scr_ar {
(1080.0 * img_ar, 1080.0)
} else {
(gw, gw / img_ar)
};
ctx.frame.scene.push_image(
crate::draw::Rect::new(-draw_w * 0.5, -draw_h * 0.5, draw_w, draw_h),
shader,
tex,
0.0,
None,
);
}
pub fn clear_nav_on_mouse(ctx: &mut FrameCtx) {
if ctx.persistent.osk.open {
return;
}
if ctx.frame.input.left_just_pressed || ctx.frame.input.escape {
ctx.persistent.nav.clear();
}
}
pub fn resolve_edges(ctx: &mut FrameCtx) {
let name = match ctx.persistent.active_root.as_deref() {
Some(n) => n.to_string(),
None => return,
};
let Some(root) = ctx.persistent.roots.get_mut(&name) else {
return;
};
let pw = ctx.frame.pw;
let ph = ctx.frame.ph;
let hw = grid_width(pw, ph) * 0.5;
let hh = 540.0_f32;
for (item, _) in &mut root.items {
if let UiItem::Popout(p) = item {
match p.edge {
Some(crate::items::PopoutEdge::Left) => {
p.closed_x = -hw - p.width;
p.closed_y = -hh;
p.open_x = -hw;
p.open_y = -hh;
}
Some(crate::items::PopoutEdge::Right) => {
p.closed_x = hw;
p.closed_y = -hh;
p.open_x = hw - p.width;
p.open_y = -hh;
}
Some(crate::items::PopoutEdge::Top) => {
p.closed_x = -hw;
p.closed_y = -hh - p.height;
p.open_x = -hw;
p.open_y = -hh;
}
Some(crate::items::PopoutEdge::Bottom) => {
p.closed_x = -hw;
p.closed_y = hh;
p.open_x = -hw;
p.open_y = hh - p.height;
}
None => {}
}
}
}
}
fn compute_click_consumed(root: &Root, mx: f32, my: f32, pw: f32, ph: f32) -> bool {
root.items.iter().any(|(item, state)| match (item, state) {
(UiItem::Popout(p), crate::widgets::ItemState::Popout(ps)) if ps.open => {
let gw = grid_width(pw, ph);
let w = if p.full_span {
match p.edge {
Some(crate::items::PopoutEdge::Left | crate::items::PopoutEdge::Right) => {
p.width
}
_ => gw,
}
} else {
p.width
};
let x = if p.full_span
&& !matches!(
p.edge,
Some(crate::items::PopoutEdge::Left | crate::items::PopoutEdge::Right)
) {
-gw * 0.5
} else {
p.open_x
};
let h = if p.full_span
&& matches!(
p.edge,
Some(crate::items::PopoutEdge::Left | crate::items::PopoutEdge::Right)
) {
1080.0
} else {
p.height
};
mx >= x && mx <= x + w && my >= p.open_y && my <= p.open_y + h
}
(UiItem::Dropdown(dd), crate::widgets::ItemState::Dropdown(ds)) if ds.open => {
let list_h = dd.height * dd.items.len() as f32;
let total_h = dd.height + list_h;
mx >= dd.x && mx <= dd.x + dd.width && my >= dd.y && my <= dd.y + total_h
}
_ => false,
})
}
pub fn tick_all(ctx: &mut FrameCtx) {
let name = match ctx.persistent.active_root.as_deref() {
Some(n) => n.to_string(),
None => return,
};
if !ctx.persistent.roots.contains_key(&name) {
return;
}
let FrameCtx {
persistent,
frame,
standalone_pane,
} = ctx;
let dt = frame.dt;
let pw = frame.pw;
let ph = frame.ph;
let default_style = persistent.default_style;
let item_count = persistent.roots[&name].items.len();
let focusable = collect_focusable(&persistent.roots[&name].items);
let nav_focus = persistent.nav.nav_focus;
let focused_id = persistent.nav.focused_id.clone();
let tx = persistent.tx.clone();
let nav_per_item: Vec<Option<Vec<usize>>> = (0..item_count)
.map(|i| {
nav_focus
.and_then(|n| focusable.get(n))
.and_then(|(fi, path, _)| if *fi == i { Some(path.clone()) } else { None })
})
.collect();
let mut any_consumes_nav = false;
let mut expired_indices: Vec<usize> = Vec::new();
let mut requested_focus: Option<String> = None;
let click_consumed = {
let root = persistent.roots.get(&name).unwrap();
compute_click_consumed(root, frame.input.mouse_x, frame.input.mouse_y, pw, ph)
|| crate::keyboard::panel_hovered(
&persistent.osk,
frame.input.mouse_x,
frame.input.mouse_y,
frame.pw,
frame.ph,
)
};
for (i, focused_entry) in nav_per_item.iter().enumerate().take(item_count) {
let focused = focused_entry.is_some();
let focused_path = focused_entry.as_deref().unwrap_or(&[]);
let root = persistent.roots.get_mut(&name).unwrap();
let (item, state) = root.items.get_mut(i).unwrap();
let mut tick_ctx = TickCtx {
input: &frame.input,
dt,
registry: &persistent.styles,
scene: &mut frame.scene,
tex_registry: persistent.tex_reg.as_ref().map_or(
&crate::textures::DUMMY_TEX as &dyn crate::textures::TextureInfo,
|r| r as &dyn crate::textures::TextureInfo,
),
tx: &tx,
default_style,
clip: None,
pw,
ph,
click_consumed: click_consumed
&& !matches!(item, UiItem::Popout(_) | UiItem::Dropdown(_)),
osk_open: persistent.osk.open,
pane: standalone_pane
.as_deref_mut()
.or(persistent.renderer.as_mut()),
tab_pages: &persistent.tab_pages,
};
let result = tick_item(
item,
state,
&mut tick_ctx,
focused_id.as_ref(),
focused,
focused_path,
);
if focused && result.consumes_nav {
any_consumes_nav = true;
}
if result.expired {
expired_indices.push(i);
}
if let Some(id) = result.request_focus {
requested_focus = Some(id);
}
}
if !expired_indices.is_empty() {
let root = persistent.roots.get_mut(&name).unwrap();
for i in expired_indices.into_iter().rev() {
root.items.remove(i);
}
}
persistent.nav.consumes_nav = any_consumes_nav;
if let Some(id) = requested_focus {
let items = &persistent.roots[&name].items;
let focusable = collect_focusable(items);
let idx = focusable
.iter()
.position(|(i, _, _)| focusable_id(&items[*i].0).as_deref() == Some(&id));
persistent.nav.focused_id = Some(id);
persistent.nav.nav_focus = idx;
}
}
pub fn resolve_nav(ctx: &mut FrameCtx) {
let name = match ctx.persistent.active_root.as_deref() {
Some(n) => n.to_string(),
None => return,
};
let Some(root) = ctx.persistent.roots.get(&name) else {
return;
};
let input = &ctx.frame.input;
let focusable = collect_focusable(&root.items);
if focusable.is_empty() {
return;
}
let consumes = ctx.persistent.nav.consumes_nav;
let nav_lr = (input.arrow_left || input.arrow_right) && !consumes;
let nav_ud = (input.arrow_up || input.arrow_down) && !consumes;
let nav_any = input.tab || nav_lr || nav_ud;
if !nav_any {
return;
}
if let Some(cur) = ctx.persistent.nav.nav_focus
&& cur >= focusable.len()
{
ctx.persistent.nav.nav_focus = None;
}
let new_focus = ctx.persistent.nav.nav_focus.map_or_else(
|| {
focusable
.iter()
.position(|(item_idx, _, _)| {
matches!(&root.items[*item_idx].0, UiItem::Button(b) if b.nav_default)
})
.unwrap_or(0)
},
|cur| {
if input.tab {
if input.shift {
if cur == 0 {
focusable.len() - 1
} else {
cur - 1
}
} else {
(cur + 1) % focusable.len()
}
} else {
let (cx, cy) = focusable[cur].2;
let dir = if input.arrow_right && nav_lr {
Some((1.0_f32, 0.0_f32))
} else if input.arrow_left && nav_lr {
Some((-1.0, 0.0))
} else if input.arrow_down && nav_ud {
Some((0.0, 1.0))
} else if input.arrow_up && nav_ud {
Some((0.0, -1.0))
} else {
None
};
dir.and_then(|d| nearest_in_dir(&focusable, cur, cx, cy, d))
.unwrap_or(cur)
}
},
);
ctx.persistent.nav.nav_focus = Some(new_focus);
if let Some((item_idx, _, _)) = focusable.get(new_focus) {
ctx.persistent.nav.focused_id = focusable_id(&root.items[*item_idx].0);
}
}
pub fn present(
ctx: &mut FrameCtx,
encoder: Option<&mut wgpu::CommandEncoder>,
view: Option<&wgpu::TextureView>,
) {
let cursor = [
ctx.frame.input.mouse_x,
ctx.frame.input.mouse_y,
f32::from(u8::from(ctx.frame.input.left_pressed)),
];
let scene = &mut ctx.frame.scene;
let pw = ctx.frame.pw;
let ph = ctx.frame.ph;
let dt = ctx.frame.dt;
let crate::threader::Persistent {
renderer,
tex_reg,
device,
queue,
clear_color,
..
} = &mut *ctx.persistent;
let Some(renderer) = renderer.as_mut() else {
return;
};
let tex_reg = tex_reg.as_mut().unwrap();
if let (Some(encoder), Some(view)) = (encoder, view) {
renderer.render(
crate::draw::GpuFrame {
encoder,
view,
device: device.as_ref().unwrap(),
queue: queue.as_ref().unwrap(),
},
scene,
pw,
ph,
dt,
cursor,
*clear_color,
tex_reg,
);
} else {
renderer.present_standalone(scene, pw, ph, dt, cursor, *clear_color, tex_reg);
}
}
pub fn send_press(tx: &mpsc::Sender<UiMsg>, action: &PressAction, id: &str, debug: bool) {
match action {
PressAction::SwitchRoot(name) => send(tx, UiMsg::SwitchRoot(name.clone()), debug),
PressAction::SwitchTabPage { tab_id, page } => {
send(
tx,
UiMsg::SwitchTabPage {
tab_id: tab_id.clone(),
page: *page,
},
debug,
);
}
PressAction::Quit => send(tx, UiMsg::Quit, debug),
PressAction::Custom(tag) => send(tx, UiMsg::Custom(tag.clone()), debug),
PressAction::Print(s) => println!("{id}: {s}"),
PressAction::Toast {
message,
duration,
x,
y,
width,
height,
} => send(
tx,
UiMsg::Toast {
message: message.clone(),
duration: *duration,
x: *x,
y: *y,
width: *width,
height: *height,
},
debug,
),
PressAction::RadioSelect { .. } | PressAction::DropdownSelect { .. } => {}
}
}
fn send(tx: &mpsc::Sender<UiMsg>, msg: UiMsg, debug: bool) {
if debug {
println!("[pane_ui] {msg:?}");
}
let _ = tx.send(msg);
}
#[inline]
fn to_absolute(x: f32, y: f32, ox: f32, oy: f32) -> (f32, f32) {
(x + ox, y + oy)
}
pub fn collect_focusable(items: &[(UiItem, ItemState)]) -> Vec<(usize, Vec<usize>, (f32, f32))> {
let mut out = Vec::new();
collect_focusable_in(items, &mut out, 0.0, 0.0);
out
}
fn collect_scroll_axis_items(
horizontal: bool,
container_x: f32,
container_y: f32,
pad_left: f32,
pad_top: f32,
gap: f32,
scroll: f32,
items: &[UiItem],
parent_i: usize,
out: &mut Vec<(usize, Vec<usize>, (f32, f32))>,
ox: f32,
oy: f32,
) {
if horizontal {
let ay = container_y + oy + pad_top;
let mut cx = container_x + ox + pad_left - scroll;
for (ci, child) in items.iter().enumerate() {
let w = item_width(child);
if let Some(c) = item_center_at(child, cx, ay) {
out.push((parent_i, vec![ci], c));
}
cx += w + gap;
}
} else {
let ax = container_x + ox + pad_left;
let ay = container_y + oy;
let mut cy = pad_top - scroll;
for (ci, child) in items.iter().enumerate() {
let h = item_height(child);
if let Some(c) = item_center_at(child, ax, ay + cy) {
out.push((parent_i, vec![ci], c));
}
cy += h + gap;
}
}
}
fn collect_pane_children(
horizontal: bool,
container_x: f32,
container_y: f32,
pad_left: f32,
pad_top: f32,
gap: f32,
scroll: f32,
items: &[UiItem],
states: &[crate::widgets::ItemState],
parent_i: usize,
path_prefix: &[usize],
out: &mut Vec<(usize, Vec<usize>, (f32, f32))>,
ox: f32,
oy: f32,
) {
if horizontal {
let ay = container_y + oy + pad_top;
let mut cx = container_x + ox + pad_left - scroll;
for (ci, (child, child_state)) in items.iter().zip(states.iter()).enumerate() {
let w = item_width(child);
let mut path = path_prefix.to_vec();
path.push(ci);
if let (UiItem::ScrollPane(sp), crate::widgets::ItemState::ScrollPane(ss)) =
(child, child_state)
{
collect_pane_children(
sp.horizontal,
cx,
ay,
sp.pad_left,
sp.pad_top,
sp.gap,
ss.scroll,
&sp.items,
&ss.children,
parent_i,
&path,
out,
0.0,
0.0,
);
} else if let Some(c) = item_center_at(child, cx, ay) {
out.push((parent_i, path, c));
}
cx += w + gap;
}
} else {
let ax = container_x + ox + pad_left;
let ay = container_y + oy;
let mut cy = pad_top - scroll;
for (ci, (child, child_state)) in items.iter().zip(states.iter()).enumerate() {
let h = item_height(child);
let mut path = path_prefix.to_vec();
path.push(ci);
if let (UiItem::ScrollPane(sp), crate::widgets::ItemState::ScrollPane(ss)) =
(child, child_state)
{
collect_pane_children(
sp.horizontal,
ax,
ay + cy,
sp.pad_left,
sp.pad_top,
sp.gap,
ss.scroll,
&sp.items,
&ss.children,
parent_i,
&path,
out,
0.0,
0.0,
);
} else if let Some(c) = item_center_at(child, ax, ay + cy) {
out.push((parent_i, path, c));
}
cy += h + gap;
}
}
}
fn collect_focusable_in(
items: &[(UiItem, ItemState)],
out: &mut Vec<(usize, Vec<usize>, (f32, f32))>,
ox: f32,
oy: f32,
) {
for (i, (item, state)) in items.iter().enumerate() {
match (item, state) {
(UiItem::ScrollList(list), ItemState::ScrollList(ss)) => {
collect_scroll_axis_items(
list.horizontal,
list.x,
list.y,
list.pad_left,
list.pad_top,
list.gap,
ss.scroll,
&list.items,
i,
out,
ox,
oy,
);
}
(UiItem::ScrollPane(pane), ItemState::ScrollPane(ss)) => {
collect_pane_children(
pane.horizontal,
pane.x,
pane.y,
pane.pad_left,
pane.pad_top,
pane.gap,
ss.scroll,
&pane.items,
&ss.children,
i,
&[],
out,
ox,
oy,
);
}
(UiItem::Tab(tab), ItemState::Tab(ts)) => {
let page = ts.active_page.min(tab.pages.len().saturating_sub(1));
if let Some(page_def) = tab.pages.get(page) {
collect_scroll_axis_items(
false,
tab.x,
tab.y,
tab.pad_left,
tab.pad_top,
tab.gap,
ts.page_scrolls[page],
&page_def.items,
i,
out,
ox,
oy,
);
}
}
(UiItem::RadioGroup(rg), _) => {
for (ci, btn) in rg.items.iter().enumerate() {
let c = (
btn.width.mul_add(0.5, rg.x + btn.x) + ox,
btn.height.mul_add(0.5, rg.y + btn.y) + oy,
);
out.push((i, vec![ci], c));
}
}
(UiItem::Dropdown(dd), ItemState::Dropdown(ds)) if ds.open => {
for (ci, btn) in dd.items.iter().enumerate() {
let c = (
btn.width.mul_add(0.5, dd.x + btn.x) + ox,
btn.height.mul_add(0.5, dd.y + btn.y) + oy,
);
out.push((i, vec![ci], c));
}
}
(UiItem::Bar(_), ItemState::Bar(bs)) => {
for (ci, &(cx, cy)) in bs.button_centers.iter().enumerate() {
out.push((i, vec![ci], (cx + ox, cy + oy)));
}
}
(UiItem::Popout(p), ItemState::Popout(ps)) if ps.open => {
if ps.child_centers.is_empty() {
for (ci, child) in p.items.iter().enumerate() {
let (ax, ay) = to_absolute(p.open_x, p.open_y, ox, oy);
if let Some(c) = item_center_at(child, ax, ay) {
out.push((i, vec![ci], c));
}
}
} else {
for (ci, &(cx, cy)) in ps.child_centers.iter().enumerate() {
out.push((i, vec![ci], (cx + ox, cy + oy)));
}
}
}
(UiItem::Popout(p), _) => {
let (cx, cy) = match p.edge {
Some(crate::items::PopoutEdge::Left) => (-800.0_f32, 0.0_f32),
Some(crate::items::PopoutEdge::Right) => (800.0, 0.0),
Some(crate::items::PopoutEdge::Top) => (0.0, -600.0),
Some(crate::items::PopoutEdge::Bottom) => (0.0, 600.0),
None => {
if let Some(pos) = p.items.iter().find_map(|child| {
if let UiItem::Button(b) = child
&& b.id == p.toggle_id
{
return Some(to_absolute(
b.width.mul_add(0.5, p.open_x + b.x),
b.height.mul_add(0.5, p.open_y + b.y),
ox,
oy,
));
}
None
}) {
pos
} else {
return;
}
}
};
out.push((i, vec![], (cx + ox, cy + oy)));
}
_ => {
if let Some(c) = item_center_offset(item, ox, oy) {
out.push((i, vec![], c));
}
}
}
}
}
pub const fn item_x(item: &UiItem) -> f32 {
match item {
UiItem::Button(b) => b.x,
UiItem::Toggle(t) => t.x,
UiItem::Slider(s) => s.x,
UiItem::TextBox(tb) => tb.x,
UiItem::Divider(d) => d.x,
UiItem::Image(img) => img.x,
UiItem::ProgressBar(p) => p.x,
UiItem::ScrollList(l) => l.x,
UiItem::ScrollPane(s) => s.x,
UiItem::Dropdown(d) => d.x,
UiItem::Tab(t) => t.x,
_ => 0.0,
}
}
pub const fn item_y(item: &UiItem) -> f32 {
match item {
UiItem::Button(b) => b.y,
UiItem::Toggle(t) => t.y,
UiItem::Slider(s) => s.y,
UiItem::TextBox(tb) => tb.y,
UiItem::Divider(d) => d.y,
UiItem::Image(img) => img.y,
UiItem::ProgressBar(p) => p.y,
UiItem::ScrollList(l) => l.y,
UiItem::ScrollPane(s) => s.y,
UiItem::Dropdown(d) => d.y,
UiItem::Tab(t) => t.y,
_ => 0.0,
}
}
pub const fn item_width(item: &UiItem) -> f32 {
match item {
UiItem::Button(b) => b.width,
UiItem::Toggle(t) => t.width,
UiItem::Slider(s) => s.width,
UiItem::TextBox(tb) => tb.width,
UiItem::Divider(d) => d.width,
UiItem::Image(img) => img.width,
UiItem::ProgressBar(p) => p.width,
UiItem::ScrollList(l) => l.width,
UiItem::ScrollPane(s) => s.width,
UiItem::Dropdown(d) => d.width,
UiItem::Tab(t) => t.width,
_ => 0.0,
}
}
pub fn item_height(item: &UiItem) -> f32 {
match item {
UiItem::Button(b) => b.height,
UiItem::Toggle(t) => t.height,
UiItem::Slider(s) => s.height,
UiItem::TextBox(tb) => tb.height,
UiItem::Label(l) => l.size * 1.4,
UiItem::Divider(d) => d.height,
UiItem::Image(img) => img.height,
UiItem::ProgressBar(p) => p.height,
UiItem::ScrollList(l) => l.height,
UiItem::ScrollPane(s) => s.height,
UiItem::Dropdown(d) => d.height,
UiItem::Tab(t) => t.height,
_ => 0.0,
}
}
const fn item_center_at(item: &UiItem, ax: f32, ay: f32) -> Option<(f32, f32)> {
match item {
UiItem::Button(b) => Some((b.width.mul_add(0.5, ax), b.height.mul_add(0.5, ay))),
UiItem::Toggle(t) => Some((t.width.mul_add(0.5, ax), t.height.mul_add(0.5, ay))),
UiItem::Slider(s) => Some((s.width.mul_add(0.5, ax), s.height.mul_add(0.5, ay))),
UiItem::TextBox(tb) => Some((tb.width.mul_add(0.5, ax), tb.height.mul_add(0.5, ay))),
UiItem::Dropdown(d) => Some((d.width.mul_add(0.5, ax), d.height.mul_add(0.5, ay))),
_ => None,
}
}
fn item_center_offset(item: &UiItem, ox: f32, oy: f32) -> Option<(f32, f32)> {
match item {
UiItem::Button(b) => Some(to_absolute(
b.width.mul_add(0.5, b.x),
b.height.mul_add(0.5, b.y),
ox,
oy,
)),
UiItem::Toggle(t) => Some(to_absolute(
t.width.mul_add(0.5, t.x),
t.height.mul_add(0.5, t.y),
ox,
oy,
)),
UiItem::Slider(s) => Some(to_absolute(
s.width.mul_add(0.5, s.x),
s.height.mul_add(0.5, s.y),
ox,
oy,
)),
UiItem::TextBox(tb) => Some(to_absolute(
tb.width.mul_add(0.5, tb.x),
tb.height.mul_add(0.5, tb.y),
ox,
oy,
)),
UiItem::Dropdown(d) => Some(to_absolute(
d.width.mul_add(0.5, d.x),
d.height.mul_add(0.5, d.y),
ox,
oy,
)),
_ => None,
}
}
pub fn focusable_id(item: &UiItem) -> Option<String> {
match item {
UiItem::TextBox(tb) => Some(tb.id.clone()),
UiItem::Button(b) => Some(b.id.clone()),
UiItem::Toggle(t) => Some(t.id.clone()),
UiItem::Slider(s) => Some(s.id.clone()),
UiItem::Dropdown(d) => Some(d.id.clone()),
UiItem::RadioGroup(r) => Some(r.id.clone()),
UiItem::ScrollPane(s) => Some(s.id.clone()),
UiItem::ScrollList(l) => Some(l.id.clone()),
UiItem::Bar(b) => Some(b.id.clone()),
UiItem::Tab(t) => Some(t.id.clone()),
_ => None,
}
}
pub fn nearest_in_dir(
focusable: &[(usize, Vec<usize>, (f32, f32))],
cur: usize,
cx: f32,
cy: f32,
dir: (f32, f32),
) -> Option<usize> {
let mut best_idx = None;
let mut best_score = f32::MAX;
for (fi, (_, _, (nx, ny))) in focusable.iter().enumerate() {
if fi == cur {
continue;
}
let dx = nx - cx;
let dy = ny - cy;
let dot = dx.mul_add(dir.0, dy * dir.1);
if dot <= 0.0 {
continue;
}
let dist = dx.hypot(dy);
let alignment = dot / dist;
let score = dist * dist / (alignment + 0.01);
if score < best_score {
best_score = score;
best_idx = Some(fi);
}
}
best_idx
}
pub fn press_in_item(tx: &mpsc::Sender<UiMsg>, item: &mut UiItem, id: &str, debug: bool) -> bool {
match item {
UiItem::Button(btn) if btn.id == id => {
send_press(tx, &btn.action, &btn.id, debug);
true
}
UiItem::ScrollList(list) => {
for child in &mut list.items {
if press_in_item(tx, child, id, debug) {
return true;
}
}
false
}
UiItem::Bar(b) => {
for child in &mut b.items {
if press_in_item(tx, child, id, debug) {
return true;
}
}
false
}
UiItem::Popout(p) => {
for child in &mut p.items {
if press_in_item(tx, child, id, debug) {
return true;
}
}
false
}
UiItem::ScrollPane(pane) => {
for child in &mut pane.items {
if press_in_item(tx, child, id, debug) {
return true;
}
}
false
}
UiItem::Tab(tab) => {
for page in &mut tab.pages {
for child in &mut page.items {
if press_in_item(tx, child, id, debug) {
return true;
}
}
}
false
}
_ => false,
}
}