diff --git a/libraries/snths_ui/ui.odin b/libraries/snths_ui/ui.odin index eebde26..cae15df 100644 --- a/libraries/snths_ui/ui.odin +++ b/libraries/snths_ui/ui.odin @@ -18,7 +18,6 @@ ctx: ^Context Context :: struct { origin: v2, - element_focus_id: ElementId, container_count: u16, containers: [dynamic]int, container_last_element: [dynamic]int, @@ -57,6 +56,17 @@ ElementType :: enum { Button, Slider, InputFloat, + Dropdown, + Custom0, + Custom1, + Custom2, + Custom3, + Custom4, + Custom5, + Custom6, + Custom7, + Custom8, + Custom9, } @rodata element_type_input := bit_set[ElementType] {.InputFloat} @@ -109,36 +119,18 @@ same_line :: proc() { ctx.same_line = true } -focus_element :: proc(ctx: ^Context, element: Element) { - ctx.element_focus_id = element.id -} - -unfocus_element :: proc(ctx: ^Context) { - ctx.element_focus_id = element_id_invalid() -} - -is_element_focused :: proc(ctx: ^Context, element: Element) -> bool { - return ctx.element_focus_id == element.id -} - -focus_next_element :: proc(ctx: ^Context, allowed_focus := element_type_input) { - if ctx.element_focus_id == element_id_invalid() { - return +focus_next_element :: proc(id: ElementId, elements: []Element, allowed_focus: bit_set[ElementType]) -> ElementId { + if id == element_id_invalid() { + return element_id_invalid() } - element_index := int(ctx.element_focus_id.element_index) - if element_index + 1 >= len(ctx.elements) { - ctx.element_focus_id = element_id_invalid() - return - } - element_index += 1 - for element_index < len(ctx.elements) { - if ctx.elements[element_index].type in allowed_focus { - ctx.element_focus_id = ctx.elements[element_index].id - return + element_index := int(id.element_index) + for element_index < len(elements) { + if elements[element_index].type in allowed_focus { + return elements[element_index].id } element_index += 1 } - ctx.element_focus_id = element_id_invalid() + return element_id_invalid() } container_begin :: proc(dimensions: Dimensions, config: ElementConfiguration, data: rawptr = nil) -> ^Element { @@ -174,7 +166,8 @@ container_end :: proc() { ctx.same_line = false } -assert_no_dimensions_percentage_in_auto_container :: proc(dimensions: Dimensions) { +element :: proc(element_type: ElementType, dimensions: Dimensions, config: ElementConfiguration, data: rawptr = nil) -> ^Element { + assert(element_type != .Container) container := _container_current() if _, is_auto := container.dimensions.width.(Size_Auto); is_auto { _, is_pixels := dimensions.width.(Size_Pixels) @@ -184,24 +177,9 @@ assert_no_dimensions_percentage_in_auto_container :: proc(dimensions: Dimensions _, is_pixels := dimensions.height.(Size_Pixels) assert(is_pixels, "Any height that is not Size_Pixels is disallowed in a container using Size_Auto for height") } -} -button :: proc(dimensions: Dimensions, config: ElementConfiguration, data: rawptr = nil) -> ^Element { - assert_no_dimensions_percentage_in_auto_container(dimensions) area := _resolve_element_area(dimensions, config.margin) - return _append_element({area = area, config = config, type = .Button, dimensions = dimensions, data = data}) -} - -slider :: proc(dimensions: Dimensions, config: ElementConfiguration, data: rawptr = nil) -> ^Element { - assert_no_dimensions_percentage_in_auto_container(dimensions) - area := _resolve_element_area(dimensions, config.margin) - return _append_element({area = area, config = config, type = .Slider, dimensions = dimensions, data = data}) -} - -input_float :: proc(dimensions: Dimensions, config: ElementConfiguration, data: rawptr = nil) -> ^Element { - assert_no_dimensions_percentage_in_auto_container(dimensions) - area := _resolve_element_area(dimensions, config.margin) - return _append_element({area = area, config = config, type = .InputFloat, dimensions = dimensions, data = data}) + return _append_element({area = area, config = config, type = element_type, dimensions = dimensions, data = data}) } get_element_outer_area :: proc(element: Element) -> rect2 { diff --git a/program.odin b/program.odin index 6f8b27e..7c95a51 100644 --- a/program.odin +++ b/program.odin @@ -26,6 +26,30 @@ UiElementData_FloatInput :: struct { value_string: cstring, } +UiButtonType :: enum { + Button, + Dropdown, +} + +UiElementData_Button :: struct { + type: UiButtonType, + options: cstring, +} + +MatrixTypes :: enum { + Identity, + Translation, + Scale, + Rotation, +} + +@rodata matrix_add_hotkeys := [MatrixTypes]rl.KeyboardKey { + .Identity = rl.KeyboardKey.I, + .Translation = rl.KeyboardKey.T, + .Scale = rl.KeyboardKey.S, + .Rotation = rl.KeyboardKey.R, +} + FontStyle :: struct { font: rl.Font, size, spacing: f32 @@ -66,6 +90,7 @@ input_float_field_as_cstring :: proc(field: ^InputFloatField) -> cstring { Program :: struct { ui_context: ui.Context, + ui_element_focus: ui.ElementId, matrix_float_pool: []f32, matrix_value_string_pool: []InputFloatField, matrix_count: int, @@ -109,7 +134,9 @@ program_init :: proc(allocator := context.allocator) { inner_area = {0, 0, WINDOW_WIDTH, WINDOW_HEIGHT}, zoom = 1.0, } + ui.init(&program.ui_context, 4000, allocator) + program.ui_element_focus = ui.element_id_invalid() } world_to_grid_matrix :: proc(grid: Grid) -> mat3 { @@ -148,12 +175,12 @@ program_update :: proc() { } if rl.IsKeyPressed(.TAB) { - ui.focus_next_element(&program.ui_context) + program.ui_element_focus = ui.focus_next_element(program.ui_element_focus, ui_elements, {.InputFloat}) } grid := program.grid grid_inner_position := rect_get_tl(grid.inner_area) - draw_grid(grid.area, {50.0, 50.0}, rl.GRAY) + draw_grid(grid.area, {32.0, 32.0}, rl.GRAY) program_render_ui(&program.ui_context, ui_elements) } @@ -171,17 +198,28 @@ draw_grid :: proc(area: rect2, line_interval: v2, color: rl.Color) { program_build_ui :: proc() -> (elements: []ui.Element, matrix_container: ^ui.Element) { ui.start(&program.ui_context, {0, 0}, {WINDOW_WIDTH, WINDOW_HEIGHT}) top_bar_container_config := ui.ElementConfiguration {margin = V2_ONE * 4.0} + top_bar_element_margin := V2_ONE * 4.0 top_bar_container := ui.container_begin({ui.Size_Percentage(1.0), ui.Size_Pixels(48.0)}, top_bar_container_config) top_bar_inner_height := ui.get_element_inner_area(top_bar_container^).height top_bar_button_dimensions := ui.Dimensions {ui.Size_Pixels(top_bar_inner_height), ui.Size_Pixels(top_bar_inner_height)} - top_bar_button_config := ui.ElementConfiguration {margin = V2_ONE * 4} + top_bar_button_config := ui.ElementConfiguration {margin = top_bar_element_margin} + top_bar_dropdown_dimensions := ui.Dimensions {ui.Size_Pixels(192.0), ui.Size_Percentage(1.0)} + top_bar_dropdown_config := ui.ElementConfiguration {margin = top_bar_element_margin} { - config := top_bar_button_config - config.label = "+" - ui.button(top_bar_button_dimensions, config) - config.label = "-" + button_config := top_bar_button_config + button_config.label = "+" + ui.element(.Button, top_bar_button_dimensions, button_config) + button_config.label = "-" ui.same_line() - ui.button(top_bar_button_dimensions, config) + ui.element(.Button, top_bar_button_dimensions, button_config) + + ui.same_line() + matrix_dropdown_config := top_bar_dropdown_config + matrix_dropdown_config.label = enum_to_raygui_options_string_hotkey(MatrixTypes, &matrix_add_hotkeys, context.temp_allocator) + matrix_dropdown_data := new(UiElementData_Button, context.temp_allocator) + matrix_dropdown_data^ = {type = .Dropdown} + + ui.element(.Button, top_bar_dropdown_dimensions, matrix_dropdown_config) } ui.container_end() @@ -212,7 +250,7 @@ ui_mat3 :: proc(mat: []f32, value_strings: []InputFloatField, dimensions: ui.Dim data := new(UiElementData_FloatInput, allocator) index := i * 3 + j data^ = {value = &mat[index], value_string = input_float_field_as_cstring(&value_strings[index])} - ui.input_float(matrix_input_dimensions, matrix_input_config, data) + ui.element(.InputFloat, matrix_input_dimensions, matrix_input_config, data) ui.same_line() } program.ui_context.same_line = false @@ -242,7 +280,7 @@ program_render_ui :: proc(ctx: ^ui.Context, elements: []ui.Element) { } area := rect_translate(rl.Rectangle(element.area), offset) - switch element.type { + #partial switch element.type { case .Container: dim := rl.Color {0, 0, 0, 192} if element.data != nil { @@ -279,8 +317,8 @@ program_render_ui :: proc(ctx: ^ui.Context, elements: []ui.Element) { rl.GuiSlider(area, element.label, "", data.value, data.min, data.max) case .InputFloat: data := cast(^UiElementData_FloatInput)element.data - if rl.GuiValueBoxFloat(area, element.label, data.value_string, data.value, ui.is_element_focused(ctx, element)) == 1 { - ui.focus_element(ctx, element) + if rl.GuiValueBoxFloat(area, element.label, data.value_string, data.value, program.ui_element_focus == element.id) == 1 { + program.ui_element_focus = element.id } } } diff --git a/utils.odin b/utils.odin index 286c318..c5c8690 100644 --- a/utils.odin +++ b/utils.odin @@ -7,6 +7,9 @@ import "core:fmt" import "core:math/linalg" import "core:mem" +IS_ENUM :: intrinsics.type_is_enum +IS_ENUMERATED_ARRAY :: intrinsics.type_is_enumerated_array + // Wraps os.read_entire_file and os.write_entire_file, but they also work with emscripten. @(require_results) read_entire_file :: proc(name: string, allocator := context.allocator, loc := #caller_location) -> (data: []byte, success: bool) { @@ -17,7 +20,7 @@ write_entire_file :: proc(name: string, data: []byte, truncate := true) -> (succ return _write_entire_file(name, data, truncate) } -/// ### RAYLIB SHORTHANDS ### /// +/// ### RAYLIB TEXT DRAWING ### /// measure_text :: proc(text: cstring, style: FontStyle) -> v2 { return rl.MeasureTextEx(style.font, text, style.size, style.spacing) } @@ -76,19 +79,33 @@ V2_RIGHT :: v2{1.0, 0.0} V2_UP :: v2{0.0, -1.0} V2_DOWN :: v2{0.0, 1.0} -/// ### MISCELANIOUS ### /// -enum_to_raygui_options_string :: proc($T: typeid, allocator: mem.Allocator) -> cstring where intrinsics.type_is_enum(T) { +/// ### ENUM STRINGIFICATION ### /// +enum_to_raygui_options_string :: proc($T: typeid, allocator: mem.Allocator) -> cstring where IS_ENUM(T) { b := strings.builder_make(allocator) for element, i in T { option := fmt.aprint(element, allocator = allocator) - strings.write_string(&b, strings.to_ada_case(option, allocator)) - strings.write_rune(&b, ';') + fmt.sbprint(&b, strings.to_ada_case(option, allocator), ";", sep = "") } strings.builder_replace_all(&b, "_", " ") b.buf[len(b.buf) - 1] = 0 return cstring(raw_data(b.buf[:len(b.buf)])) } +enum_to_raygui_options_string_hotkey :: proc($T: typeid, hotkeys: ^$N, allocator: mem.Allocator) -> cstring where IS_ENUM(T), IS_ENUMERATED_ARRAY(N) { + b := strings.builder_make(allocator) + for element, i in T { + option := fmt.aprint(element, allocator = allocator) + option_ada := strings.to_ada_case(option, allocator) + if hotkeys[element] != .KEY_NULL { + fmt.sbprint(&b, option_ada, " (", rune(hotkeys[element]), ");", sep = "") + } else { + fmt.sbprint(&b, option_ada, ";", sep = "") + } + } + strings.builder_replace_all(&b, "_", " ") + b.buf[len(b.buf) - 1] = 0 + return cstring(raw_data(b.buf[:len(b.buf)])) +} /// ### RECTANGLES ### /// rect2_to_irect2 :: proc "contextless" (rect: rect2) -> irect2 {