Compare commits

..

50 Commits

Author SHA1 Message Date
2cc1382696 Fixed Text border color not getting set correctly due to misnamed color parameter, generate Text border image with true white text since we draw it with the requested color later. 2026-09-20 16:10:10 -05:00
9fca64b643 Added render mode to BackgroundNineSlice 2026-09-19 19:10:39 -05:00
abe99ce949 Ignore sibling elements that aren't visible from fill mode space considerations 2026-09-17 12:25:48 -05:00
66fb136c83 Make elements sizing by like css' border-box box-sizing instead of like content-box for elements with specific width/height that isn't a ratio, tweaks to gui debug display, make F12 key toggle gui debug display for convenience 2026-09-11 13:05:08 -05:00
b23e5c2a42 Fix pending element recalculate requests apparently not getting processed properly... 2026-09-10 18:32:01 -05:00
747822aa61 Fixed flow containers not correctly positioning their children on the x axis when they wrap to the the next line, fix up containers vertical and horizontal alignment 2026-09-10 18:31:19 -05:00
140e7a3619 Make elements publish the :size_changed event when they change their size 2026-09-09 22:30:19 -05:00
349222f387 Flip flop Element#resolution_scaled 2026-09-04 10:31:30 -05:00
9d4bc89174 Fix Widget element unable to find children element tooltips if the widget's parent(s) had been scrolled 2026-09-04 10:31:02 -05:00
8e7c6f8a19 Many ux and api improvements 2026-05-02 18:20:17 -05:00
cd5bd51a02 Added EventBus, a simple global pub-sub event system. 2026-04-23 19:39:36 -05:00
adf517f374 Make Element#styled return 0 for border_thickness, margins, and paddings if the element is not Element#visible? 2026-04-21 13:19:02 -05:00
1c868522b7 Make Result return not okay if @data is nil 2026-04-21 12:12:16 -05:00
a79b9cc20e Added Widget, which acts like a Flow and a button. 2026-04-20 10:16:37 -05:00
c229329542 Send enter and hover events to mouse over element's parent containers- improves ux, disable applying :active styles if element does not have any specificied- fixes element blinking on click. 2026-04-18 12:29:04 -05:00
512d8a5c0f Added initial dialog support 2026-04-17 18:46:27 -05:00
d158d2fdd2 Added initial support for pages, fixed image not accounting for margin refactor 2026-04-17 17:45:58 -05:00
9f654129ef Improved handling of nine slice backgrounds, fixed scroll height, fixed container v/h_align 2026-04-17 11:38:17 -05:00
b694269409 Fixed ListBox/Menu not accounting for its border 2026-04-15 22:09:58 -05:00
3375d2d224 Fixed button image off center if button has margin left/top 2026-04-15 22:07:45 -05:00
a47505e7fa Improved debug_draw to be free of element clipping rectangle, fixed up container vertical and horizontal alignment 2026-04-15 22:06:55 -05:00
e572679f0b Renamed GuiState#menu to GuiState#active_menu to prevent conflict with DSL#menu 2026-04-15 22:02:09 -05:00
4286ca06b0 Fix up container's over estimating their width/height some more 2026-04-15 10:51:10 -05:00
a5636c82b3 Fixed Element's x and y position offset by their containers margin_left and/or margin_top, fixed containers 'inheriting' their parents and/or grandparents margin_left/top causing them to have an outer_height greater than it should be (needs a little more work.) 2026-04-13 20:00:24 -05:00
971ac5ff34 Lazy init Element background and border canvases 2026-03-27 11:20:03 -05:00
3b5902fef5 Reduce usages of Gosu.clip_to in Background 2026-03-27 11:18:55 -05:00
dfb9378c6d Remove unneeded string to integer conversions in Theme 2026-03-27 10:59:22 -05:00
3a23a8f8ca Improve clipping of TextBlock 2026-03-27 10:58:42 -05:00
d735edaec0 Make changing an element's styles trigger element to re-stylize 2026-03-21 22:42:57 -05:00
d2440b50a1 Added every and after timers to DSL 2026-03-21 21:11:51 -05:00
362fafa6fd Fixed HOME/END PAGE UP/DOWN keys incorrectly scrolling container when an EditLine or EditBox has focus 2026-03-21 21:11:51 -05:00
907257879c Fixed EditBox causing crash if parent container is scrolled and EditBox is clicked 2026-03-21 21:11:50 -05:00
e031039a77 Refactored GuiState Element style handling to simplify mutating them 2026-03-21 21:03:58 -05:00
16bd9168e4 Bump version 2026-01-28 18:02:18 -06:00
b946d5efa6 Added Result class to make writing failure resistant code easier 2026-01-28 18:02:18 -06:00
4ce6c1f499 Update README 2026-01-09 09:39:40 -06:00
958d4e65f9 Bump version 2026-01-09 09:36:08 -06:00
0519253e03 Improved GuiState to fully recalculate before returning from draw 2026-01-09 09:29:41 -06:00
97055885a6 Cache TextBlock text width and height 2026-01-09 08:59:06 -06:00
b5912de980 Remove unused code from Background#update, improves performance notably. 2026-01-09 08:34:59 -06:00
b0376d85d9 Reduce allocations in Background 2026-01-09 08:26:55 -06:00
a30d66fafb Refactored Style to remove usage of method_missing 2026-01-09 08:20:27 -06:00
0fac4a0397 Add StackProf hooks for profile recalculate 2026-01-08 22:16:54 -06:00
498cf06916 Style fixes, wip layout speed up work 2025-12-01 10:03:45 -06:00
1f57dfd38c Update to support Gosu 2.0.0 (Gosu::Image taking in a Gosu::Image fails) 2025-11-29 11:27:27 -06:00
76a8bf95c7 Update Gosu.draw_arc to support partial segments 2025-11-29 11:11:53 -06:00
1c25eeb32b Bump version 2025-06-24 13:55:40 -05:00
eabad4abd4 Refactored mesh handling, imported AABB tree implementation from I-MIC FPS 2025-05-03 18:21:11 -05:00
b3561f02c1 Fixed edit_line's with prefilled values having an offset_x that hides the text unless caret is manually moved left 2024-04-09 09:27:12 -05:00
9694cc2270 Fix compatibility issue with gosu 2.0 pre-release 2024-03-12 19:20:25 -05:00
49 changed files with 1279 additions and 489 deletions

25
Gemfile.lock Normal file
View File

@@ -0,0 +1,25 @@
PATH
remote: .
specs:
cyberarm_engine (0.24.5)
gosu (~> 1.1)
GEM
remote: https://rubygems.org/
specs:
gosu (1.4.6)
minitest (5.25.5)
rake (13.2.1)
PLATFORMS
x64-mingw-ucrt
x86_64-linux
DEPENDENCIES
bundler (~> 2.2)
cyberarm_engine!
minitest (~> 5.0)
rake (~> 13.0)
BUNDLED WITH
2.6.8

View File

@@ -35,7 +35,7 @@ class Hello < CyberarmEngine::GuiState
background Gosu::Color::GRAY
stack do
label "Hello World!"
banner "Hello World!"
button "close" do
window.close

View File

@@ -1,14 +1,15 @@
#version 330 core
@include "light_struct"
out vec4 frag_color;
@include "light_struct"
const int DIRECTIONAL = 0;
const int POINT = 1;
const int SPOT = 2;
flat in Light out_lights[7];
in vec2 out_tex_coords;
flat in int out_light_count;
flat in Light out_lights[7];
uniform sampler2D diffuse, position, texcoord, normal, depth;
@@ -27,43 +28,88 @@ vec4 directionalLight(Light light) {
return vec4(_diffuse + _ambient + _specular, 1.0);
}
vec4 pointLight(Light light) {
return vec4(0.25, 0.25, 0.25, 1);
}
vec4 spotLight(Light light) {
return vec4(0.5, 0.5, 0.5, 1);
}
vec4 calculateLighting(Light light) {
vec4 result;
// switch(light.type) {
// case DIRECTIONAL: {
// result = directionalLight(light);
// }
// case SPOT: {
// result = spotLight(light);
// }
// default: {
// result = pointLight(light);
// }
// }
if (light.type == DIRECTIONAL) {
result = directionalLight(light);
} else {
result = pointLight(light);
}
return result;
}
void main() {
frag_color = vec4(0.0);
Light light;
light.type = DIRECTIONAL;
for(int i = 0; i < out_light_count; i++)
{
frag_color += texture(diffuse, out_tex_coords) * calculateLighting(out_lights[i]);
}
light.position = vec3(100, 100, 100);
light.diffuse = vec3(0.5, 0.5, 0.5);
light.ambient = vec3(0.8, 0.8, 0.8);
light.specular = vec3(0.2, 0.2, 0.2);
light.intensity = 1.0;
frag_color = texture(diffuse, out_tex_coords) * directionalLight(light);
}
// #version 330 core
// @include "light_struct"
// out vec4 frag_color;
// const int DIRECTIONAL = 0;
// const int POINT = 1;
// const int SPOT = 2;
// in vec2 out_tex_coords;
// flat in int out_light_count;
// flat in Light out_lights[7];
// uniform sampler2D diffuse, position, texcoord, normal, depth;
// vec4 directionalLight(Light light) {
// vec3 norm = normalize(texture(normal, out_tex_coords).rgb);
// vec3 diffuse_color = texture(diffuse, out_tex_coords).rgb;
// vec3 frag_pos = texture(position, out_tex_coords).rgb;
// vec3 lightDir = normalize(light.position - frag_pos);
// float diff = max(dot(norm, lightDir), 0);
// vec3 _ambient = light.ambient;
// vec3 _diffuse = light.diffuse * diff;
// vec3 _specular = light.specular;
// return vec4(_diffuse + _ambient + _specular, 1.0);
// }
// vec4 pointLight(Light light) {
// return vec4(0.25, 0.25, 0.25, 1);
// }
// vec4 spotLight(Light light) {
// return vec4(0.5, 0.5, 0.5, 1);
// }
// vec4 calculateLighting(Light light) {
// vec4 result;
// // switch(light.type) {
// // case DIRECTIONAL: {
// // result = directionalLight(light);
// // }
// // case SPOT: {
// // result = spotLight(light);
// // }
// // default: {
// // result = pointLight(light);
// // }
// // }
// if (light.type == DIRECTIONAL) {
// result = directionalLight(light);
// } else {
// result = pointLight(light);
// }
// return result;
// }
// void main() {
// frag_color = vec4(0.0);
// for(int i = 0; i < out_light_count; i++)
// {
// frag_color += texture(diffuse, out_tex_coords) * calculateLighting(out_lights[i]);
// }
// }

View File

@@ -1,4 +1,4 @@
# version 330 core
#version 330 core
layout(location = 0) in vec3 in_position;
layout(location = 1) in vec3 in_color;

View File

@@ -9,6 +9,8 @@ require "json"
require_relative "cyberarm_engine/version"
require_relative "cyberarm_engine/stats"
require_relative "cyberarm_engine/result"
require_relative "cyberarm_engine/event_bus"
require_relative "cyberarm_engine/common"
@@ -57,6 +59,7 @@ require_relative "cyberarm_engine/ui/elements/image"
require_relative "cyberarm_engine/ui/elements/container"
require_relative "cyberarm_engine/ui/elements/flow"
require_relative "cyberarm_engine/ui/elements/stack"
require_relative "cyberarm_engine/ui/elements/widget"
require_relative "cyberarm_engine/ui/elements/check_box"
require_relative "cyberarm_engine/ui/elements/radio"
require_relative "cyberarm_engine/ui/elements/progress"
@@ -66,13 +69,13 @@ require_relative "cyberarm_engine/ui/elements/menu_item"
require_relative "cyberarm_engine/game_state"
require_relative "cyberarm_engine/ui/gui_state"
require_relative "cyberarm_engine/model"
require_relative "cyberarm_engine/model_cache"
require_relative "cyberarm_engine/model/material"
require_relative "cyberarm_engine/model/model_object"
require_relative "cyberarm_engine/model/parser"
require_relative "cyberarm_engine/model/parsers/wavefront_parser"
require_relative "cyberarm_engine/model/parsers/collada_parser" if RUBY_ENGINE != "mruby" && defined?(Nokogiri)
require_relative "cyberarm_engine/ui/dialog"
require_relative "cyberarm_engine/ui/page"
require_relative "cyberarm_engine/builtin/intro_state"
if RUBY_ENGINE != "mruby" && defined?(StackProf)
at_exit do
StackProf.results("./_stackprof.dmp")
end
end

View File

