Class: Tuile::Component::Window

Inherits:
Component
  • Object
show all
Includes:
HasCaption, HasContent
Defined in:
lib/tuile/component/window.rb,
sig/tuile.rbs

Overview

A window with a frame, a #caption and a content Tuile::Component. Doesn't support overlapping with other windows: it paints its entire contents and doesn't clip if there are other overlapping windows.

The window's content is unset by default; assign one via HasContent#content=.

Window is considered invisible if #rect is empty. The window won't draw when invisible. (Repaint of detached windows is short-circuited by #invalidate; subclasses don't need to re-check.)

Direct Known Subclasses

ConfirmWindow, InfoWindow, LogWindow, PickerWindow

Instance Attribute Summary collapse

Attributes included from HasContent

#content

Instance Method Summary collapse

Constructor Details

#initialize(caption = nil) ⇒ Window

@param caption — the border title, coerced the same way HasCaption#caption= coerces it.

Parameters:

  • caption (?(String | StyledString), nil) (defaults to: nil)


20
21
22
23
24
25
26
27
28
29
30
# File 'lib/tuile/component/window.rb', line 20

def initialize(caption = nil)
  super()
  @border_right = 1
  self.caption = caption
  @content = nil
  # The bottom row holds either a widget or chrome text, never both; see
  # the precedence note on #footer=.
  @footer_slot = Slot.new
  add_child(@footer_slot) # appended: the footer paints over the border row
  @footer_text = StyledString::EMPTY
end

Instance Attribute Details

@return — optional chrome embedded into the bottom border line, mirroring #caption on the top line. Empty by default; hidden whenever a #footer component is present.

Returns:



41
42
43
# File 'lib/tuile/component/window.rb', line 41

def footer_text
  @footer_text
end

Instance Method Details

#bottom_border(inner_w, fg) ⇒ StyledString

Builds the bottom border row. The corners take the border color; the interior is plain dashes when a #footer component occupies the row (it overpaints them) or when there's no chrome, otherwise it carries #footer_text embedded at its own width — keeping the text's own styling — with dashes filling the remainder up to the inner width.

@param inner_w — the border's interior width.

@param fg — the active-border color, or nil when inactive.

Parameters:

  • inner_w (Integer)
  • fg (Color, nil)

Returns:



181
182
183
184
185
186
187
188
189
190
# File 'lib/tuile/component/window.rb', line 181

def bottom_border(inner_w, fg)
  interior =
    if footer || @footer_text.empty?
      StyledString.styled("─" * inner_w, fg: fg)
    else
      embedded = @footer_text.slice(0, inner_w)
      embedded + StyledString.styled("─" * (inner_w - embedded.display_width), fg: fg)
    end
  StyledString.styled("└", fg: fg) + interior + StyledString.styled("┘", fg: fg)
end

#caption ⇒ StyledString

