Touch interface for phones and tablets; category toolbar for mouse and touch

- touch.rs: gesture recognition (tap, double tap, long press, drag, two-finger pan/pinch/rotate)
- Touch mode: Auto (touch device or first touch) / On / Off in Settings -> Game
- Larger interface factor on touch screens, top bar wraps, smaller minimap, fewer messages
- Toolbar grouped into categories with pop-up rows and a menu button, also on the desktop
- Long press shows information or drops held units; double tap with the hand slaps
- Savegame name via the browser's text dialog when the name field is tapped
- Touch help texts (EN/DE)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-03 10:10:53 +01:00
1 parent 6f037294c0
commit af6be62805
10 files changed
+744 -152

No files matched your search

+268 -103
View File
@@ -15,12 +15,26 @@ use macroquad::prelude::*;
// is the interface factor (automatic from the resolution or fixed in the settings).
thread_local! {
static UI_FACTOR_SETTING: std::cell::Cell<u8> = std::cell::Cell::new(0);
static TOUCH: std::cell::Cell<bool> = std::cell::Cell::new(false);
static POINTER: std::cell::Cell<(f32, f32)> = std::cell::Cell::new((-1e5, -1e5));
}
pub fn set_ui_factor(f: u8) {
UI_FACTOR_SETTING.with(|c| c.set(f));
}
// touch interface active (bigger interface, category toolbar, gestures)
pub fn set_touch(on: bool) {
TOUCH.with(|c| c.set(on));
}
pub fn touch_ui() -> bool {
TOUCH.with(|c| c.get())
}
// in touch mode the pointer is the finger (device pixels), set by the input code each frame
pub fn set_pointer(x: f32, y: f32) {
POINTER.with(|c| c.set((x, y)));
}
// device pixels per CSS pixel
pub fn dpr() -> f32 {
screen_dpi_scale()
@@ -33,6 +47,9 @@ pub fn sh() -> f32 {
(screen_height() * dpr()).round()
}
pub fn mouse() -> (f32, f32) {
if touch_ui() {
return POINTER.with(|c| c.get());
}
let (x, y) = mouse_position();
(x * dpr(), y * dpr())
}
@@ -44,7 +61,15 @@ pub fn F() -> u32 {
if set > 0 {
return set as u32;
}
((sw() / 1280.0).min(sh() / 760.0).floor() as u32).clamp(1, 4)
let auto = ((sw() / 1280.0).min(sh() / 760.0).floor() as u32).clamp(1, 4);
if touch_ui() {
// fingers need bigger buttons: about 2/3 of the device pixel ratio,
// as long as at least 40 columns fit across the screen
let want = (dpr() * 2.0 / 3.0).round() as u32;
let fit = (sw() / (40.0 * GW * 2.0)).floor() as u32;
return auto.max(want.min(fit)).clamp(1, 4);
}
auto
}
// text scales: 1x, 2x and 4x the glyph size, times F
#[allow(non_snake_case)]
@@ -206,8 +231,26 @@ pub fn tool_cost(t: Tool, g: &Game) -> String {
}
}
// Touch toolbar: tool categories (indices into TOOLS); categories with several tools open a row
pub const CATS: [(&str, &[usize]); 6] = [
("cat.dig", &[0]),
("cat.hand", &[1]),
("cat.rooms", &[2, 3, 4, 5, 6, 12, 13]),
("cat.towers", &[14, 15, 16]),
("cat.spells", &[9, 10, 11]),
("cat.orders", &[8, 7]),
];
#[derive(Clone, Copy, PartialEq, Debug)]
enum TBtn {
Cat(usize),
Tool(usize),
Menu,
}
pub enum UiAction {
None,
OpenMenu,
SelectTool(Tool),
MinimapJump(f32, f32),
CloseHelp,
@@ -226,10 +269,14 @@ pub struct Ui {
minimap_tex: Texture2D,
minimap_t: f64,
minimap_rect: Rect,
tool_rects: Vec<Rect>,
end_button: Rect,
lang_rects: Vec<(Rect, String)>,
pub hand_anim: Option<(u8, f64)>, // 1 = backhand, 2 = punch, start time
// touch interface
open_cat: Option<usize>,
tip: Option<(usize, f64)>, // tool whose description is shown, since
touch_btns: Vec<(Rect, TBtn)>,
hud_h: f32,
}
fn wrap(s: &str, cols: usize) -> Vec<String> {
@@ -272,10 +319,13 @@ impl Ui {
minimap_tex: tex,
minimap_t: -1.0,
minimap_rect: Rect::new(0.0, 0.0, 0.0, 0.0),
tool_rects: Vec::new(),
end_button: Rect::new(0.0, 0.0, 0.0, 0.0),
lang_rects: Vec::new(),
hand_anim: None,
open_cat: None,
tip: None,
touch_btns: Vec::new(),
hud_h: 0.0,
}
}
@@ -311,11 +361,32 @@ impl Ui {
if self.show_help {
return UiAction::CloseHelp;
}
for (i, r) in self.tool_rects.iter().enumerate() {
if r.contains(p) {
return UiAction::SelectTool(TOOLS[i]);
}
if let Some(&(_, b)) = self.touch_btns.iter().find(|(r, _)| r.contains(p)) {
return match b {
TBtn::Menu => {
self.open_cat = None;
UiAction::OpenMenu
}
TBtn::Tool(i) => {
self.open_cat = None;
self.tip = Some((i, get_time()));
UiAction::SelectTool(TOOLS[i])
}
TBtn::Cat(c) => {
let tools = CATS[c].1;
if tools.len() == 1 {
self.open_cat = None;
self.tip = Some((tools[0], get_time()));
UiAction::SelectTool(TOOLS[tools[0]])
} else {
self.open_cat = if self.open_cat == Some(c) { None } else { Some(c) };
UiAction::Consumed
}
}
};
}
// a tap elsewhere closes an open category row
self.open_cat = None;
if self.minimap_rect.contains(p) {
let (wx, wy) = self.minimap_world(x, y);
return UiAction::MinimapJump(wx, wy);
@@ -333,61 +404,74 @@ impl Ui {
let sh = sh();
let (mx, my) = mouse();
// ---------------- top bar (everything on the character grid)
// ---------------- top bar: segments flow over up to three lines on narrow screens
let cols = (sw / CW()).floor() as i32;
gfx.window(0.0, 0.0, cols, 3, S(), false, FRAME, BG);
self.rects.push(Rect::new(0.0, 0.0, sw, 3.0 * CH()));
let y = CH();
let mut c = 2usize; // current column
let col_x = |c: usize| c as f32 * CW();
let put = |s: &str, color: Color, c: &mut usize| {
gfx.text(s, col_x(*c), y, S(), color);
*c += s.chars().count();
};
let bar = |frac: f32, color: Color, c: &mut usize| {
ascii_bar(gfx, col_x(*c), y, 6, frac, color, S());
*c += 6;
};
put(&tr("hud.gold"), GOLD, &mut c);
c += 1;
put(&format!("{}/{}", g.gold as i32, g.gold_cap() as i32), TXT, &mut c);
c += 2;
put(&tr("hud.mana"), MANA, &mut c);
c += 1;
bar(g.mana / MANA_MAX, MANA, &mut c);
c += 1;
put(&format!("{:<3}", g.mana as i32), TXT, &mut c);
c += 2;
put(&tr("hud.core"), RED, &mut c);
c += 1;
bar(g.core_hp / CORE_HP, RED, &mut c);
c += 2;
put(&tr("hud.gate"), BLUE, &mut c);
c += 1;
bar(g.gate_hp / GATE_HP, BLUE, &mut c);
c += 2;
let cap = g.counts[1].min(30);
let mut segs: Vec<(String, Color, String)> = vec![
(tr("hud.creatures"), TXT, format!("{}/{}", g.creature_count(), cap)),
(tr("hud.workers"), TXT, g.worker_count().to_string()),
];
let wave_col = if g.wave_timer < 30.0 { RED } else { TXT };
segs.push((trf("hud.wave", &[(g.wave + 1).to_string()]), wave_col, mmss(g.wave_timer)));
segs.push((tr("hud.army"), TXT, (g.army_value() as i32).to_string()));
if !g.hand.is_empty() {
segs.push((tr("hud.hand"), FRAME_HI, format!("{}/8", g.hand.len())));
enum Part {
T(String, Color),
Bar(f32, Color),
Gap,
}
let txt = |s: String, c: Color| Part::T(s, c);
let cap = g.counts[1].min(30);
let wave_col = if g.wave_timer < 30.0 { RED } else { TXT };
let vol = if settings.muted { tr("hud.off") } else { format!("{}%", (settings.volume * 100.0).round() as i32) };
segs.push(("\u{266a}".to_string(), DIM, vol));
for (label, lc, value) in segs {
// only what still fits into the bar
if c + label.chars().count() + value.chars().count() + 2 > cols as usize - 1 {
let mut segs: Vec<Vec<Part>> = vec![
vec![txt(tr("hud.gold"), GOLD), Part::Gap, txt(format!("{}/{}", g.gold as i32, g.gold_cap() as i32), TXT)],
vec![txt(tr("hud.mana"), MANA), Part::Gap, Part::Bar(g.mana / MANA_MAX, MANA), Part::Gap, txt(format!("{:<3}", g.mana as i32), TXT)],
vec![txt(tr("hud.core"), RED), Part::Gap, Part::Bar(g.core_hp / CORE_HP, RED)],
vec![txt(tr("hud.gate"), BLUE), Part::Gap, Part::Bar(g.gate_hp / GATE_HP, BLUE)],
vec![txt(tr("hud.creatures"), TXT), Part::Gap, txt(format!("{}/{}", g.creature_count(), cap), TXT)],
vec![txt(tr("hud.workers"), TXT), Part::Gap, txt(g.worker_count().to_string(), TXT)],
vec![txt(trf("hud.wave", &[(g.wave + 1).to_string()]), wave_col), Part::Gap, txt(mmss(g.wave_timer), TXT)],
vec![txt(tr("hud.army"), TXT), Part::Gap, txt((g.army_value() as i32).to_string(), TXT)],
];
if !g.hand.is_empty() {
segs.push(vec![txt(tr("hud.hand"), FRAME_HI), Part::Gap, txt(format!("{}/8", g.hand.len()), TXT)]);
}
segs.push(vec![txt("\u{266a}".to_string(), DIM), Part::Gap, txt(vol, DIM)]);
let width = |seg: &Vec<Part>| -> usize {
seg.iter().map(|p| match p {
Part::T(s, _) => s.chars().count(),
Part::Bar(..) => 6,
Part::Gap => 1,
}).sum()
};
// place the segments: (line, column)
let mut placed: Vec<(usize, usize)> = Vec::new();
let (mut line, mut c) = (0usize, 2usize);
for seg in &segs {
let w = width(seg);
if c > 2 && c + w > cols as usize - 1 {
line += 1;
c = 2;
}
if line >= 3 || c + w > cols as usize - 1 {
break;
}
put(&label, lc, &mut c);
c += 1;
put(&value, if lc == DIM { DIM } else { TXT }, &mut c);
c += 2;
placed.push((line, c));
c += w + 2;
}
let lines = placed.iter().map(|p| p.0).max().unwrap_or(0) + 1;
let hud_rows = lines as i32 + 2;
self.hud_h = hud_rows as f32 * CH();
gfx.window(0.0, 0.0, cols, hud_rows, S(), false, FRAME, BG);
self.rects.push(Rect::new(0.0, 0.0, sw, self.hud_h));
for (seg, &(l, c0)) in segs.iter().zip(placed.iter()) {
let y = CH() * (1 + l) as f32;
let mut c = c0;
for p in seg {
match p {
Part::T(s, col) => {
gfx.text(s, c as f32 * CW(), y, S(), *col);
c += s.chars().count();
}
Part::Bar(f, col) => {
ascii_bar(gfx, c as f32 * CW(), y, 6, *f, *col, S());
c += 6;
}
Part::Gap => c += 1,
}
}
}
// ---------------- toolbar: buttons of 6 x 3 characters, icons and labels on the grid
@@ -398,45 +482,88 @@ impl Ui {
let tb_rows = (tb_h / CH()).floor() as i32;
gfx.window(0.0, tb_y, cols, tb_rows, S(), false, FRAME, BG);
self.rects.push(Rect::new(0.0, tb_y, sw, tb_h));
self.tool_rects.clear();
self.touch_btns.clear();
let mut hover_tool = None;
let bw = 6.0 * CW();
let px = K(); // one pixel of the font, times the factor
for (i, &t) in TOOLS.iter().enumerate() {
let bx = CW() + i as f32 * bw;
let by = tb_y + CH();
if bx + bw > sw - CW() {
break;
}
let pulse = (now * 2.5) as i64 % 2 == 0;
// one toolbar button: icon, optional key, label in small text
let tool_button = |bx: f32, by: f32, bw: f32, icon_id: u32, key: &str, label: &str, sel: bool, hovered: bool| {
let r = Rect::new(bx + px, by, bw - 2.0 * px, 3.0 * CH());
self.tool_rects.push(r);
let hovered = r.contains(vec2(mx, my));
if hovered {
hover_tool = Some(i);
}
let sel = self.tool == t;
draw_rectangle(r.x, r.y, r.w, r.h, if sel { Color::new(0.42, 0.26, 0.1, 1.0) } else if hovered { Color::new(0.18, 0.13, 0.1, 1.0) } else { Color::new(0.1, 0.07, 0.07, 1.0) });
// the active tool: pulsing frame and an arrow in the toolbar frame above it
let pulse = (now * 2.5) as i64 % 2 == 0;
// the active tool: pulsing frame
let frame = if sel { if pulse { WHITE } else { FRAME_HI } } else { Color::new(0.35, 0.28, 0.2, 1.0) };
draw_rectangle_lines(r.x, r.y, r.w, r.h, if sel { 3.0 * px } else { 2.0 * px }, frame);
if sel {
gfx.glyph(0x1F, bx + CW() * 2.5, tb_y, S(), if pulse { WHITE } else { FRAME_HI });
}
let ti = tool_info(t);
// icon: the 16 px atlas cell at 2*F, centred on the grid
let icon = 32.0 * px;
gfx.draw_cell(ti.icon, bx + (bw - icon) * 0.5, by + 4.0 * px, icon, icon, WHITE);
gfx.text(&keys[i], bx + 3.0 * px, by + 3.0 * px, SMALL(), if sel { FRAME_HI } else { DIM });
gfx.draw_cell(icon_id, bx + (bw - icon) * 0.5, by + 4.0 * px, icon, icon, WHITE);
if !key.is_empty() {
gfx.text(key, bx + 3.0 * px, by + 3.0 * px, SMALL(), if sel { FRAME_HI } else { DIM });
}
// label in small text, centred on whole small-character cells
let label: String = ti.label.chars().take(11).collect();
let maxc = ((bw - 2.0 * px) / (GW * SMALL())).floor() as usize;
let label: String = label.chars().take(maxc.min(11)).collect();
let lw = gfx.measure(&label, SMALL());
let lx = bx + ((bw - lw) / (GW * SMALL()) / 2.0).floor() * GW * SMALL();
gfx.text(&label, lx, by + 2.0 * CH() + 4.0 * px, SMALL(), if sel { FRAME_HI } else { TXT });
r
};
let mut popup_top = tb_y;
// categories + menu button; a category with several tools opens a row above
// (the same layout for mouse and touch; on the desktop the keys still select tools directly)
let n = CATS.len() + 1;
let bcols = (((cols - 2) as usize / n) as f32).clamp(4.0, 8.0);
let bw = bcols * CW();
let x0 = grid_center(sw, bw * n as f32);
let by = tb_y + CH();
let cur = TOOLS.iter().position(|&t| t == self.tool).unwrap_or(0);
for (c, (key, tools)) in CATS.iter().enumerate() {
let bx = x0 + c as f32 * bw;
let has = tools.contains(&cur);
let shown = if has { cur } else { tools[0] };
let ti = tool_info(TOOLS[shown]);
let label = if tools.len() == 1 { ti.label.clone() } else { tr(key) };
let hovered = !touch_ui() && Rect::new(bx + px, by, bw - 2.0 * px, 3.0 * CH()).contains(vec2(mx, my));
// single tools show their key (desktop) and their description when hovered
let key = if tools.len() == 1 && !touch_ui() { keys[tools[0]].as_str() } else { "" };
if hovered && tools.len() == 1 {
hover_tool = Some(tools[0]);
}
let r = tool_button(bx, by, bw, ti.icon, key, &label, has, self.open_cat == Some(c) || hovered);
if has {
gfx.glyph(0x1F, bx + (bw - CW()) * 0.5, tb_y, S(), if pulse { WHITE } else { FRAME_HI });
}
if tools.len() > 1 {
// small arrow: this button opens a row
gfx.glyph(0x1E, r.x + r.w - GW * SMALL() - 2.0 * px, r.y + 2.0 * px, SMALL(), if self.open_cat == Some(c) { FRAME_HI } else { DIM });
}
self.touch_btns.push((r, TBtn::Cat(c)));
}
// menu button
let bx = x0 + CATS.len() as f32 * bw;
let r = Rect::new(bx + px, by, bw - 2.0 * px, 3.0 * CH());
let hovered = !touch_ui() && r.contains(vec2(mx, my));
draw_rectangle(r.x, r.y, r.w, r.h, if hovered { Color::new(0.18, 0.13, 0.1, 1.0) } else { Color::new(0.1, 0.07, 0.07, 1.0) });
draw_rectangle_lines(r.x, r.y, r.w, r.h, 2.0 * px, Color::new(0.35, 0.28, 0.2, 1.0));
gfx.glyph(0xF0, bx + (bw - GW * TITLE()) * 0.5, by + 2.0 * px, TITLE(), FRAME_HI);
let ml = tr("cat.menu");
let maxc = ((bw - 2.0 * px) / (GW * SMALL())).floor() as usize;
let ml: String = ml.chars().take(maxc).collect();
gfx.text(&ml, bx + (bw - gfx.measure(&ml, SMALL())) * 0.5, by + 2.0 * CH() + 4.0 * px, SMALL(), TXT);
self.touch_btns.push((r, TBtn::Menu));
// open category row: (category, x0, button width, columns)
let popup = self.open_cat.map(|c| (c, x0, bw, bcols));
// touch: after choosing a tool its description is shown for a moment
if let Some((i, t)) = self.tip.filter(|_| touch_ui()) {
if now - t < 3.5 && self.open_cat.is_none() {
hover_tool = Some(i);
} else if now - t >= 3.5 {
self.tip = None;
}
}
// ---------------- minimap: whole device pixels per tile
let ms = ((14.0 * CW()) / mw().max(mh()) as f32).floor().max(1.0);
let mm_size = if touch_ui() { 10.0 } else { 14.0 };
let ms = ((mm_size * CW()) / mw().max(mh()) as f32).floor().max(1.0);
let (map_w, map_h) = (mw() as f32 * ms, mh() as f32 * ms);
let mm_cols = ((map_w / CW()).ceil() as i32 + 2).max(10);
let mm_rows = (map_h / CH()).ceil() as i32 + 2;
@@ -500,7 +627,7 @@ impl Ui {
// ---------------- info panel
if !info.is_empty() {
let icols = 30;
let icols = 30.min(cols - 2);
let mut lines: Vec<(String, Color)> = Vec::new();
for (s, c) in info.iter().skip(1) {
for l in wrap(s, icols as usize - 4) {
@@ -509,7 +636,7 @@ impl Ui {
}
let irows = lines.len() as i32 + 2;
let ix = sw - icols as f32 * CW();
let iy = 3.0 * CH();
let iy = self.hud_h;
gfx.window_titled(ix, iy, icols, irows, S(), false, FRAME, BG, &info[0].0, info[0].1);
for (k, (l, c)) in lines.iter().enumerate() {
gfx.text(l, ix + CW() * 2.0, iy + CH() * (k as f32 + 1.0), S(), *c);
@@ -521,7 +648,8 @@ impl Ui {
if !recent.is_empty() {
let mcols = (((sw - mm_w) / CW()) as i32 - 2).clamp(20, 70);
let mut lines: Vec<(String, f32)> = Vec::new();
for (m, t) in recent.iter().rev().take(5).rev() {
let keep = if touch_ui() { 3 } else { 5 };
for (m, t) in recent.iter().rev().take(keep).rev() {
let a = (1.0 - ((now - t) as f32 - 11.0) / 3.0).clamp(0.2, 1.0);
for l in wrap(&trf(m.key, &m.args), mcols as usize - 4) {
lines.push((l, a));
@@ -539,20 +667,51 @@ impl Ui {
self.log.drain(0..50);
}
// the open row of tools (wraps into several rows if needed)
if let Some((c, x0, bw, bcols)) = popup {
let tools = CATS[c].1;
let per_row = (((cols - 2) as f32 / bcols).floor() as usize).max(1).min(tools.len());
let nrows = (tools.len() + per_row - 1) / per_row;
let pw = per_row as f32 * bw + 2.0 * CW();
let ph = (nrows as f32 * 3.0 + 2.0) * CH();
let pcols = (pw / CW()) as i32;
let prows = (ph / CH()) as i32;
let px0 = ((x0 + c as f32 * bw + bw * 0.5 - pw * 0.5) / CW()).floor().clamp(0.0, ((sw - pw) / CW()).floor()) * CW();
let py0 = tb_y - ph;
gfx.window_titled(px0, py0, pcols, prows, S(), false, FRAME_HI, BG, &tr(CATS[c].0), FRAME_HI);
self.rects.push(Rect::new(px0, py0, pw, ph));
for (k, &i) in tools.iter().enumerate() {
let bx = px0 + CW() + (k % per_row) as f32 * bw;
let by = py0 + CH() + (k / per_row) as f32 * 3.0 * CH();
let ti = tool_info(TOOLS[i]);
let hovered = !touch_ui() && Rect::new(bx + px, by, bw - 2.0 * px, 3.0 * CH()).contains(vec2(mx, my));
if hovered {
hover_tool = Some(i);
}
let key = if touch_ui() { "" } else { keys[i].as_str() };
let r = tool_button(bx, by, bw, ti.icon, key, &ti.label, self.tool == TOOLS[i], hovered);
self.touch_btns.push((r, TBtn::Tool(i)));
}
popup_top = py0;
}
// ---------------- Tooltip
if let Some(i) = hover_tool {
let t = TOOLS[i];
let ti = tool_info(t);
let tcols = 34;
let tcols = 34.min(cols as usize);
let mut lines: Vec<(String, Color)> = wrap(&ti.desc, tcols - 4).into_iter().map(|l| (l, TXT)).collect();
let cost = tool_cost(t, g);
if !cost.is_empty() {
lines.push((trf("ui.cost", &[cost]), GOLD));
}
lines.push((trf("ui.key", &[keys[i].clone()]), DIM));
if !touch_ui() {
lines.push((trf("ui.key", &[keys[i].clone()]), DIM));
}
let rows = lines.len() as i32 + 2;
let tx = (((mx - tcols as f32 * CW() * 0.5) / CW()).floor() * CW()).clamp(0.0, sw - tcols as f32 * CW());
let ty = tb_y - rows as f32 * CH();
let anchor = if touch_ui() { sw * 0.5 } else { mx };
let tx = (((anchor - tcols as f32 * CW() * 0.5) / CW()).floor() * CW()).clamp(0.0, (sw - tcols as f32 * CW()).max(0.0));
let ty = popup_top - rows as f32 * CH();
gfx.window_titled(tx, ty, tcols as i32, rows, S(), false, FRAME_HI, BG, &ti.name, FRAME_HI);
for (k, (l, c)) in lines.iter().enumerate() {
gfx.text(l, tx + CW() * 2.0, ty + CH() * (k as f32 + 1.0), S(), *c);
@@ -584,18 +743,14 @@ impl Ui {
}
fn draw_help(&mut self, gfx: &Gfx) {
let keys: &[(&str, Color)] = &[
("help.goal", GOLD),
("", TXT),
("help.k_left", TXT),
("help.k_right", TXT),
("help.k_slap", TXT),
("help.k_pan", TXT),
("help.k_rot", TXT),
("help.k_zoom", TXT),
("help.k_tools", TXT),
("help.k_misc", TXT),
("help.k_misc2", TXT),
let controls: &[&str] = if touch_ui() {
&["help.t_tap", "help.t_drag", "help.t_two", "help.t_long", "help.t_double", "help.t_tools"]
} else {
&["help.k_left", "help.k_right", "help.k_slap", "help.k_pan", "help.k_rot", "help.k_zoom", "help.k_tools", "help.k_misc", "help.k_misc2"]
};
let mut keys: Vec<(&str, Color)> = vec![("help.goal", GOLD), ("", TXT)];
keys.extend(controls.iter().map(|k| (*k, TXT)));
keys.extend_from_slice(&[
("", TXT),
("help.s1", TXT),
("help.s2", TXT),
@@ -606,9 +761,19 @@ impl Ui {
("help.font", DIM),
("", TXT),
("help.start", FRAME_HI),
];
let lines: Vec<(String, Color)> = keys.iter().map(|(k, c)| (if k.is_empty() { String::new() } else { tr(k) }, *c)).collect();
let cols = 74;
]);
let cols = 74.min((sw() / CW()) as i32);
let mut lines: Vec<(String, Color)> = Vec::new();
for (k, c) in &keys {
if k.is_empty() {
lines.push((String::new(), *c));
} else {
let key = if *k == "help.start" && touch_ui() { "help.start_touch" } else { k };
for l in wrap(&tr(key), cols as usize - 6) {
lines.push((l, *c));
}
}
}
let rows = lines.len() as i32 + 4;
let sw = sw();
let sh = sh();