Class: Ruby2D::Button

Inherits:
Object
  • Object
show all
Includes:
Interactive
Defined in:
lib/ruby2d/button.rb

Overview

A clickable region with optional visual and label. Button is itself the interactive entity: it registers directly with Window's interactive registry and holds its own event handlers. The optional @visual is a rendering concern, never an event-routing one.

Constant Summary

Constants included from Interactive

Interactive::OBJECT_EVENTS, Interactive::OBJECT_EVENT_FILTER_PREDICATES

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Interactive

#_fire_event, #interactive?, #off, #on

Constructor Details

#initialize(visual = nil, x: 0, y: 0, z: 0, width: 200, height: 50, label: nil, color: nil, colour: nil, hover_color: nil, hover_colour: nil, pressed_color: nil, pressed_colour: nil, label_color: nil, label_colour: nil, hover_label_color: nil, hover_label_colour: nil, pressed_label_color: nil, pressed_label_colour: nil, stroke_color: nil, stroke_colour: nil, stroke_width: 0, label_size: 20, add: true, visible: true, padding: nil, padding_top: nil, padding_right: nil, padding_bottom: nil, padding_left: nil, &on_click) ⇒ Button

Create a button. Three forms:

Self-rendered:

Button.new(x:, y:, width:, height:, label:, color:, hover_color:) { ... }

Wrap a custom visual (any shape responding to x, y, and contains?):

Button.new(some_shape) { ... }

Visual-less hit area (nothing rendered):

Button.new(x:, y:, width:, height:) { ... }

hover_color: and pressed_color: are opt-in. Pass an explicit color, or :auto (lightens for hover, darkens for pressed). Omit for no tint. When both are set and the user is hovering+pressed, pressed wins.



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/ruby2d/button.rb', line 27

def initialize(visual = nil, x: 0, y: 0, z: 0, width: 200, height: 50,
               label: nil, color: nil, colour: nil,
               hover_color: nil, hover_colour: nil,
               pressed_color: nil, pressed_colour: nil,
               label_color: nil, label_colour: nil,
               hover_label_color: nil, hover_label_colour: nil,
               pressed_label_color: nil, pressed_label_colour: nil,
               stroke_color: nil, stroke_colour: nil, stroke_width: 0,
               label_size: 20, add: true, visible: true,
               padding: nil, padding_top: nil, padding_right: nil,
               padding_bottom: nil, padding_left: nil, &on_click)
  base_color_input = color || colour
  hover_input = hover_color || hover_colour
  pressed_input = pressed_color || pressed_colour
  hover_label_input = hover_label_color || hover_label_colour
  pressed_label_input = pressed_label_color || pressed_label_colour
  stroke_input = stroke_color || stroke_colour

  # Label tints only make sense with a label to tint. Without one they would
  # silently install no-op hover/press handlers and force the button
  # interactive for no effect, so reject the combination up front.
  if (hover_label_input || pressed_label_input) && !label
    raise ArgumentError,
          '`hover_label_color`/`pressed_label_color` require a `label:` to tint'
  end

  if visual
    @visual = visual
    @owns_visual = false
    @x = visual.x
    @y = visual.y
    @z = visual.respond_to?(:z) ? visual.z : 0
    @width = visual.respond_to?(:width) ? visual.width : 0
    @height = visual.respond_to?(:height) ? visual.height : 0
  else
    @x = x.is_a?(Symbol) ? 0 : x
    @y = y.is_a?(Symbol) ? 0 : y
    @z = z
    @width = width
    @height = height
    if label || base_color_input || stroke_input
      # Self-rendered: own a Rectangle for drawing.
      @visual = Rectangle.new(x: x, y: y, width: width, height: height, z: z,
                              color: base_color_input || '#333',
                              stroke_color: stroke_input,
                              stroke_width: stroke_width,
                              add: add, visible: visible,
                              padding: padding, padding_top: padding_top,
                              padding_right: padding_right,
                              padding_bottom: padding_bottom,
                              padding_left: padding_left)
      @owns_visual = true
    else
      # Visual-less: no rendering, no @visual. Just an interactive region.
      @visual = nil
      @owns_visual = false
    end
  end

  if label
    @label = Text.new(label, x: 0, y: 0, z: @z + 1,
                      size: label_size, color: label_color || label_colour || 'white',
                      add: add, visible: visible)
    center_label
  end

  if @visual && (hover_input || pressed_input || hover_label_input || pressed_label_input)
    install_state_tint(hover_input, pressed_input,
                       hover_label_input, pressed_label_input)
  end
  hook_visual_alignment if @owns_visual

  on(:click) { |e| on_click.call(e) } if on_click

  # Honor `add: false` for interactivity, not just rendering. Registering the
  # click/tint handlers above auto-added this Button to the interactive
  # registry; without this an `add: false` Button would keep swallowing
  # clicks while its un-added visual draws nothing. Only touches the registry
  # when handlers exist, so a handler-less Button doesn't force a window into
  # being. `add`/`remove` manage registration thereafter.
  Window.unregister_interactive(self) if interactive? && !add
