Class: Tuile::Component::MenuBar::Item

Inherits:
Object
  • Object
show all
Defined in:
lib/tuile/component/menu_bar.rb,
sig/tuile.rbs

Overview

One menu item: a caption, an optional click listener, and its children.

file = bar.add_item("File")       # minted by the bar
file.add_item("New") { create }   # …and nested by the same method
file.items.size                   # => 1

An item with children is a submenu and its own listener is dead (#submenu? decides). An item with neither children nor a listener is legal and inert: it highlights, Enter closes the menu, nothing happens — an item that looks live but does nothing is the app's error to fix, not the framework's to raise on.

Apps don't construct items; #add_item and #add_item do.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(caption, mnemonic, on_click) ⇒ Item

@param caption — already coerced by the caller.

@param mnemonic — already validated by the caller, in the case it was given in.

@param on_click

Parameters:

  • caption (StyledString)
  • mnemonic (String, nil)
  • on_click (Proc, Method, nil)


100
101
102
103
104
105
106
# File 'lib/tuile/component/menu_bar.rb', line 100

def initialize(caption, mnemonic, on_click)
  @caption = caption
  @mnemonic = mnemonic&.downcase
  @cued_caption = build_cued_caption(caption, mnemonic)
  @on_click = on_click
  @items = []
end

Instance Attribute Details

#captionStyledString (readonly)

@return — the label painted on the strip or the row.

Returns:



111
112
113
# File 'lib/tuile/component/menu_bar.rb', line 111

def caption
  @caption
end

#cued_captionStyledString (readonly)

@return#caption with the #mnemonic underlined — what both paint sites draw. Equal to #caption when there is no mnemonic or the caption doesn't contain it. Computed once, at construction: caption and mnemonic are both fixed there, and underline is a plain attribute with no theme or bg_color input, so this is not a cached theme value.

Returns:



123
124
125
# File 'lib/tuile/component/menu_bar.rb', line 123

def cued_caption
  @cued_caption
end

#items::Array[Item] (readonly)

@return — this item's children, in menu order. Read-only by convention, like Tuile::Component#children — grow it through #add_item.

Returns:



127
128
129
# File 'lib/tuile/component/menu_bar.rb', line 127

def items
  @items
end

#mnemonicString? (readonly)

@return — the downcased letter that activates this item while its own level is the live one; nil when it has none.

Returns:

  • (String, nil)


115
116
117
# File 'lib/tuile/component/menu_bar.rb', line 115

def mnemonic
  @mnemonic
end

#on_clickProc, ...

@return — no-arg callable fired when the item is activated (Enter, Space or a left click), exactly as Button#on_click. Never fired on an item with children.

Returns:

  • (Proc, Method, nil)


132
133
134
# File 'lib/tuile/component/menu_bar.rb', line 132

def on_click
  @on_click
end

Instance Method Details

#add_item(caption = nil, mnemonic: nil, &on_click) ⇒ Item

Appends a child and returns its handle.

@param caption — parsed as StyledString.parse parses it.

@param mnemonic — the letter that activates this child while this item's children are the live level; see Tuile::Component::MenuBar#add_item.

Parameters:

  • caption (?(String | StyledString), nil) (defaults to: nil)
  • mnemonic: (String, nil) (defaults to: nil)

Returns:



146
147
148
149
# File 'lib/tuile/component/menu_bar.rb', line 146

def add_item(caption = nil, mnemonic: nil, &on_click)
  validate_mnemonic(mnemonic)
  Item.send(:new, StyledString.parse(caption), mnemonic, on_click).tap { @items << _1 }
end

#build_cued_caption(caption, mnemonic) ⇒ StyledString

StyledString#slice counts columns while a caption search yields a character index, so the prefix is measured, never counted.

@param caption

@param mnemonic — in the case it was given in.

Parameters:

Returns:



185
186
187
188
189
190
191
192
193
194
195
196
197
# File 'lib/tuile/component/menu_bar.rb', line 185

def build_cued_caption(caption, mnemonic)
  return caption if mnemonic.nil?

  text = caption.to_s
  # Exact case first, so "Save As" can underline either "a" via the case
  # it was given in.
  index = text.index(mnemonic) || text.downcase.index(mnemonic.downcase)
  return caption if index.nil?

  start = StyledString.plain(text[0, index]).display_width
  caption.slice(0, start) + caption.slice(start, 1).with_underline +
    caption.slice(start + 1, caption.display_width - start - 1)
end

#inspectString

Returns:

  • (String)


152
153
154
155
# File 'lib/tuile/component/menu_bar.rb', line 152

def inspect
  mn = @mnemonic.nil? ? "" : " [#{@mnemonic}]"
  "#<#{self.class.name} #{caption.to_s.inspect}#{mn}#{submenu? ? " (#{@items.size} items)" : ""}>"
end

@return — whether this item opens a submenu, i.e. has children.

Returns:

  • (Boolean)


135
# File 'lib/tuile/component/menu_bar.rb', line 135

def submenu? = !@items.empty?

#validate_mnemonic(mnemonic) ⇒ void

This method returns an undefined value.

Rejects a mnemonic that couldn't work, or that would make two siblings ambiguous — all three at registration, since none has a sane answer at keypress time.

@param mnemonic

Parameters:

  • mnemonic (String, nil)


165
166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/tuile/component/menu_bar.rb', line 165

def validate_mnemonic(mnemonic)
  return if mnemonic.nil?

  # Not implied by printable?, which accepts " ".
  raise ArgumentError, "mnemonic must not be a space: Space activates the highlighted item" if mnemonic == " "
  unless Keys.printable?(mnemonic) && StyledString.plain(mnemonic).display_width == 1
    raise ArgumentError, "mnemonic must be a single one-column printable character; got #{mnemonic.inspect}"
  end

  down = mnemonic.downcase
  return unless @items.any? { |item| item.mnemonic == down }

  raise ArgumentError, "duplicate mnemonic #{down.inspect} among these menu items"
end