added sliders and other core module things
authorBrendan Hansen <brendan.f.hansen@gmail.com>
Fri, 11 Jun 2021 04:44:44 +0000 (23:44 -0500)
committerBrendan Hansen <brendan.f.hansen@gmail.com>
Fri, 11 Jun 2021 04:44:44 +0000 (23:44 -0500)
core/math.onyx
modules/bmfont/module.onyx
modules/bmfont/position.onyx [new file with mode: 0644]
modules/bmfont/utils.onyx
modules/immediate_mode/immediate_renderer.onyx
modules/ui/components/button.onyx
modules/ui/components/checkbox.onyx
modules/ui/components/slider.onyx [new file with mode: 0644]
modules/ui/components/textbox.onyx [new file with mode: 0644]
modules/ui/module.onyx
modules/ui/ui.onyx

index cd21285154c2457ef41d0bf97d361d31fa0a8354..c330fc97c8efef2e2290cb951b24eb988da7298c 100644 (file)
@@ -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
index 2adacab33d757e9574a50913b1f54a7845940956..a994d2efa8e87331624a1c2aa99a0835e6c0f0d5 100644 (file)
@@ -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 (file)
index 0000000..d598e79
--- /dev/null
@@ -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
index 87f160a6996a6ac92c2bc1194b5f987204e41c8d..f01e1a32052579282b7a0df3b73cdbbb48b6ad68 100644 (file)
@@ -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;
 
index 1c916c6677f91a7381aa8a59d175ac9f52a268e4..437be98e10299243dd008450c746d1343aa3e6f2 100644 (file)
@@ -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
index c165a99c3db7508289494fab0df1ec1f3ee592b3..835d5915d7cb11591f2ff5500b6572a4e665e99d 100644 (file)
@@ -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;
index 5cda0fdf14a47da0f67cb123a07e7b9adb0952fc..4acfd98f667a0f6ec97bbc9dd3970f595af906cf 100644 (file)
@@ -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 (file)
index 0000000..6d077a7
--- /dev/null
@@ -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 (file)
index 0000000..94ccd9a
--- /dev/null
@@ -0,0 +1,6 @@
+package ui
+
+
+textbox :: (use r: Rectangle) -> bool {
+    
+}
\ No newline at end of file
index 1586000aa01c14265c294f01a2f7fbc684d7020c..d4063502f74a79994696a6f4f5336062cc0fe8b5 100644 (file)
@@ -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.
index 7afcf88cb732521fca347f7552e95acaa1fde7f3..74caddc9c7ccc339e98e25db40c1b078f74a4d73 100644 (file)
@@ -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 {