Scroll long menus and lists with the mouse wheel or a swipe
Touch clicks in menus happen on release without a swipe. The first-start language picker is a flag grid that scrolls. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d8e9184a9c
commit
6629472811
4 files changed
+156
-24
No files matched your search
+51
-6
@@ -842,6 +842,8 @@ async fn main() {
|
||||
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();
|
||||
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)
|
||||
// until the mouse has really moved its position is (0, 0): no edge scrolling before that
|
||||
let mut mouse_moved = false; // automatic mode: touch interface after a touch, desktop after a mouse click
|
||||
let mut autosave_t = 0.0f32;
|
||||
@@ -1025,11 +1027,45 @@ async fn main() {
|
||||
}
|
||||
}
|
||||
let (_, wheel) = mouse_wheel();
|
||||
if wheel != 0.0 && menu.page.is_some() {
|
||||
let d = if wheel > 0.0 { -1 } else { 1 };
|
||||
// an open language dropdown scrolls first
|
||||
if !menu.lang_wheel(d) && menu.scrollable() {
|
||||
menu.scroll_lines(d);
|
||||
if wheel != 0.0 && ui.show_langsel {
|
||||
ui.langsel_wheel(if wheel > 0.0 { -1 } else { 1 });
|
||||
} else if wheel != 0.0 && menu.page.is_some() {
|
||||
menu.wheel(if wheel > 0.0 { -1 } else { 1 });
|
||||
} else if wheel != 0.0 {
|
||||
ui.overview_wheel(ux, uy, if wheel > 0.0 { -1 } else { 1 });
|
||||
}
|
||||
// touch: a swipe over a menu, the language grid or the creature overview scrolls it,
|
||||
// one line per row height
|
||||
if touch_mode {
|
||||
for g in &gestures {
|
||||
match *g {
|
||||
Gesture::Press(_) => menu_dragged = false,
|
||||
Gesture::DragStart(p) if menu.page.is_some() || ui.show_langsel || ui.overview_wheel(p.x, p.y, 0) => {
|
||||
menu_swipe = Some((p.y, 0.0));
|
||||
menu_dragged = true;
|
||||
}
|
||||
Gesture::DragMove(p) => {
|
||||
if let Some((last, acc)) = menu_swipe {
|
||||
let mut acc = acc + p.y - last;
|
||||
let step = ui::CH() * 2.0;
|
||||
while acc.abs() >= step {
|
||||
// finger up: the content follows it up, so the list scrolls down
|
||||
let d = if acc < 0.0 { 1 } else { -1 };
|
||||
if ui.show_langsel {
|
||||
ui.langsel_wheel(d);
|
||||
} else if menu.page.is_some() {
|
||||
menu.wheel(d);
|
||||
} else {
|
||||
ui.overview_wheel(p.x, p.y, d);
|
||||
}
|
||||
acc += d as f32 * step;
|
||||
}
|
||||
menu_swipe = Some((p.y, acc));
|
||||
}
|
||||
}
|
||||
Gesture::DragEnd | Gesture::Cancel => menu_swipe = None,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1269,7 +1305,16 @@ async fn main() {
|
||||
}
|
||||
|
||||
// ---------------- 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) };
|
||||
// (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 {
|
||||
!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)
|
||||
};
|
||||
if touch_mode && pressed {
|
||||
inp.touch_world = playing && !over_ui;
|
||||
inp.inspect = None;
|
||||
|
||||
+48
-8
@@ -15,7 +15,7 @@ use crate::ui::{grid_center, grid_center_y, mouse, sh, sw, tool_info, wrap, side
|
||||
use macroquad::prelude::*;
|
||||
|
||||
mod bestiary;
|
||||
mod flags;
|
||||
pub mod flags;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum Page {
|
||||
@@ -645,6 +645,7 @@ impl Menu {
|
||||
MenuAction::Consumed
|
||||
}
|
||||
Some(Btn::Tab(t)) => {
|
||||
self.scroll = 0;
|
||||
self.page = Some(Page::Settings { tab: t, from_title });
|
||||
MenuAction::Consumed
|
||||
}
|
||||
@@ -1171,9 +1172,17 @@ impl Menu {
|
||||
let half = (acts.len() + 1) / 2;
|
||||
let colw = (cols as f32 - 6.0) * CW() * 0.5;
|
||||
let rowh = CH();
|
||||
// on small screens the two columns scroll (mouse wheel, swipe)
|
||||
let vis = (((y + (rows - 6) as f32 * CH() - top) / rowh).floor() as usize).max(1).min(half);
|
||||
self.rows_visible = vis;
|
||||
self.scroll = self.scroll.min(half - vis);
|
||||
for (k, a) in acts.iter().enumerate() {
|
||||
let c = k / half;
|
||||
let r = k % half;
|
||||
if r < self.scroll || r >= self.scroll + vis {
|
||||
continue;
|
||||
}
|
||||
let r = r - self.scroll;
|
||||
let bx = x + CW() * 2.0 + c as f32 * (colw + CW() * 2.0);
|
||||
let by = top + r as f32 * rowh;
|
||||
let rect = Rect::new(bx, by - 2.0, colw, rowh - 2.0);
|
||||
@@ -1188,8 +1197,12 @@ impl Menu {
|
||||
gfx.text(&key, bx + colw - gfx.measure(&key, S()) - CW() * 0.5, by, S(), GOLD);
|
||||
self.buttons.push((rect, Btn::Key(k)));
|
||||
}
|
||||
if vis < half {
|
||||
let pos = format!("{}-{}/{}", self.scroll + 1, self.scroll + vis, half);
|
||||
gfx.text(&pos, x + cols as f32 * CW() - gfx.measure(&pos, SMALL()) - CW() * 2.0, top - CH() * 1.2, SMALL(), DIM);
|
||||
}
|
||||
let hint = tr("set.keys_hint");
|
||||
gfx.text(&hint, x + CW() * 2.0, top + half as f32 * rowh + CH() * 0.5, SMALL(), DIM);
|
||||
gfx.text(&hint, x + CW() * 2.0, top + vis as f32 * rowh + CH() * 0.5, SMALL(), DIM);
|
||||
let rw = gfx.measure(&tr("set.reset_keys"), S()) + CW() * 4.0;
|
||||
let rr = Rect::new(x + cols as f32 * CW() - rw - CW() * 2.0, y + (rows - 4) as f32 * CH(), rw, CH() * 2.0);
|
||||
button(gfx, rr, &tr("set.reset_keys"), rr.contains(vec2(mx, my)), FRAME);
|
||||
@@ -1612,7 +1625,22 @@ impl Menu {
|
||||
|
||||
// scrolling on pages with lists (mouse wheel)
|
||||
pub fn scrollable(&self) -> bool {
|
||||
matches!(self.page, Some(Page::Saves { .. }) | Some(Page::Campaign) | Some(Page::CampaignEditor) | Some(Page::Briefing { .. }))
|
||||
matches!(
|
||||
self.page,
|
||||
Some(Page::Saves { .. }) | Some(Page::Campaign) | Some(Page::CampaignEditor) | Some(Page::Briefing { .. }) | Some(Page::Settings { tab: 5, .. }) | Some(Page::Match { .. }) | Some(Page::Account)
|
||||
)
|
||||
}
|
||||
|
||||
// mouse wheel or a swipe: scrolls what is under it (an open dropdown first); d lines down
|
||||
pub fn wheel(&mut self, d: i32) {
|
||||
if self.lang_wheel(d) {
|
||||
return;
|
||||
}
|
||||
if matches!(self.page, Some(Page::Bestiary { .. })) {
|
||||
self.bestiary_wheel(d);
|
||||
} else if self.scrollable() {
|
||||
self.scroll_lines(d);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scroll_lines(&mut self, d: i32) {
|
||||
@@ -1729,11 +1757,16 @@ impl Menu {
|
||||
let sure = tr("saves.sure");
|
||||
let dw = gfx.measure(&del, S()).max(gfx.measure(&sure, S())) + CW() * 3.0;
|
||||
let bottom = y + (rows - 5) as f32 * CH();
|
||||
for (k, u) in cloud.users.iter().enumerate() {
|
||||
let ry = yy + k as f32 * CH() * 2.5;
|
||||
if ry + CH() * 2.0 > bottom {
|
||||
break;
|
||||
// more users than fit: the list scrolls (mouse wheel, swipe)
|
||||
let vis = (((bottom - yy) / (CH() * 2.5)).floor() as usize).max(1);
|
||||
self.rows_visible = vis;
|
||||
self.scroll = self.scroll.min(cloud.users.len().saturating_sub(vis));
|
||||
if cloud.users.len() > vis {
|
||||
let pos = format!("{}-{}/{}", self.scroll + 1, (self.scroll + vis).min(cloud.users.len()), cloud.users.len());
|
||||
gfx.text(&pos, ix + w - gfx.measure(&pos, SMALL()), yy - CH() * 1.2, SMALL(), DIM);
|
||||
}
|
||||
for (k, u) in cloud.users.iter().enumerate().skip(self.scroll).take(vis) {
|
||||
let ry = yy + (k - self.scroll) as f32 * CH() * 2.5;
|
||||
let label = if u.admin { format!("{} (admin)", u.name) } else { u.name.clone() };
|
||||
gfx.text(&label, ix, ry + CH() * 0.5, S(), TXT);
|
||||
if !u.admin {
|
||||
@@ -1847,7 +1880,14 @@ impl Menu {
|
||||
if !ui.rooms.is_empty() && !ui.started {
|
||||
gfx.text(&tr("net.rooms"), ix, yy, S(), GOLD);
|
||||
yy += CH() * 1.4;
|
||||
for (k, room) in ui.rooms.iter().take(4).enumerate() {
|
||||
// more than four open matches: the list scrolls (mouse wheel, swipe)
|
||||
self.rows_visible = 4;
|
||||
self.scroll = self.scroll.min(ui.rooms.len().saturating_sub(4));
|
||||
if ui.rooms.len() > 4 {
|
||||
let pos = format!("{}-{}/{}", self.scroll + 1, self.scroll + 4, ui.rooms.len());
|
||||
gfx.text(&pos, ix + 25.0 * CW(), yy - CH() * 1.4, SMALL(), DIM);
|
||||
}
|
||||
for (k, room) in ui.rooms.iter().enumerate().skip(self.scroll).take(4) {
|
||||
let label = format!("{} {}{}{}", room.code, room.players, if room.started { " *" } else { "" }, if room.paused { " p" } else { "" });
|
||||
let r = Rect::new(ix, yy, 24.0 * CW(), CH() * 2.0);
|
||||
button(gfx, r, &label, r.contains(vec2(mx, my)), FRAME);
|
||||
|
||||
@@ -92,6 +92,16 @@ impl Menu {
|
||||
self.page = Some(page);
|
||||
}
|
||||
|
||||
// mouse wheel / swipe: the previous or next entry (no wrap-around)
|
||||
pub(super) fn bestiary_wheel(&mut self, d: i32) {
|
||||
let Some(Page::Bestiary { tab, sel, .. }) = self.page else { return };
|
||||
let n = entries(tab).len() as i32;
|
||||
let s = sel as i32 + d;
|
||||
if s >= 0 && s < n {
|
||||
self.bestiary_click(Btn::BestSel(d));
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn draw_bestiary(&mut self, gfx: &Gfx, tab: usize, sel: usize, level: u32) {
|
||||
let (sw, sh) = (sw(), sh());
|
||||
let (mx, my) = mouse();
|
||||
|
||||
@@ -370,6 +370,7 @@ pub struct Ui {
|
||||
pub show_help: bool,
|
||||
pub show_overview: bool, // list of all creatures
|
||||
ov_scroll: usize,
|
||||
ov_rect: Option<Rect>, // where the creature overview is (mouse wheel, swipe)
|
||||
pub show_langsel: bool,
|
||||
pub log: Vec<(Msg, f64)>,
|
||||
pub chat_log: Vec<(Option<u8>, String, String, f64)>,
|
||||
@@ -387,6 +388,7 @@ pub struct Ui {
|
||||
end_buttons: Vec<(Rect, u8)>,
|
||||
goals_rect: Rect, // mission goals panel (click: briefing) // end dialog: 0 new game, 1 next/again, 2 campaign
|
||||
lang_rects: Vec<(Rect, String)>,
|
||||
langsel_scroll: usize, // first line of the first-start language grid
|
||||
pub hand_anim: Option<(u8, f64)>, // 1 = backhand, 2 = punch, start time
|
||||
// touch interface
|
||||
open_cat: Option<usize>,
|
||||
@@ -430,6 +432,7 @@ impl Ui {
|
||||
show_help: true,
|
||||
show_overview: false,
|
||||
ov_scroll: 0,
|
||||
ov_rect: None,
|
||||
show_langsel: false,
|
||||
log: Vec::new(),
|
||||
chat_log: Vec::new(),
|
||||
@@ -447,6 +450,7 @@ impl Ui {
|
||||
end_buttons: Vec::new(),
|
||||
goals_rect: Rect::new(0.0, 0.0, 0.0, 0.0),
|
||||
lang_rects: Vec::new(),
|
||||
langsel_scroll: 0,
|
||||
hand_anim: None,
|
||||
open_cat: None,
|
||||
tip: None,
|
||||
@@ -1026,6 +1030,7 @@ impl Ui {
|
||||
let y = (bottom - rows as f32 * CH()).max(top);
|
||||
gfx.window_titled(x, y, cols, rows, S(), false, FRAME_HI, BG, &tr("ov.title"), FRAME_HI);
|
||||
self.rects.push(Rect::new(x, y, cols as f32 * CW(), rows as f32 * CH()));
|
||||
self.ov_rect = Some(Rect::new(x, y, cols as f32 * CW(), rows as f32 * CH()));
|
||||
let mine: Vec<&Ent> = g.ents.iter().filter(|e| e.alive && e.creature()).collect();
|
||||
let wage: f32 = mine.iter().map(|e| st(e.kind).wage as f32 * (1.0 + 0.1 * (e.level - 1) as f32)).sum();
|
||||
let mood = if mine.is_empty() { 0.0 } else { mine.iter().map(|e| e.happy.clamp(0.0, 1.0)).sum::<f32>() / mine.len() as f32 };
|
||||
@@ -1152,39 +1157,71 @@ impl Ui {
|
||||
}
|
||||
|
||||
// First start: choose the language (names shown in their own language)
|
||||
// first start: every language as a button with its flag, in a grid
|
||||
fn draw_langsel(&mut self, gfx: &Gfx) {
|
||||
use crate::menu::flags::{draw_flag, FLAG_H, FLAG_W};
|
||||
let sw = sw();
|
||||
let sh = sh();
|
||||
draw_rectangle(0.0, 0.0, sw, sh, Color::new(0.0, 0.0, 0.0, 0.6));
|
||||
let langs = languages();
|
||||
let cols = 50;
|
||||
let rows = 6 + langs.len() as i32 * 3;
|
||||
let bw_c = 22; // button width in characters
|
||||
let grid = (((sw / CW()) as i32 - 6) / (bw_c + 2)).clamp(1, 3);
|
||||
let lines = (langs.len() as i32 + grid - 1) / grid;
|
||||
let cols = (grid * (bw_c + 2) + 4).min((sw / CW()) as i32);
|
||||
let rows = (lines * 3 + 6).min((sh / CH()) as i32);
|
||||
let x = (grid_center(sw, cols as f32 * CW())).max(0.0);
|
||||
let y = (grid_center_y(sh, rows as f32 * CH())).max(0.0);
|
||||
// the title shows all language names, since no language is chosen yet
|
||||
let title = langs.iter().map(|(c, _)| crate::i18n::tr_in(c, "langsel.title")).collect::<Vec<_>>().join(" / ");
|
||||
// no language is chosen yet: the title in English, the hint in English and German
|
||||
let title = crate::i18n::tr_in("en", "langsel.title");
|
||||
gfx.window_titled(x, y, cols, rows, S(), true, FRAME_HI, Color::new(0.05, 0.03, 0.04, 0.97), &title, FRAME_HI);
|
||||
self.lang_rects.clear();
|
||||
let (mx, my) = mouse();
|
||||
// more lines than fit: the grid scrolls (mouse wheel, swipe)
|
||||
let fit = ((rows - 6) / 3).max(1);
|
||||
self.langsel_scroll = self.langsel_scroll.min((lines - fit).max(0) as usize);
|
||||
let bw = bw_c as f32 * CW();
|
||||
let gx = x + (cols as f32 * CW() - grid as f32 * (bw + 2.0 * CW()) + 2.0 * CW()) * 0.5;
|
||||
for (k, (code, name)) in langs.iter().enumerate() {
|
||||
let bw = 24.0 * CW();
|
||||
let bx = grid_center(sw, bw);
|
||||
let by = y + CH() * (2.0 + k as f32 * 3.0);
|
||||
let line = k as i32 / grid - self.langsel_scroll as i32;
|
||||
if line < 0 || line >= fit {
|
||||
continue;
|
||||
}
|
||||
let bx = gx + (k as i32 % grid) as f32 * (bw + 2.0 * CW());
|
||||
let by = y + CH() * (2.0 + line as f32 * 3.0);
|
||||
let r = Rect::new(bx, by, bw, CH() * 2.0);
|
||||
let hov = r.contains(vec2(mx, my));
|
||||
draw_rectangle(r.x, r.y, r.w, r.h, if hov { Color::new(0.35, 0.22, 0.1, 1.0) } else { Color::new(0.15, 0.1, 0.08, 1.0) });
|
||||
draw_rectangle_lines(r.x, r.y, r.w, r.h, 2.0, if hov { FRAME_HI } else { FRAME });
|
||||
gfx.text(name, bx + (bw - gfx.measure(name, S())) * 0.5, by + CH() * 0.5, S(), TXT);
|
||||
let px = S();
|
||||
draw_flag(code, (bx + CW() * 0.75).floor(), (by + (r.h - FLAG_H as f32 * px) * 0.5).floor(), px);
|
||||
let tx = bx + CW() * 0.75 + FLAG_W as f32 * px + CW();
|
||||
let name: String = name.chars().take(((bx + bw - tx) / CW()).max(1.0) as usize - 1).collect();
|
||||
gfx.text(&name, tx, by + CH() * 0.5, S(), TXT);
|
||||
self.lang_rects.push((r, code.clone()));
|
||||
}
|
||||
let hint_y = y + CH() * (2.0 + langs.len() as f32 * 3.0 + 0.5);
|
||||
for (k, (c, _)) in langs.iter().enumerate() {
|
||||
let hint_y = y + CH() * (2.0 + fit as f32 * 3.0 + 0.5);
|
||||
for (k, c) in ["en", "de"].iter().enumerate() {
|
||||
let h = crate::i18n::tr_in(c, "langsel.hint");
|
||||
let h: String = h.chars().take((cols as usize).saturating_sub(4) * 2).collect();
|
||||
gfx.text(&h, x + CW() * 2.0, hint_y + CH() * k as f32, SMALL(), DIM);
|
||||
}
|
||||
self.rects.push(Rect::new(0.0, 0.0, sw, sh));
|
||||
}
|
||||
|
||||
// mouse wheel / swipe over the creature overview: scrolls it; true if it was there
|
||||
pub fn overview_wheel(&mut self, x: f32, y: f32, d: i32) -> bool {
|
||||
if !self.show_overview || !self.ov_rect.map_or(false, |r| r.contains(vec2(x, y))) {
|
||||
return false;
|
||||
}
|
||||
self.ov_scroll = (self.ov_scroll as i32 + d).max(0) as usize;
|
||||
true
|
||||
}
|
||||
|
||||
// mouse wheel / swipe on the first-start language grid
|
||||
pub fn langsel_wheel(&mut self, d: i32) {
|
||||
self.langsel_scroll = (self.langsel_scroll as i32 + d).max(0) as usize;
|
||||
}
|
||||
|
||||
fn draw_end(&mut self, g: &Game, gfx: &Gfx) {
|
||||
let sw = sw();
|
||||
let sh = sh();
|
||||
|
||||
Reference in new issue
Block a user