require_relative 'global' module MiniGL # This class is an abstract ancestor for all form components (Button, # ToggleButton and TextField). class Component # The horizontal coordinate of the component attr_reader :x # The vertical coordinate of the component attr_reader :y # The width of the component attr_reader :w # The height of the component attr_reader :h # Determines whether the control is enabled, i.e., will process user input. attr_accessor :enabled # Determines whether the control is visible, i.e., will be drawn in the # screen and process user input, if enabled. attr_accessor :visible # A container for any parameters to be passed to the code blocks called # in response to events of the control (click of a button, change of the # text in a text field, etc.). More detail can be found in the constructor # for each specific component class. attr_accessor :params def initialize(x, y, font, text, text_color, disabled_text_color) # :nodoc: @x = x @y = y @font = font @text = text @text_color = text_color @disabled_text_color = disabled_text_color @enabled = @visible = true end end # This class represents a button. class Button < Component # The current state of the button. attr_reader :state # The text of the button. attr_accessor :text # Creates a button. # # Parameters: # [x] The x-coordinate where the button will be drawn in the screen. # [y] The y-coordinate where the button will be drawn in the screen. # [font] The Gosu::Font object that will be used to draw the # button text. # [text] The button text. Can be +nil+ or empty. # [img] A spritesheet containing four images in a column, representing, # from top to bottom, the default state, the hover state (when the # mouse is over the button), the pressed state (when the mouse # button is down and the cursor is over the button) and the disabled # state. If +nil+, the +width+ and +height+ parameters must be # provided. # [text_color] Color of the button text, in hexadecimal RRGGBB format. # [disabled_text_color] Color of the button text, when it's disabled, in # hexadecimal RRGGBB format. # [over_text_color] Color of the button text, when the cursor is over it # (hexadecimal RRGGBB). # [down_text_color] Color of the button text, when it is pressed # (hexadecimal RRGGBB). # [center_x] Whether the button text should be horizontally centered in its # area (the area is defined by the image size, if an image is # given, or by the +width+ and +height+ parameters, otherwise). # [center_y] Whether the button text should be vertically centered in its # area (the area is defined by the image size, if an image is # given, or by the +width+ and +height+ parameters, otherwise). # [margin_x] The x offset, from the button x-coordinate, to draw the text. # This parameter is used only if +center+ is false. # [margin_y] The y offset, from the button y-coordinate, to draw the text. # This parameter is used only if +center+ is false. # [width] Width of the button clickable area. This parameter is used only # if +img+ is +nil+. # [height] Height of the button clickable area. This parameter is used # only if +img+ is +nil+. # [params] An object containing any parameters you want passed to the # +action+ block. When the button is clicked, the following is # called: # @action.call @params # Note that this doesn't force you to declare a block that takes # parameters. # [action] The block of code executed when the button is clicked (or by # calling the +click+ method). # # *Obs.:* This method accepts named parameters, but +x+ and +y+ are # mandatory (also, +img+ is mandatory when +width+ and +height+ are not # provided, and vice-versa). def initialize(x, y = nil, font = nil, text = nil, img = nil, text_color = 0, disabled_text_color = 0, over_text_color = 0, down_text_color = 0, center_x = true, center_y = true, margin_x = 0, margin_y = 0, width = nil, height = nil, params = nil, &action) if x.is_a? Hash y = x[:y] font = x[:font] text = x[:text] img = x[:img] text_color = x.fetch(:text_color, 0) disabled_text_color = x.fetch(:disabled_text_color, 0) over_text_color = x.fetch(:over_text_color, 0) down_text_color = x.fetch(:down_text_color, 0) center_x = x.fetch(:center_x, true) center_y = x.fetch(:center_y, true) margin_x = x.fetch(:margin_x, 0) margin_y = x.fetch(:margin_y, 0) width = x.fetch(:width, nil) height = x.fetch(:height, nil) params = x.fetch(:params, nil) x = x[:x] end super x, y, font, text, text_color, disabled_text_color @over_text_color = over_text_color @down_text_color = down_text_color @img = if img; Res.imgs img, 1, 4, true else; nil; end @w = if img; @img[0].width else; width; end @h = if img; @img[0].height else; height; end if center_x; @text_x = x + @w / 2 if @w else; @text_x = x + margin_x; end if center_y; @text_y = y + @h / 2 if @h else; @text_y = y + margin_y; end @center_x = center_x @center_y = center_y @action = action @params = params @state = :up @img_index = @enabled ? 0 : 3 end # Updates the button, checking the mouse movement and buttons to define # the button state. def update return unless @enabled and @visible mouse_over = Mouse.over? @x, @y, @w, @h mouse_press = Mouse.button_pressed? :left mouse_rel = Mouse.button_released? :left if @state == :up if mouse_over @img_index = 1 @state = :over else @img_index = 0 end elsif @state == :over if not mouse_over @img_index = 0 @state = :up elsif mouse_press @img_index = 2 @state = :down else @img_index = 1 end elsif @state == :down if not mouse_over @img_index = 0 @state = :down_out elsif mouse_rel @img_index = 1 @state = :over click else @img_index = 2 end else # :down_out if mouse_over @img_index = 2 @state = :down elsif mouse_rel @img_index = 0 @state = :up else @img_index = 0 end end end # Executes the button click action. def click @action.call @params end # Sets the position of the button in the screen. # # Parameters: # [x] The new x-coordinate for the button. # [y] The new y-coordinate for the button. def set_position(x, y) if @center_x; @text_x = x + @w / 2 else; @text_x += x - @x; end if @center_y; @text_y = y + @h / 2 else; @text_y += y - @y; end @x = x; @y = y end # Draws the button in the screen. # # Parameters: # [alpha] The opacity with which the button will be drawn. Allowed values # vary between 0 (fully transparent) and 255 (fully opaque). # [z_index] The z-order to draw the object. Objects with larger z-orders # will be drawn on top of the ones with smaller z-orders. def draw(alpha = 0xff, z_index = 0) return unless @visible color = (alpha << 24) | 0xffffff text_color = if @enabled if @state == :down @down_text_color else @state == :over ? @over_text_color : @text_color end else @disabled_text_color end text_color = (alpha << 24) | text_color @img[@img_index].draw @x, @y, z_index, 1, 1, color if @img if @text if @center_x or @center_y rel_x = @center_x ? 0.5 : 0 rel_y = @center_y ? 0.5 : 0 @font.draw_rel @text, @text_x, @text_y, z_index, rel_x, rel_y, 1, 1, text_color else @font.draw @text, @text_x, @text_y, z_index, 1, 1, text_color end end end def enabled=(value) # :nodoc: @enabled = value @state = :up @img_index = 3 end end # This class represents a toggle button, which can be also interpreted as a # check box. It is always in one of two states, given as +true+ or +false+ # by its property +checked+. class ToggleButton < Button # Defines the state of the button (returns +true+ or +false+). attr_reader :checked # Creates a ToggleButton. All parameters work the same as in Button, # except for the image, +img+, which now has to be composed of two columns # and four rows, the first column with images for the unchecked state, # and the second with images for the checked state, and for +checked+, # which defines the initial state of the ToggleButton. # # The +action+ block now will always receive a first boolean parameter # corresponding to the value of +checked+. So, if you want to pass # parameters to the block, you should declare it like this: # b = ToggleButton.new ... { |checked, params| # puts "button was checked" if checked # # do something with params # } # # *Obs.:* This method accepts named parameters, but +x+ and +y+ are # mandatory (also, +img+ is mandatory when +width+ and +height+ are not # provided, and vice-versa). def initialize(x, y = nil, font = nil, text = nil, img = nil, checked = false, text_color = 0, disabled_text_color = 0, over_text_color = 0, down_text_color = 0, center_x = true, center_y = true, margin_x = 0, margin_y = 0, width = nil, height = nil, params = nil, &action) if x.is_a? Hash y = x[:y] font = x[:font] text = x[:text] img = x[:img] checked = x.fetch(:checked, false) text_color = x.fetch(:text_color, 0) disabled_text_color = x.fetch(:disabled_text_color, 0) over_text_color = x.fetch(:over_text_color, 0) down_text_color = x.fetch(:down_text_color, 0) center_x = x.fetch(:center_x, true) center_y = x.fetch(:center_y, true) margin_x = x.fetch(:margin_x, 0) margin_y = x.fetch(:margin_y, 0) width = x.fetch(:width, nil) height = x.fetch(:height, nil) params = x.fetch(:params, nil) x = x[:x] end super x, y, font, text, nil, text_color, disabled_text_color, over_text_color, down_text_color, center_x, center_y, margin_x, margin_y, width, height, params, &action @img = if img; Res.imgs img, 2, 4, true else; nil; end @w = if img; @img[0].width else; width; end @h = if img; @img[0].height else; height; end @text_x = x + @w / 2 if center_x @text_y = y + @h / 2 if center_y @checked = checked end # Updates the button, checking the mouse movement and buttons to define # the button state. def update return unless @enabled and @visible super @img_index *= 2 @img_index += 1 if @checked end # Executes the button click action, and toggles its state. The +action+ # block always receives as a first parameter +true+, if the button has # been changed to checked, or +false+, otherwise. def click @checked = !@checked @action.call @checked, @params end # Sets the state of the button to the value given. # # Parameters: # [value] The state to be set (+true+ for checked, +false+ for unchecked). def checked=(value) click if value != @checked @checked = value end def enabled=(value) # :nodoc: @enabled = value @state = :up @img_index = @checked ? 7 : 6 end end # This class represents a text field (input). class TextField < Component # The current text inside the text field. attr_reader :text # The current 'locale' used for detecting the keys. THIS FEATURE IS # INCOMPLETE! attr_reader :locale # Creates a new text field. # # Parameters: # [x] The x-coordinate where the text field will be drawn in the screen. # [y] The y-coordinate where the text field will be drawn in the screen. # [font] The Gosu::Font object that will be used to draw the # text inside the field. # [img] The image of the text field. For a good result, you would likely # want something like a rectangle, horizontally wide, vertically # short, and with a color that contrasts with the +text_color+. # [cursor_img] An image for the blinking cursor that stands in the point # where text will be inserted. If +nil+, a simple black line # will be drawn instead. # [disabled_img] Image for the text field when it's disabled. If +nil+, # a darkened version of +img+ will be used. # [text_color] Color of the button text, in hexadecimal RRGGBB format. # [margin_x] The x offset, from the field x-coordinate, to draw the text. # [margin_y] The y offset, from the field y-coordinate, to draw the text. # [max_length] The maximum length of the text inside the field. # [active] Whether the text field must be focused by default. If +false+, # focus can be granted by clicking inside the text field or by # calling the +focus+ method. # [text] The starting text. Must not be +nil+. # [allowed_chars] A string containing all characters that can be typed # inside the text field. The complete set of supported # characters is given by the string # "abcdefghijklmnopqrstuvwxyz1234567890 ABCDEFGHIJKLMNOPQRSTUVWXYZ'-=/[]\\\\,.;\"_+?{}|<>:!@#$%¨&*()". # [text_color] The color with which the text will be drawn, in hexadecimal # RRGGBB format. # [disabled_text_color] The color with which the text will be drawn, when # the text field is disabled, in hexadecimal RRGGBB # format. # [selection_color] The color of the rectangle highlighting selected text, # in hexadecimal RRGGBB format. The rectangle will # always be drawn with 50% of opacity. # [locale] The locale to be used when detecting keys. By now, only 'en-US' # and 'pt-BR' are **partially** supported. Default is 'en-US'. If # any different value is supplied, all typed characters will be # mapped to '#'. # [params] An object containing any parameters you want passed to the # +on_text_changed+ block. When the text of the text field is # changed, the following is called: # @on_text_changed.call @text, @params # Thus, +params+ will be the second parameter. Note that this # doesn't force you to declare a block that takes parameters. # [on_text_changed] The block of code executed when the text in the text # field is changed, either by user input or by calling # +text=+. The new text is passed as a first parameter # to this block, followed by +params+. Can be +nil+. # # *Obs.:* This method accepts named parameters, but +x+, +y+, +font+ and # +img+ are mandatory. def initialize(x, y = nil, font = nil, img = nil, cursor_img = nil, disabled_img = nil, margin_x = 0, margin_y = 0, max_length = 100, active = false, text = '', allowed_chars = nil, text_color = 0, disabled_text_color = 0, selection_color = 0, locale = 'en-us', params = nil, &on_text_changed) if x.is_a? Hash y = x[:y] font = x[:font] img = x[:img] cursor_img = x.fetch(:cursor_img, nil) disabled_img = x.fetch(:disabled_img, nil) margin_x = x.fetch(:margin_x, 0) margin_y = x.fetch(:margin_y, 0) max_length = x.fetch(:max_length, 100) active = x.fetch(:active, false) text = x.fetch(:text, '') allowed_chars = x.fetch(:allowed_chars, nil) text_color = x.fetch(:text_color, 0) disabled_text_color = x.fetch(:disabled_text_color, 0) selection_color = x.fetch(:selection_color, 0) locale = x.fetch(:locale, 'en-us') params = x.fetch(:params, nil) x = x[:x] end super x, y, font, text, text_color, disabled_text_color @img = Res.img img @w = @img.width @h = @img.height @cursor_img = Res.img(cursor_img) if cursor_img @disabled_img = Res.img(disabled_img) if disabled_img @max_length = max_length @active = active @text_x = x + margin_x @text_y = y + margin_y @selection_color = selection_color @nodes = [x + margin_x] @cur_node = 0 @cursor_visible = false @cursor_timer = 0 @k = [ Gosu::KbA, Gosu::KbB, Gosu::KbC, Gosu::KbD, Gosu::KbE, Gosu::KbF, Gosu::KbG, Gosu::KbH, Gosu::KbI, Gosu::KbJ, Gosu::KbK, Gosu::KbL, Gosu::KbM, Gosu::KbN, Gosu::KbO, Gosu::KbP, Gosu::KbQ, Gosu::KbR, Gosu::KbS, Gosu::KbT, Gosu::KbU, Gosu::KbV, Gosu::KbW, Gosu::KbX, Gosu::KbY, Gosu::KbZ, Gosu::Kb1, Gosu::Kb2, Gosu::Kb3, Gosu::Kb4, Gosu::Kb5, Gosu::Kb6, Gosu::Kb7, Gosu::Kb8, Gosu::Kb9, Gosu::Kb0, Gosu::KbNumpad1, Gosu::KbNumpad2, Gosu::KbNumpad3, Gosu::KbNumpad4, Gosu::KbNumpad5, Gosu::KbNumpad6, Gosu::KbNumpad7, Gosu::KbNumpad8, Gosu::KbNumpad9, Gosu::KbNumpad0, Gosu::KbSpace, Gosu::KbBackspace, Gosu::KbDelete, Gosu::KbLeft, Gosu::KbRight, Gosu::KbHome, Gosu::KbEnd, Gosu::KbLeftShift, Gosu::KbRightShift, Gosu::KbBacktick, Gosu::KbMinus, Gosu::KbEqual, Gosu::KbBracketLeft, Gosu::KbBracketRight, Gosu::KbBackslash, Gosu::KbSemicolon, Gosu::KbApostrophe, Gosu::KbComma, Gosu::KbPeriod, Gosu::KbSlash, Gosu::KbNumpadAdd, Gosu::KbNumpadSubtract, Gosu::KbNumpadMultiply, Gosu::KbNumpadDivide ] @user_allowed_chars = allowed_chars self.locale = locale @on_text_changed = on_text_changed @params = params end # Updates the text field, checking for mouse events and keyboard input. def update return unless @enabled and @visible ################################ Mouse ################################ if Mouse.over? @x, @y, @w, @h if not @active and Mouse.button_pressed? :left focus end elsif Mouse.button_pressed? :left unfocus end return unless @active if Mouse.double_click? :left if @nodes.size > 1 @anchor1 = 0 @anchor2 = @nodes.size - 1 @cur_node = @anchor2 @double_clicked = true end set_cursor_visible elsif Mouse.button_pressed? :left set_node_by_mouse @anchor1 = @cur_node @anchor2 = nil @double_clicked = false set_cursor_visible elsif Mouse.button_down? :left if @anchor1 and not @double_clicked set_node_by_mouse if @cur_node != @anchor1; @anchor2 = @cur_node else; @anchor2 = nil; end set_cursor_visible end elsif Mouse.button_released? :left if @anchor1 and not @double_clicked if @cur_node != @anchor1; @anchor2 = @cur_node else; @anchor1 = nil; end end end @cursor_timer += 1 if @cursor_timer >= 30 @cursor_visible = (not @cursor_visible) @cursor_timer = 0 end ############################### Keyboard ############################## shift = (KB.key_down?(@k[53]) or KB.key_down?(@k[54])) if KB.key_pressed?(@k[53]) or KB.key_pressed?(@k[54]) # shift @anchor1 = @cur_node if @anchor1.nil? elsif KB.key_released?(@k[53]) or KB.key_released?(@k[54]) @anchor1 = nil if @anchor2.nil? end inserted = false for i in 0..46 # alnum if KB.key_pressed?(@k[i]) or KB.key_held?(@k[i]) remove_interval true if @anchor1 and @anchor2 if i < 26 if shift insert_char @chars[i + 37] else insert_char @chars[i] end elsif i < 36 if shift; insert_char @chars[i + 59] else; insert_char @chars[i]; end elsif shift insert_char(@chars[i + 49]) else insert_char(@chars[i - 10]) end inserted = true break end end return if inserted for i in 55..65 # special if KB.key_pressed?(@k[i]) or KB.key_held?(@k[i]) remove_interval true if @anchor1 and @anchor2 if shift; insert_char @chars[i + 19] else; insert_char @chars[i + 8]; end inserted = true break end end return if inserted for i in 66..69 # numpad operators if KB.key_pressed?(@k[i]) or KB.key_held?(@k[i]) remove_interval true if @anchor1 and @anchor2 insert_char @chars[i + 19] inserted = true break end end return if inserted if KB.key_pressed?(@k[47]) or KB.key_held?(@k[47]) # back if @anchor1 and @anchor2 remove_interval elsif @cur_node > 0 remove_char true end elsif KB.key_pressed?(@k[48]) or KB.key_held?(@k[48]) # del if @anchor1 and @anchor2 remove_interval elsif @cur_node < @nodes.size - 1 remove_char false end elsif KB.key_pressed?(@k[49]) or KB.key_held?(@k[49]) # left if @anchor1 if shift if @cur_node > 0 @cur_node -= 1 @anchor2 = @cur_node set_cursor_visible end elsif @anchor2 @cur_node = @anchor1 < @anchor2 ? @anchor1 : @anchor2 @anchor1 = nil @anchor2 = nil set_cursor_visible end elsif @cur_node > 0 @cur_node -= 1 set_cursor_visible end elsif KB.key_pressed?(@k[50]) or KB.key_held?(@k[50]) # right if @anchor1 if shift if @cur_node < @nodes.size - 1 @cur_node += 1 @anchor2 = @cur_node set_cursor_visible end elsif @anchor2 @cur_node = @anchor1 > @anchor2 ? @anchor1 : @anchor2 @anchor1 = nil @anchor2 = nil set_cursor_visible end elsif @cur_node < @nodes.size - 1 @cur_node += 1 set_cursor_visible end elsif KB.key_pressed?(@k[51]) # home @cur_node = 0 if shift; @anchor2 = @cur_node else @anchor1 = nil @anchor2 = nil end set_cursor_visible elsif KB.key_pressed?(@k[52]) # end @cur_node = @nodes.size - 1 if shift; @anchor2 = @cur_node else @anchor1 = nil @anchor2 = nil end set_cursor_visible end end # Sets the text of the text field to the specified value. # # Parameters: # [value] The new text to be set. If it's longer than the +max_length+ # parameter used in the constructor, it will be truncated to # +max_length+ characters. def text=(value) @text = value[0...@max_length] @nodes.clear; @nodes << @text_x x = @nodes[0] @text.chars.each { |char| x += @font.text_width char @nodes << x } @cur_node = @nodes.size - 1 @anchor1 = nil @anchor2 = nil set_cursor_visible @on_text_changed.call @text, @params if @on_text_changed end # Sets the locale used by the text field to detect keys. Only 'en-us' and # 'pt-br' are **partially** supported. If any different value is supplied, # all typed characters will be mapped to '#'. def locale=(value) @locale = value.downcase @chars = case @locale when 'en-us' then "abcdefghijklmnopqrstuvwxyz1234567890 ABCDEFGHIJKLMNOPQRSTUVWXYZ`-=[]\\;',./~_+{}|:\"<>?!@#$%^&*()+-*/" when 'pt-br' then "abcdefghijklmnopqrstuvwxyz1234567890 ABCDEFGHIJKLMNOPQRSTUVWXYZ'-=/[]ç~,.;\"_+?{}Ç^<>:!@#$%¨&*()+-*/" else '###################################################################################################' end @allowed_chars = if @user_allowed_chars @user_allowed_chars else @chars end end # Returns the currently selected text. def selected_text return '' if @anchor2.nil? min = @anchor1 < @anchor2 ? @anchor1 : @anchor2 max = min == @anchor1 ? @anchor2 : @anchor1 @text[min..max] end # Grants focus to the text field, so that it allows keyboard input. def focus @active = true end # Removes focus from the text field, so that no keyboard input will be # accepted. def unfocus @anchor1 = @anchor2 = nil @cursor_visible = false @cursor_timer = 0 @active = false end # Sets the position of the text field in the screen. # # Parameters: # [x] The new x-coordinate for the text field. # [y] The new y-coordinate for the text field. def set_position(x, y) d_x = x - @x d_y = y - @y @x = x; @y = y @text_x += d_x @text_y += d_y @nodes.map! do |n| n + d_x end end # Draws the text field in the screen. # # Parameters: # [alpha] The opacity with which the text field will be drawn. Allowed # values vary between 0 (fully transparent) and 255 (fully opaque). # [z_index] The z-order to draw the object. Objects with larger z-orders # will be drawn on top of the ones with smaller z-orders. def draw(alpha = 0xff, z_index = 0) return unless @visible color = (alpha << 24) | ((@enabled or @disabled_img) ? 0xffffff : 0x808080) text_color = (alpha << 24) | (@enabled ? @text_color : @disabled_text_color) img = ((@enabled or @disabled_img.nil?) ? @img : @disabled_img) img.draw @x, @y, z_index, 1, 1, color @font.draw @text, @text_x, @text_y, z_index, 1, 1, text_color if @anchor1 and @anchor2 selection_color = ((alpha / 2) << 24) | @selection_color G.window.draw_quad @nodes[@anchor1], @text_y, selection_color, @nodes[@anchor2] + 1, @text_y, selection_color, @nodes[@anchor2] + 1, @text_y + @font.height, selection_color, @nodes[@anchor1], @text_y + @font.height, selection_color, z_index end if @cursor_visible if @cursor_img @cursor_img.draw @nodes[@cur_node] - @cursor_img.width / 2, @text_y, z_index else cursor_color = alpha << 24 G.window.draw_quad @nodes[@cur_node], @text_y, cursor_color, @nodes[@cur_node] + 1, @text_y, cursor_color, @nodes[@cur_node] + 1, @text_y + @font.height, cursor_color, @nodes[@cur_node], @text_y + @font.height, cursor_color, z_index end end end def enabled=(value) # :nodoc: @enabled = value unfocus unless @enabled end def visible=(value) # :nodoc: @visible = value unfocus unless @visible end private def set_cursor_visible @cursor_visible = true @cursor_timer = 0 end def set_node_by_mouse index = @nodes.size - 1 @nodes.each_with_index do |n, i| if n >= Mouse.x index = i break end end if index > 0 d1 = @nodes[index] - Mouse.x; d2 = Mouse.x - @nodes[index - 1] index -= 1 if d1 > d2 end @cur_node = index end def insert_char(char) return unless @allowed_chars.index char and @text.length < @max_length @text.insert @cur_node, char @nodes.insert @cur_node + 1, @nodes[@cur_node] + @font.text_width(char) for i in (@cur_node + 2)..(@nodes.size - 1) @nodes[i] += @font.text_width(char) end @cur_node += 1 set_cursor_visible @on_text_changed.call @text, @params if @on_text_changed end def remove_interval(will_insert = false) min = @anchor1 < @anchor2 ? @anchor1 : @anchor2 max = min == @anchor1 ? @anchor2 : @anchor1 interval_width = 0 for i in min...max interval_width += @font.text_width(@text[i]) @nodes.delete_at min + 1 end @text[min...max] = '' for i in (min + 1)..(@nodes.size - 1) @nodes[i] -= interval_width end @cur_node = min @anchor1 = nil @anchor2 = nil set_cursor_visible @on_text_changed.call @text, @params if @on_text_changed and not will_insert end def remove_char(back) @cur_node -= 1 if back char_width = @font.text_width(@text[@cur_node]) @text[@cur_node] = '' @nodes.delete_at @cur_node + 1 for i in (@cur_node + 1)..(@nodes.size - 1) @nodes[i] -= char_width end set_cursor_visible @on_text_changed.call @text, @params if @on_text_changed end end # Represents a progress bar. class ProgressBar < Component # The maximum value for this progress bar (when the current value equals # the maximum, the bar is full). attr_reader :max_value # The current value of the progress bar (an integer greater than or equal # to zero, and less than or equal to +max_value+). attr_reader :value # Creates a progress bar. # # Parameters: # [x] The x-coordinate of the progress bar on the screen. # [y] The y-coordinate of the progress bar on the screen. # [w] Width of the progress bar, in pixels. This is the maximum space the # bar foreground can occupy. Note that the width of the foreground image # (+fg+) can be less than this. # [h] Height of the progress bar. This will be the height of the bar # foreground when +fg+ is a color (when it is an image, the height of # the image will be kept). # [bg] A background image (string or symbol that will be passed to # +Res.img+) or color (in RRGGBB hexadecimal format). # [fg] A foreground image (string or symbol that will be passed to # +Res.img+) or color (in RRGGBB hexadecimal format). The image will # be horizontally repeated when needed, if its width is less than +w+. # [max_value] The maximum value the progress bar can reach (an integer). # [value] The starting value for the progress bar. # [fg_margin_x] Horizontal margin between the background image and the # foreground image (when these are provided). # [fg_margin_y] Vertical margin between the background image and the # foreground image (when these are provided). # [font] Font that will be used to draw a text indicating the value of the # progress bar. # [text_color] Color of the text. # [format] Format to display the value. Specify '%' for a percentage and # anything else for absolute values (current/maximum). # # *Obs.:* This method accepts named parameters, but +x+, +y+, +w+, +h+, +bg+ # and +fg+ are mandatory. def initialize(x, y = nil, w = nil, h = nil, bg = nil, fg = nil, max_value = 100, value = 100, fg_margin_x = 0, fg_margin_y = 0, #fg_left = nil, fg_right = nil, font = nil, text_color = 0, format = nil) if x.is_a? Hash y = x[:y] w = x[:w] h = x[:h] bg = x[:bg] fg = x[:fg] max_value = x.fetch(:max_value, 100) value = x.fetch(:value, 100) fg_margin_x = x.fetch(:fg_margin_x, 0) fg_margin_y = x.fetch(:fg_margin_y, 0) font = x.fetch(:font, nil) text_color = x.fetch(:text_color, 0) format = x.fetch(:format, nil) x = x[:x] end super x, y, font, '', text_color, text_color @w = w @h = h if bg.is_a? Integer @bg_color = bg else # String or Symbol @bg = Res.img bg end if fg.is_a? Integer @fg_color = fg else # String or Symbol @fg = Res.img fg @fg_path = "#{Res.prefix}#{Res.img_dir}#{fg.to_s.gsub(Res.separator, '/')}.png" puts @fg_path end @fg_margin_x = fg_margin_x @fg_margin_y = fg_margin_y # @fg_left = fg_left # @fg_right = fg_right @max_value = max_value self.value = value @format = format end # Increases the current value of the progress bar by the given amount. # # Parameters: # [amount] (+Integer+) The amount to be added to the current value. If the # sum surpasses +max_value+, it is set to +max_value+. def increase(amount) @value += amount @value = @max_value if @value > @max_value end # Descreases the current value of the progress bar by the given amount. # # Parameters: # [amount] (+Integer+) The amount to be subtracted from the current value. # If the result is less than zero, it is set to zero. def decrease(amount) @value -= amount @value = 0 if @value < 0 end # Sets the value of the progress bar. # # Parameters: # [val] (+Integer+) The value to be set. It will be changed as needed to be # between zero and +max_value+. def value=(val) @value = val if @value > @max_value @value = @max_value elsif @value < 0 @value = 0 end end # Sets the value of the progress bar to a given percentage of +max_value+. # # Parameters: # [pct] (+Numeric+) The percentage of +max_value+ to set the current value # to. The final result will be changed as needed to be between zero # and +max_value+. def percentage=(pct) self.value = (pct * @max_value).round end # Draws the progress bar. # # Parameters: # [alpha] (+Fixnum+) The opacity with which the progress bar will be drawn. # Allowed values vary between 0 (fully transparent) and 255 (fully # opaque). # [z_index] (+Fixnum+) The z-order to draw the object. Objects with larger # z-orders will be drawn on top of the ones with smaller z-orders. def draw(alpha = 0xff, z_index = 0) return unless @visible if @bg c = (alpha << 24) | 0xffffff @bg.draw @x, @y, z_index, 1, 1, c else c = (alpha << 24) | @bg_color G.window.draw_quad @x, @y, c, @x + @w, @y, c, @x + @w, @y + @h, c, @x, @y + @h, c, z_index end if @fg c = (alpha << 24) | 0xffffff w1 = @fg.width w2 = (@value.to_f / @max_value * @w).round x0 = @x + @fg_margin_x x = 0 while x <= w2 - w1 @fg.draw x0 + x, @y + @fg_margin_y, z_index, 1, 1, c x += w1 end if w2 - x > 0 img = Gosu::Image.new(G.window, @fg_path, true, 0, 0, w2 - x, @fg.height) img.draw x0 + x, @y + @fg_margin_y, z_index, 1, 1, c end else c = (alpha << 24) | @fg_color rect_r = @x + (@value.to_f / @max_value * @w).round G.window.draw_quad @x, @y, c, rect_r, @y, c, rect_r, @y + @h, c, @x, @y + @h, c, z_index end if @font c = (alpha << 24) | @text_color @text = @format == '%' ? "#{(@value.to_f / @max_value * 100).round}%" : "#{@value}/#{@max_value}" @font.draw_rel @text, @x + @fg_margin_x + @w / 2, @y + @fg_margin_y + @h / 2, 0, 0.5, 0.5, 1, 1, c end end end # This class represents a "drop-down list" form component, here composed of a # group of +Button+ objects. class DropDownList < Component # The selected value in the drop-down list. This is one of the +options+. attr_reader :value # An array containing all the options (each of them +String+s) that can be # selected in the drop-down list. attr_accessor :options # Creates a new drop-down list. # # Parameters: # [x] The x-coordinate of the object. # [y] The y-coordinate of the object. # [font] Font to be used by the buttons that compose the drop-donwn list. # [img] Image of the main button, i.e., the one at the top, that toggles # visibility of the other buttons (the "option" buttons). # [opt_img] Image for the "option" buttons, as described above. # [options] Array of available options for this control (+String+s). # [option] Index of the firstly selected option. # [text_margin] Left margin of the text inside the buttons (vertically, the # text will always be centered). # [width] Width of the control, used when no image is provided. # [height] Height of the control, used when no image is provided. # [text_color] Used as the +text_color+ parameter in the constructor of the # buttons. # [disabled_text_color] Analogous to +text_color+. # [over_text_color] Same as above. # [down_text_color] Same as above. # [on_changed] Action performed when the value of the dropdown is changed. # It must be a block with two parameters, which will receive # the old and the new value, respectively. # # *Obs.:* This method accepts named parameters, but +x+, +y+, +font+ and # +options+ are mandatory (also, +img+ and +opt_img+ are mandatory when # +width+ and +height+ are not provided, and vice-versa). def initialize(x, y = nil, font = nil, img = nil, opt_img = nil, options = nil, option = 0, text_margin = 0, width = nil, height = nil, text_color = 0, disabled_text_color = 0, over_text_color = 0, down_text_color = 0, &on_changed) if x.is_a? Hash y = x[:y] font = x[:font] img = x[:img] opt_img = x[:opt_img] options = x[:options] option = x.fetch(:option, 0) text_margin = x.fetch(:text_margin, 0) width = x.fetch(:width, nil) height = x.fetch(:height, nil) text_color = x.fetch(:text_color, 0) disabled_text_color = x.fetch(:disabled_text_color, 0) over_text_color = x.fetch(:over_text_color, 0) down_text_color = x.fetch(:down_text_color, 0) x = x[:x] end super x, y, font, options[option], text_color, disabled_text_color @img = img @opt_img = opt_img @options = options @value = @options[option] @open = false @buttons = [] @buttons.push( Button.new(x, y, font, @value, img, text_color, disabled_text_color, over_text_color, down_text_color, false, true, text_margin, 0, width, height) { toggle } ) @w = @buttons[0].w @h = @buttons[0].h @options.each_with_index do |o, i| b = Button.new(x, y + (i+1) * @h, font, o, opt_img, text_color, disabled_text_color, over_text_color, down_text_color, false, true, text_margin, 0, @w, @h) { old = @value @value = @buttons[0].text = o @on_changed.call(old, o) if @on_changed toggle } b.visible = false @buttons.push b end @max_h = (@options.size + 1) * @h @on_changed = on_changed end # Updates the control. def update return unless @enabled and @visible if @open and Mouse.button_pressed? :left and not Mouse.over?(@x, @y, @w, @max_h) toggle return end @buttons.each { |b| b.update } end # Sets the currently selected value of the drop-down list. It is ignored if # it is not among the available options. def value=(val) if @options.include? val old = @value @value = @buttons[0].text = val @on_changed.call(old, val) if @on_changed end end def enabled=(value) # :nodoc: toggle if @open @buttons[0].enabled = value @enabled = value end # Draws the drop-down list. # # Parameters: # [alpha] (+Fixnum+) The opacity with which the drop-down list will be # drawn. Allowed values vary between 0 (fully transparent) and 255 # (fully opaque). # [z_index] (+Fixnum+) The z-order to draw the object. Objects with larger # z-orders will be drawn on top of the ones with smaller z-orders. def draw(alpha = 0xff, z_index = 0) return unless @visible unless @img bottom = @open ? @y + @max_h + 1 : @y + @h + 1 b_color = (alpha << 24) G.window.draw_quad @x - 1, @y - 1, b_color, @x + @w + 1, @y - 1, b_color, @x + @w + 1, bottom, b_color, @x - 1, bottom, b_color, z_index @buttons.each do |b| color = (alpha << 24) | (b.state == :over ? 0xcccccc : 0xffffff) G.window.draw_quad b.x, b.y, color, b.x + b.w, b.y, color, b.x + b.w, b.y + b.h, color, b.x, b.y + b.h, color, z_index if b.visible end end @buttons.each { |b| b.draw alpha, z_index } end private def toggle if @open @buttons[1..-1].each { |b| b.visible = false } @open = false else @buttons[1..-1].each { |b| b.visible = true } @open = true end end end end