diff --git a/riddle/fonts/Caveat-Regular.ttf b/riddle/fonts/Caveat-Regular.ttf new file mode 100644 index 0000000..6a6526a Binary files /dev/null and b/riddle/fonts/Caveat-Regular.ttf differ diff --git a/riddle/src/fb.rs b/riddle/src/fb.rs index 0a596af..fd3ced1 100644 --- a/riddle/src/fb.rs +++ b/riddle/src/fb.rs @@ -1,8 +1,5 @@ //! Geometry helpers. Drawing lives in surface.rs. -pub const SCREEN_W: usize = 1620; -pub const SCREEN_H: usize = 2160; - /// Grow-only pixel bounding box, used to build update/dissolve regions. #[derive(Clone, Copy, Debug)] pub struct BBox { @@ -22,8 +19,8 @@ impl BBox { pub fn add(&mut self, x: i32, y: i32, margin: i32) { self.x0 = self.x0.min(x - margin).max(0); self.y0 = self.y0.min(y - margin).max(0); - self.x1 = self.x1.max(x + margin).min(SCREEN_W as i32 - 1); - self.y1 = self.y1.max(y + margin).min(SCREEN_H as i32 - 1); + self.x1 = self.x1.max(x + margin); + self.y1 = self.y1.max(y + margin); } pub fn rect(&self) -> (i32, i32, i32, i32) { (self.x0, self.y0, self.x1 - self.x0 + 1, self.y1 - self.y0 + 1) diff --git a/riddle/src/help.rs b/riddle/src/help.rs index 1cc0daf..4a7094b 100644 --- a/riddle/src/help.rs +++ b/riddle/src/help.rs @@ -2,7 +2,7 @@ //! the diary's gestures; touching the pen to the page dismisses it. Detection //! is local geometry — no oracle — so the guide works even with no network. -use crate::fb::{BBox, SCREEN_H, SCREEN_W}; +use crate::fb::BBox; use crate::script; use crate::surface::{Surface, BLACK, WHITE}; use ab_glyph::FontRef; @@ -144,7 +144,7 @@ pub struct Help { /// Draw the guide panel centered on the page; returns it for later dismissal. /// The gesture list depends on the display mode: only takeover owns the /// touchscreen (5-finger exit) and the power button. -pub fn show(surf: &mut Surface, font: &FontRef, takeover: bool) -> Help { +pub fn show(surf: &mut Surface, font: &FontRef, takeover: bool, sw: usize, sh: usize) -> Help { let body = if takeover { BODY_TAKEOVER } else { BODY_WINDOWED }; let title_h = (TITLE_PX * 1.4) as usize; let line_h = (BODY_PX * 1.3) as usize; @@ -154,10 +154,10 @@ pub fn show(surf: &mut Surface, font: &FontRef, takeover: bool) -> Help { for l in body { wmax = wmax.max(script::measure(font, l, BODY_PX)); } - let pw = (wmax as usize + 2 * PAD).min(SCREEN_W - 40); + let pw = (wmax as usize + 2 * PAD).min(sw - 40); let ph = PAD + title_h + line_h / 2 + body.len() * line_h + footer_h + PAD; - let px = (SCREEN_W - pw) / 2; - let py = (SCREEN_H.saturating_sub(ph)) / 2; + let px = (sw - pw) / 2; + let py = (sh.saturating_sub(ph)) / 2; let saved = surf.copy_rect(px, py, pw, ph); surf.fill_rect(px, py, pw, ph, WHITE); @@ -191,19 +191,19 @@ impl Help { /// Replace the page with the full-screen sleep card; returns the saved page /// pixels so waking can restore them exactly. -pub fn show_sleep(surf: &mut Surface, font: &FontRef) -> Vec { - let saved = surf.copy_rect(0, 0, SCREEN_W, SCREEN_H); - surf.fill_rect(0, 0, SCREEN_W, SCREEN_H, WHITE); - frame(surf, 48, 48, SCREEN_W - 96, SCREEN_H - 96, 4); - frame(surf, 66, 66, SCREEN_W - 132, SCREEN_H - 132, 1); - let y = SCREEN_H * 38 / 100; - blit_centered(surf, font, "The diary sleeps.", 116.0, 0, SCREEN_W, y); - blit_centered(surf, font, "Press the button to wake it.", 56.0, 0, SCREEN_W, y + 230); +pub fn show_sleep(surf: &mut Surface, font: &FontRef, sw: usize, sh: usize) -> Vec { + let saved = surf.copy_rect(0, 0, sw, sh); + surf.fill_rect(0, 0, sw, sh, WHITE); + frame(surf, 48, 48, sw - 96, sh - 96, 4); + frame(surf, 66, 66, sw - 132, sh - 132, 1); + let y = sh * 38 / 100; + blit_centered(surf, font, "The diary sleeps.", 116.0, 0, sw, y); + blit_centered(surf, font, "Press the button to wake it.", 56.0, 0, sw, y + 230); saved } -pub fn restore_sleep(surf: &mut Surface, saved: &[u8]) { - surf.paste_rect(0, 0, SCREEN_W, SCREEN_H, saved); +pub fn restore_sleep(surf: &mut Surface, saved: &[u8], sw: usize, sh: usize) { + surf.paste_rect(0, 0, sw, sh, saved); } fn frame(surf: &mut Surface, x: usize, y: usize, w: usize, h: usize, t: usize) { @@ -290,7 +290,7 @@ mod tests { #[test] fn modal_renders_and_restores() { - let (w, h) = (SCREEN_W, SCREEN_H); + let (w, h) = (1620usize, 2160usize); let mut buf = vec![0xFFu8; w * h * 4]; let ptr = buf.as_mut_ptr(); let mut surf = Surface::new(ptr, buf.len(), w, h, w * 4, crate::surface::PixFmt::Rgb32); @@ -300,7 +300,7 @@ mod tests { surf.fill_rect(700, 1000, 200, 200, BLACK); let before = surf.copy_rect(0, 0, w, h); - let panel = show(&mut surf, &font, true); + let panel = show(&mut surf, &font, true, w, h); let (px, py, pw, ph) = panel.region.rect(); assert!(pw > 400 && ph > 400, "panel too small: {pw}x{ph}"); // Panel must contain ink (text + frame). @@ -336,7 +336,7 @@ mod tests { #[test] fn sleep_page_renders_and_restores() { - let (w, h) = (SCREEN_W, SCREEN_H); + let (w, h) = (1620usize, 2160usize); let mut buf = vec![0xFFu8; w * h * 4]; let ptr = buf.as_mut_ptr(); let mut surf = Surface::new(ptr, buf.len(), w, h, w * 4, crate::surface::PixFmt::Rgb32); @@ -345,7 +345,7 @@ mod tests { surf.fill_rect(300, 300, 400, 400, BLACK); let before = surf.copy_rect(0, 0, w, h); - let saved = show_sleep(&mut surf, &font); + let saved = show_sleep(&mut surf, &font, w, h); let mut black = 0usize; for y in 0..h { for x in 0..w { @@ -370,7 +370,7 @@ mod tests { enc.write_header().unwrap().write_image_data(&gray).unwrap(); eprintln!("sleep snapshot: {}", out.display()); - restore_sleep(&mut surf, &saved); + restore_sleep(&mut surf, &saved, w, h); assert_eq!(before, surf.copy_rect(0, 0, w, h), "sleep restore is not exact"); } } diff --git a/riddle/src/main.rs b/riddle/src/main.rs index 24601b3..88bac02 100644 --- a/riddle/src/main.rs +++ b/riddle/src/main.rs @@ -27,11 +27,11 @@ use std::time::{Duration, Instant}; use ab_glyph::FontRef; -use fb::{BBox, SCREEN_H, SCREEN_W}; +use fb::BBox; use oracle::Event; use surface::{Surface, BLACK, FADED, WHITE}; -const FONT_TTF: &[u8] = include_bytes!("../fonts/DancingScript.ttf"); +const FONT_TTF: &[u8] = include_bytes!("../fonts/Caveat-Regular.ttf"); const PNG_PATH: &str = "/tmp/riddle-page.png"; const IDLE_COMMIT: Duration = Duration::from_millis(2800); @@ -183,15 +183,17 @@ fn run() -> std::io::Result<()> { let (disp, mut surf) = display::Display::open()?; let takeover = matches!(disp, display::Display::Quill); + let sw = surf.w; + let sh = surf.h; eprintln!( "riddle: display {} ({}x{} stride {})", if takeover { "quill/takeover" } else { "qtfb" }, - surf.w, - surf.h, + sw, + sh, surf.stride ); - let mut pen_dev = match pen::PenDevice::open() { + let mut pen_dev = match pen::PenDevice::open(sw, sh) { Ok(p) => Some(p), Err(e) => { eprintln!("riddle: raw pen unavailable ({e}), falling back to qtfb pen events"); @@ -215,7 +217,7 @@ fn run() -> std::io::Result<()> { signal_hook::flag::register(signal_hook::consts::SIGINT, Arc::clone(&sigterm))?; // Blank page. - surf.fill_rect(0, 0, SCREEN_W, SCREEN_H, WHITE); + surf.fill_rect(0, 0, sw, sh, WHITE); disp.update_all(surf.w, surf.h); // The diary's memory (None = RIDDLE_MEMORY=off or the dir is unusable). @@ -276,7 +278,7 @@ fn run() -> std::io::Result<()> { let pressed = p.drain_pressed(); if pressed && Instant::now() >= power_grace { eprintln!("riddle: sleeping (power button)"); - let saved = help::show_sleep(&mut surf, &font); + let saved = help::show_sleep(&mut surf, &font, sw, sh); disp.full_refresh(surf.w, surf.h); // Let the flashing refresh finish before the panel loses power. std::thread::sleep(Duration::from_millis(800)); @@ -305,7 +307,7 @@ fn run() -> std::io::Result<()> { eprintln!("riddle: suspend aborted (EPD discharge timer), retrying"); } eprintln!("riddle: waking"); - help::restore_sleep(&mut surf, &saved); + help::restore_sleep(&mut surf, &saved, sw, sh); disp.full_refresh(surf.w, surf.h); power::wifi_heal(); // Discard input that queued while asleep — stale pen events @@ -424,7 +426,7 @@ fn run() -> std::io::Result<()> { surf.fill_rect(qx as usize, qy as usize, qw as usize, qh as usize, WHITE); disp.update(qx, qy, qw, qh, false); user_ink.clear(); - let panel = help::show(&mut surf, &font, takeover); + let panel = help::show(&mut surf, &font, takeover, sw, sh); let (px, py, pw, ph) = panel.region.rect(); disp.update(px, py, pw, ph, false); eprintln!("riddle: guide shown"); @@ -432,8 +434,8 @@ fn run() -> std::io::Result<()> { } else if oracle.is_none() { // No spirit at all: don't eat ink that nothing will // answer — leave the writing and put the reason below. - let y = (user_ink.bbox.y1 + 90).min(SCREEN_H as i32 - 400); - let plan = plan_reply(&font, &oracle_excuse("no oracle"), Some(y)); + let y = (user_ink.bbox.y1 + 90).min(sh as i32 - 400); + let plan = plan_reply(&font, &oracle_excuse("no oracle"), Some(y), sw, sh); State::Replying { plan, next: Instant::now(), rx: None } } else { if let Err(e) = user_ink.to_png(&surf, PNG_PATH) { @@ -486,19 +488,19 @@ fn run() -> std::io::Result<()> { State::Thinking { rx, pulse, blot_on, since } => match rx.try_recv() { Ok(result) => { - surf.fill_rect(SCREEN_W / 2 - 14, SCREEN_H / 2 - 14, 28, 28, WHITE); - disp.update(SCREEN_W as i32 / 2 - 14, SCREEN_H as i32 / 2 - 14, 28, 28, true); + surf.fill_rect(sw / 2 - 14, sh / 2 - 14, 28, 28, WHITE); + disp.update(sw as i32 / 2 - 14, sh as i32 / 2 - 14, 28, 28, true); // First streamed event: start writing now; keep the // receiver so the rest of the reply can append itself. match result { Ok(Event::Show(id)) => { // An incantation: the rest of this turn is the // conjured memory, not a reply. (rx drops here.) - match conjure(&font, &store, id, &mut surf, &disp) { + match conjure(&font, &store, id, &mut surf, &disp, sw, sh) { Some(st) => st, None => { eprintln!("riddle: memory {id} is missing"); - let plan = plan_reply(&font, &oracle_excuse("lost page"), None); + let plan = plan_reply(&font, &oracle_excuse("lost page"), None, sw, sh); turn_failed = true; State::Replying { plan, next: Instant::now(), rx: None } } @@ -506,7 +508,7 @@ fn run() -> std::io::Result<()> { } Ok(Event::Ink(text)) => { turn_reply.push_str(&text); - let plan = plan_reply(&font, &text, None); + let plan = plan_reply(&font, &text, None, sw, sh); State::Replying { plan, next: Instant::now(), rx: Some(rx) } } Ok(Event::Transcript(t)) => { @@ -518,7 +520,7 @@ fn run() -> std::io::Result<()> { Err(e) => { eprintln!("riddle: oracle failed: {e}"); turn_failed = true; - let plan = plan_reply(&font, &oracle_excuse(&e), None); + let plan = plan_reply(&font, &oracle_excuse(&e), None, sw, sh); State::Replying { plan, next: Instant::now(), rx: None } } } @@ -528,12 +530,12 @@ fn run() -> std::io::Result<()> { // The oracle never answered (stalled stream, dead pi): // stop pulsing and say so instead of thinking forever. eprintln!("riddle: oracle timed out after {}s", ORACLE_PATIENCE.as_secs()); - surf.fill_rect(SCREEN_W / 2 - 14, SCREEN_H / 2 - 14, 28, 28, WHITE); - disp.update(SCREEN_W as i32 / 2 - 14, SCREEN_H as i32 / 2 - 14, 28, 28, true); - let plan = plan_reply(&font, &oracle_excuse("timed out"), None); + surf.fill_rect(sw / 2 - 14, sh / 2 - 14, 28, 28, WHITE); + disp.update(sw as i32 / 2 - 14, sh as i32 / 2 - 14, 28, 28, true); + let plan = plan_reply(&font, &oracle_excuse("timed out"), None, sw, sh); State::Replying { plan, next: Instant::now(), rx: None } } else if pulse.elapsed() >= Duration::from_millis(600) { - let (cx, cy) = (SCREEN_W as i32 / 2, SCREEN_H as i32 / 2); + let (cx, cy) = (sw as i32 / 2, sh as i32 / 2); if blot_on { surf.fill_rect(cx as usize - 14, cy as usize - 14, 28, 28, WHITE); } else { @@ -554,7 +556,7 @@ fn run() -> std::io::Result<()> { if let Some(ref r) = rx { let drop_rx = match r.try_recv() { Ok(Ok(Event::Ink(more))) => { - if plan.next_y > SCREEN_H as i32 - 200 { + if plan.next_y > sh as i32 - 200 { // The page is full: let the rest go unwritten // rather than inking below the visible page. eprintln!("riddle: reply reached the page bottom; trailing text dropped"); @@ -562,7 +564,7 @@ fn run() -> std::io::Result<()> { } else { turn_reply.push_str(" "); turn_reply.push_str(&more); - append_reply(&font, &mut plan, &more); + append_reply(&font, &mut plan, &more, sw, sh); false } } @@ -664,7 +666,7 @@ fn run() -> std::io::Result<()> { State::Conjuring { mut plan, next, saved } => { if stylus_tapped { // The writer interrupts: today's page returns at once. - surf.paste_rect(0, 0, SCREEN_W, SCREEN_H, &saved); + surf.paste_rect(0, 0, sw, sh, &saved); disp.full_refresh(surf.w, surf.h); State::MemoryShown { saved: None, until: Instant::now(), region: plan.region } } else if Instant::now() >= next { @@ -713,7 +715,7 @@ fn run() -> std::io::Result<()> { Some(s) => { if stylus_tapped || Instant::now() >= until { // The paper swallows its memory; today's page returns. - surf.paste_rect(0, 0, SCREEN_W, SCREEN_H, &s); + surf.paste_rect(0, 0, sw, sh, &s); disp.full_refresh(surf.w, surf.h); eprintln!("riddle: memory dismissed"); State::MemoryShown { saved: None, until, region } @@ -798,14 +800,16 @@ fn conjure( id: u64, surf: &mut Surface, disp: &display::Display, + sw: usize, + sh: usize, ) -> Option { let s = store.as_ref()?; let entry = s.get(id)?.clone(); let strokes = s.strokes(id).unwrap_or_default(); eprintln!("riddle: conjuring memory {id} ({})", memory::spoken_date(id)); - let saved = surf.copy_rect(0, 0, SCREEN_W, SCREEN_H); - surf.fill_rect(0, 0, SCREEN_W, SCREEN_H, WHITE); + let saved = surf.copy_rect(0, 0, sw, sh); + surf.fill_rect(0, 0, sw, sh, WHITE); disp.update_all(surf.w, surf.h); let mut all: Vec> = Vec::new(); @@ -815,7 +819,7 @@ fn conjure( let date = memory::spoken_date(entry.id); let mut raster = script::rasterize_line(font, &date, 54.0); script::thin(&mut raster); - let x0 = (SCREEN_W as i32 - raster.width as i32) / 2; + let x0 = (sw as i32 - raster.width as i32) / 2; let mut ink_bottom = 64; for stroke in script::trace(&raster) { let mapped: Vec<(i32, i32, i32)> = @@ -838,8 +842,8 @@ fn conjure( // Tom's old reply, below. if !entry.reply.is_empty() { - let y = (ink_bottom + 130).min(SCREEN_H as i32 - 400); - let reply = plan_reply(font, &entry.reply, Some(y)); + let y = (ink_bottom + 130).min(sh as i32 - 400); + let reply = plan_reply(font, &entry.reply, Some(y), sw, sh); for stroke in reply.strokes { let mapped: Vec<(i32, i32, i32)> = stroke.iter().map(|&(x, y)| (x, y, 2)).collect(); for &(x, y, r) in &mapped { @@ -858,12 +862,12 @@ fn conjure( /// Lay out reply text and produce screen-space strokes. `y_start` continues a /// streamed reply below its previous chunk; None places the first chunk. -fn plan_reply(font: &FontRef, text: &str, y_start: Option) -> WritePlan { - let max_w = (SCREEN_W as i32 - 2 * MARGIN_X) as f32; +fn plan_reply(font: &FontRef, text: &str, y_start: Option, sw: usize, sh: usize) -> WritePlan { + let max_w = (sw as i32 - 2 * MARGIN_X) as f32; let lines = script::wrap(font, text, REPLY_PX, max_w); let line_h = (REPLY_PX * 1.25) as i32; let total_h = line_h * lines.len() as i32; - let mut y = y_start.unwrap_or(((SCREEN_H as i32 - total_h) / 3).max(60)); + let mut y = y_start.unwrap_or(((sh as i32 - total_h) / 3).max(60)); let mut strokes = Vec::new(); let mut region = BBox::empty(); let mut seed = 0x1234u32; @@ -876,7 +880,7 @@ fn plan_reply(font: &FontRef, text: &str, y_start: Option) -> WritePlan { let mut raster = script::rasterize_line(font, line_text, REPLY_PX); script::thin(&mut raster); let line_strokes = script::trace(&raster); - let x0 = (SCREEN_W as i32 - raster.width as i32) / 2; + let x0 = (sw as i32 - raster.width as i32) / 2; let wobble = jitter(); for s in line_strokes { let mapped: Vec<(i32, i32)> = s.iter().map(|&(sx, sy)| (x0 + sx, y + sy + wobble)).collect(); @@ -892,8 +896,8 @@ fn plan_reply(font: &FontRef, text: &str, y_start: Option) -> WritePlan { } /// Splice a streamed continuation chunk into a running write animation. -fn append_reply(font: &FontRef, plan: &mut WritePlan, more: &str) { - let cont = plan_reply(font, more, Some(plan.next_y)); +fn append_reply(font: &FontRef, plan: &mut WritePlan, more: &str, sw: usize, sh: usize) { + let cont = plan_reply(font, more, Some(plan.next_y), sw, sh); if cont.strokes.is_empty() { return; } diff --git a/riddle/src/pen.rs b/riddle/src/pen.rs index 91cf6b6..18731c2 100644 --- a/riddle/src/pen.rs +++ b/riddle/src/pen.rs @@ -8,13 +8,12 @@ use std::io; use std::os::fd::RawFd; -use crate::fb::{SCREEN_H, SCREEN_W}; - -// Digitizer axis ranges on the Paper Pro ("Elan marker input"). -const DIGI_MAX_X: i32 = 11180; -const DIGI_MAX_Y: i32 = 15340; pub const MAX_PRESSURE: i32 = 4096; +// EVIOCGABS ioctl: read absolute axis info (struct input_absinfo). +const EVIOCGABS_X: libc::c_ulong = 0x80184540; // _IOR('E', 0x40 + ABS_X, struct input_absinfo) +const EVIOCGABS_Y: libc::c_ulong = 0x80184541; // _IOR('E', 0x40 + ABS_Y, struct input_absinfo) + const EV_SYN: u16 = 0; const EV_KEY: u16 = 1; const EV_ABS: u16 = 3; @@ -47,6 +46,10 @@ pub struct PenSample { pub struct PenDevice { fd: RawFd, + sw: i32, + sh: i32, + digi_max_x: i32, + digi_max_y: i32, // Accumulated state between SYN_REPORTs. raw_x: i32, raw_y: i32, @@ -58,20 +61,30 @@ pub struct PenDevice { impl PenDevice { /// Find and grab the marker input device. - pub fn open() -> io::Result { + pub fn open(sw: usize, sh: usize) -> io::Result { let path = find_marker_device()?; let cpath = std::ffi::CString::new(path.clone()).unwrap(); let fd = unsafe { libc::open(cpath.as_ptr(), libc::O_RDONLY | libc::O_NONBLOCK) }; if fd < 0 { return Err(io::Error::last_os_error()); } + // Read actual digitizer axis ranges — they differ between devices + // (e.g. Paper Pro: 11180×15340, Paper Pro Move: 6760×11960). + let digi_max_x = read_abs_max(fd, EVIOCGABS_X).unwrap_or(11180); + let digi_max_y = read_abs_max(fd, EVIOCGABS_Y).unwrap_or(15340); + let grab = unsafe { libc::ioctl(fd, EVIOCGRAB, 1i32) }; if grab != 0 { eprintln!("riddle: warning: EVIOCGRAB failed ({}) — xochitl will also see the pen", io::Error::last_os_error()); } - eprintln!("riddle: pen device {path} opened (grabbed: {})", grab == 0); + eprintln!("riddle: pen device {path} opened (grabbed: {}, digi {}x{})", + grab == 0, digi_max_x, digi_max_y); Ok(Self { fd, + sw: sw as i32, + sh: sh as i32, + digi_max_x, + digi_max_y, raw_x: 0, raw_y: 0, pressure: 0, @@ -127,8 +140,8 @@ impl PenDevice { if self.dirty { self.dirty = false; out.push(PenSample { - x: self.raw_x * (SCREEN_W as i32 - 1) / DIGI_MAX_X, - y: self.raw_y * (SCREEN_H as i32 - 1) / DIGI_MAX_Y, + x: self.raw_x * (self.sw - 1) / self.digi_max_x, + y: self.raw_y * (self.sh - 1) / self.digi_max_y, pressure: self.pressure, tool: self.tool, touching: self.touching, @@ -152,6 +165,14 @@ impl Drop for PenDevice { } } +/// Read the maximum value of an ABS axis via EVIOCGABS ioctl. +/// struct input_absinfo: value(i32), minimum(i32), maximum(i32), fuzz(i32), flat(i32), resolution(i32) +fn read_abs_max(fd: RawFd, ioctl_code: libc::c_ulong) -> Option { + let mut buf = [0i32; 6]; + let ret = unsafe { libc::ioctl(fd, ioctl_code, buf.as_mut_ptr()) }; + if ret == 0 && buf[2] > 0 { Some(buf[2]) } else { None } +} + fn find_marker_device() -> io::Result { for i in 0..8 { let name_path = format!("/sys/class/input/event{i}/device/name");