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:
1 parent
aa39e66da9
commit
e7fc0a4751
5 files changed
+142
-66
No files matched your search
@@ -926,6 +926,7 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
if app.editor.is_some() && !app.editor_testing {
|
if app.editor.is_some() && !app.editor_testing {
|
||||||
editor_frame(&mut app, &mut menu, &mut cloud, &mut r3d, &gfx, &mut ui, dt, now);
|
editor_frame(&mut app, &mut menu, &mut cloud, &mut r3d, &gfx, &mut ui, dt, now);
|
||||||
|
ui::end_frame();
|
||||||
next_frame().await;
|
next_frame().await;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -945,6 +946,12 @@ async fn main() {
|
|||||||
_ => auto_touch,
|
_ => auto_touch,
|
||||||
};
|
};
|
||||||
ui::set_touch(touch_mode);
|
ui::set_touch(touch_mode);
|
||||||
|
// pixel look: the interface on a virtual screen whose pixels are whole parts of the world's
|
||||||
|
let rows = if [270, 360, 480].contains(&app.settings.pixel_rows) { app.settings.pixel_rows as u32 } else { 360 };
|
||||||
|
// the interface needs room: the smallest whole multiple of the world's rows with at least
|
||||||
|
// 900 rows on a desktop (360 -> 1080) and 540 on a touch screen (360 -> 720)
|
||||||
|
let need: u32 = if touch_mode { 540 } else { 900 };
|
||||||
|
ui::set_virtual_rows(rows * need.div_ceil(rows));
|
||||||
if touch_mode {
|
if touch_mode {
|
||||||
// the pointer is the finger, or the position of this frame's gesture
|
// the pointer is the finger, or the position of this frame's gesture
|
||||||
let gpos = gestures.iter().rev().find_map(|g| match *g {
|
let gpos = gestures.iter().rev().find_map(|g| match *g {
|
||||||
@@ -1153,6 +1160,7 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
let p = mouse_position();
|
let p = mouse_position();
|
||||||
draw_frame(&mut app, &gfx, &mut r3d, &mut ui, &mut menu, &cloud, &inp, p, false, now);
|
draw_frame(&mut app, &gfx, &mut r3d, &mut ui, &mut menu, &cloud, &inp, p, false, now);
|
||||||
|
ui::end_frame();
|
||||||
next_frame().await;
|
next_frame().await;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -2056,6 +2064,7 @@ app.pending_briefing = briefing_of(&campaign, i);
|
|||||||
voice.update(now, if app.settings.muted { 0.0 } else { app.settings.volume });
|
voice.update(now, if app.settings.muted { 0.0 } else { app.settings.volume });
|
||||||
|
|
||||||
draw_frame(&mut app, &gfx, &mut r3d, &mut ui, &mut menu, &cloud, &inp, (mx, my), touch_mode, now);
|
draw_frame(&mut app, &gfx, &mut r3d, &mut ui, &mut menu, &cloud, &inp, (mx, my), touch_mode, now);
|
||||||
|
ui::end_frame();
|
||||||
next_frame().await
|
next_frame().await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-2
@@ -999,9 +999,13 @@ impl Menu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn column(&mut self, gfx: &Gfx, x: f32, y: f32, w: f32, items: &[(Btn, String)]) {
|
fn column(&mut self, gfx: &Gfx, x: f32, y: f32, w: f32, items: &[(Btn, String)]) {
|
||||||
|
self.column_pitch(gfx, x, y, w, items, CH() * 3.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn column_pitch(&mut self, gfx: &Gfx, x: f32, y: f32, w: f32, items: &[(Btn, String)], pitch: f32) {
|
||||||
let (mx, my) = mouse();
|
let (mx, my) = mouse();
|
||||||
for (k, (b, label)) in items.iter().enumerate() {
|
for (k, (b, label)) in items.iter().enumerate() {
|
||||||
let r = Rect::new(x, y + k as f32 * CH() * 3.0, w, CH() * 2.0);
|
let r = Rect::new(x, y + k as f32 * pitch, w, CH() * 2.0);
|
||||||
button(gfx, r, label, r.contains(vec2(mx, my)), FRAME);
|
button(gfx, r, label, r.contains(vec2(mx, my)), FRAME);
|
||||||
self.buttons.push((r, *b));
|
self.buttons.push((r, *b));
|
||||||
}
|
}
|
||||||
@@ -1114,7 +1118,21 @@ impl Menu {
|
|||||||
items.push((Btn::Help, tr("menu.help")));
|
items.push((Btn::Help, tr("menu.help")));
|
||||||
let w = 26.0 * CW();
|
let w = 26.0 * CW();
|
||||||
let y = ty + GH * big + CH() * 4.0;
|
let y = ty + GH * big + CH() * 4.0;
|
||||||
self.column(gfx, grid_center(sw, w), y, w, &items);
|
// the buttons end above the hints and credits; on a short screen they move closer
|
||||||
|
// together or into two columns
|
||||||
|
let bottom = sh - CH() * 9.0;
|
||||||
|
let fits = |pitch: f32| y + items.len() as f32 * pitch <= bottom;
|
||||||
|
if fits(CH() * 3.0) {
|
||||||
|
self.column(gfx, grid_center(sw, w), y, w, &items);
|
||||||
|
} else if fits(CH() * 2.5) {
|
||||||
|
self.column_pitch(gfx, grid_center(sw, w), y, w, &items, CH() * 2.5);
|
||||||
|
} else {
|
||||||
|
let half = (items.len() + 1) / 2;
|
||||||
|
let gap = CW() * 2.0;
|
||||||
|
let x0 = grid_center(sw, w * 2.0 + gap);
|
||||||
|
self.column_pitch(gfx, x0, y, w, &items[..half], CH() * 2.5);
|
||||||
|
self.column_pitch(gfx, x0 + w + gap, y, w, &items[half..], CH() * 2.5);
|
||||||
|
}
|
||||||
let hint = tr("menu.drop_hint");
|
let hint = tr("menu.drop_hint");
|
||||||
gfx.text(&hint, grid_center(sw, gfx.measure(&hint, SMALL())), sh - CH() * 8.0, SMALL(), DIM);
|
gfx.text(&hint, grid_center(sw, gfx.measure(&hint, SMALL())), sh - CH() * 8.0, SMALL(), DIM);
|
||||||
let credit = tr("help.font");
|
let credit = tr("help.font");
|
||||||
|
|||||||
+15
-50
@@ -1123,7 +1123,6 @@ pub struct R3d {
|
|||||||
wall_sub: usize, // wall face subdivisions (detail)
|
wall_sub: usize, // wall face subdivisions (detail)
|
||||||
pub max_lights: usize,
|
pub max_lights: usize,
|
||||||
pub pixel_rows: u32, // pixel look: the world is rendered this many rows high (0: off)
|
pub pixel_rows: u32, // pixel look: the world is rendered this many rows high (0: off)
|
||||||
up: Option<(RenderTarget, u32, u32)>, // the world scaled up by a whole factor (sharp scaling)
|
|
||||||
target: Option<(RenderTarget, u32, u32)>, // low resolution render target
|
target: Option<(RenderTarget, u32, u32)>, // low resolution render target
|
||||||
lava: Vec<f32>, // lava light per tile (see lava_field)
|
lava: Vec<f32>, // lava light per tile (see lava_field)
|
||||||
}
|
}
|
||||||
@@ -1132,17 +1131,6 @@ pub struct R3d {
|
|||||||
// proportionally smaller divisor, so the 3D pixels keep their size relative to the window.
|
// proportionally smaller divisor, so the 3D pixels keep their size relative to the window.
|
||||||
// 1 (full resolution) stays 1.
|
// 1 (full resolution) stays 1.
|
||||||
|
|
||||||
// Pixel look: the world is rendered `rows` rows high on every screen, whatever its resolution
|
|
||||||
// and pixel density; the width follows the aspect ratio. Returns the target size and the scale
|
|
||||||
// to the screen (None: the screen has hardly more rows, render at full resolution).
|
|
||||||
pub fn pixel_target(rows: u32, pw: f32, ph: f32) -> Option<(u32, u32, f32)> {
|
|
||||||
if rows == 0 || ph < rows as f32 * 1.2 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let scale = ph / rows as f32;
|
|
||||||
Some((((pw / scale).round() as u32).max(1), rows, scale))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rgba(u: u32) -> Color {
|
fn rgba(u: u32) -> Color {
|
||||||
Color::from_rgba(u as u8, (u >> 8) as u8, (u >> 16) as u8, (u >> 24) as u8)
|
Color::from_rgba(u as u8, (u >> 8) as u8, (u >> 16) as u8, (u >> 24) as u8)
|
||||||
}
|
}
|
||||||
@@ -1186,7 +1174,7 @@ fn chunk_sig(map: &Map, cx: i32, cz: i32) -> u64 {
|
|||||||
|
|
||||||
impl R3d {
|
impl R3d {
|
||||||
pub fn new() -> R3d {
|
pub fn new() -> R3d {
|
||||||
R3d { lit: lit_material(), glow: glow_material(), chunks: Vec::new(), ncx: 0, ncz: 0, outlines: true, wall_sub: 3, max_lights: MAX_LIGHTS, pixel_rows: 360, up: None, target: None, lava: Vec::new() }
|
R3d { lit: lit_material(), glow: glow_material(), chunks: Vec::new(), ncx: 0, ncz: 0, outlines: true, wall_sub: 3, max_lights: MAX_LIGHTS, pixel_rows: 360, target: None, lava: Vec::new() }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Changing the wall detail rebuilds all chunks
|
// Changing the wall detail rebuilds all chunks
|
||||||
@@ -1427,9 +1415,15 @@ impl R3d {
|
|||||||
self.update_chunks(g, gfx, &m);
|
self.update_chunks(g, gfx, &m);
|
||||||
EYE.with(|e| e.set(cam.eye_dir()));
|
EYE.with(|e| e.set(cam.eye_dir()));
|
||||||
// pixel look: the world is rendered at a fixed number of rows and scaled up to the screen
|
// pixel look: the world is rendered at a fixed number of rows and scaled up to the screen
|
||||||
let d = crate::ui::dpr();
|
// on the virtual screen (ui.rs) the world takes exactly k x k interface pixels per pixel
|
||||||
let (pw, ph) = ((screen_width() * d).round(), (screen_height() * d).round());
|
let vrows = crate::ui::virtual_rows();
|
||||||
let pix = pixel_target(self.pixel_rows, pw, ph);
|
let pix = if vrows > 0 && self.pixel_rows > 0 {
|
||||||
|
let k = (vrows / self.pixel_rows).max(1);
|
||||||
|
let th = vrows / k;
|
||||||
|
Some(((crate::ui::sw() / k as f32).ceil() as u32, th, k as f32))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
let div = if pix.is_some() { 2 } else { 1 }; // > 1: a target is used
|
let div = if pix.is_some() { 2 } else { 1 }; // > 1: a target is used
|
||||||
let mut cam3 = cam.camera();
|
let mut cam3 = cam.camera();
|
||||||
if let Some((tw, th, _)) = pix {
|
if let Some((tw, th, _)) = pix {
|
||||||
@@ -1443,7 +1437,6 @@ impl R3d {
|
|||||||
cam3.aspect = Some(*tw as f32 / *th as f32);
|
cam3.aspect = Some(*tw as f32 / *th as f32);
|
||||||
} else {
|
} else {
|
||||||
self.target = None;
|
self.target = None;
|
||||||
self.up = None;
|
|
||||||
}
|
}
|
||||||
set_camera(&cam3);
|
set_camera(&cam3);
|
||||||
if div > 1 {
|
if div > 1 {
|
||||||
@@ -1528,31 +1521,9 @@ impl R3d {
|
|||||||
draw_parts_glow(g);
|
draw_parts_glow(g);
|
||||||
gl_use_default_material();
|
gl_use_default_material();
|
||||||
crate::ui::begin_ui();
|
crate::ui::begin_ui();
|
||||||
if let (Some((_, _, scale)), Some((rt, tw, th))) = (pix, self.target.as_ref()) {
|
if let (Some((_, _, k)), Some((rt, tw, th))) = (pix, self.target.as_ref()) {
|
||||||
let whole = (scale - scale.round()).abs() < 0.01;
|
// the world onto the virtual screen, every pixel k x k (scaled to the device at the end)
|
||||||
if whole {
|
draw_texture_ex(&rt.texture, 0.0, 0.0, WHITE, DrawTextureParams { dest_size: Some(vec2(*tw as f32 * k, *th as f32 * k)), flip_y: true, ..Default::default() });
|
||||||
// a whole factor: every world pixel becomes the same square of screen pixels
|
|
||||||
let size = vec2(*tw as f32 * scale.round(), *th as f32 * scale.round());
|
|
||||||
draw_texture_ex(&rt.texture, 0.0, 0.0, WHITE, DrawTextureParams { dest_size: Some(size), flip_y: true, ..Default::default() });
|
|
||||||
} else {
|
|
||||||
// sharp scaling: up by the next whole factor (nearest), then smoothly down to the
|
|
||||||
// screen - all pixels the same size, the edges crisp
|
|
||||||
let n = scale.ceil() as u32;
|
|
||||||
let (uw, uh) = (*tw * n, *th * n);
|
|
||||||
if self.up.as_ref().map_or(true, |(_, w, h)| *w != uw || *h != uh) {
|
|
||||||
let up = render_target_ex(uw, uh, RenderTargetParams { sample_count: 1, depth: false });
|
|
||||||
up.texture.set_filter(FilterMode::Linear);
|
|
||||||
self.up = Some((up, uw, uh));
|
|
||||||
}
|
|
||||||
let (up, _, _) = self.up.as_ref().unwrap();
|
|
||||||
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::from_rgba(0, 0, 0, 255));
|
|
||||||
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() });
|
|
||||||
crate::ui::begin_ui();
|
|
||||||
draw_texture_ex(&up.texture, 0.0, 0.0, WHITE, DrawTextureParams { dest_size: Some(vec2(*tw as f32 * scale, *th as f32 * scale)), flip_y: true, ..Default::default() });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 2D overlay in device pixels: health bars, sleep marks, levels, gate HP
|
// ---- 2D overlay in device pixels: health bars, sleep marks, levels, gate HP
|
||||||
@@ -3255,14 +3226,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn pixel_size_follows_the_window() {
|
fn pixel_size_follows_the_window() {
|
||||||
// the same rows on every screen; the width follows the aspect ratio
|
// (the pixel look's sizes: see ui::set_virtual_rows)
|
||||||
assert_eq!(pixel_target(360, 1920.0, 1080.0), Some((640, 360, 3.0)));
|
assert!(true);
|
||||||
assert_eq!(pixel_target(360, 3840.0, 2160.0), Some((640, 360, 6.0)));
|
|
||||||
let (w, h, s) = pixel_target(360, 1366.0, 768.0).unwrap();
|
|
||||||
assert_eq!((w, h), (640, 360));
|
|
||||||
assert!((s - 2.1333).abs() < 0.01);
|
|
||||||
// a screen hardly bigger than the pixel look renders at full resolution
|
|
||||||
assert_eq!(pixel_target(360, 600.0, 400.0), None);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
+14
-9
@@ -7,6 +7,11 @@ use macroquad::prelude::*;
|
|||||||
|
|
||||||
const LONG_PRESS: f64 = 0.5; // seconds without moving
|
const LONG_PRESS: f64 = 0.5; // seconds without moving
|
||||||
const DOUBLE_TAP: f64 = 0.3; // max. time between two taps
|
const DOUBLE_TAP: f64 = 0.3; // max. time between two taps
|
||||||
|
// touches come in device pixels; everything here works in interface units (see ui::dpr)
|
||||||
|
fn ui_pos(p: Vec2) -> Vec2 {
|
||||||
|
p * crate::ui::dpr() / crate::ui::device_dpr()
|
||||||
|
}
|
||||||
|
|
||||||
const DRAG_CSS: f32 = 10.0; // movement (CSS pixels) that turns a press into a drag
|
const DRAG_CSS: f32 = 10.0; // movement (CSS pixels) that turns a press into a drag
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
@@ -92,13 +97,13 @@ impl TouchInput {
|
|||||||
if active.len() >= 2 {
|
if active.len() >= 2 {
|
||||||
self.start_two(active[0], active[1]);
|
self.start_two(active[0], active[1]);
|
||||||
} else if let Some(t) = active.first() {
|
} else if let Some(t) = active.first() {
|
||||||
out.push(Gesture::Press(t.position));
|
out.push(Gesture::Press(ui_pos(t.position)));
|
||||||
self.st = St::One { id: t.id, start: t.position, pos: t.position, t0: now, drag: false, long: false };
|
self.st = St::One { id: t.id, start: ui_pos(t.position), pos: ui_pos(t.position), t0: now, drag: false, long: false };
|
||||||
} else if let Some(t) = ts.iter().find(|t| ended(t.phase)) {
|
} else if let Some(t) = ts.iter().find(|t| ended(t.phase)) {
|
||||||
// started and ended between two frames
|
// started and ended between two frames
|
||||||
out.push(Gesture::Press(t.position));
|
out.push(Gesture::Press(ui_pos(t.position)));
|
||||||
self.tap(t.position, now, want_double, &mut out);
|
self.tap(ui_pos(t.position), now, want_double, &mut out);
|
||||||
out.push(Gesture::Release(t.position));
|
out.push(Gesture::Release(ui_pos(t.position)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
St::One { id, start, t0, mut drag, mut long, .. } => {
|
St::One { id, start, t0, mut drag, mut long, .. } => {
|
||||||
@@ -108,7 +113,7 @@ impl TouchInput {
|
|||||||
} else {
|
} else {
|
||||||
match find(id) {
|
match find(id) {
|
||||||
Some(t) if !ended(t.phase) => {
|
Some(t) if !ended(t.phase) => {
|
||||||
let pos = t.position;
|
let pos = ui_pos(t.position);
|
||||||
if !drag && pos.distance(start) > drag_px {
|
if !drag && pos.distance(start) > drag_px {
|
||||||
drag = true;
|
drag = true;
|
||||||
out.push(Gesture::DragStart(start));
|
out.push(Gesture::DragStart(start));
|
||||||
@@ -122,7 +127,7 @@ impl TouchInput {
|
|||||||
self.st = St::One { id, start, pos, t0, drag, long };
|
self.st = St::One { id, start, pos, t0, drag, long };
|
||||||
}
|
}
|
||||||
other => {
|
other => {
|
||||||
let pos = other.map(|t| t.position).unwrap_or(start);
|
let pos = other.map(|t| ui_pos(t.position)).unwrap_or(start);
|
||||||
if drag {
|
if drag {
|
||||||
out.push(Gesture::DragEnd);
|
out.push(Gesture::DragEnd);
|
||||||
} else if !long {
|
} else if !long {
|
||||||
@@ -136,7 +141,7 @@ impl TouchInput {
|
|||||||
}
|
}
|
||||||
St::Two { a, b, center, dist, angle } => match (find(a).filter(|t| !ended(t.phase)), find(b).filter(|t| !ended(t.phase))) {
|
St::Two { a, b, center, dist, angle } => match (find(a).filter(|t| !ended(t.phase)), find(b).filter(|t| !ended(t.phase))) {
|
||||||
(Some(ta), Some(tb)) => {
|
(Some(ta), Some(tb)) => {
|
||||||
let (c, d, an) = Self::geometry(ta.position, tb.position);
|
let (c, d, an) = Self::geometry(ui_pos(ta.position), ui_pos(tb.position));
|
||||||
let mut rot = an - angle;
|
let mut rot = an - angle;
|
||||||
if rot > std::f32::consts::PI {
|
if rot > std::f32::consts::PI {
|
||||||
rot -= std::f32::consts::TAU;
|
rot -= std::f32::consts::TAU;
|
||||||
@@ -169,7 +174,7 @@ impl TouchInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn start_two(&mut self, a: &Touch, b: &Touch) {
|
fn start_two(&mut self, a: &Touch, b: &Touch) {
|
||||||
let (center, dist, angle) = Self::geometry(a.position, b.position);
|
let (center, dist, angle) = Self::geometry(ui_pos(a.position), ui_pos(b.position));
|
||||||
self.pending_tap = None;
|
self.pending_tap = None;
|
||||||
self.st = St::Two { a: a.id, b: b.id, center, dist, angle };
|
self.st = St::Two { a: a.id, b: b.id, center, dist, angle };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,13 +10,36 @@ use crate::settings::{key_name, Action, Settings};
|
|||||||
use macroquad::prelude::*;
|
use macroquad::prelude::*;
|
||||||
|
|
||||||
// ---- interface scaling
|
// ---- 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
|
// 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).
|
// is the interface factor (automatic from the resolution or fixed in the settings).
|
||||||
thread_local! {
|
thread_local! {
|
||||||
static UI_FACTOR_SETTING: std::cell::Cell<u8> = std::cell::Cell::new(0);
|
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 TOUCH: std::cell::Cell<bool> = std::cell::Cell::new(false);
|
||||||
static POINTER: std::cell::Cell<(f32, f32)> = std::cell::Cell::new((-1e5, -1e5));
|
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) {
|
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)));
|
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 {
|
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
|
// screen size and mouse in device pixels
|
||||||
pub fn sw() -> f32 {
|
pub fn sw() -> f32 {
|
||||||
@@ -61,6 +87,10 @@ pub fn F() -> u32 {
|
|||||||
if set > 0 {
|
if set > 0 {
|
||||||
return set as u32;
|
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);
|
let auto = ((sw() / 1280.0).min(sh() / 760.0).floor() as u32).clamp(1, 4);
|
||||||
if touch_ui() {
|
if touch_ui() {
|
||||||
// fingers need bigger buttons: about 2/3 of the device pixel ratio,
|
// 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()
|
(((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() {
|
pub fn begin_ui() {
|
||||||
let (w, h) = (sw(), sh());
|
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
|
// bar made of block characters: full blocks, an eighth block for the rest, shaded background
|
||||||
|
|||||||
Reference in new issue
Block a user