@@ -21,50 +21,36 @@ module CyberarmEngine
end
def draw
Gosu.clip_to(@x, @y, @width, @height) do
Gosu.draw_quad(
@top_left.x, @top_left.y, @paint.top_left,
@top_right.x, @top_right.y, @paint.top_right,
@bottom_right.x, @bottom_right.y, @paint.bottom_right,
@bottom_left.x, @bottom_left.y, @paint.bottom_left,
@z
)
if @angle.zero?
render
else
Gosu.clip_to(@x, @y, @width, @height) do
render
end
end
debug_outline if @debug
end
def render
Gosu.draw_quad(
@top_left.x, @top_left.y, @paint.top_left,
@top_right.x, @top_right.y, @paint.top_right,
@bottom_right.x, @bottom_right.y, @paint.bottom_right,
@bottom_left.x, @bottom_left.y, @paint.bottom_left,
@z
)
end
def update
origin_x = (@x + (@width / 2))
origin_y = (@y + (@height / 2))
points = [
@top_left = Vector.new(@x, @y),
@top_right = Vector.new(@x + @width, @y),
@bottom_left = Vector.new(@x, @y + @height),
@bottom_right = Vector.new(@x + @width, @y + @height)
]
[@top_left, @top_right, @bottom_left, @bottom_right].each do |vector|
temp_x = vector.x - origin_x
temp_y = vector.y - origin_y
# 90 is up here, while gosu uses 0 for up.
radians = (@angle + 90).gosu_to_radians
vector.x = (@x + (@width / 2)) + ((temp_x * Math.cos(radians)) - (temp_y * Math.sin(radians)))
vector.y = (@y + (@height / 2)) + ((temp_x * Math.sin(radians)) + (temp_y * Math.cos(radians)))
end
# [
# [:top, @top_left, @top_right],
# [:right, @top_right, @bottom_right],
# [:bottom, @bottom_right, @bottom_left],
# [:left, @bottom_left, @top_left]
# ].each do |edge|
# points.each do |point|
# puts "#{edge.first} -> #{shortest_distance(point, edge[1], edge[2])}"
# end
# end
@top_left.x = @x
@top_left.y = @y
@top_right.x = @x + @width
@top_right.y = @y
@bottom_left.x = @x
@bottom_left.y = @y + @height
@bottom_right.x = @x + @width
@bottom_right.y = @y + @height
end
def shortest_distance(point, la, lb)
@@ -168,12 +154,3 @@ module CyberarmEngine
end
end
end
# Add <=> method to support Range based gradients
module Gosu
class Color
def <=>(_other)
self
end
end
end

View File

@@ -1,10 +1,13 @@
module CyberarmEngine
class BackgroundNineSlice
include CyberarmEngine::Common
attr_accessor :x, :y, :z, :width, :height, :left, :top, :right, :bottom, :mode, :color
attr_reader :image
def initialize(image_path: nil, x: 0, y: 0, z: 0, width: 0, height: 0, mode: :tiled, left: 1, top: 1, right: 1, bottom: 1, color: Gosu::Color::WHITE)
CACHE = {}
attr_accessor :x, :y, :z, :width, :height, :mode, :color, :render_mode
attr_reader :image, :left, :top, :right, :bottom
def initialize(image_path: nil, x: 0, y: 0, z: 0, width: 0, height: 0, mode: :tiled, left: 1, top: 1, right: 1, bottom: 1, color: Gosu::Color::WHITE, render_mode: :default)
@image = get_image(image_path) if image_path
@x = x
@@ -22,6 +25,7 @@ module CyberarmEngine
@bottom = bottom
@color = color
@render_mode = render_mode
nine_slice if @image
end
@@ -32,8 +36,43 @@ module CyberarmEngine
nine_slice if @image && old_image != @image
end
def set_edges(left: @left, right: @right, top: @top, bottom: @bottom)
changed = [left == @left, right == @right, top == @top, bottom == @bottom].any?(false)
@left = left
@top = top
@right = right
@bottom = bottom
nine_slice if changed
end
def left=(n)
nine_slice if n != @left
@left = n
n
end
def right=(n)
nine_slice if n != @right
@right = n
n
end
def top=(n)
nine_slice if n != @top
@top = n
n
end
def bottom=(n)
nine_slice if n != @bottom
@bottom = n
n
end
def nine_slice
# pp [@left, @top, @right, @bottom, @image.width]
# pp [@left, @top, @right, @bottom, @image.width, @image.height]
@segment_top_left = @image.subimage(0, 0, @left, @top)
@segment_top_right = @image.subimage(@image.width - @right, 0, @right, @top)
@@ -83,57 +122,58 @@ module CyberarmEngine
end
def draw_stretched
@segment_top_left.draw(@x, @y, @z, 1, 1, @color)
@segment_top.draw(@x + @segment_top_left.width, @y, @z, width_scale, 1, @color) # SCALE X
@segment_top_right.draw((@x + @width) - @segment_top_right.width, @y, @z, 1, 1, @color)
@segment_top_left.draw(@x, @y, @z, 1, 1, @color, @render_mode)
@segment_top.draw(@x + @segment_top_left.width, @y, @z, width_scale, 1, @color, @render_mode) # SCALE X
@segment_top_right.draw((@x + @width) - @segment_top_right.width, @y, @z, 1, 1, @color, @render_mode)
@segment_right.draw((@x + @width) - @segment_right.width, @y + @top, @z, 1, height_scale, @color) # SCALE Y
@segment_bottom_right.draw((@x + @width) - @segment_bottom_right.width, @y + @height - @segment_bottom_right.height, @z, 1, 1, @color)
@segment_bottom.draw(@x + @segment_bottom_left.width, (@y + @height) - @segment_bottom.height, @z, width_scale, 1, @color) # SCALE X
@segment_bottom_left.draw(@x, (@y + @height) - @segment_bottom_left.height, @z, 1, 1, @color)
@segment_left.draw(@x, @y + @top, @z, 1, height_scale, @color) # SCALE Y
@segment_middle.draw(@x + @segment_top_left.width, @y + @segment_top.height, @z, width_scale, height_scale, @color) # SCALE X and SCALE Y
@segment_right.draw((@x + @width) - @segment_right.width, @y + @top, @z, 1, height_scale, @color, @render_mode) # SCALE Y
@segment_bottom_right.draw((@x + @width) - @segment_bottom_right.width, @y + @height - @segment_bottom_right.height, @z, 1, 1, @color, @render_mode)
@segment_bottom.draw(@x + @segment_bottom_left.width, (@y + @height) - @segment_bottom.height, @z, width_scale, 1, @color, @render_mode) # SCALE X
@segment_bottom_left.draw(@x, (@y + @height) - @segment_bottom_left.height, @z, 1, 1, @color, @render_mode)
@segment_left.draw(@x, @y + @top, @z, 1, height_scale, @color, @render_mode) # SCALE Y
@segment_middle.draw(@x + @segment_top_left.width, @y + @segment_top.height, @z, width_scale, height_scale, @color, @render_mode) # SCALE X and SCALE Y
end
def draw_tiled
@segment_top_left.draw(@x, @y, @z, 1, 1, @color)
@segment_top_left.draw(@x, @y, @z, 1, 1, @color, @render_mode)
# p [width_scale, height_scale]
Gosu.clip_to(@x + @segment_top_left.width, @y, @segment_top.width * width_scale, @segment_top.height) do
width_scale.ceil.times do |i|
@segment_top.draw(@x + @segment_top_left.width + (@segment_top.width * i), @y, @z, 1, 1, @color) # SCALE X
@segment_top.draw(@x + @segment_top_left.width + (@segment_top.width * i), @y, @z, 1, 1, @color, @render_mode) # SCALE X
end
end
@segment_top_right.draw((@x + @width) - @segment_top_right.width, @y, @z, 1, 1, @color)
@segment_top_right.draw((@x + @width) - @segment_top_right.width, @y, @z, 1, 1, @color, @render_mode)
Gosu.clip_to(@x + @width - @segment_top_right.width, @y + @top, @segment_right.width, @segment_right.height * height_scale) do
height_scale.ceil.times do |i|
@segment_right.draw((@x + @width) - @segment_right.width, @y + @top + (@segment_right.height * i), @z, 1, 1, @color) # SCALE Y
@segment_right.draw((@x + @width) - @segment_right.width, @y + @top + (@segment_right.height * i), @z, 1, 1, @color, @render_mode) # SCALE Y
end
end
@segment_bottom_right.draw((@x + @width) - @segment_bottom_right.width, @y + @height - @segment_bottom_right.height, @z, 1, 1, @color)
@segment_bottom_right.draw((@x + @width) - @segment_bottom_right.width, @y + @height - @segment_bottom_right.height, @z, 1, 1, @color, @render_mode)
Gosu.clip_to(@x + @segment_top_left.width, @y + @height - @segment_bottom.height, @segment_top.width * width_scale, @segment_bottom.height) do
width_scale.ceil.times do |i|
@segment_bottom.draw(@x + @segment_bottom_left.width + (@segment_bottom.width * i), (@y + @height) - @segment_bottom.height, @z, 1, 1, @color) # SCALE X
@segment_bottom.draw(@x + @segment_bottom_left.width + (@segment_bottom.width * i), (@y + @height) - @segment_bottom.height, @z, 1, 1, @color, @render_mode) # SCALE X
end
end
@segment_bottom_left.draw(@x, (@y + @height) - @segment_bottom_left.height, @z, 1, 1, @color)
@segment_bottom_left.draw(@x, (@y + @height) - @segment_bottom_left.height, @z, 1, 1, @color, @render_mode)
Gosu.clip_to(@x, @y + @top, @segment_left.width, @segment_left.height * height_scale) do
height_scale.ceil.times do |i|
@segment_left.draw(@x, @y + @top + (@segment_left.height * i), @z, 1, 1, @color) # SCALE Y
@segment_left.draw(@x, @y + @top + (@segment_left.height * i), @z, 1, 1, @color, @render_mode) # SCALE Y
end
end
Gosu.clip_to(@x + @segment_top_left.width, @y + @segment_top.height, @width - (@segment_left.width + @segment_right.width), @height - (@segment_top.height + @segment_bottom.height)) do
height_scale.ceil.times do |y|
width_scale.ceil.times do |x|
@segment_middle.draw(@x + @segment_top_left.width + (@segment_middle.width * x), @y + @segment_top.height + (@segment_middle.height * y), @z, 1, 1, @color) # SCALE X and SCALE Y
@segment_middle.draw(@x + @segment_top_left.width + (@segment_middle.width * x), @y + @segment_top.height + (@segment_middle.height * y), @z, 1, 1, @color, @render_mode) # SCALE X and SCALE Y
end
end
end

View File

@@ -1,5 +1,9 @@
module CyberarmEngine
GUI_DEBUG = false
module Common
ImageBlob = Data.define(:to_blob, :columns, :rows)
def push_state(klass, options = {})
window.push_state(klass, options)
end
@@ -85,7 +89,8 @@ module CyberarmEngine
unless asset
instance = nil
instance = if klass == Gosu::Image
klass.new(path, retro: retro, tileable: tileable)
path_or_blob = path.is_a?(String) ? path : ImageBlob.new(path.to_blob, path.width, path.height)
klass.new(path_or_blob, retro: retro, tileable: tileable)
else
klass.new(path)
end

View File

@@ -0,0 +1,39 @@
module CyberarmEngine
class EventBus
Subscriber = Data.define(:event, :listener, :method, :callback)
@subscribers = []
def self.subscribe(event, listener = nil, method = nil, &block)
raise "Subscriber cannot have both a method AND a callback block" if method && block_given?
subscriber = Subscriber.new(event, listener, method, block)
@subscribers << subscriber
subscriber
end
def self.unsubscribe(event, listener_or_subscriber)
@subscribers.delete_if { |s| s.event == event && (s == listener_or_subscriber || s.listener == listener_or_subscriber) }
end
def self.unsubscribe!(event)
@subscribers.delete_if { |s| s.event == event }
end
def self.announce(event, *payload)
subscribers = @subscribers.select { |s| s.event == event }
subscribers.each do |subscriber|
subscriber&.listener&.send(subscriber.method, *payload) if subscriber&.method
subscriber.callback&.call(*payload)
rescue StandardError => e
# TODO
warn "Failed to deliver event (#{event}) to listener (#{subscriber.inspect}): #{e}"
end
end
def self.publish(*args)
announce(*args)
end
end
end

View File