end

Instance Attribute Details

#label_textObject (readonly)

Returns the value of attribute label_text.



11
12
13
# File 'lib/ruby2d/button.rb', line 11

def label_text
  @label_text
end

Instance Method Details

#addObject

Re-add the button after remove. Restores rendering and re-registers in the interactive registry if any handlers are attached.



120
121
122
123
124
# File 'lib/ruby2d/button.rb', line 120

def add
  @visual.add if @visual
  @label.add if @label
  Window.register_interactive(self) if interactive?
end

#colorObject Also known as: colour

Get the button's fill color. When a hover/pressed tint is configured this returns the resting (base) color you set — not the transient tint while hovered or pressed — so it stays symmetric with color=. Otherwise it delegates to the visual. Returns nil when there is no colorable visual.



234
235
236
237
238
# File 'lib/ruby2d/button.rb', line 234

def color
  return @base_color if @base_color

  @visual.respond_to?(:color) ? @visual.color : nil
end

#color=(c) ⇒ Object

Set the button's fill color. When a hover/pressed tint is configured this updates the resting (base) color and re-applies the current state so the change survives the next hover/press cycle; otherwise it sets the visual directly. Accepts a single color or a gradient (Color::Set). A no-op on a visual-less button.



245
246
247
248
249
250
251
252
253
254
# File 'lib/ruby2d/button.rb', line 245

def color=(c)
  return unless @visual.respond_to?(:color=)

  if @base_color
    @base_color = Color.set(c)
    apply_state
  else
    @visual.color = c
  end
end

#colour=(c) ⇒ Object



258
259
260
# File 'lib/ruby2d/button.rb', line 258

def colour=(c)
  self.color = c
end

#contains?(x, y) ⇒ Boolean

Hit-test the button. Wrapped visuals delegate so non-rectangular shapes (Circle, Polygon, …) test against their actual geometry.

Returns:

  • (Boolean)


145
146
147
148
149
# File 'lib/ruby2d/button.rb', line 145

def contains?(x, y)
  return @visual.contains?(x, y) if @visual && !@owns_visual

  x >= @x && x <= (@x + @width) && y >= @y && y <= (@y + @height)
end

#heightObject



191
192
193
# File 'lib/ruby2d/button.rb', line 191

def height
  @owns_visual ? @height : (@visual && @visual.respond_to?(:height) ? @visual.height : @height)
end

#hideObject

Mark the button hidden (cascades to visual and label).



133
134
135
136
# File 'lib/ruby2d/button.rb', line 133

def hide
  @visual.hide if @visual
  @label.hide if @label
end

#labelObject

Get the label string.



218
219
220
# File 'lib/ruby2d/button.rb', line 218

