diff --git a/lib/bbs/application.rb b/lib/bbs/application.rb index dfd7ce3..505807c 100644 --- a/lib/bbs/application.rb +++ b/lib/bbs/application.rb @@ -236,6 +236,7 @@ module BBS @next = FrameBuffer.new(c, r) relayout! + @windows.each { |w| w.relayout(self) } if resized @next.fill(x: 1, y: 1, width: c, height: r, sgr: @theme[:screen]) @root.render(@next) diff --git a/lib/bbs/window.rb b/lib/bbs/window.rb index d6d8aff..bf8bb64 100644 --- a/lib/bbs/window.rb +++ b/lib/bbs/window.rb @@ -98,5 +98,24 @@ module BBS # Called once per Application idle tick (~1s). Override in subclasses that # need to poll for asynchronous updates (e.g. BBS::Windows::Stream). 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 diff --git a/lib/bbs/windows/button_bar.rb b/lib/bbs/windows/button_bar.rb index f2b83fe..eed03f4 100644 --- a/lib/bbs/windows/button_bar.rb +++ b/lib/bbs/windows/button_bar.rb @@ -32,6 +32,24 @@ module BBS BBS::Widgets::Button.new(label: d[:label], on_click: build_handler(window, app, d)) 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 aligns = descriptors.map { |d| d[:align] || default_align } @@ -68,16 +86,6 @@ module BBS widgets[i].layout(x, row_y, bw, 1) x += bw + 1 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 def button_width(button) diff --git a/lib/bbs/windows/master_detail.rb b/lib/bbs/windows/master_detail.rb index 3b8e9ae..df3675c 100644 --- a/lib/bbs/windows/master_detail.rb +++ b/lib/bbs/windows/master_detail.rb @@ -81,47 +81,68 @@ module BBS 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 def build - 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) - - 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 + place_window @list_widget = BBS::Widgets::ListBox.new( 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) - detail_x = list_x + list_w + 2 - detail_w = bounds.width - list_w - 5 @detail_view = BBS::Widgets::ScrollView.new(lines: []) - @detail_view.layout(detail_x, list_y, detail_w, panel_h) add(@detail_view) - if has_meta - meta_y = bounds.y + h - (has_buttons ? 3 : 2) + unless @bottom_meta.nil? @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) end @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