@@ -3,11 +3,12 @@ module CyberarmEngine
include Common
attr_accessor :options, :global_pause
attr_reader :game_objects
attr_reader :game_objects, :timers
def initialize(options = {})
@options = options
@game_objects = []
@timers = []
@global_pause = false
window.text_input = nil unless options[:preserve_text_input]
@@ -28,6 +29,8 @@ module CyberarmEngine
def update
@game_objects.each(&:update)
@timers.each(&:update)
@timers.delete_if(&:dead?)
end
def needs_redraw?
@@ -120,5 +123,9 @@ module CyberarmEngine
def add_game_object(object)
@game_objects << object
end
def add_timer(timer)
@timers << timer
end
end
end

View File

@@ -33,12 +33,23 @@ module Gosu
#
# @return [void]
def self.draw_arc(x, y, radius, percentage = 1.0, segments = 128, thickness = 4, color = Gosu::Color::WHITE, z = 0, mode = :default)
segments = 360.0 / segments
return if percentage == 0.0
0.step((359 * percentage), percentage > 0 ? segments : -segments) do |angle|
angle2 = angle + segments
angle_per_segment = 360.0 / segments
arc_completion = 360 * percentage
next_segment_angle = angle_per_segment
angle = 0
loop do
break if angle >= arc_completion
if angle + angle_per_segment > arc_completion
next_segment_angle = arc_completion - angle
else
next_segment_angle = angle_per_segment
end
angle2 = angle + next_segment_angle
point_a_left_x = x + Gosu.offset_x(angle, radius - thickness)
point_a_left_y = y + Gosu.offset_y(angle, radius - thickness)
@@ -93,6 +104,8 @@ module Gosu
z, mode
)
end
angle += next_segment_angle
end
end
end
end

View File

@@ -1,5 +1,8 @@
module CyberarmEngine
class Model
include OpenGL
include CyberarmEngine
attr_accessor :objects, :materials, :vertices, :uvs, :texures, :normals, :faces, :colors, :bones, :material_file,
:current_material, :current_object, :vertex_count, :smoothing
attr_reader :position, :bounding_box, :textured_material, :file_path, :positions_buffer_id, :colors_buffer_id,
@@ -49,9 +52,9 @@ module CyberarmEngine
@objects.each { |o| @vertex_count += o.vertices.size }
# start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
# build_collision_tree
# puts " Building mesh collision tree took #{((Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - start_time) / 1000.0).round(2)} seconds"
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
build_collision_tree
puts " Building mesh collision tree took #{((Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - start_time) / 1000.0).round(2)} seconds"
end
def parse(parser)
@@ -178,7 +181,7 @@ module CyberarmEngine
end
def build_collision_tree
@aabb_tree = IMICFPS::AABBTree.new
@aabb_tree = AABBTree.new
@faces.each do |face|
box = BoundingBox.new

View File

@@ -1,6 +1,6 @@
module CyberarmEngine
class Model
class ModelObject
class Mesh
attr_reader :id, :name, :vertices, :uvs, :normals, :materials, :bounding_box, :debug_color
attr_accessor :faces, :scale

View File

@@ -48,12 +48,12 @@ module CyberarmEngine
if _model
@model.current_object = _model
else
raise "Couldn't find ModelObject!"
raise "Couldn't find Mesh!"
end
end
def change_object(id, name)
@model.objects << Model::ModelObject.new(id, name)
@model.objects << Model::Mesh.new(id, name)
@model.current_object = @model.objects.last
end

View File

@@ -1,5 +1,6 @@
begin
require "opengl"
require "glu"
rescue LoadError
puts "Required gem is not installed, please install 'opengl-bindings' and try again."
exit(1)
@@ -8,7 +9,7 @@ end
module CyberarmEngine
def gl_error?
e = glGetError
if e != GL_NO_ERROR
if e != OpenGL::GL_NO_ERROR
warn "OpenGL error detected by handler at: #{caller[0]}"
warn " #{gluErrorString(e)} (#{e})\n"
exit if Window.instance&.exit_on_opengl_error?
@@ -38,3 +39,15 @@ require_relative "opengl/renderer/g_buffer"
require_relative "opengl/renderer/bounding_box_renderer"
require_relative "opengl/renderer/opengl_renderer"
require_relative "opengl/renderer/renderer"
require_relative "trees/aabb_tree_debug"
require_relative "trees/aabb_node"
require_relative "trees/aabb_tree"
require_relative "model"
require_relative "model_cache"
require_relative "model/material"
require_relative "model/mesh"
require_relative "model/parser"
require_relative "model/parsers/wavefront_parser"
require_relative "model/parsers/collada_parser" if RUBY_ENGINE != "mruby" && defined?(Nokogiri)

View File

@@ -1,5 +1,7 @@
module CyberarmEngine
class Light
include OpenGL
DIRECTIONAL = 0
POINT = 1
SPOT = 2

View File

@@ -1,5 +1,8 @@
module CyberarmEngine
class PerspectiveCamera
include OpenGL
include GLU
attr_accessor :position, :orientation, :aspect_ratio, :field_of_view,
:min_view_distance, :max_view_distance

View File

@@ -1,5 +1,7 @@
module CyberarmEngine
class GBuffer
include OpenGL
attr_reader :screen_vbo, :vertices, :uvs
attr_reader :width, :height

View File

@@ -1,5 +1,8 @@
module CyberarmEngine
class OpenGLRenderer
include OpenGL
include CyberarmEngine
@@immediate_mode_warning = false
attr_accessor :show_wireframe

View File

@@ -2,6 +2,7 @@ module CyberarmEngine
# Ref: https://github.com/vaiorabbit/ruby-opengl/blob/master/sample/OrangeBook/brick.rb
class Shader
include OpenGL
@@shaders = {} # Cache for {Shader} instances
PREPROCESSOR_CHARACTER = "@".freeze # magic character for preprocessor phase of {Shader} compilation
@@ -298,6 +299,7 @@ module CyberarmEngine
# @see Shader.use Shader.use
def use(&block)
return unless compiled?
raise "Another shader is already in use! #{Shader.active_shader.name.inspect}" if Shader.active_shader
Shader.active_shader = self

View File

@@ -0,0 +1,19 @@
module CyberarmEngine
# result pattern
class Result
attr_accessor :error, :data
def initialize(data: nil, error: nil)
@data = data
@error = error
end
def okay?
!error?
end
def error?
@error || @data.nil?
end
end
end

View File

@@ -23,7 +23,7 @@ module CyberarmEngine
@color = Gosu::Color::WHITE
end
@mode = options[:mode] || :default
@alignment = options[:alignment] || nil
@alignment = options[:alignment] || :left
@border = options[:border]
@border = true if options[:border].nil?
@@ -39,17 +39,6 @@ module CyberarmEngine
@static = options[:static] || (options[:static].nil? || options[:static] == false ? false : true)
@textobject = check_cache(@size, @font)
if @alignment
case @alignment
when :left
@x = 0 + BUTTON_PADDING
when :center
@x = (CyberarmEngine::Window.instance.width / 2) - (@textobject.text_width(@text) / 2)
when :right
@x = CyberarmEngine::Window.instance.width - BUTTON_PADDING - @textobject.text_width(@text)
end
end
end
def check_cache(size, font_name)
@@ -113,6 +102,38 @@ module CyberarmEngine
invalidate_cache! if old_color != color
end
def alignment=(value)
invalidate_cache! if @alignment != value
@alignment = value
end
def shadow=(value)
invalidate_cache! if @shadow != value
@shadow = value
end
def shadow_color=(color)
old_color = @shadow_color
if color
@shadow_color = color.is_a?(Gosu::Color) ? color : Gosu::Color.new(color)
else
raise "color cannot be nil"
end
invalidate_cache! if old_color != color
end
def shadow_alpha=(value)
invalidate_cache! if @shadow_alpha != value
@shadow_alpha = value
end
def shadow_size=(value)
invalidate_cache! if @shadow_size != value
@shadow_size = value
end
def border=(boolean)
invalidate_cache! if @border != boolean
@border = boolean
@@ -128,9 +149,16 @@ module CyberarmEngine
@border_alpha = n
end
def border_color=(n)
invalidate_cache! if @border_color != n
@border_color = n
def border_color=(color)
old_color = @border_color
if color
@border_color = color.is_a?(Gosu::Color) ? color : Gosu::Color.new(color)
else
raise "color cannot be nil"
end
invalidate_cache! if old_color != color
end
def width(text = @text)
@@ -180,23 +208,23 @@ module CyberarmEngine
img = Gosu::Image.send(:"from_#{method.to_s.split("_").last}", @text, @size, font: @font)
@cached_text_border_image = Gosu.render((_width + (@border_size * 2)).ceil, (height + (@border_size * 2)).ceil) do
img.draw(-_x, 0, @z, @factor_x, @factor_y, @border_color, @mode)
img.draw(-_x, -_y, @z, @factor_x, @factor_y, @border_color, @mode)
img.draw(-_x, 0, @z, @factor_x, @factor_y, Gosu::Color::WHITE, @mode)
img.draw(-_x, -_y, @z, @factor_x, @factor_y, Gosu::Color::WHITE, @mode)
img.draw(0, -_y, @z, @factor_x, @factor_y, @border_color, @mode)
img.draw(_x, -_y, @z, @factor_x, @factor_y, @border_color, @mode)
img.draw(0, -_y, @z, @factor_x, @factor_y, Gosu::Color::WHITE, @mode)
img.draw(_x, -_y, @z, @factor_x, @factor_y, Gosu::Color::WHITE, @mode)
img.draw(_x, 0, @z, @factor_x, @factor_y, @border_color, @mode)
img.draw(_x, _y, @z, @factor_x, @factor_y, @border_color, @mode)
img.draw(_x, 0, @z, @factor_x, @factor_y, Gosu::Color::WHITE, @mode)
img.draw(_x, _y, @z, @factor_x, @factor_y, Gosu::Color::WHITE, @mode)
img.draw(0, _y, @z, @factor_x, @factor_y, @border_color, @mode)
img.draw(-_x, _y, @z, @factor_x, @factor_y, @border_color, @mode)
img.draw(0, _y, @z, @factor_x, @factor_y, Gosu::Color::WHITE, @mode)
img.draw(-_x, _y, @z, @factor_x, @factor_y, Gosu::Color::WHITE, @mode)
end
end
@cached_text_shadow_image ||= Gosu::Image.send(:"from_#{method.to_s.split("_").last}", @text, @size, font: @font) if @shadow
@cached_text_shadow_image ||= Gosu::Image.send(:"from_#{method.to_s.split("_").last}", @text, @size, font: @font, align: @alignment) if @shadow
@gosu_cached_text_image ||= Gosu::Image.send(:"from_#{method.to_s.split("_").last}", @text, @size, font: @font)
@gosu_cached_text_image ||= Gosu::Image.send(:"from_#{method.to_s.split("_").last}", @text, @size, font: @font, align: @alignment)
@cached_text_border_image.draw(@x, @y, @z, @factor_x, @factor_y, @border_color, @mode) if @border
@@ -228,7 +256,7 @@ module CyberarmEngine
@textobject.send(method, @text, _x + @border_size, _y + @border_size, @z, @factor_x, @factor_y, white, @mode)
end
@cached_text_border_image.draw(@x - @border_size, @y - @border_size, @z, @factor_x, @factor_y, border_color)
@cached_text_border_image.draw(@x - @border_size, @y - @border_size, @z, @factor_x, @factor_y, border_color, @mode)
end
if @shadow

View File

@@ -19,5 +19,9 @@ module CyberarmEngine
@block.call if @block
end
end
def dead?
@triggered && !@looping
end
end
end

View File

