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
#load "./types"
#load "./bmfont_loader"
#load "./utils"
+#load "./position"
--- /dev/null
+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
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;
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);
+ }
+ }
}
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
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.{};
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 {
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);
}
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;
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);
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;
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);
}
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(
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
);
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;
--- /dev/null
+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
--- /dev/null
+package ui
+
+
+textbox :: (use r: Rectangle) -> bool {
+
+}
\ No newline at end of file
-@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.
*/
#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.
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 :: () {
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();
}
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 {
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 {