class

Tryst::OwnerDrawnWidget

Inherits Reference < Object

Base class for an owner-drawn widget: a canvas-backed widget skeleton so building one is ~100 lines of drawing logic, not ~400 lines of plumbing (resize handling, hover/focus state, theme colors, cleanup) every ad-hoc canvas widget would otherwise re-derive from scratch. Subclass it and override #redraw; everything else below is free.

Lives at the App layer (Tryst::App), not Tryst::UI's declarative WidgetDSL - deliberately: a registered WidgetType's own post_create hook only ever gets the narrow AppContract (see app_contract.cr), which has no Photo/#every access at all, so there's nowhere inside that seam for a persistent per-widget Crystal object like this one to live. A future ui.* DSL wrapper around a specific widget built on this kit is a separate, later concern for whichever widget ships one.

Two drawing modes

Item-based: use #canvas directly, from within a subclass (CanvasItem already covers per-item creation/hit-testing/tag-binds) - the right tool for many small interactive shapes, at the cost of Tk 8.6's own canvas primitives not being antialiased.

Surface-backed: #blit a filled pixel buffer (from anywhere - most naturally tryst-vector's Surface#blit_to) into a canvas image item this class manages. OwnerDrawnWidget itself never references tryst-vector or any specific rasterizer - it only ever deals in "a buffer someone already filled," which is what keeps core dependency-free. tryst-vector's own Shape carries real hit-testing (tvg_paint_intersects) for anyone who wants per-shape interaction on top of a blitted surface without resorting to invisible overlay items.

Mix both freely: item-based interaction regions layered over blitted chrome, or vice versa.

Placing a finished widget

#canvas is protected - it's the drawing surface a subclass's own #redraw/keybinding code reaches for (see CircularProgress), not something the widget's own caller should ever need. Placing a finished widget in a layout goes through this class's own #pack/#grid/#path/#width/#height instead - the same names Widget itself uses, forwarded straight to the underlying canvas - so slider.pack(...) reads the same as packing any other widget, and nothing about this being canvas-backed leaks past the subclass that built it.

State

#hover?/#pressed?/#focused? are tracked from real Tk events (Enter/Leave/ButtonPress/ButtonRelease/FocusIn/FocusOut); #disabled? is a plain settable flag (Tk's canvas has no native "disabled" concept the way a ttk widget does). Nothing else in this codebase tracks interaction state for any widget today - this is the first such implementation, not a port of an existing pattern. Every state change calls #redraw.

Keyboard/Tab

-takefocus 1 makes the canvas part of Tab order - a raw canvas is mouse-only by default. What a keypress actually DOES (activation, value changes) is left to the subclass; only focus tracking itself is provided here, the same as hover/pressed.

Lifecycle

Owns a real canvas widget and (lazily) a Photo. Call #destroy when done, or let the finalizer do it - same contract as Photo. Any Tween started via #animate stops calling back once the canvas is gone even if this widget's own #destroy was never called (its parent destroyed instead, the window closed) - guarded per-tick rather than wired through App#on_widget_destroyed, which has no per-instance unregister and would leak one permanent closure per widget ever created.

Constructors

new(app : App, width : Int32 = 100, height : Int32 = 100, parent = nil)
Source

Class methods

destroy_task(tweens : Array(Tween), canvas : Widget) : Proc(Nil)

@api private - see @finalize_task's assignment in #initialize for why this is split out and built exactly once, up front, rather than from inside #finalize itself. Captures tweens/canvas as plain locals, not self/@tweens/@canvas: closing over self would keep this widget permanently reachable from its own finalizer, so it could never be collected in the first place (same trap Photo.delete_task documents). tweens is captured by reference to the SAME Array #initialize built (never reassigned afterward, only mutated via #animate/#prune_finished_tweens), so this sees whatever it holds at whatever point the finalizer actually runs, not a stale snapshot.

Source

Instance methods

animate(duration_ms : Int32, easing : Easing = :linear, &block : Float64 -> Nil) : Tween

Runs a Tween scoped to this widget: same as Tween.new(app, ...), except the block stops firing once #canvas no longer exists (see this class's own doc comment on why that's a per-tick guard rather than an App#on_widget_destroyed hook), and #destroy cancels whatever's still running immediately rather than waiting for its next tick to notice.

block typically closes over self (a subclass writes animate(...) { redraw }), so the tick closure captures only a WeakRef(self) and a box for the Tween's own identity - never self, block, or canvas directly. block/canvas instead live in @animate_payloads, reached only through the live widget once weak_self.value confirms it's still reachable. Otherwise this widget stays pinned reachable from App's own timer for as long as the tween runs (forever, for a looping animation), even after every other reference to it is dropped without #destroy - defeating the finalizer safety net.

Source
blit(pixel_data : Bytes, width : Int32, height : Int32, x : Int32 = 0, y : Int32 = 0, format : PixelFormat = :argb, composite : PhotoComposite = :set) : Nil

Blits pixel_data (from anywhere that fills a buffer in the given format - most naturally tryst-vector's Surface#blit_to, which calls straight through to this) into this widget's own canvas as a single image item, creating both the backing Photo and the canvas item that hosts it on first use.

Source
destroy

Releases the underlying canvas (and its Photo, if any) now, rather than waiting for a collection - same contract as Photo#delete. Bind-callback cleanup for #canvas's own path happens for free (see App's setup_destroy_cleanup, installed unconditionally for every widget); only this class's own extra state (running tweens) needs explicit teardown here.

Source
disabled=(value : Bool) : Bool

Marks this widget disabled: no further hover/pressed tracking (a disabled widget doesn't respond to the mouse) and dropped from Tab order. Calls #redraw so a subclass can gray itself out immediately.

Source
disabled?
Source
focused?
Source
grid

Grid this widget. See Widget#grid.

Source
height

Current height in pixels. See Widget#height.

Source
hover?
Source
pack

Pack this widget. See Widget#pack.

Source
path

This widget's own Tk path - for the rare case a caller needs to hand it to raw Tcl (grid/pack config on a parent, window manager calls) rather than going through this class's own API.

Source
photo

The pixel buffer this widget draws into, if it's using surface- backed mode at all - nil until the first #blit. Sized to whatever the last #blit call gave it, not necessarily #canvas's own current size (a widget may blit a sub-region of itself).

Source
pressed?
Source
redraw

Subclasses draw here - called after every resize and every state change (#hover?/#pressed?/#focused?/#disabled? all changing). Given no arguments deliberately: #canvas's own current width/height (via #canvas.width/#canvas.height, i.e. real winfo queries) are always the authority, not a value that could go stale between when a resize fired and when this actually runs.

Source
theme
Source
width

Current width in pixels. See Widget#width.

Source