EN /HU | Belépés

Widgetek

A rubbs BBS::Application építőkövei: a BBS::Widget alaposztály, a BBS::Widgets összes widgetje (Label, Button, TextInput, TextArea, Checkbox, ListBox, ScrollView, és a rajzokhoz a Scene), a Container, és egy teljes saját widget. A window-k a Window-k oldalon vannak, maga az application az Application oldalon, a színek és a framebuffer a Rajzolás és témák oldalon. Minden a rubbs 1.0.0 forrásával egyeztetve. Az 1.0-ból kikerültek azok a widgetek, amelyeket egy board sem használt: a ProgressBar, a Spinner, a HBox, a VBox, a Panel és az AnsiArt (a rajz mostantól BBS::Art és Widgets::Scene, lásd Rajzok és grafikus képernyők); a widgeteket egy BBS::Window-ban kell elrendezni.

Hogyan működnek a widgetek

A widget egy Ruby-objektum, amely tudja, hol a helye a képernyőn, kirajzolja magát egy BBS::FrameBuffer-be, és ha fókuszálható, válaszol a billentyű- és egéreseményekre. Az application minden esemény után egy friss framebufferbe rajzolja újra az egész képernyőt, és csak a megváltozott cellákat küldi ki (lásd Rajzolás és témák), így a widget soha nem ír közvetlenül a vonalra: csak a saját állapotát módosítja, és kérésre kirajzolja azt.

Ami minden widgetre igaz:

  • A koordináták 1-től számozottak és abszolútak. A layout(x, y, width, height) képernyőoszlopot és -sort vár, az (1, 1) a terminál bal felső sarka. A window-n belüli widgetet is képernyő-koordinátákkal helyezed el, nem a window-hoz képest. Ha a window elmozdul, az összes leszármazottját magával viszi.
  • Nincs automatikus elrendezés. Minden widgeten magad hívod a layout-ot, jellemzően a window saját pozíciójából vagy az application cols és rows értékéből számolva.
  • A színek szerepnév alapján jönnek a témából. A widget egy szerepet kér, például :button_focused-ot, a téma pedig ANSI SGR-szekvenciává alakítja (lásd Rajzolás és témák).
  • A fókuszt egy BBS::FocusManager kezeli window-nként (és egy az asztalon): a Tab és a Shift-Tab a hozzáadás sorrendjében lépteti a fókuszálható, látható és engedélyezett widgeteket, és billentyűt csak a fókuszban lévő widget kap.
  • A handle_event ítéletet ad vissza: a :handled megállítja az eseményt, a :halt kilép az applicationből, a nil (vagy false) továbbengedi a következő kezelőhöz.

BBS::Widget

A BBS::Widget (lib/bbs/widget.rb) minden megjelenő elem alaposztálya. Önmagában semmit nem rajzol, és fókuszt sem kap.

RUBY
BBS::Widget.new(id: nil, bounds: nil, theme: nil)
Opció Alapérték Jelentés
id: nil Bármilyen érték, többnyire Symbol, hogy a Container#find(id) később megtalálja a widgetet.
bounds: Rect.new(1, 1, 0, 0) A kezdeti téglalap. A nulla szélesség azt jelenti, hogy nincs mit rajzolni; a legtöbb kód kihagyja, és inkább layout-ot hív.
theme: nil Egy BBS::Theme. Általában kimarad: az application vagy a window állítja be, amikor a widget bekerül.

Attribútumok

Attribútum Hozzáférés Alapérték Jelentés
bounds olvasható/írható lásd fent Egy BBS::Widget::Rect x, y, width, height mezőkkel.
parent olvasható/írható nil A befoglaló Container; a Container#add állítja be.
id olvasható/írható nil A Container#find keresési kulcsa.
visible olvasható/írható true A láthatatlan widget nem rajzolódik ki, és nem is fókuszálható.
focused olvasható/írható false A fókuszkezelő állítja. A widgetek ebből tudják, hogy a fókuszált stílust kell-e használniuk.
enabled olvasható/írható true A letiltott widgetet a Tab átugorja, nem fogad inputot, a gomb pedig szürkén rajzolódik.
theme olvasható (theme= az írás) nil A widget saját témája; lásd lent a „Téma keresése” részt.

