mirror of
https://github.com/infinitefusion/infinitefusion-e18.git
synced 2025-12-08 13:44:59 +00:00
Rearranged and renamed Animation Editor-related script files
This commit is contained in:
2
Data/Scripts/801_UI controls/001_UIControls.rb
Normal file
2
Data/Scripts/801_UI controls/001_UIControls.rb
Normal file
@@ -0,0 +1,2 @@
|
||||
# Container module for control classes.
|
||||
module UIControls; end
|
||||
192
Data/Scripts/801_UI controls/002_ControlsContainer.rb
Normal file
192
Data/Scripts/801_UI controls/002_ControlsContainer.rb
Normal file
@@ -0,0 +1,192 @@
|
||||
#===============================================================================
|
||||
# Controls are arranged in a list in self's bitmap. Each control is given a
|
||||
# "self's bitmap's width" x LINE_SPACING area of self's bitmap to draw itself
|
||||
# in.
|
||||
# TODO: The act of "capturing" a control makes other controls in this container
|
||||
# not update themselves, i.e. they won't colour themselves with a hover
|
||||
# highlight if the mouse happens to move over it while another control is
|
||||
# captured. Is there a better way of dealing with this? I'm leaning
|
||||
# towards the control itself deciding if it's captured, and it being
|
||||
# treated as uncaptured once it says its value has changed, but I think
|
||||
# this would require manually telling all other controls in this container
|
||||
# that something else is captured and they shouldn't show a hover
|
||||
# highlight when updated (perhaps as a parameter in def update), which I
|
||||
# don't think is ideal.
|
||||
#===============================================================================
|
||||
class UIControls::ControlsContainer
|
||||
attr_reader :x, :y
|
||||
attr_reader :controls
|
||||
attr_reader :values
|
||||
attr_reader :visible
|
||||
|
||||
LINE_SPACING = 28
|
||||
OFFSET_FROM_LABEL_X = 90
|
||||
OFFSET_FROM_LABEL_Y = 0
|
||||
|
||||
def initialize(x, y, width, height)
|
||||
@viewport = Viewport.new(x, y, width, height)
|
||||
@viewport.z = 99999
|
||||
@x = x
|
||||
@y = y
|
||||
@width = width
|
||||
@height = height
|
||||
@controls = []
|
||||
@control_rects = []
|
||||
@row_count = 0
|
||||
@captured = nil
|
||||
@visible = true
|
||||
end
|
||||
|
||||
def dispose
|
||||
@controls.each { |c| c[1]&.dispose }
|
||||
@controls.clear
|
||||
@viewport.dispose
|
||||
end
|
||||
|
||||
def busy?
|
||||
return !@captured.nil?
|
||||
end
|
||||
|
||||
def changed?
|
||||
return !@values.nil?
|
||||
end
|
||||
|
||||
def clear_changed
|
||||
@values = nil
|
||||
end
|
||||
|
||||
def visible=(value)
|
||||
@visible = value
|
||||
@controls.each { |c| c[1].visible = value }
|
||||
repaint if @visible
|
||||
end
|
||||
|
||||
def get_control(id)
|
||||
ret = nil
|
||||
@controls.each do |c|
|
||||
ret = c[1] if c[0] == id
|
||||
break if ret
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
def add_label(id, label, has_label = false)
|
||||
id = (id.to_s + "_label").to_sym
|
||||
add_control(id, UIControls::Label.new(*control_size(has_label), @viewport, label), has_label)
|
||||
end
|
||||
|
||||
def add_checkbox(id, value, has_label = false)
|
||||
add_control(id, UIControls::Checkbox.new(*control_size(has_label), @viewport, value), has_label)
|
||||
end
|
||||
|
||||
def add_labelled_checkbox(id, label, value)
|
||||
add_label(id, label)
|
||||
add_checkbox(id, value, true)
|
||||
end
|
||||
|
||||
def add_text_box(id, value, has_label = false)
|
||||
add_control(id, UIControls::TextBox.new(*control_size(has_label), @viewport, value), has_label)
|
||||
end
|
||||
|
||||
def add_labelled_text_box(id, label, value)
|
||||
add_label(id, label)
|
||||
add_text_box(id, value, true)
|
||||
end
|
||||
|
||||
def add_number_slider(id, min_value, max_value, value, has_label = false)
|
||||
add_control(id, UIControls::NumberSlider.new(*control_size(has_label), @viewport, min_value, max_value, value), has_label)
|
||||
end
|
||||
|
||||
def add_labelled_number_slider(id, label, min_value, max_value, value)
|
||||
add_label(id, label)
|
||||
add_number_slider(id, min_value, max_value, value, true)
|
||||
end
|
||||
|
||||
def add_number_text_box(id, min_value, max_value, value, has_label = false)
|
||||
add_control(id, UIControls::NumberTextBox.new(*control_size(has_label), @viewport, min_value, max_value, value), has_label)
|
||||
end
|
||||
|
||||
def add_labelled_number_text_box(id, label, min_value, max_value, value)
|
||||
add_label(id, label)
|
||||
add_number_text_box(id, min_value, max_value, value, true)
|
||||
end
|
||||
|
||||
def add_button(id, button_text, has_label = false)
|
||||
add_control(id, UIControls::Button.new(*control_size(has_label), @viewport, button_text), has_label)
|
||||
end
|
||||
|
||||
def add_labelled_button(id, label, button_text)
|
||||
add_label(id, label)
|
||||
add_button(id, button_text, true)
|
||||
end
|
||||
|
||||
def add_dropdown_list(id, options, value, has_label = false)
|
||||
add_control(id, UIControls::DropdownList.new(*control_size(has_label), @viewport, options, value), has_label)
|
||||
end
|
||||
|
||||
def add_labelled_dropdown_list(id, label, options, value)
|
||||
add_label(id, label)
|
||||
add_dropdown_list(id, options, value, true)
|
||||
end
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
def repaint
|
||||
@controls.each { |ctrl| ctrl[1].repaint }
|
||||
end
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
def update
|
||||
return if !@visible
|
||||
# Update controls
|
||||
if @captured
|
||||
# TODO: Ideally all controls will be updated here, if only to redraw
|
||||
# themselves if they happen to be invalidated somehow. But that
|
||||
# involves telling each control whether any other control is busy,
|
||||
# to ensure that they don't show their hover colours or anything,
|
||||
# which is fiddly and I'm not sure if it's the best approach.
|
||||
@captured.update
|
||||
@captured = nil if !@captured.busy?
|
||||
else
|
||||
@controls.each do |ctrl|
|
||||
ctrl[1].update
|
||||
@captured = ctrl[1] if ctrl[1].busy?
|
||||
end
|
||||
end
|
||||
# Check for updated controls
|
||||
@controls.each do |ctrl|
|
||||
next if !ctrl[1].changed?
|
||||
@values ||= {}
|
||||
@values[ctrl[0]] = ctrl[1].value
|
||||
ctrl[1].clear_changed
|
||||
end
|
||||
# Redraw controls if needed
|
||||
repaint
|
||||
end
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
private
|
||||
|
||||
def control_size(has_label = false)
|
||||
if has_label
|
||||
return @width - OFFSET_FROM_LABEL_X, LINE_SPACING - OFFSET_FROM_LABEL_Y
|
||||
end
|
||||
return @width, LINE_SPACING
|
||||
end
|
||||
|
||||
def add_control(id, control, add_offset = false)
|
||||
i = @controls.length
|
||||
control_y = (add_offset ? @row_count - 1 : @row_count) * LINE_SPACING
|
||||
@control_rects[i] = Rect.new(0, control_y, control.width, control.height)
|
||||
control.x = @control_rects[i].x + (add_offset ? OFFSET_FROM_LABEL_X : 0)
|
||||
control.y = @control_rects[i].y + (add_offset ? OFFSET_FROM_LABEL_Y : 0)
|
||||
control.set_interactive_rects
|
||||
@controls[i] = [id, control]
|
||||
@row_count += 1 if !add_offset
|
||||
repaint
|
||||
end
|
||||
end
|
||||
185
Data/Scripts/801_UI controls/Control elements/001_BaseControl.rb
Normal file
185
Data/Scripts/801_UI controls/Control elements/001_BaseControl.rb
Normal file
@@ -0,0 +1,185 @@
|
||||
# TODO: Add "disabled" greying out/non-editable.
|
||||
# TODO: Add indicator of whether the control's value is "lerping" between frames
|
||||
# (use yellow somehow?).
|
||||
|
||||
#===============================================================================
|
||||
#
|
||||
#===============================================================================
|
||||
class UIControls::BaseControl < BitmapSprite
|
||||
attr_reader :value
|
||||
# attr_accessor :disabled # TODO: Make use of this.
|
||||
|
||||
TEXT_COLOR = Color.black
|
||||
TEXT_SIZE = 18 # Default is 22 if size isn't explicitly set
|
||||
HOVER_COLOR = Color.cyan # For clickable area when hovering over it
|
||||
CAPTURE_COLOR = Color.pink # For area you clicked in but aren't hovering over
|
||||
|
||||
def initialize(width, height, viewport)
|
||||
super(width, height, viewport)
|
||||
self.bitmap.font.color = TEXT_COLOR
|
||||
self.bitmap.font.size = TEXT_SIZE
|
||||
|
||||
# @disabled = false # TODO: Make use of this.
|
||||
@hover_area = nil # Is a symbol from the keys for @interactions if the mouse is hovering over that interaction
|
||||
@captured_area = nil # Is a symbol from the keys for @interactions (or :none) if this control is clicked in
|
||||
clear_changed
|
||||
invalidate
|
||||
end
|
||||
|
||||
def width
|
||||
return self.bitmap.width
|
||||
end
|
||||
|
||||
def height
|
||||
return self.bitmap.height
|
||||
end
|
||||
|
||||
def mouse_pos
|
||||
mouse_coords = Mouse.getMousePos
|
||||
return nil, nil if !mouse_coords
|
||||
ret_x = mouse_coords[0] - self.viewport.rect.x - self.x
|
||||
ret_y = mouse_coords[1] - self.viewport.rect.y - self.y
|
||||
return ret_x, ret_y
|
||||
end
|
||||
|
||||
def set_interactive_rects
|
||||
@interactions = {}
|
||||
end
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
def invalid?
|
||||
return @invalid
|
||||
end
|
||||
|
||||
# Marks that the control must be redrawn to reflect current logic.
|
||||
def invalidate
|
||||
@invalid = true
|
||||
end
|
||||
|
||||
# Makes the control no longer invalid.
|
||||
def validate
|
||||
@invalid = false
|
||||
end
|
||||
|
||||
def busy?
|
||||
return !@captured_area.nil?
|
||||
end
|
||||
|
||||
def changed?
|
||||
return @changed
|
||||
end
|
||||
|
||||
def set_changed
|
||||
@changed = true
|
||||
end
|
||||
|
||||
def clear_changed
|
||||
@changed = false
|
||||
end
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
def draw_text(this_bitmap, text_x, text_y, this_text)
|
||||
text_size = this_bitmap.text_size(this_text)
|
||||
this_bitmap.draw_text(text_x, text_y, text_size.width, text_size.height, this_text, 0)
|
||||
end
|
||||
|
||||
def draw_text_centered(this_bitmap, text_x, text_y, wid, this_text)
|
||||
text_size = this_bitmap.text_size(this_text)
|
||||
this_bitmap.draw_text(text_x, text_y, wid, text_size.height, this_text, 1)
|
||||
end
|
||||
|
||||
# Redraws the control only if it is invalid.
|
||||
def repaint
|
||||
return if !invalid?
|
||||
refresh
|
||||
validate
|
||||
end
|
||||
|
||||
def refresh
|
||||
# Paint over control to erase contents (intentionally not using self.bitmap.clear)
|
||||
self.bitmap.clear
|
||||
draw_area_highlight
|
||||
end
|
||||
|
||||
def draw_area_highlight
|
||||
return if !@interactions || @interactions.empty?
|
||||
if !@captured_area || @hover_area == @captured_area
|
||||
# Draw mouse hover over area highlight
|
||||
rect = @interactions[@hover_area]
|
||||
self.bitmap.fill_rect(rect.x, rect.y, rect.width, rect.height, HOVER_COLOR) if rect
|
||||
elsif @captured_area
|
||||
# Draw captured area highlight
|
||||
rect = @interactions[@captured_area]
|
||||
self.bitmap.fill_rect(rect.x, rect.y, rect.width, rect.height, CAPTURE_COLOR) if rect
|
||||
end
|
||||
end
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
# This method is only called if the mouse is in the game window and this
|
||||
# control has interactive elements.
|
||||
def on_mouse_press
|
||||
return if !@interactions || @interactions.empty?
|
||||
return if @captured_area
|
||||
@captured_area = nil
|
||||
mouse_x, mouse_y = mouse_pos
|
||||
return if !mouse_x || !mouse_y
|
||||
@interactions.each_pair do |area, rect|
|
||||
next if !rect.contains?(mouse_x, mouse_y)
|
||||
@captured_area = area
|
||||
invalidate
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
def on_mouse_release
|
||||
@captured_area = nil
|
||||
invalidate
|
||||
end
|
||||
|
||||
def update_hover_highlight
|
||||
# Remove the hover highlight if there are no interactions for this control
|
||||
# or if the mouse is off-screen
|
||||
mouse_x, mouse_y = mouse_pos
|
||||
if !@interactions || @interactions.empty? || !mouse_x || !mouse_y
|
||||
invalidate if @hover_area
|
||||
@hover_area = nil
|
||||
return
|
||||
end
|
||||
# Check each interactive area for whether the mouse is hovering over it, and
|
||||
# set @hover_area accordingly
|
||||
in_area = false
|
||||
@interactions.each_pair do |area, rect|
|
||||
next if !rect.contains?(mouse_x, mouse_y)
|
||||
invalidate if @hover_area != area
|
||||
@hover_area = area
|
||||
in_area = true
|
||||
break
|
||||
end
|
||||
if !in_area
|
||||
invalidate if @hover_area
|
||||
@hover_area = nil
|
||||
end
|
||||
end
|
||||
|
||||
# Updates the logic on the control, invalidating it if necessary.
|
||||
def update
|
||||
return if !self.visible
|
||||
# TODO: Disabled control stuff.
|
||||
# return if self.disabled
|
||||
|
||||
update_hover_highlight
|
||||
|
||||
# Detect a mouse press/release
|
||||
if @interactions && !@interactions.empty?
|
||||
if Input.trigger?(Input::MOUSELEFT)
|
||||
on_mouse_press
|
||||
elsif busy? && Input.release?(Input::MOUSELEFT)
|
||||
on_mouse_release
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
24
Data/Scripts/801_UI controls/Control elements/002_Label.rb
Normal file
24
Data/Scripts/801_UI controls/Control elements/002_Label.rb
Normal file
@@ -0,0 +1,24 @@
|
||||
#===============================================================================
|
||||
#
|
||||
#===============================================================================
|
||||
class UIControls::Label < UIControls::BaseControl
|
||||
attr_reader :label
|
||||
|
||||
LABEL_END_X = 80
|
||||
TEXT_OFFSET_Y = 5
|
||||
|
||||
def initialize(width, height, viewport, label)
|
||||
super(width, height, viewport)
|
||||
@label = label
|
||||
end
|
||||
|
||||
def label=(value)
|
||||
@label = value
|
||||
refresh
|
||||
end
|
||||
|
||||
def refresh
|
||||
super
|
||||
draw_text(self.bitmap, 4, TEXT_OFFSET_Y, @label)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,68 @@
|
||||
#===============================================================================
|
||||
# NOTE: Strictly speaking, this is a toggle switch and not a checkbox.
|
||||
#===============================================================================
|
||||
class UIControls::Checkbox < UIControls::BaseControl
|
||||
CHECKBOX_X = 2
|
||||
CHECKBOX_WIDTH = 40
|
||||
CHECKBOX_HEIGHT = 24
|
||||
CHECKBOX_FILL_SIZE = CHECKBOX_HEIGHT - 4
|
||||
|
||||
UNCHECKED_COLOR = Color.gray
|
||||
CHECKED_COLOR = Color.new(64, 255, 64) # Green
|
||||
|
||||
def initialize(width, height, viewport, value = false)
|
||||
super(width, height, viewport)
|
||||
@value = value
|
||||
end
|
||||
|
||||
def value=(new_value)
|
||||
return if @value == new_value
|
||||
@value = new_value
|
||||
invalidate
|
||||
end
|
||||
|
||||
def set_interactive_rects
|
||||
@checkbox_rect = Rect.new(CHECKBOX_X, (height - CHECKBOX_HEIGHT) / 2,
|
||||
CHECKBOX_WIDTH, CHECKBOX_HEIGHT)
|
||||
@interactions = {
|
||||
:checkbox => @checkbox_rect
|
||||
}
|
||||
end
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
def refresh
|
||||
super
|
||||
# Draw checkbox outline
|
||||
self.bitmap.outline_rect(@checkbox_rect.x, @checkbox_rect.y,
|
||||
@checkbox_rect.width, @checkbox_rect.height,
|
||||
self.bitmap.font.color)
|
||||
# Draw checkbox fill
|
||||
if @value # If checked
|
||||
self.bitmap.fill_rect(@checkbox_rect.x + @checkbox_rect.width - CHECKBOX_FILL_SIZE - 2, @checkbox_rect.y + 2,
|
||||
CHECKBOX_FILL_SIZE, CHECKBOX_FILL_SIZE, CHECKED_COLOR)
|
||||
self.bitmap.outline_rect(@checkbox_rect.x + @checkbox_rect.width - CHECKBOX_FILL_SIZE - 2, @checkbox_rect.y + 2,
|
||||
CHECKBOX_FILL_SIZE, CHECKBOX_FILL_SIZE, self.bitmap.font.color)
|
||||
else
|
||||
self.bitmap.fill_rect(@checkbox_rect.x + 2, @checkbox_rect.y + 2,
|
||||
CHECKBOX_FILL_SIZE, CHECKBOX_FILL_SIZE, UNCHECKED_COLOR)
|
||||
self.bitmap.outline_rect(@checkbox_rect.x + 2, @checkbox_rect.y + 2,
|
||||
CHECKBOX_FILL_SIZE, CHECKBOX_FILL_SIZE, self.bitmap.font.color)
|
||||
end
|
||||
end
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
def on_mouse_release
|
||||
return if !@captured_area # Wasn't captured to begin with
|
||||
# Change this control's value
|
||||
if @captured_area == :checkbox
|
||||
mouse_x, mouse_y = mouse_pos
|
||||
if mouse_x && mouse_y && @interactions[@captured_area].contains?(mouse_x, mouse_y)
|
||||
@value = !@value # The actual change of this control's value
|
||||
set_changed
|
||||
end
|
||||
end
|
||||
super # Make this control not busy again
|
||||
end
|
||||
end
|
||||
293
Data/Scripts/801_UI controls/Control elements/004_TextBox.rb
Normal file
293
Data/Scripts/801_UI controls/Control elements/004_TextBox.rb
Normal file
@@ -0,0 +1,293 @@
|
||||
#===============================================================================
|
||||
# TODO: Support selecting part of the text by remembering the initial
|
||||
# cursor position and using it and the current cursor position to
|
||||
# decide which characters are selected. Maybe? Note that this method
|
||||
# is only triggered upon the initial mouse press, and isn't repeated
|
||||
# while it's still held down.
|
||||
#===============================================================================
|
||||
class UIControls::TextBox < UIControls::BaseControl
|
||||
TEXT_BOX_X = 2
|
||||
TEXT_BOX_WIDTH = 172
|
||||
TEXT_BOX_HEIGHT = 24
|
||||
TEXT_BOX_PADDING = 4 # Gap between sides of text box and text
|
||||
TEXT_OFFSET_Y = 5
|
||||
|
||||
def initialize(width, height, viewport, value = "")
|
||||
super(width, height, viewport)
|
||||
@value = value
|
||||
@cursor_pos = -1
|
||||
@display_pos = 0
|
||||
@cursor_timer = nil
|
||||
@cursor_shown = false
|
||||
end
|
||||
|
||||
def value=(new_value)
|
||||
return if @value == new_value
|
||||
@value = new_value
|
||||
invalidate
|
||||
end
|
||||
|
||||
def insert_char(ch)
|
||||
@value.insert(@cursor_pos, ch)
|
||||
@cursor_pos += 1
|
||||
@cursor_timer = System.uptime
|
||||
@cursor_shown = true
|
||||
invalidate
|
||||
end
|
||||
|
||||
def delete_at(index)
|
||||
@value.slice!(index)
|
||||
@cursor_pos -= 1 if @cursor_pos > index
|
||||
@cursor_timer = System.uptime
|
||||
@cursor_shown = true
|
||||
invalidate
|
||||
end
|
||||
|
||||
def cursor_pos=(val)
|
||||
@cursor_pos = val
|
||||
reset_display_pos
|
||||
@cursor_timer = System.uptime
|
||||
@cursor_shown = true
|
||||
invalidate
|
||||
end
|
||||
|
||||
def set_interactive_rects
|
||||
@text_box_rect = Rect.new(TEXT_BOX_X, (height - TEXT_BOX_HEIGHT) / 2,
|
||||
[TEXT_BOX_WIDTH, width].min, TEXT_BOX_HEIGHT)
|
||||
@interactions = {
|
||||
:text_box => @text_box_rect
|
||||
}
|
||||
end
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
def busy?
|
||||
return @cursor_pos >= 0 if @captured_area == :text_box
|
||||
return super
|
||||
end
|
||||
|
||||
def reset_interaction
|
||||
@cursor_pos = -1
|
||||
@display_pos = 0
|
||||
@cursor_timer = nil
|
||||
@initial_value = nil
|
||||
Input.text_input = false
|
||||
invalidate
|
||||
end
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
def get_cursor_index_from_mouse_position
|
||||
char_widths = []
|
||||
@value.to_s.length.times { |i| char_widths[i] = self.bitmap.text_size(@value.to_s[i]).width }
|
||||
mouse_x, mouse_y = mouse_pos
|
||||
mouse_x -= @text_box_rect.x + TEXT_BOX_PADDING
|
||||
return 0 if mouse_x < 0
|
||||
(@display_pos...char_widths.length).each do |i|
|
||||
mouse_x -= char_widths[i]
|
||||
if mouse_x <= 0
|
||||
return (mouse_x.abs >= char_widths[i] / 2) ? i : i + 1
|
||||
end
|
||||
end
|
||||
return @value.to_s.length
|
||||
end
|
||||
|
||||
def reset_display_pos
|
||||
box_width = @text_box_rect.width - (TEXT_BOX_PADDING * 2)
|
||||
char_widths = []
|
||||
@value.to_s.length.times { |i| char_widths[i] = self.bitmap.text_size(@value.to_s[i]).width }
|
||||
# Text isn't wider than the box
|
||||
if char_widths.sum <= box_width
|
||||
return false if @display_pos == 0
|
||||
@display_pos = 0
|
||||
return true
|
||||
end
|
||||
display_pos_changed = false
|
||||
# Ensure the cursor hasn't gone off the left side of the text box
|
||||
if @cursor_pos < @display_pos
|
||||
@display_pos = @cursor_pos
|
||||
display_pos_changed = true
|
||||
end
|
||||
# Ensure the cursor hasn't gone off the right side of the text box
|
||||
if @cursor_pos > @display_pos
|
||||
loop do
|
||||
cursor_x = 0
|
||||
(@display_pos...@cursor_pos).each do |i|
|
||||
cursor_x += char_widths[i] if char_widths[i]
|
||||
end
|
||||
break if cursor_x < box_width
|
||||
@display_pos += 1
|
||||
display_pos_changed = true
|
||||
break if @display_pos == @cursor_pos
|
||||
end
|
||||
end
|
||||
# Ensure there isn't empty space on the right if the text can be moved to
|
||||
# the right to fill it
|
||||
if @display_pos > 0
|
||||
cursor_x = 0
|
||||
(@display_pos...char_widths.length).each do |i|
|
||||
cursor_x += char_widths[i] if char_widths[i]
|
||||
end
|
||||
loop do
|
||||
cursor_x += char_widths[@display_pos - 1]
|
||||
break if cursor_x >= box_width
|
||||
@display_pos -= 1
|
||||
display_pos_changed = true
|
||||
break if @display_pos == 0
|
||||
end
|
||||
end
|
||||
return display_pos_changed
|
||||
end
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
def draw_area_highlight
|
||||
return if @captured_area == :text_box && (@hover_area == @captured_area || !Input.press?(Input::MOUSELEFT))
|
||||
super
|
||||
end
|
||||
|
||||
def draw_cursor(cursor_x)
|
||||
return if !@cursor_shown || @cursor_pos < 0
|
||||
cursor_y_offset = ((height - TEXT_BOX_HEIGHT) / 2) + 2
|
||||
cursor_height = height - (cursor_y_offset * 2)
|
||||
bitmap.fill_rect(cursor_x, cursor_y_offset, 2, cursor_height, self.bitmap.font.color)
|
||||
end
|
||||
|
||||
def refresh
|
||||
super
|
||||
# Draw text box outline
|
||||
self.bitmap.outline_rect(@text_box_rect.x, @text_box_rect.y,
|
||||
@text_box_rect.width, @text_box_rect.height,
|
||||
self.bitmap.font.color)
|
||||
# Draw value
|
||||
char_x = @text_box_rect.x + TEXT_BOX_PADDING
|
||||
last_char_index = @display_pos
|
||||
(@value.to_s.length - @display_pos).times do |i|
|
||||
char = @value.to_s[@display_pos + i]
|
||||
char_width = self.bitmap.text_size(char).width
|
||||
cannot_display_next_char = char_x + char_width > @text_box_rect.x + @text_box_rect.width - TEXT_BOX_PADDING
|
||||
draw_text(self.bitmap, char_x, TEXT_OFFSET_Y, char) if !cannot_display_next_char
|
||||
# Draw cursor
|
||||
draw_cursor(char_x - 1) if @display_pos + i == @cursor_pos
|
||||
break if cannot_display_next_char
|
||||
last_char_index = @display_pos + i
|
||||
char_x += char_width
|
||||
end
|
||||
# Draw cursor at end
|
||||
draw_cursor(char_x - 1) if @cursor_pos == @value.to_s.length
|
||||
# Draw left/right arrows to indicate more text beyond the text box sides
|
||||
if @display_pos > 0
|
||||
bitmap.fill_rect(@text_box_rect.x, (height / 2) - 4, 1, 8, Color.white)
|
||||
5.times do |i|
|
||||
bitmap.fill_rect(@text_box_rect.x - 2 + i, (height / 2) - (i + 1), 1, 2 * (i + 1), self.bitmap.font.color)
|
||||
end
|
||||
end
|
||||
if last_char_index < @value.to_s.length - 1
|
||||
bitmap.fill_rect(@text_box_rect.x + @text_box_rect.width - 1, (height / 2) - 4, 1, 8, Color.white)
|
||||
5.times do |i|
|
||||
bitmap.fill_rect(@text_box_rect.x + @text_box_rect.width + 1 - i, (height / 2) - (i + 1), 1, 2 * (i + 1), self.bitmap.font.color)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
def on_mouse_press
|
||||
@captured_area = nil
|
||||
super
|
||||
if @captured_area == :text_box
|
||||
# Clicked into the text box; put the text cursor in there
|
||||
@cursor_pos = get_cursor_index_from_mouse_position
|
||||
@cursor_timer = System.uptime
|
||||
invalidate
|
||||
else
|
||||
@value.strip! if @value.respond_to?("strip!")
|
||||
set_changed if @initial_value && @value != @initial_value
|
||||
reset_interaction
|
||||
end
|
||||
end
|
||||
|
||||
def on_mouse_release
|
||||
return if !@captured_area # Wasn't captured to begin with
|
||||
# Start text entry if clicked and released mouse button in the text box
|
||||
if @captured_area == :text_box
|
||||
mouse_x, mouse_y = mouse_pos
|
||||
if mouse_x && mouse_y && @interactions[@captured_area].contains?(mouse_x, mouse_y)
|
||||
@initial_value = @value.clone
|
||||
Input.text_input = true
|
||||
invalidate
|
||||
return # This control is still captured
|
||||
end
|
||||
end
|
||||
# Released mouse button outside of text box, or initially clicked outside of
|
||||
# text box; end interaction with this control
|
||||
@value.strip! if @value.respond_to?("strip!")
|
||||
set_changed if @initial_value && @value != @initial_value
|
||||
reset_interaction
|
||||
super # Make this control not busy again
|
||||
end
|
||||
|
||||
def update_special_inputs
|
||||
# Left/right to move cursor
|
||||
if Input.triggerex?(:LEFT) || Input.repeatex?(:LEFT)
|
||||
self.cursor_pos = @cursor_pos - 1 if @cursor_pos > 0
|
||||
elsif Input.triggerex?(:RIGHT) || Input.repeatex?(:RIGHT)
|
||||
self.cursor_pos = @cursor_pos + 1 if @cursor_pos < @value.to_s.length
|
||||
end
|
||||
# Home/End to jump to start/end of the text
|
||||
if Input.triggerex?(:HOME) || Input.repeatex?(:HOME)
|
||||
self.cursor_pos = 0
|
||||
elsif Input.triggerex?(:END) || Input.repeatex?(:END)
|
||||
self.cursor_pos = @value.to_s.length
|
||||
end
|
||||
# Backspace/Delete to remove text
|
||||
if Input.triggerex?(:BACKSPACE) || Input.repeatex?(:BACKSPACE)
|
||||
delete_at(@cursor_pos - 1) if @cursor_pos > 0
|
||||
elsif Input.triggerex?(:DELETE) || Input.repeatex?(:DELETE)
|
||||
delete_at(@cursor_pos) if @cursor_pos < @value.to_s.length
|
||||
end
|
||||
# Return/Escape to end text input (Escape undoes the change)
|
||||
if Input.triggerex?(:RETURN) || Input.repeatex?(:RETURN) ||
|
||||
Input.triggerex?(:KP_ENTER) || Input.repeatex?(:KP_ENTER)
|
||||
@value.strip! if @value.respond_to?("strip!")
|
||||
set_changed if @initial_value && @value != @initial_value
|
||||
reset_interaction
|
||||
@captured_area = nil
|
||||
elsif Input.triggerex?(:ESCAPE) || Input.repeatex?(:ESCAPE)
|
||||
@value = @initial_value if @initial_value
|
||||
reset_interaction
|
||||
@captured_area = nil
|
||||
end
|
||||
end
|
||||
|
||||
def update_text_entry
|
||||
ret = false
|
||||
Input.gets.each_char do |ch|
|
||||
insert_char(ch)
|
||||
ret = true
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
def update
|
||||
return if !self.visible
|
||||
super
|
||||
# TODO: Disabled control stuff.
|
||||
# return if self.disabled
|
||||
# Make the cursor flash
|
||||
if @captured_area == :text_box
|
||||
cursor_to_show = ((System.uptime - @cursor_timer) / 0.35).to_i.even?
|
||||
if cursor_to_show != @cursor_shown
|
||||
@cursor_shown = cursor_to_show
|
||||
invalidate
|
||||
end
|
||||
old_cursor_pos = @cursor_pos
|
||||
# Update cursor movement, deletions and ending text input
|
||||
update_special_inputs
|
||||
return if @cursor_pos != old_cursor_pos || !busy?
|
||||
# Detect character input and add them to @value
|
||||
char_inserted = update_text_entry
|
||||
invalidate if reset_display_pos || char_inserted
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,131 @@
|
||||
#===============================================================================
|
||||
#
|
||||
#===============================================================================
|
||||
class UIControls::NumberSlider < UIControls::BaseControl
|
||||
attr_reader :min_value
|
||||
attr_reader :max_value
|
||||
|
||||
PLUS_MINUS_SIZE = 16
|
||||
SLIDER_PADDING = 6 # Gap between sides of interactive area for slider and drawn slider bar
|
||||
|
||||
MINUS_X = 0
|
||||
SLIDER_X = MINUS_X + PLUS_MINUS_SIZE + SLIDER_PADDING
|
||||
SLIDER_LENGTH = 128
|
||||
PLUS_X = SLIDER_X + SLIDER_LENGTH + SLIDER_PADDING
|
||||
VALUE_X = PLUS_X + PLUS_MINUS_SIZE + 5
|
||||
TEXT_OFFSET_Y = 5
|
||||
|
||||
# TODO: Is there a better knob design than a big black rectangle? I'd rather
|
||||
# it not be a different colour.
|
||||
SLIDER_KNOB_COLOR = Color.black
|
||||
|
||||
def initialize(width, height, viewport, min_value, max_value, value)
|
||||
super(width, height, viewport)
|
||||
@min_value = min_value
|
||||
@max_value = max_value
|
||||
self.value = value
|
||||
end
|
||||
|
||||
def value=(new_value)
|
||||
old_val = @value
|
||||
@value = new_value.to_i.clamp(self.min_value, self.max_value)
|
||||
self.invalidate if @value != old_val
|
||||
end
|
||||
|
||||
def min_value=(new_min)
|
||||
return if new_min == @min_value
|
||||
@min_value = new_min
|
||||
@value = @value.clamp(self.min_value, self.max_value)
|
||||
self.invalidate
|
||||
end
|
||||
|
||||
def max_value=(new_max)
|
||||
return if new_max == @max_value
|
||||
@max_value = new_max
|
||||
@value = @value.clamp(self.min_value, self.max_value)
|
||||
self.invalidate
|
||||
end
|
||||
|
||||
def set_interactive_rects
|
||||
@slider_rect = Rect.new(SLIDER_X - SLIDER_PADDING, (self.height - PLUS_MINUS_SIZE) / 2, SLIDER_LENGTH + (SLIDER_PADDING * 2), PLUS_MINUS_SIZE)
|
||||
@minus_rect = Rect.new(MINUS_X, (self.height - PLUS_MINUS_SIZE) / 2, PLUS_MINUS_SIZE, PLUS_MINUS_SIZE)
|
||||
@plus_rect = Rect.new(PLUS_X, (self.height - PLUS_MINUS_SIZE) / 2, PLUS_MINUS_SIZE, PLUS_MINUS_SIZE)
|
||||
@interactions = {
|
||||
:slider => @slider_rect,
|
||||
:minus => @minus_rect,
|
||||
:plus => @plus_rect
|
||||
}
|
||||
end
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
def draw_area_highlight
|
||||
# Don't want to ever highlight the slider with the capture color, because
|
||||
# the mouse doesn't need to be on the slider to change this control's value
|
||||
if @captured_area == :slider
|
||||
rect = @interactions[@captured_area]
|
||||
self.bitmap.fill_rect(rect.x, rect.y, rect.width, rect.height, HOVER_COLOR) if rect
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def refresh
|
||||
super
|
||||
# Draw minus button
|
||||
self.bitmap.fill_rect(@minus_rect.x + 2, @minus_rect.y + (@minus_rect.height / 2) - 2, @minus_rect.width - 4, 4, self.bitmap.font.color)
|
||||
# Draw slider bar
|
||||
self.bitmap.fill_rect(SLIDER_X, (self.height / 2) - 1, SLIDER_LENGTH, 2, self.bitmap.font.color)
|
||||
# Draw notches on slider bar
|
||||
5.times do |i|
|
||||
self.bitmap.fill_rect(SLIDER_X - 1 + (i * SLIDER_LENGTH / 4), (self.height / 2) - 2, 2, 4, self.bitmap.font.color)
|
||||
end
|
||||
# Draw slider knob
|
||||
fraction = (self.value - self.min_value) / self.max_value.to_f
|
||||
knob_x = (SLIDER_LENGTH * fraction).to_i
|
||||
self.bitmap.fill_rect(SLIDER_X + knob_x - 4, (self.height / 2) - 6, 8, 12, SLIDER_KNOB_COLOR)
|
||||
# Draw plus button
|
||||
self.bitmap.fill_rect(@plus_rect.x + 2, @plus_rect.y + (@plus_rect.height / 2) - 2, @plus_rect.width - 4, 4, self.bitmap.font.color)
|
||||
self.bitmap.fill_rect(@plus_rect.x + (@plus_rect.width / 2) - 2, @plus_rect.y + 2, 4, @plus_rect.height - 4, self.bitmap.font.color)
|
||||
# Draw value text
|
||||
draw_text(self.bitmap, VALUE_X, TEXT_OFFSET_Y, self.value.to_s)
|
||||
end
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
def on_mouse_press
|
||||
super
|
||||
@initial_value = @value if @captured_area
|
||||
end
|
||||
|
||||
def on_mouse_release
|
||||
return if !@captured_area # Wasn't captured to begin with
|
||||
set_changed if @initial_value && @value != @initial_value
|
||||
@initial_value = nil
|
||||
super
|
||||
end
|
||||
|
||||
def update
|
||||
return if !self.visible
|
||||
super
|
||||
# TODO: Disabled control stuff.
|
||||
# return if self.disabled
|
||||
case @captured_area
|
||||
when :minus
|
||||
# Constant decrement of value while pressing the minus button
|
||||
if @hover_area == @captured_area && Input.repeat?(Input::MOUSELEFT)
|
||||
self.value -= 1
|
||||
end
|
||||
when :plus
|
||||
# Constant incrementing of value while pressing the plus button
|
||||
if @hover_area == @captured_area && Input.repeat?(Input::MOUSELEFT)
|
||||
self.value += 1
|
||||
end
|
||||
when :slider
|
||||
# Constant updating of value depending on mouse's x position
|
||||
mouse_x, mouse_y = mouse_pos
|
||||
return if !mouse_x || !mouse_y
|
||||
self.value = lerp(self.min_value, self.max_value + (self.max_value & 1), SLIDER_LENGTH, mouse_x - SLIDER_X)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,140 @@
|
||||
#===============================================================================
|
||||
#
|
||||
#===============================================================================
|
||||
class UIControls::NumberTextBox < UIControls::TextBox
|
||||
attr_reader :min_value
|
||||
attr_reader :max_value
|
||||
|
||||
PLUS_MINUS_SIZE = 16
|
||||
CONTROL_PADDING = 2 # Gap between buttons and text box
|
||||
|
||||
MINUS_X = 0
|
||||
TEXT_BOX_X = MINUS_X + PLUS_MINUS_SIZE + CONTROL_PADDING
|
||||
TEXT_BOX_WIDTH = 64
|
||||
TEXT_BOX_HEIGHT = 24
|
||||
PLUS_X = TEXT_BOX_X + TEXT_BOX_WIDTH + CONTROL_PADDING
|
||||
|
||||
def initialize(width, height, viewport, min_value, max_value, value)
|
||||
super(width, height, viewport, value)
|
||||
@min_value = min_value
|
||||
@max_value = max_value
|
||||
self.value = value
|
||||
end
|
||||
|
||||
def value=(new_value)
|
||||
old_val = @value
|
||||
@value = new_value.to_i.clamp(self.min_value, self.max_value)
|
||||
self.invalidate if @value != old_val
|
||||
end
|
||||
|
||||
def min_value=(new_min)
|
||||
return if new_min == @min_value
|
||||
@min_value = new_min
|
||||
@value = @value.clamp(self.min_value, self.max_value)
|
||||
self.invalidate
|
||||
end
|
||||
|
||||
def max_value=(new_max)
|
||||
return if new_max == @max_value
|
||||
@max_value = new_max
|
||||
@value = @value.clamp(self.min_value, self.max_value)
|
||||
self.invalidate
|
||||
end
|
||||
|
||||
# TODO: If current value is 0, replace it with ch instead of inserting ch?
|
||||
def insert_char(ch)
|
||||
self.value = @value.to_s.insert(@cursor_pos, ch).to_i
|
||||
@cursor_pos += 1
|
||||
@cursor_pos = @cursor_pos.clamp(0, @value.to_s.length)
|
||||
@cursor_timer = System.uptime
|
||||
@cursor_shown = true
|
||||
invalidate
|
||||
end
|
||||
|
||||
def delete_at(index)
|
||||
new_val = @value.to_s
|
||||
new_val.slice!(index)
|
||||
self.value = new_val.to_i
|
||||
@cursor_pos -= 1 if @cursor_pos > index
|
||||
@cursor_pos = @cursor_pos.clamp(0, @value.to_s.length)
|
||||
@cursor_timer = System.uptime
|
||||
@cursor_shown = true
|
||||
invalidate
|
||||
end
|
||||
|
||||
def set_interactive_rects
|
||||
@text_box_rect = Rect.new(TEXT_BOX_X, (height - TEXT_BOX_HEIGHT) / 2,
|
||||
TEXT_BOX_WIDTH, TEXT_BOX_HEIGHT)
|
||||
@minus_rect = Rect.new(MINUS_X, (self.height - PLUS_MINUS_SIZE) / 2, PLUS_MINUS_SIZE, PLUS_MINUS_SIZE)
|
||||
@plus_rect = Rect.new(PLUS_X, (self.height - PLUS_MINUS_SIZE) / 2, PLUS_MINUS_SIZE, PLUS_MINUS_SIZE)
|
||||
@interactions = {
|
||||
:text_box => @text_box_rect,
|
||||
:minus => @minus_rect,
|
||||
:plus => @plus_rect
|
||||
}
|
||||
end
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
def refresh
|
||||
super
|
||||
# Draw minus button
|
||||
self.bitmap.fill_rect(@minus_rect.x + 2, @minus_rect.y + (@minus_rect.height / 2) - 2, @minus_rect.width - 4, 4, self.bitmap.font.color)
|
||||
# Draw plus button
|
||||
self.bitmap.fill_rect(@plus_rect.x + 2, @plus_rect.y + (@plus_rect.height / 2) - 2, @plus_rect.width - 4, 4, self.bitmap.font.color)
|
||||
self.bitmap.fill_rect(@plus_rect.x + (@plus_rect.width / 2) - 2, @plus_rect.y + 2, 4, @plus_rect.height - 4, self.bitmap.font.color)
|
||||
end
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
def on_mouse_press
|
||||
@captured_area = nil
|
||||
super
|
||||
if @captured_area == :text_box
|
||||
# Clicked into the text box; put the text cursor in there
|
||||
@cursor_pos = get_cursor_index_from_mouse_position
|
||||
@cursor_timer = System.uptime
|
||||
invalidate
|
||||
elsif @captured_area
|
||||
@initial_value = @value
|
||||
else
|
||||
set_changed if @initial_value && @value != @initial_value
|
||||
reset_interaction
|
||||
end
|
||||
end
|
||||
|
||||
def update_text_entry
|
||||
ret = false
|
||||
Input.gets.each_char do |ch|
|
||||
next if !["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "-"].include?(ch)
|
||||
if ch == "-"
|
||||
next if @min_value >= 0 || @cursor_pos > 1 || (@cursor_pos > 0 && @value >= 0)
|
||||
if @value < 0
|
||||
delete_at(0) # Remove the negative sign
|
||||
ret = true
|
||||
next
|
||||
end
|
||||
end
|
||||
insert_char(ch)
|
||||
ret = true
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
def update
|
||||
return if !self.visible
|
||||
super
|
||||
case @captured_area
|
||||
when :minus
|
||||
# Constant decrement of value while pressing the minus button
|
||||
if @hover_area == @captured_area && Input.repeat?(Input::MOUSELEFT)
|
||||
self.value -= 1
|
||||
end
|
||||
when :plus
|
||||
# Constant incrementing of value while pressing the plus button
|
||||
if @hover_area == @captured_area && Input.repeat?(Input::MOUSELEFT)
|
||||
self.value += 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
75
Data/Scripts/801_UI controls/Control elements/007_Button.rb
Normal file
75
Data/Scripts/801_UI controls/Control elements/007_Button.rb
Normal file
@@ -0,0 +1,75 @@
|
||||
#===============================================================================
|
||||
#
|
||||
#===============================================================================
|
||||
class UIControls::Button < UIControls::BaseControl
|
||||
BUTTON_X = 2
|
||||
BUTTON_Y = 2
|
||||
BUTTON_PADDING = 10
|
||||
BUTTON_HEIGHT = 28
|
||||
# TODO: This will also depend on the font size.
|
||||
TEXT_BASE_OFFSET_Y = 18 # Text is centred vertically in the button
|
||||
|
||||
def initialize(width, height, viewport, text = "")
|
||||
super(width, height, viewport)
|
||||
@text = text
|
||||
@fixed_size = false
|
||||
end
|
||||
|
||||
def set_fixed_size
|
||||
@fixed_size = true
|
||||
end
|
||||
|
||||
def set_interactive_rects
|
||||
button_width = (@fixed_size) ? width - (BUTTON_X * 2) : self.bitmap.text_size(@text).width + (BUTTON_PADDING * 2)
|
||||
button_height = (@fixed_size) ? height - (2 * BUTTON_Y) : BUTTON_HEIGHT
|
||||
button_height = [button_height, height - (2 * BUTTON_Y)].min
|
||||
@button_rect = Rect.new(BUTTON_X, (height - button_height) / 2, button_width, button_height)
|
||||
@interactions = {
|
||||
:button => @button_rect
|
||||
}
|
||||
end
|
||||
|
||||
def set_changed
|
||||
@value = true
|
||||
super
|
||||
end
|
||||
|
||||
def clear_changed
|
||||
@value = false
|
||||
super
|
||||
end
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
def refresh
|
||||
super
|
||||
# Draw button outline
|
||||
self.bitmap.outline_rect(@button_rect.x, @button_rect.y,
|
||||
@button_rect.width, @button_rect.height,
|
||||
self.bitmap.font.color)
|
||||
# TODO: Make buttons look more different to text boxes?
|
||||
# shade = self.bitmap.font.color.clone
|
||||
# shade.alpha = 96
|
||||
# self.bitmap.outline_rect(@button_rect.x + 1, @button_rect.y + 1,
|
||||
# @button_rect.width - 2, @button_rect.height - 2,
|
||||
# shade, 3)
|
||||
# Draw button text
|
||||
draw_text_centered(self.bitmap, @button_rect.x,
|
||||
@button_rect.y + (@button_rect.height - TEXT_BASE_OFFSET_Y) / 2,
|
||||
@button_rect.width, @text)
|
||||
end
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
def on_mouse_release
|
||||
return if !@captured_area # Wasn't captured to begin with
|
||||
# Change this control's value
|
||||
if @captured_area == :button
|
||||
mouse_x, mouse_y = mouse_pos
|
||||
if mouse_x && mouse_y && @interactions[@captured_area].contains?(mouse_x, mouse_y)
|
||||
set_changed
|
||||
end
|
||||
end
|
||||
super # Make this control not busy again
|
||||
end
|
||||
end
|
||||
207
Data/Scripts/801_UI controls/Control elements/008_List.rb
Normal file
207
Data/Scripts/801_UI controls/Control elements/008_List.rb
Normal file
@@ -0,0 +1,207 @@
|
||||
#===============================================================================
|
||||
# TODO: Do I need to split self's bitmap into two (one for highlights and one
|
||||
# for text)? This would be to reduce lag caused by redrawing text even if
|
||||
# you're just waving the mouse over the control. There doesn't seem to be
|
||||
# any lag at the moment with a tall list.
|
||||
# TODO: Make a viewport for the list, and allow scrolling positions halfway
|
||||
# through a line? Nah.
|
||||
#===============================================================================
|
||||
class UIControls::List < UIControls::BaseControl
|
||||
LIST_X = 0
|
||||
LIST_Y = 0
|
||||
ROW_HEIGHT = 24
|
||||
TEXT_PADDING_X = 4
|
||||
TEXT_OFFSET_Y = 3
|
||||
|
||||
SELECTED_ROW_COLOR = Color.green
|
||||
|
||||
def initialize(width, height, viewport, values = [])
|
||||
super(width, height, viewport)
|
||||
@scrollbar = UIControls::Scrollbar.new(LIST_X + width - UIControls::Scrollbar::SLIDER_WIDTH, LIST_Y, height, viewport)
|
||||
@scrollbar.set_interactive_rects
|
||||
@scrollbar.range = ROW_HEIGHT
|
||||
@scrollbar.z = self.z + 1
|
||||
@rows_count = (height / ROW_HEIGHT).floor # Number of rows visible at once
|
||||
@top_row = 0
|
||||
@selected = -1
|
||||
self.values = values
|
||||
end
|
||||
|
||||
def dispose
|
||||
@scrollbar.dispose
|
||||
@scrollbar = nil
|
||||
super
|
||||
end
|
||||
|
||||
def x=(new_val)
|
||||
super(new_val)
|
||||
@scrollbar.x = new_val + LIST_X + width - UIControls::Scrollbar::SLIDER_WIDTH
|
||||
end
|
||||
|
||||
def y=(new_val)
|
||||
super(new_val)
|
||||
@scrollbar.y = new_val + LIST_Y
|
||||
end
|
||||
|
||||
# Each value in @values is an array: [id, text].
|
||||
def values=(new_vals)
|
||||
@values = new_vals
|
||||
set_interactive_rects
|
||||
@scrollbar.range = @values.length * ROW_HEIGHT
|
||||
if @scrollbar.visible
|
||||
self.top_row = (@scrollbar.position.to_f / ROW_HEIGHT).round
|
||||
else
|
||||
self.top_row = 0
|
||||
end
|
||||
self.selected = -1 if @selected >= @values.length
|
||||
invalidate
|
||||
end
|
||||
|
||||
def top_row=(val)
|
||||
old_val = @top_row
|
||||
@top_row = val
|
||||
if @scrollbar.visible
|
||||
@top_row = @top_row.clamp(0, @values.length - @rows_count)
|
||||
else
|
||||
@top_row = 0
|
||||
end
|
||||
invalidate if @top_row != old_val
|
||||
end
|
||||
|
||||
def selected=(val)
|
||||
return if @selected == val
|
||||
@selected = val
|
||||
invalidate
|
||||
end
|
||||
|
||||
# Returns the ID of the selected row.
|
||||
def value
|
||||
return nil if @selected < 0
|
||||
return @values[@selected][0]
|
||||
end
|
||||
|
||||
def set_interactive_rects
|
||||
@interactions = {}
|
||||
@values.length.times do |i|
|
||||
@interactions[i] = Rect.new(LIST_X, LIST_Y + (ROW_HEIGHT * i), width - LIST_X, ROW_HEIGHT)
|
||||
end
|
||||
end
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
def busy?
|
||||
return !@captured_area.nil?
|
||||
end
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
def draw_area_highlight
|
||||
# If a row is captured, it will automatically be selected and the selection
|
||||
# colour will be drawn over the highlight. There's no point drawing a
|
||||
# highlight at all if anything is captured.
|
||||
return if @captured_area
|
||||
# Draw mouse hover over row highlight
|
||||
rect = @interactions[@hover_area]
|
||||
if rect
|
||||
rect_y = rect.y
|
||||
rect_y -= @top_row * ROW_HEIGHT if @hover_area.is_a?(Integer)
|
||||
self.bitmap.fill_rect(rect.x, rect_y, rect.width, rect.height, HOVER_COLOR)
|
||||
end
|
||||
end
|
||||
|
||||
def repaint
|
||||
@scrollbar.repaint if @scrollbar.invalid?
|
||||
super if invalid?
|
||||
end
|
||||
|
||||
def refresh
|
||||
super
|
||||
# Draw text options
|
||||
@values.each_with_index do |val, i|
|
||||
next if i < @top_row || i >= @top_row + @rows_count
|
||||
if @selected == i
|
||||
self.bitmap.fill_rect(
|
||||
@interactions[i].x,
|
||||
@interactions[i].y - (@top_row * ROW_HEIGHT),
|
||||
@interactions[i].width, @interactions[i].height,
|
||||
SELECTED_ROW_COLOR
|
||||
)
|
||||
end
|
||||
draw_text(self.bitmap,
|
||||
@interactions[i].x + TEXT_PADDING_X,
|
||||
@interactions[i].y + TEXT_OFFSET_Y - (@top_row * ROW_HEIGHT),
|
||||
val[1])
|
||||
end
|
||||
end
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
def on_mouse_press
|
||||
@captured_area = nil
|
||||
mouse_x, mouse_y = mouse_pos
|
||||
return if !mouse_x || !mouse_y
|
||||
return if @scrollbar.visible && (@scrollbar.busy? || mouse_x >= @scrollbar.x - self.x)
|
||||
# Check for mouse presses on rows
|
||||
mouse_y += @top_row * ROW_HEIGHT
|
||||
@interactions.each_pair do |area, rect|
|
||||
next if !area.is_a?(Integer) || area < @top_row || area >= @top_row + @rows_count
|
||||
next if !rect.contains?(mouse_x, mouse_y)
|
||||
@captured_area = area
|
||||
invalidate
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
def on_mouse_release
|
||||
return if !@captured_area # Wasn't captured to begin with
|
||||
set_changed
|
||||
super
|
||||
end
|
||||
|
||||
def update_hover_highlight
|
||||
# Remove the hover highlight if there are no interactions for this control
|
||||
# or if the mouse is off-screen
|
||||
mouse_x, mouse_y = mouse_pos
|
||||
if !@interactions || @interactions.empty? || !mouse_x || !mouse_y
|
||||
invalidate if @hover_area
|
||||
@hover_area = nil
|
||||
return
|
||||
end
|
||||
# Don't update the highlight if the mouse is using the scrollbar
|
||||
if @scrollbar.visible && (@scrollbar.busy? || mouse_x >= @scrollbar.x - self.x)
|
||||
invalidate if @hover_area
|
||||
@hover_area = nil
|
||||
return
|
||||
end
|
||||
# Check each interactive area for whether the mouse is hovering over it, and
|
||||
# set @hover_area accordingly
|
||||
in_area = false
|
||||
mouse_y += @top_row * ROW_HEIGHT
|
||||
@interactions.each_pair do |area, rect|
|
||||
next if !area.is_a?(Integer) || area < @top_row || area >= @top_row + @rows_count
|
||||
next if !rect.contains?(mouse_x, mouse_y)
|
||||
invalidate if @hover_area != area
|
||||
@hover_area = area
|
||||
in_area = true
|
||||
break
|
||||
end
|
||||
if !in_area
|
||||
invalidate if @hover_area
|
||||
@hover_area = nil
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
return if !self.visible
|
||||
@scrollbar.update
|
||||
super
|
||||
# TODO: Disabled control stuff.
|
||||
# return if self.disabled
|
||||
# Refresh the list's position if changed by moving the scrollbar
|
||||
self.top_row = (@scrollbar.position.to_f / ROW_HEIGHT).round
|
||||
# Set the selected row to the row the mouse is over, if clicked on
|
||||
if @captured_area
|
||||
@selected = @hover_area if @hover_area.is_a?(Integer)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,9 @@
|
||||
#===============================================================================
|
||||
# TODO
|
||||
#===============================================================================
|
||||
class UIControls::DropdownList < UIControls::BaseControl
|
||||
def initialize(width, height, viewport, options, value)
|
||||
# NOTE: options is a hash: keys are symbols, values are display names.
|
||||
super(width, height, viewport)
|
||||
end
|
||||
end
|
||||
156
Data/Scripts/801_UI controls/Control elements/101_Scrollbar.rb
Normal file
156
Data/Scripts/801_UI controls/Control elements/101_Scrollbar.rb
Normal file
@@ -0,0 +1,156 @@
|
||||
#===============================================================================
|
||||
# TODO: Make the slider a separate sprite that moves, instead of redrawing this
|
||||
# sprite's bitmap whenever it moves? Intended to reduce lag. There doesn't
|
||||
# seem to be any lag at the moment with a tall scrollbar.
|
||||
#===============================================================================
|
||||
class UIControls::Scrollbar < UIControls::BaseControl
|
||||
SLIDER_WIDTH = 16
|
||||
WIDTH_PADDING = 0
|
||||
SCROLL_DISTANCE = 16
|
||||
TRAY_COLOR = Color.white
|
||||
SLIDER_COLOR = Color.black
|
||||
GRAB_COLOR = HOVER_COLOR # Cyan
|
||||
|
||||
attr_reader :slider_top
|
||||
|
||||
def initialize(x, y, size, viewport, horizontal = false, always_visible = false)
|
||||
if horizontal
|
||||
super(size, SLIDER_WIDTH, viewport)
|
||||
else
|
||||
super(SLIDER_WIDTH, size, viewport)
|
||||
end
|
||||
self.x = x
|
||||
self.y = y
|
||||
@horizontal = horizontal # Is vertical if not horizontal
|
||||
@tray_size = size # Number of pixels the scrollbar can move around in
|
||||
@slider_size = size
|
||||
@range = size # Total distance of the area this scrollbar is for
|
||||
@slider_top = 0 # Top pixel within @size of the scrollbar
|
||||
@always_visible = always_visible
|
||||
self.visible = @always_visible
|
||||
end
|
||||
|
||||
def position
|
||||
return 0 if @range <= @tray_size
|
||||
return (@range - @tray_size) * @slider_top / (@tray_size - @slider_size)
|
||||
end
|
||||
|
||||
# Range is the total size of the large area that the scrollbar is able to
|
||||
# show part of.
|
||||
def range=(new_val)
|
||||
raise "Can't set a scrollbar's range to 0!" if new_val == 0
|
||||
@range = new_val
|
||||
@slider_size = (@tray_size * [@tray_size.to_f / @range, 1].min).round
|
||||
if @horizontal
|
||||
@slider.width = @slider_size
|
||||
else # Vertical
|
||||
@slider.height = @slider_size
|
||||
end
|
||||
self.slider_top = @slider_top
|
||||
self.visible = (@always_visible || @range > @tray_size)
|
||||
invalidate
|
||||
end
|
||||
|
||||
def slider_top=(new_val)
|
||||
old_val = @slider_top
|
||||
@slider_top = new_val.clamp(0, @tray_size - @slider_size)
|
||||
if @horizontal
|
||||
@slider.x = @slider_top
|
||||
else # Vertical
|
||||
@slider.y = @slider_top
|
||||
end
|
||||
invalidate if @slider_top != old_val
|
||||
end
|
||||
|
||||
def set_interactive_rects
|
||||
@interactions = {}
|
||||
if @horizontal
|
||||
@slider = Rect.new(@slider_top, WIDTH_PADDING, @slider_size, height - (WIDTH_PADDING * 2))
|
||||
else # Vertical
|
||||
@slider = Rect.new(WIDTH_PADDING, @slider_top, width - (WIDTH_PADDING * 2), @slider_size)
|
||||
end
|
||||
@interactions[:slider] = @slider
|
||||
@slider_tray = Rect.new(0, 0, width, height)
|
||||
@interactions[:slider_tray] = @slider_tray
|
||||
end
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
def refresh
|
||||
super
|
||||
return if !self.visible
|
||||
# Draw the tray
|
||||
self.bitmap.fill_rect(@slider_tray.x, @slider_tray.y, @slider_tray.width, @slider_tray.height, TRAY_COLOR)
|
||||
# Draw the slider
|
||||
if @slider_size < @tray_size
|
||||
bar_color = SLIDER_COLOR
|
||||
if @captured_area == :slider || (!@captured_area && @hover_area == :slider)
|
||||
bar_color = GRAB_COLOR
|
||||
end
|
||||
self.bitmap.fill_rect(@slider.x, @slider.y, @slider.width, @slider.height, bar_color)
|
||||
end
|
||||
end
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
def on_mouse_press
|
||||
@captured_area = nil
|
||||
mouse_x, mouse_y = mouse_pos
|
||||
return if !mouse_x || !mouse_y
|
||||
# Check for mouse presses on slider/slider tray
|
||||
@interactions.each_pair do |area, rect|
|
||||
next if !rect.contains?(mouse_x, mouse_y)
|
||||
@captured_area = area
|
||||
if area == :slider
|
||||
if @horizontal
|
||||
@slider_mouse_offset = mouse_x - rect.x
|
||||
else
|
||||
@slider_mouse_offset = mouse_y - rect.y
|
||||
end
|
||||
end
|
||||
invalidate
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
def on_mouse_release
|
||||
super if @captured_area
|
||||
end
|
||||
|
||||
def update
|
||||
return if !self.visible
|
||||
super
|
||||
# TODO: Disabled control stuff.
|
||||
# return if self.disabled
|
||||
if @captured_area == :slider
|
||||
# TODO: Have a display y position for the slider bar which is in pixels,
|
||||
# and round it to the nearest row when setting @top_row? This is
|
||||
# just to make the slider bar movement smoother.
|
||||
mouse_x, mouse_y = mouse_pos
|
||||
return if !mouse_x || !mouse_y
|
||||
long_coord = (@horizontal) ? mouse_x : mouse_y
|
||||
self.slider_top = long_coord - @slider_mouse_offset
|
||||
elsif @captured_area == :slider_tray
|
||||
if Input.repeat?(Input::MOUSELEFT) && @hover_area == :slider_tray
|
||||
mouse_x, mouse_y = mouse_pos
|
||||
return if !mouse_x || !mouse_y
|
||||
long_coord = (@horizontal) ? mouse_x : mouse_y
|
||||
if long_coord < @slider_top
|
||||
self.slider_top = @slider_top - ((@tray_size - @slider_size) / 4.0).ceil
|
||||
else
|
||||
self.slider_top = @slider_top + ((@tray_size - @slider_size) / 4.0).ceil
|
||||
end
|
||||
end
|
||||
else
|
||||
mouse_x, mouse_y = mouse_pos
|
||||
if mouse_x && mouse_y && @interactions[:slider_tray].contains?(mouse_x, mouse_y)
|
||||
wheel_v = Input.scroll_v
|
||||
if wheel_v > 0 # Scroll up
|
||||
self.slider_top -= SCROLL_DISTANCE
|
||||
elsif wheel_v < 0 # Scroll down
|
||||
self.slider_top += SCROLL_DISTANCE
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user