From dbc8ee7b24241005480f99da03cff3542ff9c061 Mon Sep 17 00:00:00 2001 From: Brendan Hansen Date: Thu, 10 Jun 2021 23:44:44 -0500 Subject: [PATCH] added sliders and other core module things --- core/math.onyx | 6 ++ modules/bmfont/module.onyx | 1 + modules/bmfont/position.onyx | 95 +++++++++++++++++++ modules/bmfont/utils.onyx | 1 - .../immediate_mode/immediate_renderer.onyx | 26 +++++ modules/ui/components/button.onyx | 32 +++---- modules/ui/components/checkbox.onyx | 48 ++++------ modules/ui/components/slider.onyx | 91 ++++++++++++++++++ modules/ui/components/textbox.onyx | 6 ++ modules/ui/module.onyx | 21 +++- modules/ui/ui.onyx | 51 ++++------ 11 files changed, 297 insertions(+), 81 deletions(-) create mode 100644 modules/bmfont/position.onyx create mode 100644 modules/ui/components/slider.onyx create mode 100644 modules/ui/components/textbox.onyx diff --git a/core/math.onyx b/core/math.onyx index cd212851..c330fc97 100644 --- a/core/math.onyx +++ b/core/math.onyx @@ -293,3 +293,9 @@ ctz :: proc { wasm.ctz_i32, wasm.ctz_i64 } popcnt :: proc { wasm.popcnt_i32, wasm.popcnt_i64 } rotate_left :: proc { wasm.rotl_i32, wasm.rotl_i64 } rotate_right :: proc { wasm.rotr_i32, wasm.rotr_i64 } + + + +lerp :: (t: f32, a: $T, b: T) -> T { + return ~~(~~a * (1 - t) + ~~b * t); +} \ No newline at end of file diff --git a/modules/bmfont/module.onyx b/modules/bmfont/module.onyx index 2adacab3..a994d2ef 100644 --- a/modules/bmfont/module.onyx +++ b/modules/bmfont/module.onyx @@ -17,4 +17,5 @@ package bmfont #load "./types" #load "./bmfont_loader" #load "./utils" +#load "./position" diff --git a/modules/bmfont/position.onyx b/modules/bmfont/position.onyx new file mode 100644 index 00000000..d598e796 --- /dev/null +++ b/modules/bmfont/position.onyx @@ -0,0 +1,95 @@ +package bmfont + +#private_file math :: package core.math + +#private_file Renderable_Glyph :: struct { + pos_x, pos_y : f32; + pos_w, pos_h : f32; + + tex_x, tex_y : f32; + tex_w, tex_h : f32; +} +#private_file renderable_glyph: Renderable_Glyph; + +#private_file Rendering_Context :: struct { + font : ^BMFont; + size : f32; + + x, y : f32; + + original_x : f32; + baseline : f32; + line_height : f32; + + text : str; + text_position : u32; +} +#private_file rendering_context: Rendering_Context; + +@FontSizing // Currently, `size` is just a multipler for the baked font size. This should be changed to be height in pixels, or 'em's. +@Rename +get_character_positions :: (font: ^BMFont, size: f32, text: str, x: f32, y: f32) -> Iterator(^Renderable_Glyph) { + rendering_context.font = font; + rendering_context.size = size; + + rendering_context.x, rendering_context.y = x, y; + + rendering_context.original_x = x; + rendering_context.baseline = ~~font.common.baseline * size; + rendering_context.line_height = ~~font.common.line_height * size; + + rendering_context.text = text; + rendering_context.text_position = 0; + + next :: (data: rawptr) -> (^Renderable_Glyph, bool) { + rc := cast(^Rendering_Context) data; + char: u8; + + while true { + if rc.text_position >= rc.text.count { + return null, false; + } + + char = rc.text[rc.text_position]; + defer rc.text_position += 1; + + if char == #char "\n" { + rc.y += rc.line_height; + rc.x = rc.original_x; + continue; + } + + glyph := rc.font->get_glyph(char); + + if glyph == null { + glyph = rc.font->get_glyph(255); + assert(glyph != null, "NO NULL GLYPH"); + } + + renderable_glyph.pos_x = math.floor(rc.x + ~~glyph.xoffset * rc.size + .5); + renderable_glyph.pos_y = math.floor(rc.y + ~~glyph.yoffset * rc.size - rc.baseline + .5); + renderable_glyph.pos_w = math.floor(~~glyph.w * rc.size + .5); + renderable_glyph.pos_h = math.floor(~~glyph.h * rc.size + .5); + + renderable_glyph.tex_x = glyph.tex_x; + renderable_glyph.tex_y = glyph.tex_y; + renderable_glyph.tex_w = glyph.tex_w; + renderable_glyph.tex_h = glyph.tex_h; + + rc.x += ~~glyph.xadvance * rc.size; + + return ^renderable_glyph, true; + } + } + + close :: (data: rawptr) { + rc := cast(^Rendering_Context) data; + rc.text = null_str; + } + + return .{ + next = next, + close = close, + data = ^rendering_context, + }; +} \ No newline at end of file diff --git a/modules/bmfont/utils.onyx b/modules/bmfont/utils.onyx index 87f160a6..f01e1a32 100644 --- a/modules/bmfont/utils.onyx +++ b/modules/bmfont/utils.onyx @@ -26,7 +26,6 @@ get_width :: (use font: ^BMFont, text: str, size: f32) -> f32 { return math.max(max_x, x); } -@Incomplete // does not use the size parameter get_height :: (use font: ^BMFont, text: str, size: f32) -> f32 { line_count := 0; diff --git a/modules/immediate_mode/immediate_renderer.onyx b/modules/immediate_mode/immediate_renderer.onyx index 1c916c66..437be98e 100644 --- a/modules/immediate_mode/immediate_renderer.onyx +++ b/modules/immediate_mode/immediate_renderer.onyx @@ -218,6 +218,24 @@ Immediate_Renderer :: struct { gl.useProgram(active_shader.program); } + + @Cleanup // right now, these are raw screen coordinates, with x and y being the bottom left of the screen. + // This is backwards from what use_ortho_projections does, so it's not great to work with. + scissor :: (use ir: ^Immediate_Renderer, x: f32, y: f32, w: f32, h: f32) { + if !gl.isEnabled(gl.SCISSOR_TEST) { + ir->flush(); + gl.enable(gl.SCISSOR_TEST); + } + + gl.scissor(~~x, ~~y, ~~w, ~~h); + } + + scissor_disable :: (use ir: ^Immediate_Renderer) { + if gl.isEnabled(gl.SCISSOR_TEST) { + ir->flush(); + gl.disable(gl.SCISSOR_TEST); + } + } } @@ -262,3 +280,11 @@ use_ortho_projection :: (left: f32, right: f32, top: f32, bottom: f32) { use_alpha_shader :: (texture_id: i32 = -1) { immediate_renderer->use_alpha_shader(texture_id); } + +scissor :: (x: f32, y: f32, w: f32, h: f32) { + immediate_renderer->scissor(x, y, w, h); +} + +scissor_disable :: () { + immediate_renderer->scissor_disable(); +} \ No newline at end of file diff --git a/modules/ui/components/button.onyx b/modules/ui/components/button.onyx index c165a99c..835d5915 100644 --- a/modules/ui/components/button.onyx +++ b/modules/ui/components/button.onyx @@ -1,15 +1,6 @@ package ui use package core -// Button states are stored globally as there is not much to the state of a button. -// Forcing the end user to store a structure for each button that is just the animation -// state of the button feels very wrong. -#private button_states : map.Map(UI_Id, Button_State); -#private Button_State :: struct { - hover_time := 0.0f; - click_time := 0.0f; -} - Button_Theme :: struct { use text_theme := Text_Theme.{}; @@ -21,8 +12,7 @@ Button_Theme :: struct { border_width := 6.0f; @InPixels } -@Bug // there is a compile-time known bug if either of the 'Button_Theme's below are omitted. -default_button_theme: Button_Theme = Button_Theme.{}; +default_button_theme := Button_Theme.{}; @Themeing button :: (use r: Rectangle, text: str, theme := ^default_button_theme, site := #callsite, increment := 0) -> bool { @@ -31,13 +21,13 @@ button :: (use r: Rectangle, text: str, theme := ^default_button_theme, site := result := false; hash := get_site_hash(site, increment); - button_state := map.get(^button_states, hash); + animation_state := map.get(^animation_states, hash); if is_active_item(hash) { if mouse_state.left_button_just_up { if is_hot_item(hash) && Rectangle.contains(r, mouse_state.x, mouse_state.y) { result = true; - button_state.click_time = 1.0f; + animation_state.click_time = 1.0f; } set_active_item(0); @@ -54,9 +44,9 @@ button :: (use r: Rectangle, text: str, theme := ^default_button_theme, site := } if is_hot_item(hash) { - move_towards(^button_state.hover_time, 1.0f, 0.1f); @ThemeConfiguration + move_towards(^animation_state.hover_time, 1.0f, 0.1f); @ThemeConfiguration } else { - move_towards(^button_state.hover_time, 0.0f, 0.1f); @ThemeConfiguration + move_towards(^animation_state.hover_time, 0.0f, 0.1f); @ThemeConfiguration } border_width := theme.border_width; @@ -64,8 +54,8 @@ button :: (use r: Rectangle, text: str, theme := ^default_button_theme, site := gfx.rect(.{ x0, y0 }, .{ width, height }, theme.border_color); - surface_color := color_lerp(button_state.hover_time, theme.background_color, theme.hover_color); - surface_color = color_lerp(button_state.click_time, surface_color, theme.click_color); + surface_color := color_lerp(animation_state.hover_time, theme.background_color, theme.hover_color); + surface_color = color_lerp(animation_state.click_time, surface_color, theme.click_color); gfx.rect(.{ x0 + border_width, y0 + border_width }, .{ width - border_width * 2, height - border_width * 2 }, surface_color); text_width := bmfont.get_width(^font, text, theme.font_size); @@ -77,12 +67,12 @@ button :: (use r: Rectangle, text: str, theme := ^default_button_theme, site := y0 + ~~ font.common.baseline * theme.font_size + (height - text_height) / 2, theme.font_size, theme.text_color); - move_towards(^button_state.click_time, 0.0f, 0.08f); @ThemeConfiguration + move_towards(^animation_state.click_time, 0.0f, 0.08f); @ThemeConfiguration - if button_state.click_time > 0 || button_state.hover_time > 0 { - map.put(^button_states, hash, button_state); + if animation_state.click_time > 0 || animation_state.hover_time > 0 { + map.put(^animation_states, hash, animation_state); } else { - map.delete(^button_states, hash); + map.delete(^animation_states, hash); } return result; diff --git a/modules/ui/components/checkbox.onyx b/modules/ui/components/checkbox.onyx index 5cda0fdf..4acfd98f 100644 --- a/modules/ui/components/checkbox.onyx +++ b/modules/ui/components/checkbox.onyx @@ -1,42 +1,36 @@ package ui use package core -#private checkbox_states : map.Map(UI_Id, Checkbox_State); -#private Checkbox_State :: struct { - hover_time := 0.0f; - click_time := 0.0f; -} - Checkbox_Theme :: struct { use text_theme := Text_Theme.{}; - box_color := gfx.Color4.{ 0.2, 0.2, 0.2 }; - box_width := 4.0f; @InPixels - box_size := 20.0f; @InPixels + box_color := gfx.Color4.{ 0.2, 0.2, 0.2 }; + box_border_width := 4.0f; @InPixels + box_size := 20.0f; @InPixels checked_color := gfx.Color4.{ 1, 0, 0 }; - checked_hover_color := gfx.Color4.{ 1, 0.3, 0.3 }; + checked_hover_color := gfx.Color4.{ 1, 0.6, 0.6 }; background_color := gfx.Color4.{ 0.05, 0.05, 0.05 }; // Background of the checkbox button. hover_color := gfx.Color4.{ 0.3, 0.3, 0.3 }; click_color := gfx.Color4.{ 0.5, 0.5, 0.7 }; } -default_checkbox_theme: Checkbox_Theme = Checkbox_Theme.{}; +default_checkbox_theme := Checkbox_Theme.{}; @Themeing checkbox :: (use r: Rectangle, value: ^bool, text: str, theme := ^default_checkbox_theme, site := #callsite, increment := 0) -> bool { result := false; hash := get_site_hash(site, increment); - checkbox_state := map.get(^checkbox_states, hash); + animation_state := map.get(^animation_states, hash); if is_active_item(hash) { if mouse_state.left_button_just_up { if is_hot_item(hash) && Rectangle.contains(r, mouse_state.x, mouse_state.y) { result = true; *value = !*value; - checkbox_state.click_time = 1.0f; + animation_state.click_time = 1.0f; } set_active_item(0); @@ -53,15 +47,15 @@ checkbox :: (use r: Rectangle, value: ^bool, text: str, theme := ^default_checkb } if is_hot_item(hash) { - move_towards(^checkbox_state.hover_time, 1.0f, 0.1f); @ThemeConfiguration + move_towards(^animation_state.hover_time, 1.0f, 0.1f); @ThemeConfiguration } else { - move_towards(^checkbox_state.hover_time, 0.0f, 0.1f); @ThemeConfiguration + move_towards(^animation_state.hover_time, 0.0f, 0.1f); @ThemeConfiguration } - box_width := theme.box_width; - box_size := theme.box_size; - width, height := Rectangle.dimensions(r); + box_border_width := theme.box_border_width; + box_size := theme.box_size; + width, height := Rectangle.dimensions(r); gfx.set_texture(); gfx.rect( @@ -73,18 +67,18 @@ checkbox :: (use r: Rectangle, value: ^bool, text: str, theme := ^default_checkb surface_color : gfx.Color4; if *value { surface_color = theme.checked_color; - surface_color = color_lerp(checkbox_state.hover_time, surface_color, theme.checked_hover_color); + surface_color = color_lerp(animation_state.hover_time, surface_color, theme.checked_hover_color); } else { surface_color = theme.background_color; - surface_color = color_lerp(checkbox_state.hover_time, surface_color, theme.hover_color); + surface_color = color_lerp(animation_state.hover_time, surface_color, theme.hover_color); } - surface_color = color_lerp(checkbox_state.click_time, surface_color, theme.click_color); + surface_color = color_lerp(animation_state.click_time, surface_color, theme.click_color); gfx.rect( - .{ x0 + 4 + box_width, y0 + (height - box_size) / 2 + box_width }, - .{ box_size - box_width * 2, box_size - box_width * 2 }, + .{ x0 + 4 + box_border_width, y0 + (height - box_size) / 2 + box_border_width }, + .{ box_size - box_border_width * 2, box_size - box_border_width * 2 }, surface_color ); @@ -97,12 +91,12 @@ checkbox :: (use r: Rectangle, value: ^bool, text: str, theme := ^default_checkb y0 + ~~ font.common.baseline * theme.font_size + (height - text_height) / 2, theme.font_size, theme.text_color); - move_towards(^checkbox_state.click_time, 0.0f, 0.08f); @ThemeConfiguration + move_towards(^animation_state.click_time, 0.0f, 0.08f); @ThemeConfiguration - if checkbox_state.click_time > 0 || checkbox_state.hover_time > 0 { - map.put(^checkbox_states, hash, checkbox_state); + if animation_state.click_time > 0 || animation_state.hover_time > 0 { + map.put(^animation_states, hash, animation_state); } else { - map.delete(^checkbox_states, hash); + map.delete(^animation_states, hash); } return result; diff --git a/modules/ui/components/slider.onyx b/modules/ui/components/slider.onyx new file mode 100644 index 00000000..6d077a77 --- /dev/null +++ b/modules/ui/components/slider.onyx @@ -0,0 +1,91 @@ +package ui + +use package core + +Slider_Theme :: struct { + use text_theme := Text_Theme.{}; + + box_color := gfx.Color4.{ 0.1, 0.1, 0.1 }; + box_border_color := gfx.Color4.{ 0.2, 0.2, 0.2 }; + box_border_width := 4.0f; @InPixels + + bar_color := gfx.Color4.{ 0.4, 0.4, 0.4 }; + bar_hover_color := gfx.Color4.{ 1, 0, 0 }; +} + +default_slider_theme := Slider_Theme.{}; + +slider :: (use r: Rectangle, value: ^$T, min_value: T, max_value: T, text: str, theme := ^default_slider_theme, site := #callsite, increment := 0) -> bool { + result := false; + + hash := get_site_hash(site, increment); + animation_state := map.get(^animation_states, hash); + width, height := Rectangle.dimensions(r); + + if is_hot_item(hash) { + if mouse_state.left_button_down { + set_active_item(hash); + result = true; + + // Animate this? + adjust_slider_value(value, mouse_state.x - x0, width, min_value, max_value, 0); + } else { + set_active_item(0); + } + } + + if Rectangle.contains(r, mouse_state.x, mouse_state.y) { + set_hot_item(hash); + } + + if is_hot_item(hash) { + move_towards(^animation_state.hover_time, 1.0f, 0.1f); @ThemeConfiguration + } else { + move_towards(^animation_state.hover_time, 0.0f, 0.1f); @ThemeConfiguration + } + + box_border_width := theme.box_border_width; + + bar_color := color_lerp(animation_state.hover_time, theme.bar_color, theme.bar_hover_color); + + gfx.set_texture(); + gfx.rect(.{ x0, y0 }, .{ width, height }, theme.box_border_color); + gfx.rect( + .{ x0 + box_border_width, y0 + box_border_width }, + .{ width - box_border_width * 2, height - box_border_width * 2 }, + theme.box_border_color); + + box_width := (width - box_border_width * 2) * ~~(*value - min_value) / cast(f32) (max_value - min_value); + box_width = math.clamp(box_width, 0, width - box_border_width * 2); + gfx.rect( + .{ x0 + box_border_width, y0 + box_border_width }, + .{ box_width, height - box_border_width * 2 }, + bar_color); + + if animation_state.click_time > 0 || animation_state.hover_time > 0 { + map.put(^animation_states, hash, animation_state); + } else { + map.delete(^animation_states, hash); + } + + return result; +} + +#private_file +adjust_slider_value :: proc { + @Incomplete // the step parameter is ignored. + // Integers need to be + (value: ^i32, x: f32, width: f32, min_value: i32, max_value: i32, step: i32) { + step_width := width / ~~math.abs(max_value - min_value); + percent := (x + step_width / 2) / width; + *value = math.lerp(percent, min_value, max_value); + *value = math.clamp(*value, min_value, max_value); + }, + + @Incomplete // the step parameter is ignored. + (value: ^$T, x: f32, width: f32, min_value: T, max_value: T, step: T) { + percent := x / width; + *value = math.lerp(percent, min_value, max_value); + *value = math.clamp(*value, min_value, max_value); + }, +} \ No newline at end of file diff --git a/modules/ui/components/textbox.onyx b/modules/ui/components/textbox.onyx new file mode 100644 index 00000000..94ccd9a6 --- /dev/null +++ b/modules/ui/components/textbox.onyx @@ -0,0 +1,6 @@ +package ui + + +textbox :: (use r: Rectangle) -> bool { + +} \ No newline at end of file diff --git a/modules/ui/module.onyx b/modules/ui/module.onyx index 1586000a..d4063502 100644 --- a/modules/ui/module.onyx +++ b/modules/ui/module.onyx @@ -1,11 +1,26 @@ -@Todo // document this module better +@Todo +/* +Document this module better +Add a font cache and more preloaded fonts -/* +Add proper font texture decoding to make the WebGL texture depending on how the BMFont was encoded into the image. + +Add textboxes +*/ + +/* The goal of this module is to provide a low-friction method of producing simple user interfaces in WebGL (and OpenGL when Onyx compiles to C). +This module provides several common UI components such as: + - Buttons + - Checkbox + - Sliders + - Textboxes + +This module also provides a simple rectangle division scheme for laying out UI components. */ @@ -19,8 +34,10 @@ package ui #load "./ui" #load "./flow" + #load "./components/button" #load "./components/checkbox" +#load "./components/slider" // Package inclusions that are part of all files in the "ui" package. diff --git a/modules/ui/ui.onyx b/modules/ui/ui.onyx index 7afcf88c..74caddc9 100644 --- a/modules/ui/ui.onyx +++ b/modules/ui/ui.onyx @@ -34,8 +34,7 @@ mouse_state: MouseState = MouseState.{}; init_ui :: () { init_font(); - map.init(^button_states, default=.{}, hash_count=4); - map.init(^checkbox_states, default=.{}, hash_count=4); + map.init(^animation_states, default=.{}, hash_count=4); } clear_buttons :: () { @@ -107,33 +106,11 @@ draw_text_raw :: (text: str, x: f32, y: f32, size := DEFAULT_TEXT_SIZE, color := gl.bindTexture(gl.TEXTURE_2D, font_texture); gfx.use_alpha_shader(0); - original_x := x; - baseline := cast(f32) font.common.baseline * size; - - line_height := cast(f32) font.common.line_height * size; - - for char: text { - if char == #char "\n" { - y += line_height + .5; - x = original_x; - continue; - } - - glyph := font->get_glyph(char); - - if glyph == null { - glyph = font->get_glyph(0); - assert(glyph != null, "NO NULL GLYPH"); - } - - // Round to the nearest pixel to avoid bleeding to the next glyph - tx, ty := math.floor(x + ~~glyph.xoffset * size + .5), math.floor(y + ~~glyph.yoffset * size - baseline + .5); - w := math.floor(cast(f32) glyph.w * size + .5); - h := math.floor(cast(f32) glyph.h * size + .5); - - gfx.textured_rect(.{ tx, ty }, .{ w, h }, .{ glyph.tex_x, glyph.tex_y }, .{ glyph.tex_w, glyph.tex_h }, color = color); - - x += ~~glyph.xadvance * size; + for glyph: bmfont.get_character_positions(^font, size, text, x, y) { + gfx.textured_rect( + .{ glyph.pos_x, glyph.pos_y }, .{ glyph.pos_w, glyph.pos_h }, + .{ glyph.tex_x, glyph.tex_y }, .{ glyph.tex_w, glyph.tex_h }, + color = color); } gfx.flush(); @@ -180,6 +157,8 @@ Rectangle :: struct { } top_left :: (use r: Rectangle) -> (x: f32, y: f32) do return math.min(x0, x1), math.min(y0, y1); + top_right :: (use r: Rectangle) -> (x: f32, y: f32) do return math.max(x0, x1), math.min(y0, y1); + bottom_left :: (use r: Rectangle) -> (x: f32, y: f32) do return math.min(x0, x1), math.max(y0, y1); bottom_right :: (use r: Rectangle) -> (x: f32, y: f32) do return math.max(x0, x1), math.max(y0, y1); contains :: (use r: Rectangle, x: f32, y: f32) -> bool { @@ -197,12 +176,24 @@ Text_Theme :: struct { font_size := 1.0f; } -default_text_theme: Text_Theme = Text_Theme.{}; +default_text_theme := Text_Theme.{}; +// Animation states are stored globally as there is not much to the state of a button. +// Forcing the end user to store a structure for each button that is just the animation +// state of the component feels very wrong. +#private animation_states : map.Map(UI_Id, Animation_State); + +Animation_State :: struct { + hover_time := 0.0f; + click_time := 0.0f; +} + + + // Utilities get_site_hash :: (site: CallSite, increment := 0) -> UI_Id { -- 2.25.1