From b7440c1e4c182ad977996d91bfb8125091ffae92 Mon Sep 17 00:00:00 2001 From: Cliff Click Date: Thu, 12 Nov 2026 23:28:58 -0800 Subject: ch31 early push needs review --- chapter31/docs/31-grammar.md | 9 +- chapter31/docs/drawing.md | 18 +++- chapter31/docs/examples/Clock.smp | 105 ++++++++++++++++++++++ chapter31/docs/examples/README.md | 6 ++ chapter31/docs/examples/TicTacToeGraphical.smp | 2 +- chapter31/docs/graphics.md | 25 ++++-- chapter31/docs/time.md | 119 +++++++++++++++++++++++++ 7 files changed, 273 insertions(+), 11 deletions(-) create mode 100644 chapter31/docs/examples/Clock.smp create mode 100644 chapter31/docs/time.md (limited to 'chapter31/docs') 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< + _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/` and `../release/` directories. These cover +the examples and test build layouts regardless of the current directory. +It also checks the current directory, `build/release/`, and +`chapter31/build/release/` 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 +``. 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. -- cgit v1.2.3