Interface on the pixel grid: a virtual screen of whole multiples of the world's rows, sharp-scaled to every device; compact title menu on short screens

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-06 07:33:25 +01:00
1 parent aa39e66da9
commit e7fc0a4751
5 files changed
+142 -66

No files matched your search

+84 -5
View File
@@ -10,13 +10,36 @@ use crate::settings::{key_name, Action, Settings};
use macroquad::prelude::*;
// ---- interface scaling
// The interface is drawn in device pixels (see begin_ui). Every size is an integer multiple
// The interface is drawn on a virtual screen with a fixed number of rows (pixel look: twice the
// world's rows, the world's rows on touch screens), which is scaled up to the device at the end of
// the frame (see begin_ui, end_frame) - on every device the same pixel grid. Small screens
// draw in device pixels. Every size is an integer multiple
// of the font's 6x8 pixel glyphs: small text = 1*F, normal text = 2*F, titles = 4*F, where F
// 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));
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);
static UP_TARGET: std::cell::RefCell<Option<(RenderTarget, u32, u32)>> = std::cell::RefCell::new(None);
static FRAME_STARTED: std::cell::Cell<bool> = std::cell::Cell::new(false);
}
// the rows of the virtual screen for this frame (0: device pixels). Set once per frame before
// anything is drawn.
pub fn set_virtual_rows(rows: u32) {
let device_h = screen_height() * screen_dpi_scale();
// a screen smaller than the virtual one draws in device pixels
let rows = if rows > 0 && device_h >= rows as f32 { rows } else { 0 };
VIRTUAL_ROWS.with(|c| c.set(rows));
}
pub fn virtual_rows() -> u32 {
VIRTUAL_ROWS.with(|c| c.get())
}
// device pixels per CSS pixel
pub fn device_dpr() -> f32 {
screen_dpi_scale()
}
pub fn set_ui_factor(f: u8) {
@@ -35,9 +58,12 @@ pub fn set_pointer(x: f32, y: f32) {
POINTER.with(|c| c.set((x, y)));
}
// device pixels per CSS pixel
// interface units per CSS pixel (device pixels without the virtual screen)
pub fn dpr() -> f32 {
screen_dpi_scale()
match virtual_rows() {
0 => screen_dpi_scale(),
rows => rows as f32 / screen_height().max(1.0),
}
}
// screen size and mouse in device pixels
pub fn sw() -> f32 {
@@ -61,6 +87,10 @@ pub fn F() -> u32 {
if set > 0 {
return set as u32;
}
// the virtual screen already has the right size for the interface
if virtual_rows() > 0 {
return 1;
}
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,
@@ -108,10 +138,59 @@ pub fn grid_center_y(total: f32, h: f32) -> f32 {
(((total - h) * 0.5) / CH()).floor() * CH()
}
// 2D camera mapping one unit to one device pixel (top-left origin)
// 2D camera mapping one unit to one interface pixel (top-left origin): on the virtual screen
// (cleared the first time in a frame) or straight on the device
pub fn begin_ui() {
let (w, h) = (sw(), sh());
set_camera(&Camera2D { target: vec2(w * 0.5, h * 0.5), zoom: vec2(2.0 / w, 2.0 / h), ..Default::default() });
if virtual_rows() == 0 {
set_camera(&Camera2D { target: vec2(w * 0.5, h * 0.5), zoom: vec2(2.0 / w, 2.0 / h), ..Default::default() });
return;
}
let (tw, th) = (w.max(1.0) as u32, h.max(1.0) as u32);
let rt = UI_TARGET.with(|t| {
let mut t = t.borrow_mut();
if t.as_ref().map_or(true, |(_, a, b)| *a != tw || *b != th) {
let rt = render_target_ex(tw, th, RenderTargetParams { sample_count: 1, depth: false });
rt.texture.set_filter(FilterMode::Nearest);
*t = Some((rt, tw, th));
}
t.as_ref().unwrap().0.clone()
});
let mut c = Camera2D::from_display_rect(Rect::new(0.0, 0.0, w, h));
c.render_target = Some(rt);
set_camera(&c);
if !FRAME_STARTED.with(|f| f.replace(true)) {
clear_background(Color::new(0.0, 0.0, 0.0, 0.0));
}
}
// end of a frame: the virtual screen goes to the device - up by the next whole factor (nearest),
// then smoothly to the device size, so every pixel has the same size and stays crisp
pub fn end_frame() {
if !FRAME_STARTED.with(|f| f.replace(false)) || virtual_rows() == 0 {
set_default_camera();
return;
}
let Some((rt, tw, th)) = UI_TARGET.with(|t| t.borrow().clone()) else { return };
let scale = screen_height() * screen_dpi_scale() / th as f32;
let n = scale.ceil().max(1.0) as u32;
let (uw, uh) = (tw * n, th * n);
let up = UP_TARGET.with(|t| {
let mut t = t.borrow_mut();
if t.as_ref().map_or(true, |(_, a, b)| *a != uw || *b != uh) {
let up = render_target_ex(uw, uh, RenderTargetParams { sample_count: 1, depth: false });
up.texture.set_filter(FilterMode::Linear);
*t = Some((up, uw, uh));
}
t.as_ref().unwrap().0.clone()
});
let mut c = Camera2D::from_display_rect(Rect::new(0.0, 0.0, uw as f32, uh as f32));
c.render_target = Some(up.clone());
set_camera(&c);
clear_background(Color::new(0.0, 0.0, 0.0, 1.0));
draw_texture_ex(&rt.texture, 0.0, 0.0, WHITE, DrawTextureParams { dest_size: Some(vec2(uw as f32, uh as f32)), flip_y: true, ..Default::default() });
set_default_camera();
draw_texture_ex(&up.texture, 0.0, 0.0, WHITE, DrawTextureParams { dest_size: Some(vec2(screen_width(), screen_height())), flip_y: true, ..Default::default() });
}
// bar made of block characters: full blocks, an eighth block for the rest, shaded background