diff --git a/code/sectr/app/screen.odin b/code/sectr/app/screen.odin index 69cfb29..998c6b3 100644 --- a/code/sectr/app/screen.odin +++ b/code/sectr/app/screen.odin @@ -21,6 +21,8 @@ UI_ScreenState :: struct settings_menu : struct { container : UI_Widget, + min_zoom_inputbox : UI_TextInputBox, + max_zoom_inputbox : UI_TextInputBox, cfg_drop_down : UI_DropDown, pos, size, min_size : Vec2, is_open : b32, @@ -28,6 +30,14 @@ UI_ScreenState :: struct }, } +ui_screen_reload :: proc() { + using state := get_state() + using screen_ui.settings_menu + + min_zoom_inputbox.input_str.backing = persistent_slab_allocator() + max_zoom_inputbox.input_str.backing = persistent_slab_allocator() +} + ui_screen_tick :: proc() { profile("Screenspace Imgui") @@ -263,6 +273,10 @@ ui_screen_settings_menu :: proc( captures : rawptr = nil ) -> ( should_raise : b @static last_invalid_input_time : Time if input_box.active { + if ! input_box.was_active { + last_invalid_input_time._nsec = 0 + } + if input_box.pressed { editor_cursor_pos = i32(value_str.num) } @@ -275,10 +289,6 @@ ui_screen_settings_menu :: proc( captures : rawptr = nil ) -> ( should_raise : b editor_cursor_pos = min(i32(value_str.num), editor_cursor_pos + 1) } - if ! input_box.was_active { - last_invalid_input_time._nsec = 0 - } - iter_obj := iterator( & input_events.key_events ); iter := & iter_obj for event := iter_next( iter ); event != nil; event = iter_next( iter ) { @@ -297,14 +307,12 @@ ui_screen_settings_menu :: proc( captures : rawptr = nil ) -> ( should_raise : b } } - // append( & value_str, input_events.codes_pressed ) for code in to_slice(input_events.codes_pressed) { if value_str.num == 0 && code == '0' { last_invalid_input_time = time_now() continue } - if value_str.num >= max_value_length { last_invalid_input_time = time_now() continue @@ -397,6 +405,39 @@ ui_screen_settings_menu :: proc( captures : rawptr = nil ) -> ( should_raise : b layout.margins.left = 10 layout.font_size = 12 } + + min_zoom_config: { + using min_zoom_inputbox + digits_only = true + disallow_leading_zeros = false + disallow_decimal = false + digit_min = 0.01 + digit_max = 9999 + max_length = 5 + } + ui_text_input_box( & min_zoom_inputbox, "settings_menu.cam_min_zoom.input_box", allocator = persistent_slab_allocator() ) + { + using min_zoom_inputbox + layout.flags = {.Fixed_Width} + layout.margins.left = 5 + layout.padding.right = 5 + layout.size.min.x = 80 + style.corner_radii = { 3, 3, 3, 3 } + + if was_active + { + value, success := parse_f32(to_string(array_to_slice(input_str))) + if success { + value = clamp(value, 0.001, 9999.0) + config.cam_min_zoom = value + } + } + else + { + clear( input_str ) + append( & input_str, to_runes(str_fmt("%v", config.cam_min_zoom))) + } + } } Max_Zoom: @@ -415,6 +456,39 @@ ui_screen_settings_menu :: proc( captures : rawptr = nil ) -> ( should_raise : b layout.margins.left = 10 layout.font_size = 12 } + + max_zoom_config: { + using max_zoom_inputbox + digits_only = true + disallow_leading_zeros = false + disallow_decimal = false + digit_min = 0.01 + digit_max = 9999 + max_length = 5 + ui_text_input_box( & max_zoom_inputbox, "settings_menu.cam_max_zoom.input_box", allocator = persistent_slab_allocator() ) + { + using max_zoom_inputbox + layout.flags = {.Fixed_Width} + layout.margins.left = 5 + layout.padding.right = 5 + layout.size.min.x = 80 + style.corner_radii = { 3, 3, 3, 3 } + + if was_active + { + value, success := parse_f32(to_string(array_to_slice(input_str))) + if success { + value = clamp(value, 0.001, 9999.0) + config.cam_max_zoom = value + } + } + else + { + clear( input_str ) + append( & input_str, to_runes(str_fmt("%v", config.cam_max_zoom))) + } + } + } } } } diff --git a/code/sectr/app/ui_theme.odin b/code/sectr/app/ui_theme.odin index 183b3bc..b948049 100644 --- a/code/sectr/app/ui_theme.odin +++ b/code/sectr/app/ui_theme.odin @@ -456,3 +456,13 @@ theme_text :: proc() -> UI_Theme } return theme } + +theme_text_input_box :: proc() -> UI_Theme +{ + @static theme : UI_Theme + @static loaded : b32 = false + if ! loaded + { + } + return theme +} diff --git a/code/sectr/engine/client_api.odin b/code/sectr/engine/client_api.odin index d7d7d50..f84a4ea 100644 --- a/code/sectr/engine/client_api.odin +++ b/code/sectr/engine/client_api.odin @@ -153,7 +153,7 @@ startup :: proc( prof : ^SpallProfiler, persistent_mem, frame_mem, transient_mem color_theme = App_Thm_Dusk - font_size_canvas_scalar = 2.0 + font_size_canvas_scalar = 1.0 } Desired_OS_Scheduler_MS :: 1 diff --git a/code/sectr/engine/render.odin b/code/sectr/engine/render.odin index 3daa5f3..83f59f7 100644 --- a/code/sectr/engine/render.odin +++ b/code/sectr/engine/render.odin @@ -261,7 +261,7 @@ render_mode_screenspace :: proc( screen_extent : Extents2, screen_ui : ^UI_State } if true { - state.config.font_size_canvas_scalar = 1.5 + state.config.font_size_canvas_scalar = 2.0 zoom_adjust_size := 16 * state.project.workspace.cam.zoom over_sample := f32(state.config.font_size_canvas_scalar) debug_text("font_size_canvas_scalar: %v", config.font_size_canvas_scalar) diff --git a/code/sectr/engine/update.odin b/code/sectr/engine/update.odin index 0e9f05b..4fa3bf8 100644 --- a/code/sectr/engine/update.odin +++ b/code/sectr/engine/update.odin @@ -270,7 +270,16 @@ update :: proc( delta_time : f64 ) -> b32 } if target_index != current_index { - workspace.zoom_target = Digial_Zoom_Snap_Levels[target_index] + proposed_target := Digial_Zoom_Snap_Levels[target_index] + if proposed_target < config.cam_min_zoom { + workspace.zoom_target = Digial_Zoom_Snap_Levels[find_closest_zoom_index(config.cam_min_zoom, Digial_Zoom_Snap_Levels)] + } + else if proposed_target > config.cam_max_zoom { + workspace.zoom_target = Digial_Zoom_Snap_Levels[find_closest_zoom_index(config.cam_max_zoom, Digial_Zoom_Snap_Levels)] + } + else { + workspace.zoom_target = proposed_target + } } } @@ -341,7 +350,7 @@ update :: proc( delta_time : f64 ) -> b32 // test_draggable() // test_text_box() // test_parenting( & default_layout, & frame_style_default ) - // test_whitespace_ast( & default_layout, & frame_style_default ) + test_whitespace_ast( & default_layout, & frame_style_default ) } //endregion Workspace Imgui Tick diff --git a/code/sectr/ui/core/base.odin b/code/sectr/ui/core/base.odin index 18b8332..d100f26 100644 --- a/code/sectr/ui/core/base.odin +++ b/code/sectr/ui/core/base.odin @@ -131,6 +131,8 @@ UI_State :: struct { last_pressed_key : [MouseBtn.count] UI_Key, last_pressed_key_us : [MouseBtn.count] f32, + + last_invalid_input_time : Time, } #region("Lifetime") @@ -453,7 +455,7 @@ ui_hash_part_from_key_string :: proc ( content : string ) -> string { ui_key_from_string :: #force_inline proc "contextless" ( value : string ) -> UI_Key { // profile(#procedure) - USE_RAD_DEBUGGERS_METHOD :: true + USE_RAD_DEBUGGERS_METHOD :: false key : UI_Key diff --git a/code/sectr/ui/widgets.odin b/code/sectr/ui/widgets.odin index fb39afe..fdcad1b 100644 --- a/code/sectr/ui/widgets.odin +++ b/code/sectr/ui/widgets.odin @@ -550,6 +550,173 @@ ui_text_wrap_panel :: proc( parent : ^UI_Widget ) } #endregion("Text") +#region("Text Input") +UI_TextInput_Policy :: struct { + disallow_decimal : b32, + disallow_leading_zeros : b32, + digits_only : b32, + digit_min, digit_max : f32, + max_length : u32, +} + +// Note(Ed): Currently only supports single-line input (may stay that way). +// This is a precursor to a proper text editor widget +UI_TextInputBox :: struct { + using widget : UI_Widget, + input_str : Array(rune), + editor_cursor_pos : Vec2i, + using policy : UI_TextInput_Policy, +} + +ui_text_input_box :: proc( text_input_box : ^UI_TextInputBox, label : string, flags : UI_BoxFlags = {.Mouse_Clickable, .Focusable, .Click_To_Focus}, allocator := context.allocator ) +{ + state := get_state() + iter_next :: next + input := state.input + input_events := & get_state().input_events + ui := ui_context() + + text_input_box.box = ui_box_make( flags, label ) + text_input_box.signal = ui_signal_from_box( text_input_box.box ) + using text_input_box + + // TODO(Ed): This thing needs to be pulling from the theme stack + app_color := app_color_theme() + if active do style.bg_color = app_color.input_box_bg_active + else if hot do style.bg_color = app_color.input_box_bg_hot + else do style.bg_color = app_color.input_box_bg + + if input_str.header == nil { + error : AllocatorError + input_str, error = make( Array(rune), Kilo, allocator ) + ensure(error == AllocatorError.None, "Failed to allocate array for input_str of input_box") + } + + if active + { + // TODO(Ed): Can problably be moved to ui_signal + if ! was_active { + ui.last_invalid_input_time = {} + } + if pressed { + editor_cursor_pos = { i32(input_str.num), 0 } + } + + // TODO(Ed): Abstract this to navigation bindings + if btn_pressed(input.keyboard.left) { + editor_cursor_pos.x = max(0, editor_cursor_pos.x - 1) + } + if btn_pressed(input.keyboard.right) { + editor_cursor_pos.x = min(i32(input_str.num), editor_cursor_pos.x + 1) + } + + // TODO(Ed): Confirm btn_pressed is working (if not figure out why) + // if btn_pressed(input.keyboard.left) { + // if input_str.num > 0 { + // editor_cursor_pos = max(0, editor_cursor_pos - 1) + // remove_at( value_str, u64(editor_cursor_pos) ) + // } + // } + // if btn_pressed(input.keyboard.right) { + // screen_ui.active = 0 + // } + + iter_obj := iterator( & input_events.key_events ); iter := & iter_obj + for event := iter_next( iter ); event != nil; event = iter_next( iter ) + { + if event.frame_id != state.frame do break + + if event.key == .backspace && event.type == .Key_Pressed { + if input_str.num > 0 { + editor_cursor_pos.x = max(0, editor_cursor_pos.x - 1) + remove_at( input_str, u64(editor_cursor_pos.x) ) + break + } + } + if event.key == .enter && event.type == .Key_Pressed { + ui.active = 0 + break + } + } + + decimal_detected := false + for code in to_slice(input_events.codes_pressed) + { + accept_digits := ! digits_only || '0' <= code && code <= '9' + accept_decimal := ! disallow_decimal || ! decimal_detected && code =='.' + + if disallow_leading_zeros && input_str.num == 0 && code == '0' { + ui.last_invalid_input_time = time_now() + continue + } + if max_length > 0 && input_str.num >= u64(max_length) { + ui.last_invalid_input_time = time_now() + continue + } + + if accept_digits || accept_decimal { + append_at( & input_str, code, u64(editor_cursor_pos.x)) + editor_cursor_pos.x = min(editor_cursor_pos.x + 1, i32(input_str.num)) + } + else { + ui.last_invalid_input_time = time_now() + continue + } + } + clear( input_events.codes_pressed ) + + invalid_color := RGBA8 { 70, 40, 40, 255} + + // Visual feedback - change background color briefly when invalid input occurs + feedback_duration :: 0.2 // seconds + curr_duration := duration_seconds( time_diff( ui.last_invalid_input_time, time_now() )) + if ui.last_invalid_input_time._nsec != 0 && curr_duration < feedback_duration { + style.bg_color = invalid_color + } + } + + ui_parent(text_input_box) + name :: proc( label : string ) -> string { + parent_label := (transmute(^string) context.user_ptr) ^ + return str_intern(str_fmt("%v: %v", parent_label, label )).str + } + context.user_ptr = & parent.label + + // TODO(Ed): Allow for left and center alignment of text + value_txt : UI_Widget; { + scope(theme_text) + value_txt = ui_text(name("input_str"), to_str_runes_pair(array_to_slice(input_str))) + using value_txt + layout.alignment = {0.0, 0.0} + layout.text_alignment = {1.0, 0.5} + layout.anchor.left = 0.0 + layout.size.min = cast(Vec2) measure_text_size( text.str, style.font, layout.font_size, 0 ) + + if active { + ui_parent(value_txt) + + ascent, descent, line_gap := get_font_vertical_metrics(style.font, layout.font_size) + cursor_height := ascent - descent + + text_before_cursor := to_string(array_to_slice(input_str)[ :editor_cursor_pos.x ]) + cursor_x_offset := measure_text_size(text_before_cursor, style.font, layout.font_size, 0).x + text_size := measure_text_size(to_string(array_to_slice(input_str)), style.font, layout.font_size, 0).x + + ui_layout( UI_Layout { + flags = { .Fixed_Width }, + size = range2({1, 0}, {}), + anchor = range2({1.0, 0},{0.0, 0.0}), + alignment = { 0.0, 0 }, + pos = { cursor_x_offset - text_size, 0 } + }) + cursor_widget := ui_widget(name("cursor"), {}) + cursor_widget.style.bg_color = RGBA8{255, 255, 255, 255} + cursor_widget.layout.anchor.right = 0.0 + } + } +} +#endregion("Text Input") + #region("Vertical Box") /* Vertical Boxes automatically manage a collection of widgets and