scrolling_area; better mouse support
authorBrendan Hansen <brendan.f.hansen@gmail.com>
Thu, 8 Jul 2021 15:43:46 +0000 (10:43 -0500)
committerBrendan Hansen <brendan.f.hansen@gmail.com>
Thu, 8 Jul 2021 15:43:46 +0000 (10:43 -0500)
modules/immediate_mode/immediate_renderer.onyx
modules/immediate_mode/transform.onyx
modules/ui/components/button.onyx
modules/ui/components/checkbox.onyx
modules/ui/components/scrollable_area.onyx
modules/ui/components/textbox.onyx
modules/ui/input.onyx [new file with mode: 0644]
modules/ui/module.onyx
modules/ui/ui.onyx

index a8455f690ff047fd4ea56dc083fd9844f68ad466..517f23c0039bf89aaf90d267acaa3e5efb4064c9 100644 (file)
@@ -144,7 +144,7 @@ Immediate_Renderer :: struct {
     flush :: (use ir: ^Immediate_Renderer) {
         if vertex_count == 0 do return;
 
-        if world_transform_dirty || true {
+        if world_transform_dirty {
             world_transform_dirty = false;
             world_transform := array.get_ptr(^world_transform_stack, -1);
             world_matrix := transform_to_matrix(world_transform);
@@ -295,8 +295,6 @@ 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.
     push_scissor :: (use ir: ^Immediate_Renderer, x: f32, y: f32, w: f32, h: f32, intersect_previous := true) {
         if vertex_count > 0 do ir->flush();
 
@@ -319,6 +317,8 @@ Immediate_Renderer :: struct {
     }
 
     pop_scissor :: (use ir: ^Immediate_Renderer) {
+        if vertex_count > 0 do ir->flush();
+        
         array.pop(^scissor_stack);
 
         if scissor_stack.count > 0 {
@@ -382,7 +382,9 @@ Immediate_Renderer :: struct {
         world_transform_dirty = true;
 
         array.push(^world_transform_stack, world_transform_stack[world_transform_stack.count - 1]);
-        transform_identity(array.get_ptr(^world_transform_stack, -1));
+        *array.get_ptr(^world_transform_stack, -1) = *array.get_ptr(^world_transform_stack, -2);
+
+        // transform_identity(array.get_ptr(^world_transform_stack, -1));
     }
 
     pop_matrix :: (use ir: ^Immediate_Renderer) {
@@ -400,6 +402,10 @@ Immediate_Renderer :: struct {
         return array.get_ptr(^world_transform_stack, -1);
     }
 
+    apply_transform :: (use ir: ^Immediate_Renderer, transform: Transform) {
+        transform_apply(array.get_ptr(^world_transform_stack, -1), transform);
+    }
+
     to_screen_coordinates :: (use ir: ^Immediate_Renderer, use v: Vector2) -> Vector2 {
         transform := ir->get_transform();
 
@@ -503,3 +509,4 @@ set_window_size :: (width: i32, height: i32) {
 push_matrix :: () do global_renderer->push_matrix();
 pop_matrix  :: () do global_renderer->pop_matrix();
 identity    :: () do global_renderer->identity();
+apply_transform :: (transform: Transform) do global_renderer->apply_transform(transform);
index 56ed1a71c7772cf1bf8d9134d8abcd4c792ef2db..0489747fe9cf2e803d6a3946ec6f71e57fb0b4b7 100644 (file)
@@ -34,4 +34,21 @@ transform_to_matrix :: (use t: ^Transform) -> [16] f32 {
         0, 0, 1, 0,
         tx, ty, 0, 1
     ];
+}
+
+transform_translate :: (use t: ^Transform, v: Vector2) {
+    translation.x += v.x;
+    translation.y += v.y;
+}
+
+transform_scale :: (use t: ^Transform, v: Vector2) {
+    scale.x *= v.x;
+    scale.y *= v.y;
+}
+
+transform_apply :: (use t: ^Transform, other: Transform) {
+    translation.x += other.translation.x * scale.x;
+    translation.y += other.translation.y * scale.y;
+    scale.x *= other.scale.x;
+    scale.y *= other.scale.y;
 }
\ No newline at end of file
index 0ba70b53df4165510e10aa674f50134d8d2b4770..7e27da186a833babd33311f1ed40fdceed05ab52 100644 (file)
@@ -20,10 +20,11 @@ button :: (use r: Rectangle, text: str, theme := ^default_button_theme, site :=
 
     hash := get_site_hash(site, increment);
     animation_state := map.get(^animation_states, hash);
+    mx, my := get_mouse_position();
 
     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) {
+            if is_hot_item(hash) && Rectangle.contains(r, mx, my) {
                 result = true;
                 animation_state.click_time = 1.0f;
             }
@@ -37,7 +38,7 @@ button :: (use r: Rectangle, text: str, theme := ^default_button_theme, site :=
         }
     }
 
-    if Rectangle.contains(r, mouse_state.x, mouse_state.y) {
+    if Rectangle.contains(r, mx, my) {
         set_hot_item(hash);
     }
 
index d844a507bacc462278e60d46576af22d3ad07a3e..1d6066a2b139f39d558cb5aaf15fe72fe43f675e 100644 (file)
@@ -24,10 +24,11 @@ checkbox :: (use r: Rectangle, value: ^bool, text: str, theme := ^default_checkb
 
     hash := get_site_hash(site, increment);
     animation_state := map.get(^animation_states, hash);
+    mx, my := get_mouse_position();
 
     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) {
+            if is_hot_item(hash) && Rectangle.contains(r, mx, my) {
                 result = true;
                 *value = !*value;
                 animation_state.click_time = 1.0f;
@@ -42,7 +43,7 @@ checkbox :: (use r: Rectangle, value: ^bool, text: str, theme := ^default_checkb
         }
     }
 
-    if Rectangle.contains(r, mouse_state.x, mouse_state.y) {
+    if Rectangle.contains(r, mx, my) {
         set_hot_item(hash);
     }
 
index 5cab4ab97c149791ee927a8b7335bfc885e082e0..b13fef5860b567c649499257f854a1a1b7ba0681 100644 (file)
@@ -1,10 +1,88 @@
 package ui
+
 use package core
 
-scrollable_area_start :: (use r: Rectangle) {
+#private_file
+SA_State :: struct {
+    transform: gfx.Transform = .{
+        translation = .{ 0, 0 },
+        scale       = .{ 1, 1 },
+    };
+
+    dragging := false;
+}
+
+#private
+scrollable_area_states : map.Map(UI_Id, SA_State);
+
+scrollable_area_start :: (use r: Rectangle, site := #callsite) {
+    hash := get_site_hash(site, 0);
+    x, y := Rectangle.top_left(r);    
+    width, height := Rectangle.dimensions(r);
+
+    state := map.get(^scrollable_area_states, hash);
+
+    mx, my := get_mouse_position();
+    if Rectangle.contains(r, mx, my) {
+        if hot_item == 0 do set_hot_item(hash);
+    }
+
+    if is_hot_item(hash) {
+        speed :: 30.0f; @ThemeConfiguration
+        scale_speed :: 0.02f; @ThemeConfiguration
+
+        if is_key_down(38)  do state.transform.translation.y += speed;
+        if is_key_down(40)  do state.transform.translation.y -= speed;
+        if is_key_down(39)  do state.transform.translation.x -= speed;
+        if is_key_down(37)  do state.transform.translation.x += speed;
+
+        if is_key_down(187) do zoom(^state, r, 1.02);
+        if is_key_down(189) do zoom(^state, r, 0.98);
+
+        if mouse_state.left_button_just_down && !state.dragging {
+            state.dragging = true;
+        }
+
+        if state.dragging {
+            if !mouse_state.left_button_down {
+                state.dragging = false;
+
+            } else {
+                dx, dy := get_mouse_delta();
+                state.transform.translation.x -= dx;
+                state.transform.translation.y -= dy;
+            }
+        }
+
+        if mouse_state.dwheel > 0 do zoom(^state, r, 1.04);
+        if mouse_state.dwheel < 0 do zoom(^state, r, 0.96);
+
+    } else {
+        state.dragging = false;
+    }
+
+    map.put(^scrollable_area_states, hash, state);
+
+    gfx.push_scissor(x, y, width, height);
+    gfx.push_matrix();
+    gfx.apply_transform(state.transform);
+
+    zoom :: (state: ^SA_State, r: Rectangle, scale := 1.0f) {
+        x, y := Rectangle.top_left(r);    
+        width, height := Rectangle.dimensions(r);
+
+        bx: f32 = (state.transform.translation.x - (width / 2) - x) / state.transform.scale.x;
+        by: f32 = (state.transform.translation.y - (height / 2) - y) / state.transform.scale.y;
+
+        state.transform.scale.x *= scale;
+        state.transform.scale.y *= scale;
 
+        state.transform.translation.x = bx * state.transform.scale.x + (width / 2) + x;
+        state.transform.translation.y = by * state.transform.scale.y + (height / 2) + y;
+    }
 }
 
 scrollable_area_end :: () {
-    
+    gfx.pop_scissor();
+    gfx.pop_matrix();
 }
\ No newline at end of file
index 52d26a7e7d593af263ffed864bf2a87688e81c61..ed5d979a3b306448ba536d735dc9b4236ce389c5 100644 (file)
@@ -46,6 +46,7 @@ textbox :: (use r: Rectangle, text_buffer: ^string.String_Buffer, theme := ^defa
 
     hash := get_site_hash(site, increment);
     animation_state := map.get(^animation_states, hash);
+    mx, my := get_mouse_position();
 
     border_width  := theme.border_width;
     width, height := Rectangle.dimensions(r);
@@ -58,7 +59,7 @@ textbox :: (use r: Rectangle, text_buffer: ^string.String_Buffer, theme := ^defa
     text_y := y0 + ~~ font.common.baseline * theme.font_size + (height - text_height) / 2;
 
     if is_hot_item(hash) && !is_active_item(hash) {
-        if mouse_state.left_button_down && Rectangle.contains(r, mouse_state.x, mouse_state.y) {
+        if mouse_state.left_button_down && Rectangle.contains(r, mx, my) {
             set_active_item(hash);
             textbox_editing_state.hash = hash;
             textbox_editing_state.cursor_animation_speed = theme.cursor_blink_speed;
@@ -68,14 +69,14 @@ textbox :: (use r: Rectangle, text_buffer: ^string.String_Buffer, theme := ^defa
     }
 
     if is_active_item(hash) {
-        if mouse_state.left_button_just_down && !Rectangle.contains(r, mouse_state.x, mouse_state.y) {
+        if mouse_state.left_button_just_down && !Rectangle.contains(r, mx, my) {
             set_active_item(0);
             textbox_editing_state.hash = 0;
             textbox_editing_state.cursor_position = 0;
         }
     }
 
-    if Rectangle.contains(r, mouse_state.x, mouse_state.y) {
+    if Rectangle.contains(r, mx, my) {
         set_hot_item(hash);
     }
 
@@ -84,9 +85,9 @@ textbox :: (use r: Rectangle, text_buffer: ^string.String_Buffer, theme := ^defa
         move_towards(^textbox_editing_state.cursor_animation, 0.0f, textbox_editing_state.cursor_animation_speed);
         if textbox_editing_state.cursor_animation <= 0.0f do textbox_editing_state.cursor_animation = 1.0f;
 
-        if mouse_state.left_button_down && Rectangle.contains(r, mouse_state.x, mouse_state.y) {
+        if mouse_state.left_button_down && Rectangle.contains(r, mx, my) {
             textbox_editing_state.cursor_animation = 1.0f;
-            textbox_editing_state.cursor_position = get_cursor_position(text_buffer, text_x, text_y, theme.font_size, mouse_state.x, mouse_state.y);
+            textbox_editing_state.cursor_position = get_cursor_position(text_buffer, text_x, text_y, theme.font_size, mx, my);
         }
 
         if keyboard_state.keys_down_this_frame > 0 {
@@ -139,6 +140,7 @@ textbox :: (use r: Rectangle, text_buffer: ^string.String_Buffer, theme := ^defa
     }
 
     gfx.set_texture();
+    gfx.push_scissor(x0, y0, width, height);
     gfx.rect(.{ x0, y0 }, .{ width, height }, theme.border_color);
 
     surface_color := color_lerp(animation_state.hover_time, theme.background_color, theme.hover_color);
@@ -161,6 +163,8 @@ textbox :: (use r: Rectangle, text_buffer: ^string.String_Buffer, theme := ^defa
             color=cursor_color);
     }
 
+    gfx.pop_scissor();
+
     move_towards(^animation_state.click_time, 0.0f, theme.click_decay_speed);
 
     if animation_state.click_time > 0 || animation_state.hover_time > 0 {
diff --git a/modules/ui/input.onyx b/modules/ui/input.onyx
new file mode 100644 (file)
index 0000000..5406e4c
--- /dev/null
@@ -0,0 +1,162 @@
+package ui
+
+
+Mouse_State :: struct {
+    left_button_down      := false;
+    left_button_just_down := false;
+    left_button_just_up   := false;
+
+    right_button_down      := false;
+    right_button_just_down := false;
+    right_button_just_up   := false;
+
+    dwheel : f32 = 0;
+
+    x_  : f32 = 0;
+    y_  : f32 = 0;
+    dx_ : f32 = 0;
+    dy_ : f32 = 0;
+}
+
+mouse_state := Mouse_State.{};
+
+Keyboard_State :: struct {
+    Max_Keys_Per_Frame :: 4;
+
+    Key_Event :: struct {
+        code: u32 = 0;
+        modifiers: Modifiers = ~~0;
+
+        Modifiers :: enum #flags {
+            CTRL; ALT; SHIFT; META;
+        }
+    }
+
+    keycodes_down_this_frame  : [Max_Keys_Per_Frame] Key_Event;
+    keys_down_this_frame      : u32;
+
+    keycodes_up_this_frame    : [Max_Keys_Per_Frame] Key_Event;
+    keys_up_this_frame        : u32;
+
+    Key_Count :: 256;
+
+    Key_State :: enum (u8) {
+        Up;
+        Down;
+        Just_Up;
+        Just_Down;
+    }
+
+    state : [Key_Count] Key_State;
+}
+
+@Note // This assumes that this gets zero intialized.
+keyboard_state: Keyboard_State;
+
+
+
+
+//
+// Telling the UI system about hardware updates
+//
+
+update_mouse_position :: (new_x: f32, new_y: f32) {
+    mouse_state.dx_ += mouse_state.x_ - new_x;
+    mouse_state.dy_ += mouse_state.y_ - new_y;
+    mouse_state.x_  = new_x;
+    mouse_state.y_  = new_y;
+}
+
+#private_file Mouse_Button_Kind :: enum { Left; Right; Middle; WheelUp; WheelDown; }
+
+button_pressed :: (kind: Mouse_Button_Kind) {
+    switch kind {
+        case .Left {
+            mouse_state.left_button_down      = true;
+            mouse_state.left_button_just_down = true;
+        }
+
+        case .Right {
+            mouse_state.right_button_down      = true;
+            mouse_state.right_button_just_down = true;
+        }
+        
+        case .WheelUp   do mouse_state.dwheel =  1.0;
+        case .WheelDown do mouse_state.dwheel = -1.0;
+    }
+}
+
+button_released :: (kind: Mouse_Button_Kind) {
+    switch kind {
+        case .Left {
+            mouse_state.left_button_down    = false;
+            mouse_state.left_button_just_up = true;
+        }
+
+        case .Right {
+            mouse_state.right_button_down    = false;
+            mouse_state.right_button_just_up = true;
+        }
+
+        case .WheelUp   do mouse_state.dwheel = 0;
+        case .WheelDown do mouse_state.dwheel = 0;
+    }
+}
+
+#private_file
+__key_state_transition_table := Keyboard_State.Key_State.[
+                    /* KeyUp */                        /* KeyDown */
+    /* Up */        Keyboard_State.Key_State.Up,       Keyboard_State.Key_State.Just_Down,
+    /* Down */      Keyboard_State.Key_State.Just_Up,  Keyboard_State.Key_State.Down,
+    /* Just_Up */   Keyboard_State.Key_State.Up,       Keyboard_State.Key_State.Just_Down,
+    /* Just_Down */ Keyboard_State.Key_State.Just_Up,  Keyboard_State.Key_State.Down,
+]
+
+key_down :: (keycode: u32, modifiers: Keyboard_State.Key_Event.Modifiers) {
+    keyboard_state.keycodes_down_this_frame[keyboard_state.keys_down_this_frame] = .{
+        keycode,
+        modifiers
+    };
+
+    keyboard_state.keys_down_this_frame += 1;
+    keyboard_state.state[keycode] = __key_state_transition_table[cast(i32) keyboard_state.state[keycode] * 2 + 1];
+}
+
+key_up :: (keycode: u32, modifiers: Keyboard_State.Key_Event.Modifiers) {
+    keyboard_state.keycodes_up_this_frame[keyboard_state.keys_up_this_frame] = .{
+        keycode,
+        modifiers
+    };
+
+    keyboard_state.keys_up_this_frame += 1;
+    keyboard_state.state[keycode] = __key_state_transition_table[cast(i32) keyboard_state.state[keycode] * 2];
+}
+
+
+
+//
+// Querying the UI system for current input state
+//
+
+// Relative to the current transformations on the immediate renderer
+get_mouse_position :: () -> (x: f32, y: f32) {
+    transform := gfx.global_renderer->get_transform();
+    return (mouse_state.x_ - transform.translation.x) / transform.scale.x,
+           (mouse_state.y_ - transform.translation.y) / transform.scale.y;
+}
+
+get_mouse_delta :: () -> (dx: f32, dy: f32) {
+    transform := gfx.global_renderer->get_transform();
+    return mouse_state.dx_ / transform.scale.x,
+           mouse_state.dy_ / transform.scale.y;
+}
+
+is_key_down :: (keycode: u32) -> bool {
+    s := keyboard_state.state[keycode];
+    return s == .Down || s == .Just_Down;
+}
+
+is_key_up :: (keycode: u32) -> bool {
+    s := keyboard_state.state[keycode];
+    return s == .Up || s == .Just_Up;
+}
\ No newline at end of file
index 2174ce95f8ef751d52f92c20685af30e78fc2949..341a5dc0e6f8ea5d21699bc249a58d8508c65362 100644 (file)
@@ -41,13 +41,14 @@ package ui
 
 #load "./ui"
 #load "./flow"
+#load "./input"
 
 #load "./components/button"
 #load "./components/checkbox"
 #load "./components/slider"
 #load "./components/radio"
 #load "./components/textbox"
-
+#load "./components/scrollable_area"
 
 // Package inclusions that are part of all files in the "ui" package.
 #private gfx    :: package immediate_mode    // The immediate_mode module needs to be accessible
index dcb5027ade2ab606f643c1604f996e247b15656a..6310c56b98a7992cdc4e24646727ada12be44772 100644 (file)
@@ -17,58 +17,26 @@ UI_Id :: #type u32
 #private active_item : UI_Id = 0
 #private hot_item_was_set := false
 
-Mouse_State :: struct {
-    left_button_down      := false;
-    left_button_just_down := false;
-    left_button_just_up   := false;
-
-    right_button_down      := false;
-    right_button_just_down := false;
-    right_button_just_up   := false;
-
-    x: f32 = 0;
-    y: f32 = 0;
-}
-
-mouse_state := Mouse_State.{};
-
-Keyboard_State :: struct {
-    Max_Keys_Per_Frame :: 4;
-
-    Key_State :: struct {
-        code: u32 = 0;
-        modifiers: Modifiers = ~~0;
-
-        Modifiers :: enum #flags {
-            CTRL; ALT; SHIFT; META;
-        }
-    }
-
-    keycodes_down_this_frame  : [Max_Keys_Per_Frame] Key_State;
-    keys_down_this_frame      : u32;
-
-    keycodes_up_this_frame    : [Max_Keys_Per_Frame] Key_State;
-    keys_up_this_frame        : u32;
-}
-
-@Note // This assumes that this gets zero intialized.
-keyboard_state: Keyboard_State;
-
 
 init_ui :: () {
     init_font();
 
     map.init(^animation_states, default=.{}, hash_count=4);
+    map.init(^scrollable_area_states, default=.{}, hash_count=4);
 }
 
 // This function should be called at the end of drawing a frame, after all of the UI elements
 // had a chance to interact with the hardware events.
-clear_buttons :: () {
+end_frame :: () {
     mouse_state.left_button_just_up    = false;
     mouse_state.left_button_just_down  = false;
     mouse_state.right_button_just_up   = false;
     mouse_state.right_button_just_down = false;
 
+    mouse_state.dwheel = 0;
+    mouse_state.dx_ = 0;
+    mouse_state.dy_ = 0;
+
     // I don't think these need to be cleared every frame, so long as you don't try
     // to use them without checking keys_down_this_frame first.
     // for ^key: keyboard_state.keycodes_down_this_frame do *key = .{};
@@ -81,62 +49,6 @@ clear_buttons :: () {
     hot_item_was_set = false;
 }
 
-//
-// Telling the UI system about hardware updates
-//
-
-update_mouse_position :: (new_x: f32, new_y: f32) {
-    mouse_state.x = new_x;
-    mouse_state.y = new_y;
-}
-
-#private_file Mouse_Button_Kind :: enum { Left; Right; Middle; }
-
-button_pressed :: (kind: Mouse_Button_Kind) {
-    switch kind {
-        case .Left {
-            mouse_state.left_button_down      = true;
-            mouse_state.left_button_just_down = true;
-        }
-        case .Right {
-            mouse_state.right_button_down      = true;
-            mouse_state.right_button_just_down = true;
-        }
-    }
-}
-
-button_released :: (kind: Mouse_Button_Kind) {
-    switch kind {
-        case .Left {
-            mouse_state.left_button_down    = false;
-            mouse_state.left_button_just_up = true;
-        }
-
-        case .Right {
-            mouse_state.right_button_down    = false;
-            mouse_state.right_button_just_up = true;
-        }
-    }
-}
-
-key_down :: (keycode: u32, modifiers: Keyboard_State.Key_State.Modifiers) {
-    keyboard_state.keycodes_down_this_frame[keyboard_state.keys_down_this_frame] = .{
-        keycode,
-        modifiers
-    };
-
-    keyboard_state.keys_down_this_frame += 1;
-}
-
-key_up :: (keycode: u32, modifiers: Keyboard_State.Key_State.Modifiers) {
-    keyboard_state.keycodes_up_this_frame[keyboard_state.keys_up_this_frame] = .{
-        keycode,
-        modifiers
-    };
-
-    keyboard_state.keys_up_this_frame += 1;
-}
-
 set_active_item :: (id: UI_Id) -> bool {
     active_item = id;
     return true;