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:
1 parent
6f037294c0
commit
af6be62805
10 files changed
+744
-152
No files matched your search
+195
-47
@@ -14,6 +14,7 @@ mod settings;
|
||||
mod sfx;
|
||||
mod tracks;
|
||||
mod sound;
|
||||
mod touch;
|
||||
mod ui;
|
||||
mod xm;
|
||||
|
||||
@@ -26,6 +27,7 @@ use menu::{Menu, MenuAction, Page};
|
||||
use render3d::{Cam, Overlay, R3d};
|
||||
use settings::{bindable_keys, Action, Settings};
|
||||
use sound::SoundBank;
|
||||
use touch::{Gesture, TouchInput};
|
||||
use ui::{hover_info, Tool, Ui, UiAction, TOOLS};
|
||||
|
||||
const SETTINGS_KEY: &str = "dc.settings";
|
||||
@@ -54,6 +56,60 @@ struct Input {
|
||||
rdrag: bool,
|
||||
mlast: Option<(f32, f32)>, // middle button drag
|
||||
minimap_drag: bool,
|
||||
// touch
|
||||
tlast: Option<Vec2>, // last position of a one-finger pan (device pixels)
|
||||
touch_world: bool, // the current finger started on the game world
|
||||
inspect: Option<(f32, f32, f64)>, // long press: show information for this point (CSS pixels)
|
||||
}
|
||||
|
||||
fn area_tool(t: Tool) -> bool {
|
||||
matches!(t, Tool::Dig | Tool::Room(_) | Tool::Sell)
|
||||
}
|
||||
|
||||
// Use the active tool on the world at (mx, my) (CSS pixels): a click, or a tap on touch screens
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn world_action(app: &mut App, ui: &mut Ui, r3d: &R3d, inp: &mut Input, mx: f32, my: f32, slap: bool, now: f64) {
|
||||
if app.game.state != 0 {
|
||||
return;
|
||||
}
|
||||
let tile = app.cam.pick_tile(&app.game.map, mx, my).map(|(x, z, _)| (x, z));
|
||||
let ground = app.cam.ground_point(mx, my);
|
||||
let (wx, wy) = (ground.x, ground.y);
|
||||
match (ui.tool, tile) {
|
||||
(Tool::Dig | Tool::Room(_) | Tool::Sell, Some(t)) => inp.ldown = Some(t),
|
||||
(Tool::Hand, _) => {
|
||||
let j = r3d.pick_ent(&app.game, &app.cam, mx, my, false);
|
||||
if slap {
|
||||
let kind = app.game.slap(j);
|
||||
if kind > 0 {
|
||||
ui.hand_anim = Some((kind, now));
|
||||
}
|
||||
} else {
|
||||
app.game.pickup(j);
|
||||
}
|
||||
}
|
||||
(Tool::Flag, Some((x, z))) => app.game.set_flag(x, z),
|
||||
(Tool::Lightning, _) => app.game.lightning(wx, wy),
|
||||
(Tool::Heal, _) => app.game.heal(wx, wy),
|
||||
(Tool::Summon, _) => app.game.summon(wx, wy),
|
||||
(Tool::Tower(k), Some((x, z))) => app.game.build_tower(k, x, z),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Finish a selection rectangle at the tile under (mx, my)
|
||||
fn finish_rect(app: &mut App, tool: Tool, start: (i32, i32), mx: f32, my: f32) {
|
||||
let Some((ex, ey, _)) = app.cam.pick_tile(&app.game.map, mx, my) else { return };
|
||||
let (sx, sy) = start;
|
||||
match tool {
|
||||
Tool::Dig => {
|
||||
let on = !app.game.map.t[idx(sx, sy)].marked;
|
||||
app.game.mark_rect(sx, sy, ex, ey, on);
|
||||
}
|
||||
Tool::Room(r) => app.game.build_rect(r, sx, sy, ex, ey),
|
||||
Tool::Sell => app.game.sell_rect(sx, sy, ex, ey),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn new_game(seed: u32, s: &Settings) -> (Game, Cam) {
|
||||
@@ -229,17 +285,44 @@ async fn main() {
|
||||
let (game, cam) = new_game(seed, &settings);
|
||||
let mut app = App { game, cam, settings, seed, status: None };
|
||||
app.game.hints = app.settings.hints;
|
||||
let mut inp = Input { ldown: None, rdown: None, rlast: (0.0, 0.0), rdrag: false, mlast: None, minimap_drag: false };
|
||||
let mut inp = Input { ldown: None, rdown: None, rlast: (0.0, 0.0), rdrag: false, mlast: None, minimap_drag: false, tlast: None, touch_world: false, inspect: None };
|
||||
let mut touch = TouchInput::new();
|
||||
let mut auto_touch = platform::coarse_pointer(); // automatic mode: touch interface after a touch, desktop after a mouse click
|
||||
let mut autosave_t = 0.0f32;
|
||||
let mut cache_t = 0.0f32;
|
||||
|
||||
loop {
|
||||
let dt = get_frame_time().min(0.1);
|
||||
let now = get_time();
|
||||
let (mx, my) = mouse_position(); // CSS pixels (3D picking)
|
||||
let (ux, uy) = ui::mouse(); // device pixels (interface)
|
||||
let mut settings_changed = false;
|
||||
|
||||
// ---------------- touch or mouse: which interface is active
|
||||
// (with the touch interface touches are no longer turned into mouse events)
|
||||
simulate_mouse_with_touch(app.settings.touch_ui == 2);
|
||||
let gestures = if app.settings.touch_ui == 2 { Vec::new() } else { touch.update(now, ui.tool == Tool::Hand && !menu.open()) };
|
||||
if !touches().is_empty() {
|
||||
auto_touch = true;
|
||||
} else if app.settings.touch_ui != 2 && (is_mouse_button_pressed(MouseButton::Left) || is_mouse_button_pressed(MouseButton::Right)) {
|
||||
auto_touch = false;
|
||||
}
|
||||
let touch_mode = match app.settings.touch_ui {
|
||||
1 => true,
|
||||
2 => false,
|
||||
_ => auto_touch,
|
||||
};
|
||||
ui::set_touch(touch_mode);
|
||||
if touch_mode {
|
||||
// the pointer is the finger, or the position of this frame's gesture
|
||||
let gpos = gestures.iter().rev().find_map(|g| match *g {
|
||||
Gesture::Press(p) | Gesture::Tap(p) | Gesture::Release(p) | Gesture::LongPress(p) | Gesture::DoubleTap(p) => Some(p),
|
||||
_ => None,
|
||||
});
|
||||
let p = touch.finger().or(gpos).unwrap_or(vec2(-1e5, -1e5));
|
||||
ui::set_pointer(p.x, p.y);
|
||||
}
|
||||
let (ux, uy) = ui::mouse(); // device pixels (interface)
|
||||
let (mx, my) = if touch_mode { (ux / ui::dpr(), uy / ui::dpr()) } else { mouse_position() }; // CSS pixels (3D picking)
|
||||
|
||||
// ---------------- savegame files: file picker result or dropped onto the window
|
||||
let mut incoming: Option<Vec<u8>> = platform::take_file();
|
||||
for f in get_dropped_files() {
|
||||
@@ -294,7 +377,8 @@ async fn main() {
|
||||
menu.finish_capture();
|
||||
}
|
||||
}
|
||||
draw_frame(&mut app, &gfx, &mut r3d, &mut ui, &mut menu, &cloud, &inp, now);
|
||||
let p = mouse_position();
|
||||
draw_frame(&mut app, &gfx, &mut r3d, &mut ui, &mut menu, &cloud, &inp, p, false, now);
|
||||
next_frame().await;
|
||||
continue;
|
||||
}
|
||||
@@ -386,7 +470,7 @@ async fn main() {
|
||||
move_cam(pan, 0.0, cam);
|
||||
}
|
||||
let edge = 6.0;
|
||||
if keys.edge_scroll && mx >= 0.0 && my >= 0.0 && mx < screen_width() && my < screen_height() {
|
||||
if keys.edge_scroll && !touch_mode && mx >= 0.0 && my >= 0.0 && mx < screen_width() && my < screen_height() {
|
||||
if mx < edge {
|
||||
move_cam(-pan, 0.0, cam);
|
||||
} else if mx > screen_width() - edge {
|
||||
@@ -411,7 +495,7 @@ async fn main() {
|
||||
if is_key_down(key(Action::TiltDown)) {
|
||||
cam.pitch -= rot * 0.6;
|
||||
}
|
||||
if is_mouse_button_down(MouseButton::Middle) {
|
||||
if is_mouse_button_down(MouseButton::Middle) && !touch_mode {
|
||||
if let Some((lx, ly)) = inp.mlast {
|
||||
let inv = if keys.invert_mouse { -1.0 } else { 1.0 };
|
||||
cam.yaw -= (mx - lx) * 0.008 * keys.rotate_speed * inv;
|
||||
@@ -431,6 +515,22 @@ async fn main() {
|
||||
cam.tx += before.x - after.x;
|
||||
cam.tz += before.y - after.y;
|
||||
}
|
||||
// touch: two fingers move, zoom and rotate the camera
|
||||
for g in &gestures {
|
||||
if let Gesture::Camera { center, pan, zoom, rotate } = *g {
|
||||
let d = ui::dpr();
|
||||
let a = cam.ground_point((center.x - pan.x) / d, (center.y - pan.y) / d);
|
||||
let b = cam.ground_point(center.x / d, center.y / d);
|
||||
cam.tx -= b.x - a.x;
|
||||
cam.tz -= b.y - a.y;
|
||||
let before = cam.ground_point(center.x / d, center.y / d);
|
||||
cam.zoom = (cam.zoom / zoom.max(0.01)).clamp(6.0, 45.0);
|
||||
cam.yaw += rotate; // the scene turns with the fingers
|
||||
let after = cam.ground_point(center.x / d, center.y / d);
|
||||
cam.tx += before.x - after.x;
|
||||
cam.tz += before.y - after.y;
|
||||
}
|
||||
}
|
||||
// right button: drag = pan, click = drop
|
||||
if is_mouse_button_pressed(MouseButton::Right) {
|
||||
inp.rdown = Some((mx, my));
|
||||
@@ -463,8 +563,13 @@ async fn main() {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- left button
|
||||
if is_mouse_button_pressed(MouseButton::Left) {
|
||||
// ---------------- left button (touch: a finger touching the interface)
|
||||
let pressed = if touch_mode { gestures.iter().any(|g| matches!(g, Gesture::Press(_))) } else { is_mouse_button_pressed(MouseButton::Left) };
|
||||
if touch_mode && pressed {
|
||||
inp.touch_world = playing && !over_ui;
|
||||
inp.inspect = None;
|
||||
}
|
||||
if pressed {
|
||||
if dialog {
|
||||
match ui.click(ux, uy, &app.game) {
|
||||
UiAction::SelectLanguage(code) => {
|
||||
@@ -525,6 +630,11 @@ async fn main() {
|
||||
MenuAction::CloudDelete(id) => {
|
||||
cloud.delete(&id);
|
||||
}
|
||||
MenuAction::EditName => {
|
||||
if let Some(n) = platform::prompt(&i18n::tr("saves.name"), &menu.save_name) {
|
||||
menu.save_name = n.chars().filter(|c| !c.is_control()).take(40).collect();
|
||||
}
|
||||
}
|
||||
MenuAction::Help => ui.show_help = true,
|
||||
MenuAction::MainMenu => {
|
||||
if app.game.state == 0 {
|
||||
@@ -551,55 +661,86 @@ async fn main() {
|
||||
(app.game, app.cam) = new_game(app.seed, &app.settings);
|
||||
app.game.hints = app.settings.hints;
|
||||
}
|
||||
UiAction::OpenMenu => menu.page = Some(Page::Pause),
|
||||
UiAction::CloseHelp | UiAction::SelectLanguage(_) | UiAction::Consumed => {}
|
||||
UiAction::None => {
|
||||
if app.game.state == 0 {
|
||||
match (ui.tool, hover_tile) {
|
||||
(Tool::Dig | Tool::Room(_) | Tool::Sell, Some(t)) => inp.ldown = Some(t),
|
||||
(Tool::Hand, _) => {
|
||||
let j = r3d.pick_ent(&app.game, &app.cam, mx, my, false);
|
||||
if is_key_down(KeyCode::LeftShift) || is_key_down(KeyCode::RightShift) {
|
||||
let kind = app.game.slap(j);
|
||||
if kind > 0 {
|
||||
ui.hand_anim = Some((kind, now));
|
||||
}
|
||||
} else {
|
||||
app.game.pickup(j);
|
||||
}
|
||||
}
|
||||
(Tool::Flag, Some((x, z))) => app.game.set_flag(x, z),
|
||||
(Tool::Lightning, _) => app.game.lightning(wx, wy),
|
||||
(Tool::Heal, _) => app.game.heal(wx, wy),
|
||||
(Tool::Summon, _) => app.game.summon(wx, wy),
|
||||
(Tool::Tower(k), Some((x, z))) => app.game.build_tower(k, x, z),
|
||||
_ => {}
|
||||
if touch_mode {
|
||||
// touch: area tools start their rectangle, everything else acts on the tap
|
||||
if area_tool(ui.tool) && app.game.state == 0 {
|
||||
inp.ldown = hover_tile;
|
||||
}
|
||||
} else {
|
||||
let slap = is_key_down(KeyCode::LeftShift) || is_key_down(KeyCode::RightShift);
|
||||
world_action(&mut app, &mut ui, &r3d, &mut inp, mx, my, slap, now);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let held = if touch_mode { touch.finger().is_some() } else { is_mouse_button_down(MouseButton::Left) };
|
||||
if inp.minimap_drag {
|
||||
if is_mouse_button_down(MouseButton::Left) && ui.minimap_contains(ux, uy) {
|
||||
if held && ui.minimap_contains(ux, uy) {
|
||||
let (x, y) = ui.minimap_world(ux, uy);
|
||||
app.cam.tx = x;
|
||||
app.cam.tz = y;
|
||||
} else if !is_mouse_button_down(MouseButton::Left) {
|
||||
} else if !held {
|
||||
inp.minimap_drag = false;
|
||||
}
|
||||
}
|
||||
if is_mouse_button_released(MouseButton::Left) {
|
||||
if let Some((sx, sy)) = inp.ldown.take() {
|
||||
if let (true, Some((ex, ey))) = (playing, hover_tile) {
|
||||
match ui.tool {
|
||||
Tool::Dig => {
|
||||
let on = !app.game.map.t[idx(sx, sy)].marked;
|
||||
app.game.mark_rect(sx, sy, ex, ey, on);
|
||||
}
|
||||
Tool::Room(r) => app.game.build_rect(r, sx, sy, ex, ey),
|
||||
Tool::Sell => app.game.sell_rect(sx, sy, ex, ey),
|
||||
_ => {}
|
||||
|
||||
// ---------------- touch gestures on the game world
|
||||
if touch_mode {
|
||||
let d = ui::dpr();
|
||||
for g in &gestures {
|
||||
match *g {
|
||||
Gesture::Cancel => {
|
||||
inp.ldown = None;
|
||||
inp.tlast = None;
|
||||
}
|
||||
Gesture::Tap(p) | Gesture::DoubleTap(p) if playing && !ui.over_ui(p.x, p.y) => {
|
||||
if !area_tool(ui.tool) {
|
||||
let slap = matches!(g, Gesture::DoubleTap(_));
|
||||
world_action(&mut app, &mut ui, &r3d, &mut inp, p.x / d, p.y / d, slap, now);
|
||||
}
|
||||
}
|
||||
Gesture::LongPress(p) if playing && inp.touch_world => {
|
||||
inp.ldown = None;
|
||||
if ui.tool == Tool::Hand && !app.game.hand.is_empty() {
|
||||
let gp = app.cam.ground_point(p.x / d, p.y / d);
|
||||
app.game.drop(gp.x, gp.y);
|
||||
} else {
|
||||
inp.inspect = Some((p.x / d, p.y / d, now));
|
||||
}
|
||||
}
|
||||
Gesture::DragStart(p) if playing && inp.touch_world && inp.ldown.is_none() => inp.tlast = Some(p),
|
||||
Gesture::DragMove(p) => {
|
||||
if let Some(l) = inp.tlast {
|
||||
let a = app.cam.ground_point(l.x / d, l.y / d);
|
||||
let b = app.cam.ground_point(p.x / d, p.y / d);
|
||||
app.cam.tx -= b.x - a.x;
|
||||
app.cam.tz -= b.y - a.y;
|
||||
inp.tlast = Some(p);
|
||||
}
|
||||
}
|
||||
Gesture::Release(p) => {
|
||||
inp.tlast = None;
|
||||
if let Some(start) = inp.ldown.take() {
|
||||
if playing && inp.touch_world {
|
||||
finish_rect(&mut app, ui.tool, start, p.x / d, p.y / d);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if inp.inspect.map_or(false, |(_, _, t)| now - t > 6.0) {
|
||||
inp.inspect = None;
|
||||
}
|
||||
}
|
||||
if !touch_mode && is_mouse_button_released(MouseButton::Left) {
|
||||
if let Some(start) = inp.ldown.take() {
|
||||
if playing {
|
||||
finish_rect(&mut app, ui.tool, start, mx, my);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -643,15 +784,20 @@ async fn main() {
|
||||
ui.log.push((m, now));
|
||||
}
|
||||
|
||||
draw_frame(&mut app, &gfx, &mut r3d, &mut ui, &mut menu, &cloud, &inp, now);
|
||||
draw_frame(&mut app, &gfx, &mut r3d, &mut ui, &mut menu, &cloud, &inp, (mx, my), touch_mode, now);
|
||||
next_frame().await
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn draw_frame(app: &mut App, gfx: &Gfx, r3d: &mut R3d, ui: &mut Ui, menu: &mut Menu, cloud: &Cloud, inp: &Input, now: f64) {
|
||||
let (mx, my) = mouse_position();
|
||||
let (ux, uy) = ui::mouse();
|
||||
// pointer: CSS pixels of the mouse or finger; touch: hover highlight and information only
|
||||
// while a finger is down or for a long-pressed point
|
||||
fn draw_frame(app: &mut App, gfx: &Gfx, r3d: &mut R3d, ui: &mut Ui, menu: &mut Menu, cloud: &Cloud, inp: &Input, pointer: (f32, f32), touch: bool, now: f64) {
|
||||
let (mx, my) = match (touch, inp.inspect) {
|
||||
(true, Some((x, y, _))) if inp.ldown.is_none() => (x, y),
|
||||
_ => pointer,
|
||||
};
|
||||
let (ux, uy) = (mx * ui::dpr(), my * ui::dpr());
|
||||
let blocked = menu.open() || ui.show_help || ui.show_langsel;
|
||||
let over_ui = blocked || ui.over_ui(ux, uy);
|
||||
let hover_tile = app.cam.pick_tile(&app.game.map, mx, my).map(|(x, z, _)| (x, z));
|
||||
@@ -681,7 +827,7 @@ fn draw_frame(app: &mut App, gfx: &Gfx, r3d: &mut R3d, ui: &mut Ui, menu: &mut M
|
||||
Some(Page::Title) | Some(Page::Settings { from_title: true, .. }) | Some(Page::KeyCapture { from_title: true, .. })
|
||||
);
|
||||
if !title_screen {
|
||||
let info = if over_ui {
|
||||
let info = if over_ui || (touch && inp.inspect.is_none()) {
|
||||
Vec::new()
|
||||
} else {
|
||||
let ent = r3d.pick_ent(&app.game, &app.cam, mx, my, false);
|
||||
@@ -698,5 +844,7 @@ fn draw_frame(app: &mut App, gfx: &Gfx, r3d: &mut R3d, ui: &mut Ui, menu: &mut M
|
||||
if !menu.open() || ui.show_help || ui.show_langsel {
|
||||
ui.draw_dialogs(&app.game, gfx);
|
||||
}
|
||||
ui.draw_cursor(gfx, &app.game, now, blocked);
|
||||
if !touch {
|
||||
ui.draw_cursor(gfx, &app.game, now, blocked);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user