@@ -0,0 +1,126 @@
# frozen_string_literal: true
module CyberarmEngine
class AABBTree
class AABBNode
attr_accessor :bounding_box, :parent, :object
attr_reader :a, :b
def initialize(parent:, object:, bounding_box:)
@parent = parent
@object = object
@bounding_box = bounding_box
@a = nil
@b = nil
end
def a=(leaf)
@a = leaf
@a.parent = self
end
def b=(leaf)
@b = leaf
@b.parent = self
end
def leaf?
@object
end
def insert_subtree(leaf)
if leaf?
new_node = AABBNode.new(parent: nil, object: nil, bounding_box: @bounding_box.union(leaf.bounding_box))
new_node.a = self
new_node.b = leaf
new_node
else
cost_a = @a.bounding_box.volume + @b.bounding_box.union(leaf.bounding_box).volume
cost_b = @b.bounding_box.volume + @a.bounding_box.union(leaf.bounding_box).volume
if cost_a == cost_b
cost_a = @a.proximity(leaf)
cost_b = @b.proximity(leaf)
end
if cost_b < cost_a
self.b = @b.insert_subtree(leaf)
else
self.a = @a.insert_subtree(leaf)
end
@bounding_box = @bounding_box.union(leaf.bounding_box)
self
end
end
def search_subtree(collider, items = [])
if @bounding_box.intersect?(collider)
if leaf?
items << self
else
@a.search_subtree(collider, items)
@b.search_subtree(collider, items)
end
end
items
end
def remove_subtree(leaf)
if leaf
self
elsif leaf.parent == self
other_child = other(leaf)
other_child.parent = @parent
other_child
else
leaf.parent.disown_child(leaf)
self
end
end
def other(leaf)
@a == leaf ? @b : @a
end
def disown_child(leaf)
value = other(leaf)
raise "Can not replace child of a leaf!" if @parent.leaf?
raise "Node is not a child of parent!" unless leaf.child_of?(@parent)
if @parent.a == self
@parent.a = value
else
@parent.b = value
end
@parent.update_bounding_box
end
def child_of?(leaf)
self == leaf.a || self == leaf.b
end
def proximity(leaf)
(@bounding_box - leaf.bounding_box).sum.abs
end
def update_bounding_box
node = self
unless node.leaf?
node.bounding_box = node.a.bounding_box.union(node.b.bounding_box)
while (node = node.parent)
node.bounding_box = node.a.bounding_box.union(node.b.bounding_box)
end
end
end
end
end
end

View File

@@ -0,0 +1,55 @@
# frozen_string_literal: true
module CyberarmEngine
class AABBTree
include AABBTreeDebug
attr_reader :root, :objects, :branches, :leaves
def initialize
@objects = {}
@root = nil
@branches = 0
@leaves = 0
end
def insert(object, bounding_box)
raise "BoundingBox can't be nil!" unless bounding_box
raise "Object can't be nil!" unless object
# raise "Object already in tree!" if @objects[object] # FIXME
leaf = AABBNode.new(parent: nil, object: object, bounding_box: bounding_box.dup)
@objects[object] = leaf
insert_leaf(leaf)
end
def insert_leaf(leaf)
@root = @root ? @root.insert_subtree(leaf) : leaf
end
def update(object, bounding_box)
leaf = remove(object)
leaf.bounding_box = bounding_box
insert_leaf(leaf)
end
# Returns a list of all objects that collided with collider
def search(collider, return_nodes = false)
items = []
if @root
items = @root.search_subtree(collider)
items.map!(&:object) unless return_nodes
end
items
end
def remove(object)
leaf = @objects.delete(object)
@root = @root.remove_subtree(leaf) if leaf
leaf
end
end
end

View File

@@ -0,0 +1,29 @@
# frozen_string_literal: true
module CyberarmEngine
# Gets included into AABBTree
module AABBTreeDebug
def inspect
@branches = 0
@leaves = 0
if @root
node = @root
debug_search(node.a)
debug_search(node.b)
end
puts "<#{self.class}:#{object_id}> has #{@branches} branches and #{@leaves} leaves"
end
def debug_search(node)
if node.leaf?
@leaves += 1
else
@branches += 1
debug_search(node.a)
debug_search(node.b)
end
end
end
end

View File

@@ -62,36 +62,36 @@ module CyberarmEngine
def update
# TOP
@top.x = @element.x + @element.style.border_thickness_left
@top.y = @element.y
@top.x = @element.x + @element.styled(:margin_left) + @element.styled(:border_thickness_left)
@top.y = @element.y + @element.styled(:margin_top)
@top.z = @element.z
@top.width = @element.width - @element.style.border_thickness_left
@top.height = @element.style.border_thickness_top
@top.width = @element.width - @element.styled(:border_thickness_left)
@top.height = @element.styled(:border_thickness_top)
# RIGHT
@right.x = @element.x + @element.width
@right.y = @element.y + @element.style.border_thickness_top
@right.x = @element.x + @element.styled(:margin_left) + @element.width
@right.y = @element.y + @element.styled(:margin_top) + @element.styled(:border_thickness_top)
@right.z = @element.z
@right.width = -@element.style.border_thickness_right
@right.height = @element.height - @element.style.border_thickness_top
@right.width = -@element.styled(:border_thickness_right)
@right.height = @element.height - @element.styled(:border_thickness_top)
# BOTTOM
@bottom.x = @element.x
@bottom.y = @element.y + @element.height
@bottom.x = @element.x + @element.styled(:margin_left)
@bottom.y = @element.y + @element.styled(:margin_top) + @element.height
@bottom.z = @element.z
@bottom.width = @element.width - @element.style.border_thickness_right
@bottom.height = -@element.style.border_thickness_bottom
@bottom.width = @element.width - @element.styled(:border_thickness_right)
@bottom.height = -@element.styled(:border_thickness_bottom)
# LEFT
@left.x = @element.x
@left.y = @element.y
@left.x = @element.x + @element.styled(:margin_left)
@left.y = @element.y + @element.styled(:margin_top)
@left.z = @element.z
@left.width = @element.style.border_thickness_left
@left.height = @element.height - @element.style.border_thickness_bottom
@left.width = @element.styled(:border_thickness_left)
@left.height = @element.height - @element.styled(:border_thickness_bottom)
@top.update
@right.update

View File

@@ -0,0 +1,24 @@
module CyberarmEngine
class Dialog < CyberarmEngine::GuiState
def draw
previous_state&.draw
Gosu.flush
super
end
def update
super
return unless window.current_state == self
window.states.reverse.each do |state|
# Don't update ourselves, forever
next if state == self && state.is_a?(CyberarmEngine::GuiState)
state.update
end
end
end
end

View File

@@ -1,5 +1,17 @@
module CyberarmEngine
module DSL
def every(milliseconds, &block)
element_parent.root.gui_state.add_timer(
CyberarmEngine::Timer.new(milliseconds, true, &block)
)
end
def after(milliseconds, &block)
element_parent.root.gui_state.add_timer(
CyberarmEngine::Timer.new(milliseconds, false, &block)
)
end
def flow(options = {}, &block)
container(CyberarmEngine::Element::Flow, options, &block)
end
@@ -8,6 +20,10 @@ module CyberarmEngine
container(CyberarmEngine::Element::Stack, options, &block)
end
def widget(options = {}, &block)
container(CyberarmEngine::Element::Widget, options, &block)
end
def menu(options = {}, &block)
container(CyberarmEngine::Element::Menu, options, &block)
end
@@ -100,8 +116,20 @@ module CyberarmEngine
add_element(Element::Slider.new(options, block))
end
def page(klass, options = {})
element_parent.root.gui_state.page(klass, options)
end
def current_page
element_parent.root.gui_state.current_page
end
def dialog(klass, options = {})
element_parent.root.gui_state.window.push_state(klass, options)
end
def background(color = Gosu::Color::NONE)
element_parent.style.default[:background] = color
element_parent.style.background = color
end
def theme(theme)

View File