A BBS::Widget::Rect egy Struct, contains?(px, py) metódussal (igaz, ha a pont belül van) és egy to_s-sel, például #2,2 8x1.

Metódusok

Metódus Mit csinál
layout(x, y, width, height) Beállítja a bounds-ot. Az a widget, amely a saját részeit rendezi el, felülírhatja.
render(frame) Kirajzolja a widgetet egy BBS::FrameBuffer-be. Az alaposztályban üres.
handle_event(event) Válaszol egy BBS::Event-re. Az alaposztályban nil-t ad.
focusable? Az alaposztályban false; az interaktív widgetek true-t adnak.
on_focus / on_blur A fókuszkezelő hívja, amikor a widget megkapja vagy elveszti a fókuszt. Alapból üres.
theme=(theme) Beállítja a témát. Container-en minden gyereknek továbbadja.
hit_test(x, y) Saját magát adja vissza, ha a pont a bounds-on belül van, különben nil-t. A konténerek a legmélyebb gyereket adják.
each { ... } Saját magát adja át a blokknak (a konténerek a walk-kal járják be a fájukat).
redraw! Továbbadja a kérést a szülőnek. Az application úgyis újrarajzol minden esemény után, így ritkán kell.
style(key) Protected: egy témaszerep SGR-stringje, a render-ben használatos.

Téma keresése

A style(key) a widget saját témáját használja, vagy ha az nil, a szülőjéét. Csak egy szintet néz fel. Az application abban a pillanatban adja oda a témát az egész fának, amikor átadod neki: az asztalon az add(widget) és az open_window(window) is theme=-t hív, a Container#theme= pedig minden akkor létező gyereknek továbbadja. Ezért előbb építsd fel az egész fát, és csak utána add hozzá. Ha a window megnyitása után kerül be egy widget két szint mélyre, nem kap stílust (a terminál alapszíneivel jelenik meg), hacsak magad nem hívod meg rajta a widget.theme = app.theme-et.

RUBY
require 'bbs'

win = BBS::Window.new
win.theme = BBS::Theme.synchronet
box = BBS::Container.new
win.add(box)                      # a box-nak nincs saját témája, a szülőjének van
label = BBS::Widgets::Label.new(text: 'late')
box.add(label)                    # két szint mélyen, a theme= után
p label.send(:style, :text)       # => nil, a terminál alapszíneivel rajzolódik
label.theme = win.theme           # a megoldás
p label.send(:style, :text)       # => "\e[0;37;40m"

Események

A handle_event egy BBS::Event-et kap:

Mező / metódus Jelentés
kind A widgetekhez :key vagy :mouse jut el.
key A nevesített billentyűk Symbolok (:up, :down, :left, :right, :home, :end, :page_up, :page_down, :enter, :backspace, :delete, :tab, :shift_tab, :escape, :f1… , :alt_x, :shift_up, :ctrl_left…), a nyomtatható billentyű egykarakteres String.
key?, mouse? A fajta.
char? Igaz, ha a billentyű egykarakteres String.
alt?, alt_char Igaz egy :alt_x billentyűre, és a betűje ('x').
mouse Egy BBS::Telnet::MouseEvent: kind (:press, :release, :move, :wheel), button (:left, :middle, :right, görgőnél :up/:down), x, y és mods.
x, y Az egér pozíciója, 1-től számozott képernyő-koordinátákban.

Tesztben kézzel is gyárthatsz eseményt: BBS::Event.key(:down), BBS::Event.key('a'), BBS::Event.mouse(BBS::Telnet::MouseEvent.new(kind: :press, button: :left, x: 3, y: 1)).

Hová kerülnek a widgetek

Window-ba

A widgetek többsége egy BBS::Window-ban él. Létrehozod a window-t, elhelyezed, hozzáadod a widgeteket a képernyőpozíciójukkal, meghívod a reset_focus-t, hogy a window fókuszkezelője lássa őket, és megnyitod. Erre jó hely egy helper metódus az applicationön. A kész window-típusok (Info, Form, MasterDetail, Stream) maguk építik fel a widgetjeiket; lásd Window-k.