Read through this method, never @caption — the ivar stays nil until the first non-empty set (#caption= short-circuits when unchanged).

@return — the caption; empty when never set.

Returns:



6952
# File 'sig/tuile.rbs', line 6952

def caption: () -> StyledString

#caption= ⇒ void

This method returns an undefined value.

Sets the caption and invalidates the component. No-op when unchanged. A String is parsed via StyledString.parse (embedded ANSI is honored); a StyledString is used as-is; nil clears it.

@param new_caption

Parameters:



6959
# File 'sig/tuile.rbs', line 6959

def caption=: ((String | StyledString)? new_caption) -> void

#content_rect ⇒ Rect

The interior the content fills: inside the border on three sides, and on the fourth only while there is a right border — #scrollbar= drops it so the content's own bar takes that column.

One rect for both jobs, since a window paints and places its children in the same coordinates: it is what #relayout assigns the content, and what #repaint blanks when there is no content to cover it.

@return — may be empty, for a window too small to have an inside.

Returns:



128
129
130
# File 'lib/tuile/component/window.rb', line 128

def content_rect
  Rect.new(1, 1, rect.width - 1 - @border_right, rect.height - 2)
end

#focusable? ⇒ Boolean

Returns:

  • (Boolean)


32
# File 'lib/tuile/component/window.rb', line 32

def focusable? = true

@return — optional focusable component occupying the bottom border row, always spanning the full inner width.

Returns:



36
# File 'lib/tuile/component/window.rb', line 36

def footer = @footer_slot.content

#footer=(new_footer) ⇒ void

This method returns an undefined value.

Mounts a component in the bottom border row, spanning the full inner width and positioned automatically; nil removes it.

Precedence: a footer component present hides #footer_text; absent, the text embeds into the bottom border. No window needs both at once.

@param new_footer

Parameters:



67
68
69
70
71
# File 'lib/tuile/component/window.rb', line 67

def footer=(new_footer)
  @footer_slot.content = new_footer
  invalidate_layout
  invalidate # repaint border row that the footer covers/uncovers
end

#handle_focus ⇒ void

This method returns an undefined value.



6964
# File 'sig/tuile.rbs', line 6964

def handle_focus: () -> void

#inspect_details ⇒ ::Array[String]

Adds caption="…" to Tuile::Component#inspect, omitted while empty.

Returns:

  • (::Array[String])


6962
# File 'sig/tuile.rbs', line 6962

def inspect_details: () -> ::Array[String]

This method returns an undefined value.

Positions the footer slot over the bottom border row, spanning the full inner width (the only dimension a bottom-row widget needs — the window already knows it).

An unoccupied slot gets an empty rect, not the row — a Slot clears whatever it is given, which would blank the border underneath.



201
202
203
204
205
206
207
208
209
# File 'lib/tuile/component/window.rb', line 201

def layout_footer
  if footer.nil? || rect.empty?
    @footer_slot.rect = Rect.new(0, 0, 0, 0)
    return
  end

  width = [rect.width - 2, 0].max
  @footer_slot.rect = Rect.new(1, rect.height - 1, width, 1)
end

#relayout ⇒ void

This method returns an undefined value.

The content takes the interior, the footer slot the bottom border row.



114
115
116
117
# File 'lib/tuile/component/window.rb', line 114

def relayout
  content&.rect = content_rect
  layout_footer
end

#repaint(canvas) ⇒ void

This method returns an undefined value.

Fully repaints the window: the border ring here, the interior through the content and footer it re-invalidates.

Deliberately not super: the default would blank the whole rect first, because the content slot is inset by the border and so never tiles — and every one of those blanked border cells is one this method is about to repaint identically, which marks it dirty and makes Buffer#flush re-emit it. That cost 925 bytes on every unchanged repaint of an 80×25 window, paid on each focus change (D_component_contract). The ring is this window's own paint and the interior is the content's, so the only cell nobody covers is an interior with no content in it — cleared here, exactly.

@param canvas — see Tuile::Component#repaint.

Parameters:



101
102
103
104
105
106
107
108
# File 'lib/tuile/component/window.rb', line 101

def repaint(canvas)
  return if rect.empty?

  inner = content_rect
  canvas.fill(inner) if content.nil? && !inner.empty?
  invalidate_children
  repaint_border(canvas)
end

#repaint_border(canvas) ⇒ void

This method returns an undefined value.

Paints the window border via Tuile::Canvas#set_text / Tuile::Canvas#set_char, so the border cells inherit Tuile::ComponentBackground#effective — a Tuile::Component#bg_color on the window tints border and content alike. Both border rows are clipped by display width, so no caption overflows the box; when the window is active the whole border — the caption's own colors included — is drawn in Theme#active_border_color.

@param canvas — the surface to paint onto.

Parameters:



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/tuile/component/window.rb', line 140

def repaint_border(canvas)
  return if rect.empty?

  w = rect.width
  h = rect.height
  inner_w = [w - 2, 0].max

  fg = active? ? screen.theme.active_border_color : nil
  bar = StyledString::Style.new(fg: fg)
  canvas.set_text(0, 0, top_border(inner_w, fg).slice(0, w))
  (1..(h - 2)).each do |row|
    canvas.set_char(0, row, "│", bar)
    # Skipped once {#scrollbar=} has given that column to the content: the
    # bar would paint over the border anyway, and painting it first only
    # dirties the column into every frame's diff (`D_component_contract`).
    canvas.set_char(w - 1, row, "│", bar) if @border_right.positive?
  end
  canvas.set_text(0, h - 1, bottom_border(inner_w, fg).slice(0, w)) if h >= 2
end

#scrollbar=(value) ⇒ void

This method returns an undefined value.

@param value

Parameters:

  • value (Boolean)


75
76
77
78
79
80
81
82
83
84
85
# File 'lib/tuile/component/window.rb', line 75

def scrollbar=(value)
  unless content.respond_to?(:scrollbar_visibility=)
    raise Tuile::Error,
          "scrollbar= requires a content component that supports scrollbar_visibility=, got #{content.inspect}"
  end

  content.scrollbar_visibility = value ? :visible : :gone
  @border_right = value ? 0 : 1
  invalidate
  invalidate_layout
end

#top_border(inner_w, fg) ⇒ StyledString

Builds the top border row: corners, #caption embedded at its own width, dashes filling the remainder. The caption keeps its own styling unless fg is set — an active window's border claims it.

@param inner_w — the border's interior width.

@param fg — the active-border color, or nil when inactive.

Parameters:

  • inner_w (Integer)
  • fg (Color, nil)

Returns:



166
167
168
169
170
171
# File 'lib/tuile/component/window.rb', line 166

def top_border(inner_w, fg)
  title = caption.slice(0, inner_w)
  title = title.with_fg(fg) if fg
  dashes = StyledString.styled("─" * (inner_w - title.display_width), fg: fg)
  StyledString.styled("┌", fg: fg) + title + dashes + StyledString.styled("┐", fg: fg)
end