Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added riddle/fonts/Caveat-Regular.ttf
Binary file not shown.
7 changes: 2 additions & 5 deletions riddle/src/fb.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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)
Expand Down
40 changes: 20 additions & 20 deletions riddle/src/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -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<u8> {
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<u8> {
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) {
Expand Down Expand Up @@ -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);
Expand All @@ -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).
Expand Down Expand Up @@ -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);
Expand All @@ -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 {
Expand All @@ -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");
}
}
76 changes: 40 additions & 36 deletions riddle/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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");
Expand All @@ -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).
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -424,16 +426,16 @@ 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");
State::Help { panel: Some(panel), until: Instant::now() + Duration::from_secs(45) }
} 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) {
Expand Down Expand Up @@ -486,27 +488,27 @@ 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 }
}
}
}
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)) => {
Expand All @@ -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 }
}
}
Expand All @@ -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 {
Expand All @@ -554,15 +556,15 @@ 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");
true
} 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
}
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 }
Expand Down Expand Up @@ -798,14 +800,16 @@ fn conjure(
id: u64,
surf: &mut Surface,
disp: &display::Display,
sw: usize,
sh: usize,
) -> Option<State> {
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<(i32, i32, i32)>> = Vec::new();
Expand All @@ -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)> =
Expand All @@ -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 {
Expand All @@ -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<i32>) -> WritePlan {
let max_w = (SCREEN_W as i32 - 2 * MARGIN_X) as f32;
fn plan_reply(font: &FontRef, text: &str, y_start: Option<i32>, 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;
Expand All @@ -876,7 +880,7 @@ fn plan_reply(font: &FontRef, text: &str, y_start: Option<i32>) -> 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();
Expand All @@ -892,8 +896,8 @@ fn plan_reply(font: &FontRef, text: &str, y_start: Option<i32>) -> 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;
}
Expand Down
Loading