summaryrefslogtreecommitdiff
path: root/chapter31/docs
diff options
context:
space:
mode:
authorCliff Click <cliffc@acm.org>2026-11-12 23:28:58 -0800
committerCliff Click <cliffc@acm.org>2026-11-12 23:28:58 -0800
commitb7440c1e4c182ad977996d91bfb8125091ffae92 (patch)
tree8f530f554d1b1e1c1eba02f361286e3b6b575849 /chapter31/docs
parent40d2d3f458846494c6e4bbf284fe5a61bb4bb417 (diff)
downloadSimple-b7440c1e4c182ad977996d91bfb8125091ffae92.tar.gz
Simple-b7440c1e4c182ad977996d91bfb8125091ffae92.zip
ch31 early push needs review
Diffstat (limited to 'chapter31/docs')
-rw-r--r--chapter31/docs/31-grammar.md9
-rw-r--r--chapter31/docs/drawing.md18
-rw-r--r--chapter31/docs/examples/Clock.smp105
-rw-r--r--chapter31/docs/examples/README.md6
-rw-r--r--chapter31/docs/examples/TicTacToeGraphical.smp2
-rw-r--r--chapter31/docs/graphics.md25
-rw-r--r--chapter31/docs/time.md119
7 files changed, 273 insertions, 11 deletions
diff --git a/chapter31/docs/31-grammar.md b/chapter31/docs/31-grammar.md
index 19f834af..3083fcc2 100644
--- a/chapter31/docs/31-grammar.md
+++ b/chapter31/docs/31-grammar.md
@@ -195,10 +195,13 @@ FLOAT_LITERAL : DIGIT+ ('.' DIGIT* ('e' DIGIT+)? | 'e' '-'? DIGIT+) 'f'?
// Interpolation re-enters the ordinary expression grammar, recursively.
stringExpression : '"' (STRING_TEXT | ESCAPE | '%%' | interpolation)* '"' ;
interpolation : '%' format? '(' assignment ')' ;
-format : FORMAT_FLAG* FORMAT_WIDTH? ('.' FORMAT_WIDTH?)? FORMAT_KIND ;
+format : FORMAT_FLAG* FORMAT_WIDTH? ('.' FORMAT_WIDTH?)? FORMAT_KIND TIME_STYLE? ;
// Flags: - + space # 0. Width/precision: decimal digits, no expressions.
-// Kinds: d i u o x X f F e E g G a A c s b.
-// Duplicate/inapplicable flags and precision on c/b are rejected.
+// Kinds: d i u o x X f F e E g G a A c s b t.
+// TIME_STYLE: F r R M D T, allowed only after t; default F (ISO UTC).
+// Time styles: ISO, 12-hour, 24-hour, elapsed MM:SS.mmm, DD/MM/YY, HH:MM:SS.
+// Duplicate/inapplicable flags and precision on c/b/t are rejected.
+// Time accepts width and left alignment only; see time.md.
// STRING_TEXT excludes double quote, backslash, and percent.
CHARACTER_LITERAL : '\'' (ESCAPE | ~['\\]) '\'' ;
fragment ESCAPE : '\\' [0ntr\\'"%] ;
diff --git a/chapter31/docs/drawing.md b/chapter31/docs/drawing.md
index 3fec1f12..af0a7f06 100644
--- a/chapter31/docs/drawing.md
+++ b/chapter31/docs/drawing.md
@@ -51,6 +51,22 @@ for computed components. The font masks use leading-zero octal, with five
digits after the prefix: each digit is one three-pixel row, top to bottom.
For example, `A` is `025755`: rows `010`, `101`, `111`, `101`, `101`.
+## Button widget
+
+`new sys.draw.Button(x,y,width,height,label)` creates a rectangular software
+button. It owns no window or native resources. Feed pointer coordinates and
+the held mouse-button state to `update(x,y,down)` once per event-loop iteration.
+It returns true once on release inside, provided the press began inside.
+Dragging in from outside does not activate it; releasing outside cancels.
+
+`hovered` and `pressed` expose visual state. Set `enabled=false` to cancel a
+pending press and disable activation; `label` can be changed at any time.
+`contains(x,y)` uses half-open bounds. `draw(canvas,scale,foreground,background,active)`
+paints the button and centers its 3x5 label when it fits. Colors belong to the
+caller. Keyboard shortcuts belong to the application; this small widget has
+no focus system. The [clock example](examples/Clock.smp) shows mouse activation
+and a Space shortcut in the same single-threaded loop.
+
## Graphical tic-tac-toe
From `chapter31`:
@@ -72,7 +88,7 @@ polling still processes window events and waits between polls.
`make graphical-tictactoe CTAGS=` builds without opening a window or requiring
MiniFB. Its output is `build/examples/x86_64_v2_win64/TicTacToeGraphical.exe`
on Windows or `build/examples/x86_64_v2_SystemV/TicTacToeGraphical` on Linux.
-The explicit backend path follows [the pixel-window instructions](graphics.md).
+Backend discovery and explicit overrides follow [the pixel-window instructions](graphics.md).
Passing any argument performs a five-poll, self-closing smoke test.
The original `make tictactoe` still builds the terminal game.
diff --git a/chapter31/docs/examples/Clock.smp b/chapter31/docs/examples/Clock.smp
new file mode 100644
index 00000000..900126e1
--- /dev/null
+++ b/chapter31/docs/examples/Clock.smp
@@ -0,0 +1,105 @@
+// Local clocks; click the button or press Space to switch. Q/Escape exits.
+// Any argument runs ten frames, exercising both faces for smoke tests.
+u32 GREEN=0x62ff70, DIM=0x123d20, BACKGROUND=0x020b05;
+val main = { u8[~][] args ->
+ val screen=new sys.draw.Canvas(720,540);
+ if( sys.gfx.open("Simple digital and analog clocks",screen.width,screen.height)!=sys.gfx.OK ) return 1;
+ val button=new sys.draw.Button(220,454,280,52,"ANALOG CLOCK");
+ val timers=new sys.time.Timers();
+ i64 frame=timers.every(33);
+ if( frame<0 ) { sys.gfx.close(); return 1; }
+ bool analog=false, held=false;
+ int frames=0, status=sys.gfx.OK;
+ while( status==sys.gfx.OK ) {
+ bool space=sys.gfx.key(' ');
+ bool down=sys.gfx.button(sys.gfx.LEFT);
+ bool click=button.update(sys.gfx.mouseX(),sys.gfx.mouseY(),down);
+ if( click || (space && !held) ) analog=!analog;
+ held=space;
+ button.label=analog ? "DIGITAL CLOCK" : "ANALOG CLOCK";
+ if( sys.gfx.key('Q') ) break;
+ if( timers.poll(sys.time.monotonic())==frame ) {
+ _draw(screen,sys.time.now(),analog);
+ button.draw(screen,3,GREEN,BACKGROUND,DIM);
+ status=sys.gfx.present(screen.pixels);
+ if( args#>1 ) {
+ frames++;
+ if( frames==5 ) analog=true;
+ if( frames==10 ) break;
+ }
+ }
+ if( status==sys.gfx.OK ) status=sys.gfx.wait(sys.time.remaining(timers.nextDeadline(),sys.time.monotonic()));
+ }
+ sys.gfx.close();
+ return status==sys.gfx.ERROR ? 1 : 0;
+};
+val _draw = { !sys.draw.Canvas screen, i64 wall, i64 analog ->
+ val d=sys.time.local(wall);
+ screen.clear(BACKGROUND);
+ screen.text(266,24,"SIMPLE CLOCK",4,GREEN);
+ if( analog ) _analog_noInline(screen,d.hour,d.minute,d.second,d.millisecond);
+ else _digital_noInline(screen,d.hour,d.minute,d.second);
+ screen.text(298,412,"%(sys.time.formatLocal(wall,sys.time.DATE))",4,GREEN);
+ screen.text(249,520,"SPACE SWITCH Q OR ESC QUIT",2,GREEN);
+};
+// a..g bits: top, upper-right, lower-right, bottom, lower-left, upper-left,
+// middle. Faint unlit bars retain the old LED figure-eight silhouette.
+val _digit_noInline = { !sys.draw.Canvas c, int x, int y, i64 digit ->
+ int bits=digit==0 ? 0x3f : digit==1 ? 0x06 : digit==2 ? 0x5b :
+ digit==3 ? 0x4f : digit==4 ? 0x66 : digit==5 ? 0x6d :
+ digit==6 ? 0x7d : digit==7 ? 0x07 : digit==8 ? 0x7f : digit==9 ? 0x6f : 0;
+ for( int i=0; i<7; i++ ) {
+ u32 color=(bits&(1<<i))!=0 ? GREEN : DIM;
+ if( i==0 || i==3 || i==6 ) {
+ int dy=i==0 ? 0 : i==3 ? 144 : 72;
+ c.line(x+14,y+dy,x+58,y+dy,10,color);
+ } else {
+ bool right=i==1 || i==2;
+ int dx=right ? 68 : 4, dy=i==1 || i==5 ? 12 : 84;
+ c.line(x+dx,y+dy,x+dx,y+dy+48,10,color);
+ }
+ }
+};
+val _digital_noInline = { !sys.draw.Canvas c, int hour, int minute, int second ->
+ _digit_noInline(c,68,166,hour/10); _digit_noInline(c,154,166,hour-hour/10*10);
+ _digit_noInline(c,284,166,minute/10); _digit_noInline(c,370,166,minute-minute/10*10);
+ _digit_noInline(c,500,166,second/10); _digit_noInline(c,586,166,second-second/10*10);
+ for( int i=0; i<2; i++ ) {
+ c.fillCircle(254+i*216,214,6,GREEN);
+ c.fillCircle(254+i*216,262,6,GREEN);
+ }
+ c.text(272,352,"LOCAL TIME 24H",3,GREEN);
+};
+// Rational sine approximation, accurate enough for pixel clock geometry.
+// Arguments are nonnegative turns; no additional native math dependency.
+val _sin_noInline = { f64 turn ->
+ i64 whole=turn; turn-=whole;
+ bool negative=turn>0.5;
+ f64 x=(negative ? turn-0.5 : turn)*6.283185307179586;
+ f64 product=x*(3.141592653589793-x);
+ f64 numerator=negative ? -16.0 : 16.0;
+ return numerator*product/(49.34802200544679-4*product);
+};
+val _hand_noInline = { !sys.draw.Canvas c, f64 turn, int length, int thickness, u32 color ->
+ int x=360+_sin_noInline(turn)*length, y=232-_sin_noInline(turn+0.25)*length;
+ c.line(360,232,x,y,thickness,color);
+};
+val _analog_noInline = { !sys.draw.Canvas c, int hour, int minute, int second, int millisecond ->
+ c.circle(360,232,154,3,GREEN);
+ for( int i=0; i<60; i++ ) {
+ f64 turn=i/60.0;
+ bool major=i-i/5*5==0;
+ int inner=major ? 133 : 143;
+ int x0=360+_sin_noInline(turn)*inner, y0=232-_sin_noInline(turn+0.25)*inner;
+ int x1=360+_sin_noInline(turn)*149, y1=232-_sin_noInline(turn+0.25)*149;
+ c.line(x0,y0,x1,y1,major ? 3 : 1,GREEN);
+ }
+ c.text(349,100,"12",2,GREEN); c.text(484,227,"3",2,GREEN);
+ c.text(357,354,"6",2,GREEN); c.text(230,227,"9",2,GREEN);
+ f64 seconds=second+millisecond/1000.0;
+ f64 minutes=minute+seconds/60.0;
+ _hand_noInline(c,(hour-hour/12*12+minutes/60.0)/12.0,78,8,GREEN);
+ _hand_noInline(c,minutes/60.0,112,5,GREEN);
+ _hand_noInline(c,seconds/60.0,128,2,0xc3ffcb);
+ c.fillCircle(360,232,6,GREEN);
+};
diff --git a/chapter31/docs/examples/README.md b/chapter31/docs/examples/README.md
index a1d81be2..46965ac0 100644
--- a/chapter31/docs/examples/README.md
+++ b/chapter31/docs/examples/README.md
@@ -10,6 +10,12 @@ The [graphical tic-tac-toe](TicTacToeGraphical.smp) uses the Simple
replay, and Q/Escape to quit. The terminal game below remains available.
Optional [background audio](../audio.md) supplies soft move and win/loss cues.
+The [digital and analog clock](Clock.smp) demonstrates the [time library](../time.md),
+green seven-segment digits, smooth clock hands, and a reusable software button.
+Run `make run-clock CTAGS=`; click the button or press Space to switch faces,
+and Q/Escape to exit. Both faces show system-local time.
+`make clock-example CTAGS=` builds it without starting a window.
+
From `chapter31`, build the tic-tac-toe executable (without starting it):
```sh
diff --git a/chapter31/docs/examples/TicTacToeGraphical.smp b/chapter31/docs/examples/TicTacToeGraphical.smp
index b4f8bcf4..fddcf254 100644
--- a/chapter31/docs/examples/TicTacToeGraphical.smp
+++ b/chapter31/docs/examples/TicTacToeGraphical.smp
@@ -13,7 +13,7 @@ val main = { u8[~][] args ->
bool mouseHeld = false, keyHeld = false, replayHeld = false;
int hover = -1, frames = 0;
while( result==sys.gfx.OK ) {
- result = sys.gfx.poll(true);
+ result = sys.gfx.wait(17);
if( result!=sys.gfx.OK ) break;
if( sys.gfx.key('Q') ) break;
bool down = sys.gfx.button(sys.gfx.LEFT);
diff --git a/chapter31/docs/graphics.md b/chapter31/docs/graphics.md
index 2394a401..4977b490 100644
--- a/chapter31/docs/graphics.md
+++ b/chapter31/docs/graphics.md
@@ -21,7 +21,7 @@ downloads dependencies unless you explicitly request `make libs` or `make init`
windows. It excludes the interactive `run-graphics` target.
The runtime contains a small loader using the OS dynamic-library facilities.
-It does nothing until `sys.gfx.open`. Without a configured backend, `open`
+It does nothing until `sys.gfx.open`. Without an available backend, `open`
prints a diagnostic to stderr and returns `sys.gfx.ERROR` (-1). Programs can
return a nonzero status or fall back to text; the library does not terminate
the process. Non-graphics programs ignore even a broken graphics configuration.
@@ -92,9 +92,16 @@ SIMPLE_MINIFB="$PWD/build/release/x86_64_v2_SystemV/libsimple_minifb.so" \
xvfb-run -a build/examples/x86_64_v2_SystemV/Graphics smoke
```
-The backend path is explicit: a DLL installed elsewhere or a different working
-directory cannot silently select the wrong version. `SIMPLE_MINIFB=off` disables
-graphics; an absent, unloadable, or incompatible backend produces a diagnostic.
+With no `SIMPLE_MINIFB` setting, the runtime searches beside the executable,
+then its `../../release/<ABI>` and `../release/<ABI>` directories. These cover
+the examples and test build layouts regardless of the current directory.
+It also checks the current directory, `build/release/<ABI>`, and
+`chapter31/build/release/<ABI>` relative to the current directory. Thus built
+examples such as `Clock.exe` can be launched directly without configuration.
+
+`SIMPLE_MINIFB` overrides discovery; an invalid explicit path reports an error
+without falling back. `SIMPLE_MINIFB=off` disables graphics for headless tests.
+An absent, unloadable, or incompatible backend produces a diagnostic.
A backend that loads but cannot create a window also reports an error cleanly.
## Initial interface
@@ -103,15 +110,21 @@ A backend that loads but cannot create a window also reports an error cleanly.
| --- | --- |
| `open(title,width,height)` | Open one window; returns `OK` or `ERROR`. |
| `present(pixels)` | Present a `u32[~]` of exactly `width*height` pixels, packed `0x00RRGGBB`. |
-| `poll(wait)` | Process input; `true` waits about 17ms first to avoid a busy loop. |
+| `poll(wait)` | Process input; `true` waits up to 17ms, waking for input. |
+| `wait(timeoutMs)` | Wait for input or timeout, then process events; 0 polls, -1 waits indefinitely. |
| `mouseX()`, `mouseY()` | Current mouse position, origin at upper left. |
| `button(LEFT)`, `key(code)` | Current pressed state; printable keys use uppercase ASCII. |
| `close()` | Release native window and pixel storage; safe to repeat. |
-`present` and `poll` return `OK` (0), `CLOSED` (1, normal close/Escape), or
+`present`, `poll`, and `wait` return `OK` (0), `CLOSED` (1, normal close/Escape), or
`ERROR` (-1). Stop polling after either terminal result and call `close`.
The initial input API samples held state; it is not a queue of every transition.
+Waits may return early; values below -1 are errors. Recheck the monotonic
+clock after waking. The [time library and animation example](time.md) combine
+this wait with cooperative timer deadlines. Backend API version 2 adds the
+wait operation; rebuild older MiniFB adapters from the cached sources.
+
MiniFB retains its last framebuffer for repaint events. The adapter copies the
Simple array into native-owned storage before presenting; it never retains a
Simple heap pointer. Framebuffer allocation, the copy, and window/input calls
diff --git a/chapter31/docs/time.md b/chapter31/docs/time.md
new file mode 100644
index 00000000..04865626
--- /dev/null
+++ b/chapter31/docs/time.md
@@ -0,0 +1,119 @@
+# Time and single-threaded animation
+
+`sys.time` uses bare `i64` values: absolute instants are milliseconds since
+the Unix epoch in UTC; durations are milliseconds. Monotonic readings belong
+to a separate clock domain. Never subtract a wall-clock reading from a
+monotonic deadline.
+
+## Clocks and waits
+
+| Call | Result |
+| --- | --- |
+| `time.now()` | Unix-epoch milliseconds; may jump when the system clock changes. |
+| `time.monotonic()` | Monotonic milliseconds for elapsed time and deadlines. |
+| `time.nanoTime()` | Monotonic nanoseconds, with the clock's available resolution. |
+| `time.sleep(ms)` | Sleep for a nonnegative duration. |
+| `time.sleepUntil(deadline)` | Wait for a monotonic millisecond deadline; returns immediately if past. |
+| `time.remaining(deadline, current)` | Nonnegative difference, saturating on overflow. |
+
+Clock errors return `-1`; sleeps return zero on success or `-1` on error,
+with `sys.libc.errno()` available. Interrupted sleeps resume rather than
+returning early. Wakeup times are approximate, and these calls do not wake a
+suspended laptop. Sleeping blocks the one Simple execution thread; use
+`gfx.wait` when window input must remain responsive.
+
+`seconds(n)`, `minutes(n)`, `hours(n)`, and `days(n)` convert signed counts
+to milliseconds. `toSeconds(ms)`, `toMinutes(ms)`, `toHours(ms)`, and
+`toDays(ms)` truncate toward zero. A duration day is exactly 24 hours,
+independent of daylight saving. `toNanos(ms)` and `fromNanos(ns)` convert
+between millisecond and nanosecond units. Multiplication overflow returns
+`-1`. Constants `SECOND`, `MINUTE`, `HOUR`, and `DAY` are also available.
+
+## String interpolation
+
+Time formatting is built into the existing string interpolation syntax:
+
+| Format | Example | Named style |
+| --- | --- | --- |
+| `%t(instant)` or `%tF(instant)` | `2026-11-12T00:34:05.123Z` | `time.ISO` |
+| `%tr(instant)` | `12:34am` | `time.CLOCK12` |
+| `%tR(instant)` | `00:34` | `time.CLOCK24` |
+| `%tT(instant)` | `00:34:05` | `time.CLOCK24_SECONDS` |
+| `%tD(instant)` | `12/11/26` | `time.DATE` |
+| `%tM(duration)` | `02:03.456` | `time.ELAPSED` |
+
+For example, `"UTC %tR(sys.time.now())"`. Width and left alignment work as
+for strings: `%30t(instant)` and `%-30t(instant)`. Precision and numeric flags
+are rejected. `%tM` formats a duration, with total minutes rather than wrapping
+at an hour; negative durations keep their sign.
+
+Interpolation uses UTC, making its output stable across machines.
+`time.format(instant, style)` is the equivalent library call;
+`time.formatLocal(instant, style)` uses the system's local zone. Local ISO
+output includes a numeric offset instead of `Z` when nonzero, retaining
+offset seconds for historical zones. Invalid calendar values/styles produce
+`<invalid time>`. The ISO form retains all millisecond bits within the supported
+calendar range; short formats intentionally omit information.
+
+## Calendar values and absolute parsing
+
+`time.utc(instant)` and `time.local(instant)` return a `DateTime` with fields
+`year`, `month`, `day`, `hour`, `minute`, `second`, `millisecond`, `weekday`
+(Sunday = 0), `offsetSeconds` (east of UTC), and `daylight`. A zero year means
+conversion failed. `time.zoneName(instant)` returns the local abbreviation or
+an empty string on error. Local conversion uses the operating system's zone
+rules, including DST at that instant; no time-zone database is downloaded.
+
+`time.makeUTC(year, month, day, hour, minute, second, millisecond)` validates
+and constructs an instant, returning `-1` on invalid fields. `time.valid(...)`,
+`time.leap(year)`, and `time.monthDays(year, month)` support calendar checks.
+Calendar conversion supports Gregorian years 1 through 9999, bounded by
+`time.MIN` and `time.MAX`, and rejects leap seconds.
+
+`time.parse(text)` accepts strict absolute ISO text:
+`YYYY-MM-DDTHH:mm:ss[.SSS]Z` or the same with an explicit `+HH:MM`/`-HH:MM`
+offset (optionally with `:SS`). Fractions have exactly three digits. No relative
+time parsing, implicit local zone, locale-dependent parsing, or named-zone
+conversion is provided. Offsets must have hours 00–23 and minutes/seconds 00–59.
+Invalid text or an out-of-range result returns `-1`. By design, this sentinel
+is indistinguishable from the valid instant `1969-12-31T23:59:59.999Z`;
+formatting and calendar conversion still support that instant.
+
+## Cooperative timers
+
+Create `val timers = new sys.time.Timers();` on the game thread:
+
+| Operation | Meaning |
+| --- | --- |
+| `after(delay)` | Register a one-shot timer; return its positive ID, or `-1`. |
+| `every(period)` | Register a repeating timer; period must be positive. |
+| `afterAt(current, delay)`, `everyAt(current, period)` | Supply a monotonic reading explicitly, useful for deterministic tests. |
+| `cancel(id)` | Remove a timer; return whether it was present. |
+| `nextDeadline()` | Earliest monotonic deadline, or `-1` if empty. |
+| `poll(current)` | Return the next due ID, or `-1` if none. |
+
+Poll repeatedly to drain due timers. Deadlines order delivery, then registration
+ID breaks ties. Repeating timers advance from their previous deadline, skipping
+missed ticks to the first future deadline rather than drifting or replaying a
+backlog. Deadline overflow rejects registration or cancels a repeating timer.
+There are no callbacks or timer threads: the caller handles each returned ID.
+
+`gfx.wait(timeoutMs)` processes events after waiting for input or a timeout:
+zero polls, `-1` waits indefinitely, values below `-1` fail. It returns the same
+`OK`, `CLOSED`, or `ERROR` statuses as `gfx.poll`, and may wake early. Re-read the
+monotonic clock after each wake. This allows one thread to handle timers,
+input, simulation, and drawing without busy waiting.
+
+The [digital and analog clock example](examples/Clock.smp) combines these pieces. From
+the repository root, with graphics dependencies already cached:
+
+```sh
+make -C chapter31 clock-example CTAGS= # Build without opening a window.
+make -C chapter31 run-clock CTAGS= # Build the backend and run.
+```
+
+Click the button or press Space to switch between green seven-segment digits
+and an analog dial with smoothly moving hands. Both display system-local time;
+Q/Escape exits. Passing any argument directly to the executable closes it after
+ten frames, switching faces halfway through. Hands are computed from the current
+time, so delayed frames do not create a simulation catch-up backlog.