---
title: Widgetek
url: 'https://wiki.teletypegames.org/hu/engines/rubbs/widgets'
markdown: 'https://wiki.teletypegames.org/hu/engines/rubbs/widgets.md'
lang: hu
date: '2026-10-27'
description: '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 Applicat…'
taxonomy:
  tag:
    - reference
---

# Widgetek

> A [rubbs](https://wiki.teletypegames.org/hu/engines/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](https://wiki.teletypegames.org/hu/engines/rubbs/windows) oldalon vannak, maga az application az [Application](https://wiki.teletypegames.org/hu/engines/rubbs/application) oldalon, a színek és a framebuffer a [Rajzolás és témák](https://wiki.teletypegames.org/hu/engines/rubbs/drawing) 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](https://wiki.teletypegames.org/hu/engines/rubbs/art)); 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](https://wiki.teletypegames.org/hu/engines/rubbs/drawing)), í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](https://wiki.teletypegames.org/hu/engines/rubbs/drawing)).
- **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](https://wiki.teletypegames.org/hu/engines/rubbs/windows).

```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](https://wiki.teletypegames.org/hu/engines/rubbs/art) 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

- [rubbs](https://wiki.teletypegames.org/hu/engines/rubbs) — áttekintés
- [Application](https://wiki.teletypegames.org/hu/engines/rubbs/application) — `define`, `init`, `add`, `open_window`, `on_tick`, `with_line`
- [Window-k](https://wiki.teletypegames.org/hu/engines/rubbs/windows) — `BBS::Window`, a kész window-típusok, dialógusok
- [Rajzolás és témák](https://wiki.teletypegames.org/hu/engines/rubbs/drawing) — témák és szerepek, `FrameBuffer`, Markdown
- [Rajzok és grafikus képernyők](https://wiki.teletypegames.org/hu/engines/rubbs/art) — `BBS::Art`, `Widgets::Scene`, évszakos rétegek

'; var prevLevel = 2; for (var i = 0; i < headings.length; i++) { var h = headings\[i\]; var text = h.textContent.trim(); var slug = text.toLowerCase() .replace(/\[áàâä\]/g,'a').replace(/\[éèêë\]/g,'e').replace(/\[íìîï\]/g,'i') .replace(/\[óòôöő\]/g,'o').replace(/\[úùûüű\]/g,'u') .replace(/\[^a-z0-9\]+/g, '-').replace(/^-|-$/g, ''); if (!slug) slug = 'heading-' + i; if (used\[slug\]) { slug += '-' + i; } used\[slug\] = true; h.id = slug; var level = parseInt(h.tagName.charAt(1)); var cls = level > 2 ? ' class="toc-child"' : ''; html += '- [' + text + '](#'%20+%20slug%20+%20')
'; } html += '
'; toc.innerHTML = html; // Scroll-spy: highlight current heading var links = toc.querySelectorAll('a'); var ids = \[\]; for (var j = 0; j < links.length; j++) ids.push(links\[j\].getAttribute('href').slice(1)); function updateActive() { var current = ''; for (var k = 0; k < ids.length; k++) { var el = document.getElementById(ids\[k\]); if (el && el.getBoundingClientRect().top <= 80) current = ids\[k\]; } for (var m = 0; m < links.length; m++) { links\[m\].parentNode.classList.toggle('active', links\[m\].getAttribute('href') === '#' + current); } } window.addEventListener('scroll', updateActive, { passive: true }); updateActive(); })();

---

## Navigation

- Parent: [RuBBS](https://wiki.teletypegames.org/hu/engines/rubbs.md)
- Previous: [Telnet, session és store](https://wiki.teletypegames.org/hu/engines/rubbs/telnet-and-store.md)
- Next: [Window-k](https://wiki.teletypegames.org/hu/engines/rubbs/windows.md)
