├── .travis.yml ├── spec ├── spec_helper.cr └── Glass_spec.cr ├── src ├── Glass │ └── version.cr ├── UI │ ├── Example.cr │ ├── Widget.cr │ └── Container.cr ├── Percent │ └── Percent.cr ├── Point │ └── Point.cr ├── Main.cr ├── Color │ └── Color.cr ├── Window │ └── Window.cr └── Image │ └── Image.cr ├── .gitignore ├── img └── example_window.png ├── shard.lock ├── .editorconfig ├── shard.yml ├── LICENSE └── README.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: crystal 2 | -------------------------------------------------------------------------------- /spec/spec_helper.cr: -------------------------------------------------------------------------------- 1 | require "spec" 2 | require "../src/Glass" 3 | -------------------------------------------------------------------------------- /src/Glass/version.cr: -------------------------------------------------------------------------------- 1 | module Glass 2 | VERSION = "0.1.0" 3 | end 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /doc/ 2 | /lib/ 3 | /bin/ 4 | /.shards/ 5 | Main 6 | *.dwarf 7 | -------------------------------------------------------------------------------- /img/example_window.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nilsmartel/Glass/HEAD/img/example_window.png -------------------------------------------------------------------------------- /shard.lock: -------------------------------------------------------------------------------- 1 | version: 2.0 2 | shards: 3 | crsfml: 4 | git: https://github.com/oprypin/crsfml.git 5 | version: 2.5.0 6 | 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cr] 2 | charset = utf-8 3 | end_of_line = lf 4 | insert_final_newline = true 5 | indent_style = space 6 | indent_size = 2 7 | trim_trailing_whitespace = true 8 | -------------------------------------------------------------------------------- /spec/Glass_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | describe Glass do 4 | # TODO: Write tests 5 | 6 | it "works" do 7 | false.should eq(true) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /shard.yml: -------------------------------------------------------------------------------- 1 | name: Glass 2 | version: 0.1.0 3 | 4 | authors: 5 | - sombrastudios 6 | 7 | dependencies: 8 | crsfml: 9 | github: oprypin/crsfml 10 | 11 | 12 | targets: 13 | Glass: 14 | main: ./src/Main.cr 15 | 16 | crystal: 0.35.0 17 | 18 | license: MIT 19 | -------------------------------------------------------------------------------- /src/UI/Example.cr: -------------------------------------------------------------------------------- 1 | require "./Widget" 2 | 3 | module Glass 4 | property background_color : SF::Color 5 | 6 | class Example < Widget 7 | @width = 64_u32 8 | @height = 64_u32 9 | 10 | def initialize(@background_color : SF::Color) 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /src/Percent/Percent.cr: -------------------------------------------------------------------------------- 1 | module Glass 2 | struct Percent 3 | @value : Float64 4 | 5 | def initialize(value : Number) 6 | @value = value.as_f64 7 | end 8 | 9 | def *(other : Number) : Float64 10 | other.to_f64 * @value 11 | end 12 | 13 | # Printing related Methods 14 | 15 | def to_s 16 | (@value * 100.0).to_i32.to_s 17 | end 18 | 19 | def to_s(io) 20 | io << @to_s 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /src/Point/Point.cr: -------------------------------------------------------------------------------- 1 | module Glass 2 | struct Point 3 | property x : Int32 4 | property y : Int32 5 | 6 | def initialize(@x, @y) 7 | end 8 | 9 | # def initialize(px, py : Numbers) 10 | # @x = px.to(Int32) 11 | # @y = py.to(Int32) 12 | # end 13 | 14 | def +(p : Point) : Point 15 | Point.new x + p.x, y + p.y 16 | end 17 | 18 | def -(p : Point) : Point 19 | Point.new x - p.x, y - p.y 20 | end 21 | 22 | def *(s) : Point 23 | Point.new x*s, y*s 24 | end 25 | 26 | def abs : Point 27 | Point.new x.abs, y.abs 28 | end 29 | 30 | # TODO add Bunch of Methods for Subtracting, Adding etc. 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /src/Main.cr: -------------------------------------------------------------------------------- 1 | require "crsfml" 2 | require "./Glass/*" 3 | require "./Color" 4 | require "./Image" 5 | require "./UI/*" 6 | require "./Window/Window" 7 | 8 | module Glass 9 | # create the root widget 10 | widget = new_ui 11 | # create the window 12 | window = Window.new "Glass Window", widget 13 | 14 | window.run 15 | end 16 | 17 | def new_ui : Glass::Widget 18 | img = SF::Image.new 256, 256 19 | 20 | ui = Glass::AbsolutContainer.new img 21 | ui.background_color = SF::Color.new 64_u8, 64_u8, 64_u8 22 | container = Glass::VerticalContainer.new 23 | container.add_widget(Glass::Example.new SF::Color.new 128, 64, 32) 24 | container.add_widget(Glass::Example.new SF::Color.new 32, 128, 64) 25 | container.add_widget(Glass::Example.new SF::Color.new 64, 32, 128) 26 | 27 | ui.add_widget container 28 | ui 29 | end 30 | -------------------------------------------------------------------------------- /src/Color/Color.cr: -------------------------------------------------------------------------------- 1 | # This File simply extends the functionality of SF::Color 2 | # for later use 3 | # 4 | # I'm pretty sure this will come in handy later 5 | module SF 6 | struct Color 7 | def from_hsv(h, s, v : Float32) : SF::Color 8 | return new v*255, v*255, v*255 if s == 0 9 | 10 | q = if l < 0.5 11 | l * (1_f32 + s) 12 | else 13 | l + s - l*s 14 | end 15 | 16 | p = 2_f32 * l - q 17 | 18 | r = (hue_to_rgb p, q, h + 1_f32/3_f32) * 255 19 | g = (hue_to_rgb p, q, h) * 255 20 | b = (hue_to_rgb p, q, h - 1_f32/3_f32) * 255 21 | 22 | return new r.as(UInt8), g.as(UInt8), b.as(UInt8) 23 | end 24 | 25 | private def hue_to_rgb(p, q, t : Float32) : Float32 26 | t += 1 if t < 0 27 | t -= 1 if t > 1 28 | return p + (q - p) * 6_f32 * t if t < 1_f32 / 6_f32 29 | return q if t < 0.5_f32 30 | return p + (q - p) * (2_f32/3_f32 - t) * 6_f32 if t < 2_f32 / 3_f32 31 | 32 | return p 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 sombrastudios 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Window/Window.cr: -------------------------------------------------------------------------------- 1 | require "crsfml" 2 | require "../UI/Widget" 3 | 4 | module Glass 5 | class Window 6 | @window : SF::RenderWindow 7 | @widget : Widget = Widget.new(nil) 8 | 9 | def initialize(title : String, width : Int32, height : Int32) 10 | @window = SF::RenderWindow.new( 11 | SF::VideoMode.new(width, height), title 12 | ) 13 | end 14 | 15 | def initialize(title : String, @widget : Widget) 16 | @window = SF::RenderWindow.new( 17 | SF::VideoMode.new( 18 | @widget.width.to_i32, 19 | @widget.height.to_i32 20 | ), 21 | title 22 | ) 23 | end 24 | 25 | # Start Window and poll events etc. 26 | def run : Nil 27 | while @window.open? 28 | while event = @window.poll_event 29 | puts event 30 | 31 | @window.close if event.is_a? SF::Event::Closed 32 | end 33 | 34 | # # TODO in theory events determine soley which part of the GUI needs to be rerendered 35 | render 36 | end 37 | end 38 | 39 | def set_widget(widget : Widget) 40 | @widget = widget 41 | end 42 | 43 | def render : Nil 44 | @widget.render 45 | 46 | unless (img = @widget.get_image).nil? 47 | sprite = SF::Sprite.new SF::Texture.from_image img 48 | @window.draw sprite 49 | end 50 | 51 | @window.display 52 | end 53 | 54 | def display 55 | if @window.open? 56 | @window.display 57 | else 58 | raise "Tried to display closed window" 59 | end 60 | end 61 | 62 | def open? 63 | @window.open? 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Glass 2 | 3 | (W.I.P) GUI Library written in pure Crystal 4 | 5 | --- 6 | 7 | ## TODO 8 | - Completly change positioning and stretching system. At the moment I'm solely focusing on the minimal window size it seems 9 | 10 | 11 | ## Creating a simple Window 12 | in order to create a simple Window one first needs to set up the UI using widgets 13 | 14 | In order to do this I will just define a fairly simple function 15 | ```ruby 16 | def new_ui() : Glass::Widget 17 | img = SF::Image.new(256, 256) 18 | ui = Glass::AbsolutContainer.new(img) 19 | ui.background_color = SF::Color.new(64_u8, 64_u8, 64_u8) 20 | container = Glass::VerticalContainer.new() 21 | container + Glass::Example.new(128_u8, 64_u8, 32_u8) 22 | container + Glass::Example.new(32_u8, 128_u8, 64_u8) 23 | container + Glass::Example.new(64_u8, 32_u8, 128_u8) 24 | ui + container 25 | ui 26 | end 27 | ``` 28 | 29 | Starting the window now is pretty easy! 30 | 31 | ```ruby 32 | widget = new_ui 33 | 34 | window = Window.new "Glass Window", widget 35 | 36 | window.run 37 | ``` 38 | 39 | the application should end up looking like this 40 | 41 | ![Example Application](./img/example_window.png "Example Window") 42 | 43 | *Note: this is heavily W.I.P. and no useful functionality has been exposed yet* 44 | 45 | ## Contributing 46 | 47 | 1. Fork it ( https://github.com/nilsmartel/Glass/fork ) 48 | 2. Create your feature branch (git checkout -b my-new-feature) 49 | 3. Commit your changes (git commit -am 'Add some feature') 50 | 4. Push to the branch (git push origin my-new-feature) 51 | 5. Create a new Pull Request 52 | 53 | ## Contributors 54 | 55 | * [[nilsmartel]](https://github.com/nilsmartel) nilsmartel - creator, maintainer 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/Image/Image.cr: -------------------------------------------------------------------------------- 1 | require "../Color" 2 | require "../Point" 3 | require "crsfml" 4 | 5 | module Glass 6 | # Represents an (Cropped) Sector from an Image 7 | struct ImageClip 8 | property pos : Point 9 | property width : UInt32 10 | property height : UInt32 11 | @image : SF::Image 12 | 13 | def get_image : SF::Image 14 | @image 15 | end 16 | 17 | # Create an ImageClip from an SF::Image 18 | def initialize(@image : SF::Image) 19 | @pos = Point.new 0, 0 20 | @width = @image.size.x.to_u32 21 | @height = @image.size.y.to_u32 22 | end 23 | 24 | def initialize(@image, @pos, @width, @height) 25 | end 26 | 27 | def set_pixel(x, y : Int, c : SF::Color) 28 | if (check_bounds x, y) # && check_bounds x, y, @image.size 29 | x += @pos.x 30 | y += @pos.y 31 | @image.set_pixel x, y, c 32 | end 33 | end 34 | 35 | def set_pixel(pos : Point, c : SF::Color) 36 | set_pixel pos.x, pos.y, c 37 | end 38 | 39 | # Returns a Cropped Clip, that's in the Bounds of this Clip 40 | # and relative to it's position 41 | def get_clip(p : Point, w, h : UInt32) : ImageClip 42 | if p.x > @width 43 | w = 0 44 | elsif p.x + w > @width 45 | w = @width - p.x 46 | end 47 | 48 | if p.y > @height 49 | h = 0 50 | elsif p.y + h > @height 51 | h = @height - p.y 52 | end 53 | 54 | ImageClip.new @image, pos + p, w.to_u32, h.to_u32 55 | end 56 | 57 | private def check_bounds(x, y : Number) : Bool 58 | check_bounds x, y, @width, @height 59 | end 60 | 61 | # private def check_bounds(x, y : Number, v : SF::Vector2u) : Bool 62 | # check_bounds x, y, v.x.to_i32, v.y.to_i32 63 | # end 64 | 65 | private def check_bounds(pos : Point, width, height : Number) : Bool 66 | return check_bounds pos.x, pos.y, width, height 67 | end 68 | 69 | private def check_bounds(x, y, width, height : Number) : Bool 70 | return !(x < 0 || x >= width || y < 0 || y >= height) 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /src/UI/Widget.cr: -------------------------------------------------------------------------------- 1 | require "crsfml" 2 | require "../Percent" 3 | require "../Image" 4 | 5 | module Glass 6 | class Widget 7 | # Unique ID used to indentify this widget. 8 | @id : String | Nil = nil 9 | # Class name shared by multiple Widgets to determine them 10 | @class : String | Nil = nil 11 | 12 | @width : UInt32 | Nil | Percent = nil 13 | @height : UInt32 | Nil | Percent = nil 14 | @parent : Widget | Nil = nil 15 | property image : ImageClip | Nil 16 | property background_color = SF::Color.new(128_u8, 128_u8, 128_u8, 255_u8) 17 | 18 | def initialize(@image) 19 | end 20 | 21 | def id 22 | @id 23 | end 24 | 25 | def class 26 | @class 27 | end 28 | 29 | def min_width : UInt32 30 | 0 31 | end 32 | 33 | def min_height : UInt32 34 | 0 35 | end 36 | 37 | # Returns width of Widget or 0 (if width equals nil) 38 | def width : UInt32 39 | unless (w = @width).nil? 40 | if w.is_a?(UInt32) 41 | return w 42 | else 43 | if (p = @parent).is_a?(Widget) 44 | return (w * p.width).to_u32 45 | else 46 | raise "width of Widget could not be determined" 47 | end 48 | end 49 | end 50 | 51 | 0_u32 52 | end 53 | 54 | # Returns height of Widget or 0 (if height equals nil) 55 | def height : UInt32 56 | unless (h = @height).nil? 57 | if h.is_a?(UInt32) 58 | return h 59 | else 60 | if (p = @parent).is_a?(Widget) 61 | return (h * p.height).to_u32 62 | else 63 | raise "Height of Widget could not be determined" 64 | end 65 | end 66 | end 67 | 68 | 0_u32 69 | end 70 | 71 | def set_parent(w : Widget | Nil) 72 | @parent = w 73 | end 74 | 75 | # Sets the Position Relative to it's parent and updates it's ImageClip 76 | def set_pos(x, y : Int32) 77 | unless (p = @parent).nil? 78 | unless (i = p.image).nil? 79 | @image = i.get_clip Point.new(x, y), width, height 80 | end 81 | end 82 | end 83 | 84 | def set_pos(p : Point) 85 | set_pos p.x, p.y 86 | end 87 | 88 | def set_pos 89 | unless (p = @parent).nil? 90 | unless (i = p.image).nil? 91 | @image = i.get_clip get_pos, width, height 92 | end 93 | end 94 | end 95 | 96 | def get_pos : Point 97 | unless (i = @image).nil? 98 | return i.pos 99 | end 100 | 101 | # TODO does this even make sense? 102 | Point.new(0, 0) 103 | end 104 | 105 | # Render Widget 106 | def render 107 | # Make sure you only iterate over the pixels you truly need 108 | unless (i = @image).nil? 109 | w = min width, i.width 110 | h = min height, i.height 111 | # Iterate over each vertical and horizontal pixel of this container 112 | (0...w).each do |x| 113 | (0...h).each do |y| 114 | i.set_pixel x, y, @background_color 115 | end 116 | end 117 | end 118 | end 119 | 120 | def get_image : SF::Image 121 | if (i = @image).is_a?(ImageClip) 122 | return i.get_image 123 | else 124 | raise "Couldn't retrieve Image" 125 | end 126 | end 127 | 128 | # TODO 129 | # def hover, click, keyboard, focus etc... 130 | # this is going to be a shitload of work 131 | # 132 | # will be fun anyways, but non-the-less. 133 | # It will take some time untill I implement this 134 | end 135 | end 136 | 137 | # returns the greater of two Numbers 138 | private def max(a, b : UInt32) : UInt32 139 | a > b ? a : b 140 | end 141 | 142 | # returns the smaller of two Numbers 143 | private def min(a, b : UInt32) : UInt32 144 | a < b ? a : b 145 | end 146 | -------------------------------------------------------------------------------- /src/UI/Container.cr: -------------------------------------------------------------------------------- 1 | 2 | require "./Widget" 3 | 4 | # Implement function to set position of children! 5 | # Called when Parents position gets setted 6 | 7 | module Glass 8 | 9 | class Container < Widget 10 | @childs : Array(Widget) = [] of Widget 11 | # maps (string) ids to the direct children of this container. 12 | @child_map = {} of String => Widget 13 | 14 | 15 | def initialize() 16 | @width = nil 17 | @height= nil 18 | end 19 | 20 | def get_image() : SF::Image | Nil 21 | unless (i = @image).nil? 22 | return i.get_image 23 | end 24 | 25 | end 26 | 27 | def set_height(y : UInt32 | Nil) 28 | @height = y 29 | @image = parent.image.get_clip @image.pos, width, height 30 | end 31 | 32 | def set_width(x : UInt32 | Nil) 33 | @width = x 34 | @image = parent.image.get_clip @image.pos, width, height 35 | end 36 | 37 | # Sets the Position Relative to it's parent and updates it's ImageClip 38 | # Furthermore this method calls `set_pos` on all of it's children 39 | def set_pos(x, y : Int32) 40 | super x, y 41 | childs = @childs 42 | @childs = [] of Widget 43 | childs.each do |widget| 44 | self + widget 45 | end 46 | end 47 | 48 | def set_pos(p : Point) 49 | set_pos p.x, p.y 50 | end 51 | 52 | def render() 53 | super 54 | 55 | @childs.each do |widget| 56 | widget.render 57 | end 58 | end 59 | 60 | def render_test() 61 | (0...256).each do |x| 62 | (0...256).each do |y| 63 | if (x * y) & 1 == 0 64 | @image.set_pixel x, y, background_color 65 | end 66 | end 67 | end 68 | end 69 | 70 | # Method to recalculte position and size of all childs 71 | # and furthermore, assign adjusted ImageClips 72 | def on_update() 73 | end 74 | 75 | def min_width() : UInt32 76 | width 77 | end 78 | 79 | def min_height() : UInt32 80 | height 81 | end 82 | 83 | 84 | # TODO is this the right way to pass a list of Widgets? 85 | def add_widget(*widgets : Widget) 86 | widgets.each do |widget| 87 | 88 | if (id = widget.id).is_a?(String) 89 | @child_map[id] = widget 90 | end 91 | 92 | widget.set_parent self 93 | widget.set_pos 0, 0 94 | @childs << widget 95 | end 96 | end 97 | 98 | def get_widget(id : String) : Widget | Nil 99 | if @child_map.includes?(id) 100 | return @child_map[id] 101 | end 102 | 103 | return @childs.find do |widget| 104 | if (found = widget.get_widget id).is_a?(Widget) 105 | return found 106 | end 107 | return nil 108 | end 109 | end 110 | end 111 | 112 | class AbsolutContainer < Container 113 | 114 | def initialize(i : ImageClip) 115 | @width = i.width 116 | @height = i.height 117 | @image = i 118 | end 119 | 120 | def initialize(image : SF::Image) 121 | i = ImageClip.new image 122 | @width = i.width 123 | @height = i.height 124 | @image = i 125 | end 126 | 127 | # Add a Widget to the Container at Coordinates (x, y) 128 | def add_widget(widget, x, y : Int32) 129 | widget.set_parent self 130 | widget.set_pos x, y 131 | @childs << widget 132 | end 133 | 134 | # Move all Elements inside of the Container 135 | def move_content(p : Point) 136 | @childs.each do |widget| 137 | widget.set_pos widget.get_pos + p 138 | end 139 | end 140 | 141 | def move_content(x, y : Int32) 142 | move_content Point.new x, y 143 | end 144 | end 145 | 146 | class VerticalContainer < Container 147 | @widget_height : UInt32 = 0_u32 148 | 149 | def add_widget(*widgets : Widget) 150 | widgets.each do |widget| 151 | widget.set_parent self 152 | widget.set_pos 0, @widget_height.to_i32 153 | @widget_height += widget.height 154 | @childs << widget 155 | end 156 | end 157 | 158 | def set_pos(x, y : Int32) 159 | @widget_height = 0_u32 160 | super x, y 161 | end 162 | 163 | def height() : UInt32 164 | unless (y = @height).nil? 165 | return super 166 | end 167 | 168 | h = 0_u32 169 | 170 | @childs.each do |widget| 171 | h += widget.height 172 | end 173 | 174 | h 175 | end 176 | 177 | def width() : UInt32 178 | unless (x = @width).nil? 179 | return super 180 | end 181 | w = 0_u32 182 | 183 | @childs.each do |widget| 184 | w = widget.width if widget.width > w 185 | end 186 | 187 | w 188 | end 189 | end 190 | 191 | class HorizontalContainer < Container 192 | @widget_width : UInt32 = 0_u32 193 | 194 | def add_widget(*widgets : Widget) 195 | widgets.each do |widget| 196 | widget.set_parent self 197 | widget.set_pos @widget_width.to_i32, 0 198 | @widget_width += widget.width 199 | @childs << widget 200 | end 201 | end 202 | 203 | def set_pos(x, y : Int32) 204 | @widget_width = 0_u32 205 | super x, y 206 | end 207 | 208 | def height() : UInt32 209 | unless (y = @height).nil? 210 | return super 211 | end 212 | 213 | h = 0_u32 214 | 215 | @childs.each do |widget| 216 | h = widget.height if widget.height > h 217 | end 218 | 219 | h 220 | end 221 | 222 | def width() : UInt32 223 | unless (x = @width).nil? 224 | return super 225 | end 226 | 227 | w : UInt32 = 0_u32 228 | 229 | @childs.each do |widget| 230 | w += widget.width 231 | end 232 | 233 | w 234 | end 235 | end 236 | end 237 | 238 | ## TODO: Create scrollable Containers with fixed min_height | min_width 239 | 240 | --------------------------------------------------------------------------------