Module: Teek::UI::WidgetDSL
- Included in:
- Session
- Defined in:
- lib/teek/ui/widget_dsl.rb
Overview
The build surface: ui.<widget> methods that APPEND nodes to the
Document tree. They never touch Tk - widgets become live only when
the realizer runs at realize.
Names are deliberately Tk-free (the litmus test: if decoding a name needs Tk knowledge, the name is wrong) - see the design sketch for the full vocabulary rationale.
Mixed into Session rather than living on a separate accessor, so the
DSL reads as ui.button(...), not ui.widgets.button(...).
Included classes must provide @document (a Document), @stack
(an Array of Node, current-parent stack seeded with
@document.root), @scope_stack (an Array of Scope, current-scope
stack seeded with [Scope::TOP_LEVEL] - see #component), @vars
(an Array of Var), and @images (an Array of Image) - Session
sets all five up in initialize. They must also provide
#build_open? (a predicate the tree-mutating methods below check
via #raise_if_closed! - true before the initial realize and again
for the duration of an #add block, false otherwise).
Constant Summary collapse
- ORIENTATIONS =
Orientation values a
ui.splitaccepts - the same plain words Tk's own -orient option uses, so no translation is needed at realize. %i[horizontal vertical].freeze
- MENU_BAR_HOSTS =
Node types a menu_bar is allowed to attach to - the root window itself, or a ui.window toplevel. Attaching a -menu to anything else (a plain frame) isn't a real Tk option, so this fails fast at declaration time rather than surfacing as a cryptic Tcl error later.
%i[root window].freeze
Instance Attribute Summary collapse
-
#modal ⇒ ModalStack?
A push/pop stack for modal window handles - see ModalStack.
Instance Method Summary collapse
-
#[](name) ⇒ Handle?
Look up a named widget declared in the CURRENT scope: at the top level outside any #component, that's everything built outside one; inside a component's own block, only that component's own names - a sibling component's (or the top level's) same-named node is never found this way, and vice versa.
-
#box(name = nil, **opts, &block) ⇒ Object
boxis a bare alternate spelling ofpanel- same node type, so the realizer only ever has to know about:panel. -
#cell(row:, col:, span: 1) {|_self| ... } ⇒ void
Position the single widget declared in the block at (row, col) in the enclosing
ui.grid. -
#component(label = nil) {|c| ... } ⇒ ComponentHandle
Opens a fresh Scope around the block, so names declared inside it (
ui.button(:save), ...) never collide with the same name used elsewhere - in another component, or at the top level - no matter how many components share the same (or no)label, since Scope identity, not label, is what makes two scopes distinct. -
#context_menu(name = nil, **opts) {|m| ... } ⇒ Handle
A standalone popup menu - built the same declarative way as a menu_bar's dropdowns, but not attached to anything automatically.
-
#current_path ⇒ String
The current build-parent ancestry, as a readable breadcrumb (e.g.
"column > row") - derived from@stack, the one thing only the builder (not the Document) knows: which containers are currently open. -
#dialog(name = nil, modal: true, resizable: false, **opts, &block) ⇒ Handle
windowwith dialog-appropriate defaults - modal and fixed-size, for the common "small modal window" case (confirmations, pickers). -
#image(path, **opts) ⇒ Image
Declare an image, loaded from a file.
-
#menu_bar(name = nil, **opts) {|mb| ... } ⇒ Handle
A window's menu bar - the row of top-level dropdowns (File/Edit/...) along its top edge.
-
#overlay(at:) ⇒ void
Floats the single widget declared in the block on top of the enclosing
ui.canvas, positioned at a fixed corner/edge/center anchor via Tk'splacegeometry manager - a "use sparingly" escape valve for the one legitimate absolute-position case (a status readout or button bar layered over canvas content), not a general-purpose layout mode. -
#pane(name = nil, weight: nil, **opts, &block) ⇒ Handle
One region of an enclosing
ui.split. -
#raw {|app| ... } ⇒ nil
The build-time escape hatch.
-
#screens ⇒ Screens
A push/pop stack for content screens - see Screens.
-
#split(name = nil, orientation: :horizontal, **opts) {|s| ... } ⇒ Handle
A draggable split - two or more
#pane-declared regions, resizable by dragging the sash between them. -
#stretch(columns: [], rows: []) ⇒ void
Mark which columns/rows of the enclosing
ui.gridabsorb leftover space - the named replacement forgrid columnconfigure -weight. -
#tab(label, name = nil, **opts, &block) ⇒ Handle
One page of an enclosing
ui.tabs, labeledlabelin the tab bar. -
#var(initial) ⇒ Var
Declare a reactive variable.
Instance Attribute Details
#modal ⇒ ModalStack?
A push/pop stack for modal window handles - see ModalStack. nil
until assigned; unlike #screens it isn't created automatically,
since its callbacks (on_enter:/on_exit:) are mandatory and
app-specific: ui.modal = Teek::UI::ModalStack.new(on_enter:, on_exit:).
348 349 350 |
# File 'lib/teek/ui/widget_dsl.rb', line 348 def modal @modal end |
Instance Method Details
#[](name) ⇒ Handle?
Look up a named widget declared in the CURRENT scope: at the top level outside any #component, that's everything built outside one; inside a component's own block, only that component's own names - a sibling component's (or the top level's) same-named node is never found this way, and vice versa. See #component.
72 73 74 75 |
# File 'lib/teek/ui/widget_dsl.rb', line 72 def [](name) node = @document.find(name, scope: current_scope) node && Handle.new(node) end |
#box(name = nil, **opts, &block) ⇒ Object
box is a bare alternate spelling of panel - same node type, so
the realizer only ever has to know about :panel.
52 53 54 |
# File 'lib/teek/ui/widget_dsl.rb', line 52 def box(name = nil, **opts, &block) append_container(:panel, name, opts, &block) end |
#cell(row:, col:, span: 1) {|_self| ... } ⇒ void
This method returns an undefined value.
Position the single widget declared in the block at (row, col) in
the enclosing ui.grid. Only valid directly inside a grid's block.
95 96 97 98 99 100 101 102 103 104 105 106 107 108 |
# File 'lib/teek/ui/widget_dsl.rb', line 95 def cell(row:, col:, span: 1) grid_node = current_grid!('cell') before = grid_node.children.length yield self if block_given? placed = grid_node.children[before..] unless placed.length == 1 raise ArgumentError, "cell needs exactly one widget declared in its block (got #{placed.length})" end node = placed.first node.layout = (node.layout || {}).merge(cell: { row: row, col: col, span: span }) end |
#component(label = nil) {|c| ... } ⇒ ComponentHandle
Opens a fresh Scope around the block, so names declared inside
it (ui.button(:save), ...) never collide with the same name
used elsewhere - in another component, or at the top level - no
matter how many components share the same (or no) label, since
Scope identity, not label, is what makes two scopes distinct.
Splices its content directly into whatever's currently open -
this is scope isolation only, not an extra layer of nesting, so
a component built inside ui.panel(:p) { } attaches as an
ordinary child of :p, exactly like any other widget declared
right there would. The common 80% case - a plain method that
takes ui and appends into whatever's already open
(def toolbar(ui) = ui.row { ... }) - needs none of this; reach
for #component only when scope isolation itself is the point
(reuse across files, avoiding name collisions).
The returned ComponentHandle is the disciplined way for the
caller to reach into the component's own named widgets afterward
(screen.handle(:action)/screen[:action]) - the global ui[]
never sees into a component's scope (see #[]), so a component
built in one file and mounted from another stays reachable only
through the facade it hands back, not by guessing its internal
names.
324 325 326 327 328 329 330 331 332 333 334 |
# File 'lib/teek/ui/widget_dsl.rb', line 324 def component(label = nil, &block) raise_if_closed! scope = Scope.new(label, parent: current_scope) @scope_stack.push(scope) begin block.call(self) if block ensure @scope_stack.pop end ComponentHandle.new(@document, scope) end |
#context_menu(name = nil, **opts) {|m| ... } ⇒ Handle
A standalone popup menu - built the same declarative way as a
menu_bar's dropdowns, but not attached to anything automatically.
Wire it to a widget with handle.on_right_click(this).
239 240 241 242 243 244 245 |
# File 'lib/teek/ui/widget_dsl.rb', line 239 def (name = nil, **opts, &block) raise_if_closed! node = @document.create(type: :context_menu, name: name, opts: opts, scope: current_scope) @stack.last.add_child(node) (node, &block) Handle.new(node) end |
#current_path ⇒ String
The current build-parent ancestry, as a readable breadcrumb (e.g.
"column > row") - derived from @stack, the one thing only the
builder (not the Document) knows: which containers are
currently open. Useful in a build-time error message (+"X added
outside a Y; current parent: ##current_path"+) or just to orient
yourself while poking around mid-build.
84 85 86 87 |
# File 'lib/teek/ui/widget_dsl.rb', line 84 def current_path crumbs = @stack.reject { |node| node.type == :root }.map(&:display_name) crumbs.empty? ? '(top level)' : crumbs.join(' > ') end |
#dialog(name = nil, modal: true, resizable: false, **opts, &block) ⇒ Handle
window with dialog-appropriate defaults - modal and fixed-size,
for the common "small modal window" case (confirmations, pickers).
Same underlying node type as window, just different defaults for
modal:/resizable: - both still overridable.
61 62 63 |
# File 'lib/teek/ui/widget_dsl.rb', line 61 def dialog(name = nil, modal: true, resizable: false, **opts, &block) append_container(:window, name, opts.merge(modal: modal, resizable: resizable), &block) end |
#image(path, **opts) ⇒ Image
Declare an image, loaded from a file. Its Tcl image name is
allocated now (no interpreter needed - it's just a string); the
backing Photo - and the actual file load - only becomes
real at realize. Pass it as a widget's image: option (or a
later handle.configure(image: ...)) to display it.
289 290 291 292 293 294 295 |
# File 'lib/teek/ui/widget_dsl.rb', line 289 def image(path, **opts) raise_if_closed! @image_count = (@image_count || 0) + 1 img = Image.new("teek_ui_image_#{@image_count}", path, opts) @images << img img end |
#menu_bar(name = nil, **opts) {|mb| ... } ⇒ Handle
A window's menu bar - the row of top-level dropdowns (File/Edit/...)
along its top edge. Valid at the top level of a build or directly
inside ui.window - attaches to whichever of those it's declared
in once realized.
220 221 222 223 224 225 226 227 228 229 230 231 |
# File 'lib/teek/ui/widget_dsl.rb', line 220 def (name = nil, **opts, &block) raise_if_closed! parent = @stack.last unless MENU_BAR_HOSTS.include?(parent.type) raise ArgumentError, "menu_bar can only be declared at the top level of a build or directly inside ui.window" end node = @document.create(type: :menu_bar, name: name, opts: opts, scope: current_scope) parent.add_child(node) (node, &block) Handle.new(node) end |
#overlay(at:) ⇒ void
This method returns an undefined value.
Floats the single widget declared in the block on top of the
enclosing ui.canvas, positioned at a fixed corner/edge/center
anchor via Tk's place geometry manager - a "use sparingly"
escape valve for the one legitimate absolute-position case (a
status readout or button bar layered over canvas content), not a
general-purpose layout mode. Stays correctly positioned across a
canvas resize with nothing to redo by hand - place's relative
coordinates are fractions of the canvas's current size, recomputed
live by Tk on every resize. Only valid directly inside a
ui.canvas block.
138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 |
# File 'lib/teek/ui/widget_dsl.rb', line 138 def (at:) canvas_node = current_canvas!('overlay') unless OverlayAnchors::POSITIONS.key?(at) raise ArgumentError, "overlay's at: must be one of #{OverlayAnchors::POSITIONS.keys.join(', ')} (got #{at.inspect})" end before = canvas_node.children.length yield if block_given? placed = canvas_node.children[before..] unless placed.length == 1 raise ArgumentError, "overlay needs exactly one widget declared in its block (got #{placed.length})" end node = placed.first node.layout = (node.layout || {}).merge(overlay: { at: at }) end |
#pane(name = nil, weight: nil, **opts, &block) ⇒ Handle
One region of an enclosing ui.split. Only valid directly inside a
ui.split block; its own block builds the pane's content with the
ordinary widget DSL, same as any other container.
200 201 202 203 204 |
# File 'lib/teek/ui/widget_dsl.rb', line 200 def pane(name = nil, weight: nil, **opts, &block) current_split!('pane') opts = weight.nil? ? opts : opts.merge(pane_weight: weight) append_container(:pane, name, opts, &block) end |
#raw {|app| ... } ⇒ nil
The build-time escape hatch. A widget has no Tk path yet during
build, so app.command(handle.path, ...) mid-build can't work -
ui.raw defers the block instead, running it at realize with the
live app in scope. It's a closure, so it can still reference sibling
widgets by name (ui[:other].path) even if they're declared later -
by the time any raw block runs, the whole tree has already been
realized once over (same forward-reference guarantee event target:
gets). For anything after realize, a live Handle/session.app is
the escape hatch instead - see the README for the full split.
258 259 260 261 262 263 |
# File 'lib/teek/ui/widget_dsl.rb', line 258 def raw(&block) raise_if_closed! node = @document.create(type: :raw_op, opts: { block: block }) @stack.last.add_child(node) nil end |
#screens ⇒ Screens
A push/pop stack for content screens - see Screens. One stack per build, created on first access.
339 340 341 |
# File 'lib/teek/ui/widget_dsl.rb', line 339 def screens @screens ||= Screens.new(document: @document) end |
#split(name = nil, orientation: :horizontal, **opts) {|s| ... } ⇒ Handle
A draggable split - two or more #pane-declared regions, resizable
by dragging the sash between them. Maps to ttk::panedwindow.
181 182 183 184 185 186 187 |
# File 'lib/teek/ui/widget_dsl.rb', line 181 def split(name = nil, orientation: :horizontal, **opts, &block) unless ORIENTATIONS.include?(orientation) raise ArgumentError, "split's orientation must be :horizontal or :vertical (got #{orientation.inspect})" end append_container(:split, name, opts.merge(orient: orientation.to_s), &block) end |
#stretch(columns: [], rows: []) ⇒ void
This method returns an undefined value.
Mark which columns/rows of the enclosing ui.grid absorb leftover
space - the named replacement for grid columnconfigure -weight.
Only valid directly inside a grid's block.
116 117 118 119 120 121 |
# File 'lib/teek/ui/widget_dsl.rb', line 116 def stretch(columns: [], rows: []) grid_node = current_grid!('stretch') grid_node.opts[:stretch_columns] = Array(columns) if columns.any? grid_node.opts[:stretch_rows] = Array(rows) if rows.any? end |
#tab(label, name = nil, **opts, &block) ⇒ Handle
One page of an enclosing ui.tabs, labeled label in the tab bar.
Only valid directly inside a ui.tabs block; its own block builds
the pane's content with the ordinary widget DSL, same as any other
container.
164 165 166 167 |
# File 'lib/teek/ui/widget_dsl.rb', line 164 def tab(label, name = nil, **opts, &block) current_tabs!('tab') append_container(:tab, name, opts.merge(tab_label: label), &block) end |
#var(initial) ⇒ Var
Declare a reactive variable. Its Tcl variable name is allocated now
(no interpreter needed - it's just a string); the variable itself
only becomes real at realize. Bind it to widgets with bind:.
271 272 273 274 275 276 277 |
# File 'lib/teek/ui/widget_dsl.rb', line 271 def var(initial) raise_if_closed! @var_count = (@var_count || 0) + 1 v = Var.new("::teek_ui_var_#{@var_count}", initial) @vars << v v end |