RUBY
require 'bbs'

APP = BBS::Application.define do
  theme BBS::Theme.synchronet
  status_hint 'Esc', 'Close'

  helpers do
    def settings_window
      win = BBS::Window.new(title: ' Settings ')
      win.layout(5, 3, 40, 11)

      label = BBS::Widgets::Label.new(text: 'Handle:', style_key: :input_label)
      label.layout(7, 5, 9, 1)
      win.add(label)

      handle = BBS::Widgets::TextInput.new(id: :handle, placeholder: 'Your handle', max_length: 20)
      handle.layout(17, 5, 24, 1)
      win.add(handle)

      ansi = BBS::Widgets::Checkbox.new(id: :ansi, label: 'ANSI colours', checked: true)
      ansi.layout(7, 7, 30, 1)
      win.add(ansi)

      themes = BBS::Widgets::ListBox.new(id: :theme, items: %i[synchronet nortoncommander mono])
      themes.layout(7, 9, 20, 3)
      win.add(themes)

      save = BBS::Widgets::Button.new(label: '&Save', on_click: lambda { |_btn|
        context[:handle] = win.find(:handle).value
        close_window(win)
      })
      save.layout(30, 11, 12, 1)
      win.add(save)

      win.reset_focus
      win
    end
  end

  init do
    title = BBS::Widgets::Label.new(text: 'My BBS', align: :center, style_key: :accent)
    title.layout(1, 1, cols, 1)
    add(title)
    open_window(settings_window)
  end
end

Fejetlenül, 50×15-ös képernyőn futtatva, a tasi begépelése, Tab és Szóköz (ez kiveszi a pipát) után így néz ki. A keret élénkfehér kéken, az aktív window címe fekete világosszürkén, a Handle: cián, a beviteli mező és a lista fehér kéken, a gomb fekete világosszürkén, piros S-sel:

TEXT
                      My BBS

    ╔═[■]═  Settings ═══════════════════1══╗
    ║                                      ║
    ║ Handle:   tasi                       ║
    ║                                      ║
    ║ [ ] ANSI colours                     ║
    ║                                      ║
    ║ synchronet                           ║
    ║ nortoncommander                      ║
    ║ mono                     [ Save ]    ║
    ║                                      ║
    ╚══════════════════════════════════════╝

 Esc Close

Az Alt-S (vagy az S, ha a fókuszban lévő widget nem fogad betűt) megnyomja a gombot, a context[:handle]-be teszi a tasi-t, és bezárja a window-t.

Ha egy callback be akarja zárni a window-t, maga hívja a close_window(win)-t. A Button on_click-jének eredményéből :handled (vagy :halt) lesz, így ha :close_window-t ad vissza, annak nincs hatása.

Az asztalra

Az application init blokkjában (vagy bármely application-metódusból) add(widget)-del hozzáadott widgetek az asztalra kerülnek, a window-k mögötti területre. Ide tesz egy board üdvözlő artot, órát vagy egy nagy „főmenü” gombot:

RUBY
require 'bbs'

APP = BBS::Application.define do
  theme BBS::Theme.neumanntronics

  init do
    art = BBS::Widgets::Scene.new(art: BBS::Art.load('art/desktop.yml'), align: :center, valign: :middle)
    art.layout(1, 2, cols, rows - 2)
    add(art)

    open_menu = BBS::Widgets::Button.new(label: '&Menu', on_click: ->(_) { open_window(:main) })
    open_menu.layout(cols - 12, rows - 2, 10, 1)
    add(open_menu)
  end
end

Az asztal úgy működik, mint egy keret nélküli window, három eltéréssel (a forrással ellenőrizve):

  • Csak akkor kap billentyűt, ha nincs nyitva window (mindig a legfelső window kapja az első esélyt, és a Tabot is ő viszi).
  • Egy kattintás csak a fókuszban lévő asztali widgethez jut el; nincs hit test. Egy második gombra kattintva semmi sem történik, amíg a Tab oda nem vitte a fókuszt.
  • A gombok hotkeye (&M) csak window-n belül működik.

