├── .rspec
├── lib
├── fidgit
│ ├── version.rb
│ ├── chingu_ext
│ │ └── window.rb
│ ├── elements
│ │ ├── horizontal.rb
│ │ ├── vertical.rb
│ │ ├── composite.rb
│ │ ├── main_packer.rb
│ │ ├── tool_tip.rb
│ │ ├── color_well.rb
│ │ ├── list.rb
│ │ ├── packer.rb
│ │ ├── group.rb
│ │ ├── color_picker.rb
│ │ ├── image_frame.rb
│ │ ├── toggle_button.rb
│ │ ├── scroll_area.rb
│ │ ├── radio_button.rb
│ │ ├── text_line.rb
│ │ ├── scroll_window.rb
│ │ ├── combo_box.rb
│ │ ├── label.rb
│ │ ├── button.rb
│ │ ├── slider.rb
│ │ ├── scroll_bar.rb
│ │ ├── menu_pane.rb
│ │ ├── file_browser.rb
│ │ ├── container.rb
│ │ ├── grid.rb
│ │ ├── element.rb
│ │ └── text_area.rb
│ ├── window.rb
│ ├── gosu_ext
│ │ ├── image.rb
│ │ └── color.rb
│ ├── standard_ext
│ │ └── hash.rb
│ ├── states
│ │ ├── file_dialog.rb
│ │ ├── dialog_state.rb
│ │ ├── message_dialog.rb
│ │ └── gui_state.rb
│ ├── cursor.rb
│ ├── selection.rb
│ ├── redirector.rb
│ ├── history.rb
│ ├── schema.rb
│ └── event.rb
└── fidgit.rb
├── media
└── images
│ ├── arrow.png
│ ├── pixel.png
│ ├── combo_arrow.png
│ ├── file_file.png
│ └── file_directory.png
├── .gitignore
├── Gemfile
├── examples
├── media
│ └── images
│ │ └── head_icon.png
├── _all_examples.rb
├── color_picker_example.rb
├── color_well_example.rb
├── combo_box_example.rb
├── helpers
│ └── example_window.rb
├── list_example.rb
├── menu_pane_example.rb
├── radio_button_example.rb
├── text_area_example.rb
├── readme_example.rb
├── grid_packer_example.rb
├── label_example.rb
├── slider_example.rb
├── splash_example.rb
├── button_and_toggle_button_example.rb
├── file_dialog_example.rb
├── scroll_window_example.rb
├── align_example.rb
└── message_dialog_example.rb
├── Rakefile
├── spec
└── fidgit
│ ├── elements
│ ├── helpers
│ │ └── helper.rb
│ ├── label_spec.rb
│ └── image_frame_spec.rb
│ ├── gosu_ext
│ ├── helpers
│ │ └── helper.rb
│ └── color_spec.rb
│ ├── helpers
│ └── helper.rb
│ ├── schema_test.yml
│ ├── schema_spec.rb
│ ├── redirector_spec.rb
│ ├── history_spec.rb
│ └── event_spec.rb
├── CHANGELOG.md
├── LICENSE.txt
├── fidgit.gemspec
├── config
└── default_schema.yml
└── README.md
/.rspec:
--------------------------------------------------------------------------------
1 | --colour
2 | --format documentation
3 |
--------------------------------------------------------------------------------
/lib/fidgit/version.rb:
--------------------------------------------------------------------------------
1 | module Fidgit
2 | VERSION = '0.2.7'
3 | end
4 |
--------------------------------------------------------------------------------
/media/images/arrow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gosu/fidgit/HEAD/media/images/arrow.png
--------------------------------------------------------------------------------
/media/images/pixel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gosu/fidgit/HEAD/media/images/pixel.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | pkg/*
2 | *.gem
3 | .bundle
4 | Gemfile.lock
5 | doc/
6 | .yardoc
7 | README.html
8 | .idea/
--------------------------------------------------------------------------------
/media/images/combo_arrow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gosu/fidgit/HEAD/media/images/combo_arrow.png
--------------------------------------------------------------------------------
/media/images/file_file.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gosu/fidgit/HEAD/media/images/file_file.png
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "http://rubygems.org"
2 |
3 | # Specify your gem's dependencies in fidgit.gemspec
4 | gemspec
--------------------------------------------------------------------------------
/media/images/file_directory.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gosu/fidgit/HEAD/media/images/file_directory.png
--------------------------------------------------------------------------------
/examples/media/images/head_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gosu/fidgit/HEAD/examples/media/images/head_icon.png
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'bundler'
2 | Bundler::GemHelper.install_tasks
3 |
4 | # Specs
5 | desc "Run rspec 2.0"
6 | task :rspec do
7 | system "rspec spec"
8 | end
--------------------------------------------------------------------------------
/spec/fidgit/elements/helpers/helper.rb:
--------------------------------------------------------------------------------
1 | require "rspec"
2 |
3 | require_relative File.join(File.dirname(__FILE__), "..", "..", "..", "..", "lib", "fidgit")
--------------------------------------------------------------------------------
/spec/fidgit/gosu_ext/helpers/helper.rb:
--------------------------------------------------------------------------------
1 | require "rspec"
2 |
3 | require_relative File.join(File.dirname(__FILE__), "..", "..", "..", "..", "lib", "fidgit")
--------------------------------------------------------------------------------
/spec/fidgit/helpers/helper.rb:
--------------------------------------------------------------------------------
1 | require "rspec"
2 | require "fileutils"
3 |
4 | $LOAD_PATH.unshift File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "..", "lib", "fidgit"))
--------------------------------------------------------------------------------
/lib/fidgit/chingu_ext/window.rb:
--------------------------------------------------------------------------------
1 | module Chingu
2 | class Window
3 | # Include fidgits extra functionality in the Chingu window (which is minimal).
4 | include Fidgit::Window
5 | end
6 | end
--------------------------------------------------------------------------------
/lib/fidgit/elements/horizontal.rb:
--------------------------------------------------------------------------------
1 | module Fidgit
2 | # A vertically aligned element packing container.
3 | class Horizontal < Grid
4 | def initialize(options = {})
5 | options[:num_rows] = 1
6 |
7 | super options
8 | end
9 | end
10 | end
--------------------------------------------------------------------------------
/lib/fidgit/elements/vertical.rb:
--------------------------------------------------------------------------------
1 | module Fidgit
2 | # A vertically aligned element packing container.
3 | class Vertical < Grid
4 | def initialize(options = {})
5 | options[:num_columns] = 1
6 |
7 | super options
8 | end
9 | end
10 | end
--------------------------------------------------------------------------------
/lib/fidgit/window.rb:
--------------------------------------------------------------------------------
1 | module Fidgit
2 | module Window
3 |
4 | def self.included(base)
5 | base.send :include, Methods
6 | end
7 |
8 | module Methods
9 |
10 | def close
11 | super
12 | GuiState.clear
13 | end
14 | end
15 |
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/lib/fidgit/gosu_ext/image.rb:
--------------------------------------------------------------------------------
1 | module Gosu
2 | EmptyImageSource = Struct.new(:columns, :rows) do
3 | def to_blob
4 | "\0\0\0\0" * (columns * rows)
5 | end
6 | end
7 |
8 | class Image
9 | def self.create(width, height)
10 | self.new(EmptyImageSource.new(width, height))
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/examples/_all_examples.rb:
--------------------------------------------------------------------------------
1 | # Run all examples, one after the other
2 | require_relative 'helpers/example_window'
3 |
4 |
5 | examples = Dir.glob(File.join(File.dirname(__FILE__), "*.rb")) - [__FILE__]
6 | examples.each_with_index do |file_name, index|
7 | ENV['FIDGIT_EXAMPLES_TEXT'] = "[#{index + 1} of #{examples.size + 1}]"
8 | `ruby #{file_name}`
9 | end
10 |
--------------------------------------------------------------------------------
/examples/color_picker_example.rb:
--------------------------------------------------------------------------------
1 | require_relative 'helpers/example_window'
2 |
3 | class ExampleState < Fidgit::GuiState
4 | def initialize
5 | super
6 |
7 | vertical do
8 | my_label = label 'No color picked'
9 |
10 | color_picker(width: 100) do |sender, color|
11 | my_label.text = color.to_s
12 | end
13 | end
14 | end
15 | end
16 |
17 | ExampleWindow.new.show
--------------------------------------------------------------------------------
/lib/fidgit/elements/composite.rb:
--------------------------------------------------------------------------------
1 | module Fidgit
2 | # A composite element, made up of other elements (but manages them internally).
3 | class Composite < Packer
4 | DEBUG_BORDER_COLOR = Gosu::Color.rgba(0, 255, 0, 100) # Color to draw an outline in when debugging layout.
5 |
6 | # @param (see Element#initialize)
7 | #
8 | # @option (see Element#initialize)
9 | def initialize(options = {})
10 | options[:border_color] = DEBUG_BORDER_COLOR if Fidgit.debug_mode?
11 |
12 | super(options)
13 | end
14 | end
15 | end
--------------------------------------------------------------------------------
/examples/color_well_example.rb:
--------------------------------------------------------------------------------
1 | require_relative 'helpers/example_window'
2 |
3 | class ExampleState < Fidgit::GuiState
4 | def initialize
5 | super
6 |
7 | vertical do
8 | my_label = label "No color selected."
9 |
10 | group do
11 | grid num_columns: 15, padding: 0, spacing: 4 do
12 | 150.times do
13 | color_well(color: Gosu::Color.rgb(rand(255), rand(255), rand(255)))
14 | end
15 | end
16 |
17 | subscribe :changed do |sender, color|
18 | my_label.text = color.to_s
19 | end
20 | end
21 | end
22 | end
23 | end
24 |
25 | ExampleWindow.new.show
--------------------------------------------------------------------------------
/examples/combo_box_example.rb:
--------------------------------------------------------------------------------
1 | require_relative 'helpers/example_window'
2 |
3 | # Example for Button and ToggleButton
4 | class ExampleState < Fidgit::GuiState
5 | def initialize
6 | super
7 |
8 | vertical do
9 | my_label = label "Label", tip: "I'm a label"
10 |
11 | combo_box(value: 1, tip: "I'm a combo box; press me and make a selection!") do
12 | subscribe :changed do |sender, value|
13 | my_label.text = "Chose #{value}!"
14 | end
15 |
16 | item "One", 1
17 | item "Two", 2
18 | item "Three", 3
19 | end
20 | end
21 | end
22 | end
23 |
24 | ExampleWindow.new.show
--------------------------------------------------------------------------------
/examples/helpers/example_window.rb:
--------------------------------------------------------------------------------
1 | require_relative '../../lib/fidgit'
2 |
3 | media_dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'media'))
4 | Gosu::Image.autoload_dirs << File.join(media_dir, 'images')
5 | Gosu::Sample.autoload_dirs << File.join(media_dir, 'samples')
6 | Gosu::Font.autoload_dirs << File.join(media_dir, 'fonts')
7 |
8 | class ExampleWindow < Chingu::Window
9 | def initialize(options = {})
10 | super(640, 480, false)
11 |
12 | on_input(:escape) { close }
13 |
14 | caption = "#{File.basename($0).chomp(".rb").tr('_', ' ')} #{ENV['FIDGIT_EXAMPLES_TEXT']}"
15 | push_game_state ExampleState
16 | end
17 | end
--------------------------------------------------------------------------------
/examples/list_example.rb:
--------------------------------------------------------------------------------
1 | require_relative 'helpers/example_window'
2 |
3 | # Labels can have text and/or icons.
4 | class ExampleState < Fidgit::GuiState
5 | def initialize
6 | super
7 |
8 | vertical do
9 | my_label = label "No clicky"
10 |
11 | list do
12 | item "chunky bacon", :CHUNKYBACON, tip: "You prefer Chunky Bacon, don't you?"
13 | item "lentils", :LENTILS, tip: "Lentils? Well, I suppose someone has to like them"
14 |
15 | subscribe :changed do |sender, value|
16 | my_label.text = "I like #{value} more than anything in the world!"
17 | end
18 | end
19 | end
20 | end
21 | end
22 |
23 | ExampleWindow.new.show
--------------------------------------------------------------------------------
/lib/fidgit/elements/main_packer.rb:
--------------------------------------------------------------------------------
1 | require_relative "vertical"
2 |
3 | module Fidgit
4 | # Main container that can contains a single "proper" packing element.
5 | class MainPacker < Vertical
6 | def initialize(options = {})
7 | options = {
8 | width: $window.width,
9 | height: $window.height,
10 | }.merge! options
11 |
12 | super options
13 | end
14 |
15 | def width; $window.width; end
16 | def height; $window.height; end
17 | def width=(value); ; end
18 | def height=(value); ; end
19 |
20 | def add(element)
21 | raise "MainPacker can only contain packing elements" unless element.is_a? Packer
22 | super(element)
23 | end
24 | end
25 | end
--------------------------------------------------------------------------------
/lib/fidgit/standard_ext/hash.rb:
--------------------------------------------------------------------------------
1 | class Hash
2 | # Merge not only the hashes, but all nested hashes as well.
3 | # Written by Stefan Rusterholz (apeiros) from http://www.ruby-forum.com/topic/142809
4 | def deep_merge!(other)
5 | merger = lambda do |key, a, b|
6 | (a.is_a?(Hash) && b.is_a?(Hash)) ? a.merge!(b, &merger) : b
7 | end
8 |
9 | merge!(other, &merger)
10 | end
11 |
12 | # Merge not only the hashes, but all nested hashes as well.
13 | # Written by Stefan Rusterholz (apeiros) from http://www.ruby-forum.com/topic/142809
14 | def deep_merge(other)
15 | merger = lambda do |key, a, b|
16 | (a.is_a?(Hash) && b.is_a?(Hash)) ? a.merge(b, &merger) : b
17 | end
18 |
19 | merge(other, &merger)
20 | end
21 | end
--------------------------------------------------------------------------------
/lib/fidgit/states/file_dialog.rb:
--------------------------------------------------------------------------------
1 | module Fidgit
2 | # A simple dialog that manages a message with a set of buttons beneath it.
3 | class FileDialog < DialogState
4 | def initialize(type, options = {}, &block)
5 | options = {
6 | show: true,
7 | background_color: DEFAULT_BACKGROUND_COLOR,
8 | border_color: DEFAULT_BORDER_COLOR,
9 | }.merge! options
10 |
11 | super(options)
12 |
13 | vertical align: :center, padding: 0 do |packer|
14 | FileBrowser.new(type, { parent: packer }.merge!(options)) do |sender, result, file_name|
15 | hide
16 | block.call result, file_name if block
17 | end
18 | end
19 |
20 | show if options[:show]
21 | end
22 | end
23 | end
24 |
25 |
--------------------------------------------------------------------------------
/spec/fidgit/schema_test.yml:
--------------------------------------------------------------------------------
1 | # Test case schema
2 | ---
3 |
4 | # Define all constant values here. For colours, use [R, G, B] or [R, G, B, A].
5 | # Reference constants with ?constant_name
6 | :constants:
7 | # General constants (strings and numbers).
8 | :scroll_bar_thickness: 12
9 |
10 | # Gosu::Color constants
11 | :none: [0, 0, 0, 0]
12 | :white: [255, 255, 255]
13 | :gray: [100, 100, 100]
14 | :dark_gray: [50, 50, 50]
15 |
16 | # Default element attributes.
17 | :elements:
18 | :Button:
19 | :background_color: ?gray
20 |
21 | :disabled:
22 | :background_color: ?dark_gray
23 |
24 | :Element:
25 | :color: ?white
26 | :align_h: :left
27 |
28 | :HorizontalScrollBar:
29 | :height: ?scroll_bar_thickness
30 |
31 | :VerticalScrollBar:
32 | :width: ?scroll_bar_thickness
33 |
--------------------------------------------------------------------------------
/examples/menu_pane_example.rb:
--------------------------------------------------------------------------------
1 | require_relative 'helpers/example_window'
2 |
3 | # Labels can have text and/or icons.
4 | class ExampleState < Fidgit::GuiState
5 | def initialize
6 | super
7 |
8 | vertical do
9 | my_label = label "Right click to open context menu"
10 |
11 | my_label.subscribe :released_right_mouse_button do
12 | menu do
13 | item "Chunky bacon", :CHUNKY_BACON, shortcut_text: "Ctrl-^-*"
14 | separator
15 | item "Lentils", :LENTILS, shortcut_text: "Alt-F15"
16 | item "Tepid gruel", :GRUEL, shortcut_text: "CenterMeta"
17 |
18 | subscribe :selected do |sender, value|
19 | my_label.text = "I like #{value} more than anything. Mmmm!"
20 | end
21 | end
22 | end
23 | end
24 | end
25 | end
26 |
27 | ExampleWindow.new.show
--------------------------------------------------------------------------------
/examples/radio_button_example.rb:
--------------------------------------------------------------------------------
1 | require_relative 'helpers/example_window'
2 |
3 | class ExampleState < Fidgit::GuiState
4 | def initialize
5 | super
6 |
7 | vertical do
8 | my_label = label "No button selected"
9 |
10 | button("Deselect") do
11 | @group.value = nil
12 | end
13 |
14 | button("Select #7") do
15 | @group.value = 7
16 | end
17 |
18 | @group = group do
19 | grid num_columns: 5, padding: 0 do
20 | 15.times do |i|
21 | radio_button "##{i}", i, width: 60
22 | end
23 | end
24 |
25 | subscribe :changed do |sender, value|
26 | my_label.text = if value
27 | "Button #{value.to_s} selected"
28 | else
29 | "No button selected"
30 | end
31 | end
32 | end
33 | end
34 | end
35 | end
36 |
37 | ExampleWindow.new.show
--------------------------------------------------------------------------------
/examples/text_area_example.rb:
--------------------------------------------------------------------------------
1 | require_relative 'helpers/example_window'
2 |
3 | class ExampleState < Fidgit::GuiState
4 | def initialize
5 | super
6 |
7 | string = "Hello, my name is Brian the snail!"
8 | horizontal do
9 | vertical do
10 | label 'disabled'
11 | text_area(text: "Can't even select this text", width: 200, enabled: false)
12 | end
13 |
14 | vertical do
15 | label 'mirrors to right'
16 | text_area(width: 200, text: string) do |_, text|
17 | @mirror.text = text
18 | end
19 | end
20 |
21 | vertical do
22 | my_label = label 'not editable'
23 | font = Gosu::Font.new($window, "", my_label.font.height)
24 | font["a"] = Gosu::Image["head_icon.png"]
25 | @mirror = text_area(text: string, width: 200, editable: false, font: font)
26 | end
27 | end
28 | end
29 | end
30 |
31 | ExampleWindow.new.show
--------------------------------------------------------------------------------
/lib/fidgit/cursor.rb:
--------------------------------------------------------------------------------
1 | module Fidgit
2 | class Cursor < Chingu::GameObject
3 | ARROW = 'arrow.png'
4 | HAND = 'hand.png'
5 |
6 | def initialize(options = {})
7 | options = {
8 | image: Gosu::Image[ARROW],
9 | rotation_center: :top_left,
10 | zorder: Float::INFINITY
11 | }.merge!(options)
12 |
13 | super(options)
14 |
15 | nil
16 | end
17 |
18 | # Is the mouse pointer position inside the game window pane?
19 | def inside_window?
20 | x >= 0 and y >= 0 and x < $window.width and y < $window.height
21 | end
22 |
23 | def update
24 | self.x, self.y = $window.mouse_x, $window.mouse_y
25 |
26 | super
27 |
28 | nil
29 | end
30 |
31 | def draw
32 | # Prevent system and game mouse from being shown at the same time.
33 | super if inside_window? and $window.current_game_state.is_a? GuiState and not $window.needs_cursor?
34 | end
35 | end
36 | end
--------------------------------------------------------------------------------
/examples/readme_example.rb:
--------------------------------------------------------------------------------
1 | require_relative "../lib/fidgit"
2 |
3 | # Normally, you'd load this from the gem using:
4 | # require 'fidgit'
5 |
6 | class MyGame < Chingu::Window
7 | def initialize
8 | super(640, 480, false)
9 |
10 | # To use the Fidgit features, a Fidgit::GuiState must be active.
11 | push_game_state MyGuiState
12 | end
13 | end
14 |
15 | class MyGuiState < Fidgit::GuiState
16 | def initialize
17 | super
18 |
19 | # Create a vertically packed section, centred on the window.
20 | vertical align: :center do
21 | # Create a label with a dark green background.
22 | my_label = label "Hello world!", background_color: Gosu::Color.rgb(0, 100, 0)
23 |
24 | # Create a button that, when clicked, changes the label text.
25 | button("Goodbye", align_h: :center, tip: "Press me and be done with it!") do
26 | my_label.text = "Goodbye cruel world!"
27 | end
28 | end
29 | end
30 | end
31 |
32 | MyGame.new.show
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | Fidgit changelog
2 | ================
3 |
4 | v0.2.3
5 | ------
6 |
7 | * Exposed #handle accessor for Slider.
8 | * Made Slider accept being disabled.
9 | * Made Slider handle show tip.
10 |
11 | v0.2.2
12 | ------
13 |
14 | * Prevented ToggleButton border colour changing.
15 | * More careful about stripping out html tags in TextArea.
16 |
17 | v0.2.1
18 | ------
19 |
20 | * Added: Click in ScrollBar gutter to scroll window by height/width of view window.
21 | * Added: Click and drag to select text in enabled TextArea.
22 | * Fixed: Color changes on disabling buttons.
23 |
24 | v0.2.0
25 | ------
26 |
27 | * Added editable attribute to TextArea (Allows selection, but not alteration).
28 | * Added Element#font= and :font option.
29 | * Added Gosu::Color#colorize to use when using in-line text styling.
30 | * Managed layout of entities and XML tags (Used by Gosu) in TextArea text better (tags still don't like newlines inside them).
31 | * Changed license from LGPL to MIT.
--------------------------------------------------------------------------------
/spec/fidgit/elements/label_spec.rb:
--------------------------------------------------------------------------------
1 | require_relative "helpers/helper"
2 |
3 | include Fidgit
4 |
5 | describe Label do
6 | before :all do
7 | $window = Chingu::Window.new(100, 100, false) unless $window
8 | end
9 |
10 | context "with default parameters" do
11 | subject { Label.new( "Hello world!") }
12 |
13 | it "should have text value set" do
14 | subject.text.should eq "Hello world!"
15 | end
16 |
17 | it "should have white text" do
18 | subject.color.should eq Gosu::Color.rgb(255, 255, 255)
19 | end
20 |
21 | it "should have a transparent background" do
22 | subject.background_color.should be_transparent
23 | end
24 |
25 | it "should have a transparent border" do
26 | subject.border_color.should be_transparent
27 | end
28 |
29 | it "should be enabled" do
30 | subject.should be_enabled
31 | end
32 |
33 | it "should not have an image" do
34 | subject.icon.should be_nil
35 | end
36 | end
37 | end
--------------------------------------------------------------------------------
/lib/fidgit/elements/tool_tip.rb:
--------------------------------------------------------------------------------
1 | module Fidgit
2 | class ToolTip < TextLine
3 | def x=(value); super(value); recalc; value; end
4 | def y=(value); super(value); recalc; value; end
5 | def hit?(x, y); false; end
6 |
7 |
8 | # @param (see Label#initialize)
9 | #
10 | # @option (see Label#initialize)
11 | def initialize(options = {}, &block)
12 | options = {
13 | z: Float::INFINITY,
14 | background_color: default(:background_color),
15 | border_color: default(:border_color),
16 | text: '',
17 | }.merge! options
18 |
19 | super(options[:text], options)
20 | end
21 |
22 | protected
23 | def layout
24 | super
25 |
26 | # Ensure the tip can't go over the edge of the screen. If it can't be avoided, align with left edge of screen.
27 | rect.x = [[x, $window.width - width - padding_right].min, 0].max
28 | rect.y = [[y, $window.height - height - padding_bottom].min, 0].max
29 |
30 | nil
31 | end
32 | end
33 | end
--------------------------------------------------------------------------------
/lib/fidgit/elements/color_well.rb:
--------------------------------------------------------------------------------
1 | module Fidgit
2 | class ColorWell < RadioButton
3 | alias_method :color, :value
4 |
5 | # @param (see RadioButton#initialize)
6 | # @option (see RadioButton#initialize)
7 | def initialize(options = {}, &block)
8 | options = {
9 | width: default(:width),
10 | height: default(:height),
11 | color: default(:color),
12 | outline_color: default(:outline_color),
13 | checked_border_color: default(:checked, :border_color),
14 | }.merge! options
15 |
16 | @outline_color = options[:outline_color].dup
17 |
18 | super('', (options[:color] || options[:value]).dup, options)
19 | end
20 |
21 | protected
22 | def draw_background
23 | super
24 |
25 | draw_frame x + 2, y + 2, width - 4, height - 4, 1, z, @outline_color
26 |
27 | nil
28 | end
29 |
30 | protected
31 | def draw_foreground
32 | draw_rect x + 3, y + 3, width - 6, height - 6, z, value
33 |
34 | nil
35 | end
36 | end
37 | end
--------------------------------------------------------------------------------
/examples/grid_packer_example.rb:
--------------------------------------------------------------------------------
1 | require_relative 'helpers/example_window'
2 |
3 | class ExampleState < Fidgit::GuiState
4 | BORDER_COLOR = Gosu::Color.rgb(255, 0, 0)
5 | FIXED_NUM = 5
6 | NUM_CELLS = 17
7 |
8 | def initialize
9 | super
10 |
11 | vertical do
12 | label "Grid with #{FIXED_NUM} columns"
13 | grid num_columns: FIXED_NUM, border_color: BORDER_COLOR, cell_border_color: Gosu::Color.rgba(0, 255, 0, 255), cell_border_thickness: 1 do
14 | NUM_CELLS.times do |i|
15 | label "Cell #{i}", font_height: rand(15) + 15, border_color: BORDER_COLOR, border_thickness: 1
16 | end
17 | end
18 |
19 | label "Grid with #{FIXED_NUM} rows"
20 | grid num_rows: FIXED_NUM, border_color: BORDER_COLOR, cell_background_color: Gosu::Color.rgba(0, 100, 100, 255) do
21 | NUM_CELLS.times do |i|
22 | label "Cell #{i}", font_height: rand(15) + 15, border_color: BORDER_COLOR, border_thickness: 1
23 | end
24 | end
25 | end
26 | end
27 | end
28 |
29 | ExampleWindow.new.show
--------------------------------------------------------------------------------
/examples/label_example.rb:
--------------------------------------------------------------------------------
1 | require_relative 'helpers/example_window'
2 |
3 | # Labels can have text and/or icons.
4 | class ExampleState < Fidgit::GuiState
5 | def initialize
6 | super
7 |
8 | vertical background_color: Gosu::Color.rgb(255, 0, 0) do
9 | label "Hello!", tip: 'A label with text'
10 | label "Hello!", icon: Gosu::Image["head_icon.png"], tip: 'A label with text & icon'
11 | label '', icon: Gosu::Image["head_icon.png"], tip: 'A label with just icon'
12 | label '', background_color: Gosu::Color.rgb(0, 255, 0), tip: 'No text or icon'
13 | end
14 |
15 | vertical do
16 | label ":left justification", width: 400, background_color: Gosu::Color.rgb(0, 100, 0), justify: :left, tip: 'A label with text'
17 | label ":right justification", width: 400, background_color: Gosu::Color.rgb(0, 100, 0), justify: :right, tip: 'A label with text'
18 | label ":center justification", width: 400, background_color: Gosu::Color.rgb(0, 100, 0), justify: :center, tip: 'A label with text'
19 | end
20 | end
21 | end
22 |
23 | ExampleWindow.new.show
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2012 Bil Bas (Spooner)
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/lib/fidgit/elements/list.rb:
--------------------------------------------------------------------------------
1 | module Fidgit
2 | class List < Composite
3 | class Item < RadioButton
4 | end
5 |
6 | event :changed
7 |
8 | def size; @items.size; end
9 | def clear; @items.clear; end
10 |
11 | def initialize(options = {})
12 | options = {
13 | background_color: default(:background_color),
14 | border_color: default(:border_color),
15 | }.merge! options
16 |
17 | super options
18 |
19 | group do
20 | subscribe :changed do |sender, value|
21 | publish :changed, value
22 | end
23 |
24 | @items = vertical spacing: 0
25 | end
26 | end
27 |
28 | # @param [String] text
29 | # @option options [Gosu::Image] :icon
30 | def item(text, value, options = {}, &block)
31 | Item.new(text, value, { parent: @items }.merge!(options), &block)
32 | end
33 |
34 | protected
35 | def layout
36 | super
37 | if @items
38 | max_width = @items.each.to_a.map {|c| c.width }.max || 0
39 | @items.each {|c| c.rect.width = max_width }
40 | end
41 |
42 | nil
43 | end
44 | end
45 | end
--------------------------------------------------------------------------------
/examples/slider_example.rb:
--------------------------------------------------------------------------------
1 | require_relative 'helpers/example_window'
2 |
3 | class ExampleState < Fidgit::GuiState
4 | def initialize
5 | super
6 |
7 | vertical do
8 | horizontal do
9 | # Discrete values (0..100)
10 | slider = slider(width: 100, range: 0..5, value: 3, tip: "Discrete value is") do |sender, value|
11 | @discrete_label.text = "Discrete slider is at #{value}"
12 | end
13 |
14 | @discrete_label = label "Discrete slider is at #{slider.value}"
15 | end
16 |
17 | horizontal do
18 | # Continuous values (0.0..1.0)
19 |
20 | slider = slider(width: 100, range: 0.0..100.0, value: 77.2, tip: "Continuous value is") do |sender, value|
21 | @continuous_label.text = "Continuous slider is at #{"%.03f" % value}%"
22 | end
23 |
24 | @continuous_label = label "Continuous slider is at #{"%.03f" % slider.value}%"
25 | end
26 |
27 | horizontal do
28 | slider(width: 100, range: 0.0..100.0, value: 77.2, tip: "Disabled slider value is", enabled: false)
29 | end
30 | end
31 | end
32 | end
33 |
34 | ExampleWindow.new.show
--------------------------------------------------------------------------------
/fidgit.gemspec:
--------------------------------------------------------------------------------
1 | $LOAD_PATH.unshift File.expand_path('../lib', __FILE__)
2 | require 'fidgit/version'
3 |
4 | Gem::Specification.new do |s|
5 | s.name = 'fidgit'
6 | s.version = Fidgit::VERSION
7 | s.platform = Gem::Platform::RUBY
8 | s.authors = ['Bil Bas (Spooner)']
9 | s.email = ['bil.bagpuss@gmail.com']
10 | s.homepage = 'https://github.com/gosu/fidgit/'
11 | s.summary = 'Fidgit is a GUI library built on Gosu/Chingu'
12 | s.description = 'Fidgit is a GUI library built on Gosu/Chingu'
13 |
14 | s.files = `git ls-files`.split("\n")
15 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
16 | s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
17 | s.require_paths = ['lib']
18 | s.license = 'MIT'
19 |
20 | s.add_runtime_dependency('gosu', '~> 0.7')
21 | s.add_runtime_dependency('chingu', '~> 0.9rc9')
22 | s.add_runtime_dependency('clipboard', '~> 0.9')
23 |
24 | s.add_development_dependency('rspec', '~> 2.8')
25 | s.add_development_dependency('rake', '~> 12.3', '=> 12.3.3')
26 | s.add_development_dependency('yard', '~> 0.9.11')
27 | s.add_development_dependency('RedCloth', '~> 4.2', '>= 4.2.9')
28 | end
29 |
--------------------------------------------------------------------------------
/examples/splash_example.rb:
--------------------------------------------------------------------------------
1 | require_relative 'helpers/example_window'
2 |
3 | # By using a splash screen of some sort, one can switch to another resolution for the main game.
4 | class ExampleState < Fidgit::GuiState
5 | def initialize
6 | super
7 | vertical do
8 | horizontal do
9 | text = label "Width:"
10 |
11 | @width_combo = combo_box(value: [640, 480]) do
12 | [[640, 480], [800, 600], [Gosu::screen_width, Gosu::screen_height]].each do |width, height|
13 | item "#{width}x#{height}", [width, height]
14 | end
15 | end
16 | end
17 |
18 | @full_screen_button = toggle_button "Fullscreen?"
19 |
20 | button "Load game", icon: Gosu::Image["head_icon.png"] do
21 | $window.close
22 | window = Chingu::Window.new(*@width_combo.value, @full_screen_button.value)
23 | window.push_game_state ExampleAfterState
24 | window.show
25 | end
26 | end
27 | end
28 | end
29 |
30 | class ExampleAfterState < Fidgit::GuiState
31 | def initialize
32 | super
33 |
34 | on_input(:esc, :exit)
35 |
36 | vertical do
37 | label "Game loaded!", icon: Gosu::Image["head_icon.png"], border_color: Gosu::Color.rgb(255, 255, 255)
38 | end
39 | end
40 | end
41 |
42 | ExampleWindow.new.show
--------------------------------------------------------------------------------
/lib/fidgit/elements/packer.rb:
--------------------------------------------------------------------------------
1 | module Fidgit
2 | # Container that auto-packs elements.
3 | #
4 | # @abstract
5 | class Packer < Container
6 | attr_reader :spacing_h, :spacing_v
7 |
8 | # @param (see Container#initialize)
9 | #
10 | # @option (see Container#initialize)
11 | def initialize(options = {})
12 | options = {
13 | }.merge! options
14 |
15 | @spacing_h = options[:spacing_h] || options[:spacing] || default(:spacing_h)
16 | @spacing_v = options[:spacing_v] || options[:spacing] || default(:spacing_v)
17 |
18 | super(options)
19 | end
20 |
21 | protected
22 | # Recalculate the size of the container.
23 | # Should be overridden by any descendant that manages the positions of its children.
24 | def layout
25 | # This assumes that the container overlaps all the children.
26 |
27 | # Move all children if we have moved.
28 | @children.each.with_index do |child, index|
29 | child.x = padding_left + x
30 | child.y = padding_top + y
31 | end
32 |
33 | # Make us as wrap around the largest child.
34 | rect.width = (@children.map {|c| c.width }.max || 0) + padding_left + padding_right
35 | rect.height = (@children.map {|c| c.height }.max || 0) + padding_top + padding_bottom
36 |
37 | super
38 | end
39 | end
40 | end
--------------------------------------------------------------------------------
/examples/button_and_toggle_button_example.rb:
--------------------------------------------------------------------------------
1 | require_relative 'helpers/example_window'
2 |
3 | # Example for Button and ToggleButton
4 | class ExampleState < Fidgit::GuiState
5 | def initialize
6 | super
7 |
8 | vertical do
9 | my_label = label "Label", tip: "I'm a label"
10 |
11 | buttons = []
12 |
13 | # A plain button, with some text on it.
14 | buttons << button("Button", tip: "I'm a button; press me!", shortcut: :auto) do
15 | my_label.text = "Pressed the button!"
16 | end
17 |
18 | # Buttons with icons in each possible positions.
19 | [:left, :right, :top, :bottom].each do |position|
20 | buttons << button("Icon at #{position}", icon: Gosu::Image["head_icon.png"], icon_position: position, icon_options: { factor: 2 }, tip: "I'm a button; press me!", shortcut: :auto) do
21 | my_label.text = "Pressed the button (icon to #{position})!"
22 | end
23 | end
24 |
25 | # A toggling button.
26 | buttons << toggle_button("ToggleButton", tip: "I'm a button that toggles", shortcut: :o) do |sender, value|
27 | my_label.text = "Turned the toggle button #{value ? "on" : "off"}!"
28 | end
29 |
30 | # A toggle-button that controls whether all the other buttons are enabled.
31 | toggle_button("Enable other two buttons", value: true) do |sender, value|
32 | buttons.each {|button| button.enabled = value }
33 | end
34 | end
35 | end
36 | end
37 |
38 | ExampleWindow.new.show
--------------------------------------------------------------------------------
/examples/file_dialog_example.rb:
--------------------------------------------------------------------------------
1 | require_relative 'helpers/example_window'
2 |
3 |
4 | Fidgit::Element.schema.merge_elements!(Element: { font_height: 15 })
5 |
6 | class ExampleState < Fidgit::GuiState
7 | def initialize
8 | super
9 |
10 | container.background_color = Gosu::Color.rgb(50, 50, 50)
11 | vertical align: :center do
12 | full_base_directory = ''
13 | restricted_base_directory = File.expand_path(File.join(__FILE__, '..', '..'))
14 | directory = File.join(restricted_base_directory, 'media', 'images')
15 |
16 | my_label = label "No files are actually loaded or saved by this example"
17 | button("Load...(limited path access)") do
18 | file_dialog(:open, base_directory: restricted_base_directory, directory: directory, pattern: "*.png") do |result, file|
19 | case result
20 | when :open
21 | my_label.text = "Loaded #{file}"
22 | when :cancel
23 | my_label.text = "Loading cancelled"
24 | end
25 | end
26 | end
27 |
28 | button("Save...(unrestricted path access)") do
29 | file_dialog(:save, base_directory: full_base_directory, directory: directory, pattern: "*.png") do |result, file|
30 | case result
31 | when :save
32 | my_label.text = "Saved #{file}"
33 | when :cancel
34 | my_label.text = "Save cancelled"
35 | end
36 | end
37 | end
38 | end
39 | end
40 | end
41 |
42 | ExampleWindow.new.show
--------------------------------------------------------------------------------
/examples/scroll_window_example.rb:
--------------------------------------------------------------------------------
1 | require_relative 'helpers/example_window'
2 |
3 | # Example for Button and ToggleButton
4 | class ExampleState < Fidgit::GuiState
5 | HEIGHT = 225
6 | WIDTH = 140
7 | def initialize
8 | super
9 |
10 | vertical do
11 | horizontal do
12 | [
13 | [20, "All cheer the ascendancy of number "], # Should have both scrollers
14 | [20, "#"], # Only has v-scroller.
15 | [4, "#"], # No scrollers.
16 | [4, "All cheer the ascendancy of number "], # Only has h-scroller
17 | ].each do |num_labels, text|
18 | vertical do
19 | scroll_window(width: WIDTH, height: HEIGHT, background_color: Gosu::Color.rgb(0, 100, 0)) do
20 | vertical do
21 | (1..num_labels).each do |i|
22 | label "#{text}#{i}!"
23 | end
24 | end
25 | end
26 | end
27 | end
28 | end
29 |
30 | horizontal padding: 0 do
31 | vertical do
32 | scroll_window(width: 300, height: 150) do
33 | text_area(text: "Hello world! " * 19, width: 284)
34 | end
35 | end
36 |
37 | vertical do
38 | scroll_window(width: 300, height: 150) do
39 | %w[One Two Three Four Five Six].each do |name|
40 | toggle_button(name)
41 | end
42 | end
43 | end
44 | end
45 | end
46 | end
47 | end
48 |
49 | ExampleWindow.new.show
--------------------------------------------------------------------------------
/lib/fidgit/elements/group.rb:
--------------------------------------------------------------------------------
1 | module Fidgit
2 | class Group < Packer
3 | attr_reader :selected
4 |
5 | event :changed
6 |
7 | def value; @selected ? @selected.value : nil; end
8 |
9 | # @example
10 | # group do
11 | # horizontal do
12 | # radio_button 1, text: '1', checked: true
13 | # radio_button 2, text: '2'
14 | # subscribe :changed do |sender, value|
15 | # puts value
16 | # end
17 | # end
18 | # end
19 | #
20 | # @param (see Packer#initialize)
21 | #
22 | # @option (see Packer#initialize)
23 | def initialize(options = {}, &block)
24 | super(options)
25 |
26 | @selected = nil
27 | @buttons = []
28 | end
29 |
30 | def add_button(button)
31 | @buttons.push button
32 | self.value = button.value if button.checked?
33 | nil
34 | end
35 |
36 | def remove_button(button)
37 | self.value = nil if button == @selected
38 | @buttons.delete button
39 | nil
40 | end
41 |
42 | # @example
43 | # @my_group = group do
44 | # horizontal do
45 | # radio_button(1, text: '1', checked: true)
46 | # radio_button(2, text: '2')
47 | # end
48 | # end
49 | #
50 | # # later
51 | # @my_group.value = 2
52 | def value=(value)
53 | if value != self.value
54 | button = @buttons.find { |b| b.value == value }
55 | @selected.uncheck if @selected and @selected.checked?
56 | @selected = button
57 | @selected.check if @selected and not @selected.checked?
58 | publish :changed, self.value
59 | end
60 |
61 | value
62 | end
63 | end
64 | end
65 |
--------------------------------------------------------------------------------
/lib/fidgit/elements/color_picker.rb:
--------------------------------------------------------------------------------
1 | module Fidgit
2 | class ColorPicker < Composite
3 | CHANNELS = [:red, :green, :blue]
4 | DEFAULT_CHANNEL_NAMES = CHANNELS.map {|c| c.to_s.capitalize }
5 |
6 | INDICATOR_HEIGHT = 25
7 |
8 | event :changed
9 |
10 | def color; @color.dup; end
11 |
12 | def color=(value)
13 | @color = value.dup
14 | CHANNELS.each do |channel|
15 | @sliders[channel].value = @color.send channel
16 | end
17 |
18 | publish :changed, @color.dup
19 |
20 | value
21 | end
22 |
23 | # @param (see Composite#initialize)
24 | # @option (see Composite#initialize)
25 | def initialize(options = {}, &block)
26 | options = {
27 | padding: 0,
28 | spacing: 0,
29 | channel_names: DEFAULT_CHANNEL_NAMES,
30 | color: default(:color),
31 | indicator_height: default(:indicator_height),
32 | }.merge! options
33 |
34 | @color = options[:color].dup
35 | @indicator_height = options[:indicator_height]
36 |
37 | super(options)
38 |
39 | slider_width = width
40 | vertical do
41 | @sliders = {}
42 | CHANNELS.each_with_index do |channel, i|
43 | @sliders[channel] = slider(value: @color.send(channel), range: 0..255, width: slider_width,
44 | tip: options[:channel_names][i]) do |sender, value|
45 | @color.send "#{channel}=", value
46 | @indicator.background_color = @color
47 | publish :changed, @color.dup
48 | end
49 | end
50 |
51 | @indicator = label '', background_color: @color, width: slider_width, height: @indicator_height
52 | end
53 | end
54 |
55 | protected
56 | # Use block as an event handler.
57 | def post_init_block(&block)
58 | subscribe :changed, &block
59 | end
60 | end
61 | end
--------------------------------------------------------------------------------
/examples/align_example.rb:
--------------------------------------------------------------------------------
1 | require_relative 'helpers/example_window'
2 |
3 | # Change font and labels in the schema.
4 | Fidgit::Element.schema.merge_elements!(Element: { font_height: 24 }, Label: { background_color: "?dark_blue" })
5 |
6 | class ExampleState < Fidgit::GuiState
7 | ROW_BACKGROUND = Gosu::Color.rgb(0, 100, 0)
8 | CELL_BACKGROUND = Gosu::Color.rgb(100, 0, 0)
9 | OUTER_BACKGROUND = Gosu::Color.rgb(100, 0, 100)
10 |
11 | def initialize
12 | super
13 |
14 | vertical align: :center, background_color: OUTER_BACKGROUND do
15 | label "h => align_h, v => align_v", align_h: :center
16 |
17 | grid num_columns: 4, align: :center, cell_background_color: CELL_BACKGROUND, background_color: ROW_BACKGROUND do
18 | label "xxx"
19 | label "h fill", align_h: :fill
20 | label "h right", align_h: :right
21 | label "h center", align_h: :center
22 |
23 |
24 | vertical do
25 | label "xxx"
26 | label "xxx"
27 | end
28 | label "v fill", align_v: :fill
29 | label "v center", align_v: :center
30 | label "v bottom", align_v: :bottom
31 |
32 | vertical align_h: :center do
33 | label "h center"
34 | label "h center"
35 | end
36 | label "top right", align: [:top, :left]
37 | label "bottom left", align_h: :left, align_v: :bottom
38 | label "h/v fill", align: :fill
39 |
40 | label ""
41 | label "bottom right", align_h: :right, align_v: :bottom
42 | label "bottom center", align_h: :center, align_v: :bottom
43 | vertical align_h: :right do
44 | label "h right"
45 | label "h right"
46 | end
47 |
48 | label "Blah, bleh!"
49 | label "Yada, yada, yada"
50 | label "Bazingo by jingo!"
51 | end
52 | end
53 | end
54 | end
55 |
56 | ExampleWindow.new.show
--------------------------------------------------------------------------------
/lib/fidgit/elements/image_frame.rb:
--------------------------------------------------------------------------------
1 | module Fidgit
2 | # A wrapper around a Gosu::Image to show it in the GUI.
3 | class ImageFrame < Element
4 | ENABLED_COLOR = Gosu::Color::WHITE
5 | DISABLED_COLOR = Gosu::Color.rgb(150, 150, 150)
6 |
7 | attr_reader :image, :factor_x, :factor_y
8 |
9 | def thumbnail?; @thumbnail; end
10 |
11 | # @param (see Element#initialize)
12 | # @param [Gosu::Image] image Gosu image to display.
13 | #
14 | # @option (see Element#initialize)
15 | # @option options [Boolean] :thumbnail (false) Is the image expanded to be square?
16 | def initialize(image, options = {})
17 | options = {
18 | thumbnail: false,
19 | factor: 1,
20 | }.merge! options
21 |
22 | @thumbnail = options[:thumbnail]
23 | @factor_x = options[:factor_x] || options[:factor]
24 | @factor_y = options[:factor_y] || options[:factor]
25 |
26 | super(options)
27 |
28 | self.image = image
29 | end
30 |
31 | def image=(image)
32 | @image = image
33 |
34 | recalc
35 |
36 | image
37 | end
38 |
39 |
40 | def draw_foreground
41 | @image.draw(x + padding_left, y + padding_top, z, factor_x, factor_y, enabled? ? ENABLED_COLOR : DISABLED_COLOR) if @image
42 | end
43 |
44 | protected
45 | def layout
46 | if @image
47 | if @thumbnail
48 | size = [@image.width, @image.height].max
49 | rect.width = size * @factor_x
50 | rect.height = size * @factor_y
51 | else
52 | rect.width = @image.width * @factor_x
53 | rect.height = @image.height * @factor_y
54 | end
55 | else
56 | rect.width = rect.height = 0
57 | end
58 |
59 | rect.width += padding_left + padding_right
60 | rect.height += padding_top + padding_bottom
61 |
62 | nil
63 | end
64 | end
65 | end
--------------------------------------------------------------------------------
/lib/fidgit/states/dialog_state.rb:
--------------------------------------------------------------------------------
1 | module Fidgit
2 | # A modal dialog.
3 | # @abstract
4 | class DialogState < GuiState
5 | DEFAULT_BACKGROUND_COLOR = Gosu::Color.rgb(75, 75, 75)
6 | DEFAULT_BORDER_COLOR = Gosu::Color.rgb(255, 255, 255)
7 |
8 | DEFAULT_SHADOW_COLOR = Gosu::Color.rgba(0, 0, 0, 100)
9 | DEFAULT_SHADOW_OFFSET = 8
10 |
11 | def initialize(options = {})
12 | # @option options [Gosu::Color] :shadow_color (transparent black) Color of the shadow.
13 | # @option options [Gosu::Color] :shadow_offset (8) Distance shadow is offset to bottom and left.
14 | # @option options [Gosu::Color] :shadow_full (false) Shadow fills whole screen. Ignores :shadow_offset option if true.
15 | options = {
16 | shadow_color: DEFAULT_SHADOW_COLOR,
17 | shadow_offset: DEFAULT_SHADOW_OFFSET,
18 | shadow_full: false,
19 | }.merge! options
20 |
21 | @shadow_color = options[:shadow_color].dup
22 | @shadow_offset = options[:shadow_offset]
23 | @shadow_full = options[:shadow_full]
24 |
25 | super()
26 | end
27 |
28 | def draw
29 | $window.game_state_manager.previous_game_state.draw # Keep the underlying state being shown.
30 | $window.flush
31 |
32 | if @shadow_full
33 | draw_rect 0, 0, $window.width, $window.height, -Float::INFINITY, @shadow_color
34 | elsif @shadow_offset > 0
35 | dialog = container[0]
36 | draw_rect dialog.x + @shadow_offset, dialog.y + @shadow_offset, dialog.width, dialog.height, -Float::INFINITY, @shadow_color
37 | end
38 |
39 | super
40 | end
41 |
42 | def show
43 | $window.game_state_manager.push(self, finalize: false) unless $window.game_state_manager.game_states.include? self
44 | nil
45 | end
46 |
47 | def hide
48 | $window.game_state_manager.pop(setup: false) if $window.game_state_manager.current == self
49 | nil
50 | end
51 | end
52 | end
--------------------------------------------------------------------------------
/lib/fidgit/elements/toggle_button.rb:
--------------------------------------------------------------------------------
1 | module Fidgit
2 | # A button that toggles its value from false<->true when clicked.
3 | class ToggleButton < Button
4 | event :changed
5 |
6 | attr_reader :value
7 | def value=(value); @value = value; update_status; end
8 |
9 | # @param (see Button#initialize)
10 | #
11 | # @option (see Button#initialize)
12 | def initialize(text, options = {}, &block)
13 | options = {
14 | value: false
15 | }.merge! options
16 |
17 | @value = options[:value]
18 |
19 | super(text, options)
20 |
21 | @text_on = (options[:text_on] || text).dup
22 | @icon_on = options[:icon_on] || icon
23 | @tip_on = (options[:tip_on] || tip).dup
24 | @border_color_on = (options[:border_color_on] || options[:border_color] || default(:toggled, :border_color)).dup
25 |
26 | @text_off = (options[:text_off] || text).dup
27 | @icon_off = options[:icon_off] || icon
28 | @tip_off = (options[:tip_off] || tip).dup
29 | @border_color_off = (options[:border_color_off] || options[:border_color] || default(:border_color)).dup
30 |
31 | update_status
32 |
33 | subscribe :clicked_left_mouse_button do |sender, x, y|
34 | @value = (not @value)
35 | update_status
36 | publish :changed, @value
37 | end
38 | end
39 |
40 | protected
41 | # The block for a toggle-button is connected to :changed event.
42 | def post_init_block(&block)
43 | subscribe :changed, &block
44 | end
45 |
46 | protected
47 | def update_status
48 | if @value
49 | self.text = @text_on.dup
50 | @icon = @icon_on ? @icon_on.dup : nil
51 | @tip = @tip_on.dup
52 | @border_color = @border_color_on.dup
53 | else
54 | self.text = @text_off.dup
55 | @icon = @icon_off ? @icon_off.dup : nil
56 | @tip = @tip_off.dup
57 | @border_color = @border_color_off.dup
58 | end
59 |
60 | recalc
61 |
62 | nil
63 | end
64 | end
65 | end
--------------------------------------------------------------------------------
/lib/fidgit.rb:
--------------------------------------------------------------------------------
1 | require 'chingu'
2 | require 'clipboard'
3 |
4 | require_relative "fidgit/cursor"
5 | require_relative "fidgit/event"
6 | require_relative "fidgit/history"
7 | require_relative "fidgit/redirector"
8 | require_relative "fidgit/schema"
9 | require_relative "fidgit/selection"
10 | require_relative "fidgit/version"
11 | require_relative "fidgit/window"
12 |
13 | require_relative "fidgit/chingu_ext/window"
14 | require_relative "fidgit/gosu_ext/color"
15 | require_relative "fidgit/gosu_ext/image"
16 | require_relative "fidgit/standard_ext/hash"
17 |
18 | require_relative "fidgit/elements/element"
19 | require_relative "fidgit/elements/container"
20 | require_relative "fidgit/elements/packer"
21 | require_relative "fidgit/elements/composite"
22 | require_relative "fidgit/elements/grid"
23 | require_relative "fidgit/elements/group"
24 | require_relative "fidgit/elements/label"
25 | require_relative "fidgit/elements/button"
26 | require_relative "fidgit/elements/radio_button"
27 |
28 | require_relative "fidgit/elements/color_picker"
29 | require_relative "fidgit/elements/color_well"
30 | require_relative "fidgit/elements/combo_box"
31 | require_relative "fidgit/elements/file_browser"
32 | require_relative "fidgit/elements/horizontal"
33 | require_relative "fidgit/elements/image_frame"
34 | require_relative "fidgit/elements/list"
35 | require_relative "fidgit/elements/main_packer"
36 | require_relative "fidgit/elements/menu_pane"
37 | require_relative "fidgit/elements/scroll_area"
38 | require_relative "fidgit/elements/scroll_bar"
39 | require_relative "fidgit/elements/scroll_window"
40 | require_relative "fidgit/elements/slider"
41 | require_relative "fidgit/elements/text_area"
42 | require_relative "fidgit/elements/text_line"
43 | require_relative "fidgit/elements/toggle_button"
44 | require_relative "fidgit/elements/tool_tip"
45 | require_relative "fidgit/elements/vertical"
46 |
47 | require_relative "fidgit/states/gui_state"
48 | require_relative "fidgit/states/dialog_state"
49 | require_relative "fidgit/states/file_dialog"
50 | require_relative "fidgit/states/message_dialog"
--------------------------------------------------------------------------------
/lib/fidgit/elements/scroll_area.rb:
--------------------------------------------------------------------------------
1 | module Fidgit
2 | # A basic scrolling area. It is not managed in any way (use ScrollWindow for that).
3 | class ScrollArea < Container
4 | # @return [Vertical] The content shown within this ScrollArea
5 | attr_reader :content
6 |
7 | def offset_x; x - @content.x; end
8 | def offset_y; y - @content.y; end
9 |
10 | def offset_x=(value)
11 | @content.x = x - [[@content.width - width, value].min, 0].max
12 | end
13 |
14 | def offset_y=(value)
15 | @content.y = y - [[@content.height - height, value].min, 0].max
16 | end
17 |
18 | # @option options [Number] :offset (0)
19 | # @option options [Number] :offset_x (value of :offset option)
20 | # @option options [Number] :offset_y (value of :offset option)
21 | # @option options [Element] :owner The owner of the content, such as the scroll-window containing the content.
22 | def initialize(options = {})
23 | options = {
24 | offset: 0,
25 | owner: nil,
26 | }.merge! options
27 |
28 | @owner = options[:owner]
29 |
30 | super(options)
31 |
32 | @content = Vertical.new(parent: self, padding: 0)
33 |
34 | self.offset_x = options[:offset_x] || options[:offset]
35 | self.offset_y = options[:offset_y] || options[:offset]
36 | end
37 |
38 | def hit_element(x, y)
39 | # Only pass on mouse events if they are inside the window.
40 | if hit?(x, y)
41 | @content.hit_element(x, y) || self
42 | else
43 | nil
44 | end
45 | end
46 |
47 | def recalc
48 | super
49 | # Always recalc our owner if our content resizes, even though our size can't change even if the content changes
50 | # (may encourage ScrollWindow to show/hide scroll-bars, for example)
51 | @owner.recalc if @owner
52 | end
53 |
54 | protected
55 | def draw_foreground
56 | $window.clip_to(*rect) do
57 | @content.draw
58 | end
59 | end
60 |
61 | protected
62 | def post_init_block(&block)
63 | with(&block)
64 | end
65 | end
66 | end
--------------------------------------------------------------------------------
/spec/fidgit/elements/image_frame_spec.rb:
--------------------------------------------------------------------------------
1 | require_relative 'helpers/helper'
2 |
3 | require 'fidgit'
4 |
5 | def check_thumbnail_is_square
6 | {square: [10, 10], tall: [5, 12], wide: [6, 5]}.each_pair do |type, dimensions|
7 | context "with a #{type} image" do
8 | it "should be square and just large enough to contain the image" do
9 | subject = described_class.new(Gosu::Image.create(*dimensions), thumbnail: true)
10 | subject.width.should equal dimensions.max
11 | subject.height.should equal dimensions.max
12 | end
13 | end
14 | end
15 | end
16 |
17 | def check_image_dimensions
18 | {square: [10, 10], tall: [5, 12], wide: [6, 5]}.each_pair do |type, dimensions|
19 | context "with a #{type} image" do
20 | it "should be just large enough to contain the image" do
21 | subject = described_class.new(Gosu::Image.create(*dimensions))
22 | subject.width.should equal dimensions.first
23 | subject.height.should equal dimensions.last
24 | end
25 | end
26 | end
27 | end
28 |
29 | module Fidgit
30 | describe ImageFrame do
31 | before :all do
32 | $window = Chingu::Window.new(100, 100, false) unless $window
33 | end
34 |
35 | before :each do
36 | @image = Gosu::Image.create(10, 10)
37 | end
38 |
39 | subject { described_class.new(@image) }
40 |
41 | describe '#image' do
42 | it "should have the image set" do
43 | subject.image.should be @image
44 | end
45 | end
46 |
47 | describe '#width and #height' do
48 | check_image_dimensions
49 | end
50 |
51 | describe "Thumbnails" do
52 | subject { described_class.new(@image, thumbnail: true) }
53 |
54 | describe '#image=' do
55 | it "should update the height and width" do
56 | image = Gosu::Image.create(8, 2)
57 | subject.image = image
58 | subject.height.should equal 8
59 | subject.width.should equal 8
60 | end
61 | end
62 |
63 | describe '#width and #height' do
64 | check_thumbnail_is_square
65 | end
66 | end
67 | end
68 | end
--------------------------------------------------------------------------------
/lib/fidgit/elements/radio_button.rb:
--------------------------------------------------------------------------------
1 | module Fidgit
2 | class RadioButton < Button
3 | attr_reader :group, :value
4 |
5 | event :changed
6 |
7 | def checked?; @checked; end
8 |
9 | # @param (see Button#initialize)
10 | # @param [Object] value
11 | #
12 | # @option (see Button#initialize)
13 | # @option options [Boolean] :checked
14 | def initialize(text, value, options = {}, &block)
15 | options = {
16 | checked: false,
17 | checked_border_color: default(:checked, :border_color),
18 | }.merge! options
19 |
20 | @checked = options[:checked]
21 | @value = value
22 |
23 | super(text, options)
24 |
25 | @checked_border_color = options[:checked_border_color].dup
26 | @unchecked_border_color = border_color
27 | add_to_group
28 |
29 | @border_color = (checked? ? @checked_border_color : @unchecked_border_color).dup
30 | end
31 |
32 | def clicked_left_mouse_button(sender, x, y)
33 | super
34 | check
35 | nil
36 | end
37 |
38 | # Check the button and update its group. This may uncheck another button in the group if one is selected.
39 | def check
40 | return if checked?
41 |
42 | @checked = true
43 | @group.value = value
44 | @border_color = @checked_border_color.dup
45 | publish :changed, @checked
46 |
47 | nil
48 | end
49 |
50 | # Uncheck the button and update its group.
51 | def uncheck
52 | return unless checked?
53 |
54 | @checked = false
55 | @group.value = value
56 | @border_color = @unchecked_border_color.dup
57 | publish :changed, @checked
58 |
59 | nil
60 | end
61 |
62 | protected
63 | def parent=(parent)
64 | @group.remove_button self if @parent
65 | super(parent)
66 | add_to_group if parent
67 | parent
68 | end
69 |
70 | protected
71 | def add_to_group
72 | container = parent
73 | while container and not container.is_a? Group
74 | container = container.parent
75 | end
76 |
77 | raise "#{self.class.name} must be placed inside a group element" unless container
78 |
79 | @group = container
80 | @group.add_button self
81 | nil
82 | end
83 | end
84 | end
--------------------------------------------------------------------------------
/lib/fidgit/states/message_dialog.rb:
--------------------------------------------------------------------------------
1 | module Fidgit
2 | # A simple dialog that manages a message with a set of buttons beneath it.
3 | class MessageDialog < DialogState
4 | VALID_TYPES = [:ok, :ok_cancel, :yes_no, :yes_no_cancel, :quit_cancel, :quit_save_cancel]
5 |
6 | attr_reader :type
7 |
8 | # @param [String] message
9 | #
10 | # @option options [Symbol] :type (:ok) One from :ok, :ok_cancel, :yes_no, :yes_no_cancel, :quit_cancel or :quit_save_cancel
11 | # @option options [String] :ok_text ("OK")
12 | # @option options [String] :yes_text ("Yes")
13 | # @option options [String] :no_text ("No")
14 | # @option options [String] :cancel_text ("Cancel")
15 | # @option options [String] :save_text ("Save")
16 | # @option options [String] :quit_text ("Quit")
17 | # @option options [Boolean] :show (true) Whether to show the message immediately (otherwise need to use #show later).
18 | #
19 | # @yield when the dialog is closed.
20 | # @yieldparam [Symbol] result :ok, :yes, :no, :quit, :save or :cancel, depending on the button pressed.
21 | def initialize(message, options = {}, &block)
22 | options = {
23 | type: :ok,
24 | ok_text: "OK",
25 | yes_text: "Yes",
26 | no_text: "No",
27 | quit_text: "Quit",
28 | save_text: "Save",
29 | cancel_text: "Cancel",
30 | show: true,
31 | background_color: DEFAULT_BACKGROUND_COLOR,
32 | border_color: DEFAULT_BORDER_COLOR,
33 | width: $window.width / 2
34 | }.merge! options
35 |
36 | @type = options[:type]
37 | raise ArgumentError, ":type must be one of #{VALID_TYPES}, not #{@type}" unless VALID_TYPES.include? @type
38 |
39 | super(options)
40 |
41 | # Dialog is forced to the centre.
42 | options[:align_h] = options[:align_v] = :center
43 |
44 | vertical options do
45 | text_area(text: message, enabled: false, width: options[:width] - padding_left - padding_right)
46 |
47 | horizontal align_h: :center do
48 | @type.to_s.split('_').each do |type|
49 | button(options[:"#{type}_text"]) do
50 | hide
51 | block.call type.to_sym if block
52 | end
53 | end
54 | end
55 | end
56 |
57 | show if options[:show]
58 | end
59 | end
60 | end
--------------------------------------------------------------------------------
/spec/fidgit/schema_spec.rb:
--------------------------------------------------------------------------------
1 | require_relative 'helpers/helper'
2 |
3 | require 'fidgit'
4 |
5 | SCHEMA_FILE_NAME = File.expand_path(File.join(__FILE__, '..', 'schema_test.yml'))
6 |
7 | include Fidgit
8 |
9 | describe Schema do
10 | subject { Schema.new(Hash.new) }
11 |
12 | context "given the default schema" do
13 | subject { Schema.new(YAML.load(File.read(SCHEMA_FILE_NAME))) }
14 |
15 | describe "#constant" do
16 | it "should have the constant :scroll_bar_thickness" do
17 | subject.constant(:scroll_bar_thickness).should equal 12
18 | end
19 |
20 | it "should have the color constant :none" do
21 | subject.constant(:none).should eq Gosu::Color.rgba(0, 0, 0, 0)
22 | end
23 |
24 | it "should have the color constant :white" do
25 | subject.constant(:white).should eq Gosu::Color.rgb(255, 255, 255)
26 | end
27 |
28 | it "should not have the constant :moon_cow_height" do
29 | subject.constant(:moon_cow_height).should be_nil
30 | end
31 | end
32 |
33 | describe "#default" do
34 | it "should fail if the class given is not an Element" do
35 | ->{ subject.default(String, :frog) }.should raise_error ArgumentError
36 | end
37 |
38 | it "should fail if the value is not ever defined" do
39 | ->{ subject.default(Element, :knee_walking_turkey) }.should raise_error
40 | end
41 |
42 | it "should give the correct value for a defined color" do
43 | subject.default(Element, :color).should eq Gosu::Color.rgb(255, 255, 255)
44 | end
45 |
46 | it "should give the symbol name if the value is one" do
47 | subject.default(Element, :align_h).should be :left
48 | end
49 |
50 | it "should give the correct value for a defined constant" do
51 | subject.default(VerticalScrollBar, :width).should equal 12
52 | end
53 |
54 | it "should give the correct value for a color defined in an ancestor class" do
55 | subject.default(Label, :color).should eq Gosu::Color.rgb(255, 255, 255)
56 | end
57 |
58 | it "should give the correct value for a defined nested value" do
59 | subject.default(Fidgit::Button, [:disabled, :background_color]).should eq Gosu::Color.rgb(50, 50, 50)
60 | end
61 |
62 | it "should give the outer value for an undefined nested value" do
63 | subject.default(Element, [:hover, :color]).should eq Gosu::Color.rgb(255, 255, 255)
64 | end
65 | end
66 | end
67 | end
--------------------------------------------------------------------------------
/examples/message_dialog_example.rb:
--------------------------------------------------------------------------------
1 | require_relative 'helpers/example_window'
2 |
3 | # Example for Button and ToggleButton
4 | class ExampleState < Fidgit::GuiState
5 | def initialize
6 | super
7 |
8 | vertical do
9 | my_label = label "Why not open a dialog? You know you want to!", tip: "I'm a label"
10 |
11 | button("Open an ok message dialog") do
12 | message "System shutdown immanent"
13 | end
14 |
15 | button("Open an ok/cancel message dialog") do
16 | message("Really 'rm -rf .'?", type: :ok_cancel) do |result|
17 | my_label.text = case result
18 | when :ok then "All your base are belong to us!"
19 | when :cancel then "Cancelled"
20 | end
21 | end
22 | end
23 |
24 | button("Open a yes/no message dialog") do
25 | message("Do you like any sorts of cheese? Even Government cheese counts, you know!", type: :yes_no, yes_text: "Yay!", no_text: "Nay!") do |result|
26 | my_label.text = case result
27 | when :yes then "You like cheese"
28 | when :no then "You don't like cheese"
29 | end
30 | end
31 | end
32 |
33 | button("Open a yes/no/cancel message dialog") do
34 | message("Do you know what you are doing?", type: :yes_no_cancel) do |result|
35 | my_label.text = case result
36 | when :yes then "I'm not convinced you know what you are doing"
37 | when :no then "At least you are aware of your own shortcomings"
38 | when :cancel then "Don't avoid the question!"
39 | end
40 | end
41 | end
42 |
43 | button("Open quit/cancel message dialog") do
44 | message("Really leave us?", type: :quit_cancel) do |result|
45 | my_label.text = case result
46 | when :quit then "Quit! Bye!"
47 | when :cancel then "Oh, you are staying? Yay!"
48 | end
49 | end
50 | end
51 |
52 | button("Open quit/save/cancel message dialog") do
53 | message("You have unsaved data.\nSave before quitting?", type: :quit_save_cancel) do |result|
54 | my_label.text = case result
55 | when :quit then "File discarded and quit"
56 | when :save then "File saved and quit"
57 | when :cancel then "Nothing happened. You should be more committed"
58 | end
59 | end
60 | end
61 | end
62 | end
63 | end
64 |
65 | ExampleWindow.new.show
--------------------------------------------------------------------------------
/spec/fidgit/redirector_spec.rb:
--------------------------------------------------------------------------------
1 | require_relative 'helpers/helper'
2 |
3 | require 'redirector'
4 |
5 | include Fidgit
6 |
7 | describe RedirectorMethods do
8 | describe "::Object" do
9 | subject { Object.new }
10 |
11 | describe '#instance_methods_eval' do
12 | it "should fail if a block is not provided" do
13 | ->{ subject.instance_methods_eval }.should raise_error
14 | end
15 |
16 | it "should yield the target" do
17 | subject.instance_methods_eval { |target| @frog = target }
18 | @frog.should equal subject
19 | end
20 |
21 | it "should allow ivars to be read from the calling context" do
22 | @frog = 5
23 | fish = 0
24 | subject.instance_methods_eval { fish = @frog }
25 | fish.should equal 5
26 | end
27 |
28 | it "should allow ivars to be written to the calling context" do
29 | subject.instance_methods_eval { @frog = 10 }
30 | @frog.should equal 10
31 | end
32 |
33 | it "should not allow access to ivars in the subject" do
34 | subject.instance_variable_set :@frog, 5
35 | fish = 0
36 | subject.instance_methods_eval { fish = @frog }
37 | fish.should be_nil
38 | end
39 |
40 | it "should allow access to methods in the subject" do
41 | subject.should_receive(:frog)
42 | subject.instance_methods_eval { frog }
43 | end
44 |
45 | it "should allow stacked calls" do
46 | object1 = Object.new
47 | should_not_receive :frog
48 | object1.should_not_receive :frog
49 | subject.should_receive :frog
50 |
51 | object1.instance_methods_eval do
52 | subject.instance_methods_eval do
53 | frog
54 | end
55 | end
56 | end
57 |
58 | it "should fail if method does not exist on the subject or context" do
59 | ->{ subject.instance_methods_eval { frog } }.should raise_error NameError
60 | end
61 |
62 | it "should call the method on the context, if it doesn't exist on the subject" do
63 | should_receive(:frog)
64 | subject.instance_methods_eval { frog }
65 | end
66 |
67 | [:public, :protected, :private].each do |access|
68 | it "should preserve #{access} access for methods redirected on the context" do
69 | class << self; def frog; end; end
70 | (class << self; self; end).send access, :frog
71 | subject.should_receive :frog
72 | subject.instance_methods_eval { frog }
73 | (send "#{access}_methods").should include :frog
74 | end
75 | end
76 | end
77 | end
78 | end
--------------------------------------------------------------------------------
/lib/fidgit/selection.rb:
--------------------------------------------------------------------------------
1 | module Fidgit
2 | class Selection
3 | MIN_DRAG_DISTANCE = 2
4 |
5 | def size; @items.size; end
6 | def empty?; @items.empty?; end
7 | def [](index); @items[index]; end
8 | def each(&block); @items.each(&block); end
9 | def to_a; @items.dup; end
10 | def include?(object); @items.include? object; end
11 |
12 | # Current being dragged?
13 | def dragging?; @dragging; end
14 |
15 | # Actually moved during a dragging operation?
16 | def moved?; @moved; end
17 |
18 | def initialize
19 | @items = []
20 | @moved = false
21 | @dragging = false
22 | end
23 |
24 | def add(object)
25 | object.selected = true
26 | @items.push(object)
27 |
28 | self
29 | end
30 |
31 | def remove(object)
32 | @items.delete(object)
33 | object.selected = false
34 | object.dragging = false
35 |
36 | self
37 | end
38 |
39 | def clear
40 | end_drag if dragging?
41 | @items.each { |o| o.selected = false; o.dragging = false }
42 | @items.clear
43 |
44 | self
45 | end
46 |
47 | def begin_drag(x, y)
48 | @initial_x, @initial_y = x, y
49 | @last_x, @last_y = x, y
50 | @dragging = true
51 | @moved = false
52 |
53 | self
54 | end
55 |
56 | def end_drag
57 | @items.each do |object|
58 | object.x, object.y = object.x.round, object.y.round
59 | object.dragging = false
60 | end
61 | @dragging = false
62 | @moved = false
63 |
64 | self
65 | end
66 |
67 | # Move all dragged object back to original positions.
68 | def reset_drag
69 | if moved?
70 | @items.each do |o|
71 | o.x += @initial_x - @last_x
72 | o.y += @initial_y - @last_y
73 | end
74 | end
75 |
76 | self.end_drag
77 |
78 | self
79 | end
80 |
81 | def update_drag(x, y)
82 | x, y = x.round, y.round
83 |
84 | # If the mouse has been dragged far enough from the initial click position, then 'pick up' the objects and drag.
85 | unless moved?
86 | if distance(@initial_x, @initial_y, x, y) > MIN_DRAG_DISTANCE
87 | @items.each { |o| o.dragging = true }
88 | @moved = true
89 | end
90 | end
91 |
92 | if moved?
93 | @items.each do |o|
94 | o.x += x - @last_x
95 | o.y += y - @last_y
96 | end
97 |
98 | @last_x, @last_y = x, y
99 | end
100 |
101 | self
102 | end
103 | end
104 | end
--------------------------------------------------------------------------------
/lib/fidgit/elements/text_line.rb:
--------------------------------------------------------------------------------
1 | module Fidgit
2 | # Used internally by the label.
3 | class TextLine < Element
4 | VALID_JUSTIFICATION = [:left, :right, :center]
5 |
6 | attr_reader :color, :justify
7 |
8 | def color=(color)
9 | raise ArgumentError.new("Text must be a Gosu::Color") unless color.is_a? Gosu::Color
10 |
11 | @color = color.dup
12 |
13 | color
14 | end
15 |
16 | def text; @text.dup; end
17 |
18 | def text=(text)
19 | raise ArgumentError.new("Text must be a String") unless text.respond_to? :to_s
20 |
21 | @text = text.to_s.dup
22 |
23 | recalc
24 | text
25 | end
26 |
27 | def justify=(justify)
28 | raise ArgumentError.new("Justify must be one of #{VALID_JUSTIFICATION.inspect}") unless VALID_JUSTIFICATION.include? justify
29 | @justify = justify
30 | end
31 |
32 | # @param (see Element#initialize)
33 | # @param [String] text The string to display in the line of text.
34 | #
35 | # @option (see Element#initialize)
36 | # @option options [:left, :right, :center] :justify (:left) Text justification.
37 | def initialize(text, options = {})
38 | options = {
39 | color: default(:color),
40 | justify: default(:justify),
41 | }.merge! options
42 |
43 | super(options)
44 |
45 | self.justify = options[:justify]
46 | self.color = options[:color]
47 | self.text = text
48 | end
49 |
50 | def draw_foreground
51 | case @justify
52 | when :left
53 | rel_x = 0.0
54 | draw_x = x + padding_left
55 |
56 | when :right
57 | rel_x = 1.0
58 | draw_x = x + rect.width - padding_right
59 |
60 | when :center
61 | rel_x = 0.5
62 | draw_x = (x + padding_left) + (rect.width - padding_right - padding_left) / 2.0
63 | end
64 |
65 | font.draw_rel(@text, draw_x, y + padding_top, z, rel_x, 0, 1, 1, color)
66 | end
67 |
68 | def min_width
69 | if @text.empty?
70 | [padding_left + padding_right, super].max
71 | else
72 | [padding_left + font.text_width(@text) + padding_right, super].max
73 | end
74 | end
75 |
76 | protected
77 | def layout
78 | rect.width = [min_width, max_width].min
79 |
80 | if @text.empty?
81 | rect.height = [[padding_top + padding_bottom, min_height].max, max_height].min
82 | else
83 | rect.height = [[padding_top + font.height + padding_bottom, min_height].max, max_height].min
84 | end
85 | end
86 |
87 | public
88 | def to_s
89 | "#{super} '#{@text}'"
90 | end
91 | end
92 | end
--------------------------------------------------------------------------------
/lib/fidgit/elements/scroll_window.rb:
--------------------------------------------------------------------------------
1 | module Fidgit
2 | class ScrollWindow < Composite
3 | def content; @view.content; end
4 | def offset_x; @view.offset_x; end
5 | def offset_x=(value); @view.offset_x = value; end
6 | def offset_y; @view.offset_y; end
7 | def offset_y=(value); @view.offset_y = value; end
8 |
9 | def view_width; @view.width; end
10 | def view_height; @view.height; end
11 | def content_width; @view.content.width; end
12 | def content_height; @view.content.height; end
13 | def width=(value); super(value); end
14 | def height=(value); super(value); end
15 |
16 | def initialize(options = {})
17 | options = {
18 | scroll_bar_thickness: default(:scroll_bar_thickness),
19 | }.merge! options
20 |
21 | super(options)
22 |
23 | @grid = grid num_columns: 2, padding: 0, spacing: 0 do
24 | @view = scroll_area(owner: self, width: options[:width], height: options[:height])
25 | @spacer = label '', padding: 0, width: 0, height: 0
26 | end
27 |
28 | @scroll_bar_v = VerticalScrollBar.new(owner: self, width: options[:scroll_bar_thickness], align_v: :fill)
29 | @scroll_bar_h = HorizontalScrollBar.new(owner: self, height: options[:scroll_bar_thickness], align_h: :fill)
30 | end
31 |
32 | protected
33 | def layout
34 | # Prevent recursive layouts.
35 | return if @in_layout
36 |
37 | @in_layout = true
38 |
39 | if @view
40 | # Constrain the values of the offsets.
41 | @view.offset_x = @view.offset_x
42 | @view.offset_y = @view.offset_y
43 |
44 | if content_height > view_height
45 | unless @scroll_bar_v.parent
46 | @view.send(:rect).width -= @scroll_bar_v.width
47 | @grid.remove @spacer
48 | @grid.insert 1, @scroll_bar_v
49 | end
50 | else
51 | if @scroll_bar_v.parent
52 | @view.send(:rect).width += @scroll_bar_v.width
53 | @grid.remove @scroll_bar_v
54 | @grid.insert 1, @spacer
55 | end
56 | end
57 |
58 | if content_width > view_width
59 | unless @scroll_bar_h.parent
60 | @view.send(:rect).height -= @scroll_bar_h.height
61 | @grid.add @scroll_bar_h
62 | end
63 | else
64 | if @scroll_bar_h.parent
65 | @view.send(:rect).height += @scroll_bar_h.height
66 | @grid.remove @scroll_bar_h
67 | end
68 | end
69 | end
70 |
71 | super
72 |
73 | @in_layout = false
74 | end
75 |
76 | protected
77 | def post_init_block(&block)
78 | @view.content.with(&block)
79 | end
80 | end
81 | end
--------------------------------------------------------------------------------
/lib/fidgit/elements/combo_box.rb:
--------------------------------------------------------------------------------
1 | require 'forwardable'
2 |
3 | module Fidgit
4 | class ComboBox < Button
5 | extend Forwardable
6 |
7 | ARROW_IMAGE = "combo_arrow.png"
8 |
9 | def_delegators :@menu, :each
10 |
11 | event :changed
12 |
13 | def index; @menu.index(@value) end
14 | def value; @value; end
15 |
16 | def value=(value)
17 | if @value != value
18 | @value = value
19 | item = @menu.find(@value)
20 | self.text = item.text
21 | self.icon = item.icon
22 | publish :changed, @value
23 | end
24 |
25 | value
26 | end
27 |
28 | def index=(index)
29 | if index.between?(0, @menu.size - 1)
30 | self.value = @menu[index].value
31 | end
32 |
33 | index
34 | end
35 |
36 | # @param (see Button#initialize)
37 | # @option (see Button#initialize)
38 | # @option options [] :value
39 | def initialize(options = {}, &block)
40 | options = {
41 | background_color: default(:background_color),
42 | border_color: default(:border_color),
43 | }.merge! options
44 |
45 | @value = options[:value]
46 |
47 | @hover_index = 0
48 |
49 | @menu = MenuPane.new(show: false) do
50 | subscribe :selected do |widget, value|
51 | self.value = value
52 | end
53 | end
54 |
55 | @@arrow ||= Gosu::Image[ARROW_IMAGE]
56 |
57 | super('', options)
58 |
59 | rect.height = [height, font.height + padding_top + padding_bottom].max
60 | rect.width = [width, font.height * 4 + padding_left + padding_right].max
61 | end
62 |
63 | def item(text, value, options = {}, &block)
64 | item = @menu.item(text, value, options, &block)
65 |
66 | # Force text to be updated if the item added has the same value.
67 | if item.value == @value
68 | self.text = item.text
69 | self.icon = item.icon
70 | end
71 |
72 | recalc
73 |
74 | item
75 | end
76 |
77 | def draw
78 | super
79 | size = height / @@arrow.width.to_f
80 | @@arrow.draw x + width - height, y, z, size, size
81 | end
82 |
83 | def clicked_left_mouse_button(sender, x, y)
84 | @menu.x = self.x
85 | @menu.y = self.y + height + border_thickness
86 | $window.game_state_manager.current_game_state.show_menu @menu
87 |
88 | nil
89 | end
90 |
91 | def clear
92 | self.text = ""
93 | self.icon = nil
94 | @menu.clear
95 | end
96 |
97 | protected
98 | def layout
99 | super
100 |
101 | # Max width of all items + allow size for the arrow.
102 | rect.width = [@menu.width + height, min_width].max
103 |
104 | nil
105 | end
106 |
107 |
108 | protected
109 | # Any combo-box passed a block will allow you access to its methods.
110 | def post_init_block(&block)
111 | with(&block)
112 | end
113 | end
114 | end
--------------------------------------------------------------------------------
/lib/fidgit/redirector.rb:
--------------------------------------------------------------------------------
1 | module Fidgit
2 | # Redirects methods to an object, but does not mask methods and ivars from the calling context.
3 | module RedirectorMethods
4 | # Evaluate a block accessing methods and ivars from the calling context, but calling public methods
5 | # (not ivars or non-public methods) on this object in preference.
6 | def instance_methods_eval(&block)
7 | raise ArgumentError, "block required" unless block_given?
8 |
9 | context = eval('self', block.binding)
10 |
11 | context.send :push_redirection_target, self
12 |
13 | begin
14 | yield context
15 | ensure
16 | context.send :pop_redirection_target
17 | end
18 |
19 | self
20 | end
21 |
22 | protected
23 | def push_redirection_target(target)
24 | meta_class = class << self; self; end
25 | base_methods = Object.public_instance_methods
26 |
27 | # Redirect just the public methods of the target, less those that are on Object.
28 | methods_to_redirect = target.public_methods - base_methods
29 |
30 | # Only hide those public/private/protected methods that are being redirected.
31 | methods_overridden = []
32 | [:public, :protected, :private].each do |access|
33 | methods_to_hide = meta_class.send("#{access}_instance_methods", false) & methods_to_redirect
34 | methods_to_hide.each do |meth|
35 | # Take a reference to the method we are about to override.
36 | methods_overridden.push [meth, method(meth), access]
37 | meta_class.send :remove_method, meth
38 | end
39 | end
40 |
41 | # Add a method, to redirect calls to the target.
42 | methods_to_redirect.each do |meth|
43 | meta_class.send :define_method, meth do |*args, &block|
44 | target.send meth, *args, &block
45 | end
46 | end
47 |
48 | redirection_stack.push [target, methods_overridden, methods_to_redirect]
49 |
50 | target
51 | end
52 |
53 | protected
54 | def pop_redirection_target
55 | meta_class = class << self; self; end
56 |
57 | target, methods_to_recreate, methods_to_remove = redirection_stack.pop
58 |
59 | # Remove the redirection methods
60 | methods_to_remove.reverse_each do |meth|
61 | meta_class.send :remove_method, meth
62 | end
63 |
64 | # Replace with the previous versions of the methods.
65 | methods_to_recreate.reverse_each do |meth, reference, access|
66 | meta_class.send :define_method, meth, reference
67 | meta_class.send access, meth unless access == :public
68 | end
69 |
70 | target
71 | end
72 |
73 | # Direct access to the redirection stack.
74 | private
75 | def redirection_stack
76 | @_redirection_stack ||= []
77 | end
78 | end
79 | end
80 |
81 | class Object
82 | include Fidgit::RedirectorMethods
83 | end
--------------------------------------------------------------------------------
/lib/fidgit/history.rb:
--------------------------------------------------------------------------------
1 | module Fidgit
2 | # Manages a history of actions, along with doing, undoing and redoing those actions.
3 | class History
4 | # Maximum number of actions in the History before Actions are deleted.
5 | DEFAULT_MAX_SIZE = 250
6 |
7 | # An action in the History. Inherit actions from this in order to add them to a History.
8 | class Action
9 | # Perform the action.
10 | def do; raise NotImplementedError, "#{self.class} does not have a do method defined"; end
11 |
12 | # Reverse the action.
13 | def undo; raise NotImplementedError, "#{self.class} does not have an undo method defined"; end
14 | end
15 |
16 | # Is there an action that can be undone?
17 | def can_undo?; @last_done >= 0; end
18 |
19 | # Is there an action that has been undone that can now be redone?
20 | def can_redo?; @last_done < (@actions.size - 1); end
21 |
22 | def initialize(max_size = DEFAULT_MAX_SIZE)
23 | @max_size = max_size
24 | @actions = []
25 | @last_done = -1 # Last command that was performed.
26 | end
27 |
28 | # Perform a History::Action, adding it to the history.
29 | # If there are currently any actions that have been undone, they will be permanently lost and cannot be redone.
30 | #
31 | # @param [History::Action] action Action to be performed
32 | def do(action)
33 | raise ArgumentError, "Parameter, 'action', expected to be a #{Action}, but received: #{action}" unless action.is_a? Action
34 |
35 | # Remove all undone actions when a new one is performed.
36 | if can_redo?
37 | if @last_done == -1
38 | @actions.clear
39 | else
40 | @actions = @actions[0..@last_done]
41 | end
42 | end
43 |
44 | # If history is too big, remove the oldest action.
45 | if @actions.size >= @max_size
46 | @actions.shift
47 | end
48 |
49 | @last_done = @actions.size
50 | @actions << action
51 | action.do
52 |
53 | nil
54 | end
55 |
56 | # Perform a History::Action, replacing the last action that was performed.
57 | #
58 | # @param [History::Action] action Action to be performed
59 | def replace_last(action)
60 | raise ArgumentError, "Parameter, 'action', expected to be a #{Action}, but received: #{action}" unless action.is_a? Action
61 |
62 | @actions[@last_done].undo
63 | @actions[@last_done] = action
64 | action.do
65 |
66 | nil
67 | end
68 |
69 | # Undo the last action that was performed.
70 | def undo
71 | raise "Can't undo unless there are commands in past" unless can_undo?
72 |
73 | @actions[@last_done].undo
74 | @last_done -= 1
75 |
76 | nil
77 | end
78 |
79 | # Redo the last action that was undone.
80 | def redo
81 | raise "Can't redo if there are no commands in the future" unless can_redo?
82 |
83 | @last_done += 1
84 | @actions[@last_done].do
85 |
86 | nil
87 | end
88 | end
89 | end
--------------------------------------------------------------------------------
/lib/fidgit/elements/label.rb:
--------------------------------------------------------------------------------
1 | module Fidgit
2 | class Label < Composite
3 | ICON_POSITIONS = [:top, :bottom, :left, :right]
4 |
5 | attr_reader :icon_position
6 |
7 | attr_accessor :background_color, :border_color
8 |
9 | def_delegators :@text, :text, :color, :font, :color=, :text=
10 |
11 | def icon; @icon ? @icon.image : nil; end
12 |
13 | def hit_element(x, y)
14 | # The sub-elements should never get events.
15 | hit?(x, y) ? self : nil
16 | end
17 |
18 | def icon=(icon)
19 | raise ArgumentError.new("Icon must be a Gosu::Image") unless icon.is_a? Gosu::Image or icon.nil?
20 |
21 | @contents.remove(@icon) if @icon.image
22 | @icon.image = icon
23 | position = [:left, :top].include?(icon_position) ? 0 : 1
24 | @contents.insert(position, @icon) if @icon.image
25 |
26 | icon
27 | end
28 |
29 | # Set the position of the icon, respective to the text.
30 | def icon_position=(position)
31 | raise ArgumentError.new("icon_position must be one of #{ICON_POSITIONS}") unless ICON_POSITIONS.include? position
32 |
33 | @icon_position = position
34 |
35 | case @icon_position
36 | when :top, :bottom
37 | @contents.instance_variable_set :@type, :fixed_columns
38 | @contents.instance_variable_set :@num_columns, 1
39 | when :left, :right
40 | @contents.instance_variable_set :@type, :fixed_rows
41 | @contents.instance_variable_set :@num_rows, 1
42 | end
43 |
44 | self.icon = @icon.image if @icon.image # Force the icon into the correct position.
45 |
46 | position
47 | end
48 |
49 | # @param (see Element#initialize)
50 | # @param [String] text The string to display in the label.
51 | #
52 | # @option (see Element#initialize)
53 | # @option options [Gosu::Image, nil] :icon (nil)
54 | # @option options [:left, :right, :center] :justify (:left) Text justification.
55 | def initialize(text, options = {})
56 | options = {
57 | color: default(:color),
58 | justify: default(:justify),
59 | background_color: default(:background_color),
60 | border_color: default(:border_color),
61 | icon_options: {},
62 | font_name: default(:font_name),
63 | font_height: default(:font_height),
64 | icon_position: default(:icon_position),
65 | }.merge! options
66 |
67 | super(options)
68 |
69 | # Bit of a fudge since font info is managed circularly here!
70 | # By using a grid, we'll be able to turn it around easily (in theory).
71 | @contents = grid num_rows: 1, padding: 0, spacing_h: spacing_h, spacing_v: spacing_v, width: options[:width], height: options[:height], z: z do |contents|
72 | @text = TextLine.new(text, parent: contents, justify: options[:justify], color: options[:color], padding: 0, z: z,
73 | font_name: options[:font_name], font_height: options[:font_height], align_h: :fill, align_v: :center)
74 | end
75 |
76 | # Create an image frame, but don't show it unless there is an image in it.
77 | @icon = ImageFrame.new(nil, options[:icon_options].merge(z: z, align: :center))
78 | @icon.image = options[:icon]
79 |
80 | self.icon_position = options[:icon_position]
81 | end
82 | end
83 | end
--------------------------------------------------------------------------------
/lib/fidgit/elements/button.rb:
--------------------------------------------------------------------------------
1 | module Fidgit
2 | class Button < Label
3 | # @param (see Label#initialize)
4 | # @option (see Label#initialize)
5 | # @option options [Symbol] :shortcut (nil) Adds a shortcut key for this element, that activates it. :auto takes the first letter of the text.
6 | def initialize(text, options = {}, &block)
7 | options = {
8 | color: default(:color),
9 | background_color: default(:background_color),
10 | border_color: default(:border_color),
11 | shortcut_color: default(:shortcut_color),
12 | shortcut: nil,
13 | }.merge! options
14 |
15 | @shortcut_color = options[:shortcut_color].dup
16 |
17 | @shortcut = if options[:shortcut] == :auto
18 | raise ArgumentError.new("Can't use :auto for :shortcut without text") if text.empty?
19 | text[0].downcase.to_sym
20 | else
21 | options[:shortcut]
22 | end
23 |
24 | raise ArgumentError.new(":shortcut must be a symbol") unless @shortcut.nil? or @shortcut.is_a? Symbol
25 |
26 | super(text, options)
27 |
28 | self.text = text # Force shortcut to be written out properly.
29 |
30 | update_colors
31 | end
32 |
33 | def text=(value)
34 | if @shortcut
35 | super value.sub(/#{Regexp.escape @shortcut}/i) {|char| "#{char}" }
36 | else
37 | super value
38 | end
39 | end
40 |
41 | def parent=(value)
42 | if @shortcut
43 | state = $window.game_state_manager.inside_state || $window.current_game_state
44 | if parent
45 | raise ArgumentError.new("Repeat of shortcut #{@shortcut.inspect}") if state.input.has_key? @shortcut
46 | state.on_input(@shortcut) { activate unless state.focus }
47 | else
48 | state.input.delete @shortcut
49 | end
50 | end
51 |
52 | super(value)
53 | end
54 |
55 | def clicked_left_mouse_button(sender, x, y)
56 | # TODO: Play click sound?
57 | nil
58 | end
59 |
60 | def enabled=(value)
61 | super(value)
62 | update_colors
63 |
64 | value
65 | end
66 |
67 | def enter(sender)
68 | @mouse_over = true
69 | update_colors
70 |
71 | nil
72 | end
73 |
74 | def leave(sender)
75 | @mouse_over = false
76 | update_colors
77 |
78 | nil
79 | end
80 |
81 | protected
82 | def update_colors
83 | [:color, :background_color].each do |attribute|
84 | send :"#{attribute.to_s}=", enabled? ? default(attribute) : default(:disabled, attribute)
85 | end
86 |
87 | self.background_color = if @mouse_over and enabled?
88 | default(:hover, :background_color)
89 | else
90 | default(:background_color)
91 | end
92 |
93 | @icon.enabled = enabled? if @icon
94 |
95 | nil
96 | end
97 |
98 | protected
99 | # A block added to any button subscribes to LMB click.
100 | def post_init_block(&block)
101 | subscribe :clicked_left_mouse_button, &block
102 | end
103 |
104 | public
105 | # Activate the button, as though it had been clicked on.
106 | # Does not do anything if the button is disabled.
107 | def activate
108 | publish(:clicked_left_mouse_button, x + width / 2, y + height / 2) if enabled?
109 | end
110 | end
111 | end
--------------------------------------------------------------------------------
/lib/fidgit/elements/slider.rb:
--------------------------------------------------------------------------------
1 | module Fidgit
2 | class Slider < Composite
3 | # @private
4 | class Handle < Element
5 | event :begin_drag
6 | event :end_drag
7 | event :update_drag
8 |
9 | def drag?(button); button == :left; end
10 |
11 | # @param (see Element#initialize)
12 | #
13 | # @option (see Element#initialize)
14 | def initialize(options = {}, &block)
15 | options = {
16 | background_color: default(:background_color),
17 | border_color: default(:border_color),
18 | }.merge! options
19 |
20 | super options
21 |
22 | subscribe :begin_drag do |sender, x, y|
23 | # Store position of the handle when it starts to drag.
24 | @drag_start_pos = [x - self.x, y - self.y]
25 | end
26 |
27 | subscribe :update_drag do |sender, x, y|
28 | if parent.enabled?
29 | parent.handle_dragged_to x - @drag_start_pos[0], y - @drag_start_pos[1]
30 | else
31 | publish :end_drag
32 | end
33 | end
34 |
35 | subscribe :end_drag do
36 | @drag_start_pos = nil
37 | end
38 | end
39 |
40 | def tip; parent.tip; end
41 | end
42 |
43 | event :changed
44 |
45 | attr_reader :value, :range, :handle
46 |
47 | # @param (see Composite#initialize)
48 | #
49 | # @option (see Composite#initialize)
50 | # @option options [Range] :range (0.0..1.0)
51 | # @option options [Range] :value (minimum of :range)
52 | def initialize(options = {}, &block)
53 | options = {
54 | range: 0.0..1.0,
55 | height: 25,
56 | background_color: default(:background_color),
57 | border_color: default(:border_color),
58 | groove_color: default(:groove_color),
59 | handle_color: default(:handle_color),
60 | groove_thickness: 5,
61 | }.merge! options
62 |
63 | @range = options[:range].dup
64 | @groove_color = options[:groove_color].dup
65 | @groove_thickness = options[:groove_thickness]
66 | @continuous = @range.min.is_a?(Float) || @range.max.is_a?(Float)
67 |
68 | super(options)
69 |
70 | @handle = Handle.new(parent: self, width: (height / 2 - padding_left), height: height - padding_top + padding_bottom,
71 | background_color: options[:handle_color])
72 |
73 | self.value = options.has_key?(:value) ? options[:value] : @range.min
74 | end
75 |
76 | def value=(value)
77 | @value = @continuous ? value.to_f : value.round
78 | @value = [[@value, @range.min].max, @range.max].min
79 | @handle.x = x + padding_left + ((width - @handle.width) * (@value - @range.min) / (@range.max - @range.min).to_f)
80 | publish :changed, @value
81 |
82 | @value
83 | end
84 |
85 | def tip
86 | tip = super
87 | tip.empty? ? @value.to_s : "#{tip}: #{@value}"
88 | end
89 |
90 | def left_mouse_button(sender, x, y)
91 | # In this case, x should be the centre of the handle after it has moved.
92 | self.value = ((x - (@handle.width / 2) - self.x) / (width - @handle.width)) * (@range.max - @range.min) + @range.min
93 | @mouse_down = true
94 |
95 | nil
96 | end
97 |
98 | def handle_dragged_to(x, y)
99 | # In this case, x is the left-hand side fo the handle.
100 | self.value = ((x - self.x) / (width - @handle.width)) * (@range.max - @range.min) + @range.min
101 | end
102 |
103 | protected
104 | # Prevent standard packing layout change.
105 | def layout
106 | nil
107 | end
108 |
109 | protected
110 | def draw_background
111 | super
112 | # Draw a groove for the handle to move along.
113 | draw_rect x + (@handle.width / 2), y + (height - @groove_thickness) / 2, width - @handle.width, @groove_thickness, z, @groove_color
114 | nil
115 | end
116 |
117 | protected
118 | # Use block as an event handler.
119 | def post_init_block(&block)
120 | subscribe :changed, &block
121 | end
122 | end
123 | end
--------------------------------------------------------------------------------
/lib/fidgit/elements/scroll_bar.rb:
--------------------------------------------------------------------------------
1 | module Fidgit
2 | # @abstract
3 | class ScrollBar < Composite
4 | class Handle < Element
5 | event :begin_drag
6 | event :update_drag
7 | event :end_drag
8 |
9 | def drag?(button); button == :left; end
10 |
11 | def initialize(options = {})
12 | super options
13 |
14 | subscribe :begin_drag do |sender, x, y|
15 | # Store position of the handle when it starts to drag.
16 | @drag_start_pos = [x - self.x, y - self.y]
17 | end
18 |
19 | subscribe :update_drag do |sender, x, y|
20 | parent.parent.handle_dragged_to x - @drag_start_pos[0], y - @drag_start_pos[1]
21 | end
22 |
23 | subscribe :end_drag do
24 | @drag_start_pos = nil
25 | end
26 | end
27 | end
28 |
29 | def initialize(options = {})
30 | options = {
31 | background_color: default(:background_color),
32 | border_color: default(:border_color),
33 | rail_width: default(:rail_width),
34 | rail_color: default(:rail_color),
35 | handle_color: default(:handle_color),
36 | owner: nil,
37 | }.merge! options
38 |
39 | @owner = options[:owner]
40 | @rail_thickness = options[:rail_width]
41 | @rail_color = options[:rail_color]
42 |
43 | super options
44 |
45 | @handle_container = Container.new(parent: self, width: options[:width], height: options[:height]) do
46 | @handle = Handle.new(parent: self, x: x, y: y, background_color: options[:handle_color])
47 | end
48 |
49 | subscribe :left_mouse_button do |sender, x, y|
50 | clicked_to_move x, y
51 | end
52 | end
53 | end
54 |
55 | class HorizontalScrollBar < ScrollBar
56 | attr_reader :owner
57 |
58 | def initialize(options = {})
59 | super options
60 |
61 | @handle.height = height
62 |
63 | @handle_container.subscribe :left_mouse_button do |sender, x, y|
64 | distance = @owner.view_width
65 | @owner.offset_x += (x > @handle.x)? +distance : -distance
66 | end
67 | end
68 |
69 | def update
70 | window = parent.parent
71 |
72 | # Resize and re-locate the handles based on changes to the scroll-window.
73 | content_width = window.content_width.to_f
74 | @handle.width = (window.view_width * width) / content_width
75 | @handle.x = x + (window.offset_x * width) / content_width
76 | end
77 |
78 | def draw_foreground
79 | draw_rect x + padding_left, y + (height - @rail_thickness) / 2, width, @rail_thickness, z, @rail_color
80 | super
81 | end
82 |
83 | def handle_dragged_to(x, y)
84 | @owner.offset_x = @owner.content_width * ((x - self.x) / width.to_f)
85 | end
86 |
87 | def clicked_to_move(x, y)
88 | new_x = x < @handle.x ? @handle.x - @handle.width : @handle.x + @handle.width
89 | handle_dragged_to new_x, @handle.y
90 | end
91 | end
92 |
93 | class VerticalScrollBar < ScrollBar
94 | def initialize(options = {})
95 | super options
96 |
97 | @handle.width = width
98 |
99 | @handle_container.subscribe :left_mouse_button do |sender, x, y|
100 | distance = @owner.view_height
101 | @owner.offset_y += (y > @handle.y)? +distance : -distance
102 | end
103 | end
104 |
105 | def update
106 | window = parent.parent
107 | content_height = window.content_height.to_f
108 | @handle.height = (window.view_height * height) / content_height
109 | @handle.y = y + (window.offset_y * height) / content_height
110 | end
111 |
112 | def draw_foreground
113 | draw_rect x + (width - @rail_thickness) / 2, y + padding_top, @rail_thickness, height, z, @rail_color
114 | super
115 | end
116 |
117 | def handle_dragged_to(x, y)
118 | @owner.offset_y = @owner.content_height * ((y - self.y) / height.to_f)
119 | end
120 |
121 | def clicked_to_move(x, y)
122 | new_y = y < @handle.y ? @handle.y - @handle.height : @handle.y + @handle.height
123 | handle_dragged_to @handle.x, new_y
124 | end
125 | end
126 | end
--------------------------------------------------------------------------------
/lib/fidgit/gosu_ext/color.rb:
--------------------------------------------------------------------------------
1 | module Gosu
2 | class Color
3 | # Is the color completely transparent?
4 | def transparent?; alpha == 0; end
5 | # Is the color completely opaque?
6 | def opaque?; alpha == 255; end
7 |
8 | # RGB in 0..255 format (Alpha assumed 255)
9 | #
10 | # @param [Integer] red
11 | # @param [Integer] green
12 | # @param [Integer] blue
13 | # @return [Color]
14 | def self.rgb(red, green, blue)
15 | new(255, red, green, blue)
16 | end
17 |
18 | # RGBA in 0..255 format
19 | #
20 | # @param [Integer] red
21 | # @param [Integer] green
22 | # @param [Integer] blue
23 | # @param [Integer] alpha
24 | # @return [Color]
25 | def self.rgba(red, green, blue, alpha)
26 | new(alpha, red, green, blue)
27 | end
28 |
29 | # ARGB in 0..255 format (equivalent to Color.new, but explicit)
30 | #
31 | # @param [Integer] alpha
32 | # @param (see Color.rgb)
33 | # @return [Color]
34 | def self.argb(alpha, red, green, blue)
35 | new(alpha, red, green, blue)
36 | end
37 |
38 | # HSV format (alpha assumed to be 255)
39 | #
40 | # @param [Float] hue 0.0..360.0
41 | # @param [Float] saturation 0.0..1.0
42 | # @param [Float] value 0.0..1.0
43 | # @return [Color]
44 | def self.hsv(hue, saturation, value)
45 | from_hsv(hue, saturation, value)
46 | end
47 |
48 | # HSVA format
49 | #
50 | # @param [Float] hue 0.0..360.0
51 | # @param [Float] saturation 0.0..1.0
52 | # @param [Float] value 0.0..1.0
53 | # @param [Integer] alpha 1..255
54 | # @return [Color]
55 | def self.hsva(hue, saturation, value, alpha)
56 | from_ahsv(alpha, hue, saturation, value)
57 | end
58 |
59 | class << self
60 | alias_method :ahsv, :from_ahsv
61 | end
62 |
63 | # Convert from an RGBA array, as used by TexPlay.
64 | #
65 | # @param [Array] color TexPlay color [r, g, b, a] in range 0.0..1.0
66 | # @return [Color]
67 | def self.from_tex_play(color)
68 | rgba(*color.map {|c| (c * 255).to_i })
69 | end
70 |
71 | # Convert to an RGBA array, as used by TexPlay.
72 | #
73 | # @return [Array] TexPlay color array [r, g, b, a] in range 0.0..1.0
74 | def to_tex_play
75 | [red / 255.0, green / 255.0, blue / 255.0, alpha / 255.0]
76 | end
77 |
78 | # Convert to a 6-digit hexadecimal value, appropriate for passing to the Gosu ++ tag. The alpha channel is ignored.
79 | #
80 | # @return [String] RGB hexadecimal string, such as "AABB00"
81 | def to_hex
82 | "%02x%02x%02x" % [red, green, blue]
83 | end
84 |
85 | # Colorize text for in-line rendering by Gosu.
86 | # e.g. "frog" => "frog"
87 | def colorize(text)
88 | "#{text}"
89 | end
90 |
91 | # Show the Color as or, if opaque, (Gosu default is '(ARGB:0/0/0/0)')
92 | def to_s
93 | if opaque?
94 | ""
95 | else
96 | ""
97 | end
98 | end
99 |
100 | def +(other)
101 | raise ArgumentError, "Can only add another #{self.class}" unless other.is_a? Color
102 |
103 | copy = Color.new(0)
104 |
105 | copy.red = [red + other.red, 255].min
106 | copy.green = [green + other.green, 255].min
107 | copy.blue = [blue + other.blue, 255].min
108 | copy.alpha = [alpha + other.alpha, 255].min
109 |
110 | copy
111 | end
112 |
113 | def -(other)
114 | raise ArgumentError, "Can only take away another #{self.class}" unless other.is_a? Color
115 |
116 | copy = Color.new(0)
117 |
118 | copy.red = [red - other.red, 0].max
119 | copy.green = [green - other.green, 0].max
120 | copy.blue = [blue - other.blue, 0].max
121 | copy.alpha = [alpha - other.alpha, 0].max
122 |
123 | copy
124 | end
125 |
126 | def ==(other)
127 | if other.is_a? Color
128 | red == other.red and green == other.green and blue == other.blue and alpha == other.alpha
129 | else
130 | false
131 | end
132 | end
133 | end
134 | end
--------------------------------------------------------------------------------
/spec/fidgit/gosu_ext/color_spec.rb:
--------------------------------------------------------------------------------
1 | require_relative "helpers/helper"
2 |
3 | include Gosu
4 |
5 | describe Color do
6 | describe "rgb" do
7 | it "should create a color with the correct values from channel values" do
8 | color = Color.rgb(1, 2, 3)
9 | color.red.should equal 1
10 | color.green.should equal 2
11 | color.blue.should equal 3
12 | color.alpha.should equal 255
13 | end
14 | end
15 |
16 | describe "rgba" do
17 | it "should create a color with the correct value from channel valuess" do
18 | color = Color.rgba(1, 2, 3, 4)
19 | color.red.should equal 1
20 | color.green.should equal 2
21 | color.blue.should equal 3
22 | color.alpha.should equal 4
23 | end
24 | end
25 |
26 | describe "argb" do
27 | it "should create a color with the correct values from channel values" do
28 | color = Color.argb(1, 2, 3, 4)
29 | color.red.should equal 2
30 | color.green.should equal 3
31 | color.blue.should equal 4
32 | color.alpha.should equal 1
33 | end
34 | end
35 |
36 | describe "ahsv" do
37 | it "should create a color with the correct values from channel values" do
38 | color = Color.ahsv(1, 180.0, 0.5, 0.7)
39 | color.hue.should be_within(0.001).of(180.0)
40 | color.saturation.should be_within(0.01).of(0.5)
41 | color.value.should be_within(0.01).of(0.7)
42 | color.alpha.should equal 1
43 | end
44 | end
45 |
46 | describe "hsva" do
47 | it "should create a color with the correct values from channel values" do
48 | color = Color.hsva(180.0, 0.5, 0.7, 4)
49 | color.hue.should be_within(0.001).of(180.0)
50 | color.saturation.should be_within(0.01).of(0.5)
51 | color.value.should be_within(0.01).of(0.7)
52 | color.alpha.should equal 4
53 | end
54 | end
55 |
56 | describe "from_tex_play" do
57 | it "should create a color with the correct values" do
58 | color = Color.from_tex_play([1 / 255.0, 2 / 255.0, 3 / 255.0, 4 / 255.0])
59 | color.red.should equal 1
60 | color.green.should equal 2
61 | color.blue.should equal 3
62 | color.alpha.should equal 4
63 | end
64 | end
65 |
66 | describe "#to_tex_play" do
67 | it "should create an array with the correct values" do
68 | array = Color.rgba(1, 2, 3, 4).to_tex_play
69 | array.should eq [1 / 255.0, 2 / 255.0, 3 / 255.0, 4 / 255.0]
70 | end
71 | end
72 |
73 | describe "#==" do
74 | it "should return true for colours that are identical" do
75 | (Color.rgb(1, 2, 3) == Color.rgb(1, 2, 3)).should be_true
76 | end
77 |
78 | it "should return false for colours that are not the same" do
79 | (Color.rgb(1, 2, 3) == Color.rgb(4, 2, 3)).should be_false
80 | end
81 | end
82 |
83 | describe "#transparent?" do
84 | it "should be true if alpha is 0" do
85 | Color.rgba(1, 1, 1, 0).should be_transparent
86 | end
87 |
88 | it "should be false if 0 > alpha > 255" do
89 | Color.rgba(1, 1, 1, 2).should_not be_transparent
90 | end
91 |
92 | it "should be false if alpha == 255" do
93 | Color.rgba(1, 1, 1, 255).should_not be_transparent
94 | end
95 | end
96 |
97 | describe "#opaque?" do
98 | it "should be true if alpha is 0" do
99 | Color.rgba(1, 1, 1, 0).should_not be_opaque
100 | end
101 |
102 | it "should be false if 0 > alpha > 255" do
103 | Color.rgba(1, 1, 1, 2).should_not be_opaque
104 | end
105 |
106 | it "should be false if alpha == 255" do
107 | Color.rgba(1, 1, 1, 255).should be_opaque
108 | end
109 | end
110 |
111 | describe "#+" do
112 | it "should add two colours together" do
113 | (Color.rgba(1, 2, 3, 4) + Color.rgba(10, 20, 30, 40)).should == Color.rgba(11, 22, 33, 44)
114 | end
115 |
116 | it "should cap values at 255" do
117 | (Color.rgba(56, 56, 56, 56) + Color.rgba(200, 200, 200, 200)).should == Color.rgba(255, 255, 255, 255)
118 | end
119 | end
120 |
121 | describe "#-" do
122 | it "should subtract one color from another two colours together" do
123 | (Color.rgba(10, 20, 30, 40) - Color.rgba(1, 2, 3, 4)).should == Color.rgba(9, 18, 27, 36)
124 | end
125 |
126 | it "should cap values at 0" do
127 | (Color.rgba(56, 56, 56, 56) - Color.rgba(57, 57, 57, 57)).should == Color.rgba(0, 0, 0, 0)
128 | end
129 | end
130 | end
--------------------------------------------------------------------------------
/spec/fidgit/history_spec.rb:
--------------------------------------------------------------------------------
1 | require_relative 'helpers/helper'
2 |
3 | require 'history'
4 |
5 | module Fidgit
6 | class History
7 | class Maths < Action
8 | def initialize(value)
9 | @value = value
10 | end
11 | end
12 |
13 | class Add < Maths
14 | def do
15 | $x += @value
16 | end
17 |
18 | def undo
19 | $x -= @value
20 | end
21 | end
22 |
23 | class Sub < Maths
24 | def do
25 | $x -= @value
26 | end
27 |
28 | def undo
29 | $x += @value
30 | end
31 | end
32 | end
33 |
34 | describe History do
35 | before :each do
36 | $x = 0 # Used as target of test action.
37 | @object = described_class.new
38 | end
39 |
40 | describe "initialize()" do
41 | it "should produce an object that is not undoable or redoable" do
42 | @object.can_undo?.should be_false
43 | @object.can_redo?.should be_false
44 | end
45 | end
46 |
47 | describe "do()" do
48 | it "should raise an error if the parameter is incorrect" do
49 | lambda { @object.do(12) }.should raise_error ArgumentError
50 | end
51 |
52 | it "should perform the action's do() method correctly" do
53 | @object.do(History::Add.new(2))
54 |
55 | $x.should == 2
56 | @object.can_undo?.should be_true
57 | @object.can_redo?.should be_false
58 | end
59 |
60 | it "should correctly delete any re-doable actions, even if there is only one" do
61 | @object.do(History::Add.new(2))
62 | @object.undo
63 | @object.do(History::Add.new(3))
64 | @object.undo
65 |
66 | @object.can_undo?.should be_false
67 | end
68 | end
69 |
70 | describe "undo()" do
71 | it "should raise an error if there is nothing to undo" do
72 | lambda { @object.undo }.should raise_error
73 | end
74 |
75 | it "should perform the undo method on the action" do
76 | @object.do(History::Add.new(2))
77 | @object.undo
78 |
79 | $x.should == 0
80 | @object.can_undo?.should be_false
81 | @object.can_redo?.should be_true
82 | end
83 | end
84 |
85 | describe "redo()" do
86 | it "should raise an error if there is nothing to redo" do
87 | lambda { @object.redo }.should raise_error
88 | end
89 |
90 | it "should perform the undo method on the action" do
91 | @object.do(History::Add.new(2))
92 | @object.undo
93 | @object.redo
94 |
95 | $x.should == 2
96 | @object.can_undo?.should be_true
97 | @object.can_redo?.should be_false
98 | end
99 | end
100 |
101 | describe "replace_last()" do
102 | it "should raise an error if there is nothing to redo" do
103 | lambda { @object.replace_last(12) }.should raise_error ArgumentError
104 | end
105 |
106 | it "should raise an error if there is nothing to replace" do
107 | lambda { @object.replace_last(History::Add2.new) }.should raise_error
108 | end
109 |
110 | it "should correctly replace the last action" do
111 | @object.do(History::Add.new(2))
112 | @object.replace_last(History::Sub.new(1))
113 |
114 | $x.should == -1
115 | @object.can_redo?.should be_false
116 | @object.can_undo?.should be_true
117 | end
118 |
119 | it "should not remove redoable actions" do
120 | @object.do(History::Add.new(2))
121 | @object.do(History::Add.new(8))
122 | @object.undo
123 | @object.replace_last(History::Sub.new(1))
124 |
125 | $x.should == -1
126 | @object.can_redo?.should be_true
127 | @object.can_undo?.should be_true
128 |
129 | @object.redo
130 | $x.should == 7
131 | end
132 | end
133 | end
134 |
135 | # Abstract class, so doesn't really do anything.
136 | describe History::Action do
137 | before :each do
138 | @object = described_class.new
139 | end
140 |
141 | describe "do()" do
142 | it "should raise an error" do
143 | lambda { @object.do }.should raise_error NotImplementedError
144 | end
145 | end
146 |
147 | describe "undo()" do
148 | it "should raise an error" do
149 | lambda { @object.undo }.should raise_error NotImplementedError
150 | end
151 | end
152 | end
153 | end
154 |
--------------------------------------------------------------------------------
/lib/fidgit/schema.rb:
--------------------------------------------------------------------------------
1 | module Fidgit
2 | # An object that manages Schema values. Usually loaded from a YAML file.
3 | #
4 | # @example
5 | # schema = Schema.new(YAML.load(file.read('default_schema.yml')))
6 | # default_color = schema.default(Element, :disabled, :color)
7 | # schema.merge_schema!(YAML.load(file.read('override_schema.yml'))
8 | # overridden_color = schema.default(Element, :disabled, :color)
9 | class Schema
10 | CONSTANT_PREFIX = '?'
11 |
12 | # @param [Hash Hash>] schema data containing
13 | def initialize(schema)
14 | @constants = {}
15 | @elements = {}
16 |
17 | merge_schema! schema
18 | end
19 |
20 | # Merge in a hash containing constant values.
21 | #
22 | # @param [Hash Hash>] constants_hash Containing :colors, :constants and :elements hashes.
23 | def merge_schema!(schema)
24 | merge_constants!(schema[:constants]) if schema[:constants]
25 | merge_elements!(schema[:elements]) if schema[:elements]
26 |
27 | self
28 | end
29 |
30 | # Merge in a hash containing constant values. Arrays will be resolved as colors in RGBA or RGB format.
31 | #
32 | # @param [Hash Object>] constants_hash
33 | def merge_constants!(constants_hash)
34 | constants_hash.each_pair do |name, value|
35 | @constants[name] = case value
36 | when Array
37 | case value.size
38 | when 3 then Gosu::Color.rgb(*value)
39 | when 4 then Gosu::Color.rgba(*value)
40 | else
41 | raise "Colors must be in 0..255, RGB or RGBA array format"
42 | end
43 | else
44 | value
45 | end
46 | end
47 |
48 | self
49 | end
50 |
51 | # Merge in a hash containing default values for each element.
52 | #
53 | # @param [Hash Hash>] elements_hash
54 | def merge_elements!(elements_hash)
55 | elements_hash.each_pair do |klass_names, data|
56 | klass = Fidgit
57 | klass_names.to_s.split('::').each do |klass_name|
58 | klass = klass.const_get klass_name
59 | end
60 |
61 | raise "elements must be names of classes derived from #{Element}" unless klass.ancestors.include? Fidgit::Element
62 | @elements[klass] ||= {}
63 | @elements[klass].deep_merge! data
64 | end
65 |
66 | self
67 | end
68 |
69 | # Get the constant value associated with +name+.
70 | #
71 | # @param [Symbol] name
72 | # @return [Object]
73 | def constant(name)
74 | @constants[name]
75 | end
76 |
77 | # @param [Class] klass Class to look for defaults for.
78 | # @param [Symbol, Array] names Hash names to search for in that class's schema.
79 | def default(klass, names)
80 | raise ArgumentError, "#{klass} is not a descendent of the #{Element} class" unless klass.ancestors.include? Element
81 | value = default_internal(klass, Array(names), true)
82 | raise("Failed to find named value #{names.inspect} for class #{klass}") unless value
83 | value
84 | end
85 |
86 | protected
87 | # @param [Class] klass Class to look for defaults for.
88 | # @param [Array] names Hash names to search for in that class's schema.
89 | # @param [Boolean] default_to_outer Whether to default to an outer value (used internally)
90 | def default_internal(klass, names, default_to_outer)
91 | # Find the value by moving through the nested hash via the names.
92 | value = @elements[klass]
93 |
94 | names.each do |name|
95 | break unless value.is_a? Hash
96 | value = value.has_key?(name) ? value[name] : nil
97 | end
98 |
99 | # Convert the value to a color/constant if they are symbols.
100 | value = if value.is_a? String and value[0] == CONSTANT_PREFIX
101 | str = value[1..-1]
102 | constant(str.to_sym) || value # If the value isn't a constant, return the string.
103 | else
104 | value
105 | end
106 |
107 | # If we didn't find the value for this class, default to parent class value.
108 | if value.nil? and klass != Element and klass.ancestors.include? Element
109 | # Check if any ancestors define the fully named value.
110 | value = default_internal(klass.superclass, names, false)
111 | end
112 |
113 | if value.nil? and default_to_outer and names.size > 1
114 | # Check the outer values (e.g. if [:hover, :color] is not defined, try [:color]).
115 | value = default_internal(klass, names[1..-1], true)
116 | end
117 |
118 | value
119 | end
120 | end
121 | end
122 |
--------------------------------------------------------------------------------
/lib/fidgit/elements/menu_pane.rb:
--------------------------------------------------------------------------------
1 | require 'forwardable'
2 |
3 | module Fidgit
4 | class MenuPane < Composite
5 | # An item within the menu.
6 | class Item < Button
7 | attr_reader :value, :shortcut_text
8 |
9 | # @param (see Button#initialize)
10 | #
11 | # @option (see Button#initialize)
12 | # @param [any] value Value if the user picks this item
13 | # @option options [String] :shortcut_text ('')
14 | def initialize(text, value, options = {})
15 | options = {
16 | enabled: true,
17 | border_color: default(:border_color),
18 | shortcut_text: '',
19 | }.merge! options
20 |
21 | @value = value
22 | @shortcut_text = options[:shortcut_text]
23 |
24 | super(text, options)
25 | end
26 |
27 | def draw_foreground
28 | super
29 |
30 | unless @shortcut_text.empty?
31 | font.draw_rel("#{@shortcut_text}", rect.right - padding_right, y + ((height - font.height) / 2).floor, z, 1, 0, 1, 1, color)
32 | end
33 |
34 | nil
35 | end
36 |
37 | protected
38 | def layout
39 | super
40 |
41 | # Ignore layout request when asked before TextLine has been created.
42 | rect.width += font.text_width(" #{@shortcut_text}") unless @shortcut_text.empty? or @text.nil?
43 |
44 | nil
45 | end
46 | end
47 |
48 | class Separator < Label
49 | # @param (see Item#initialize)
50 | #
51 | # @option (see Item#initialize)
52 | def initialize(options = {})
53 | options = {
54 | height: default(:line_height),
55 | background_color: default(:background_color),
56 | padding: 0,
57 | }.merge! options
58 |
59 | super '', options
60 | end
61 | end
62 |
63 | extend Forwardable
64 |
65 | def_delegators :@items, :each, :clear, :size, :[]
66 |
67 | event :selected
68 |
69 | def index(value); @items.index find(value); end
70 | def x=(value); super(value); recalc; end
71 | def y=(value); super(value); recalc; end
72 |
73 | # @option (see Composite#initialize)
74 | # @option options [Float] :x (cursor x, if in a GuiState)
75 | # @option options [Float] :y (cursor y, if in a GuiState)
76 | # @option options [Boolean] :show (true) Whether to show immediately (show later with #show).
77 | def initialize(options = {}, &block)
78 | options = {
79 | background_color: default(:color),
80 | z: Float::INFINITY,
81 | show: true,
82 | }.merge! options
83 |
84 | state = $window.current_game_state
85 | if state.is_a? GuiState
86 | cursor = $window.current_game_state.cursor
87 | options = {
88 | x: cursor.x,
89 | y: cursor.y,
90 | }.merge! options
91 | end
92 |
93 | @items = nil
94 |
95 | super(options)
96 |
97 | @items = vertical spacing: 0, padding: 0
98 |
99 | if options[:show] and state.is_a? GuiState
100 | show
101 | end
102 | end
103 |
104 | def find(value)
105 | @items.find {|c| c.value == value }
106 | end
107 |
108 | def separator(options = {})
109 | options[:z] = z
110 |
111 | Separator.new({ parent: @items }.merge!(options))
112 | end
113 |
114 | def item(text, value, options = {}, &block)
115 | options = options.merge({
116 | parent: @items,
117 | z: z,
118 | })
119 | item = Item.new(text, value, options, &block)
120 |
121 | item.subscribe :left_mouse_button, method(:item_selected)
122 | item.subscribe :right_mouse_button, method(:item_selected)
123 |
124 | item
125 | end
126 |
127 | def item_selected(sender, x, y)
128 | publish(:selected, sender.value)
129 |
130 | $window.game_state_manager.current_game_state.hide_menu
131 |
132 | nil
133 | end
134 |
135 | def show
136 | $window.game_state_manager.current_game_state.show_menu self
137 | nil
138 | end
139 |
140 | protected
141 | def layout
142 | super
143 |
144 | if @items
145 | # Ensure the menu can't go over the edge of the screen. If it can't be avoided, align with top-left edge of screen.
146 | rect.x = [[x, $window.width - width - padding_right].min, 0].max
147 | rect.y = [[y, $window.height - height - padding_bottom].min, 0].max
148 |
149 | # Move the actual list if the menu has moved to keep on the screen.
150 | @items.x = x + padding_left
151 | @items.y = y + padding_top
152 |
153 | # Ensure that all items are of the same width.
154 | max_width = @items.map(&:width).max || 0
155 | @items.each {|c| c.rect.width = max_width }
156 |
157 | @items.recalc # Move all the items inside the packer to correct ones.
158 | end
159 |
160 | nil
161 | end
162 | end
163 | end
--------------------------------------------------------------------------------
/config/default_schema.yml:
--------------------------------------------------------------------------------
1 | # Default schema for Fidgit
2 | ---
3 |
4 | # Define all constant values here. For colours, use [R, G, B] or [R, G, B, A].
5 | # Reference constants with ?constant_name
6 | :constants:
7 | # General constants (strings and numbers).
8 | :scroll_bar_thickness: 12
9 |
10 | # Gosu::Color constants
11 | :none: [0, 0, 0, 0]
12 |
13 | :black: [0, 0, 0]
14 | :white: [255, 255, 255]
15 |
16 | :very_dark_gray: [50, 50, 50]
17 | :dark_gray: [100, 100, 100]
18 | :gray: [150, 150, 150]
19 | :light_gray: [200, 200, 200]
20 |
21 | :red: [255, 0, 0]
22 | :green: [0, 0, 255]
23 | :blue: [0, 0, 255]
24 |
25 | :dark_red: [100, 0, 0]
26 | :dark_green: [0, 100, 0]
27 | :dark_blue: [0, 0, 100]
28 |
29 | # Default element attributes.
30 | :elements:
31 | :Button: # < Label
32 | :background_color: ?dark_gray
33 | :border_color: ?light_gray
34 | :border_thickness: 2
35 | :shortcut_color: ?red
36 | :padding_top: 2
37 | :padding_right: 4
38 | :padding_bottom: 2
39 | :padding_left: 4
40 |
41 | :disabled:
42 | :background_color: ?very_dark_gray
43 | :border_color: ?gray
44 |
45 | :hover:
46 | :background_color: ?gray
47 |
48 | :ColorPicker:
49 | :indicator_height: 30
50 |
51 | :ColorWell: # < RadioButton
52 | :height: 32
53 | :outline_color: ?gray
54 | :width: 32
55 |
56 | :checked:
57 | :border_color: ?very_dark_gray
58 |
59 | :ComboBox: # < Button
60 | :background_color: ?dark_gray
61 | :border_color: ?white
62 |
63 | :Composite: # < Container
64 | :padding_top: 0
65 | :padding_right: 0
66 | :padding_bottom: 0
67 | :padding_left: 0
68 |
69 | :Container: # < Element
70 | {}
71 |
72 | :Element:
73 | :align_h: :left
74 | :align_v: :top
75 | :background_color: ?none
76 | :background_image: nil
77 | :border_color: ?none
78 | :border_thickness: 0
79 | :color: ?white
80 | :font_height: 30
81 | :font_name: :default
82 | :padding_top: 2
83 | :padding_right: 4
84 | :padding_bottom: 2
85 | :padding_left: 4
86 |
87 |
88 | :FileBrowser: # < Composite
89 | :pattern: "*.*"
90 | :show_extension: true
91 |
92 | :Grid: # < Packer
93 | :cell_background_color: ?none
94 | :cell_border_color: ?none
95 | :cell_border_thickness: 0
96 |
97 | :Group: # < Packer
98 | :padding_top: 0
99 | :padding_right: 0
100 | :padding_bottom: 0
101 | :padding_left: 0
102 |
103 | :Horizontal: # < Grid
104 | {}
105 |
106 | :HorizontalScrollBar: # < ScrollBar
107 | :height: ?scroll_bar_thickness
108 |
109 | :ImageFrame: # 0
102 | if num_filled_columns > 0
103 | @widths[filled_columns.index true] += extra_width
104 | else
105 | @widths[-1] += extra_width
106 | end
107 | end
108 | end
109 |
110 | # Expand the size of each filled row to make the minimum size required.
111 | unless @heights.empty?
112 | num_filled_rows = filled_rows.select {|value| value }.count
113 | total_height = @heights.inject(0, :+) + (padding_left + padding_right) + ((@num_rows - 1) * spacing_v)
114 | extra_height = min_height - total_height
115 | if extra_height > 0
116 | if num_filled_rows > 0
117 | @heights[filled_rows.index true] += extra_height
118 | else
119 | @heights[-1] += extra_height
120 | end
121 | end
122 | end
123 |
124 | # Actually place all the elements into the grid positions, modified by valign and align.
125 | current_y = y + padding_top
126 | @rows.each_with_index do |row, row_num|
127 | current_x = x + padding_left
128 |
129 | row.each_with_index do |element, column_num|
130 | element.x = current_x + element.border_thickness
131 |
132 | case element.align_h # Take horizontal alignment into consideration.
133 | when :fill
134 | if element.width < @widths[column_num]
135 | element.width = @widths[column_num]
136 | element.send :repack if element.is_a? Grid
137 | end
138 | when :center
139 | element.x += (@widths[column_num] - element.width) / 2
140 | when :right
141 | element.x += @widths[column_num] - element.width
142 | end
143 |
144 | current_x += @widths[column_num]
145 | current_x += spacing_h unless column_num == @num_columns - 1
146 |
147 | element.y = current_y + element.border_thickness
148 |
149 | case element.align_v # Take horizontal alignment into consideration.
150 | when :fill
151 | if element.height < @heights[row_num]
152 | element.height = @heights[row_num]
153 | element.send :repack if element.is_a? Grid
154 | end
155 | when :center
156 | element.y += (@heights[row_num] - element.height) / 2
157 | when :bottom
158 | element.y += @heights[row_num] - element.height
159 | else
160 | end
161 | end
162 |
163 | self.width = current_x - x + padding_left if row_num == 0
164 |
165 | current_y += @heights[row_num] unless row.empty?
166 | current_y += spacing_h unless row_num == num_rows - 1
167 | end
168 |
169 | self.height = current_y - y + padding_top
170 |
171 | nil
172 | end
173 |
174 | protected
175 | # @yield The rectangle of each cell within the grid.
176 | # @yieldparam [Number] x
177 | # @yieldparam [Number] y
178 | # @yieldparam [Number] width
179 | # @yieldparam [Number] height
180 | def each_cell_rect
181 | x = self.x + padding_left
182 |
183 | @widths.each_with_index do |width, column_num|
184 | y = self.y + padding_top
185 |
186 | @heights.each_with_index do |height, row_num|
187 | yield x, y, width, height if @rows[row_num][column_num]
188 | y += height + spacing_v
189 | end
190 |
191 | x += width + spacing_h
192 | end
193 |
194 | nil
195 | end
196 |
197 | protected
198 | def draw_background
199 | super
200 |
201 | # Draw the cell backgrounds.
202 | unless @cell_background_color.transparent?
203 | each_cell_rect do |x, y, width, height|
204 | draw_rect x, y, width, height, z, @cell_background_color
205 | end
206 | end
207 |
208 | nil
209 | end
210 |
211 | protected
212 | def draw_border
213 | super
214 |
215 | # Draw the cell borders.
216 | if @cell_border_thickness > 0 and not @cell_border_color.transparent?
217 | each_cell_rect do |x, y, width, height|
218 | draw_frame x, y, width, height, @cell_border_thickness, z, @cell_border_color
219 | end
220 | end
221 |
222 | nil
223 | end
224 | end
225 | end
--------------------------------------------------------------------------------
/spec/fidgit/event_spec.rb:
--------------------------------------------------------------------------------
1 | require_relative 'helpers/helper'
2 |
3 | require 'event'
4 |
5 | module Fidgit
6 | describe Event do
7 | before :each do
8 | class Test
9 | include Event
10 | end
11 | end
12 |
13 | after :each do
14 | Fidgit.send :remove_const, :Test
15 | end
16 |
17 | subject { Test }
18 |
19 | describe "events" do
20 | it "should initially be empty" do
21 | subject.events.should be_empty
22 | end
23 | end
24 |
25 | describe "event" do
26 | it "should add the event to the list of events handled" do
27 | subject.event :frog
28 | subject.events.should include :frog
29 | end
30 |
31 | it "should inherit parent's events and be able to add more" do
32 | Test.event :frog
33 | class Test2 < Test; end
34 | Test2.event :fish
35 | Test2.events.should include :frog
36 | Test2.events.should include :fish
37 | end
38 | end
39 |
40 | context "When included into a class that is instanced" do
41 | subject { Test.event :frog; Test.new }
42 |
43 | describe "#subscribe" do
44 | it "should add a handler as a block" do
45 | subject.subscribe(:frog) { puts "hello" }
46 | end
47 |
48 | it "should fail if the event name isn't handled by this object" do
49 | ->{ subject.subscribe(:unhandled_event) {} }.should raise_error ArgumentError
50 | end
51 |
52 | it "should add a handler as a method" do
53 | subject.stub! :handler
54 | subject.subscribe(:frog, subject.method(:handler))
55 | end
56 |
57 | it "should fail if neither a method or a block is passed" do
58 | ->{ subject.subscribe(:frog) }.should raise_error ArgumentError
59 | end
60 |
61 | it "should fail if both a method and a block is passed" do
62 | subject.stub! :handler
63 | ->{ subject.subscribe(:frog, subject.method(:handler)) { } }.should raise_error ArgumentError
64 | end
65 |
66 | it "should return a Subscription" do
67 | handler = proc {}
68 | result = subject.subscribe(:frog, &handler)
69 | result.should be_a Fidgit::Event::Subscription
70 | result.handler.should be handler
71 | result.publisher.should be subject
72 | result.event.should equal :frog
73 | end
74 |
75 | it "should return a Subscription that can use Subscription#unsubscribe" do
76 | handler_ran = false
77 | subscription = subject.subscribe(:frog) { handler_ran = true }
78 | subscription.unsubscribe
79 | subject.publish :frog
80 | handler_ran.should be_false
81 | end
82 | end
83 |
84 | describe "#unsubscribe" do
85 | it "should accept a Subscription" do
86 | handler_ran = false
87 | subscription = subject.subscribe(:frog) { handler_ran = true }
88 | subject.unsubscribe subscription
89 | subject.publish :frog
90 | handler_ran.should be_false
91 | end
92 |
93 | it "should accept a handler" do
94 | handler_ran = false
95 | handler = proc { handler_ran = true }
96 | subject.subscribe(:frog, &handler)
97 | subject.unsubscribe handler
98 | subject.publish :frog
99 | handler_ran.should be_false
100 | end
101 |
102 | it "should accept an event and handler" do
103 | handler_ran = false
104 | handler = proc { handler_ran = true }
105 | subject.subscribe(:frog, &handler)
106 | subject.unsubscribe :frog, handler
107 | subject.publish :frog
108 | handler_ran.should be_false
109 | end
110 |
111 | it "should require 1..2 arguments" do
112 | ->{ subject.unsubscribe }.should raise_error ArgumentError
113 | ->{ subject.unsubscribe 1, 2, 3 }.should raise_error ArgumentError
114 | end
115 |
116 | it "should fail with bad types" do
117 | ->{ subject.unsubscribe 1 }.should raise_error TypeError
118 | ->{ subject.unsubscribe 1, ->{} }.should raise_error TypeError
119 | ->{ subject.unsubscribe :event, 2 }.should raise_error TypeError
120 | end
121 |
122 | it "should fail if passed a Subscription to another object" do
123 | subscription = Fidgit::Event::Subscription.new(Object.new.extend(Fidgit::Event), :event, proc {})
124 | ->{ subject.unsubscribe subscription }.should raise_error ArgumentError
125 | end
126 | end
127 |
128 | describe "#publish" do
129 | it "should return nil if there are no handlers" do
130 | subject.publish(:frog).should be_nil
131 | end
132 |
133 | it "should return nil if there are no handlers that handle the event" do
134 | subject.should_receive(:frog).with(subject)
135 | subject.should_receive(:handler).with(subject)
136 | subject.subscribe(:frog, subject.method(:handler))
137 | subject.publish(:frog).should be_nil
138 | end
139 |
140 | it "should return :handled if a manual handler handled the event and not call other handlers" do
141 | subject.should_not_receive(:handler1)
142 | subject.should_receive(:handler2).with(subject).and_return(:handled)
143 | subject.subscribe(:frog, subject.method(:handler1))
144 | subject.subscribe(:frog, subject.method(:handler2))
145 | subject.publish(:frog).should == :handled
146 | end
147 |
148 | it "should return :handled if an automatic handler handled the event and not call other handlers" do
149 | subject.should_receive(:frog).with(subject).and_return(:handled)
150 | subject.should_not_receive(:handler2)
151 | subject.subscribe(:frog, subject.method(:handler2))
152 | subject.publish(:frog).should == :handled
153 | end
154 |
155 | it "should pass the object as the first parameter" do
156 | subject.should_receive(:handler).with(subject)
157 | subject.subscribe(:frog, subject.method(:handler))
158 | subject.publish :frog
159 | end
160 |
161 | it "should call all the handlers, once each" do
162 | subject.should_receive(:handler1).with(subject)
163 | subject.should_receive(:handler2).with(subject)
164 | subject.subscribe(:frog, subject.method(:handler1))
165 | subject.subscribe(:frog, subject.method(:handler2))
166 | subject.publish(:frog).should be_nil
167 | end
168 |
169 | it "should pass parameters passed to it" do
170 | subject.should_receive(:handler).with(subject, 1, 2)
171 | subject.subscribe(:frog, subject.method(:handler))
172 | subject.publish(:frog, 1, 2).should be_nil
173 | end
174 |
175 | it "should do nothing if the subject is disabled" do
176 | subject.should_receive(:enabled?).and_return(false)
177 | subject.should_not_receive(:handler)
178 | subject.subscribe(:frog, subject.method(:handler))
179 | subject.publish(:frog, 1, 2).should be_nil
180 | end
181 |
182 | it "should act normally if the subject is enabled" do
183 | subject.should_receive(:enabled?).and_return(true)
184 | subject.should_receive(:handler)
185 | subject.subscribe(:frog, subject.method(:handler))
186 | subject.publish(:frog, 1, 2).should be_nil
187 | end
188 |
189 | it "should only call the handlers requested" do
190 | Test.event :fish
191 |
192 | subject.should_receive(:handler1).with(subject)
193 | subject.should_not_receive(:handler2)
194 | subject.subscribe(:frog, subject.method(:handler1))
195 | subject.subscribe(:fish, subject.method(:handler2))
196 | subject.publish(:frog).should be_nil
197 | end
198 |
199 | it "should automatically call a method on the publisher if it exists" do
200 | subject.should_receive(:frog).with(subject)
201 | subject.publish(:frog).should be_nil
202 | end
203 |
204 | it "should fail if the event name isn't handled by this object" do
205 | ->{ subject.publish(:unhandled_event) }.should raise_error ArgumentError
206 | end
207 | end
208 | end
209 | end
210 | end
--------------------------------------------------------------------------------
/lib/fidgit/elements/element.rb:
--------------------------------------------------------------------------------
1 | # The Fidgit GUI framework for Gosu.
2 | module Fidgit
3 | class << self
4 | attr_accessor :debug_mode
5 | end
6 |
7 | self.debug_mode = false
8 |
9 | def self.debug_mode?; debug_mode; end
10 |
11 | # An element within the GUI environment.
12 | # @abstract
13 | class Element
14 | include Event
15 |
16 | event :left_mouse_button
17 | event :holding_left_mouse_button
18 | event :released_left_mouse_button
19 | event :clicked_left_mouse_button
20 |
21 | event :right_mouse_button
22 | event :holding_right_mouse_button
23 | event :released_right_mouse_button
24 | event :clicked_right_mouse_button
25 |
26 | event :middle_mouse_button
27 | event :holding_middle_mouse_button
28 | event :released_middle_mouse_button
29 | event :clicked_middle_mouse_button
30 |
31 | event :mouse_wheel_up
32 | event :mouse_wheel_down
33 |
34 | event :enter
35 | event :hover
36 | event :leave
37 |
38 | DEFAULT_SCHEMA_FILE = File.expand_path(File.join(__FILE__, '..', '..', '..', '..', 'config', 'default_schema.yml'))
39 |
40 | VALID_ALIGN_H = [:left, :center, :right, :fill]
41 | VALID_ALIGN_V = [:top, :center, :bottom, :fill]
42 |
43 | attr_reader :z, :tip, :padding_top, :padding_right, :padding_bottom, :padding_left,
44 | :align_h, :align_v, :parent, :border_thickness, :font
45 |
46 | attr_accessor :background_color
47 | attr_writer :tip
48 |
49 | def x; rect.x; end
50 | def x=(value); rect.x = value; end
51 |
52 | def y; rect.y; end
53 | def y=(value); rect.y = value; end
54 |
55 | # Width not including border.
56 | def width; rect.width; end
57 | def width=(value); rect.width = [[value, @width_range.max].min, @width_range.min].max; end
58 | def min_width; @width_range.min; end
59 | def max_width; @width_range.max; end
60 | # Width including border thickness.
61 | def outer_width; rect.width + @border_thickness * 2; end
62 |
63 | # Height not including border.
64 | def height; rect.height; end
65 | def height=(value); rect.height = [[value, @height_range.max].min, @height_range.min].max; end
66 | def min_height; @height_range.min; end
67 | def max_height; @height_range.max; end
68 | # Height including border thickness.
69 | def outer_height; rect.height + @border_thickness * 2; end
70 |
71 | # Can the object be dragged?
72 | def drag?(button); false; end
73 |
74 | def enabled?; @enabled; end
75 |
76 | def enabled=(value)
77 | if @mouse_over and enabled? and not value
78 | $window.current_game_state.unset_mouse_over
79 | end
80 |
81 | @enabled = value
82 | end
83 |
84 | def rect; @rect; end; protected :rect
85 |
86 | def self.schema; @@schema ||= Schema.new(YAML.load(File.read(DEFAULT_SCHEMA_FILE)));; end
87 |
88 | class << self
89 | alias_method :original_new, :new
90 |
91 | def new(*args, &block)
92 | obj = original_new(*args) # Block should be ignored.
93 | obj.send :post_init
94 | obj.send :post_init_block, &block if block_given?
95 | obj
96 | end
97 | end
98 |
99 | # Get the default value from the schema.
100 | #
101 | # @param [Symbol, Array] names
102 | def default(*names)
103 | self.class.schema.default(self.class, names)
104 | end
105 |
106 | # @param [Element, nil] parent
107 | #
108 | # @option options [Number] :x (0)
109 | # @option options [Number] :y (0)
110 | # @option options [Number] :z (0)
111 | #
112 | # @option options [Number] :width (auto)
113 | # @option options [Number] :min_width (value of :width option)
114 | # @option options [Number] :max_width (value of :width option)
115 | #
116 | # @option options [Number] :height (auto)
117 | # @option options [Number] :min_height (value of :height option)
118 | # @option options [Number] :max_height (value of :height option)
119 | #
120 | # @option options [String] :tip ('') Tool-tip text
121 | # @option options [String, :default] :font_name (:default, which resolves as the default Gosu font)
122 | # @option options [String] :font_height (30)
123 | # @option options [Gosu::Font] :font Use this instead of :font_name and :font_height
124 | #
125 | # @option options [Gosu::Color] :background_color (transparent)
126 | # @option options [Gosu::Color] :border_color (transparent)
127 | #
128 | # @option options [Boolean] :enabled (true)
129 | #
130 | # @option options [Number] :padding (4)
131 | # @option options [Number] :padding_h (:padding option)
132 | # @option options [Number] :padding_v (:padding option)
133 | # @option options [Number] :padding_top (:padding_v option)
134 | # @option options [Number] :padding_right (:padding_h option)
135 | # @option options [Number] :padding_bottom (:padding_v option)
136 | # @option options [Number] :padding_left (:padding_h option)
137 | #
138 | # @option options [Symbol] :align Align both horizontally and vertically. One of :center, :fill or [, ] such as [:top, :right].
139 | # @option options [Symbol] :align_h (value or :align else :left) One of :left, :center, :right :fill
140 | # @option options [Symbol] :align_v (value of :align else :top) One of :top, :center, :bottom, :fill
141 |
142 | # @yield instance_methods_eval with respect to self.
143 | def initialize(options = {}, &block)
144 | options = {
145 | x: 0,
146 | y: 0,
147 | z: 0,
148 | tip: '',
149 | font_name: default(:font_name),
150 | font_height: default(:font_height),
151 | background_color: default(:background_color),
152 | border_color: default(:border_color),
153 | border_thickness: default(:border_thickness),
154 | enabled: true,
155 | }.merge! options
156 |
157 | @enabled = options[:enabled]
158 |
159 | @mouse_over = false
160 |
161 | # Alignment and min/max dimensions.
162 | @align_h = options[:align_h] || Array(options[:align]).last || default(:align_h)
163 | raise ArgumentError, "Invalid align_h: #{@align_h}" unless VALID_ALIGN_H.include? @align_h
164 |
165 | min_width = (options[:min_width] || options[:width] || 0)
166 | max_width = (options[:max_width] || options[:width] || Float::INFINITY)
167 | @width_range = min_width..max_width
168 |
169 | @align_v = options[:align_v] || Array(options[:align]).first || default(:align_v)
170 | raise ArgumentError, "Invalid align_v: #{@align_v}" unless VALID_ALIGN_V.include? @align_v
171 |
172 | min_height = (options[:min_height] || options[:height] || 0)
173 | max_height = (options[:max_height] || options[:height] || Float::INFINITY)
174 | @height_range = min_height..max_height
175 |
176 | @background_color = options[:background_color].dup
177 | @border_color = options[:border_color].dup
178 | @border_thickness = options[:border_thickness]
179 |
180 | @padding_top = options[:padding_top] || options[:padding_v] || options[:padding] || default(:padding_top)
181 | @padding_right = options[:padding_right] || options[:padding_h] || options[:padding] || default(:padding_right)
182 | @padding_bottom = options[:padding_bottom] || options[:padding_v] || options[:padding] || default(:padding_bottom)
183 | @padding_left = options[:padding_left] || options[:padding_h] || options[:padding] || default(:padding_left)
184 | self.parent = options[:parent]
185 |
186 | @z = options[:z]
187 | @tip = options[:tip].dup
188 | font_name = if options[:font_name].nil? or options[:font_name] == :default
189 | Gosu::default_font_name
190 | else
191 | options[:font_name].dup
192 | end
193 |
194 | @font = options[:font] || Gosu::Font[font_name, options[:font_height]]
195 |
196 | @rect = Chingu::Rect.new(options[:x], options[:y], options[:width] || 0, options[:height] || 0)
197 | end
198 |
199 | def font=(font)
200 | raise TypeError unless font.is_a? Gosu::Font
201 | @font = font
202 | recalc
203 | font
204 | end
205 |
206 | def recalc
207 | old_width, old_height = width, height
208 | layout
209 | parent.recalc if parent and (width != old_width or height != old_height)
210 |
211 | nil
212 | end
213 |
214 | # Check if a point (screen coordinates) is over the element.
215 | def hit?(x, y)
216 | @rect.collide_point?(x, y)
217 | end
218 |
219 | # Redraw the element.
220 | def draw
221 | draw_background
222 | draw_border
223 | draw_foreground
224 | nil
225 | end
226 |
227 | # Update the element.
228 | def update
229 | nil
230 | end
231 |
232 | def draw_rect(*args)
233 | $window.current_game_state.draw_rect(*args)
234 | end
235 |
236 | def draw_frame(*args)
237 | $window.current_game_state.draw_frame(*args)
238 | end
239 |
240 | protected
241 | def parent=(parent); @parent = parent; end
242 |
243 | protected
244 | def draw_background
245 | draw_rect(x, y, width, height, z, @background_color) unless @background_color.transparent?
246 | end
247 |
248 | protected
249 | def draw_border
250 | draw_frame(x, y, width, height, @border_thickness, z, @border_color) if @border_thickness > 0 and not @border_color.transparent?
251 | end
252 |
253 | protected
254 | def draw_foreground
255 | nil
256 | end
257 |
258 | protected
259 | # Should be overridden in children to recalculate the width and height of the element and, if a container
260 | # manage the positions of its children.
261 | def layout
262 | nil
263 | end
264 |
265 | protected
266 | def post_init
267 | recalc
268 | @parent.send :add, self if @parent
269 | end
270 |
271 | public
272 | # Evaluate a block, just like it was a constructor block.
273 | def with(&block)
274 | raise ArgumentError.new("Must pass a block") unless block_given?
275 | case block.arity
276 | when 1
277 | yield self
278 | when 0
279 | instance_methods_eval(&block)
280 | else
281 | raise "block arity must be 0 or 1"
282 | end
283 | end
284 |
285 | protected
286 | # By default, elements do not accept block arguments.
287 | def post_init_block(&block)
288 | raise ArgumentError, "does not accept a block"
289 | end
290 |
291 | public
292 | def to_s
293 | "#{self.class} (#{x}, #{y}) #{width}x#{height}"
294 | end
295 | end
296 | end
--------------------------------------------------------------------------------
/lib/fidgit/states/gui_state.rb:
--------------------------------------------------------------------------------
1 | require 'forwardable'
2 |
3 | module Fidgit
4 | class GuiState < Chingu::GameState
5 | # A 1x1 white pixel used for drawing.
6 | PIXEL_IMAGE = 'pixel.png'
7 |
8 | extend Forwardable
9 |
10 | def_delegators :@container, :horizontal, :vertical, :grid
11 |
12 | # The Container that contains all the elements for this GuiState.
13 | # @return [Packer]
14 | attr_reader :container
15 |
16 | # The element with focus.
17 | # @return [Element]
18 | attr_reader :focus
19 |
20 | # The Cursor.
21 | # @return [Cursor]
22 | def cursor; @@cursor; end
23 |
24 | # Sets the focus to a particular element.
25 | def focus=(element)
26 | @focus.publish :blur if @focus and element
27 | @focus = element
28 | end
29 |
30 | # Delay, in ms, before a tool-tip will appear.
31 | def tool_tip_delay
32 | 500 # TODO: configure this.
33 | end
34 |
35 | # Show a file_dialog.
36 | # (see FileDialog#initialize)
37 | def file_dialog(type, options = {}, &block)
38 | FileDialog.new(type, options, &block)
39 | end
40 |
41 | # (see MenuPane#initialize)
42 | def menu(options = {}, &block); MenuPane.new(options, &block); end
43 |
44 | # (see MessageDialog#initialize)
45 | def message(text, options = {}, &block); MessageDialog.new(text, options, &block); end
46 |
47 | # (see Container#clear)
48 | def clear(*args, &block); @container.clear(*args, &block); end
49 |
50 | def initialize
51 | # The container is where the user puts their content.
52 | @container = MainPacker.new
53 | @menu = nil
54 | @last_cursor_pos = [-1, -1]
55 | @mouse_over = nil
56 |
57 | unless defined? @@draw_pixel
58 | media_dir = File.expand_path(File.join(File.dirname(__FILE__), '..', '..', '..', 'media'))
59 | Gosu::Image.autoload_dirs << File.join(media_dir, 'images')
60 | Gosu::Sample.autoload_dirs << File.join(media_dir, 'sounds')
61 |
62 | @@draw_pixel = Gosu::Image.new($window, File.join(media_dir, 'images', PIXEL_IMAGE), true) # Must be tileable or it will blur.
63 | @@cursor = Cursor.new
64 | end
65 |
66 | @min_drag_distance = 0
67 |
68 | super()
69 |
70 | add_inputs(
71 | left_mouse_button: ->{ redirect_mouse_button(:left) },
72 | holding_left_mouse_button: ->{ redirect_holding_mouse_button(:left) },
73 | released_left_mouse_button: ->{ redirect_released_mouse_button(:left) },
74 |
75 | middle_mouse_button: ->{ redirect_mouse_button(:middle) },
76 | holding_middle_mouse_button: ->{ redirect_holding_mouse_button(:middle) },
77 | released_middle_mouse_button: ->{ redirect_released_mouse_button(:middle) },
78 |
79 | right_mouse_button: ->{ redirect_mouse_button(:right) },
80 | holding_right_mouse_button: ->{ redirect_holding_mouse_button(:right) },
81 | released_right_mouse_button: ->{ redirect_released_mouse_button(:right) },
82 |
83 | mouse_wheel_up: :redirect_mouse_wheel_up,
84 | mouse_wheel_down: :redirect_mouse_wheel_down,
85 |
86 | x: -> { if @focus and (holding_any?(:left_control, :right_control)) then @focus.cut end },
87 | c: -> { if @focus and (holding_any?(:left_control, :right_control)) then @focus.copy end },
88 | v: -> { if @focus and (holding_any?(:left_control, :right_control)) then @focus.paste end }
89 | )
90 | end
91 |
92 | # Internationalisation helper.
93 | def t(*args); I18n.t(*args); end
94 |
95 | # Clear the data which is specific to the current $window.
96 | def self.clear
97 | remove_class_variable '@@cursor' if defined? @@cursor
98 | remove_class_variable '@@draw_pixel' if defined? @@draw_pixel
99 | end
100 |
101 | def update
102 | cursor.update
103 | @tool_tip.update if @tool_tip
104 | @menu.update if @menu
105 | @container.update
106 |
107 | # Check menu first, then other elements.
108 | new_mouse_over = @menu.hit_element(cursor.x, cursor.y) if @menu
109 | new_mouse_over = @container.hit_element(cursor.x, cursor.y) unless new_mouse_over
110 |
111 | if new_mouse_over
112 | new_mouse_over.publish :enter if new_mouse_over != @mouse_over
113 | new_mouse_over.publish :hover, cursor.x, cursor.y
114 | end
115 |
116 | @mouse_over.publish :leave if @mouse_over and new_mouse_over != @mouse_over
117 |
118 | @mouse_over = new_mouse_over
119 |
120 | # Check if the mouse has moved, and no menu is shown, so we can show a tooltip.
121 | if [cursor.x, cursor.y] == @last_cursor_pos and (not @menu)
122 | if @mouse_over and (Gosu::milliseconds - @mouse_moved_at) > tool_tip_delay
123 | if text = @mouse_over.tip and not text.empty?
124 | @tool_tip ||= ToolTip.new
125 | @tool_tip.text = text
126 | @tool_tip.x = cursor.x
127 | @tool_tip.y = cursor.y + cursor.height # Place the tip beneath the cursor.
128 | else
129 | @tool_tip = nil
130 | @mouse_moved_at = Gosu::milliseconds
131 | end
132 | end
133 | else
134 | @tool_tip = nil
135 | @mouse_moved_at = Gosu::milliseconds
136 | end
137 |
138 | # The element that grabs input.
139 | @active_element = @dragging_element || @focus || @mouse_over
140 |
141 | @last_cursor_pos = [cursor.x, cursor.y]
142 |
143 | super
144 | end
145 |
146 | def write_tree
147 | puts "=== #{self.class} ==="
148 | indent = " "
149 | @container.write_tree(indent)
150 | @menu.write_tree(indent) if @menu
151 | @tool_tip.write_tree(indent) if @tool_tip
152 | end
153 |
154 | def draw
155 | @container.draw
156 | @menu.draw if @menu
157 | @tool_tip.draw if @tool_tip
158 | cursor.draw
159 |
160 | super
161 | end
162 |
163 | def setup
164 | super
165 |
166 | @tool_tip = nil
167 | @mouse_over = nil # Element the mouse is hovering over.
168 | @mouse_down_on = Hash.new # Element that each button was pressed over.
169 | @mouse_down_pos = Hash.new # Position that each button was pressed down at.
170 | @drag_button = nil
171 | @dragging_element = nil
172 | @focus = nil
173 | @mouse_moved_at = Gosu::milliseconds
174 |
175 | nil
176 | end
177 |
178 | def finalize
179 | unset_mouse_over
180 |
181 | if @focus
182 | @focus.publish :blur
183 | @focus = nil
184 | end
185 |
186 | @tool_tip = nil
187 |
188 | nil
189 | end
190 |
191 | # Called by active elements when they are disabled.
192 | def unset_mouse_over
193 | @mouse_over.publish :leave if @mouse_over
194 | @mouse_over = nil
195 | end
196 |
197 | # Set the menu pane to be displayed.
198 | #
199 | # @param [MenuPane] menu Menu to display.
200 | # @return nil
201 | def show_menu(menu)
202 | hide_menu if @menu
203 | @menu = menu
204 |
205 | nil
206 | end
207 |
208 | # Hides the currently shown menu, if any.
209 | # @return nil
210 | def hide_menu
211 | @menu = nil
212 |
213 | nil
214 | end
215 |
216 | # Flush all pending drawing to the screen.
217 | def flush
218 | $window.flush
219 | end
220 |
221 | # Draw a filled rectangle.
222 | def draw_rect(x, y, width, height, z, color, mode = :default)
223 | @@draw_pixel.draw x, y, z, width, height, color, mode
224 |
225 | nil
226 | end
227 |
228 | # Draw an unfilled rectangle.
229 | def draw_frame(x, y, width, height, thickness, z, color, mode = :default)
230 | draw_rect(x - thickness, y, thickness, height, z, color, mode) # left
231 | draw_rect(x - thickness, y - thickness, width + thickness * 2, thickness, z, color, mode) # top (full)
232 | draw_rect(x + width, y, thickness, height, z, color, mode) # right
233 | draw_rect(x - thickness, y + height, width + thickness * 2, thickness, z, color, mode) # bottom (full)
234 |
235 | nil
236 | end
237 |
238 | def distance(x1, y1, x2, y2)
239 | Gosu.distance(x1, y1, x2, y2)
240 | end
241 |
242 | def show
243 | $window.game_state_manager.push self unless $window.game_state_manager.game_states.include? self
244 | nil
245 | end
246 |
247 | def hide
248 | $window.game_state_manager.pop if $window.game_state_manager.current == self
249 | nil
250 | end
251 |
252 | protected
253 | def redirect_mouse_button(button)
254 | # Ensure that if the user clicks away from a menu, it is automatically closed.
255 | hide_menu unless @menu and @menu == @mouse_over
256 |
257 | # Blur if clicking outside the focused element.
258 | if @focus and @mouse_over != @focus
259 | @focus.publish :blur
260 | @focus = nil
261 | end
262 |
263 | # Publish :left_mouse_button for the element that is clicked.
264 | if @mouse_over
265 | @mouse_down_pos[button] = [cursor.x, cursor.y]
266 | @mouse_down_on[button] = @mouse_over
267 | @mouse_over.publish :"#{button}_mouse_button", *@mouse_down_pos[button]
268 | else
269 | @mouse_down_pos[button] = nil
270 | @mouse_down_on[button] = nil
271 | end
272 |
273 | nil
274 | end
275 |
276 | protected
277 | def redirect_released_mouse_button(button)
278 | # Ensure that if the user clicks away from a menu, it is automatically closed.
279 | hide_menu if @menu and @mouse_over != @menu
280 |
281 | if @mouse_over
282 | @mouse_over.publish :"released_#{button}_mouse_button", cursor.x, cursor.y
283 | @mouse_over.publish :"clicked_#{button}_mouse_button", cursor.x, cursor.y if @mouse_over == @mouse_down_on[button]
284 | end
285 |
286 | if @dragging_element and @drag_button == button
287 | @dragging_element.publish :end_drag, cursor.x, cursor.y, @drag_button, @mouse_over
288 | @dragging_element = nil
289 | @drag_button = nil
290 | end
291 |
292 | @mouse_down_on[button] = nil
293 | @mouse_down_pos[button] = nil
294 |
295 | nil
296 | end
297 |
298 | protected
299 | def redirect_holding_mouse_button(button)
300 | if not @dragging_element and @mouse_down_on[button] and @mouse_down_on[button].drag?(button) and
301 | distance(*@mouse_down_pos[button], cursor.x, cursor.y) > @min_drag_distance
302 | @drag_button = button
303 | @dragging_element = @mouse_down_on[button]
304 | @dragging_element.publish :begin_drag, *@mouse_down_pos[button], :left
305 | end
306 |
307 | if @dragging_element
308 | if @drag_button == button
309 | @dragging_element.publish :update_drag, cursor.x, cursor.y
310 | end
311 | else
312 | @mouse_over.publish :"holding_#{button}_mouse_button", cursor.x, cursor.y if @mouse_over
313 | end
314 |
315 | nil
316 | end
317 |
318 | protected
319 | def redirect_mouse_wheel_up
320 | @active_element.publish :mouse_wheel_up, cursor.x, cursor.y if @active_element
321 | nil
322 | end
323 |
324 | protected
325 | def redirect_mouse_wheel_down
326 | @active_element.publish :mouse_wheel_down, cursor.x, cursor.y if @active_element
327 | nil
328 | end
329 | end
330 | end
--------------------------------------------------------------------------------
/lib/fidgit/elements/text_area.rb:
--------------------------------------------------------------------------------
1 | module Fidgit
2 | class TextArea < Element
3 | TAGS_PATTERN = %r%<[a-z](?:=[a-f0-9]+)?>|[a-z]>%i
4 |
5 | # @return [Number]
6 | attr_reader :min_height
7 | # @return [Number]
8 | attr_reader :max_height
9 |
10 | # @return [Number]
11 | attr_reader :line_spacing
12 |
13 | # @param [Boolean] value
14 | # @return [Boolean]
15 | attr_writer :editable
16 |
17 | # @return [String] Text, but stripped of tags.
18 | attr_reader :stripped_text
19 |
20 | event :begin_drag
21 | event :update_drag
22 | event :end_drag
23 |
24 | event :changed
25 | event :focus
26 | event :blur
27 |
28 | def drag?(button); button == :left; end
29 |
30 | # Is the area editable? This will always be false if the Element is disabled.
31 | def editable?
32 | enabled? and @editable
33 | end
34 |
35 | # Text within the element.
36 | # @return [String]
37 | def text
38 | @text_input.text.force_encoding '-8'
39 | end
40 |
41 | # Returns the range of the selection.
42 | #
43 | # @return [Range]
44 | def selection_range
45 | from = [@text_input.selection_start, caret_position].min
46 | to = [@text_input.selection_start, caret_position].max
47 |
48 | (from...to)
49 | end
50 |
51 | # Returns the text within the selection.
52 | #
53 | # @return [String]
54 | def selection_text
55 | stripped_text[selection_range]
56 | end
57 |
58 | # Sets the text within the selection. The caret will be placed at the end of the inserted text.
59 | #
60 | # @param [String] str Text to insert.
61 | # @return [String] The new selection text.
62 | def selection_text=(str)
63 | from = [@text_input.selection_start, @text_input.caret_pos].min
64 | to = [@text_input.selection_start, @text_input.caret_pos].max
65 | new_length = str.length
66 |
67 | full_text = text
68 | tags_length_before = (0...from).inject(0) {|m, i| m + @tags[i].length }
69 | tags_length_inside = (from...to).inject(0) {|m, i| m + @tags[i].length }
70 | range = (selection_range.first + tags_length_before)...(selection_range.last + tags_length_before + tags_length_inside)
71 | full_text[range] = str.encode('-8', undef: :replace)
72 | @text_input.text = full_text
73 |
74 | @text_input.selection_start = @text_input.caret_pos = from + new_length
75 |
76 | recalc # This may roll back the text if it is too long!
77 |
78 | publish :changed, self.text
79 |
80 | str
81 | end
82 |
83 | # Position of the caret.
84 | #
85 | # @return [Integer] Number in range 0..text.length
86 | def caret_position
87 | @text_input.caret_pos
88 | end
89 |
90 | # Position of the caret.
91 | #
92 | # @param [Integer] pos Position of caret in the text.
93 | # @return [Integer] New position of caret.
94 | def caret_position=(position)
95 | raise ArgumentError, "Caret position must be in the range 0 to the length of the text (inclusive)" unless position.between?(0, stripped_text.length)
96 | @text_input.caret_pos = position
97 |
98 | position
99 | end
100 |
101 | # Sets caret to the end of the text.
102 | #
103 | # @param [String] text
104 | # @return [String] Current string (may be the old one if passed on was too long).
105 | def text=(text)
106 | @text_input.text = text
107 | recalc # This may roll back the text if it is too long.
108 | publish :changed, self.text
109 | self.text
110 | end
111 |
112 |
113 | # @param (see Element#initialize)
114 | #
115 | # @option (see Element#initialize)
116 | # @option options [String] :text ("")
117 | # @option options [Integer] :height Sets both min and max height at once.
118 | # @option options [Integer] :min_height
119 | # @option options [Integer] :max_height (Infinite)
120 | # @option options [Number] :line_spacing (0)
121 | # @option options [Boolean] :editable (true)
122 | def initialize(options = {}, &block)
123 | options = {
124 | text: '',
125 | max_height: Float::INFINITY,
126 | line_spacing: default(:line_spacing),
127 | background_color: default(:background_color),
128 | border_color: default(:border_color),
129 | caret_color: default(:caret_color),
130 | caret_period: default(:caret_period),
131 | focused_border_color: default(:focused, :border_color),
132 | selection_color: default(:selection_color),
133 | editable: true,
134 | }.merge! options
135 |
136 | @line_spacing = options[:line_spacing]
137 | @caret_color = options[:caret_color].dup
138 | @caret_period = options[:caret_period]
139 | @focused_border_color = options[:focused_border_color].dup
140 | @selection_color = options[:selection_color].dup
141 | @editable = options[:editable]
142 |
143 | @lines = [''] # List of lines of wrapped text.
144 | @caret_positions = [[0, 0]] # [x, y] of each position the caret can be in.
145 | @char_widths = [] # Width of each character in the text.
146 | @text_input = Gosu::TextInput.new
147 | @old_text = ''
148 | @old_caret_position = 0
149 | @old_selection_start = 0
150 | @tags = Hash.new("") # Hash of tags embedded in the text.
151 |
152 | @text_input.text = options[:text].dup
153 | @stripped_text = '' # Text stripped of xml tags.
154 |
155 | super(options)
156 |
157 | min_height = padding_left + padding_right + font.height
158 | if options[:height]
159 | @max_height = @min_height = [options[:height], min_height].max
160 | else
161 | @max_height = [options[:max_height], min_height].max
162 | @min_height = options[:min_height] ? [options[:min_height], min_height].max : min_height
163 | end
164 | rect.height = [padding_left + padding_right + font.height, @min_height].max
165 |
166 | subscribe :left_mouse_button, method(:click_in_text)
167 | subscribe :right_mouse_button, method(:click_in_text)
168 |
169 | # Handle dragging.
170 | subscribe :begin_drag do |sender, x, y|
171 | # Store position of the handle when it starts to drag.
172 | @drag_start_pos = [x - self.x, y - self.y]
173 | end
174 |
175 | subscribe :update_drag do |sender, x, y|
176 | index = text_index_at_position(x, y)
177 | self.caret_position = [index, @stripped_text.length].min if index
178 | end
179 |
180 | subscribe :end_drag do
181 | @drag_start_pos = nil
182 | end
183 | end
184 |
185 | # @return [nil]
186 | def click_in_text(sender, x, y)
187 | publish :focus unless focused?
188 |
189 | # Move caret to position the user clicks on.
190 | index = text_index_at_position x, y
191 | self.caret_position = @text_input.selection_start = [index, @stripped_text.length].min if index
192 |
193 | nil
194 | end
195 |
196 | # Does the element have the focus?
197 | def focused?; @focused; end
198 |
199 | # @return [nil]
200 | def focus(sender)
201 | @focused = true
202 | $window.current_game_state.focus = self
203 | $window.text_input = @text_input
204 |
205 | nil
206 | end
207 |
208 | # @return [nil]
209 | def blur(sender)
210 | if focused?
211 | $window.current_game_state.focus = nil
212 | $window.text_input = nil
213 | end
214 |
215 | @focused = false
216 |
217 | nil
218 | end
219 |
220 | # Draw the text area.
221 | #
222 | # @return [nil]
223 | def draw_foreground
224 | # Always roll back changes made by the user unless the text is editable.
225 | if editable? or text == @old_text
226 | recalc if focused? # Workaround for Windows draw/update bug.
227 | @old_caret_position = caret_position
228 | @old_selection_start = @text_input.selection_start
229 | else
230 | roll_back
231 | end
232 |
233 | if caret_position > stripped_text.length
234 | self.caret_position = stripped_text.length
235 | end
236 |
237 | if @text_input.selection_start >= stripped_text.length
238 | @text_input.selection_start = stripped_text.length
239 | end
240 |
241 | # Draw the selection.
242 | selection_range.each do |pos|
243 | char_x, char_y = @caret_positions[pos]
244 | char_width = @char_widths[pos]
245 | left, top = x + padding_left + char_x, y + padding_top + char_y
246 | draw_rect left, top, char_width, font.height, z, @selection_color
247 | end
248 |
249 | # Draw text.
250 | @lines.each_with_index do |line, index|
251 | font.draw(line, x + padding_left, y + padding_top + y_at_line(index), z)
252 | end
253 |
254 | # Draw the caret.
255 | if focused? and ((Gosu::milliseconds / @caret_period) % 2 == 0)
256 | caret_x, caret_y = @caret_positions[caret_position]
257 | left, top = x + padding_left + caret_x, y + padding_top + caret_y
258 | draw_rect left, top, 1, font.height, z, @caret_color
259 | end
260 | end
261 |
262 | protected
263 | # Index of character in reference to the displayable text.
264 | def text_index_at_position(x, y)
265 | # Move caret to position the user clicks on.
266 | mouse_x, mouse_y = x - (self.x + padding_left), y - (self.y + padding_top)
267 | @char_widths.each.with_index do |width, i|
268 | char_x, char_y = @caret_positions[i]
269 | if mouse_x.between?(char_x, char_x + width) and mouse_y.between?(char_y, char_y + font.height)
270 | return i
271 | end
272 | end
273 |
274 | nil # Didn't find a character at that position.
275 | end
276 |
277 | # y position of the
278 | protected
279 | def y_at_line(lines_number)
280 | lines_number * (font.height + line_spacing)
281 | end
282 |
283 |
284 | protected
285 | # Helper for #recalc
286 | # @return [Integer]
287 | def position_letters_in_word(word, line_width)
288 | # Strip tags before measuring word.
289 | word.gsub(ENTITIES_AND_TAGS_PATTERN, '').each_char do |c|
290 | char_width = font.text_width(c)
291 | line_width += char_width
292 | @caret_positions.push [line_width, y_at_line(@lines.size)]
293 | @char_widths.push char_width
294 | end
295 |
296 | line_width
297 | end
298 |
299 | protected
300 | # @return [nil]
301 | def layout
302 | # Don't need to re-layout if the text hasn't changed.
303 | return if @old_text == text
304 |
305 | publish :changed, self.text
306 |
307 | # Save these in case we are too long.
308 | old_lines = @lines
309 | old_caret_positions = @caret_positions
310 | old_char_widths = @char_widths
311 |
312 | @lines = []
313 | @caret_positions = [[0, 0]] # Position 0 is before the first character.
314 | @char_widths = []
315 |
316 | space_width = font.text_width ' '
317 | max_width = width - padding_left - padding_right - space_width
318 |
319 | line = ''
320 | line_width = 0
321 | word = ''
322 | word_width = 0
323 |
324 | strip_tags
325 |
326 | stripped_text.each_char.with_index do |char, i|
327 | tag = @tags[i]
328 |
329 | case char
330 | when "\n"
331 | char_width = 0
332 | else
333 | char_width = font.text_width char
334 | end
335 |
336 | overall_width = line_width + (line_width == 0 ? 0 : space_width) + word_width + char_width
337 | if overall_width > max_width and not (char == ' ' and not word.empty?)
338 | if line.empty?
339 | # The current word is longer than the whole word, so split it.
340 | # Go back and set all the character positions we have.
341 | position_letters_in_word(word, line_width)
342 |
343 | # Push as much of the current word as possible as a complete line.
344 | @lines.push word + tag + (char == ' ' ? '' : '-')
345 | line_width = font.text_width(word)
346 |
347 | word = ''
348 | word_width = 0
349 | else
350 |
351 | # Adding the current word would be too wide, so add the current line and start a new one.
352 | @lines.push line
353 | line = ''
354 | end
355 |
356 | widen_last_character line_width
357 | line_width = 0
358 | end
359 |
360 | case char
361 | when "\n"
362 | # A new-line ends the word and puts it on the line.
363 | line += word + tag
364 | line_width = position_letters_in_word(word, line_width)
365 | @caret_positions.push [line_width, y_at_line(@lines.size)]
366 | @char_widths.push 0
367 | widen_last_character line_width
368 | @lines.push line
369 | word = ''
370 | word_width = 0
371 | line = ''
372 | line_width = 0
373 |
374 | when ' '
375 | # A space ends a word and puts it on the line.
376 | line += word + tag + char
377 | line_width = position_letters_in_word(word, line_width)
378 | line_width += space_width
379 | @caret_positions.push [line_width, y_at_line(@lines.size)]
380 | @char_widths.push space_width
381 |
382 | word = ''
383 | word_width = 0
384 |
385 | else
386 | # If there was a previous line and we start a new line, put the caret pos on the current line.
387 | if line.empty?
388 | @caret_positions[-1] = [0, y_at_line(@lines.size)]
389 | end
390 |
391 | # Start building up a new word.
392 | word += tag + char
393 | word_width += char_width
394 | end
395 | end
396 |
397 | # Add any remaining word on the last line.
398 | unless word.empty?
399 | line_width = position_letters_in_word(word, line_width)
400 | @char_widths << width - line_width - padding_left - padding_right
401 | line += word
402 | end
403 |
404 | @lines.push line if @lines.empty? or not line.empty?
405 |
406 | # Roll back if the height is too long.
407 | new_height = padding_left + padding_right + y_at_line(@lines.size)
408 | if new_height <= max_height
409 | @old_text = text
410 | rect.height = [new_height, @min_height].max
411 | @old_caret_position = caret_position
412 | @old_selection_start = @text_input.selection_start
413 | else
414 | roll_back
415 | end
416 |
417 | nil
418 | end
419 |
420 | protected
421 | def roll_back
422 | @text_input.text = @old_text
423 | self.caret_position = @old_caret_position
424 | @text_input.selection_start = @old_selection_start
425 | recalc
426 | end
427 |
428 | protected
429 | def widen_last_character(line_width)
430 | @char_widths[-1] += (width - line_width - padding_left - padding_right) unless @char_widths.empty?
431 | end
432 |
433 | public
434 | # Cut the selection and copy it to the clipboard.
435 | def cut
436 | str = selection_text
437 | unless str.empty?
438 | Clipboard.copy str
439 | self.selection_text = '' if editable?
440 | end
441 | end
442 |
443 | public
444 | # Copy the selection to the clipboard.
445 | def copy
446 | str = selection_text
447 | Clipboard.copy str unless str.empty?
448 | end
449 |
450 | public
451 | # Paste the contents of the clipboard into the TextArea.
452 | def paste
453 | self.selection_text = Clipboard.paste
454 | end
455 |
456 | protected
457 | # Use block as an event handler.
458 | def post_init_block(&block)
459 | subscribe :changed, &block
460 | end
461 |
462 | protected
463 | # Strip XML tags ("")
464 | def strip_tags
465 | tags_length = 0
466 | @tags = Hash.new('')
467 |
468 | @stripped_text = text.gsub(TAGS_PATTERN) do |tag|
469 | pos = $`.length - tags_length
470 | tags_length += tag.length
471 | @tags[pos] += tag
472 | end
473 | end
474 | end
475 | end
--------------------------------------------------------------------------------