Files
rubbs/lib/bbs/windows/info.rb
2026-05-13 20:35:46 +02:00

213 lines
6.6 KiB
Ruby

# frozen_string_literal: true
require_relative '../window'
require_relative '../widgets'
require_relative '../frame_buffer'
require_relative 'button_bar'
module BBS
module Windows
# A centered modal dialog for messages, splash screens, info displays.
#
# BBS::Windows::Info.open(app,
# title: ' Welcome ',
# width: 70, height: 17,
# body: [
# { text: banner_line, style: :accent },
# "plain label with optional \e[1;33mraw ANSI\e[0m",
# :spacer,
# :rule,
# { kv: [['Online users', '5'], ['Ruby', RUBY_VERSION]] },
# ],
# buttons: [{ label: '&Continue', primary: true }]
# )
#
# When `scroll: true`, body items are flattened into a single ScrollView
# filling the inner area. Use this for long, paginated content lists.
#
# Body item types:
# String → label with :window_text style
# :spacer | '' | nil → blank row
# :rule → horizontal ─── line
# { text:, style:, align: } → styled label
# { kv: [[k, v], ...], label_width: } → key/value rows
# { widget:, height: } → arbitrary widget (non-scroll only)
# BBS::Widget → as-is, 1 row (non-scroll only)
#
# Default buttons: a single centered `[ &Close ]` if `buttons:` is nil.
class Info < Window
DEFAULT_BUTTON = [{ label: '&Close', cancel: true }].freeze
DEFAULT_PADDING = { sides: 2, top: 1, bottom: 2 }.freeze
def self.open(app, **kw)
new(app: app, **kw).tap(&:attach)
end
attr_reader :scroll_view
def initialize(app:, title:, body: [], buttons: nil, width: 60, height: nil,
scroll: false, padding: nil)
super(title: title, modal: true)
self.theme = app.theme
@app = app
@body = body
@buttons = buttons.nil? ? DEFAULT_BUTTON.dup : Array(buttons)
@scroll = scroll
@width = width
@height = height
@padding = DEFAULT_PADDING.merge(padding || {})
build
end
def attach
@app.open_window(self)
reset_focus
focus_manager.focus(@primary_button) if @primary_button
self
end
private
def build
button_h = @buttons.any? ? 2 : 0
body_rows = @scroll ? 10 : @body.sum { |it| item_rows(it) }
h = @height || (@padding[:top] + body_rows + button_h + @padding[:bottom])
w = [@width, @app.cols - 4].min
h = [h, @app.rows - 4].min
x = (@app.cols - w) / 2 + 1
y = (@app.rows - h) / 2 + 1
layout(x, y, w, h)
inner_w = w - @padding[:sides] * 2
inner_x = bounds.x + @padding[:sides]
inner_y = bounds.y + @padding[:top]
body_h = h - @padding[:top] - @padding[:bottom] - button_h
body_h = [body_h, 0].max
if @scroll
place_scroll_body(inner_x, inner_y, inner_w, body_h)
else
place_static_body(inner_x, inner_y, inner_w)
end
@primary_button = ButtonBar.attach(self, @app, @buttons)[:primary]
end
def item_rows(item)
case item
when :spacer, :rule, '', nil then 1
when String then 1
when Hash
return item[:kv].length if item[:kv]
return item[:height] || 1 if item[:widget]
1
when BBS::Widget
h = item.bounds.height
h.positive? ? h : 1
else 1
end
end
def place_scroll_body(x, y, w, h)
lines = []
@body.each { |item| append_scroll_lines(lines, item, w) }
@scroll_view = BBS::Widgets::ScrollView.new(lines: lines)
@scroll_view.layout(x, y, w, h)
add(@scroll_view)
end
def append_scroll_lines(lines, item, width)
base = theme[:text]
case item
when :spacer, '', nil
lines << ''
when :rule
lines << "#{theme[:label]}#{'─' * width}#{base}"
when String
lines << item
when Hash
if item[:kv]
kw = item[:label_width] || default_kv_label_width(item[:kv])
item[:kv].each do |k, v|
lines << "#{theme[:input_label]}#{k.to_s.ljust(kw)}#{base} #{v}"
end
elsif item[:text]
sgr = item[:style] ? theme[item[:style]] : base
lines << "#{sgr}#{item[:text]}#{base}"
end
when BBS::Widget
raise ArgumentError, 'scroll mode does not support widget body items'
end
end
def place_static_body(x, y, w)
row = 0
@body.each { |item| row += place_static_item(item, x, y + row, w) }
end
def place_static_item(item, x, y, w)
case item
when :spacer, '', nil
1
when :rule
label = BBS::Widgets::Label.new(text: '─' * w, style_key: :label)
label.layout(x, y, w, 1)
add(label)
1
when String
label = BBS::Widgets::Label.new(text: item, style_key: :window_text)
label.layout(x, y, w, 1)
add(label)
1
when Hash
place_static_hash(item, x, y, w)
when BBS::Widget
h = item.bounds.height.positive? ? item.bounds.height : 1
item.layout(x, y, w, h)
add(item)
h
else
1
end
end
def place_static_hash(item, x, y, w)
if item[:kv]
kw = item[:label_width] || default_kv_label_width(item[:kv])
item[:kv].each_with_index do |(k, v), i|
label = BBS::Widgets::Label.new(text: k.to_s.ljust(kw), style_key: :input_label)
label.layout(x, y + i, kw, 1)
add(label)
value = BBS::Widgets::Label.new(text: v.to_s, style_key: :window_text)
value.layout(x + kw + 2, y + i, w - kw - 2, 1)
add(value)
end
item[:kv].length
elsif item[:widget]
wd = item[:widget]
h = item[:height] || (wd.bounds.height.positive? ? wd.bounds.height : 1)
wd.layout(x, y, w, h)
add(wd)
h
elsif item[:text]
label = BBS::Widgets::Label.new(
text: item[:text],
style_key: item[:style] || :window_text,
align: item[:align] || :left
)
label.layout(x, y, w, 1)
add(label)
1
else
1
end
end
def default_kv_label_width(pairs)
max = pairs.map { |k, _| k.to_s.length }.max || 0
[max + 1, 16].min.clamp(8, 16)
end
end
end
end