rgame 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +135 -1
- data/README.md +67 -65
- data/docs/api/README.md +86 -82
- data/docs/api/app.md +125 -68
- data/docs/api/assets.md +203 -165
- data/docs/api/audio.md +130 -89
- data/docs/api/cli.md +259 -0
- data/docs/api/components.md +1032 -265
- data/docs/api/drawing.md +160 -160
- data/docs/api/examples.md +263 -0
- data/docs/api/game.md +149 -50
- data/docs/api/images.md +39 -39
- data/docs/api/input.md +226 -148
- data/docs/api/internals.md +241 -62
- data/docs/api/localization.md +285 -0
- data/docs/api/scene_graph.md +397 -244
- data/docs/api/signals.md +81 -76
- data/docs/api/systems.md +186 -73
- data/docs/api/text.md +58 -41
- data/docs/api/tile_maps.md +237 -0
- data/docs/api/toolbox.md +395 -146
- data/docs/api/ui.md +883 -54
- data/docs/api/values.md +216 -33
- data/examples/assets/README.md +322 -0
- data/examples/assets/blip.ogg +0 -0
- data/examples/assets/glyphs.json +5 -0
- data/examples/assets/glyphs.png +0 -0
- data/examples/assets/hero.json +12 -0
- data/examples/assets/hero.png +0 -0
- data/examples/assets/icons.json +13 -0
- data/examples/assets/icons.png +0 -0
- data/examples/assets/music.ogg +0 -0
- data/examples/assets/skills.json +10 -0
- data/examples/assets/skills.png +0 -0
- data/examples/assets/tileset.png +0 -0
- data/examples/assets/tileset.tsx +65 -0
- data/examples/assets/town.tmx +26 -0
- data/examples/assets/ui.json +11 -0
- data/examples/assets/ui.png +0 -0
- data/examples/collision/locales/en.yml +8 -0
- data/examples/collision/main.rb +316 -0
- data/examples/collision_tiles/locales/en.yml +9 -0
- data/examples/collision_tiles/main.rb +274 -0
- data/examples/fullscreen/locales/en.yml +10 -0
- data/examples/fullscreen/main.rb +216 -0
- data/examples/game_menu/locales/en.yml +8 -0
- data/examples/game_menu/main.rb +170 -0
- data/examples/input_glyphs/locales/en.yml +14 -0
- data/examples/input_glyphs/main.rb +213 -0
- data/examples/jump_topdown/locales/en.yml +9 -0
- data/examples/jump_topdown/main.rb +178 -0
- data/examples/localization/locales/de.yml +12 -0
- data/examples/localization/locales/en.yml +13 -0
- data/examples/localization/main.rb +158 -0
- data/examples/menu_navigation/locales/en.yml +23 -0
- data/examples/menu_navigation/main.rb +365 -0
- data/examples/music/locales/en.yml +7 -0
- data/examples/music/main.rb +134 -0
- data/examples/pathfinding/locales/en.yml +17 -0
- data/examples/pathfinding/main.rb +298 -0
- data/examples/pooling/locales/en.yml +7 -0
- data/examples/pooling/main.rb +259 -0
- data/examples/quick_wheel/locales/en.yml +16 -0
- data/examples/quick_wheel/main.rb +184 -0
- data/examples/radial_menu/locales/en.yml +16 -0
- data/examples/radial_menu/main.rb +184 -0
- data/examples/save_load/locales/en.yml +11 -0
- data/examples/save_load/main.rb +207 -0
- data/examples/save_load_ids/locales/en.yml +11 -0
- data/examples/save_load_ids/main.rb +322 -0
- data/examples/scroll_map/locales/en.yml +4 -0
- data/examples/scroll_map/main.rb +140 -0
- data/examples/signals/locales/en.yml +6 -0
- data/examples/signals/main.rb +278 -0
- data/examples/skill_bar/locales/en.yml +14 -0
- data/examples/skill_bar/main.rb +159 -0
- data/examples/sound/locales/en.yml +6 -0
- data/examples/sound/main.rb +122 -0
- data/examples/split_screen/locales/en.yml +9 -0
- data/examples/split_screen/main.rb +304 -0
- data/examples/sprite/locales/en.yml +8 -0
- data/examples/sprite/main.rb +180 -0
- data/examples/timer/locales/en.yml +12 -0
- data/examples/timer/main.rb +273 -0
- data/examples/velocity/locales/en.yml +6 -0
- data/examples/velocity/main.rb +196 -0
- data/examples/walk/locales/en.yml +4 -0
- data/examples/walk/main.rb +99 -0
- data/exe/rgame +9 -0
- data/ext/rgame_core/app/app.c +33 -3
- data/ext/rgame_core/app/locale.c +67 -0
- data/ext/rgame_core/app/locale.h +28 -0
- data/ext/rgame_core/audio/audio.c +39 -2
- data/ext/rgame_core/example.rb +0 -49
- data/ext/rgame_core/extconf.rb +0 -125
- data/ext/rgame_core/include/rgame/core.h +38 -2
- data/ext/rgame_core/ruby/audio_ext.c +10 -5
- data/ext/rgame_core/ruby/core_ext.c +30 -7
- data/ext/rgame_core/ruby/core_ext.h +3 -0
- data/ext/rgame_core/ruby/locale_ext.c +44 -0
- data/ext/rgame_core/ruby/recording_ext.c +1 -1
- data/ext/rgame_core/ruby/renderer_ext.c +20 -20
- data/ext/rgame_util/extconf.rb +2 -20
- data/ext/rgame_util/route_search.c +305 -0
- data/ext/rgame_util/route_search.h +86 -0
- data/ext/rgame_util/route_search_ext.c +150 -0
- data/ext/rgame_util/solid_grid.c +58 -0
- data/ext/rgame_util/solid_grid.h +49 -0
- data/ext/rgame_util/solid_grid_ext.c +161 -0
- data/ext/rgame_util/tile_sweep.c +164 -0
- data/ext/rgame_util/tile_sweep.h +62 -0
- data/ext/rgame_util/tile_sweep_ext.c +155 -0
- data/ext/rgame_util/util_ext.c +3 -0
- data/ext/rgame_util/util_ext.h +15 -0
- data/lib/rgame/boot.rb +0 -10
- data/lib/rgame/cli/new_project.rb +139 -0
- data/lib/rgame/cli/templates/Gemfile.tt +23 -0
- data/lib/rgame/cli/templates/README.md.tt +93 -0
- data/lib/rgame/cli/templates/Rakefile.tt +9 -0
- data/lib/rgame/cli/templates/assets/locales/en.yml.tt +10 -0
- data/lib/rgame/cli/templates/game.rb.tt +23 -0
- data/lib/rgame/cli/templates/gitignore.tt +12 -0
- data/lib/rgame/cli/templates/main.rb.tt +11 -0
- data/lib/rgame/cli/templates/nodes/root.rb.tt +24 -0
- data/lib/rgame/cli/templates/rspec.tt +2 -0
- data/lib/rgame/cli/templates/rubocop.yml.tt +75 -0
- data/lib/rgame/cli/templates/ruby-version.tt +1 -0
- data/lib/rgame/cli/templates/spec/locales_spec.rb.tt +18 -0
- data/lib/rgame/cli/templates/spec/nodes/root_spec.rb.tt +18 -0
- data/lib/rgame/cli/templates/spec/spec_helper.rb.tt +37 -0
- data/lib/rgame/cli.rb +66 -0
- data/lib/rgame/core/app.rb +6 -44
- data/lib/rgame/core/asset_manager.rb +13 -31
- data/lib/rgame/core/audio.rb +37 -16
- data/lib/rgame/core/font.rb +0 -3
- data/lib/rgame/core/locale.rb +22 -0
- data/lib/rgame/core/nine_slice.rb +0 -21
- data/lib/rgame/core/renderer.rb +6 -63
- data/lib/rgame/core/sprite_sheet.rb +0 -3
- data/lib/rgame/core/tile_map_renderer.rb +2 -19
- data/lib/rgame/core/ui_atlas.rb +28 -13
- data/lib/rgame/core.rb +1 -8
- data/lib/rgame/engine/actor_blockers.rb +131 -0
- data/lib/rgame/engine/animation_set.rb +1 -0
- data/lib/rgame/engine/audio_director.rb +36 -6
- data/lib/rgame/engine/bounds_blockers.rb +74 -0
- data/lib/rgame/engine/camera.rb +3 -3
- data/lib/rgame/engine/circle_collider.rb +4 -2
- data/lib/rgame/engine/collision_box.rb +26 -1
- data/lib/rgame/engine/collision_system.rb +110 -22
- data/lib/rgame/engine/component.rb +34 -10
- data/lib/rgame/engine/components/action_trigger.rb +0 -1
- data/lib/rgame/engine/components/animated_sprite.rb +27 -25
- data/lib/rgame/engine/components/box_collider.rb +99 -0
- data/lib/rgame/engine/components/camera_follow.rb +6 -5
- data/lib/rgame/engine/components/character_body.rb +20 -61
- data/lib/rgame/engine/components/circle_collider.rb +47 -11
- data/lib/rgame/engine/components/collision_world.rb +159 -31
- data/lib/rgame/engine/components/despawn_offscreen.rb +24 -8
- data/lib/rgame/engine/components/feet_collider.rb +61 -0
- data/lib/rgame/engine/components/hop.rb +76 -0
- data/lib/rgame/engine/components/identity.rb +73 -0
- data/lib/rgame/engine/components/mover.rb +285 -0
- data/lib/rgame/engine/components/navigator.rb +145 -0
- data/lib/rgame/engine/components/path_follow.rb +123 -31
- data/lib/rgame/engine/components/player_controller.rb +5 -2
- data/lib/rgame/engine/components/pool.rb +1 -1
- data/lib/rgame/engine/components/screen_wrap.rb +33 -11
- data/lib/rgame/engine/components/sprite.rb +12 -6
- data/lib/rgame/engine/components/targeting.rb +9 -11
- data/lib/rgame/engine/components/thrust_controller.rb +1 -1
- data/lib/rgame/engine/components/tile_world.rb +41 -13
- data/lib/rgame/engine/components/timer.rb +1 -1
- data/lib/rgame/engine/components/velocity.rb +23 -7
- data/lib/rgame/engine/components/wander_controller.rb +6 -2
- data/lib/rgame/engine/components/world.rb +133 -0
- data/lib/rgame/engine/contact_set.rb +74 -0
- data/lib/rgame/engine/culling.rb +4 -6
- data/lib/rgame/engine/debug_overlay.rb +5 -11
- data/lib/rgame/engine/i18n/plural.rb +45 -0
- data/lib/rgame/engine/i18n/plural_rules.rb +82 -0
- data/lib/rgame/engine/i18n/template.rb +59 -0
- data/lib/rgame/engine/i18n.rb +276 -51
- data/lib/rgame/engine/input/action_mapper.rb +0 -26
- data/lib/rgame/engine/input/actions.rb +2 -8
- data/lib/rgame/engine/input/input_map.rb +39 -21
- data/lib/rgame/engine/nav_grid.rb +87 -0
- data/lib/rgame/engine/node2d.rb +248 -133
- data/lib/rgame/engine/path.rb +4 -6
- data/lib/rgame/engine/players.rb +6 -13
- data/lib/rgame/engine/presentation.rb +171 -0
- data/lib/rgame/engine/scene/scene_stack.rb +4 -4
- data/lib/rgame/engine/sealed_privates.rb +54 -0
- data/lib/rgame/engine/spatial_hash.rb +51 -19
- data/lib/rgame/engine/text.rb +194 -0
- data/lib/rgame/engine/tile_blockers.rb +63 -0
- data/lib/rgame/engine/tile_map.rb +2 -3
- data/lib/rgame/engine/tile_map_layer.rb +0 -2
- data/lib/rgame/engine/tileset.rb +2 -4
- data/lib/rgame/engine/timer.rb +2 -2
- data/lib/rgame/engine/ui/button.rb +248 -0
- data/lib/rgame/engine/ui/column.rb +20 -0
- data/lib/rgame/engine/ui/icon_button.rb +93 -0
- data/lib/rgame/engine/ui/menu.rb +246 -71
- data/lib/rgame/engine/ui/navigation.rb +57 -0
- data/lib/rgame/engine/ui/nine_slice_style.rb +50 -0
- data/lib/rgame/engine/ui/option_button.rb +163 -0
- data/lib/rgame/engine/ui/panel_button.rb +32 -0
- data/lib/rgame/engine/ui/panel_menu.rb +36 -0
- data/lib/rgame/engine/ui/pointing.rb +146 -0
- data/lib/rgame/engine/ui/radial_menu.rb +85 -0
- data/lib/rgame/engine/ui/ring.rb +55 -0
- data/lib/rgame/engine/ui/row.rb +21 -0
- data/lib/rgame/engine/ui/shape_style.rb +102 -0
- data/lib/rgame/engine/ui/stack.rb +58 -0
- data/lib/rgame/engine/ui/stepping.rb +93 -0
- data/lib/rgame/engine/ui/text_button.rb +59 -0
- data/lib/rgame/engine/viewports.rb +2 -5
- data/lib/rgame/engine/world_view.rb +5 -4
- data/lib/rgame/engine.rb +31 -25
- data/lib/rgame/game.rb +99 -27
- data/lib/rgame/rubocop/cop/game/draw_in_local_space.rb +103 -0
- data/lib/rgame/rubocop/cop/game/hot_path.rb +36 -0
- data/lib/rgame/rubocop/cop/game/layer_boundary.rb +43 -0
- data/lib/rgame/rubocop/cop/game/no_core_in_engine_layer.rb +100 -0
- data/lib/rgame/rubocop/cop/game/no_engine_in_core_layer.rb +84 -0
- data/lib/rgame/rubocop/cop/game/no_interpolation_in_hot_path.rb +50 -0
- data/lib/rgame/rubocop/cop/game/no_literal_text.rb +41 -0
- data/lib/rgame/rubocop/cop/game/no_needless_allocation.rb +112 -0
- data/lib/rgame/rubocop/default.yml +39 -0
- data/lib/rgame/rubocop/plugin.rb +45 -0
- data/lib/rgame/rubocop.rb +11 -0
- data/lib/rgame/util/color.rb +20 -24
- data/lib/rgame/util/controls.rb +14 -28
- data/lib/rgame/util/route_search.rb +27 -0
- data/lib/rgame/util/save_file.rb +107 -0
- data/lib/rgame/util/solid_grid.rb +37 -0
- data/lib/rgame/util/tensor.rb +0 -9
- data/lib/rgame/util/tile_sweep.rb +36 -0
- data/lib/rgame/util/z.rb +4 -14
- data/lib/rgame/util.rb +4 -3
- data/lib/rgame/version.rb +1 -1
- data/lib/rgame.rb +0 -15
- metadata +152 -11
- data/lib/rgame/engine/actor.rb +0 -53
- data/lib/rgame/engine/body.rb +0 -49
- data/lib/rgame/engine/cached_label.rb +0 -33
- data/lib/rgame/engine/input/player_controller.rb +0 -14
- data/lib/rgame/engine/matrix.rb +0 -32
- data/lib/rgame/engine/resettable.rb +0 -67
- data/lib/rgame/engine/tile_collision.rb +0 -78
- data/lib/rgame/engine/ui/menu_item.rb +0 -84
data/lib/rgame/engine/ui/menu.rb
CHANGED
|
@@ -3,20 +3,102 @@
|
|
|
3
3
|
module RGame
|
|
4
4
|
module Engine
|
|
5
5
|
module UI
|
|
6
|
-
#
|
|
7
|
-
# controller.
|
|
6
|
+
# Buttons to choose from, navigated by keyboard or controller.
|
|
8
7
|
#
|
|
9
|
-
# menu = layer.add_node(UI::Menu.new(item_width: 220, item_height: 44))
|
|
10
|
-
# menu.
|
|
11
|
-
# menu.
|
|
8
|
+
# menu = layer.add_node(UI::Menu.new(layout: UI::Column.new(item_width: 220, item_height: 44)))
|
|
9
|
+
# menu.add(UI::PanelButton.new(label: 'Resume')).on_activated { close }
|
|
10
|
+
# menu.add(UI::PanelButton.new(label: 'Quit')).on_activated { game.close }
|
|
12
11
|
#
|
|
13
12
|
# ## Focus is the whole design
|
|
14
13
|
#
|
|
15
|
-
# With no pointer there is no hover, so something has to own *which
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
14
|
+
# With no pointer there is no hover, so something has to own *which button
|
|
15
|
+
# is focused*. That is this class, and everything else about a menu
|
|
16
|
+
# follows from it: a button draws differently because it is focused, and
|
|
17
|
+
# `ui_confirm` activates the focused one.
|
|
18
|
+
#
|
|
19
|
+
# ## The menu holds buttons; it does not build them
|
|
20
|
+
#
|
|
21
|
+
# What a button looks like is the button's business — any UI::Button
|
|
22
|
+
# subclass, shipped or written by the game, goes in through `add`. The menu
|
|
23
|
+
# never picks a look, so one menu can hold a panel button next to a game's
|
|
24
|
+
# own.
|
|
25
|
+
#
|
|
26
|
+
# ## Two questions are handed off, and one is kept
|
|
27
|
+
#
|
|
28
|
+
# A list and a radial wheel are the same menu. What differs is **where the
|
|
29
|
+
# buttons sit** and **how input moves focus**, so those two are passed in:
|
|
30
|
+
#
|
|
31
|
+
# | | Answers | Shipped |
|
|
32
|
+
# |---|---|---|
|
|
33
|
+
# | `layout:` | where each button goes, its size, and the bounds of them all | UI::Column, UI::Row, UI::Ring |
|
|
34
|
+
# | `navigation:` | which button this frame's input focuses | UI::Stepping (default), UI::Pointing, or nil |
|
|
35
|
+
#
|
|
36
|
+
# What stays here is what every menu does the same way: holding the
|
|
37
|
+
# buttons, passing `ui_confirm` to the focused one as a press and a
|
|
38
|
+
# release — when that activates is the button's `activate_on:` — and
|
|
39
|
+
# passing each button's `hotkey` to that button, focused or not. A
|
|
40
|
+
# navigation cannot forget to do either, because it is never asked to.
|
|
41
|
+
#
|
|
42
|
+
# ## A menu with no navigation
|
|
43
|
+
#
|
|
44
|
+
# `navigation: nil` says input never moves focus: the menu focuses nothing
|
|
45
|
+
# when a button is added, so `ui_confirm` has nothing to act on. What
|
|
46
|
+
# focuses a button then is the game calling `focus` — which confirm then
|
|
47
|
+
# acts on, as it would for any navigation.
|
|
48
|
+
#
|
|
49
|
+
# ## A menu acts only on a press it saw start
|
|
50
|
+
#
|
|
51
|
+
# A menu takes no press until it has seen `ui_confirm` up, and no hotkey
|
|
52
|
+
# press on a button until it has seen that hotkey up since the button was
|
|
53
|
+
# added. A menu opened from `on_activated` — a submenu — is controlled
|
|
54
|
+
# later in the same tick, while the key that opened it is still down, and
|
|
55
|
+
# would otherwise read the same press edge again and activate its own
|
|
56
|
+
# button with it.
|
|
57
|
+
#
|
|
58
|
+
# wheel = UI::Menu.new(x: 320, y: 240, navigation: UI::Pointing.new,
|
|
59
|
+
# layout: UI::Ring.new(radius: 120, item_width: 96, item_height: 30))
|
|
60
|
+
#
|
|
61
|
+
# ## A scope for its buttons' keys
|
|
62
|
+
#
|
|
63
|
+
# `scope:` puts a scope in front of every label given as a key, so the
|
|
64
|
+
# buttons of a title menu say `label: 'play'` and draw `title_menu.play`:
|
|
65
|
+
#
|
|
66
|
+
# menu = UI::Menu.new(layout: column, scope: 'title_menu')
|
|
67
|
+
# menu.add(UI::PanelButton.new(label: 'play')) # title_menu.play
|
|
68
|
+
# menu.add(UI::PanelButton.new(label: Engine::Text.new('quit', scope: 'common'))) # common.quit
|
|
69
|
+
#
|
|
70
|
+
# The menu sets it as each button is added, on a button whose
|
|
71
|
+
# `label_scope` is still nil. A label given as an Engine::Text keeps its own
|
|
72
|
+
# scope, nil included, and so does a literal. The scope reaches the
|
|
73
|
+
# buttons of this menu and nothing deeper in the tree.
|
|
74
|
+
#
|
|
75
|
+
# ## Open and closed
|
|
76
|
+
#
|
|
77
|
+
# A closed menu draws nothing — neither its own backdrop nor its buttons —
|
|
78
|
+
# and its input moves no focus and presses nothing. A menu starts open;
|
|
79
|
+
# `close` and `open` are what a pause menu toggles. Closing is not
|
|
80
|
+
# pausing: a closed menu still ticks, so a button's pressed feedback runs
|
|
81
|
+
# out while it is shut instead of waiting to be seen again.
|
|
82
|
+
#
|
|
83
|
+
# ## A menu held open by an action
|
|
84
|
+
#
|
|
85
|
+
# `trigger:` names an action that opens the menu while it is held. Letting
|
|
86
|
+
# it go activates the button focused at that moment, if any, and closes —
|
|
87
|
+
# the console quick wheel, held open by a shoulder button:
|
|
88
|
+
#
|
|
89
|
+
# wheel = UI::RadialMenu.new(x: 320, y: 240, radius: 150, button_width: 64, trigger: :quick_menu)
|
|
90
|
+
#
|
|
91
|
+
# A menu with a trigger starts closed, and `ui_confirm` activates nothing
|
|
92
|
+
# on it: letting go is the only way to choose. Opening only happens on a
|
|
93
|
+
# press this menu saw start, like every other press, so a trigger already
|
|
94
|
+
# down when the menu appears opens nothing; and a trigger that comes up
|
|
95
|
+
# while the menu is paused closes it without choosing. `open` raises on
|
|
96
|
+
# such a menu, because a menu opened by hand would wait for the release of
|
|
97
|
+
# a press it never saw; `close` does not, and is how a game cancels a held
|
|
98
|
+
# wheel.
|
|
99
|
+
#
|
|
100
|
+
# What the world does while the menu is open is the game's to decide, from
|
|
101
|
+
# `on_opened` and `on_closed`. The menu only says when.
|
|
20
102
|
#
|
|
21
103
|
# ## Focus is per player, and that costs nothing
|
|
22
104
|
#
|
|
@@ -29,85 +111,178 @@ module RGame
|
|
|
29
111
|
#
|
|
30
112
|
# ## What this is not
|
|
31
113
|
#
|
|
32
|
-
# It is a menu, not a widget library.
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
# of it is a reference, and how UI should be laid out in general is still
|
|
36
|
-
# an open question — see docs/api/ui.md, "What this is not".
|
|
114
|
+
# It is a menu, not a widget library. Every button is the same size, placed
|
|
115
|
+
# by its layout, and that is the whole of its layout story — see
|
|
116
|
+
# docs/api/ui.md, "What this is not".
|
|
37
117
|
class Menu < Node2D
|
|
38
|
-
|
|
39
|
-
#
|
|
40
|
-
|
|
41
|
-
|
|
118
|
+
signal :on_opened
|
|
119
|
+
# Emits the button a trigger's release activated, or nil.
|
|
120
|
+
signal :on_closed, Signal.define(:button)
|
|
121
|
+
|
|
122
|
+
# `trigger` is an action name, or nil. `scope` is a String, or nil.
|
|
123
|
+
attr_reader :buttons, :focused_index, :layout, :navigation, :trigger, :scope
|
|
124
|
+
|
|
125
|
+
# The rectangle the layout says encloses every button, relative to the
|
|
126
|
+
# menu — what a subclass draws its backdrop round. Copied on each `add`,
|
|
127
|
+
# so reading them on a draw path costs nothing.
|
|
128
|
+
attr_reader :bounds_x, :bounds_y, :bounds_width, :bounds_height
|
|
129
|
+
|
|
130
|
+
def initialize(layout:, navigation: Stepping.new, trigger: nil, scope: nil, **)
|
|
42
131
|
super(**)
|
|
43
|
-
@
|
|
44
|
-
@
|
|
45
|
-
@
|
|
46
|
-
@
|
|
47
|
-
@
|
|
48
|
-
@
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
index = @focused_index
|
|
73
|
-
@items.size.times do
|
|
74
|
-
index = (index + delta) % @items.size
|
|
75
|
-
next unless @items[index].enabled?
|
|
76
|
-
|
|
77
|
-
focus(index)
|
|
78
|
-
return
|
|
79
|
-
end
|
|
132
|
+
@scope = scope&.to_s&.freeze
|
|
133
|
+
@layout = layout
|
|
134
|
+
@navigation = navigation
|
|
135
|
+
@trigger = trigger
|
|
136
|
+
@open = trigger.nil?
|
|
137
|
+
@buttons = []
|
|
138
|
+
@bounds_x = @bounds_y = @bounds_width = @bounds_height = 0
|
|
139
|
+
@focused_index = nil
|
|
140
|
+
@confirm_seen_up = false
|
|
141
|
+
@trigger_seen_up = false
|
|
142
|
+
@hotkey_seen_up = []
|
|
143
|
+
navigation&.attach(self)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Adds a button, gives it the menu's `scope` unless it has a
|
|
147
|
+
# `label_scope` already, re-arranges them all, and returns it, so a
|
|
148
|
+
# caller can connect to its signal in the same line. Raises TypeError for
|
|
149
|
+
# anything that is not a UI::Button.
|
|
150
|
+
def add(button)
|
|
151
|
+
raise TypeError, "a Menu holds UI::Button instances, not #{button.class}" unless button.is_a?(Button)
|
|
152
|
+
|
|
153
|
+
button.label_scope = @scope if @scope && button.label_scope.nil?
|
|
154
|
+
@buttons << button
|
|
155
|
+
@hotkey_seen_up << false
|
|
156
|
+
add_node(button)
|
|
157
|
+
@layout.arrange(@buttons)
|
|
158
|
+
@bounds_x, @bounds_y, @bounds_width, @bounds_height = @layout.bounds(@buttons)
|
|
159
|
+
@navigation&.on_buttons_changed
|
|
160
|
+
button
|
|
80
161
|
end
|
|
81
162
|
|
|
163
|
+
# The focused button, or nil when nothing is — which under UI::Pointing
|
|
164
|
+
# is whenever the stick is at rest.
|
|
165
|
+
def focused = @focused_index && @buttons[@focused_index]
|
|
166
|
+
|
|
167
|
+
# Focuses the button at `index`, or nothing for nil. Only the buttons
|
|
168
|
+
# whose focus actually changes are told. It does not check `enabled?`:
|
|
169
|
+
# which buttons may take focus is the navigation's rule, and a disabled
|
|
170
|
+
# button cannot be activated whatever holds it.
|
|
82
171
|
def focus(index)
|
|
172
|
+
previous = focused
|
|
83
173
|
@focused_index = index
|
|
84
|
-
|
|
174
|
+
current = focused
|
|
175
|
+
previous.focused = false if previous && !previous.equal?(current)
|
|
176
|
+
current&.focused = true
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def open? = @open
|
|
180
|
+
|
|
181
|
+
# Opens the menu, lets the navigation forget the last opening, and emits
|
|
182
|
+
# `on_opened`. Nothing if already open. Raises on a menu with a trigger,
|
|
183
|
+
# which only its trigger opens.
|
|
184
|
+
def open
|
|
185
|
+
raise "this menu is opened by holding #{@trigger.inspect}, not by #open" if @trigger
|
|
186
|
+
return if @open
|
|
187
|
+
|
|
188
|
+
open_now
|
|
85
189
|
end
|
|
86
190
|
|
|
191
|
+
# Closes the menu without activating anything and emits `on_closed` with
|
|
192
|
+
# nil. Nothing if already closed.
|
|
193
|
+
def close
|
|
194
|
+
close_with(nil) if @open
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Draws nothing while closed.
|
|
198
|
+
def draw(renderer, view)
|
|
199
|
+
super if @open
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# A trigger's press first, so an opening's first frame already reads the
|
|
203
|
+
# stick; then navigation, so a focus change and a confirm on the same
|
|
204
|
+
# frame confirm the newly focused button; then every hotkey; then
|
|
205
|
+
# confirm, on a menu with no trigger; and a trigger's release last, so it
|
|
206
|
+
# chooses what this frame focused.
|
|
87
207
|
def on_control(actions)
|
|
88
|
-
|
|
89
|
-
|
|
208
|
+
trigger_edge = control_trigger(actions) if @trigger
|
|
209
|
+
open_now if trigger_edge == :press
|
|
210
|
+
return unless @open
|
|
90
211
|
|
|
91
|
-
|
|
92
|
-
|
|
212
|
+
@navigation&.on_control(actions)
|
|
213
|
+
press_hotkeys(actions)
|
|
214
|
+
@trigger ? release_trigger(trigger_edge) : confirm(actions)
|
|
215
|
+
end
|
|
93
216
|
|
|
94
|
-
|
|
95
|
-
|
|
217
|
+
# Lets the navigation count time, then does what every node does.
|
|
218
|
+
def update(dt)
|
|
219
|
+
@navigation&.update(dt) unless @paused
|
|
220
|
+
super
|
|
96
221
|
end
|
|
97
222
|
|
|
98
223
|
private
|
|
99
224
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
225
|
+
def open_now
|
|
226
|
+
@open = true
|
|
227
|
+
@navigation&.on_opened
|
|
228
|
+
on_opened_signal.emit
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def close_with(button)
|
|
232
|
+
@open = false
|
|
233
|
+
on_closed_signal.emit(button)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def control_trigger(actions)
|
|
237
|
+
edge = press_edge(actions, @trigger, @trigger_seen_up)
|
|
238
|
+
@trigger_seen_up = !actions.held?(@trigger)
|
|
239
|
+
edge
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def release_trigger(edge)
|
|
243
|
+
case edge
|
|
244
|
+
when :release then close_with(focused&.activate)
|
|
245
|
+
when :missed_release then close_with(nil)
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def confirm(actions)
|
|
250
|
+
edge = press_edge(actions, :ui_confirm, @confirm_seen_up)
|
|
251
|
+
@confirm_seen_up = !actions.held?(:ui_confirm)
|
|
252
|
+
button = focused
|
|
253
|
+
pass_edge(button, edge, :confirm) if button
|
|
254
|
+
end
|
|
104
255
|
|
|
105
|
-
|
|
106
|
-
|
|
256
|
+
def press_hotkeys(actions)
|
|
257
|
+
index = 0
|
|
258
|
+
while index < @buttons.size
|
|
259
|
+
button = @buttons[index]
|
|
260
|
+
hotkey = button.hotkey
|
|
261
|
+
if hotkey
|
|
262
|
+
edge = press_edge(actions, hotkey, @hotkey_seen_up[index])
|
|
263
|
+
@hotkey_seen_up[index] = !actions.held?(hotkey)
|
|
264
|
+
pass_edge(button, edge, :hotkey)
|
|
265
|
+
end
|
|
266
|
+
index += 1
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def press_edge(actions, action, seen_up)
|
|
271
|
+
if actions.pressed?(action)
|
|
272
|
+
:press if seen_up
|
|
273
|
+
elsif actions.released?(action)
|
|
274
|
+
:release
|
|
275
|
+
elsif !actions.held?(action)
|
|
276
|
+
:missed_release
|
|
277
|
+
end
|
|
278
|
+
end
|
|
107
279
|
|
|
108
|
-
|
|
280
|
+
def pass_edge(button, edge, source)
|
|
281
|
+
case edge
|
|
282
|
+
when :press then button.press(source)
|
|
283
|
+
when :release then button.release(source)
|
|
284
|
+
when :missed_release then button.cancel_press(source)
|
|
109
285
|
end
|
|
110
|
-
focus(0)
|
|
111
286
|
end
|
|
112
287
|
end
|
|
113
288
|
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
module UI
|
|
6
|
+
# How a Menu turns a player's input into focus. The base of UI::Stepping
|
|
7
|
+
# and UI::Pointing, and the class to subclass for a third way.
|
|
8
|
+
#
|
|
9
|
+
# class Cycling < UI::Navigation
|
|
10
|
+
# def on_control(actions)
|
|
11
|
+
# menu.focus((menu.focused_index.to_i + 1) % menu.buttons.size) if actions.pressed?(:ui_right)
|
|
12
|
+
# end
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# A navigation only decides **which button is focused**. Confirming the
|
|
16
|
+
# focused button, pressing it and firing its signal are the Menu's,
|
|
17
|
+
# and the same for every navigation, so a subclass cannot forget them.
|
|
18
|
+
#
|
|
19
|
+
# ## One per menu
|
|
20
|
+
#
|
|
21
|
+
# A navigation may keep state about the menu it drives — UI::Pointing keeps
|
|
22
|
+
# the last direction read — so it belongs to exactly one. Handing the same
|
|
23
|
+
# instance to a second Menu raises, rather than leaving two menus quietly
|
|
24
|
+
# sharing one pointer. Build a fresh one for each menu; the constructor
|
|
25
|
+
# default already does.
|
|
26
|
+
class Navigation
|
|
27
|
+
attr_reader :menu
|
|
28
|
+
|
|
29
|
+
# Called by Menu when it is built with this navigation.
|
|
30
|
+
def attach(menu)
|
|
31
|
+
if @menu && !@menu.equal?(menu)
|
|
32
|
+
raise ArgumentError, "this #{self.class} already navigates another menu — build one per menu"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
@menu = menu
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Reads this frame's actions and moves focus with `menu.focus`. Called
|
|
39
|
+
# before the Menu handles `ui_confirm`, so a focus change and a confirm on
|
|
40
|
+
# the same frame activate the newly focused button.
|
|
41
|
+
def on_control(actions); end
|
|
42
|
+
|
|
43
|
+
# Called after a button is added, for a navigation that has an opinion
|
|
44
|
+
# about where focus starts.
|
|
45
|
+
def on_buttons_changed; end
|
|
46
|
+
|
|
47
|
+
# Called by Menu on each update while the menu is not paused, for a
|
|
48
|
+
# navigation that counts time. Time enters here and nowhere else.
|
|
49
|
+
def update(dt); end
|
|
50
|
+
|
|
51
|
+
# Called by Menu when it opens, for a navigation holding state that
|
|
52
|
+
# belongs to one opening — UI::Pointing forgets its aim.
|
|
53
|
+
def on_opened; end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
module UI
|
|
6
|
+
# A button background drawn from a UI atlas: one nine-slice element per
|
|
7
|
+
# state, stretched over the button's whole slot.
|
|
8
|
+
#
|
|
9
|
+
# style = UI::NineSliceStyle.new(idle: :button_idle, focused: :button_focus,
|
|
10
|
+
# pressed: :button_pressed, disabled: :button_disabled)
|
|
11
|
+
# menu.add(UI::TextButton.new(label: 'Play', style: style))
|
|
12
|
+
#
|
|
13
|
+
# Every state is required, so an atlas missing its disabled element is a
|
|
14
|
+
# missing keyword where the style is built rather than a lookup failure the
|
|
15
|
+
# first frame a button is disabled.
|
|
16
|
+
#
|
|
17
|
+
# A style is anything answering `draw(renderer, state, width, height)` in
|
|
18
|
+
# the button's local space, drawing at `z: 0` or below so the button's own
|
|
19
|
+
# content, at `z: 1`, stays on top. This one draws at the nine-slice
|
|
20
|
+
# default of 0.
|
|
21
|
+
#
|
|
22
|
+
# A style may also answer `content_color(state)`: the colour a button's
|
|
23
|
+
# label or icon should take over that state's background, or nil to keep
|
|
24
|
+
# the button's own. Art is the game's, so this one cannot know what reads
|
|
25
|
+
# on it and always answers nil — PanelButton's dark label is chosen for
|
|
26
|
+
# the shipped atlas instead.
|
|
27
|
+
class NineSliceStyle
|
|
28
|
+
# The element name drawn for each state, keyed by `idle`, `focused`,
|
|
29
|
+
# `pressed` and `disabled`.
|
|
30
|
+
attr_reader :elements
|
|
31
|
+
|
|
32
|
+
def initialize(idle:, focused:, pressed:, disabled:)
|
|
33
|
+
@elements = { idle: idle, focused: focused, pressed: pressed, disabled: disabled }.freeze
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# A copy with some elements replaced — a game whose atlas differs from
|
|
37
|
+
# the shipped one in a single state names only that state.
|
|
38
|
+
#
|
|
39
|
+
# UI::PanelButton::STYLE.with(focused: :my_glow)
|
|
40
|
+
def with(**changes) = self.class.new(**@elements, **changes)
|
|
41
|
+
|
|
42
|
+
def content_color(_state) = nil
|
|
43
|
+
|
|
44
|
+
def draw(renderer, state, width, height)
|
|
45
|
+
renderer.nine_slice(@elements.fetch(state), 0, 0, width, height)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
module UI
|
|
6
|
+
# A menu row whose value is chosen from a list, moved with `ui_left` and
|
|
7
|
+
# `ui_right`.
|
|
8
|
+
#
|
|
9
|
+
# quality = menu.add(UI::OptionButton.new(label: 'shadows', values: %i[off low high]))
|
|
10
|
+
# quality.on_changed { |value| settings.shadows = value }
|
|
11
|
+
#
|
|
12
|
+
# It draws `Label < value >`, and the chevrons appear only where
|
|
13
|
+
# there is somewhere to go — which is the only feedback a player gets that
|
|
14
|
+
# they have reached an end of the list.
|
|
15
|
+
#
|
|
16
|
+
# ## Values move by clamping, while focus wraps
|
|
17
|
+
#
|
|
18
|
+
# `Menu` wraps focus at the ends, and this deliberately does not. A list of
|
|
19
|
+
# menu buttons has no magnitude, so joining its ends only makes a short list
|
|
20
|
+
# quicker to get around. A list of *values* usually does have one — volume,
|
|
21
|
+
# difficulty, a resolution — and wrapping turns "one louder" at the top of
|
|
22
|
+
# the range into silence. Every settings screen a player has used clamps,
|
|
23
|
+
# so this does too.
|
|
24
|
+
#
|
|
25
|
+
# ## A caption is a key, unless it is a Text
|
|
26
|
+
#
|
|
27
|
+
# `display` turns a value into what is drawn for it: a key, which the
|
|
28
|
+
# button makes an Engine::Text of under its `label_scope` just as it does
|
|
29
|
+
# the label, or a `Text`, used as it is. The default makes a Symbol value
|
|
30
|
+
# its own key and anything else a literal, so `%i[off low high]` reads
|
|
31
|
+
# the keys `off`, `low` and `high`, and `[0, 50, 100]` draws the numbers:
|
|
32
|
+
#
|
|
33
|
+
# UI::OptionButton.new(label: 'volume', values: [0, 50, 100],
|
|
34
|
+
# display: ->(percent) { Engine::Text.literal("#{percent}%") })
|
|
35
|
+
#
|
|
36
|
+
# `display` is called for the whole list in the constructor. Doing it in
|
|
37
|
+
# `on_draw` instead would allocate a String every frame for every row on
|
|
38
|
+
# screen, which is what `Game/NoInterpolationInHotPath` refuses — and the
|
|
39
|
+
# values themselves are what a game acts on, so they cannot simply be
|
|
40
|
+
# stored as text.
|
|
41
|
+
#
|
|
42
|
+
# A caption `Text` with variables shows the values its last `with` was
|
|
43
|
+
# given, as a label does.
|
|
44
|
+
#
|
|
45
|
+
# The value column is as wide as the widest caption. It is measured again
|
|
46
|
+
# whenever any caption's String changes — a locale switch, a new scope, a
|
|
47
|
+
# `with` with new values — which a draw notices by object identity, so a
|
|
48
|
+
# switch to longer captions widens it and an unchanged draw measures
|
|
49
|
+
# nothing.
|
|
50
|
+
class OptionButton < PanelButton
|
|
51
|
+
# Emits the newly selected value, which is the only thing a listener
|
|
52
|
+
# wants; `index` is available on the button for anything that needs it.
|
|
53
|
+
signal :on_changed, Signal.define(:value)
|
|
54
|
+
|
|
55
|
+
LEFT_CHEVRON = '<'
|
|
56
|
+
RIGHT_CHEVRON = '>'
|
|
57
|
+
|
|
58
|
+
PADDING = 12
|
|
59
|
+
GAP = 8
|
|
60
|
+
|
|
61
|
+
DISPLAY = ->(value) { value.is_a?(Symbol) ? value : Text.literal(value.to_s) }
|
|
62
|
+
|
|
63
|
+
def initialize(label:, values:, index: 0, display: DISPLAY, **)
|
|
64
|
+
super(label: label, **)
|
|
65
|
+
@values = values.to_a.freeze
|
|
66
|
+
shown = @values.map { |value| display.call(value) }
|
|
67
|
+
@captions = shown.map { |caption| text_for(caption) }.freeze
|
|
68
|
+
@keyed_captions = @captions.reject.with_index { |_caption, at| shown[at].is_a?(Text) }.freeze
|
|
69
|
+
@measured = Array.new(@captions.size)
|
|
70
|
+
@index = @values.empty? ? 0 : index.clamp(0, @values.size - 1)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
attr_reader :values, :index
|
|
74
|
+
|
|
75
|
+
def value = @values[@index]
|
|
76
|
+
|
|
77
|
+
# The Engine::Text drawn for the current value, or nil for an empty list.
|
|
78
|
+
def caption = @captions[@index]
|
|
79
|
+
|
|
80
|
+
# Scopes the captions `display` gave as keys along with the label.
|
|
81
|
+
def label_scope=(scope)
|
|
82
|
+
super
|
|
83
|
+
@keyed_captions.each { |caption| caption.scope = label_scope }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Selects `value` if the list holds it, and says whether it did. A game
|
|
87
|
+
# restoring a saved setting does not have to know where in the list it
|
|
88
|
+
# sits, and a value that is no longer offered — an old save, a list that
|
|
89
|
+
# changed between versions — leaves the button where it was rather than
|
|
90
|
+
# raising.
|
|
91
|
+
def value=(value)
|
|
92
|
+
found = @values.index(value)
|
|
93
|
+
@index = found if found
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Moves the selection by `delta`, clamped. Returns the button when it
|
|
97
|
+
# actually moved and nil otherwise, so a caller can tell "pressed at the
|
|
98
|
+
# end of the list" from "changed" without comparing values.
|
|
99
|
+
#
|
|
100
|
+
# Disabled and empty are both nil for the same reason `activate` is:
|
|
101
|
+
# a caller never has to check first.
|
|
102
|
+
def adjust(delta)
|
|
103
|
+
return nil unless enabled?
|
|
104
|
+
return nil if @values.empty?
|
|
105
|
+
|
|
106
|
+
moved = (@index + delta).clamp(0, @values.size - 1)
|
|
107
|
+
return nil if moved == @index
|
|
108
|
+
|
|
109
|
+
@index = moved
|
|
110
|
+
on_changed_signal.emit(value)
|
|
111
|
+
self
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
private
|
|
115
|
+
|
|
116
|
+
def draw_foreground(renderer)
|
|
117
|
+
y = label_y(renderer)
|
|
118
|
+
color = current_label_color
|
|
119
|
+
renderer.text(@label, PADDING, y, z: 1, color: color) if @label
|
|
120
|
+
draw_value(renderer, y, color)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def draw_value(renderer, y, color)
|
|
124
|
+
return if @values.empty?
|
|
125
|
+
|
|
126
|
+
chevron = renderer.text_width(RIGHT_CHEVRON)
|
|
127
|
+
right = width - PADDING - chevron
|
|
128
|
+
column = column_width(renderer)
|
|
129
|
+
left = right - GAP - column - GAP - chevron
|
|
130
|
+
|
|
131
|
+
renderer.text(LEFT_CHEVRON, left, y, z: 1, color: color) if @index.positive?
|
|
132
|
+
renderer.text(RIGHT_CHEVRON, right, y, z: 1, color: color) if @index < @values.size - 1
|
|
133
|
+
|
|
134
|
+
text = caption.to_s
|
|
135
|
+
centred = left + chevron + GAP + ((column - renderer.text_width(text)) / 2)
|
|
136
|
+
renderer.text(text, centred, y, z: 1, color: color)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def column_width(renderer)
|
|
140
|
+
return @column_width if captions_measured?
|
|
141
|
+
|
|
142
|
+
@column_width = 0
|
|
143
|
+
@captions.each_with_index do |caption, at|
|
|
144
|
+
@measured[at] = caption.to_s
|
|
145
|
+
width = renderer.text_width(@measured[at])
|
|
146
|
+
@column_width = width if width > @column_width
|
|
147
|
+
end
|
|
148
|
+
@column_width
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def captions_measured?
|
|
152
|
+
at = 0
|
|
153
|
+
while at < @captions.size
|
|
154
|
+
return false unless @captions[at].to_s.equal?(@measured[at])
|
|
155
|
+
|
|
156
|
+
at += 1
|
|
157
|
+
end
|
|
158
|
+
true
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
module UI
|
|
6
|
+
# A UI::TextButton on a nine-slice: the shipped atlas's button, one element
|
|
7
|
+
# per state, with a dark label.
|
|
8
|
+
#
|
|
9
|
+
# resume = menu.add(UI::PanelButton.new(label: 'resume'))
|
|
10
|
+
# resume.on_activated { cutscene.close }
|
|
11
|
+
#
|
|
12
|
+
# menu.add(UI::PanelButton.new(label: 'save_game', enabled: false))
|
|
13
|
+
# menu.add(UI::PanelButton.new(label: 'load', style: UI::PanelButton::STYLE.with(idle: :mine)))
|
|
14
|
+
#
|
|
15
|
+
# `STYLE` names the element drawn for each state — which is why the shipped
|
|
16
|
+
# atlas has one for each of the four. It differs from a TextButton only in
|
|
17
|
+
# its defaults; every one of them can still be passed.
|
|
18
|
+
class PanelButton < TextButton
|
|
19
|
+
STYLE = NineSliceStyle.new(idle: :button_idle, focused: :button_focus,
|
|
20
|
+
pressed: :button_pressed, disabled: :button_disabled)
|
|
21
|
+
|
|
22
|
+
LABEL_COLOR = Util::Color.new(46, 34, 24)
|
|
23
|
+
DISABLED_LABEL_COLOR = Util::Color.new(120, 110, 100)
|
|
24
|
+
|
|
25
|
+
def initialize(label:, style: STYLE, label_color: LABEL_COLOR,
|
|
26
|
+
disabled_label_color: DISABLED_LABEL_COLOR, **)
|
|
27
|
+
super
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|