def label
  @label ? @label.content : nil
end

#label=(text) ⇒ Object

Set the label string.



223
224
225
226
227
228
# File 'lib/ruby2d/button.rb', line 223

def label=(text)
  return unless @label

  @label.content = text
  center_label
end

#removeObject

Remove the button: drop visual + label from the render list and unregister Button from the interactive registry.



112
113
114
115
116
# File 'lib/ruby2d/button.rb', line 112

def remove
  @visual.remove if @visual
  @label.remove if @label
  Window.unregister_interactive(self)
end

#showObject

Mark the button visible (cascades to visual and label).



127
128
129
130
# File 'lib/ruby2d/button.rb', line 127

def show
  @visual.show if @visual
  @label.show if @label
end

#visible?Boolean

Whether the button is currently visible.

Returns:

  • (Boolean)


139
140
141
# File 'lib/ruby2d/button.rb', line 139

def visible?
  @visual ? @visual.visible? : true
end

#widthObject



187
188
189
# File 'lib/ruby2d/button.rb', line 187

def width
  @owns_visual ? @width : (@visual && @visual.respond_to?(:width) ? @visual.width : @width)
end

#xObject



151
152
153
# File 'lib/ruby2d/button.rb', line 151

def x
  @owns_visual ? @x : (@visual ? @visual.x : @x)
end

#x=(x) ⇒ Object

Set the x position. Pass a symbol (:left, :center, :right) to set alignment intent on the underlying visual (owned visuals only).



157
158
159
160
161
162
163
164
165
# File 'lib/ruby2d/button.rb', line 157

def x=(x)
  return self.x_align = x if x.is_a?(Symbol)

  @x = x
  # Move the visual too — owned or wrapped — so the visual, the (delegated)
  # hit region, the getter, and the label all move together.
  @visual.x = x if @visual.respond_to?(:x=)
  center_label if @label
end

#x_alignObject

Horizontal alignment intent of the underlying visual, or nil.



196
197
198
# File 'lib/ruby2d/button.rb', line 196

def x_align
  @visual.respond_to?(:x_align) ? @visual.x_align : nil
end

#x_align=(sym) ⇒ Object

Set alignment intent on the underlying visual (owned visuals only), so these mirror the getters above rather than writing a @x_align the getters would never read. A wrapped visual is positioned by its owner, so alignment there is the caller's to set on the visual itself.



209
210
211
# File 'lib/ruby2d/button.rb', line 209

def x_align=(sym)
  @visual.x_align = sym if @owns_visual
end

#yObject



167
168
169
# File 'lib/ruby2d/button.rb', line 167

def y
  @owns_visual ? @y : (@visual ? @visual.y : @y)
end

#y=(y) ⇒ Object

Set the y position. Pass a symbol (:top, :center, :bottom) to set alignment intent on the underlying visual (owned visuals only).



173
174
175
176
177
178
179
180
181
# File 'lib/ruby2d/button.rb', line 173

def y=(y)
  return self.y_align = y if y.is_a?(Symbol)

  @y = y
  # Move the visual too — owned or wrapped — so the visual, the (delegated)
  # hit region, the getter, and the label all move together.
  @visual.y = y if @visual.respond_to?(:y=)
  center_label if @label
end

#y_alignObject

Vertical alignment intent of the underlying visual, or nil.



201
202
203
# File 'lib/ruby2d/button.rb', line 201

def y_align
  @visual.respond_to?(:y_align) ? @visual.y_align : nil
end

#y_align=(sym) ⇒ Object



213
214
215
# File 'lib/ruby2d/button.rb', line 213

def y_align=(sym)
  @visual.y_align = sym if @owns_visual
end

#zObject



183
184
185
# File 'lib/ruby2d/button.rb', line 183

def z
  @owns_visual ? @z : (@visual && @visual.respond_to?(:z) ? @visual.z : @z)
end