use crate::anim::{Anim, Spring, SpringValue};
use crate::draw::Rect;
use crate::items::UiItem;
use crate::loader::UiMsg;
use crate::styles::{DrawLabel, StyleCtx, StyleId, Transition, push_component};
use crate::threader::FrameCtx;
use crate::widgets::{ItemState, WidgetState};
fn any_nav_grabbed_in(id: &str, items: &[UiItem], states: &[ItemState]) -> bool {
for (item, state) in items.iter().zip(states.iter()) {
match (item, state) {
(UiItem::TextBox(tb), ItemState::TextBox(ts)) if tb.id == id => {
if ts.nav_grabbed {
return true;
}
}
(UiItem::ScrollPane(p), ItemState::ScrollPane(s)) => {
if any_nav_grabbed_in(id, &p.items, &s.children) {
return true;
}
}
(UiItem::Tab(t), ItemState::Tab(s)) => {
for (page, page_states) in t.pages.iter().zip(s.children.iter()) {
if any_nav_grabbed_in(id, &page.items, page_states) {
return true;
}
}
}
_ => {}
}
}
false
}
pub fn any_textbox_nav_grabbed(id: &str, items: &[(UiItem, ItemState)]) -> bool {
for (item, state) in items {
match (item, state) {
(UiItem::TextBox(tb), ItemState::TextBox(ts)) if tb.id == id => {
if ts.nav_grabbed {
return true;
}
}
(UiItem::ScrollPane(p), ItemState::ScrollPane(s)) => {
if any_nav_grabbed_in(id, &p.items, &s.children) {
return true;
}
}
(UiItem::Tab(t), ItemState::Tab(s)) => {
for (page, page_states) in t.pages.iter().zip(s.children.iter()) {
if any_nav_grabbed_in(id, &page.items, page_states) {
return true;
}
}
}
_ => {}
}
}
false
}
fn with_textbox_in<F>(id: &str, items: &mut [UiItem], states: &mut [ItemState], f: &mut F) -> bool
where
F: FnMut(&mut crate::items::TextBox, &mut crate::widgets::TextBoxState),
{
for (item, state) in items.iter_mut().zip(states.iter_mut()) {
match (item, state) {
(UiItem::TextBox(tb), ItemState::TextBox(ts)) if tb.id == id => {
f(tb, ts);
return true;
}
(UiItem::ScrollPane(p), ItemState::ScrollPane(s)) => {
if with_textbox_in(id, &mut p.items, &mut s.children, f) {
return true;
}
}
(UiItem::Tab(t), ItemState::Tab(s)) => {
for (page, page_states) in t.pages.iter_mut().zip(s.children.iter_mut()) {
if with_textbox_in(id, &mut page.items, page_states, f) {
return true;
}
}
}
_ => {}
}
}
false
}
pub fn with_textbox_mut<F>(id: &str, items: &mut [(UiItem, ItemState)], mut f: F) -> bool
where
F: FnMut(&mut crate::items::TextBox, &mut crate::widgets::TextBoxState),
{
for (item, state) in items.iter_mut() {
match (item, state) {
(UiItem::TextBox(tb), ItemState::TextBox(ts)) if tb.id == id => {
f(tb, ts);
return true;
}
(UiItem::ScrollPane(p), ItemState::ScrollPane(s)) => {
if with_textbox_in(id, &mut p.items, &mut s.children, &mut f) {
return true;
}
}
(UiItem::Tab(t), ItemState::Tab(s)) => {
for (page, page_states) in t.pages.iter_mut().zip(s.children.iter_mut()) {
if with_textbox_in(id, &mut page.items, page_states, &mut f) {
return true;
}
}
}
_ => {}
}
}
false
}
const KEY_W: f32 = 80.0;
const KEY_H: f32 = 70.0;
const KEY_GAP: f32 = 8.0;
const ROW_GAP: f32 = 10.0;
const PAD: f32 = 20.0;
const MOD_KEY_W: f32 = 124.0;
const SPECIAL_KEY_W: f32 = 240.0;
const Z_OSK: f32 = 4.0;
const Z_OSK_KEY: f32 = 4.5;
const BASE_CONTENT_W: f32 = 10.0 * KEY_W + 9.0 * KEY_GAP; const BASE_PANEL_W: f32 = 2.0 * PAD + BASE_CONTENT_W; const BASE_PANEL_H: f32 = 2.0 * PAD + 4.0 * KEY_H + 3.0 * ROW_GAP;
const ALPHA_ROWS: [&[&str]; 4] = [
&["q", "w", "e", "r", "t", "y", "u", "i", "o", "p"],
&["a", "s", "d", "f", "g", "h", "j", "k", "l"],
&["SHIFT", "z", "x", "c", "v", "b", "n", "m", "BACK"],
&["SYM", "SPACE", "DONE"],
];
const SHIFT_ROWS: [&[&str]; 4] = [
&["Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P"],
&["A", "S", "D", "F", "G", "H", "J", "K", "L"],
&["SHIFT", "Z", "X", "C", "V", "B", "N", "M", "BACK"],
&["SYM", "SPACE", "DONE"],
];
const SYM_ROWS: [&[&str]; 4] = [
&["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"],
&["!", "@", "#", "$", "%", "^", "&", "*", "-", "+"],
&["ABC", "(", ")", "_", "=", "[", "]", ";", "BACK"],
&["SYM", "SPACE", "DONE"],
];
const fn get_rows(layer: OskLayer, shift: bool) -> &'static [&'static [&'static str]; 4] {
match (layer, shift) {
(OskLayer::Alpha, false) => &ALPHA_ROWS,
(OskLayer::Alpha, true) => &SHIFT_ROWS,
(OskLayer::Symbols, _) => &SYM_ROWS,
}
}
const ANIM_STRIDE: usize = 12;
const MAX_KEYS: usize = 4 * ANIM_STRIDE;
const fn key_index(row: usize, col: usize) -> usize {
row * ANIM_STRIDE + col
}
const FILL: f32 = 0.30;
fn osk_scale(_pw: f32, _ph: f32) -> f32 {
1080.0 * FILL / BASE_PANEL_H
}
fn panel_y(slide_t: f32, scaled_ph: f32) -> f32 {
let closed = 540.0 + 10.0;
let open = 540.0 - scaled_ph - 10.0;
(open - closed).mul_add(slide_t.clamp(0.0, 1.0), closed)
}
fn key_rect(row: usize, col: usize, rows: &[&[&str]; 4], px: f32, py: f32, s: f32) -> Rect {
let cx = PAD.mul_add(s, px);
let cy = PAD.mul_add(s, py);
let row_y = (row as f32).mul_add((KEY_H + ROW_GAP) * s, cy);
let cw = BASE_CONTENT_W * s;
match row {
3 => {
let sw = SPECIAL_KEY_W * s;
let kg = KEY_GAP * s;
let total = 3.0f32.mul_add(sw, 2.0 * kg);
let start_x = (cw - total).mul_add(0.5, cx);
Rect {
x: (col as f32).mul_add(sw + kg, start_x),
y: row_y,
w: sw,
h: KEY_H * s,
}
}
2 => {
let mod_w = MOD_KEY_W * s;
let kw = KEY_W * s;
if col == 0 {
Rect {
x: cx,
y: row_y,
w: mod_w,
h: KEY_H * s,
}
} else if col == rows[2].len() - 1 {
let x = 7.0f32
.mul_add(KEY_W, 8.0f32.mul_add(KEY_GAP, MOD_KEY_W))
.mul_add(s, cx);
Rect {
x,
y: row_y,
w: mod_w,
h: KEY_H * s,
}
} else {
let x = ((col - 1) as f32)
.mul_add(KEY_W, (col as f32).mul_add(KEY_GAP, MOD_KEY_W))
.mul_add(s, cx);
Rect {
x,
y: row_y,
w: kw,
h: KEY_H * s,
}
}
}
_ => {
let kw = KEY_W * s;
let kg = KEY_GAP * s;
let n = rows[row].len() as f32;
let row_w = n.mul_add(kw, (n - 1.0) * kg);
let start_x = (cw - row_w).mul_add(0.5, cx);
Rect {
x: (col as f32).mul_add(kw + kg, start_x),
y: row_y,
w: kw,
h: KEY_H * s,
}
}
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum OskLayer {
Alpha,
Symbols,
}
pub struct OskState {
pub open: bool,
pub row: usize,
pub col: usize,
pub layer: OskLayer,
pub shift: bool,
pub slide: SpringValue,
pub key_anims: Vec<Anim>,
}
impl Default for OskState {
fn default() -> Self {
Self {
open: false,
row: 0,
col: 0,
layer: OskLayer::Alpha,
shift: false,
slide: SpringValue::new(0.0, Spring::BOUNCY),
key_anims: (0..MAX_KEYS).map(|_| Anim::new(Spring::BOUNCY)).collect(),
}
}
}
pub fn panel_hovered(osk: &OskState, mx: f32, my: f32, pw: f32, ph: f32) -> bool {
let slide_t = osk.slide.t();
if slide_t <= 0.001 {
return false;
}
let s = osk_scale(pw, ph);
let spw = BASE_PANEL_W * s;
let sph = BASE_PANEL_H * s;
let px = -spw * 0.5;
let py = panel_y(slide_t, sph);
Rect {
x: px,
y: py,
w: spw,
h: sph,
}
.contains(mx, my)
}
pub fn tick_osk(ctx: &mut FrameCtx) {
let Some(style_id) = ctx.persistent.default_style else {
return;
};
let focused_id = ctx.persistent.nav.focused_id.clone();
let active = ctx.persistent.active_root.clone();
let nav_grabbed = match (&focused_id, &active) {
(Some(id), Some(name)) => ctx
.persistent
.roots
.get(name)
.is_some_and(|root| any_textbox_nav_grabbed(id, &root.items)),
_ => false,
};
let hw_present = ctx.frame.input.keyboard_present();
let want_open = nav_grabbed && !hw_present;
let just_opened = want_open && !ctx.persistent.osk.open;
if just_opened {
ctx.persistent.osk.open = true;
ctx.persistent.osk.row = 0;
ctx.persistent.osk.col = 0;
ctx.persistent.osk.layer = OskLayer::Alpha;
ctx.persistent.osk.shift = false;
} else if !want_open && ctx.persistent.osk.open {
ctx.persistent.osk.open = false;
}
let dt = ctx.frame.dt;
let target = if ctx.persistent.osk.open { 1.0 } else { 0.0 };
ctx.persistent.osk.slide.update(target, dt);
if ctx.persistent.osk.open {
ctx.persistent.nav.consumes_nav = true;
}
let slide_t = ctx.persistent.osk.slide.t();
if slide_t <= 0.001 {
return;
}
let s = osk_scale(ctx.frame.pw, ctx.frame.ph);
let pw = BASE_PANEL_W * s;
let ph = BASE_PANEL_H * s;
let px = -pw * 0.5;
let py = panel_y(slide_t, ph);
draw_osk(ctx, style_id, px, py, pw, ph, s, dt);
handle_osk_input(
ctx,
px,
py,
pw,
ph,
s,
focused_id.as_deref(),
active.as_deref(),
just_opened,
);
}
fn draw_osk(
ctx: &mut FrameCtx,
style_id: StyleId,
px: f32,
py: f32,
pw: f32,
ph: f32,
s: f32,
dt: f32,
) {
let osk_row = ctx.persistent.osk.row;
let osk_col = ctx.persistent.osk.col;
let osk_open = ctx.persistent.osk.open;
let osk_layer = ctx.persistent.osk.layer;
let osk_shift = ctx.persistent.osk.shift;
let mx = ctx.frame.input.mouse_x;
let my = ctx.frame.input.mouse_y;
let mouse_held = ctx.frame.input.left_pressed;
let nav_confirming = osk_open && (ctx.frame.input.space || ctx.frame.input.enter);
{
let registry = &ctx.persistent.styles;
let tex: &dyn crate::textures::TextureInfo = ctx
.persistent
.tex_reg
.as_ref()
.map_or(&crate::textures::DUMMY_TEX, |r| r);
let scene = &mut ctx.frame.scene;
registry.draw(
style_id,
Rect {
x: px,
y: py,
w: pw,
h: ph,
},
WidgetState::default(),
DrawLabel {
label: None,
z: Z_OSK,
clip: None,
alpha: 1.0,
},
scene,
tex,
);
}
let rows = get_rows(osk_layer, osk_shift);
for (r, row_keys) in rows.iter().enumerate() {
for (c, &key_label) in row_keys.iter().enumerate() {
let rect = key_rect(r, c, rows, px, py, s);
let is_focused = osk_open && r == osk_row && c == osk_col;
let is_hovered = is_focused || rect.contains(mx, my);
let is_pressed = (is_hovered && mouse_held) || (is_focused && nav_confirming);
let modifier_active = (key_label == "SHIFT" && osk_shift)
|| (key_label == "SYM" && osk_layer == OskLayer::Symbols);
let visual = WidgetState {
focused: is_focused,
hovered: is_hovered || modifier_active,
pressed: is_pressed,
..WidgetState::default()
};
let ki = key_index(r, c);
ctx.persistent.osk.key_anims[ki].transition(visual);
ctx.persistent.osk.key_anims[ki].update(dt);
let from = ctx.persistent.osk.key_anims[ki].prev_state();
let t = ctx.persistent.osk.key_anims[ki].t();
let registry = &ctx.persistent.styles;
let tex: &dyn crate::textures::TextureInfo = ctx
.persistent
.tex_reg
.as_ref()
.map_or(&crate::textures::DUMMY_TEX, |r| r);
let scene = &mut ctx.frame.scene;
let vs = push_component(
scene,
StyleCtx {
registry,
tex_registry: tex,
},
style_id,
Transition {
from,
to: visual,
t,
},
rect,
Z_OSK_KEY,
None,
);
let font_size = vs
.font_size
.unwrap_or_else(|| (rect.h * 0.45).clamp(10.0, 48.0));
let color = vs.text_color.unwrap_or(crate::draw::Color::WHITE);
scene.push_text(&crate::draw::TextDraw {
text: key_label,
x: rect.x + vs.text_offset_x,
y: (rect.h - font_size).mul_add(0.5, rect.y) + vs.text_offset_y,
w: rect.w,
size: font_size,
color,
align: vs.text_align,
font: vs.font.as_deref(),
bold: vs.bold,
italic: vs.italic,
clip: None,
z: Z_OSK_KEY,
});
}
}
}
fn handle_osk_input(
ctx: &mut FrameCtx,
px: f32,
py: f32,
pw: f32,
ph: f32,
s: f32,
focused_id: Option<&str>,
active: Option<&str>,
just_opened: bool,
) {
if !ctx.persistent.osk.open || just_opened {
return;
}
let rows = get_rows(ctx.persistent.osk.layer, ctx.persistent.osk.shift);
if ctx.frame.input.left_just_pressed {
let mx = ctx.frame.input.mouse_x;
let my = ctx.frame.input.mouse_y;
if !(Rect {
x: px,
y: py,
w: pw,
h: ph,
}
.contains(mx, my))
{
return;
}
let mut clicked_key: Option<(usize, usize)> = None;
'hit: for (r, row_keys) in rows.iter().enumerate() {
for (c, _) in row_keys.iter().enumerate() {
if key_rect(r, c, rows, px, py, s).contains(mx, my) {
clicked_key = Some((r, c));
break 'hit;
}
}
}
if let Some((r, c)) = clicked_key {
ctx.persistent.osk.row = r;
ctx.persistent.osk.col = c;
let label = get_rows(ctx.persistent.osk.layer, ctx.persistent.osk.shift)[r][c];
fire_key(label, ctx, focused_id, active);
}
return;
}
let (mut row, mut col) = (ctx.persistent.osk.row, ctx.persistent.osk.col);
if ctx.frame.input.arrow_up && row > 0 {
row -= 1;
col = col.min(rows[row].len() - 1);
}
if ctx.frame.input.arrow_down && row + 1 < rows.len() {
row += 1;
col = col.min(rows[row].len() - 1);
}
if ctx.frame.input.arrow_left && col > 0 {
col -= 1;
}
if ctx.frame.input.arrow_right && col + 1 < rows[row].len() {
col += 1;
}
ctx.persistent.osk.row = row;
ctx.persistent.osk.col = col;
if ctx.frame.input.escape {
ctx.persistent.osk.open = false;
if let (Some(id), Some(name)) = (focused_id, active)
&& let Some(root) = ctx.persistent.roots.get_mut(name)
{
for (item, state) in &mut root.items {
if let (UiItem::TextBox(tb), ItemState::TextBox(ts)) = (item, state)
&& tb.id == id
{
ts.nav_grabbed = false;
}
}
}
return;
}
if ctx.frame.input.space || ctx.frame.input.enter {
let label = get_rows(ctx.persistent.osk.layer, ctx.persistent.osk.shift)[row][col];
fire_key(label, ctx, focused_id, active);
}
}
fn fire_key(key: &str, ctx: &mut FrameCtx, focused_id: Option<&str>, active: Option<&str>) {
match key {
"SHIFT" => {
ctx.persistent.osk.shift = !ctx.persistent.osk.shift;
}
"SYM" => {
ctx.persistent.osk.layer = OskLayer::Symbols;
ctx.persistent.osk.shift = false;
}
"ABC" => {
ctx.persistent.osk.layer = OskLayer::Alpha;
ctx.persistent.osk.shift = false;
}
"DONE" => {
ctx.persistent.osk.open = false;
if let (Some(id), Some(name)) = (focused_id, active)
&& let Some(root) = ctx.persistent.roots.get_mut(name)
{
let tx = ctx.persistent.tx.clone();
with_textbox_mut(id, &mut root.items, |_tb, ts| {
ts.nav_grabbed = false;
let value = ts.text.clone();
let _ = tx.send(UiMsg::TextSubmitted(id.to_owned(), value));
});
}
}
"BACK" => {
if let (Some(id), Some(name)) = (focused_id, active)
&& let Some(root) = ctx.persistent.roots.get_mut(name)
{
let tx = ctx.persistent.tx.clone();
with_textbox_mut(id, &mut root.items, |_tb, ts| {
if ts.cursor_pos > 0 {
ts.text.remove(ts.cursor_pos - 1);
ts.cursor_pos -= 1;
let value = ts.text.clone();
let _ = tx.send(UiMsg::TextChanged(id.to_owned(), value));
}
});
}
}
_ => {
let ch = if key == "SPACE" {
' '
} else {
key.chars().next().unwrap_or(' ')
};
let typed = if let (Some(id), Some(name)) = (focused_id, active)
&& let Some(root) = ctx.persistent.roots.get_mut(name)
{
let tx = ctx.persistent.tx.clone();
with_textbox_mut(id, &mut root.items, |tb, ts| {
if tb.max_len.is_none_or(|max| ts.text.chars().count() < max) {
ts.text.insert(ts.cursor_pos, ch);
ts.cursor_pos += 1;
let value = ts.text.clone();
let _ = tx.send(UiMsg::TextChanged(id.to_owned(), value));
}
})
} else {
false
};
if typed {
ctx.persistent.osk.shift = false;
}
}
}
}