Title screen, pause menu, settings dialog and savegames
- title screen (continue/new/load/settings/help) and Esc pause menu - settings: language & sound, camera, graphics, game, rebindable keys; stored in localStorage and in every savegame (always applied on load) - savegames as JSON (.dcsave): download, load via file picker or drag & drop - autosave to localStorage every 2 minutes and when the tab is hidden/closed - configurable wall detail, light count, interface size, game speed, hints - audio behind a cargo feature so native tests run (cargo test --no-default-features) - savegame round-trip test Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fbf6f223fc
commit
fb7dab3683
17 files changed
+1742
-345
No files matched your search
+434
@@ -0,0 +1,434 @@
|
||||
// Menus: title screen, pause menu and the settings dialog (tabs, values, key bindings).
|
||||
// Drawn as CP437 windows on top of the (paused) game world.
|
||||
|
||||
use crate::gfx::*;
|
||||
use crate::i18n::{languages, tr, trf};
|
||||
use crate::settings::{all_actions, key_name, Action, Settings};
|
||||
use crate::ui::{tool_info, CH, CW, DIM, FRAME, FRAME_HI, GOLD, S, TOOLS, TXT};
|
||||
use macroquad::prelude::*;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum Page {
|
||||
Title,
|
||||
Pause,
|
||||
Settings { tab: usize, from_title: bool },
|
||||
KeyCapture { action: usize, from_title: bool },
|
||||
}
|
||||
|
||||
pub const TABS: [&str; 5] = ["set.tab.sound", "set.tab.camera", "set.tab.graphics", "set.tab.game", "set.tab.keys"];
|
||||
|
||||
// One adjustable value in the settings dialog
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum Item {
|
||||
Language,
|
||||
Volume,
|
||||
MusicVolume,
|
||||
Mute,
|
||||
RotateSpeed,
|
||||
ZoomSpeed,
|
||||
EdgeScroll,
|
||||
InvertMouse,
|
||||
Outlines,
|
||||
WallDetail,
|
||||
Lights,
|
||||
UiScale,
|
||||
GameSpeed,
|
||||
Hints,
|
||||
}
|
||||
|
||||
fn tab_items(tab: usize) -> &'static [Item] {
|
||||
use Item::*;
|
||||
match tab {
|
||||
0 => &[Language, Volume, MusicVolume, Mute],
|
||||
1 => &[RotateSpeed, ZoomSpeed, EdgeScroll, InvertMouse],
|
||||
2 => &[Outlines, WallDetail, Lights, UiScale],
|
||||
3 => &[GameSpeed, Hints],
|
||||
_ => &[],
|
||||
}
|
||||
}
|
||||
|
||||
fn item_label(it: Item) -> String {
|
||||
tr(match it {
|
||||
Item::Language => "set.language",
|
||||
Item::Volume => "set.volume",
|
||||
Item::MusicVolume => "set.music",
|
||||
Item::Mute => "set.mute",
|
||||
Item::RotateSpeed => "set.rotate_speed",
|
||||
Item::ZoomSpeed => "set.zoom_speed",
|
||||
Item::EdgeScroll => "set.edge_scroll",
|
||||
Item::InvertMouse => "set.invert",
|
||||
Item::Outlines => "set.outlines",
|
||||
Item::WallDetail => "set.wall_detail",
|
||||
Item::Lights => "set.lights",
|
||||
Item::UiScale => "set.ui_scale",
|
||||
Item::GameSpeed => "set.game_speed",
|
||||
Item::Hints => "set.hints",
|
||||
})
|
||||
}
|
||||
|
||||
fn onoff(b: bool) -> String {
|
||||
tr(if b { "val.on" } else { "val.off" })
|
||||
}
|
||||
|
||||
fn pct(v: f32) -> String {
|
||||
format!("{}%", (v * 100.0).round() as i32)
|
||||
}
|
||||
|
||||
fn item_value(it: Item, s: &Settings) -> String {
|
||||
match it {
|
||||
Item::Language => languages().into_iter().find(|(c, _)| *c == s.language).map(|(_, n)| n).unwrap_or_else(|| s.language.clone()),
|
||||
Item::Volume => pct(s.volume),
|
||||
Item::MusicVolume => pct(s.music_volume),
|
||||
Item::Mute => onoff(s.muted),
|
||||
Item::RotateSpeed => pct(s.rotate_speed),
|
||||
Item::ZoomSpeed => pct(s.zoom_speed),
|
||||
Item::EdgeScroll => onoff(s.edge_scroll),
|
||||
Item::InvertMouse => onoff(s.invert_mouse),
|
||||
Item::Outlines => onoff(s.outlines),
|
||||
Item::WallDetail => tr(match s.wall_detail {
|
||||
1 => "val.low",
|
||||
3 => "val.high",
|
||||
_ => "val.medium",
|
||||
}),
|
||||
Item::Lights => s.max_lights.to_string(),
|
||||
Item::UiScale => tr(if s.ui_scale >= 3 { "val.large" } else { "val.normal" }),
|
||||
Item::GameSpeed => format!("{}x", s.game_speed as i32),
|
||||
Item::Hints => onoff(s.hints),
|
||||
}
|
||||
}
|
||||
|
||||
fn cycle<T: PartialEq + Copy>(opts: &[T], cur: T, d: i32) -> T {
|
||||
let i = opts.iter().position(|&o| o == cur).unwrap_or(0) as i32;
|
||||
opts[(i + d).rem_euclid(opts.len() as i32) as usize]
|
||||
}
|
||||
|
||||
fn step(v: f32, d: i32, lo: f32, hi: f32, st: f32) -> f32 {
|
||||
((v + d as f32 * st) * 100.0).round().clamp(lo * 100.0, hi * 100.0) / 100.0
|
||||
}
|
||||
|
||||
// Change a value; d = -1 / +1 (toggles flip either way)
|
||||
fn item_change(it: Item, s: &mut Settings, d: i32) {
|
||||
match it {
|
||||
Item::Language => {
|
||||
let codes: Vec<String> = languages().into_iter().map(|(c, _)| c).collect();
|
||||
let i = codes.iter().position(|c| *c == s.language).unwrap_or(0) as i32;
|
||||
s.language = codes[(i + d).rem_euclid(codes.len() as i32) as usize].clone();
|
||||
}
|
||||
Item::Volume => s.volume = step(s.volume, d, 0.0, 1.0, 0.1),
|
||||
Item::MusicVolume => s.music_volume = step(s.music_volume, d, 0.0, 1.0, 0.1),
|
||||
Item::Mute => s.muted = !s.muted,
|
||||
Item::RotateSpeed => s.rotate_speed = step(s.rotate_speed, d, 0.25, 2.5, 0.25),
|
||||
Item::ZoomSpeed => s.zoom_speed = step(s.zoom_speed, d, 0.25, 2.5, 0.25),
|
||||
Item::EdgeScroll => s.edge_scroll = !s.edge_scroll,
|
||||
Item::InvertMouse => s.invert_mouse = !s.invert_mouse,
|
||||
Item::Outlines => s.outlines = !s.outlines,
|
||||
Item::WallDetail => s.wall_detail = cycle(&[1u8, 2, 3], s.wall_detail, d),
|
||||
Item::Lights => s.max_lights = cycle(&[8u8, 16, 24], s.max_lights, d),
|
||||
Item::UiScale => s.ui_scale = cycle(&[2u8, 3], s.ui_scale, d),
|
||||
Item::GameSpeed => s.game_speed = cycle(&[1.0f32, 2.0], s.game_speed, d),
|
||||
Item::Hints => s.hints = !s.hints,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn action_label(a: Action) -> String {
|
||||
match a {
|
||||
Action::Tool(i) => tool_info(TOOLS[i]).name,
|
||||
other => tr(&format!("key.{}", other.id())),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Debug)]
|
||||
enum Btn {
|
||||
Continue,
|
||||
Resume,
|
||||
New,
|
||||
Load,
|
||||
Save,
|
||||
Settings,
|
||||
Help,
|
||||
MainMenu,
|
||||
Back,
|
||||
Tab(usize),
|
||||
Change(usize, i32),
|
||||
Key(usize),
|
||||
ResetKeys,
|
||||
}
|
||||
|
||||
pub enum MenuAction {
|
||||
None,
|
||||
Consumed,
|
||||
Continue, // load the autosave
|
||||
Resume,
|
||||
NewGame,
|
||||
Load,
|
||||
Save,
|
||||
Help,
|
||||
MainMenu,
|
||||
SettingsChanged,
|
||||
}
|
||||
|
||||
pub struct Menu {
|
||||
pub page: Option<Page>,
|
||||
pub has_autosave: bool,
|
||||
buttons: Vec<(Rect, Btn)>,
|
||||
}
|
||||
|
||||
fn button(gfx: &Gfx, r: Rect, label: &str, hot: bool, accent: Color) {
|
||||
draw_rectangle(r.x, r.y, r.w, r.h, if hot { Color::new(0.35, 0.22, 0.1, 1.0) } else { Color::new(0.13, 0.09, 0.08, 1.0) });
|
||||
draw_rectangle_lines(r.x, r.y, r.w, r.h, 2.0, if hot { FRAME_HI } else { accent });
|
||||
let s = S();
|
||||
gfx.text(label, r.x + (r.w - gfx.measure(label, s)) * 0.5, r.y + (r.h - GH * s) * 0.5, s, if hot { FRAME_HI } else { TXT });
|
||||
}
|
||||
|
||||
impl Menu {
|
||||
pub fn new() -> Menu {
|
||||
Menu { page: Some(Page::Title), has_autosave: false, buttons: Vec::new() }
|
||||
}
|
||||
|
||||
pub fn open(&self) -> bool {
|
||||
self.page.is_some()
|
||||
}
|
||||
|
||||
// Esc: go one level back
|
||||
pub fn back(&mut self) {
|
||||
self.page = match self.page {
|
||||
Some(Page::KeyCapture { from_title, .. }) => Some(Page::Settings { tab: 4, from_title }),
|
||||
Some(Page::Settings { from_title: true, .. }) => Some(Page::Title),
|
||||
Some(Page::Settings { from_title: false, .. }) => Some(Page::Pause),
|
||||
Some(Page::Pause) => None,
|
||||
Some(Page::Title) => Some(Page::Title),
|
||||
None => Some(Page::Pause),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn capturing(&self) -> Option<Action> {
|
||||
match self.page {
|
||||
Some(Page::KeyCapture { action, .. }) => all_actions().get(action).copied(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn finish_capture(&mut self) {
|
||||
if let Some(Page::KeyCapture { from_title, .. }) = self.page {
|
||||
self.page = Some(Page::Settings { tab: 4, from_title });
|
||||
}
|
||||
}
|
||||
|
||||
pub fn click(&mut self, x: f32, y: f32, settings: &mut Settings) -> MenuAction {
|
||||
if self.page.is_none() {
|
||||
return MenuAction::None;
|
||||
}
|
||||
let hit = self.buttons.iter().find(|(r, _)| r.contains(vec2(x, y))).map(|(_, b)| *b);
|
||||
let from_title = matches!(self.page, Some(Page::Title) | Some(Page::Settings { from_title: true, .. }) | Some(Page::KeyCapture { from_title: true, .. }));
|
||||
match hit {
|
||||
None => MenuAction::Consumed,
|
||||
Some(Btn::Continue) => MenuAction::Continue,
|
||||
Some(Btn::Resume) => {
|
||||
self.page = None;
|
||||
MenuAction::Resume
|
||||
}
|
||||
Some(Btn::New) => MenuAction::NewGame,
|
||||
Some(Btn::Load) => MenuAction::Load,
|
||||
Some(Btn::Save) => MenuAction::Save,
|
||||
Some(Btn::Help) => MenuAction::Help,
|
||||
Some(Btn::MainMenu) => MenuAction::MainMenu,
|
||||
Some(Btn::Settings) => {
|
||||
self.page = Some(Page::Settings { tab: 0, from_title });
|
||||
MenuAction::Consumed
|
||||
}
|
||||
Some(Btn::Back) => {
|
||||
self.back();
|
||||
MenuAction::Consumed
|
||||
}
|
||||
Some(Btn::Tab(t)) => {
|
||||
self.page = Some(Page::Settings { tab: t, from_title });
|
||||
MenuAction::Consumed
|
||||
}
|
||||
Some(Btn::Change(i, d)) => {
|
||||
if let Some(Page::Settings { tab, .. }) = self.page {
|
||||
if let Some(&it) = tab_items(tab).get(i) {
|
||||
item_change(it, settings, d);
|
||||
return MenuAction::SettingsChanged;
|
||||
}
|
||||
}
|
||||
MenuAction::Consumed
|
||||
}
|
||||
Some(Btn::Key(a)) => {
|
||||
self.page = Some(Page::KeyCapture { action: a, from_title });
|
||||
MenuAction::Consumed
|
||||
}
|
||||
Some(Btn::ResetKeys) => {
|
||||
settings.reset_keys();
|
||||
MenuAction::SettingsChanged
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw(&mut self, gfx: &Gfx, settings: &Settings, status: Option<&str>) {
|
||||
self.buttons.clear();
|
||||
let Some(page) = self.page else { return };
|
||||
let sw = screen_width();
|
||||
let sh = screen_height();
|
||||
draw_rectangle(0.0, 0.0, sw, sh, Color::new(0.0, 0.0, 0.0, if page == Page::Title { 0.55 } else { 0.45 }));
|
||||
match page {
|
||||
Page::Title => self.draw_title(gfx),
|
||||
Page::Pause => self.draw_pause(gfx),
|
||||
Page::Settings { tab, .. } => self.draw_settings(gfx, settings, tab),
|
||||
Page::KeyCapture { action, .. } => {
|
||||
self.draw_settings(gfx, settings, 4);
|
||||
self.buttons.clear();
|
||||
let a = all_actions()[action];
|
||||
let text = trf("set.press_key", &[action_label(a)]);
|
||||
let cols = (text.chars().count() as i32 + 8).max(36);
|
||||
let x = (sw - cols as f32 * CW()) * 0.5;
|
||||
let y = sh * 0.4;
|
||||
gfx.window(x, y, cols, 5, S(), true, FRAME_HI, Color::new(0.05, 0.03, 0.04, 0.98));
|
||||
gfx.text(&text, x + CW() * 4.0, y + CH() * 1.5, S(), TXT);
|
||||
let esc = tr("set.esc_cancel");
|
||||
gfx.text(&esc, x + CW() * 4.0, y + CH() * 3.0, S(), DIM);
|
||||
}
|
||||
}
|
||||
if let Some(st) = status {
|
||||
let w = gfx.measure(st, S()) + CW() * 4.0;
|
||||
let x = (sw - w) * 0.5;
|
||||
let y = sh - CH() * 9.0;
|
||||
draw_rectangle(x, y, w, CH() * 2.0, Color::new(0.05, 0.03, 0.04, 0.95));
|
||||
draw_rectangle_lines(x, y, w, CH() * 2.0, 2.0, GOLD);
|
||||
gfx.text(st, x + CW() * 2.0, y + CH() * 0.5, S(), GOLD);
|
||||
}
|
||||
}
|
||||
|
||||
fn column(&mut self, gfx: &Gfx, x: f32, y: f32, w: f32, items: &[(Btn, String)]) {
|
||||
let (mx, my) = mouse_position();
|
||||
for (k, (b, label)) in items.iter().enumerate() {
|
||||
let r = Rect::new(x, y + k as f32 * CH() * 3.0, w, CH() * 2.2);
|
||||
button(gfx, r, label, r.contains(vec2(mx, my)), FRAME);
|
||||
self.buttons.push((r, *b));
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_title(&mut self, gfx: &Gfx) {
|
||||
let sw = screen_width();
|
||||
let sh = screen_height();
|
||||
let big = (S() * 4.0).min((sw / (GW * 14.0)).floor()).max(S());
|
||||
let title = "DungeonCraft";
|
||||
let ty = sh * 0.12;
|
||||
gfx.text(title, (sw - gfx.measure(title, big)) * 0.5 + big, ty + big, big, Color::new(0.0, 0.0, 0.0, 0.8));
|
||||
gfx.text(title, (sw - gfx.measure(title, big)) * 0.5, ty, big, FRAME_HI);
|
||||
let sub = tr("menu.subtitle");
|
||||
gfx.text(&sub, (sw - gfx.measure(&sub, S())) * 0.5, ty + GH * big + CH(), S(), TXT);
|
||||
let mut items = Vec::new();
|
||||
if self.has_autosave {
|
||||
items.push((Btn::Continue, tr("menu.continue")));
|
||||
}
|
||||
items.push((Btn::New, tr("menu.new")));
|
||||
items.push((Btn::Load, tr("menu.load")));
|
||||
items.push((Btn::Settings, tr("menu.settings")));
|
||||
items.push((Btn::Help, tr("menu.help")));
|
||||
let w = 26.0 * CW();
|
||||
let y = ty + GH * big + CH() * 4.0;
|
||||
self.column(gfx, (sw - w) * 0.5, y, w, &items);
|
||||
let hint = tr("menu.drop_hint");
|
||||
gfx.text(&hint, (sw - gfx.measure(&hint, (S() / 2.0).max(1.0))) * 0.5, sh - CH() * 7.5, (S() / 2.0).max(1.0), DIM);
|
||||
let credit = tr("help.font");
|
||||
gfx.text(&credit, (sw - gfx.measure(&credit, (S() / 2.0).max(1.0))) * 0.5, sh - CH() * 6.5, (S() / 2.0).max(1.0), DIM);
|
||||
}
|
||||
|
||||
fn draw_pause(&mut self, gfx: &Gfx) {
|
||||
let sw = screen_width();
|
||||
let sh = screen_height();
|
||||
let items = vec![
|
||||
(Btn::Resume, tr("menu.resume")),
|
||||
(Btn::Save, tr("menu.save")),
|
||||
(Btn::Load, tr("menu.load")),
|
||||
(Btn::Settings, tr("menu.settings")),
|
||||
(Btn::Help, tr("menu.help")),
|
||||
(Btn::MainMenu, tr("menu.main")),
|
||||
];
|
||||
let cols = 32;
|
||||
let rows = items.len() as i32 * 3 + 4;
|
||||
let x = (sw - cols as f32 * CW()) * 0.5;
|
||||
let y = ((sh - rows as f32 * CH()) * 0.5).max(0.0);
|
||||
gfx.window_titled(x, y, cols, rows, S(), true, FRAME_HI, Color::new(0.05, 0.03, 0.04, 0.97), &tr("menu.paused"), FRAME_HI);
|
||||
let w = (cols - 6) as f32 * CW();
|
||||
self.column(gfx, x + CW() * 3.0, y + CH() * 2.0, w, &items);
|
||||
}
|
||||
|
||||
fn draw_settings(&mut self, gfx: &Gfx, s: &Settings, tab: usize) {
|
||||
let sw = screen_width();
|
||||
let sh = screen_height();
|
||||
let (mx, my) = mouse_position();
|
||||
let cols = ((sw / CW()) as i32 - 2).min(66);
|
||||
let rows = ((sh / CH()) as i32 - 2).min(28);
|
||||
let x = ((sw - cols as f32 * CW()) * 0.5).max(0.0);
|
||||
let y = ((sh - rows as f32 * CH()) * 0.5).max(0.0);
|
||||
gfx.window_titled(x, y, cols, rows, S(), true, FRAME_HI, Color::new(0.05, 0.03, 0.04, 0.97), &tr("menu.settings"), FRAME_HI);
|
||||
// tabs
|
||||
let mut tx = x + CW() * 2.0;
|
||||
let ty = y + CH() * 1.5;
|
||||
for (i, key) in TABS.iter().enumerate() {
|
||||
let label = tr(key);
|
||||
let w = gfx.measure(&label, S()) + CW() * 2.0;
|
||||
let r = Rect::new(tx, ty, w, CH() * 2.0);
|
||||
let hot = r.contains(vec2(mx, my));
|
||||
draw_rectangle(r.x, r.y, r.w, r.h, if i == tab { Color::new(0.35, 0.22, 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, if i == tab || hot { FRAME_HI } else { FRAME });
|
||||
gfx.text(&label, r.x + CW(), r.y + CH() * 0.5, S(), if i == tab { FRAME_HI } else { TXT });
|
||||
self.buttons.push((r, Btn::Tab(i)));
|
||||
tx += w + CW();
|
||||
}
|
||||
let top = ty + CH() * 3.5;
|
||||
if tab == 4 {
|
||||
// key bindings in two columns
|
||||
let acts = all_actions();
|
||||
let half = (acts.len() + 1) / 2;
|
||||
let colw = (cols as f32 - 6.0) * CW() * 0.5;
|
||||
let rowh = CH() * 1.35;
|
||||
for (k, a) in acts.iter().enumerate() {
|
||||
let c = k / half;
|
||||
let r = k % half;
|
||||
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);
|
||||
let hot = rect.contains(vec2(mx, my));
|
||||
if hot {
|
||||
draw_rectangle(rect.x, rect.y, rect.w, rect.h, Color::new(0.25, 0.16, 0.08, 1.0));
|
||||
}
|
||||
let key = format!("[{}]", key_name(s.key(*a)));
|
||||
let maxc = ((colw - gfx.measure(&key, S()) - CW()) / CW()).max(4.0) as usize;
|
||||
let label: String = action_label(*a).chars().take(maxc).collect();
|
||||
gfx.text(&label, bx + CW() * 0.5, by, S(), TXT);
|
||||
gfx.text(&key, bx + colw - gfx.measure(&key, S()) - CW() * 0.5, by, S(), GOLD);
|
||||
self.buttons.push((rect, Btn::Key(k)));
|
||||
}
|
||||
let hint = tr("set.keys_hint");
|
||||
gfx.text(&hint, x + CW() * 2.0, top + half as f32 * rowh + CH() * 0.5, (S() / 2.0).max(1.0), 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.2);
|
||||
button(gfx, rr, &tr("set.reset_keys"), rr.contains(vec2(mx, my)), FRAME);
|
||||
self.buttons.push((rr, Btn::ResetKeys));
|
||||
} else {
|
||||
for (i, &it) in tab_items(tab).iter().enumerate() {
|
||||
let by = top + i as f32 * CH() * 2.5;
|
||||
gfx.text(&item_label(it), x + CW() * 3.0, by + CH() * 0.4, S(), TXT);
|
||||
// value with < > buttons
|
||||
let vw = 18.0 * CW();
|
||||
let vx = x + cols as f32 * CW() - vw - CW() * 3.0;
|
||||
let left = Rect::new(vx, by, CW() * 2.5, CH() * 1.8);
|
||||
let right = Rect::new(vx + vw - CW() * 2.5, by, CW() * 2.5, CH() * 1.8);
|
||||
button(gfx, left, "<", left.contains(vec2(mx, my)), FRAME);
|
||||
button(gfx, right, ">", right.contains(vec2(mx, my)), FRAME);
|
||||
let val = item_value(it, s);
|
||||
gfx.text(&val, vx + (vw - gfx.measure(&val, S())) * 0.5, by + CH() * 0.4, S(), GOLD);
|
||||
let mid = Rect::new(vx + CW() * 2.5, by, vw - CW() * 5.0, CH() * 1.8);
|
||||
self.buttons.push((left, Btn::Change(i, -1)));
|
||||
self.buttons.push((right, Btn::Change(i, 1)));
|
||||
self.buttons.push((mid, Btn::Change(i, 1)));
|
||||
}
|
||||
}
|
||||
let bw = gfx.measure(&tr("menu.back"), S()) + CW() * 6.0;
|
||||
let br = Rect::new(x + CW() * 2.0, y + (rows - 4) as f32 * CH(), bw, CH() * 2.2);
|
||||
button(gfx, br, &tr("menu.back"), br.contains(vec2(mx, my)), FRAME);
|
||||
self.buttons.push((br, Btn::Back));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user