Fókusz és billentyűk window-n belül

A window ebben a sorrendben kezel egy billentyűt (BBS::Window#handle_event):

  1. A Tab és a Shift-Tab lépteti a fókuszt.
  2. A fókuszban lévő widget megkapja a billentyűt. Ha visszaad valamit, az az eredmény.
  3. Egy módosítóval lenyomott mozgatóbillentyűt, amit senki nem vitt el (Shift-Le, Ctrl-PgUp…), sima billentyűként újrapróbál, így a Shift-Le egy listában is lépteti a kijelölést.
  4. Egy betű vagy egy Alt-betű megnyomja azt az engedélyezett gombot, amelyiknek ez a hotkeye.
  5. A window close_key-e (alapból Esc) bezárja a window-t.

A bal kattintás a kurzor alatti widgetre teszi a fókuszt, aztán az esemény a fókuszban lévő widgethez megy. A görgőesemény a kurzor alatti widgethez jut akkor is, ha nincs rajta fókusz, így egy ScrollView vagy ListBox kattintás nélkül is görgethető.

Widgetek

Mind a BBS::Widgets modulban vannak, mindegyik a saját fájljában a lib/bbs/widgets/ mappában. Minden konstruktor elfogadja a BBS::Widget-től örökölt id:, bounds: és theme: opciót is.

Label

Egy sor szöveg, a szélességre vágva. Nem fókuszálható.

RUBY
BBS::Widgets::Label.new(text: '', align: :left, style_key: :text)
Opció Alapérték Jelentés
text: '' A szöveg. Bármi, aminek van to_s-e.
align: :left :left, :center vagy :right a szélességen belül.
style_key: :text A témaszerep, amivel rajzolódik, pl. :accent, :label, :error, :window_text, :input_label.

Mindhárom olvasható/írható attribútum, így a label helyben frissíthető (status.text = 'Saved'). A szöveg write_ansi-val íródik ki, tehát a beágyazott SGR-szekvenciák kiszínezhetik egy részét; a vágás és az igazítás viszont látható karakternek számolja az escape-karaktereket, így egy jobbra vagy középre igazított, escape-eket tartalmazó label túl balra kerül. A színezett labelt hagyd balra igazítva, vagy használj két labelt.

RUBY
require 'bbs'

l1 = BBS::Widgets::Label.new(text: 'left')
l2 = BBS::Widgets::Label.new(text: 'center', align: :center)
l3 = BBS::Widgets::Label.new(text: 'right', align: :right, style_key: :accent)
[l1, l2, l3].each_with_index { |l, i| l.layout(1, i + 1, 20, 1) }
TEXT
left
       center
               right

Button

Kattintható gomb, [ Label ] alakban, a szélességében középre téve, kitöltött háttérrel.

RUBY
BBS::Widgets::Button.new(label: 'OK', on_click: nil)
Opció Alapérték Jelentés
label: 'OK' A felirat. Az & jelöli a hotkey betűjét: a '&Save' [ Save ]-et rajzol, az S a :button_hotkey színében.
on_click: nil ->(button) { ... }. A :halt visszatérési érték kilép az applicationből; minden más kezeltnek számít.

Attribútumok: label, on_click (olvasható/írható). Metódusok: hotkey (a hotkey kisbetűsen, vagy nil), display_label (a felirat & nélkül), fire (úgy hívja meg az on_click-et, mint egy gombnyomás; letiltott gombon nem csinál semmit).

Viselkedés:

  • Billentyű: Enter vagy Szóköz, ha fókuszban van.
  • Hotkey: window-n belül a hotkey betűje magában vagy Alttal, ha a fókuszban lévő widget nem viszi el a billentyűt. A TextInput elviszi a sima betűket, így amíg szövegmező van fókuszban, Alt plusz a betű kell.
  • Egér: bal gombos lenyomás a gombon belül.
  • Stílusok: :button, :button_focused, :button_hotkey, :button_disabled (ha enabled = false; ilyenkor a hotkey nincs kiemelve).
RUBY
require 'bbs'

quit = BBS::Widgets::Button.new(label: '&Quit', on_click: ->(_) { :halt })
p quit.hotkey                                  # => "q"
p quit.display_label                           # => "Quit"
p quit.handle_event(BBS::Event.key(:enter))    # => :halt

ok = BBS::Widgets::Button.new(label: 'OK', on_click: ->(b) { puts "clicked #{b.label}" })
ok.handle_event(BBS::Event.key(' '))           # kiírja: "clicked OK", visszaad: :handled

Egy fókuszban lévő &Save gomb egy letiltott mellett (12-12 oszlop):

TEXT
  [ Save ]    [ Cancel ]

TextInput

Egysoros szövegmező kurzorral, vízszintes görgetéssel, placeholderrel és opcionális jelszómaszkkal.

RUBY
BBS::Widgets::TextInput.new(value: '', placeholder: '', max_length: 256,
                            on_submit: nil, on_change: nil, password: false)
Opció Alapérték Jelentés
value: '' A kezdő szöveg (másolatként).
placeholder: '' Szürke tipp (:text_dim), amíg a mező üres és nincs fókuszban.
max_length: 256 E hossz fölött a leütések elnyelődnek.
on_submit: nil ->(value, input) { ... } Enterre. Az eredményét adja vissza (ha az üres, :handled-et).
on_change: nil ->(value, input) { ... } minden szerkesztés után (gépelés, Backspace, Delete).
password: false A karakterek helyett *-ot rajzol.

Attribútumok: value (olvasható; a value= lecseréli a szöveget, és a végére teszi a kurzort), placeholder, max_length, on_submit, on_change, password (olvasható/írható).

Billentyűk: Bal, Jobb, Home, End mozgatja a kurzort; Backspace és Delete szerkeszt; Enter beküld; minden nyomtatható karakter beszúródik. A számára haszontalan billentyűkre (Esc, funkcióbillentyűk, Alt-betűk, Fel/Le) nil-t ad, így azok a window-hoz kerülnek: az Esc szövegmezőből is bezárja a window-t, az Alt-S is megnyomja a &Save-et. Egérkezelése nincs azon túl, hogy kattintásra a window ráteszi a fókuszt. Stílusok: :input, :input_focused, a placeholderhez :text_dim. A kurzor _.

RUBY
require 'bbs'

input = BBS::Widgets::TextInput.new(
  placeholder: 'Say something',
  on_change: ->(v, _) { puts "change #{v}" },
  on_submit: lambda { |v, field|
    puts "submit #{v}"
    field.value = ''        # üríti a következő sorhoz, a kurzor visszaáll 0-ra
    :handled
  }
)
input.layout(1, 1, 30, 1)
'hi'.each_char { |c| input.handle_event(BBS::Event.key(c)) }   # change h, change hi
p input.handle_event(BBS::Event.key(:escape))                   # => nil (továbbmegy a window-hoz)
input.handle_event(BBS::Event.key(:enter))                      # submit hi
p input.value                                                   # => ""

Egy üres, fókusz nélküli mező placeholderrel, és egy fókuszban lévő jelszómező secret tartalommal:

TEXT
Your name
******_

TextArea

Többsoros szerkesztő.

RUBY
BBS::Widgets::TextArea.new(value: '', max_length: 8_000, on_change: nil)
Opció Alapérték Jelentés
value: '' Kezdő szöveg; a sorokat "\n" választja el.
max_length: 8_000 Az összes karakter (sortörésekkel együtt); itt megáll a gépelés. Az Enter ezután is tör sort.
on_change: nil ->(value, area) { ... } gépelés, Enter vagy Backspace után.

Metódusok: value (a sorok "\n"-nel összefűzve), length. Az on_change olvasható/írható; value= nincs, a szöveg cseréjéhez új TextArea kell.

Billentyűk: a nyilak mozgatnak (a sor elején a Bal az előző sor végére visz, a sor végén a Jobb a következő sorra), Home/End a soron belül, az Enter a kurzornál kettévágja a sort, a Backspace töröl vagy összevonja az előző sorral, a nyomtatható karakterek beszúródnak. A nézet függőlegesen görgetve követi a kurzort; a hosszú sorokat levágja, nem tördeli és oldalra sem görgeti. Delete billentyű nincs, és Tab karakter sem (a Tab a fókuszt lépteti). Stílusok: :input, :input_focused.

RUBY
require 'bbs'

body = BBS::Widgets::TextArea.new(value: "one\ntwo")
body.layout(1, 1, 40, 6)
body.handle_event(BBS::Event.key(:end))     # a "one" vége
body.handle_event(BBS::Event.key(:enter))   # új üres sor utána
body.handle_event(BBS::Event.key('!'))
p body.value    # => "one\n!\ntwo"
p body.length   # => 9

Checkbox

Kapcsoló, [x] Label vagy [ ] Label alakban.

RUBY
BBS::Widgets::Checkbox.new(label: '', checked: false, on_change: nil)
Opció Alapérték Jelentés
label: '' A doboz utáni szöveg.
checked: false Kezdőállapot.
on_change: nil ->(checked, box) { ... } minden váltás után.

Attribútumok: label, checked, on_change (olvasható/írható). Szóköz vagy Enter vált. Kattintásra (a window-n keresztül) fókuszt kap, de nem vált. Stílusok: :text, fókuszban :button_focused.

RUBY
require 'bbs'

remember = BBS::Widgets::Checkbox.new(label: 'Remember me', checked: true,
                                      on_change: ->(on, _) { puts "checked=#{on}" })
remember.handle_event(BBS::Event.key(' '))   # kiírja: checked=false
TEXT
[x] Remember me

ListBox

Görgethető lista kijelöléssel.

RUBY
BBS::Widgets::ListBox.new(items: [], on_select: nil, on_activate: nil, label: nil)
Opció Alapérték Jelentés
items: [] Bármilyen objektumok.
label: nil ->(item) { ... }, ami egy sor szövegét adja; nil esetén item.to_s.
on_select: nil ->(item, list) { ... }, ha a kijelölés másik sorra kerül (billentyű, kattintás, görgő). Nem hívódik, ha helyben marad.
on_activate: nil ->(item, list) { ... } Enterre. Az eredményét adja vissza (ha az üres, :handled-et).

Attribútumok (olvasható/írható): items, selected (index, 0-ról indul), scroll (a legfelső látható sor indexe), on_select, on_activate, label. Metódus: selected_item.

Billentyűk: Fel, Le, PgUp és PgDn (a magasságnyit), Home, End mozgatja a kijelölést, és be is görgeti; Enter aktivál. Más billentyűre nil-t ad. Egér: a bal kattintás kijelöli az alatta lévő sort (nem aktivál, dupla kattintás nincs); a görgő három sorral lépteti a kijelölést. Stílusok: a sorokra :listbox, a kijelölt sorra :listbox_selected, ha a lista nincs fókuszban, és :listbox_focused, ha igen.

Ha lecseréled az items-et, a selected-et és a scroll-t magadnak kell visszaállítanod; a selected közvetlen beállítása nem görgeti be a sort. Üres listán az Enter nil-lel hívja az on_activate-et.

RUBY
require 'bbs'

list = BBS::Widgets::ListBox.new(
  items: %w[alpha beta gamma delta],
  label: ->(name) { name.upcase },
  on_select:   ->(item, _) { puts "selected #{item}" },
  on_activate: ->(item, _) { puts "activated #{item}"; :handled }
)
list.layout(1, 1, 20, 2)
list.handle_event(BBS::Event.key(:down))    # selected beta
list.handle_event(BBS::Event.key(:end))     # selected delta
p list.scroll                               # => 2, a nézet követte
list.handle_event(BBS::Event.key(:enter))   # activated delta
wheel = BBS::Telnet::MouseEvent.new(kind: :wheel, button: :up, x: 3, y: 1)
list.handle_event(BBS::Event.mouse(wheel))  # selected alpha

Ugyanez a lista három sor magasan, beta kijelölve (a sor teljes szélességében a kijelölés színével rajzolódik):

TEXT
ALPHA
BETA
GAMMA

Egy tipikus window-beli főlista Enterre megnyitja a részleteket:

RUBY
threads = BBS::Widgets::ListBox.new(
  items: services[:boards].threads(:general),
  label: ->(t) { "#{t.subject.ljust(30)} #{t.author}" },
  on_activate: ->(t, _) { open_window(:thread, thread: t) }
)

ScrollView

Csak olvasható, görgethető sorblokk görgetősávval. Ez jeleníti meg a Markdownt és a hosszú szövegeket.

RUBY
BBS::Widgets::ScrollView.new(lines: [])
Opció Alapérték Jelentés
lines: [] Stringek tömbje, soronként egy. A beágyazott ANSI SGR-szekvenciák megmaradnak; minden sor a szélességre vágódik.

Attribútumok (olvasható/írható): lines, scroll (a legfelső látható sor indexe). Fókuszálható.

Billentyűk: Fel, Le, PgUp, PgDn, Home, End. Görgő: kattanásonként három sor (window-n belül fókusz nélkül is működik). Ha több a sor, mint a magasság, az utolsó oszlop görgetősáv lesz: │ a :scrollbar, █ csúszka a :scrollbar_thumb színében. A szöveg alapstílusa :text.

RUBY
require 'bbs'

text  = File.read('NEWS.md')
lines = BBS::Markdown.to_lines(text, width: 59, theme: BBS::Theme.synchronet)
news  = BBS::Widgets::ScrollView.new(lines: lines)
news.layout(11, 5, 60, 15)    # egy oszloppal szélesebb a szövegnél: hely a görgetősávnak

Tíz sor egy három sor magas nézetben:

TEXT
line 1             █
line 2             │
line 3             │

Konténerek

Container

A BBS::Container (lib/bbs/container.rb) gyerekeket tartó widget. A BBS::Window ennek alosztálya, és egy board ebben rendezi el a widgetjeit. Önmagában nem rendez el semmit: a layout csak a saját bounds-át állítja.

RUBY
BBS::Container.new(id: nil, bounds: nil, theme: nil)
Metódus Mit csinál
children A gyerekek tömbje, hozzáadási sorrendben (ez a rajzolási és a Tab-sorrend is).
add(child) Hozzáfűzi a gyereket, beállítja a parent-jét, és visszaadja.
remove(child) Kiveszi a gyereket, és törli a parent-jét.
clear Minden gyereket kivesz.
walk { ... } Átadja önmagát és minden leszármazottját, mélységi bejárással.
find(id) Az első widget walk-sorrendben, amelyiknek egyezik az id-je, vagy nil.
focusable_descendants Minden látható, fókuszálható leszármazott, sorrendben.
render(frame) Sorban kirajzolja a látható gyerekeket (a későbbiek kerülnek felülre).
hit_test(x, y) A pont alatti legfelső látható leszármazott, vagy maga a konténer, kívül pedig nil.
theme=(theme) Beállítja a témát magán és minden gyerekén.
RUBY
require 'bbs'

form = BBS::Container.new
form.add(BBS::Widgets::TextInput.new(id: :name))
form.add(BBS::Widgets::Label.new(text: 'not focusable'))
form.add(BBS::Widgets::Checkbox.new(id: :news, label: 'Newsletter'))
p form.find(:news).label                       # => "Newsletter"
p form.focusable_descendants.map(&:id)          # => [:name, :news]

Scene: rajz widgetként

A BBS::Widgets::Scene (lib/bbs/widgets/scene.rb) egy BBS::Art rajzot (YAML-fájl ANSI és PETSCII arccal) mutat a modern arcon, és minden rajzoláskor élő adattal tölti ki a hotspotjait. Nem fókuszálható. A 0.x AnsiArt widgetjét váltja; a rajzok, a hotspotok, az évszakos rétegek és az art-preview a Rajzok és grafikus képernyők oldalon vannak.

RUBY
require 'bbs'

house = BBS::Art.load('art/house.yml')
scene = BBS::Widgets::Scene.new(art: house, align: :center, valign: :middle, fill_parent: true)
scene.fill(:sign) { |spot| "\e[1;37m#{inside_count}".ljust(spot.width) }
add(scene)
Opció Alapérték Jelentés
art: — A BBS::Art.
fills: {} Hotspotnév → blokk, ugyanaz, mint a fill(name) { ... } hívás.
align: / valign: :left / :top Hol áll a rajz a bounds-on belül: :left, :center, :right / :top, :middle, :bottom.
crop: true A rajznál kisebb képernyőn a bounds-ra vágja; false esetén ilyenkor semmit sem rajzol (ezt a fits? mondja meg).
fill_parent: false Minden rajzoláskor a szülő bounds-át veszi át (az asztalét, a menüsor és a státuszsor között).

Saját widget írása

Származtass a BBS::Widget-ből (vagy a BBS::Container-ből, ha gyerekeket tart). A szerződés:

  • Vedd át a saját kulcsszavas opcióidat, a többit (**kw) add tovább a super-nek, hogy az id:, bounds: és theme: továbbra is működjön.
  • A focusable? adjon true-t, ha inputot fogad.
  • A render(frame) csak a bounds-on belül rajzoljon, frame.move(bounds.x, bounds.y)-nal kezdve, a színeket pedig style(:role)-lal kérje. Meglévő témaszerepeket használj, hogy minden preset értelmesen színezze a widgetet.
  • A handle_event(event) adjon :handled-et arra, amit felhasznált, és nil-t minden másra, hogy a Tab, az Esc és a hotkeyek tovább működjenek.
  • Az eseménykezelőben csak állapotot változtass, ott soha ne rajzolj: az application minden esemény után újrarajzol.

Egy csillagos értékelő widget, teljes egészében:

RUBY
require 'bbs'

class Rating < BBS::Widget
  attr_reader :value
  attr_accessor :on_change

  def initialize(value: 0, max: 5, on_change: nil, **kw)
    super(**kw)
    @value     = value
    @max       = max
    @on_change = on_change
  end

  def focusable? = true

  def value=(v)
    v = v.clamp(0, @max)
    return if v == @value

    @value = v
    @on_change&.call(@value, self)
  end

  def render(frame)
    return if bounds.width <= 0

    stars = ('*' * @value).ljust(@max, '.')
    frame.move(bounds.x, bounds.y)
    frame.write("[#{stars}] #{@value}/#{@max}"[0, bounds.width],
                sgr: style(focused ? :input_focused : :input))
  end

  def handle_event(event)
    return nil unless enabled

    if event.key?
      case event.key
      when :left, '-' then self.value = @value - 1
      when :right, '+' then self.value = @value + 1
      when '0'..'9'   then self.value = event.key.to_i
      else return nil
      end
      :handled
    elsif event.mouse? && event.mouse.kind == :press && bounds.contains?(event.x, event.y)
      self.value = event.x - bounds.x
      :handled
    end
  end
end

Kipróbálva szerver nélkül, egyenesen egy framebufferbe:

RUBY
r = Rating.new(value: 2, on_change: ->(v, _) { puts "rating #{v}" })
r.theme = BBS::Theme.synchronet
r.layout(1, 1, 20, 1)
r.handle_event(BBS::Event.key(:right))   # rating 3
r.handle_event(BBS::Event.key('9'))      # rating 5 (korlátozva)
p r.handle_event(BBS::Event.key(:up))    # => nil, nem a miénk
click = BBS::Telnet::MouseEvent.new(kind: :press, button: :left, x: 3, y: 1)
r.handle_event(BBS::Event.mouse(click))  # rating 2: a második csillagra kattintott

fb = BBS::FrameBuffer.new(20, 1)
r.render(fb)
TEXT
[**...] 2/5

Window-ban ugyanúgy használható, mint bármelyik beépített widget:

RUBY
stars = Rating.new(id: :stars, value: 3)
stars.layout(win.bounds.x + 2, win.bounds.y + 2, 16, 1)
win.add(stars)
win.reset_focus

Lásd még