Relayout open windows on terminal resize
NAWS mid-session updates now propagate to the window stack: Application re-runs each open window's relayout on resize. Window#relayout re-centers (clamped to the screen edge) by shifting all descendants; MasterDetail recomputes its full geometry and re-renders the detail at the new width. ButtonBar placement is extracted into ButtonBar.place for reuse. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -236,6 +236,7 @@ module BBS
|
|||||||
@next = FrameBuffer.new(c, r)
|
@next = FrameBuffer.new(c, r)
|
||||||
|
|
||||||
relayout!
|
relayout!
|
||||||
|
@windows.each { |w| w.relayout(self) } if resized
|
||||||
|
|
||||||
@next.fill(x: 1, y: 1, width: c, height: r, sgr: @theme[:screen])
|
@next.fill(x: 1, y: 1, width: c, height: r, sgr: @theme[:screen])
|
||||||
@root.render(@next)
|
@root.render(@next)
|
||||||
|
|||||||
@@ -98,5 +98,24 @@ module BBS
|
|||||||
# Called once per Application idle tick (~1s). Override in subclasses that
|
# Called once per Application idle tick (~1s). Override in subclasses that
|
||||||
# need to poll for asynchronous updates (e.g. BBS::Windows::Stream).
|
# need to poll for asynchronous updates (e.g. BBS::Windows::Stream).
|
||||||
def tick(app); end
|
def tick(app); end
|
||||||
|
|
||||||
|
# Called by the Application when the terminal is resized. The default
|
||||||
|
# keeps the window's size and re-centers it, shifting every descendant
|
||||||
|
# along. Subclasses whose geometry depends on the terminal size override
|
||||||
|
# this (e.g. BBS::Windows::MasterDetail).
|
||||||
|
def relayout(app)
|
||||||
|
dx = [(app.cols - bounds.width) / 2 + 1, 1].max - bounds.x
|
||||||
|
dy = [(app.rows - bounds.height) / 2 + 1, 1].max - bounds.y
|
||||||
|
shift(dx, dy)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Move the window and all its descendants by (dx, dy).
|
||||||
|
def shift(dx, dy)
|
||||||
|
return if dx.zero? && dy.zero?
|
||||||
|
walk do |w|
|
||||||
|
b = w.bounds
|
||||||
|
w.bounds = Widget::Rect.new(b.x + dx, b.y + dy, b.width, b.height)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -32,6 +32,24 @@ module BBS
|
|||||||
BBS::Widgets::Button.new(label: d[:label], on_click: build_handler(window, app, d))
|
BBS::Widgets::Button.new(label: d[:label], on_click: build_handler(window, app, d))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
place(window, widgets, descriptors)
|
||||||
|
|
||||||
|
widgets.each { |w| window.add(w) }
|
||||||
|
|
||||||
|
primary_idx = descriptors.index { |d| d[:primary] }
|
||||||
|
primary = primary_idx ? widgets[primary_idx] : nil
|
||||||
|
|
||||||
|
by_key = {}
|
||||||
|
descriptors.each_with_index { |d, i| by_key[d[:key]] = widgets[i] if d[:key] }
|
||||||
|
|
||||||
|
{ widgets: widgets, primary: primary, by_key: by_key }
|
||||||
|
end
|
||||||
|
|
||||||
|
# Position (or re-position) already-built bar widgets against the
|
||||||
|
# window's current bounds. Used by attach and by window relayouts.
|
||||||
|
def place(window, widgets, descriptors)
|
||||||
|
return if widgets.empty?
|
||||||
|
|
||||||
default_align = descriptors.length == 1 ? :center : :right
|
default_align = descriptors.length == 1 ? :center : :right
|
||||||
aligns = descriptors.map { |d| d[:align] || default_align }
|
aligns = descriptors.map { |d| d[:align] || default_align }
|
||||||
|
|
||||||
@@ -68,16 +86,6 @@ module BBS
|
|||||||
widgets[i].layout(x, row_y, bw, 1)
|
widgets[i].layout(x, row_y, bw, 1)
|
||||||
x += bw + 1
|
x += bw + 1
|
||||||
end
|
end
|
||||||
|
|
||||||
widgets.each { |w| window.add(w) }
|
|
||||||
|
|
||||||
primary_idx = descriptors.index { |d| d[:primary] }
|
|
||||||
primary = primary_idx ? widgets[primary_idx] : nil
|
|
||||||
|
|
||||||
by_key = {}
|
|
||||||
descriptors.each_with_index { |d, i| by_key[d[:key]] = widgets[i] if d[:key] }
|
|
||||||
|
|
||||||
{ widgets: widgets, primary: primary, by_key: by_key }
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def button_width(button)
|
def button_width(button)
|
||||||
|
|||||||
@@ -81,47 +81,68 @@ module BBS
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Terminal resized: recompute the whole geometry against the new size
|
||||||
|
# and re-render the detail at the new pane width.
|
||||||
|
def relayout(_app = @app)
|
||||||
|
place_window
|
||||||
|
place_panes
|
||||||
|
ButtonBar.place(self, @bar[:widgets], @actions)
|
||||||
|
reload_detail
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def build
|
def build
|
||||||
w = @width || @app.cols - 4
|
place_window
|
||||||
h = @height || @app.rows - 6
|
|
||||||
x = (@app.cols - w) / 2 + 1
|
|
||||||
y = (@app.rows - h) / 2 + 1
|
|
||||||
layout(x, y, w, h)
|
|
||||||
|
|
||||||
has_buttons = !@actions.empty?
|
|
||||||
has_meta = !@bottom_meta.nil?
|
|
||||||
panel_h = h - 4 - (has_meta ? 1 : 0) - (has_buttons ? 1 : 0)
|
|
||||||
panel_h = [panel_h, 3].max
|
|
||||||
|
|
||||||
list_w = @list_width || [(w / 3).clamp(20, 40), w - 10].min
|
|
||||||
list_x = bounds.x + 2
|
|
||||||
list_y = bounds.y + 2
|
|
||||||
|
|
||||||
@list_widget = BBS::Widgets::ListBox.new(
|
@list_widget = BBS::Widgets::ListBox.new(
|
||||||
items: @items,
|
items: @items,
|
||||||
label: ->(item) { " #{@item_label.call(item)}".ljust(list_w) }
|
label: ->(item) { " #{@item_label.call(item)}".ljust(@list_w || 0) }
|
||||||
)
|
)
|
||||||
@list_widget.layout(list_x, list_y, list_w, panel_h)
|
|
||||||
add(@list_widget)
|
add(@list_widget)
|
||||||
|
|
||||||
detail_x = list_x + list_w + 2
|
|
||||||
detail_w = bounds.width - list_w - 5
|
|
||||||
@detail_view = BBS::Widgets::ScrollView.new(lines: [])
|
@detail_view = BBS::Widgets::ScrollView.new(lines: [])
|
||||||
@detail_view.layout(detail_x, list_y, detail_w, panel_h)
|
|
||||||
add(@detail_view)
|
add(@detail_view)
|
||||||
|
|
||||||
if has_meta
|
unless @bottom_meta.nil?
|
||||||
meta_y = bounds.y + h - (has_buttons ? 3 : 2)
|
|
||||||
@meta_label = BBS::Widgets::Label.new(text: '', style_key: :input_label)
|
@meta_label = BBS::Widgets::Label.new(text: '', style_key: :input_label)
|
||||||
@meta_label.layout(bounds.x + 2, meta_y, bounds.width - 4, 1)
|
|
||||||
add(@meta_label)
|
add(@meta_label)
|
||||||
end
|
end
|
||||||
|
|
||||||
@list_widget.on_select = ->(_, _) { reload_detail }
|
@list_widget.on_select = ->(_, _) { reload_detail }
|
||||||
|
|
||||||
ButtonBar.attach(self, @app, @actions)
|
@bar = ButtonBar.attach(self, @app, @actions)
|
||||||
|
place_panes
|
||||||
|
end
|
||||||
|
|
||||||
|
def place_window
|
||||||
|
w = @width || @app.cols - 4
|
||||||
|
h = @height || @app.rows - 6
|
||||||
|
x = (@app.cols - w) / 2 + 1
|
||||||
|
y = (@app.rows - h) / 2 + 1
|
||||||
|
layout(x, y, w, h)
|
||||||
|
end
|
||||||
|
|
||||||
|
def place_panes
|
||||||
|
h = bounds.height
|
||||||
|
has_buttons = !@actions.empty?
|
||||||
|
has_meta = !@meta_label.nil?
|
||||||
|
panel_h = h - 4 - (has_meta ? 1 : 0) - (has_buttons ? 1 : 0)
|
||||||
|
panel_h = [panel_h, 3].max
|
||||||
|
|
||||||
|
@list_w = @list_width || [(bounds.width / 3).clamp(20, 40), bounds.width - 10].min
|
||||||
|
list_x = bounds.x + 2
|
||||||
|
list_y = bounds.y + 2
|
||||||
|
@list_widget.layout(list_x, list_y, @list_w, panel_h)
|
||||||
|
|
||||||
|
detail_x = list_x + @list_w + 2
|
||||||
|
detail_w = bounds.width - @list_w - 5
|
||||||
|
@detail_view.layout(detail_x, list_y, detail_w, panel_h)
|
||||||
|
|
||||||
|
if @meta_label
|
||||||
|
meta_y = bounds.y + h - (has_buttons ? 3 : 2)
|
||||||
|
@meta_label.layout(bounds.x + 2, meta_y, bounds.width - 4, 1)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user