@@ -18,6 +18,7 @@ module CyberarmEngine
@visible = !@options.key?(:visible) ? true : @options[:visible]
@tip = @options[:tip] || ""
@debug = @options[:debug].nil? ? nil : @options[:debug]
@debug_color = @options[:debug_color].nil? ? Gosu::Color::RED : @options[:debug_color]
@style = Style.new(options)
@@ -35,15 +36,15 @@ module CyberarmEngine
@width = 0
@height = 0
@style.width = default(:width) || nil
@style.width = default(:width) || nil
@style.height = default(:height) || nil
@style.background_canvas = Background.new
@style.background_nine_slice_canvas = BackgroundNineSlice.new
@style.background_image_canvas = BackgroundImage.new
@style.border_canvas = BorderCanvas.new(element: self)
@background_canvas = nil # Background.new
@background_nine_slice_canvas = nil # BackgroundNineSlice.new
@background_image_canvas = nil # BackgroundImage.new
@border_canvas = nil # BorderCanvas.new(element: self)
@style_event = :default
@style_event = enabled? ? :default : :disabled
stylize
@@ -52,33 +53,52 @@ module CyberarmEngine
root.gui_state.request_focus(self) if @options[:autofocus]
end
def resolution_scaled(n)
return n unless @style.design_width
((Gosu.screen_width(window).to_f / @style.design_width) * n).floor
end
def stylize
@style_event = :disabled unless enabled?
set_static_position
set_color
set_font
set_padding
set_margin
set_background
set_background_nine_slice
set_background_image
set_border_thickness
set_border_color
set_border
root.gui_state.request_repaint
end
def styled(key)
case key
when :border_color_bottom, :border_color_left, :border_color_right, :border_color_top
safe_style_fetch(key, :border_color)
when :border_thickness_bottom, :border_thickness_left, :border_thickness_right, :border_thickness_top
visible? ? safe_style_fetch(key, :border_thickness) : 0
when :margin_bottom, :margin_left, :margin_right, :margin_top
visible? ? safe_style_fetch(key, :margin) : 0
when :padding_bottom, :padding_left, :padding_right, :padding_top
visible? ? safe_style_fetch(key, :padding) : 0
else
safe_style_fetch(key)
end
end
def safe_style_fetch(key, fallback_key = nil)
# Attempt to return value for requested key
v = @style.hash.dig(@style_event, key)
v = @style.send(@style_event)&.send(key) || @style.send(key)
return v if v
# Attempt to return overriding value
if fallback_key
v = @style.hash.dig(@style_event, fallback_key)
v = @style.send(@style_event)&.send(fallback_key) || @style.send(fallback_key)
return v if v
end
@@ -92,86 +112,73 @@ module CyberarmEngine
end
def set_color
@style.color = safe_style_fetch(:color)
@text&.color = @style.color
@text&.color = safe_style_fetch(:color)
end
def set_font
@text&.swap_font(safe_style_fetch(:text_size), safe_style_fetch(:font))
@text&.swap_font(resolution_scaled(safe_style_fetch(:text_size)), safe_style_fetch(:font))
end
def set_background
@style.background = safe_style_fetch(:background)
return unless bg = safe_style_fetch(:background)
@style.background_canvas.background = @style.background
@background_canvas ||= Background.new
@background_canvas.background = bg
end
def set_background_nine_slice
@style.background_nine_slice = safe_style_fetch(:background_nine_slice)
return unless img = safe_style_fetch(:background_nine_slice)
@style.background_nine_slice_mode = safe_style_fetch(:background_nine_slice_mode) || :stretch
@style.background_nine_slice_color = safe_style_fetch(:background_nine_slice_color) || Gosu::Color::WHITE
@style.background_nine_slice_canvas.color = @style.background_nine_slice_color
@background_nine_slice_canvas ||= BackgroundNineSlice.new
@style.background_nine_slice_from_edge = safe_style_fetch(:background_nine_slice_from_edge)
@background_nine_slice_canvas.image = img
@style.background_nine_slice_left = safe_style_fetch(:background_nine_slice_left, :background_nine_slice_from_edge)
@style.background_nine_slice_top = safe_style_fetch(:background_nine_slice_top, :background_nine_slice_from_edge)
@style.background_nine_slice_right = safe_style_fetch(:background_nine_slice_right, :background_nine_slice_from_edge)
@style.background_nine_slice_bottom = safe_style_fetch(:background_nine_slice_bottom, :background_nine_slice_from_edge)
@background_nine_slice_canvas.x = @x + styled(:margin_left)
@background_nine_slice_canvas.y = @y + styled(:margin_top)
@background_nine_slice_canvas.z = @z
@background_nine_slice_canvas.width = width
@background_nine_slice_canvas.height = height
@background_nine_slice_canvas.mode = safe_style_fetch(:background_nine_slice_mode) || :stretch
@background_nine_slice_canvas.color = safe_style_fetch(:background_nine_slice_color) || Gosu::Color::WHITE
@background_nine_slice_canvas.set_edges(
left: safe_style_fetch(:background_nine_slice_left, :background_nine_slice_from_edge) || 1,
top: safe_style_fetch(:background_nine_slice_top, :background_nine_slice_from_edge) || 1,
right: safe_style_fetch(:background_nine_slice_right, :background_nine_slice_from_edge) || 1,
bottom: safe_style_fetch(:background_nine_slice_bottom, :background_nine_slice_from_edge) || 1
)
end
def set_background_image
@style.background_image = safe_style_fetch(:background_image)
@style.background_image_mode = safe_style_fetch(:background_image_mode) || :stretch
@style.background_image_color = safe_style_fetch(:background_image_color) || Gosu::Color::WHITE
@style.background_image_canvas.mode = @style.background_image_mode
@style.background_image_canvas.color = @style.background_image_color
return unless img = safe_style_fetch(:background_image)
@background_image_canvas ||= BackgroundImage.new
@background_image_canvas.image = img
@background_image_canvas.mode = safe_style_fetch(:background_image_mode) || :stretch
@background_image_canvas.color = safe_style_fetch(:background_image_color) || Gosu::Color::WHITE
end
def set_border_thickness
@style.border_thickness = safe_style_fetch(:border_thickness)
def set_border
return unless [
safe_style_fetch(:border_thickness_bottom, :border_thickness),
safe_style_fetch(:border_thickness_left, :border_thickness),
safe_style_fetch(:border_thickness_right, :border_thickness),
safe_style_fetch(:border_thickness_top, :border_thickness),
].any?(&:positive?)
@style.border_thickness_left = safe_style_fetch(:border_thickness_left, :border_thickness)
@style.border_thickness_right = safe_style_fetch(:border_thickness_right, :border_thickness)
@style.border_thickness_top = safe_style_fetch(:border_thickness_top, :border_thickness)
@style.border_thickness_bottom = safe_style_fetch(:border_thickness_bottom, :border_thickness)
end
@border_canvas ||= BorderCanvas.new(element: self)
def set_border_color
@style.border_color = safe_style_fetch(:border_color)
@style.border_color_left = safe_style_fetch(:border_color_left, :border_color)
@style.border_color_right = safe_style_fetch(:border_color_right, :border_color)
@style.border_color_top = safe_style_fetch(:border_color_top, :border_color)
@style.border_color_bottom = safe_style_fetch(:border_color_bottom, :border_color)
@style.border_canvas.color = [
@style.border_color_top,
@style.border_color_right,
@style.border_color_bottom,
@style.border_color_left
@border_canvas&.color = [
styled(:border_color_top),
styled(:border_color_right),
styled(:border_color_bottom),
styled(:border_color_left)
]
end
def set_padding
@style.padding = safe_style_fetch(:padding)
@style.padding_left = safe_style_fetch(:padding_left, :padding)
@style.padding_right = safe_style_fetch(:padding_right, :padding)
@style.padding_top = safe_style_fetch(:padding_top, :padding)
@style.padding_bottom = safe_style_fetch(:padding_bottom, :padding)
end
def set_margin
@style.margin = safe_style_fetch(:margin)
@style.margin_left = safe_style_fetch(:margin_left, :margin)
@style.margin_right = safe_style_fetch(:margin_right, :margin)
@style.margin_top = safe_style_fetch(:margin_top, :margin)
@style.margin_bottom = safe_style_fetch(:margin_bottom, :margin)
end
def update_styles(event = :default)
old_width = width
old_height = height
@@ -208,6 +215,7 @@ module CyberarmEngine
event(:blur)
event(:changed)
event(:size_changed)
end
def enter(_sender)
@@ -215,7 +223,7 @@ module CyberarmEngine
if !@enabled
update_styles(:disabled)
elsif @focus
elsif @focus && !@style.active.empty?
update_styles(:active)
else
update_styles(:hover)
@@ -230,7 +238,7 @@ module CyberarmEngine
unless @enabled
update_styles(:disabled)
else
update_styles(:active)
update_styles(:active) unless @style.active.empty?
end
window.current_state.focus = self
@@ -276,6 +284,7 @@ module CyberarmEngine
root.gui_state.request_repaint if @enabled != boolean
@enabled = boolean
@style_event = :default if boolean && @style_event == :disabled
recalculate
@@ -322,15 +331,16 @@ module CyberarmEngine
return unless visible?
return unless element_visible?
@style.background_canvas.draw
@style.background_nine_slice_canvas.draw
@style.background_image_canvas.draw
@style.border_canvas.draw
@background_canvas&.draw
@background_nine_slice_canvas&.draw
@background_image_canvas&.draw
@border_canvas&.draw
render
end
def debug_draw
return if @debug == false || !CyberarmEngine::GUI_DEBUG # allow elements to opt out of debug drawing, makes debugging some things easier.
return if CyberarmEngine.const_defined?("GUI_DEBUG_ONLY_ELEMENT") && self.class == GUI_DEBUG_ONLY_ELEMENT
Gosu.draw_line(
@@ -357,6 +367,11 @@ module CyberarmEngine
def update
recalculate_if_size_changed
if @style.dirty?
@style.mark_clean!
stylize
end
end
def button_down(id)
@@ -373,13 +388,13 @@ module CyberarmEngine
end
def hit?(x, y)
x.between?(@x, @x + width) &&
y.between?(@y, @y + height)
x.between?(@x + styled(:margin_left), @x + styled(:margin_left) + width) &&
y.between?(@y + styled(:margin_top), @y + styled(:margin_top) + height)
end
def width
if visible?
inner_width + @width
inner_width + content_width
else
0
end
@@ -394,16 +409,16 @@ module CyberarmEngine
end
def outer_width
@style.margin_left + width + @style.margin_right
styled(:margin_left) + width + styled(:margin_right)
end
def inner_width
(@style.border_thickness_left + @style.padding_left) + (@style.padding_right + @style.border_thickness_right)
(styled(:border_thickness_left) + styled(:padding_left)) + (styled(:padding_right) + styled(:border_thickness_right))
end
def height
if visible?
inner_height + @height
inner_height + content_height
else
0
end
@@ -418,11 +433,11 @@ module CyberarmEngine
end
def outer_height
@style.margin_top + height + @style.margin_bottom
styled(:margin_top) + height + styled(:margin_bottom)
end
def inner_height
(@style.border_thickness_top + @style.padding_top) + (@style.padding_bottom + @style.border_thickness_bottom)
(styled(:border_thickness_top) + styled(:padding_top)) + (styled(:padding_bottom) + styled(:border_thickness_bottom))
end
def scroll_width
@@ -454,9 +469,9 @@ module CyberarmEngine
pairs_ << a_ unless pairs_.last == a_
@cached_scroll_height = pairs_.sum { |pair| + @style.padding_top + @style.border_thickness_top + pair.map(&:outer_height).max } + @style.padding_bottom + @style.border_thickness_bottom
@cached_scroll_height = pairs_.sum { |pair| pair.map(&:outer_height).max } + noncontent_height
else
@cached_scroll_height = @style.padding_top + @style.border_thickness_top + @children.sum(&:outer_height) + @style.padding_bottom + @style.border_thickness_bottom
@cached_scroll_height = noncontent_height + @children.sum(&:outer_height)
end
end
@@ -473,84 +488,67 @@ module CyberarmEngine
new_size = if size.is_a?(Float) && size.between?(0.0, 1.0)
(@parent.send(:"content_#{dimension}") * size).floor - send(:"noncontent_#{dimension}").floor
elsif size.is_a?(Numeric)
(size - send(:"noncontent_#{dimension}")).floor
else
size
end
# Handle fill behavior
if @parent && @style.fill &&
if @parent && styled(:fill) &&
(dimension == :width && @parent.is_a?(Flow) ||
dimension == :height && @parent.is_a?(Stack))
new_size = space_available_width - noncontent_width if dimension == :width && @parent.is_a?(Flow)
new_size = space_available_height - noncontent_height if dimension == :height && @parent.is_a?(Stack)
end
return @style.send(:"min_#{dimension}") if @style.send(:"min_#{dimension}") && new_size.to_f < @style.send(:"min_#{dimension}")
return @style.send(:"max_#{dimension}") if @style.send(:"max_#{dimension}") && new_size.to_f > @style.send(:"max_#{dimension}")
return styled(:"min_#{dimension}") if styled(:"min_#{dimension}") && new_size.to_f < styled(:"min_#{dimension}")
return styled(:"max_#{dimension}") if styled(:"max_#{dimension}") && new_size.to_f > styled(:"max_#{dimension}")
new_size
end
def space_available_width
# TODO: This may get expensive if there are a lot of children, probably should cache it somehow
fill_siblings = @parent.children.select { |c| c.style.fill }.count.to_f # include self since we're dividing
fill_siblings = @parent.children.select { |c| c.styled(:fill) }.count.to_f # include self since we're dividing
available_space = ((@parent.content_width - (@parent.children.reject { |c| c.style.fill }).map(&:outer_width).sum) / fill_siblings)
available_space = ((@parent.content_width - (@parent.children.reject { |c| c.styled(:fill) }).map(&:outer_width).sum) / fill_siblings)
(available_space.nan? || available_space.infinite?) ? 0 : available_space.floor # The parent element might not have its dimensions, yet.
end
def space_available_height
# TODO: This may get expensive if there are a lot of children, probably should cache it somehow
fill_siblings = @parent.children.select { |c| c.style.fill }.count.to_f # include self since we're dividing
fill_siblings = @parent.children.select { |c| c.styled(:fill) && c.visible? }.count.to_f # include self since we're dividing
available_space = ((@parent.content_height - (@parent.children.reject { |c| c.style.fill }).map(&:outer_height).sum) / fill_siblings)
available_space = ((@parent.content_height - (@parent.children.reject { |c| c.styled(:fill) }).map(&:outer_height).sum) / fill_siblings)
(available_space.nan? || available_space.infinite?) ? 0 : available_space.floor # The parent element might not have its dimensions, yet.
end
def background=(_background)
root.gui_state.request_repaint
@style.background_canvas.background = _background
@background_canvas.background = _background
update_background
end
def update_background
@style.background_canvas.x = @x
@style.background_canvas.y = @y
@style.background_canvas.z = @z
@style.background_canvas.width = width
@style.background_canvas.height = height
@background_canvas&.x = @x + styled(:margin_left)
@background_canvas&.y = @y + styled(:margin_top)
@background_canvas&.z = @z
@background_canvas&.width = width
@background_canvas&.height = height
@style.background_canvas.update
update_background_nine_slice
@background_canvas&.update
set_background_nine_slice
update_background_image
@style.border_canvas.update
@border_canvas&.update
end
def background_nine_slice=(_image_path)
root.gui_state.request_repaint
@style.background_nine_slice_canvas.image = _image_path
update_background_nine_slice
end
def update_background_nine_slice
@style.background_nine_slice_canvas.x = @x
@style.background_nine_slice_canvas.y = @y
@style.background_nine_slice_canvas.z = @z
@style.background_nine_slice_canvas.width = width
@style.background_nine_slice_canvas.height = height
@style.background_nine_slice_canvas.mode = @style.background_nine_slice_mode
@style.background_nine_slice_canvas.color = @style.background_nine_slice_color
@style.background_nine_slice_canvas.left = @style.background_nine_slice_left
@style.background_nine_slice_canvas.top = @style.background_nine_slice_top
@style.background_nine_slice_canvas.right = @style.background_nine_slice_right
@style.background_nine_slice_canvas.bottom = @style.background_nine_slice_bottom
@style.background_nine_slice_canvas.image = @style.background_nine_slice
@background_nine_slice_canvas.image = _image_path
set_background_nine_slice
end
def background_image=(image_path)
@@ -561,16 +559,18 @@ module CyberarmEngine
end
def update_background_image
@style.background_image_canvas.x = @x
@style.background_image_canvas.y = @y
@style.background_image_canvas.z = @z
@style.background_image_canvas.width = width
@style.background_image_canvas.height = height
return unless @background_image_canvas
@style.background_image_canvas.mode = @style.background_image_mode
@style.background_image_canvas.color = @style.background_image_color
@background_image_canvas.x = @x + styled(:margin_left)
@background_image_canvas.y = @y + styled(:margin_top)
@background_image_canvas.z = @z
@background_image_canvas.width = width
@background_image_canvas.height = height
@style.background_image_canvas.image = @style.background_image
@background_image_canvas.mode = safe_style_fetch(:background_image_mode) || :stretch
@background_image_canvas.color = safe_style_fetch(:background_image_color) || Gosu::Color::WHITE
@background_image_canvas.image = safe_style_fetch(:background_image)
end
def recalculate_if_size_changed
@@ -579,6 +579,8 @@ module CyberarmEngine
@old_width = width
@old_height = height
publish(:size_changed)
end
end
@@ -634,7 +636,7 @@ module CyberarmEngine
root.gui_state.request_recalculate if @parent && !is_a?(ToolTip) && (width != old_width || height != old_height)
root.gui_state.request_repaint if width != old_width || height != old_height
root.gui_state.menu.recalculate if root.gui_state.menu && root.gui_state.menu.parent == self
root.gui_state.active_menu.recalculate if root.gui_state.active_menu && root.gui_state.active_menu.parent == self
end
def layout

View File

@@ -10,7 +10,7 @@ module CyberarmEngine
super(text_or_image, options, block)
@style.background_canvas.background = @style.background
@background_canvas.background = styled(:background)
end
def render
@@ -23,8 +23,8 @@ module CyberarmEngine
def draw_image
@image.draw(
@style.border_thickness_left + @style.padding_left + @x,
@style.border_thickness_top + @style.padding_top + @y,
styled(:border_thickness_left) + styled(:margin_left) + styled(:padding_left) + @x,
styled(:border_thickness_top) + styled(:margin_top) + styled(:padding_top) + @y,
@z + 2,
@scale_x, @scale_y, @text.color
)
@@ -35,20 +35,15 @@ module CyberarmEngine
end
def layout
unless @enabled
@style.background_canvas.background = @style.disabled[:background]
@text.color = @style.disabled[:color]
else
@style.background_canvas.background = @style.background
@text.color = @style.color
end
@background_canvas.background = styled(:background)
@text.color = styled(:color)
if @image
@width = 0
@height = 0
_width = dimensional_size(@style.image_width, :width)
_height = dimensional_size(@style.image_height, :height)
_width = dimensional_size(styled(:image_width), :width)
_height = dimensional_size(styled(:image_height), :height)
if _width && _height
@scale_x = _width.to_f / @image.width
@@ -78,6 +73,8 @@ module CyberarmEngine
end
def value=(value)
old_value = self.value
if value.is_a?(Gosu::Image)
@image = value
else
@@ -93,7 +90,9 @@ module CyberarmEngine
recalculate
end
publish(:changed, self.value)
publish(:changed, self.value) if value != old_value
self.value
end
end
end

View File

@@ -46,8 +46,12 @@ module CyberarmEngine
end
def value=(bool)
old_value = this.value
@toggle_button.value = bool
publish(:changed, @toggle_button.value)
publish(:changed, bool) if bool != old_value
bool
end
end
end

View File

@@ -7,13 +7,13 @@ module CyberarmEngine
attr_reader :children, :gui_state, :scroll_position, :scroll_target_position
def self.current_container
@@current_container
@current_container
end
def self.current_container=(container)
raise ArgumentError, "Expected container to an an instance of CyberarmEngine::Element::Container, got #{container.class}" unless container.is_a?(CyberarmEngine::Element::Container)
@@current_container = container
@current_container = container
end
def initialize(options = {}, block = nil)
@@ -26,6 +26,11 @@ module CyberarmEngine
@scroll_chunk = 120
@scroll_speed = 40
if @gui_state
@width = window.width
@height = window.height
end
@text_color = options[:color]
@children = []
@@ -34,7 +39,7 @@ module CyberarmEngine
end
def build
@block.call(self) if @block
@block&.call(self)
root.gui_state.request_recalculate_for(self)
end
@@ -53,7 +58,7 @@ module CyberarmEngine
old_container = CyberarmEngine::Element::Container.current_container
CyberarmEngine::Element::Container.current_container = self
block.call(self) if block
block&.call(self)
CyberarmEngine::Element::Container.current_container = old_container
@@ -66,7 +71,7 @@ module CyberarmEngine
old_container = CyberarmEngine::Element::Container.current_container
CyberarmEngine::Element::Container.current_container = self
block.call(self) if block
block&.call(self)
CyberarmEngine::Element::Container.current_container = old_container
@@ -75,33 +80,41 @@ module CyberarmEngine
def render
Gosu.clip_to(
@x + @style.border_thickness_left + @style.padding_left,
@y + @style.border_thickness_top + @style.padding_top,
@x - 1 + styled(:margin_left) + styled(:border_thickness_left) + styled(:padding_left),
@y - 1 + styled(:margin_top) + styled(:border_thickness_top) + styled(:padding_top),
content_width + 1,
content_height + 1
) do
Gosu.translate(@scroll_position.x, @scroll_position.y) do
@children.each(&:draw)
@children.each do |child|
next unless child.visible?
child.debug_draw
end
end
end
debug_draw
end
def debug_draw
super
@children.each do |child|
child.debug_draw
end
end
def update
update_scroll if @style.scroll
update_scroll if styled(:scroll)
@children.each(&:update)
end
def hit_element?(x, y)
# return nil if element was not hit, or array of hit elements if hit, includes self.
def hit_element?(x, y, elements = [])
return unless hit?(x, y)
elements << self
# Offset child hit point by scroll position/offset
child_x = x - @scroll_position.x
child_y = y - @scroll_position.y
@@ -111,20 +124,23 @@ module CyberarmEngine
case child
when Container
if element = child.hit_element?(child_x, child_y)
return element
if (child.hit_element?(child_x, child_y, elements))
return elements
end
else
return child if child.hit?(child_x, child_y)
if child.hit?(child_x, child_y)
elements << child
return elements
end
end
end
self if hit?(x, y)
elements
end
def update_child_element_visibity(child)
child.element_visible = child.x >= (@x - @scroll_position.x) - child.width && child.x <= (@x - @scroll_position.x) + width &&
child.y >= (@y - @scroll_position.y) - child.height && child.y <= (@y - @scroll_position.y) + height
child.element_visible = child.x >= ((styled(:margin_left) + @x) - @scroll_position.x) - child.width && child.x <= ((styled(:margin_left) + @x) - @scroll_position.x) + width &&
child.y >= ((styled(:margin_top) + @y) - @scroll_position.y) - child.height && child.y <= ((styled(:margin_top) + @y) - @scroll_position.y) + height
end
def update_scroll
@@ -151,7 +167,7 @@ module CyberarmEngine
end
def recalculate
@current_position = Vector.new(@style.margin_left + @style.padding_left, @style.margin_top + @style.padding_top)
@current_position = Vector.new(styled(:margin_left) + styled(:padding_left), styled(:margin_top) + styled(:padding_top))
return unless visible?
@@ -170,7 +186,7 @@ module CyberarmEngine
@cached_scroll_height = nil
if is_root?
@width = @style.width = window.width
@width = @style.width = window.width
@height = @style.height = window.height
else
@width = 0
@@ -179,39 +195,39 @@ module CyberarmEngine
_width = dimensional_size(@style.width, :width)
_height = dimensional_size(@style.height, :height)
@width = _width || (@children.map { |c| c.x + c.outer_width }.max || 0).floor
@height = _height || (@children.map { |c| c.y + c.outer_height }.max || 0).floor
@width = _width || (@children.map { |c| c.x + c.outer_width }.max.to_f - (styled(:margin_left) + styled(:padding_left))).floor
@height = _height || (@children.map { |c| c.y + c.outer_height }.max.to_f - (styled(:margin_top) + styled(:padding_top))).floor
end
# FIXME: Correctly handle alignment when element has siblings
# FIXME: Enable alignment for any element, not just containers
if @style.v_align
if styled(:v_align)
space = space_available_height
case @style.v_align
case styled(:v_align)
when :center
@y = parent.height / 2 - height / 2
@y = parent.y + parent.outer_height / 2 - outer_height / 2
when :bottom
@y = parent.height - height
@y = parent.y + parent.outer_height - outer_height
end
end
if @style.h_align
if styled(:h_align)
space = space_available_width
case @style.h_align
case styled(:h_align)
when :center
@x = parent.width / 2 - width / 2
@x = parent.x + parent.outer_width / 2 - outer_width / 2
when :right
@x = parent.width - width
@x = parent.x + parent.outer_width - outer_width
end
end
# t = Gosu.milliseconds
# Move children to parent after positioning
@children.each do |child|
child.x += (@x + @style.border_thickness_left) - style.margin_left
child.y += (@y + @style.border_thickness_top) - style.margin_top
child.x += (@x + styled(:border_thickness_left))
child.y += (@y + styled(:border_thickness_top))
child.stylize
child.recalculate
@@ -221,7 +237,7 @@ module CyberarmEngine
update_child_element_visibity(child)
end
# puts "TOOK: #{Gosu.milliseconds - t}ms to recalculate #{self.class}:0x#{self.object_id.to_s(16)}'s #{@children.count} children"
# puts "TOOK: #{Gosu.milliseconds - t}ms to recalculate #{self.class}:0x#{object_id.to_s(16)}'s #{@children.count} children" if is_root?
update_background
@@ -246,13 +262,6 @@ module CyberarmEngine
end
def max_width
# _width = dimensional_size(@style.width, :width)
# if _width
# outer_width
# else
# window.width - (@parent ? @parent.style.margin_right + @style.margin_right : @style.margin_right)
# end
outer_width
end
@@ -262,8 +271,8 @@ module CyberarmEngine
end
def position_on_current_line(element) # Flow
element.x = element.style.margin_left + @current_position.x
element.y = element.style.margin_top + @current_position.y
element.x = @current_position.x
element.y = @current_position.y
@current_position.x += element.outer_width
end
@@ -279,87 +288,87 @@ module CyberarmEngine
end
def position_on_next_line(element) # Flow
@current_position.x = @style.margin_left + @style.padding_left
@current_position.x = styled(:margin_left) + styled(:padding_left)
@current_position.y += tallest_neighbor(element, @current_position.y).outer_height
element.x = element.style.margin_left + @current_position.x
element.y = element.style.margin_top + @current_position.y
element.x = @current_position.x
element.y = @current_position.y
@current_position.x += element.outer_width
end
def move_to_next_line(element) # Stack
element.x = element.style.margin_left + @current_position.x
element.y = element.style.margin_top + @current_position.y
element.x = @current_position.x
element.y = @current_position.y
@current_position.y += element.outer_height
end
def mouse_wheel_up(sender, x, y)
return unless @style.scroll
return unless styled(:scroll)
# Allow overscrolling UP, only if one can scroll DOWN
if height < scroll_height
if @scroll_target_position.y > 0
@scroll_target_position.y = @scroll_chunk
else
@scroll_target_position.y += @scroll_chunk
end
return unless height < scroll_height
return :handled
if @scroll_target_position.y.positive?
@scroll_target_position.y = @scroll_chunk
else
@scroll_target_position.y += @scroll_chunk
end
:handled
end
def mouse_wheel_down(sender, x, y)
return unless @style.scroll
return unless styled(:scroll)
return unless height < scroll_height
if @scroll_target_position.y > 0
if @scroll_target_position.y.positive?
@scroll_target_position.y = -@scroll_chunk
else
@scroll_target_position.y -= @scroll_chunk
end
return :handled
:handled
end
def scroll_jump_to_top(sender, x, y)
return unless @style.scroll
return unless styled(:scroll)
@scroll_position.y = 0
@scroll_target_position.y = 0
return :handled
:handled
end
def scroll_jump_to_end(sender, x, y)
return unless @style.scroll
return unless styled(:scroll)
@scroll_position.y = -max_scroll_height
@scroll_target_position.y = -max_scroll_height
return :handled
:handled
end
def scroll_page_up(sender, x, y)
return unless @style.scroll
return unless styled(:scroll)
@scroll_position.y += height
@scroll_position.y = 0 if @scroll_position.y > 0
@scroll_target_position.y = @scroll_position.y
return :handled
:handled
end
def scroll_page_down(sender, x, y)
return unless @style.scroll
return unless styled(:scroll)
@scroll_position.y -= height
@scroll_position.y = -max_scroll_height if @scroll_position.y < -max_scroll_height
@scroll_target_position.y = @scroll_position.y
return :handled
:handled
end
def scroll_top

View File

@@ -45,19 +45,23 @@ module CyberarmEngine
def draw_selection
selection_width = caret_position - selection_start_position
selection_start_line = calculate_line(@text_input.caret_pos)
selection_end_line = calculate_line(@text_input.selection_start)
Gosu.draw_rect(selection_start_position, @text.y, selection_width, @text.textobject.height,
# pp [selection_width, selection_start_line, selection_end_line]
Gosu.draw_rect(selection_start_position, @text.y + selection_start_line * @text.textobject.height, selection_width, @text.textobject.height,
default(:selection_color), @z)
end
def text_input_position_for(_method)
line = @text_input.text[0...@text_input.caret_pos].lines.last
def text_input_position_for(method)
line = @text_input.text[0...@text_input.send(method)].lines.last
_x = @text.x + @offset_x
if @type == :password
_x + @text.width(default(:password_character) * line.length)
_x + @text.width(default(:password_character) * line.length) - @style.border_thickness_left
else
_x + @text.width(line)
_x + @text.width(line) - @style.border_thickness_left
end
end
@@ -66,9 +70,12 @@ module CyberarmEngine
@text_input.selection_start = @text_input.caret_pos = int
end
def calculate_line(caret_pos = @text_input.caret_pos)
sub_text = @text_input.text[0...caret_pos].lines.size - 1
end
def calculate_active_line
sub_text = @text_input.text[0...@text_input.caret_pos]
@active_line = sub_text.lines.size - 1
@active_line = calculate_line(@text_input.caret_pos)
end
def caret_stay_left_of_last_newline
@@ -79,8 +86,20 @@ module CyberarmEngine
end
def caret_position_under_mouse(mouse_x, mouse_y)
active_line = row_at(mouse_y)
right_offset = column_at(mouse_x, mouse_y)
# get y scroll offset of element to get EditBox's selected row
y_scroll_offset = 0
e = parent
while (e)
if e.is_a?(Container)
y_scroll_offset += e.scroll_position.y
# root element has no parent so loop will terminate
e = e.parent
end
end
active_line = row_at(mouse_y - y_scroll_offset)
right_offset = column_at(mouse_x, mouse_y - y_scroll_offset)
buffer = @text_input.text.lines[0..active_line].join if active_line != 0
buffer = @text_input.text.lines.first if active_line == 0
@@ -164,6 +183,8 @@ module CyberarmEngine
pos = text.length
end
pp [direction, pos, @active_line]
set_position(pos)
end

View File

@@ -168,7 +168,7 @@ module CyberarmEngine
@last_text = @text.text
@last_pos = caret_pos
if caret_pos.between?(@offset_x, @width + @offset_x)
if caret_pos.between?(@offset_x + 1, @width + @offset_x)
# Do nothing
elsif caret_pos < @offset_x
@@ -197,9 +197,9 @@ module CyberarmEngine
def text_input_position_for(method)
if @type == :password
@text.x + @text.width(default(:password_character) * @text_input.text[0...@text_input.send(method)].length)
@text.x + @text.width(default(:password_character) * @text_input.text[0...@text_input.send(method)].length) - styled(:border_thickness_left)
else
@text.x + @text.width(@text_input.text[0...@text_input.send(method)]) - @style.border_thickness_left
@text.x + @text.width(@text_input.text[0...@text_input.send(method)]) - styled(:border_thickness_left)
end
end
@@ -291,6 +291,8 @@ module CyberarmEngine
def value=(string)
@text_input.text = string
string
end
end
end

View File

@@ -14,10 +14,10 @@ module CyberarmEngine
def render
@image.draw(
@style.border_thickness_left + @style.padding_left + @x,
@style.border_thickness_top + @style.padding_top + @y,
styled(:margin_left) + styled(:border_thickness_left) + styled(:padding_left) + @x,
styled(:margin_top) + styled(:border_thickness_top) + styled(:padding_top) + @y,
@z + 2,
@scale_x, @scale_y, @style.color
@scale_x, @scale_y, styled(:color)
)
end
@@ -56,12 +56,18 @@ module CyberarmEngine
end
def value=(path_or_image, retro: false, tileable: false)
old_value = self.value
@path = path_or_image if path_or_image.is_a?(String)
@image = Gosu::Image.new(path_or_image, retro: retro, tileable: tileable) if @path
@image = path_or_image unless @path
recalculate
publish(:changed, @image) if old_value != self.value
self.value
end
def path

View File

@@ -10,7 +10,7 @@ module CyberarmEngine
super(@choose, options, block)
@style.background_canvas.background = default(:background)
@background_canvas.background = default(:background)
@menu = Menu.new(parent: self, theme: @options[:theme])
@@ -21,7 +21,7 @@ module CyberarmEngine
super
w = @text.textobject.text_width("")
@text.textobject.draw_text("", @x + content_width - w, @y + @style.padding_top, @z, 1, 1, @text.color)
@text.textobject.draw_text("", @x + content_width - w, @y + styled(:padding_top), @z, 1, 1, @text.color)
end
def choose=(item)
@@ -56,7 +56,7 @@ module CyberarmEngine
def show_menu
@menu.clear do
@menu.style.width = width
@menu.style.width = width - (@menu.styled(:border_thickness_left) + @menu.styled(:border_thickness_right))
@items.each do |item|
# prevent already selected item from appearing in list

View File

@@ -11,10 +11,10 @@ module CyberarmEngine
# FIXME: properly find scrollable parent, if any.
parent_scroll_top = parent&.parent ? parent.parent.scroll_top : 0
@x = @parent.x
@y = parent_scroll_top + @parent.y + @parent.height
@x = @parent.x + @parent.styled(:border_thickness_left)
@y = parent_scroll_top + @parent.y + @parent.height + @parent.styled(:border_thickness_top)
@y = (parent_scroll_top + @parent.y) - height if @y + height > window.height
@y = (parent_scroll_top + @parent.y + @parent.styled(:border_thickness_top)) - height if @y + height > window.height
end
def show

View File

@@ -11,7 +11,7 @@ module CyberarmEngine
@marquee_offset = 0
@marquee_animation_time = Gosu.milliseconds
@type = options[:type] || :linear
@fraction_background = Background.new(background: @style.fraction_background)
@fraction_background = Background.new(background: styled(:fraction_background))
self.value = options[:fraction] || 0.0
end
@@ -31,13 +31,13 @@ module CyberarmEngine
def update_background
super
@fraction_background.x = (@style.border_thickness_left + @style.padding_left + @x) + @marquee_offset
@fraction_background.y = @style.border_thickness_top + @style.padding_top + @y
@fraction_background.x = (styled(:border_thickness_left) + styled(:padding_left) + @x) + @marquee_offset
@fraction_background.y = styled(:border_thickness_top) + styled(:padding_top) + @y
@fraction_background.z = @z
@fraction_background.width = @width * (@type == :marquee ? @marquee_width : @fraction)
@fraction_background.height = @height
@fraction_background.background = @style.fraction_background
@fraction_background.background = styled(:fraction_background)
end
def update
@@ -83,9 +83,11 @@ module CyberarmEngine
@fraction = decimal.clamp(0.0, 1.0)
update_background
root.gui_state.request_repaint if @fraction != old_value
if @fraction != old_value
root.gui_state.request_repaint
publish(:changed, @fraction)
end
publish(:changed, @fraction)
@fraction
end
end

View File

@@ -62,10 +62,10 @@ module CyberarmEngine
end
def position_handle
@handle.x = @x + @handle.style.margin_left + @style.padding_left + @style.border_thickness_left +
@handle.x = styled(:margin_left) + @x + @handle.styled(:margin_left) + styled(:padding_left) + styled(:border_thickness_left) +
((content_width - @handle.outer_width) * (@value - @range.min) / (@range.max - @range.min).to_f)
@handle.y = @y + @handle.style.margin_top + @style.border_thickness_top + @style.padding_top
@handle.y = styled(:margin_top) + @y + @handle.styled(:margin_top) + styled(:border_thickness_top) + styled(:padding_top)
end
def draw
@@ -94,13 +94,17 @@ module CyberarmEngine
end
def value=(n)
old_value = this.value
@value = n
position_handle
@handle.recalculate
root.gui_state.request_repaint
publish(:changed, @value)
publish(:changed, @value) if old_value != n
n
end
end
end

View File

@@ -8,6 +8,7 @@ module CyberarmEngine
text, font: @options[:font], z: @z, color: @options[:color],
size: @options[:text_size], shadow: @options[:text_shadow],
static: @options[:text_static],
alignment: @options[:text_align],
shadow_size: @options[:text_shadow_size],
shadow_color: @options[:text_shadow_color],
border: @options[:text_border],
@@ -16,6 +17,8 @@ module CyberarmEngine
)
@raw_text = text
@text_width = @text.width
@text_height = @text.height
end
def update
@@ -29,8 +32,8 @@ module CyberarmEngine
def render
# Gosu.clip_to is too expensive to always use so check if we actually need it.
if @text.width > width || @text.height > height
Gosu.clip_to(@x, @y, width, height) do
if @text_width > width || @text_height > height
Gosu.clip_to(@text.x, @text.y, @width, @height) do
@text.draw
end
else
@@ -39,11 +42,18 @@ module CyberarmEngine
end
def layout
unless @enabled
@text.color = @style.disabled[:color]
else
@text.color = @style.color
end
@text.color = styled(:color)
@text.alignment = styled(:text_align)
@text.shadow = styled(:text_shadow)
@text.shadow_color = styled(:text_shadow_color)
@text.shadow_alpha = styled(:text_shadow_alpha)
@text.shadow_size = styled(:text_shadow_size)
@text.border = styled(:text_border)
@text.border_color = styled(:text_border_color)
@text.border_alpha = styled(:text_border_alpha)
@text.border_size = styled(:text_border_size)
@width = 0
@height = 0
@@ -53,37 +63,41 @@ module CyberarmEngine
handle_text_wrapping(_width)
@width = _width || @text.width.floor
@height = _height || @text.height.floor
# Update cached text width and height
@text_width = @text.width
@text_height = @text.height
@text.y = @style.border_thickness_top + @style.padding_top + @y
@width = _width || @text_width.floor
@height = _height || @text_height.floor
@text.y = styled(:margin_top) + styled(:border_thickness_top) + styled(:padding_top) + @y
@text.z = @z + 3
if (text_alignment = @options[:text_align] || @options[:text_h_align])
case text_alignment
when :left
@text.x = @style.border_thickness_left + @style.padding_left + @x
@text.x = styled(:margin_left) + styled(:border_thickness_left) + styled(:padding_left) + @x
when :center
@text.x = if @text.width <= width
@x + width / 2 - @text.width / 2
@text.x = if @text_width <= width
@x + styled(:margin_left) + width / 2 - @text_width / 2
else # Act as left aligned
@style.border_thickness_left + @style.padding_left + @x
styled(:margin_left) + styled(:border_thickness_left) + styled(:padding_left) + @x
end
when :right
@text.x = @x + outer_width - (@text.width + @style.border_thickness_right + @style.padding_right)
@text.x = @x + styled(:margin_left) + outer_width - (@text_width + styled(:border_thickness_right) + styled(:padding_right))
end
end
if (vertical_alignment = @options[:text_v_align])
case vertical_alignment
when :center
@text.y = if @text.height <= height
@y + height / 2 - @text.height / 2
@text.y = if @text_height <= height
@y + styled(:margin_top) + height / 2 - @text_height / 2
else
@style.border_thickness_top + @style.padding_top + @y
styled(:margin_top) + styled(:border_thickness_top) + styled(:padding_top) + @y
end
when :bottom
@text.y = @y + outer_height - (@text.height + @style.border_thickness_bottom + @style.padding_bottom)
@text.y = @y + styled(:margin_top) + outer_height - (@text_height + styled(:border_thickness_bottom) + styled(:padding_bottom))
end
end
@@ -93,7 +107,7 @@ module CyberarmEngine
def handle_text_wrapping(max_width)
max_width ||= @parent&.content_width
max_width ||= @x - (window.width + noncontent_width)
wrap_behavior = style.text_wrap
wrap_behavior = styled(:text_wrap)
copy = @raw_text.to_s.dup
# Only perform text wrapping: if it is enabled, is possible to wrap, and text is too long to fit on one line
@@ -180,9 +194,12 @@ module CyberarmEngine
recalculate
end
root.gui_state.request_repaint if old_value != @raw_text
if old_value != @raw_text
root.gui_state.request_repaint
publish(:changed, @raw_text)
end
publish(:changed, self.value)
value
end
end
@@ -208,6 +225,9 @@ module CyberarmEngine
end
class ToolTip < TextBlock
def needs_repaint?
@needs_repaint || !value.to_s.empty?
end
end
class Link < TextBlock

View File

@@ -50,6 +50,8 @@ module CyberarmEngine
end
def value=(boolean)
old_value = self.value
@value = boolean
if boolean
@@ -60,7 +62,9 @@ module CyberarmEngine
recalculate
publish(:changed, @value)
publish(:changed, @value) if old_value != boolean
boolean
end
end
end

View File

@@ -0,0 +1,45 @@
module CyberarmEngine
class Element
# Special container that has a layout like a Flow
# and makes all its children mirror its styling (i.e. hover, active, disabled...)
class Widget < Flow
def update_styles(style = :default)
super
@children.each do |child|
recursive_styles(style, self)
end
end
# Make child elements mirror the widgets styling
# disabled elements will not have their styling overridden
def recursive_styles(style, container)
container.children.each do |child|
child.update_styles(style)
recursive_styles(style, child) if child.is_a?(Container)
end
end
# Enable child elements to display their tooltips
# but fall back to the Widget if no hit element has a tip
def tip
parent_scroll_position = CyberarmEngine::Vector.new
element = self
while (parent_element = element.parent)
parent_scroll_position += parent_element.scroll_position
element = parent_element
end
elements = hit_element?(window.mouse_x - parent_scroll_position.x, window.mouse_y - parent_scroll_position.y)
return @tip unless elements
elements.delete(self) # prevent infinite recursive loop (Widget#tip)
elements.reverse.find { |e| !e.tip.empty? }&.tip || @tip
end
end
end
end

View File

@@ -13,7 +13,7 @@ module CyberarmEngine
def publish(event, *args)
raise ArgumentError, "#{self.class} does not handle #{event.inspect}" unless @event_handler.include?(event)
return unless enabled?
return if !enabled? && event != :changed
was_handled = false

View File

@@ -6,6 +6,7 @@ module CyberarmEngine
def initialize(options = {})
@options = options
@game_objects = []
@timers = []
@global_pause = false
@down_keys = {}
@@ -17,6 +18,10 @@ module CyberarmEngine
@active_width = window.width
@active_height = window.height
@pages = {}
@page_host = nil
@page = nil
@menu = nil
@focus = nil
@mouse_over = nil
@@ -26,6 +31,7 @@ module CyberarmEngine
@dragging_element = nil
@pending_recalculate_request = false
@pending_element_recalculate_requests = []
@hit_elements = []
@needs_repaint = false
@@ -38,14 +44,45 @@ module CyberarmEngine
@tip = Element::ToolTip.new("", parent: @root_container, z: Float::INFINITY, theme: current_theme)
end
def menu
def active_menu
@menu
end
def page(klass, options = {})
@page&.blur
@pages[klass] = klass.new(parent: options[:parent] || self) unless @pages[klass]
@page = @pages[klass]
@page.options = options
page_host.clear do
@page.setup
end
@page.focus
end
def current_page
@page
end
def page_host
return @page.parent if @page.parent.is_a?(CyberarmEngine::Element::Container)
return @page_host if @page_host && @page_host.is_a?(CyberarmEngine::Element::Container)
@root_container
end
def page_host=(container)
raise "page host must be a CyberarmEngine::Element::Container" unless container.is_a?(Container)
@page_host = container
end
# throws :blur event to focused element and sets GuiState focused element
# Does NOT throw :focus event at element or set element as focused
def focus=(element)
@focus.publish(:blur) if @focus && element && @focus != element
@hit_elements.delete(@focus)
@focus = element
end
@@ -54,24 +91,37 @@ module CyberarmEngine
end
def draw
# t = Gosu.milliseconds
# report_recalculate_time = @pending_recalculate_request || @pending_element_recalculate_requests.size.positive?
StackProf.start(mode: :wall) if RUBY_ENGINE != "mruby" && defined?(StackProf)
Stats.frame.start_timing(:gui_element_recalculate_requests)
# puts "PENDING REQUESTS: #{@pending_element_recalculate_requests.size}" if @pending_element_recalculate_requests.size.positive?
@pending_element_recalculate_requests.each(&:recalculate)
@pending_element_recalculate_requests.clear
while (e = @pending_element_recalculate_requests.shift)
# its pointless to recalculate individual elements when we've been asked to recalculate the whole gui
if @pending_recalculate_request
@pending_element_recalculate_requests.clear
break
end
e.recalculate
end
Stats.frame.end_timing(:gui_element_recalculate_requests)
if @pending_recalculate_request
Stats.frame.start_timing(:gui_recalculate)
@root_container.recalculate
Stats.frame.start_timing(:gui_recalculate)
while(@pending_recalculate_request)
@pending_recalculate_request = false
Stats.frame.end_timing(:gui_recalculate)
@root_container.recalculate
end
StackProf.stop if RUBY_ENGINE != "mruby" && defined?(StackProf)
Stats.frame.end_timing(:gui_recalculate)
# puts "TOOK: #{Gosu.milliseconds - t}ms to recalculate #{self.class}:0x#{object_id.to_s(16)}" if report_recalculate_time && Gosu.milliseconds - t > 0
super
if @menu
@@ -92,6 +142,8 @@ module CyberarmEngine
end
@needs_repaint = false
@page&.draw
end
def needs_repaint?
@@ -121,22 +173,37 @@ module CyberarmEngine
return unless window.has_focus?
return unless window.current_state == self
new_mouse_over = @menu.hit_element?(window.mouse_x, window.mouse_y) if @menu
new_mouse_over ||= @root_container.hit_element?(window.mouse_x, window.mouse_y)
# list of containers decending down to hit element
new_hit_elements = (@menu || @root_container).hit_element?(window.mouse_x, window.mouse_y)
# element the mouse is over, if any.
new_mouse_over = new_hit_elements&.find { |c| c.is_a?(CyberarmEngine::Element::Widget) } || new_hit_elements&.last
if new_mouse_over
new_mouse_over.publish(:enter) if new_mouse_over != @mouse_over
new_mouse_over.publish(:hover)
# puts "#{new_mouse_over.class}[#{new_mouse_over.value}]: #{new_mouse_over.x}:#{new_mouse_over.y} #{new_mouse_over.width}:#{new_mouse_over.height}" if new_mouse_over != @mouse_over
# is the currently hit element the same as last hit element?
same_element = @mouse_over && new_mouse_over == @mouse_over
unless @hit_elements == new_hit_elements
added_hit_elements = (new_hit_elements || []) - @hit_elements
removed_hit_elements = @hit_elements - (new_hit_elements || [])
added_hit_elements.each do |e|
e.publish(:enter)
e.publish(:hover)
end
removed_hit_elements.each do |e|
e.publish(:leave)
end
end
@mouse_over.publish(:leave) if @mouse_over && new_mouse_over != @mouse_over
@mouse_over = new_mouse_over
@hit_elements = new_hit_elements || []
redirect_holding_mouse_button(:left) if @mouse_over && Gosu.button_down?(Gosu::MS_LEFT)
redirect_holding_mouse_button(:middle) if @mouse_over && Gosu.button_down?(Gosu::MS_MIDDLE)
redirect_holding_mouse_button(:right) if @mouse_over && Gosu.button_down?(Gosu::MS_RIGHT)
if Vector.new(window.mouse_x, window.mouse_y) == @last_mouse_pos
# handle tooltip
if same_element
if @mouse_over && (Gosu.milliseconds - @mouse_moved_at) > tool_tip_delay
@tip.value = @mouse_over.tip if @mouse_over
@tip.x = window.mouse_x
@@ -156,6 +223,8 @@ module CyberarmEngine
@last_mouse_pos = Vector.new(window.mouse_x, window.mouse_y)
@mouse_pos = @last_mouse_pos.clone
@page&.update
end
def button_down(id)
@@ -170,9 +239,14 @@ module CyberarmEngine
redirect_mouse_button(:right)
when Gosu::KB_F5
request_recalculate
when Gosu::KB_F12
CyberarmEngine.const_set(:GUI_DEBUG, !CyberarmEngine::GUI_DEBUG)
request_repaint
end
@focus.button_down(id) if @focus.respond_to?(:button_down)
@page&.button_down(id)
end
def button_up(id)
@@ -203,6 +277,8 @@ module CyberarmEngine
# Prevents menu from popping back up if the listbox is clicked to hide it.
@hid_menu_for = nil
@page&.button_up(id)
end
def tool_tip_delay
@@ -213,6 +289,7 @@ module CyberarmEngine
hide_menu unless @menu && (@menu == @mouse_over) || (@mouse_over&.parent == @menu)
if @focus && @mouse_over != @focus
@hit_elements.delete(@focus)
@focus.publish(:blur)
@focus = nil
end
@@ -266,10 +343,14 @@ module CyberarmEngine
end
def redirect_scroll_jump_to(edge)
return if window.text_input
@mouse_over.publish(:"scroll_jump_to_#{edge}", window.mouse_x, window.mouse_y) if (@mouse_over && !@menu) || (@mouse_over && @mouse_over == @menu)
end
def redirect_scroll_page(edge)
return if window.text_input
@mouse_over.publish(:"scroll_page_#{edge}", window.mouse_x, window.mouse_y) if (@mouse_over && !@menu) || (@mouse_over && @mouse_over == @menu)
end

View File

@@ -0,0 +1,39 @@
module CyberarmEngine
class Page
include CyberarmEngine::DSL
include CyberarmEngine::Common
attr_reader :parent
def initialize(parent:)
@parent = parent
@options = {}
end
def options=(options)
@options = options
end
def setup
end
def focus
end
def blur
end
def draw
end
def update
end
def button_down(id)
end
def button_up(id)
end
end
end

View File

@@ -16,35 +16,79 @@ module Gosu
end
module CyberarmEngine
class Style
attr_reader :hash
class StyleData
def initialize(hash = {})
h = hash
# h = Marshal.load(Marshal.dump(hash))
h[:default] = {}
h.each do |key, value|
next if value.is_a?(Hash)
h[:default][key] = value
end
@hash = h
@hash = hash
@dirty = false
end
def method_missing(method, *args)
if method.to_s.end_with?("=")
raise "Did not expect more than 1 argument" if args.size > 1
%i[
x y z design_width width height min_width min_height max_width max_height color background
background_image background_image_mode background_image_color
background_nine_slice background_nine_slice_mode background_nine_slice_color background_nine_slice_from_edge
background_nine_slice_left background_nine_slice_top background_nine_slice_right background_nine_slice_bottom
border_color border_color_left border_color_right border_color_top border_color_bottom
border_thickness border_thickness_left border_thickness_right border_thickness_top border_thickness_bottom
padding padding_left padding_right padding_top padding_bottom
margin margin_left margin_right margin_top margin_bottom
aspect_ratio
@hash[method.to_s.sub("=", "").to_sym] = args.first
elsif args.empty?
@hash[method]
else
raise ArgumentError, "Did not expect arguments"
fraction_background scroll fill text_wrap v_align h_align delay tag font
text_align text_static text_size
text_shadow text_shadow_size text_shadow_alpha text_shadow_color
text_border text_border_size text_border_alpha text_border_color
image_width image_height
].each do |item|
define_method(item) do
@hash[item]
end
define_method(:"#{item}=") do |value|
@dirty = true if @hash[item] != value
@hash[item] = value
end
end
# NOTE: do not change return value
def default
nil
end
def dirty?
@dirty
end
def mark_clean!
@dirty = false
end
def empty?
@hash.empty?
end
end
class Style < StyleData
attr_reader :hash, :hover, :active, :disabled
def initialize(hash = {})
@hash = hash
@hover = StyleData.new(hash[:hover] || {})
@active = StyleData.new(hash[:active] || {})
@disabled = StyleData.new(hash[:disabled] || {})
@substyles = [@hover, @active, @disabled]
super
end
def dirty?
@dirty || @substyles.any?(&:dirty?)
end
def mark_clean!
@substyles.each(&:mark_clean!)
@dirty = false
end
end
end

View File

@@ -30,6 +30,11 @@ module CyberarmEngine
end
end
# css-like style classes, however First In, First Out- that is,
options[:style_class]&.each do |klass|
hash = deep_merge(hash, _theme[klass]) if _theme[klass]
end
deep_merge(hash, options)
end
@@ -53,6 +58,7 @@ module CyberarmEngine
y: 0,
z: 30,
design_width: nil,
width: nil,
height: nil,
color: Gosu::Color::WHITE,
@@ -68,25 +74,25 @@ module CyberarmEngine
debug_color: Gosu::Color::YELLOW
},
Button: { # < Label
Button: { # < TextBlock
margin: 1,
padding: 4,
border_thickness: 1,
border_color: ["ffd59674".hex, "ffff8746".hex],
border_color: [0xffd59674, 0xffff8746],
border_radius: 0,
background: ["ffc75e61".to_i(16), "ffe26623".to_i(16)],
background: [0xffc75e61, 0xffe26623],
text_align: :center,
text_v_align: :center,
text_wrap: :none,
hover: {
color: Gosu::Color.rgb(200, 200, 200),
background: ["ffB23E41".to_i(16), "ffFF7C00".to_i(16)]
background: [0xffB23E41, 0xffFF7C00]
},
active: {
color: Gosu::Color::BLACK,
background: ["ffB23E41".to_i(16)]
background: [0xffB23E41]
},
disabled: {
@@ -121,7 +127,13 @@ module CyberarmEngine
text_size: 28,
text_wrap: :word_wrap, # :word_wrap, :break_word, :none
text_shadow: false,
text_shadow_color: 0,
text_shadow_alpha: 30,
text_shadow_size: 2,
text_border: false,
text_border_color: 0,
text_border_alpha: 30,
text_border_size: 2,
text_align: :left,
font: "Arial",
margin: 0,

View File

@@ -1,4 +1,4 @@
module CyberarmEngine
NAME = "InDev".freeze
VERSION = "0.24.4".freeze
VERSION = "0.25.1".freeze
end