Add browser gamepad support with pad button pictograms
Co-Authored-By: deepseek-v4.1-flash (opencode-go) via CodeCraft
This commit is contained in:
1 parent
073999d592
commit
8d51d6ec6a
26 files changed
+1066
-22
No files matched your search
+212
-1
@@ -6,7 +6,7 @@ use macroquad::prelude::*;
|
||||
|
||||
pub const CELL: f32 = 16.0;
|
||||
pub const ATLAS_COLS: u32 = 16;
|
||||
pub const ATLAS_ROWS: u32 = 18; // rows 14-17: cells only the high-res atlases fill (see HD_SETS)
|
||||
pub const ATLAS_ROWS: u32 = 20; // rows 14-17: cells only the high-res atlases fill (see HD_SETS); rows 18-19: gamepad pictograms
|
||||
|
||||
// atlas indices (4 variants each)
|
||||
pub const T_ROCK: u32 = 0;
|
||||
@@ -98,6 +98,30 @@ pub const T_GIFT: u32 = 220; // a wrapped present (Christmas)
|
||||
pub const T_FIR: u32 = 221; // fir needles with baubles (Christmas)
|
||||
pub const T_EGG: u32 = 222; // 2 variants: a painted egg (Easter)
|
||||
|
||||
// gamepad pictograms (rows 18-19 of the atlas): face buttons of the three pad types, d-pad,
|
||||
// sticks, shoulders and the select/start buttons
|
||||
pub const I_PAD_XBOX_A: u32 = 288; // A, B, X, Y on coloured discs
|
||||
pub const I_PAD_XBOX_B: u32 = 289;
|
||||
pub const I_PAD_XBOX_X: u32 = 290;
|
||||
pub const I_PAD_XBOX_Y: u32 = 291;
|
||||
pub const I_PAD_PS_CROSS: u32 = 292; // cross, circle, square, triangle on dark discs
|
||||
pub const I_PAD_PS_CIRCLE: u32 = 293;
|
||||
pub const I_PAD_PS_SQUARE: u32 = 294;
|
||||
pub const I_PAD_PS_TRIANGLE: u32 = 295;
|
||||
pub const I_PAD_NIN_A: u32 = 296; // A, B, X, Y on light discs
|
||||
pub const I_PAD_NIN_B: u32 = 297;
|
||||
pub const I_PAD_NIN_X: u32 = 298;
|
||||
pub const I_PAD_NIN_Y: u32 = 299;
|
||||
pub const I_PAD_DPAD: u32 = 300;
|
||||
pub const I_PAD_STICK_L: u32 = 301;
|
||||
pub const I_PAD_STICK_R: u32 = 302;
|
||||
pub const I_PAD_LB: u32 = 303;
|
||||
pub const I_PAD_RB: u32 = 304;
|
||||
pub const I_PAD_LT: u32 = 305;
|
||||
pub const I_PAD_RT: u32 = 306;
|
||||
pub const I_PAD_START: u32 = 307;
|
||||
pub const I_PAD_SELECT: u32 = 308;
|
||||
|
||||
pub struct Gfx {
|
||||
pub atlas: Texture2D,
|
||||
pub sprites: Texture2D,
|
||||
@@ -335,6 +359,42 @@ fn earth_px(x: i32, y: i32, v: u32) -> Color {
|
||||
shade(base, (big + fine) as i32)
|
||||
}
|
||||
|
||||
// ---- gamepad pictograms (16x16 cells; see I_PAD_*)
|
||||
|
||||
// tiny 3x5 letters for the button pictograms
|
||||
fn pad_glyph(ch: char, x: i32, y: i32) -> bool {
|
||||
if !(0..3).contains(&x) || !(0..5).contains(&y) {
|
||||
return false;
|
||||
}
|
||||
let rows: [u8; 5] = match ch {
|
||||
'A' => [0b010, 0b101, 0b111, 0b101, 0b101],
|
||||
'B' => [0b110, 0b101, 0b110, 0b101, 0b110],
|
||||
'X' => [0b101, 0b101, 0b010, 0b101, 0b101],
|
||||
'Y' => [0b101, 0b101, 0b010, 0b010, 0b010],
|
||||
'L' => [0b100, 0b100, 0b100, 0b100, 0b111],
|
||||
'R' => [0b110, 0b101, 0b110, 0b101, 0b101],
|
||||
'T' => [0b111, 0b010, 0b010, 0b010, 0b010],
|
||||
_ => [0; 5],
|
||||
};
|
||||
rows[y as usize] >> (2 - x) & 1 == 1
|
||||
}
|
||||
|
||||
// a round button: rim, fill, a highlight top left
|
||||
fn pad_disc(x: i32, y: i32, fill: (i32, i32, i32)) -> Option<Color> {
|
||||
let (dx, dy) = (x - 8, y - 8);
|
||||
let d2 = dx * dx + dy * dy;
|
||||
if d2 > 30 {
|
||||
return None;
|
||||
}
|
||||
Some(if d2 >= 22 {
|
||||
shade((fill.0 / 2, fill.1 / 2, fill.2 / 2), (rnd(x, y, 5) * 4.0) as i32)
|
||||
} else if dx + dy <= -7 {
|
||||
shade(fill, 26)
|
||||
} else {
|
||||
c(fill.0, fill.1, fill.2)
|
||||
})
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
// the interface draws atlas cells from the high-res atlas (the high-res graphics style)
|
||||
static HIGH_RES_UI: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
|
||||
@@ -1787,6 +1847,129 @@ fn gen_atlas() -> Image {
|
||||
});
|
||||
cv.cell(D_RUBY, |x, y| gems(x, y, 200, c(255, 150, 170), c(215, 30, 60), c(120, 12, 36)));
|
||||
cv.cell(D_DIAMOND, |x, y| gems(x, y, 210, c(255, 255, 255), c(170, 232, 255), c(80, 150, 200)));
|
||||
// ---- gamepad pictograms
|
||||
// a face button with a letter (Xbox, Nintendo)
|
||||
let face = |ch: char, fill: (i32, i32, i32), dark: bool| {
|
||||
move |x: i32, y: i32| {
|
||||
let disc = pad_disc(x, y, fill)?;
|
||||
if pad_glyph(ch, x - 7, y - 6) {
|
||||
return Some(if dark { c(42, 44, 50) } else { c(250, 250, 250) });
|
||||
}
|
||||
Some(disc)
|
||||
}
|
||||
};
|
||||
cv.cell(I_PAD_XBOX_A, face('A', (74, 166, 86), false));
|
||||
cv.cell(I_PAD_XBOX_B, face('B', (196, 66, 58), false));
|
||||
cv.cell(I_PAD_XBOX_X, face('X', (64, 108, 204), false));
|
||||
cv.cell(I_PAD_XBOX_Y, face('Y', (214, 176, 64), false));
|
||||
cv.cell(I_PAD_NIN_A, face('A', (200, 202, 210), true));
|
||||
cv.cell(I_PAD_NIN_B, face('B', (200, 202, 210), true));
|
||||
cv.cell(I_PAD_NIN_X, face('X', (200, 202, 210), true));
|
||||
cv.cell(I_PAD_NIN_Y, face('Y', (200, 202, 210), true));
|
||||
// PlayStation: the shape symbols on dark discs
|
||||
let symbol = |sym: u8, col: Color| {
|
||||
move |x: i32, y: i32| {
|
||||
let disc = pad_disc(x, y, (58, 60, 68))?;
|
||||
let (dx, dy) = (x - 8, y - 8);
|
||||
let on = match sym {
|
||||
0 => dx.abs() <= 3 && dy.abs() <= 3 && (dx == dy || dx == -dy),
|
||||
1 => {
|
||||
let d = ((dx * dx + dy * dy) as f32).sqrt();
|
||||
d >= 2.6 && d <= 4.1
|
||||
}
|
||||
2 => dx.abs() <= 3 && dy.abs() <= 3 && (dx.abs() == 3 || dy.abs() == 3),
|
||||
_ => dy >= -3 && dy <= 3 && (dx.abs() == (dy + 3) / 2 || (dy == 3 && dx.abs() <= 3)),
|
||||
};
|
||||
Some(if on { col } else { disc })
|
||||
}
|
||||
};
|
||||
cv.cell(I_PAD_PS_CROSS, symbol(0, c(96, 154, 255)));
|
||||
cv.cell(I_PAD_PS_CIRCLE, symbol(1, c(240, 96, 96)));
|
||||
cv.cell(I_PAD_PS_SQUARE, symbol(2, c(238, 130, 234)));
|
||||
cv.cell(I_PAD_PS_TRIANGLE, symbol(3, c(100, 214, 152)));
|
||||
// d-pad: a cross with a dark centre
|
||||
cv.cell(I_PAD_DPAD, |x, y| {
|
||||
let arm = (5..=10).contains(&x) || (5..=10).contains(&y);
|
||||
if !arm || !(1..=14).contains(&x) || !(1..=14).contains(&y) {
|
||||
return None;
|
||||
}
|
||||
let mid = (x - 8).abs() <= 1 && (y - 8).abs() <= 1;
|
||||
let edge = x == 1 || y == 1 || x == 14 || y == 14;
|
||||
Some(if mid { c(70, 72, 80) } else if edge { c(116, 118, 128) } else { c(172, 174, 186) })
|
||||
});
|
||||
// sticks: a ring with a knob, L/R in the corner
|
||||
let stick = |side: char| {
|
||||
move |x: i32, y: i32| {
|
||||
let (dx, dy) = (x - 8, y - 8);
|
||||
let d = ((dx * dx + dy * dy) as f32).sqrt();
|
||||
if d <= 6.0 {
|
||||
return Some(if d >= 4.6 {
|
||||
c(148, 150, 162)
|
||||
} else if d <= 2.8 {
|
||||
c(208, 210, 220)
|
||||
} else {
|
||||
c(88, 90, 102)
|
||||
});
|
||||
}
|
||||
if pad_glyph(side, x - 11, y - 10) {
|
||||
return Some(c(190, 192, 202));
|
||||
}
|
||||
None
|
||||
}
|
||||
};
|
||||
cv.cell(I_PAD_STICK_L, stick('L'));
|
||||
cv.cell(I_PAD_STICK_R, stick('R'));
|
||||
// shoulders (LB/RB) and triggers (LT/RT): a rounded body with its letters
|
||||
let bumper = |a: char, b: char, tall: bool| {
|
||||
move |x: i32, y: i32| {
|
||||
let (y0, y1) = if tall { (2, 10) } else { (4, 11) };
|
||||
let corner = (x == 3 || x == 12) && (y == y0 || y == y1);
|
||||
if !(3..=12).contains(&x) || !(y0..=y1).contains(&y) || corner {
|
||||
return None;
|
||||
}
|
||||
if pad_glyph(a, x - 4, y - (y0 + 1)) || pad_glyph(b, x - 8, y - (y0 + 1)) {
|
||||
return Some(c(52, 54, 62));
|
||||
}
|
||||
Some(if x == 3 || x == 12 || y == y0 {
|
||||
c(128, 130, 140)
|
||||
} else if y == y1 {
|
||||
c(94, 96, 106)
|
||||
} else {
|
||||
c(190, 192, 202)
|
||||
})
|
||||
}
|
||||
};
|
||||
cv.cell(I_PAD_LB, bumper('L', 'B', false));
|
||||
cv.cell(I_PAD_RB, bumper('R', 'B', false));
|
||||
cv.cell(I_PAD_LT, bumper('L', 'T', true));
|
||||
cv.cell(I_PAD_RT, bumper('R', 'T', true));
|
||||
// start: two bars (pause); select: two lines (list)
|
||||
cv.cell(I_PAD_START, |x, y| {
|
||||
if !(3..=12).contains(&x) || !(4..=11).contains(&y) {
|
||||
return None;
|
||||
}
|
||||
let bars = (x == 5 || x == 6 || x == 9 || x == 10) && (6..=9).contains(&y);
|
||||
Some(if bars {
|
||||
c(232, 234, 242)
|
||||
} else if x == 3 || y == 4 || x == 12 || y == 11 {
|
||||
c(68, 70, 80)
|
||||
} else {
|
||||
c(118, 120, 132)
|
||||
})
|
||||
});
|
||||
cv.cell(I_PAD_SELECT, |x, y| {
|
||||
if !(2..=13).contains(&x) || !(4..=11).contains(&y) {
|
||||
return None;
|
||||
}
|
||||
let lines = (5..=10).contains(&x) && (y == 6 || y == 9);
|
||||
Some(if lines {
|
||||
c(232, 234, 242)
|
||||
} else if x == 2 || y == 4 || x == 13 || y == 11 {
|
||||
c(68, 70, 80)
|
||||
} else {
|
||||
c(118, 120, 132)
|
||||
})
|
||||
});
|
||||
cv.img
|
||||
}
|
||||
|
||||
@@ -3563,6 +3746,34 @@ mod atlas_dump {
|
||||
}
|
||||
out.export_png("/tmp/atlases.png");
|
||||
}
|
||||
|
||||
// `cargo test --bins dump_pad_icons -- --ignored`: the gamepad pictograms, 6x scaled, in
|
||||
// /tmp/pad-icons.png (Xbox, PlayStation, Nintendo, then d-pad, sticks and co.)
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn dump_pad_icons() {
|
||||
let atlas = gen_atlas();
|
||||
let ids: [u32; 21] = [
|
||||
I_PAD_XBOX_A, I_PAD_XBOX_B, I_PAD_XBOX_X, I_PAD_XBOX_Y,
|
||||
I_PAD_PS_CROSS, I_PAD_PS_CIRCLE, I_PAD_PS_SQUARE, I_PAD_PS_TRIANGLE,
|
||||
I_PAD_NIN_A, I_PAD_NIN_B, I_PAD_NIN_X, I_PAD_NIN_Y,
|
||||
I_PAD_DPAD, I_PAD_STICK_L, I_PAD_STICK_R, I_PAD_LB, I_PAD_RB, I_PAD_LT, I_PAD_RT,
|
||||
I_PAD_START, I_PAD_SELECT,
|
||||
];
|
||||
let k = 6;
|
||||
let cols = 7;
|
||||
let rows = (ids.len() as u32).div_ceil(cols);
|
||||
let mut out = Image::gen_image_color((cols * 16 * k) as u16, (rows * 16 * k) as u16, BLACK);
|
||||
for (i, &id) in ids.iter().enumerate() {
|
||||
let (ox, oy) = ((id % ATLAS_COLS) * 16, (id / ATLAS_COLS) * 16);
|
||||
for y in 0..16 * k {
|
||||
for x in 0..16 * k {
|
||||
out.set_pixel((i as u32 % cols) * 16 * k + x, (i as u32 / cols) * 16 * k + y, atlas.get_pixel(ox + x / k, oy + y / k));
|
||||
}
|
||||
}
|
||||
}
|
||||
out.export_png("/tmp/pad-icons.png");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
+70
-16
@@ -30,6 +30,7 @@ mod personality;
|
||||
mod platform;
|
||||
mod portraits;
|
||||
mod orders;
|
||||
mod pad;
|
||||
mod particles;
|
||||
mod props;
|
||||
mod research;
|
||||
@@ -933,6 +934,7 @@ async fn main() {
|
||||
apply_game_settings(&mut app.game, &app.settings);
|
||||
let mut inp = Input { ldown: None, rdown: None, rlast: (0.0, 0.0), rdrag: false, mlast: None, mclick: -1.0, minimap_drag: false, tlast: None, touch_world: false, inspect: None };
|
||||
let mut touch = TouchInput::new();
|
||||
let mut pad = pad::Pad::new();
|
||||
let mut auto_touch = platform::coarse_pointer();
|
||||
let mut menu_swipe: Option<(f32, f32)> = None; // a swipe over a menu: last finger y, unused distance
|
||||
let mut menu_dragged = false; // the finger on a menu has swiped (no click when it lifts)
|
||||
@@ -1024,8 +1026,19 @@ async fn main() {
|
||||
let p = touch.finger().or(gpos).unwrap_or(vec2(-1e5, -1e5));
|
||||
ui::set_pointer(p.x, p.y);
|
||||
}
|
||||
// ---------------- gamepad: the browser's Gamepad API (the desktop build has none)
|
||||
pad.poll(app.settings.gamepad);
|
||||
ui::set_gamepad(pad.connected());
|
||||
// the pad's pointer starts where the mouse is and moves with the right stick; a mouse
|
||||
// movement takes it over again, so both can be used side by side (a finger still wins)
|
||||
if pad.connected() && !(touch_mode && touch.finger().is_some()) {
|
||||
let m = mouse_position();
|
||||
let moved = mouse_delta_position() != Vec2::ZERO;
|
||||
let (px, py) = pad.aim(dt, ui::sw(), ui::sh(), (m.0 * ui::dpr(), m.1 * ui::dpr()), moved);
|
||||
ui::set_pointer(px, py);
|
||||
}
|
||||
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)
|
||||
let (mx, my) = if touch_mode || ui::gamepad_ui() { (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();
|
||||
@@ -1242,8 +1255,8 @@ async fn main() {
|
||||
let keys = app.settings.clone();
|
||||
let key = |a: Action| keys.key(a);
|
||||
|
||||
// ---------------- Esc: chat, dialogs, menus, pause menu
|
||||
if is_key_pressed(KeyCode::Escape) {
|
||||
// ---------------- Esc (or the pad's B / Start): chat, dialogs, menus, pause menu
|
||||
if is_key_pressed(KeyCode::Escape) || pad.pressed(pad::Btn::East) || pad.pressed(pad::Btn::Start) {
|
||||
if ui.chat_open {
|
||||
ui.chat_open = false;
|
||||
ui.chat_text.clear();
|
||||
@@ -1293,9 +1306,28 @@ async fn main() {
|
||||
inp.ldown = None;
|
||||
}
|
||||
}
|
||||
if is_key_pressed(key(Action::Help)) {
|
||||
if is_key_pressed(key(Action::Help)) || pad.pressed(pad::Btn::Select) {
|
||||
ui.show_help = true;
|
||||
}
|
||||
// the pad cycles the tools: LB back, RB forward
|
||||
let tool_step = if pad.pressed(pad::Btn::Rb) {
|
||||
1
|
||||
} else if pad.pressed(pad::Btn::Lb) {
|
||||
-1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
if tool_step != 0 {
|
||||
let cur = TOOLS.iter().position(|&t| t == ui.tool).unwrap_or(0) as i32;
|
||||
for k in 1..=TOOLS.len() as i32 {
|
||||
let i = (cur + tool_step * k).rem_euclid(TOOLS.len() as i32) as usize;
|
||||
if app.game.tool_allowed(i) && app.game.tool_visible(i) {
|
||||
ui.tool = TOOLS[i];
|
||||
inp.ldown = None;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if is_key_pressed(key(Action::Pause)) && app.game.state == 0 {
|
||||
app.game.paused = !app.game.paused;
|
||||
}
|
||||
@@ -1346,11 +1378,19 @@ async fn main() {
|
||||
if is_key_down(key(Action::CamRight)) || is_key_down(KeyCode::Right) {
|
||||
move_cam(pan, 0.0, cam);
|
||||
}
|
||||
// the pad's left stick moves the map, as W/A/S/D do
|
||||
if pad.connected() {
|
||||
let s = pad.stick(pad::Stick::Left);
|
||||
if s != Vec2::ZERO {
|
||||
let k = pan * 1.8;
|
||||
move_cam(s.x * k, -s.y * k, cam);
|
||||
}
|
||||
}
|
||||
let edge = 6.0;
|
||||
if !mouse_moved && mouse_delta_position() != Vec2::ZERO {
|
||||
mouse_moved = true;
|
||||
}
|
||||
if keys.edge_scroll && mouse_moved && !touch_mode && mx >= 0.0 && my >= 0.0 && mx < screen_width() && my < screen_height() {
|
||||
if keys.edge_scroll && mouse_moved && !touch_mode && !ui::gamepad_ui() && 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 {
|
||||
@@ -1363,18 +1403,22 @@ async fn main() {
|
||||
}
|
||||
}
|
||||
let rot = 1.8 * dt * keys.rotate_speed;
|
||||
if is_key_down(key(Action::RotateLeft)) {
|
||||
if is_key_down(key(Action::RotateLeft)) || pad.down(pad::Btn::Left) {
|
||||
cam.yaw += rot;
|
||||
}
|
||||
if is_key_down(key(Action::RotateRight)) {
|
||||
if is_key_down(key(Action::RotateRight)) || pad.down(pad::Btn::Right) {
|
||||
cam.yaw -= rot;
|
||||
}
|
||||
if is_key_down(key(Action::TiltUp)) {
|
||||
if is_key_down(key(Action::TiltUp)) || pad.down(pad::Btn::Up) {
|
||||
cam.pitch += rot * 0.6;
|
||||
}
|
||||
if is_key_down(key(Action::TiltDown)) {
|
||||
if is_key_down(key(Action::TiltDown)) || pad.down(pad::Btn::Down) {
|
||||
cam.pitch -= rot * 0.6;
|
||||
}
|
||||
// the pad's left stick click resets the view
|
||||
if pad.pressed(pad::Btn::LStick) {
|
||||
cam.reset_view();
|
||||
}
|
||||
// a double click with the middle button: the standard angle and turn (same place)
|
||||
if is_mouse_button_pressed(MouseButton::Middle) && !touch_mode {
|
||||
let now = get_time();
|
||||
@@ -1405,6 +1449,15 @@ async fn main() {
|
||||
cam.tx += before.x - after.x;
|
||||
cam.tz += before.y - after.y;
|
||||
}
|
||||
// the pad's triggers zoom (RT in, LT out)
|
||||
let zoom_in = if pad.down(pad::Btn::Rt) { 1.0 } else { 0.0 } - if pad.down(pad::Btn::Lt) { 1.0 } else { 0.0 };
|
||||
if zoom_in != 0.0 && !over_ui {
|
||||
let before = cam.ground_point(mx, my);
|
||||
cam.zoom = (cam.zoom * (1.0 - zoom_in * 1.4 * dt * keys.zoom_speed)).clamp(6.0, 45.0);
|
||||
let after = cam.ground_point(mx, my);
|
||||
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 {
|
||||
@@ -1421,8 +1474,8 @@ async fn main() {
|
||||
cam.tz += before.y - after.y;
|
||||
}
|
||||
}
|
||||
// right button: drag = pan, click = drop
|
||||
if is_mouse_button_pressed(MouseButton::Right) {
|
||||
// right button (or the pad's X / square): drag = pan, click = drop
|
||||
if is_mouse_button_pressed(MouseButton::Right) || pad.pressed(pad::Btn::West) {
|
||||
inp.rdown = Some((mx, my));
|
||||
inp.rlast = (mx, my);
|
||||
inp.rdrag = false;
|
||||
@@ -1441,7 +1494,7 @@ async fn main() {
|
||||
inp.rlast = (mx, my);
|
||||
}
|
||||
}
|
||||
if is_mouse_button_released(MouseButton::Right) {
|
||||
if is_mouse_button_released(MouseButton::Right) || pad.released(pad::Btn::West) {
|
||||
if !inp.rdrag && !over_ui && app.game.state == 0 {
|
||||
if inp.ldown.is_some() {
|
||||
inp.ldown = None;
|
||||
@@ -1459,13 +1512,13 @@ async fn main() {
|
||||
// (in menus and dialogs a touch clicks when the finger lifts without a swipe, so that
|
||||
// swiping a list scrolls it instead of picking the entry under the finger)
|
||||
let menu_touch = touch_mode && (in_menu || dialog);
|
||||
let pressed = if menu_touch {
|
||||
let pressed = (if menu_touch {
|
||||
!menu_dragged && gestures.iter().any(|g| matches!(g, Gesture::Release(_)))
|
||||
} else if touch_mode {
|
||||
gestures.iter().any(|g| matches!(g, Gesture::Press(_)))
|
||||
} else {
|
||||
is_mouse_button_pressed(MouseButton::Left)
|
||||
};
|
||||
}) || pad.pressed(pad::Btn::South);
|
||||
if touch_mode && pressed {
|
||||
inp.touch_world = playing && !over_ui;
|
||||
inp.inspect = None;
|
||||
@@ -1916,7 +1969,7 @@ app.pending_briefing = briefing_of(&campaign, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
let held = if touch_mode { touch.finger().is_some() } else { is_mouse_button_down(MouseButton::Left) };
|
||||
let held = (if touch_mode { touch.finger().is_some() } else { is_mouse_button_down(MouseButton::Left) }) || pad.down(pad::Btn::South);
|
||||
if inp.minimap_drag {
|
||||
if held && ui.minimap_contains(ux, uy) {
|
||||
let (x, y) = ui.minimap_world(ux, uy);
|
||||
@@ -1976,7 +2029,8 @@ app.pending_briefing = briefing_of(&campaign, i);
|
||||
inp.inspect = None;
|
||||
}
|
||||
}
|
||||
if !touch_mode && is_mouse_button_released(MouseButton::Left) {
|
||||
let released = (if touch_mode { false } else { is_mouse_button_released(MouseButton::Left) }) || pad.released(pad::Btn::South);
|
||||
if released {
|
||||
if let Some(start) = inp.ldown.take() {
|
||||
if playing {
|
||||
finish_rect(&mut app, ui.tool, start, mx, my);
|
||||
|
||||
+21
-1
@@ -67,6 +67,7 @@ pub enum Item {
|
||||
Hints,
|
||||
Events,
|
||||
TouchUi,
|
||||
Gamepad,
|
||||
Voice,
|
||||
Track,
|
||||
}
|
||||
@@ -77,7 +78,7 @@ fn tab_items(tab: usize) -> &'static [Item] {
|
||||
0 => &[Language, Volume, MusicVolume, Track, Mute, Voice],
|
||||
1 => &[RotateSpeed, PanSpeed, ZoomSpeed, EdgeScroll, InvertMouse],
|
||||
2 => &[Outlines, WallDetail, Lights, UiScale, RenderScale],
|
||||
3 => &[GameSpeed, Hints, Events, TouchUi],
|
||||
3 => &[GameSpeed, Hints, Events, TouchUi, Gamepad],
|
||||
4 => &[DebugFps, DebugFrameTime, DebugPing, DebugTriggerZones, DebugCommandMarkers, DebugPathLines, DebugHoliday],
|
||||
_ => &[],
|
||||
}
|
||||
@@ -110,6 +111,7 @@ fn item_label(it: Item) -> String {
|
||||
Item::Hints => "set.hints",
|
||||
Item::Events => "set.events",
|
||||
Item::TouchUi => "set.touch_ui",
|
||||
Item::Gamepad => "set.gamepad",
|
||||
Item::Voice => "set.voice",
|
||||
Item::Track => "set.track",
|
||||
})
|
||||
@@ -169,6 +171,12 @@ fn item_value(it: Item, s: &Settings) -> String {
|
||||
2 => tr("val.off"),
|
||||
_ => trf("val.auto_state", &[tr(if crate::ui::touch_ui() { "val.on" } else { "val.off" })]),
|
||||
},
|
||||
Item::Gamepad => tr(match s.gamepad {
|
||||
1 => "val.xbox",
|
||||
2 => "val.playstation",
|
||||
3 => "val.nintendo",
|
||||
_ => "val.auto_plain",
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,6 +229,7 @@ fn item_change(it: Item, s: &mut Settings, d: i32) {
|
||||
Item::Hints => s.hints = !s.hints,
|
||||
Item::Events => s.events = !s.events,
|
||||
Item::TouchUi => s.touch_ui = cycle(&[0u8, 1, 2], s.touch_ui, d),
|
||||
Item::Gamepad => s.gamepad = cycle(&[0u8, 1, 2, 3], s.gamepad, d),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1514,6 +1523,17 @@ impl Menu {
|
||||
// value with < > buttons
|
||||
let vw = 18.0 * CW();
|
||||
let vx = x + cols as f32 * CW() - vw - CW() * 3.0;
|
||||
if it == Item::Gamepad {
|
||||
// the type's face buttons, right before the value: the pictograms the game uses
|
||||
let kind = crate::pad::kind();
|
||||
let isz = 2.0 * CH();
|
||||
let gap = CW();
|
||||
let mut ix = vx - CW() - 4.0 * isz - 3.0 * gap;
|
||||
for b in [crate::pad::Btn::North, crate::pad::Btn::West, crate::pad::Btn::East, crate::pad::Btn::South] {
|
||||
gfx.draw_cell(crate::ui::pad_icon(kind, b), ix, by, isz, isz, WHITE);
|
||||
ix += isz + gap;
|
||||
}
|
||||
}
|
||||
if it == Item::Language {
|
||||
self.lang_field(gfx, vx, by, vw);
|
||||
continue;
|
||||
|
||||
+229
@@ -0,0 +1,229 @@
|
||||
// Gamepads via the browser's Gamepad API (see www/dc_plugin.js): polling, buttons, sticks and
|
||||
// the virtual pointer. Native builds have no gamepad input (miniquad does not poll pads), so
|
||||
// platform's stubs report "no gamepad" there.
|
||||
//
|
||||
// The standard Gamepad API mapping: buttons 0-3 = face buttons, 4/5 = shoulders, 6/7 = triggers,
|
||||
// 8/9 = select/start, 10/11 = stick clicks, 12-15 = d-pad; axes 0/1 = left stick, 2/3 = right.
|
||||
|
||||
use macroquad::prelude::Vec2;
|
||||
use std::cell::Cell;
|
||||
|
||||
// the standard Gamepad API button order; every variant keeps its index (RStick is 11), even the
|
||||
// ones no action uses yet
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub enum Btn {
|
||||
South, // A (Xbox, Nintendo), cross (PlayStation)
|
||||
East, // B (Xbox), circle (PlayStation), A (Nintendo)
|
||||
West, // X (Xbox), square (PlayStation), Y (Nintendo)
|
||||
North, // Y (Xbox), triangle (PlayStation), X (Nintendo)
|
||||
Lb,
|
||||
Rb,
|
||||
Lt,
|
||||
Rt,
|
||||
Select,
|
||||
Start,
|
||||
LStick,
|
||||
RStick, // kept for the standard button index (11); no action uses it yet
|
||||
Up,
|
||||
Down,
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
impl Btn {
|
||||
fn bit(self) -> u32 {
|
||||
1 << self as u32
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum Stick {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
// The button pictograms the interface shows (Xbox, PlayStation, Nintendo).
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum Kind {
|
||||
Xbox,
|
||||
PlayStation,
|
||||
Nintendo,
|
||||
}
|
||||
|
||||
// Settings value: 0 automatic, 1 Xbox, 2 PlayStation, 3 Nintendo.
|
||||
pub fn kind_for(setting: u8, detected: i32) -> Kind {
|
||||
match setting {
|
||||
1 => Kind::Xbox,
|
||||
2 => Kind::PlayStation,
|
||||
3 => Kind::Nintendo,
|
||||
_ => match detected {
|
||||
2 => Kind::PlayStation,
|
||||
3 => Kind::Nintendo,
|
||||
_ => Kind::Xbox,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static KIND: Cell<Kind> = const { Cell::new(Kind::Xbox) };
|
||||
}
|
||||
|
||||
// The kind whose pictograms the interface draws: the setting, or the automatic choice.
|
||||
pub fn kind() -> Kind {
|
||||
KIND.with(|c| c.get())
|
||||
}
|
||||
|
||||
// Below this deflection a stick rests (controllers do not return to exactly 0).
|
||||
const DEAD: f32 = 0.18;
|
||||
|
||||
pub struct Pad {
|
||||
connected: bool,
|
||||
buttons: u32,
|
||||
prev: u32,
|
||||
axes: [f32; 4],
|
||||
pointer: (f32, f32), // device pixels of the interface
|
||||
pointer_ready: bool,
|
||||
}
|
||||
|
||||
impl Pad {
|
||||
pub fn new() -> Pad {
|
||||
Pad { connected: false, buttons: 0, prev: 0, axes: [0.0; 4], pointer: (0.0, 0.0), pointer_ready: false }
|
||||
}
|
||||
|
||||
// Read the pad for this frame; kind_setting = Settings::gamepad.
|
||||
pub fn poll(&mut self, kind_setting: u8) {
|
||||
self.prev = if self.connected { self.buttons } else { 0 };
|
||||
let slot = crate::platform::gamepad_slot();
|
||||
self.connected = slot >= 0;
|
||||
let detected = if self.connected { crate::platform::gamepad_kind(slot) } else { 0 };
|
||||
KIND.with(|c| c.set(kind_for(kind_setting, detected)));
|
||||
if !self.connected {
|
||||
self.buttons = 0;
|
||||
self.axes = [0.0; 4];
|
||||
return;
|
||||
}
|
||||
self.buttons = crate::platform::gamepad_buttons(slot);
|
||||
for (i, a) in self.axes.iter_mut().enumerate() {
|
||||
*a = crate::platform::gamepad_axis(slot, i as i32);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn connected(&self) -> bool {
|
||||
self.connected
|
||||
}
|
||||
|
||||
pub fn down(&self, b: Btn) -> bool {
|
||||
self.connected && self.buttons & b.bit() != 0
|
||||
}
|
||||
|
||||
pub fn pressed(&self, b: Btn) -> bool {
|
||||
self.connected && self.buttons & b.bit() != 0 && self.prev & b.bit() == 0
|
||||
}
|
||||
|
||||
pub fn released(&self, b: Btn) -> bool {
|
||||
self.connected && self.buttons & b.bit() == 0 && self.prev & b.bit() != 0
|
||||
}
|
||||
|
||||
// stick deflection, dead zone removed and scaled back to 0..1
|
||||
pub fn stick(&self, side: Stick) -> Vec2 {
|
||||
let (i, j) = match side {
|
||||
Stick::Left => (0, 1),
|
||||
Stick::Right => (2, 3),
|
||||
};
|
||||
let v = Vec2::new(self.axes[i], self.axes[j]);
|
||||
let l = v.length();
|
||||
if l <= DEAD {
|
||||
Vec2::ZERO
|
||||
} else {
|
||||
v / l * ((l - DEAD) / (1.0 - DEAD))
|
||||
}
|
||||
}
|
||||
|
||||
// Move the interface pointer with the right stick. Mouse movements take it over at once, so
|
||||
// mouse and pad can be used side by side; (w, h) is the interface size in device pixels.
|
||||
// Returns the pointer in device pixels.
|
||||
pub fn aim(&mut self, dt: f32, w: f32, h: f32, mouse: (f32, f32), mouse_moved: bool) -> (f32, f32) {
|
||||
let w = w.max(1.0);
|
||||
let h = h.max(1.0);
|
||||
if !self.pointer_ready || mouse_moved {
|
||||
self.pointer = (mouse.0.clamp(0.0, w), mouse.1.clamp(0.0, h));
|
||||
self.pointer_ready = true;
|
||||
}
|
||||
let s = self.stick(Stick::Right);
|
||||
let speed = w.max(640.0) * 1.15; // a full deflection crosses the screen in about a second
|
||||
self.pointer.0 = (self.pointer.0 + s.x * speed * dt).clamp(0.0, w);
|
||||
self.pointer.1 = (self.pointer.1 + s.y * speed * dt).clamp(0.0, h);
|
||||
self.pointer
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn the_setting_picks_the_pictograms() {
|
||||
assert_eq!(kind_for(0, 1), Kind::Xbox);
|
||||
assert_eq!(kind_for(0, 2), Kind::PlayStation);
|
||||
assert_eq!(kind_for(0, 3), Kind::Nintendo);
|
||||
assert_eq!(kind_for(0, 0), Kind::Xbox);
|
||||
assert_eq!(kind_for(2, 1), Kind::PlayStation);
|
||||
assert_eq!(kind_for(3, 1), Kind::Nintendo);
|
||||
assert_eq!(kind_for(1, 2), Kind::Xbox);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sticks_have_a_dead_zone() {
|
||||
let mut p = Pad::new();
|
||||
p.connected = true;
|
||||
p.axes = [0.1, -0.12, 0.0, 0.0];
|
||||
assert_eq!(p.stick(Stick::Left), Vec2::ZERO);
|
||||
p.axes = [1.0, 0.0, 0.0, 0.0];
|
||||
let s = p.stick(Stick::Left);
|
||||
assert!((s.x - 1.0).abs() < 0.001 && s.y.abs() < 0.001);
|
||||
p.axes = [0.0, -0.59, 0.0, 0.0];
|
||||
let s = p.stick(Stick::Left);
|
||||
assert!((s.y + 0.5).abs() < 0.001);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buttons_report_edges() {
|
||||
let mut p = Pad::new();
|
||||
p.connected = true;
|
||||
p.buttons = 0;
|
||||
p.prev = 0;
|
||||
assert!(!p.down(Btn::South));
|
||||
p.buttons = Btn::South.bit();
|
||||
assert!(p.pressed(Btn::South) && p.down(Btn::South) && !p.released(Btn::South));
|
||||
p.prev = p.buttons;
|
||||
assert!(!p.pressed(Btn::South) && p.down(Btn::South));
|
||||
p.buttons = 0;
|
||||
p.prev = Btn::South.bit();
|
||||
assert!(!p.down(Btn::South) && p.released(Btn::South));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_disconnected_pad_stays_silent() {
|
||||
let mut p = Pad::new();
|
||||
p.buttons = Btn::South.bit();
|
||||
p.prev = 0;
|
||||
assert!(!p.down(Btn::South));
|
||||
assert!(!p.pressed(Btn::South));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_pointer_follows_the_mouse_and_the_stick() {
|
||||
let mut p = Pad::new();
|
||||
p.connected = true;
|
||||
// the first frame takes the mouse position
|
||||
assert_eq!(p.aim(0.016, 1000.0, 600.0, (100.0, 200.0), false), (100.0, 200.0));
|
||||
// the right stick moves it
|
||||
p.axes = [0.0, 0.0, 1.0, 0.0];
|
||||
let (x, _) = p.aim(0.1, 1000.0, 600.0, (0.0, 0.0), false);
|
||||
assert!(x > 200.0);
|
||||
// a mouse movement takes it over again (a resting stick moves nothing)
|
||||
p.axes = [0.0, 0.0, 0.0, 0.0];
|
||||
assert_eq!(p.aim(0.1, 1000.0, 600.0, (5.0, 5.0), true), (5.0, 5.0));
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,10 @@ mod imp {
|
||||
fn dc_ws_take(id: i32) -> i32;
|
||||
fn dc_ws_close(id: i32);
|
||||
fn dc_renderer() -> i32;
|
||||
fn dc_gamepad_slot() -> i32;
|
||||
fn dc_gamepad_buttons(slot: i32) -> u32;
|
||||
fn dc_gamepad_axis(slot: i32, axis: i32) -> f32;
|
||||
fn dc_gamepad_kind(slot: i32) -> i32;
|
||||
}
|
||||
|
||||
// starts a request; poll the returned id with http_poll()
|
||||
@@ -189,6 +193,25 @@ mod imp {
|
||||
String::from_utf8(buf).unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
// the first connected gamepad (-1: none), see www/dc_plugin.js
|
||||
pub fn gamepad_slot() -> i32 {
|
||||
unsafe { dc_gamepad_slot() }
|
||||
}
|
||||
|
||||
// bit i: standard button i is down
|
||||
pub fn gamepad_buttons(slot: i32) -> u32 {
|
||||
unsafe { dc_gamepad_buttons(slot) }
|
||||
}
|
||||
|
||||
pub fn gamepad_axis(slot: i32, axis: i32) -> f32 {
|
||||
unsafe { dc_gamepad_axis(slot, axis) }
|
||||
}
|
||||
|
||||
// 0 unknown, 1 Xbox, 2 PlayStation, 3 Nintendo
|
||||
pub fn gamepad_kind(slot: i32) -> i32 {
|
||||
unsafe { dc_gamepad_kind(slot) }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
@@ -364,6 +387,21 @@ mod imp {
|
||||
return "OpenGL".into();
|
||||
}
|
||||
}
|
||||
|
||||
// The Gamepad API only exists in browsers. The desktop build has no gamepad input yet
|
||||
// (miniquad does not poll gamepads; that would need a crate like gilrs).
|
||||
pub fn gamepad_slot() -> i32 {
|
||||
-1
|
||||
}
|
||||
pub fn gamepad_buttons(_slot: i32) -> u32 {
|
||||
0
|
||||
}
|
||||
pub fn gamepad_axis(_slot: i32, _axis: i32) -> f32 {
|
||||
0.0
|
||||
}
|
||||
pub fn gamepad_kind(_slot: i32) -> i32 {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
pub use imp::*;
|
||||
|
||||
@@ -153,6 +153,7 @@ pub struct Settings {
|
||||
pub render_scale: u8, // old pixel-look divisor (no longer used, kept for old settings files)
|
||||
pub pixel_rows: u16, // graphics style: rows of the world on every screen (270 pixel art, 1080 high-res)
|
||||
pub touch_ui: u8, // touch interface: 0 = automatic (after the first touch), 1 = on, 2 = off
|
||||
pub gamepad: u8, // gamepad pictograms: 0 automatic (from the pad's id), 1 Xbox, 2 PlayStation, 3 Nintendo
|
||||
pub debug_fps: bool, // show FPS and rendering backend
|
||||
pub debug_frame_time: bool, // show the frame-time line and graph
|
||||
pub debug_ping: bool, // show network ping
|
||||
@@ -195,6 +196,7 @@ impl Default for Settings {
|
||||
render_scale: 1,
|
||||
pixel_rows: PIXEL_ART_ROWS,
|
||||
touch_ui: 0,
|
||||
gamepad: 0,
|
||||
debug_fps: false,
|
||||
debug_frame_time: false,
|
||||
debug_ping: false,
|
||||
|
||||
@@ -19,6 +19,7 @@ use macroquad::prelude::*;
|
||||
// setting; automatic from the resolution in device pixels).
|
||||
thread_local! {
|
||||
static TOUCH: std::cell::Cell<bool> = std::cell::Cell::new(false);
|
||||
static GAMEPAD: std::cell::Cell<bool> = std::cell::Cell::new(false);
|
||||
static POINTER: std::cell::Cell<(f32, f32)> = std::cell::Cell::new((-1e5, -1e5));
|
||||
static VIRTUAL_ROWS: std::cell::Cell<u32> = std::cell::Cell::new(0);
|
||||
static UI_TARGET: std::cell::RefCell<Option<(RenderTarget, u32, u32)>> = std::cell::RefCell::new(None);
|
||||
@@ -50,6 +51,13 @@ pub fn set_touch(on: bool) {
|
||||
pub fn touch_ui() -> bool {
|
||||
TOUCH.with(|c| c.get())
|
||||
}
|
||||
// a gamepad is connected: the interface shows its button pictograms (see pad.rs)
|
||||
pub fn set_gamepad(on: bool) {
|
||||
GAMEPAD.with(|c| c.set(on));
|
||||
}
|
||||
pub fn gamepad_ui() -> bool {
|
||||
GAMEPAD.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)));
|
||||
@@ -70,7 +78,7 @@ pub fn sh() -> f32 {
|
||||
(screen_height() * dpr()).round()
|
||||
}
|
||||
pub fn mouse() -> (f32, f32) {
|
||||
if touch_ui() {
|
||||
if touch_ui() || gamepad_ui() {
|
||||
return POINTER.with(|c| c.get());
|
||||
}
|
||||
let (x, y) = mouse_position();
|
||||
@@ -443,6 +451,35 @@ pub const CATS: [(&str, &[usize]); 7] = [
|
||||
("cat.orders", &[8, 45, 46, 7]),
|
||||
];
|
||||
|
||||
// The atlas cell of a gamepad button's pictogram, in the pad type's own symbols: Xbox A/B/X/Y,
|
||||
// PlayStation cross/circle/square/triangle, Nintendo A/B/X/Y.
|
||||
pub fn pad_icon(kind: crate::pad::Kind, b: crate::pad::Btn) -> u32 {
|
||||
use crate::pad::{Btn, Kind};
|
||||
match (kind, b) {
|
||||
(Kind::Xbox, Btn::South) => I_PAD_XBOX_A,
|
||||
(Kind::Xbox, Btn::East) => I_PAD_XBOX_B,
|
||||
(Kind::Xbox, Btn::West) => I_PAD_XBOX_X,
|
||||
(Kind::Xbox, Btn::North) => I_PAD_XBOX_Y,
|
||||
(Kind::PlayStation, Btn::South) => I_PAD_PS_CROSS,
|
||||
(Kind::PlayStation, Btn::East) => I_PAD_PS_CIRCLE,
|
||||
(Kind::PlayStation, Btn::West) => I_PAD_PS_SQUARE,
|
||||
(Kind::PlayStation, Btn::North) => I_PAD_PS_TRIANGLE,
|
||||
(Kind::Nintendo, Btn::South) => I_PAD_NIN_A,
|
||||
(Kind::Nintendo, Btn::East) => I_PAD_NIN_B,
|
||||
(Kind::Nintendo, Btn::West) => I_PAD_NIN_X,
|
||||
(Kind::Nintendo, Btn::North) => I_PAD_NIN_Y,
|
||||
(_, Btn::Lb) => I_PAD_LB,
|
||||
(_, Btn::Rb) => I_PAD_RB,
|
||||
(_, Btn::Lt) => I_PAD_LT,
|
||||
(_, Btn::Rt) => I_PAD_RT,
|
||||
(_, Btn::Start) => I_PAD_START,
|
||||
(_, Btn::Select) => I_PAD_SELECT,
|
||||
(_, Btn::LStick) => I_PAD_STICK_L,
|
||||
(_, Btn::RStick) => I_PAD_STICK_R,
|
||||
_ => I_PAD_DPAD,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Debug)]
|
||||
enum TBtn {
|
||||
Cat(usize),
|
||||
@@ -897,7 +934,8 @@ impl Ui {
|
||||
}
|
||||
|
||||
// ---------------- toolbar: buttons of 6 x 3 characters, icons and labels on the grid
|
||||
let tb_rows = 5;
|
||||
// (with a gamepad one row more: the pad's buttons as pictograms underneath)
|
||||
let tb_rows = if gamepad_ui() { 6 } else { 5 };
|
||||
let tb_y = sh - tb_rows as f32 * CH();
|
||||
let tb_y = (tb_y / CH()).floor() * CH();
|
||||
let tb_h = sh - tb_y;
|
||||
@@ -954,7 +992,7 @@ impl Ui {
|
||||
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 { "" };
|
||||
let key = if tools.len() == 1 && !touch_ui() && !gamepad_ui() { keys[tools[0]].as_str() } else { "" };
|
||||
if hovered && tools.len() == 1 {
|
||||
hover_tool = Some(tools[0]);
|
||||
}
|
||||
@@ -1010,6 +1048,44 @@ impl Ui {
|
||||
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));
|
||||
// gamepad: its buttons as pictograms, on the extra row under the buttons
|
||||
if gamepad_ui() {
|
||||
let kind = crate::pad::kind();
|
||||
let icon = 2.0 * CH(); // a 16 px atlas cell at 2*F
|
||||
let gap = CW();
|
||||
let south = [pad_icon(kind, crate::pad::Btn::South)];
|
||||
let east = [pad_icon(kind, crate::pad::Btn::East)];
|
||||
let shoulders = [I_PAD_LB, I_PAD_RB];
|
||||
let start = [I_PAD_START];
|
||||
let sticks = [I_PAD_STICK_L, I_PAD_STICK_R];
|
||||
let triggers = [I_PAD_LT, I_PAD_RT];
|
||||
let entries: [(&[u32], &str); 6] = [
|
||||
(&south, "pad.use"),
|
||||
(&east, "pad.back"),
|
||||
(&shoulders, "pad.tool"),
|
||||
(&start, "pad.menu"),
|
||||
(&sticks, "pad.camera"),
|
||||
(&triggers, "pad.zoom"),
|
||||
];
|
||||
let width = |icons: &[u32], label: &str| icon * icons.len() as f32 + gap * 0.5 * (icons.len() - 1) as f32 + gap + gfx.measure(label, SMALL());
|
||||
let total = |n: usize| entries[..n].iter().map(|(i, k)| width(i, &tr(k))).sum::<f32>() + gap * 1.5 * (n - 1) as f32;
|
||||
let avail = sw - 2.0 * CW();
|
||||
let mut n = entries.len();
|
||||
while n > 2 && total(n) > avail {
|
||||
n -= 1;
|
||||
}
|
||||
let mut lx = grid_center(sw, total(n));
|
||||
let ly = tb_y + 4.0 * CH();
|
||||
for (icons, key) in entries.iter().take(n) {
|
||||
for &id in icons.iter() {
|
||||
gfx.draw_cell(id, lx, ly, icon, icon, WHITE);
|
||||
lx += icon + gap * 0.5;
|
||||
}
|
||||
let label = tr(key);
|
||||
gfx.text(&label, lx, ly + (icon - GH * SMALL()) * 0.5, SMALL(), TXT);
|
||||
lx += gfx.measure(&label, SMALL()) + gap * 1.5;
|
||||
}
|
||||
}
|
||||
// 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
|
||||
@@ -1524,7 +1600,9 @@ impl Ui {
|
||||
}
|
||||
|
||||
fn draw_help(&mut self, gfx: &Gfx) {
|
||||
let controls: &[&str] = if touch_ui() {
|
||||
let controls: &[&str] = if gamepad_ui() {
|
||||
&["help.p_move", "help.p_aim", "help.p_rot", "help.p_zoom", "help.p_use", "help.p_back", "help.p_tool"]
|
||||
} else 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_reset", "help.k_zoom", "help.k_tools", "help.k_misc", "help.k_misc2"]
|
||||
|
||||
Reference in new issue
Block a user