Restructured the codebase yet again but this time with compiler support for monlithic packages
So no need to stage generate symbolic links in a flat directory for the compiler
This commit is contained in:
0
code/sectr/.ODIN_MONOLITHIC_PACKAGE
Normal file
0
code/sectr/.ODIN_MONOLITHIC_PACKAGE
Normal file
41
code/sectr/app/scratch.odin
Normal file
41
code/sectr/app/scratch.odin
Normal file
@ -0,0 +1,41 @@
|
||||
package sectr
|
||||
|
||||
// Scratch space
|
||||
|
||||
import rl "vendor:raylib"
|
||||
|
||||
DebugData :: struct {
|
||||
square_size : i32,
|
||||
square_pos : rl.Vector2,
|
||||
|
||||
draw_debug_text_y : f32,
|
||||
|
||||
cursor_locked : b32,
|
||||
cursor_unlock_pos : Vec2, // Raylib changes the mose position on lock, we want restore the position the user would be in on screen
|
||||
mouse_vis : b32,
|
||||
last_mouse_pos : Vec2,
|
||||
|
||||
// UI Vis
|
||||
draw_ui_box_bounds_points : bool,
|
||||
draw_UI_padding_bounds : bool,
|
||||
draw_ui_content_bounds : bool,
|
||||
|
||||
// Test First
|
||||
frame_2_created : b32,
|
||||
|
||||
// Test Draggable
|
||||
draggable_box_pos : Vec2,
|
||||
draggable_box_size : Vec2,
|
||||
box_original_size : Vec2,
|
||||
|
||||
// Test parsing
|
||||
path_lorem : string,
|
||||
lorem_content : []byte,
|
||||
lorem_parse : PWS_ParseResult,
|
||||
|
||||
// Test 3d Viewport
|
||||
cam_vp : rl.Camera3D,
|
||||
viewport_rt : rl.RenderTexture,
|
||||
|
||||
proto_text_shader : rl.Shader
|
||||
}
|
320
code/sectr/app/screen.odin
Normal file
320
code/sectr/app/screen.odin
Normal file
@ -0,0 +1,320 @@
|
||||
package sectr
|
||||
|
||||
UI_ScreenState :: struct
|
||||
{
|
||||
using base : UI_State,
|
||||
|
||||
floating : UI_FloatingManager,
|
||||
|
||||
// TODO(Ed): The docked should be the base, floating is should be nested within as a 'veiwport' to a 'desktop' or 'canvas'
|
||||
// docked : UI_Docking,
|
||||
|
||||
menu_bar : struct
|
||||
{
|
||||
pos, size : Vec2,
|
||||
container : UI_HBox,
|
||||
settings_btn : struct
|
||||
{
|
||||
using widget : UI_Widget,
|
||||
}
|
||||
},
|
||||
settings_menu : struct
|
||||
{
|
||||
pos, size, min_size : Vec2,
|
||||
container : UI_Widget,
|
||||
is_open : b32,
|
||||
is_maximized : b32,
|
||||
},
|
||||
}
|
||||
|
||||
ui_screen_tick :: proc() {
|
||||
profile("Screenspace Imgui")
|
||||
|
||||
using state := get_state()
|
||||
ui_graph_build( & screen_ui )
|
||||
ui := ui_context
|
||||
|
||||
ui_floating_manager_begin( & screen_ui.floating )
|
||||
{
|
||||
ui_floating("Menu Bar", ui_screen_menu_bar)
|
||||
ui_floating("Settings Menu", ui_screen_settings_menu)
|
||||
}
|
||||
ui_floating_manager_end()
|
||||
}
|
||||
|
||||
ui_screen_menu_bar :: proc( captures : rawptr = nil ) -> (should_raise : b32 = false )
|
||||
{
|
||||
profile("App Menu Bar")
|
||||
fmt :: str_fmt_alloc
|
||||
|
||||
@(deferred_none = ui_theme_pop)
|
||||
ui_theme_app_menu_bar_default :: proc()
|
||||
{
|
||||
@static theme : UI_Theme
|
||||
@static loaded : b32 = false
|
||||
if true && ! loaded
|
||||
{
|
||||
layout := UI_Layout {
|
||||
flags = {},
|
||||
anchor = range2({},{}),
|
||||
alignment = {0.5, 0.5},
|
||||
text_alignment = {0.0, 1.5},
|
||||
font_size = 12,
|
||||
margins = {0, 0, 0, 0},
|
||||
padding = {0, 0, 0, 0},
|
||||
border_width = 0.6,
|
||||
pos = {0, 0},
|
||||
size = range2({},{})
|
||||
}
|
||||
style := UI_Style {
|
||||
bg_color = Color_ThmDark_BG,
|
||||
border_color = Color_ThmDark_Border_Default,
|
||||
corner_radii = {},
|
||||
blur_size = 0,
|
||||
font = get_state().default_font,
|
||||
text_color = Color_ThmDark_Text_Default,
|
||||
cursor = {},
|
||||
}
|
||||
|
||||
// loaded = true
|
||||
layout_combo := to_ui_layout_combo(layout)
|
||||
style_combo := to_ui_style_combo(style)
|
||||
{
|
||||
using layout_combo.hot
|
||||
using style_combo.hot
|
||||
bg_color = Color_ThmDark_Btn_BG_Hot
|
||||
text_color = Color_ThmDark_Text_Hot
|
||||
}
|
||||
{
|
||||
using layout_combo.active
|
||||
using style_combo.active
|
||||
bg_color = Color_ThmDark_Btn_BG_Active
|
||||
text_color = Color_ThmDark_Text_Active
|
||||
}
|
||||
|
||||
theme = UI_Theme {
|
||||
layout_combo, style_combo
|
||||
}
|
||||
}
|
||||
ui_layout_push(theme.layout)
|
||||
ui_style_push(theme.style)
|
||||
}
|
||||
|
||||
using state := get_state()
|
||||
using screen_ui
|
||||
{
|
||||
using screen_ui.menu_bar
|
||||
ui_theme_app_menu_bar_default()
|
||||
container = ui_hbox( .Left_To_Right, "Menu Bar" )
|
||||
{
|
||||
using container
|
||||
layout.flags = {.Fixed_Position_X, .Fixed_Position_Y, .Fixed_Width, .Fixed_Height, .Origin_At_Anchor_Center}
|
||||
layout.pos = pos
|
||||
layout.size = range2( size, {})
|
||||
text = str_intern("menu_bar")
|
||||
}
|
||||
|
||||
ui_theme_btn_default()
|
||||
move_box := ui_button("Move Box");
|
||||
{
|
||||
using move_box
|
||||
if active {
|
||||
pos += input.mouse.delta
|
||||
should_raise = true
|
||||
}
|
||||
layout.anchor.ratio.x = 0.4
|
||||
}
|
||||
|
||||
spacer := ui_spacer("Menu Bar: Move Spacer")
|
||||
spacer.layout.flags |= {.Fixed_Width}
|
||||
spacer.layout.size.min.x = 30
|
||||
|
||||
// TODO(Ed): Implement an external composition for theme interpolation using the settings btn
|
||||
settings_btn.widget = ui_button("Menu Bar: Settings Btn")
|
||||
{
|
||||
using settings_btn
|
||||
text = str_intern("Settings")
|
||||
layout.flags = {
|
||||
// .Scale_Width_By_Height_Ratio,
|
||||
.Fixed_Width
|
||||
}
|
||||
layout.size.min.x = 100
|
||||
if pressed {
|
||||
screen_ui.settings_menu.is_open = true
|
||||
}
|
||||
}
|
||||
|
||||
spacer = ui_spacer("Menu Bar: End Spacer")
|
||||
spacer.layout.anchor.ratio.x = 1.0
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ui_screen_settings_menu :: proc( captures : rawptr = nil ) -> ( should_raise : b32 = false)
|
||||
{
|
||||
profile("Settings Menu")
|
||||
using state := get_state()
|
||||
using state.screen_ui
|
||||
if ! settings_menu.is_open do return
|
||||
|
||||
using settings_menu
|
||||
if size.x < min_size.x do size.x = min_size.x
|
||||
if size.y < min_size.y do size.y = min_size.y
|
||||
|
||||
ui_theme_transparent()
|
||||
container = ui_widget("Settings Menu", {})
|
||||
{
|
||||
using container
|
||||
layout.flags = { .Fixed_Width, .Fixed_Height, .Fixed_Position_X, .Fixed_Position_Y, .Origin_At_Anchor_Center }
|
||||
style.bg_color = Color_ThmDark_Translucent_Panel
|
||||
style.border_color = { 0, 0, 0, 200 }
|
||||
layout.alignment = {0.0, 0.0}
|
||||
layout.border_width = 1.0
|
||||
layout.pos = pos
|
||||
layout.size = range2( size, {})
|
||||
}
|
||||
ui_parent(container)
|
||||
if settings_menu.is_maximized {
|
||||
using container
|
||||
layout.flags = {.Origin_At_Anchor_Center }
|
||||
layout.pos = {}
|
||||
}
|
||||
should_raise |= ui_resizable_handles( & container, & pos, & size/*, compute_layout = true*/)
|
||||
// ui_box_compute_layout(container)
|
||||
|
||||
vbox := ui_vbox_begin( .Top_To_Bottom, "Settings Menu: VBox", {.Mouse_Clickable}, compute_layout = true)
|
||||
{
|
||||
should_raise |= b32(vbox.active)
|
||||
ui_parent(vbox)
|
||||
|
||||
ui_layout( UI_Layout {
|
||||
// font_size = 16,
|
||||
// alignment = {0, 1},
|
||||
})
|
||||
ui_style( UI_Style {
|
||||
// bg_color = Color_Transparent,
|
||||
font = default_font,
|
||||
text_color = Color_White,
|
||||
})
|
||||
ui_style_ref().hot.bg_color = Color_Blue
|
||||
frame_bar := ui_hbox_begin(.Left_To_Right, "Settings Menu: Frame Bar", { .Mouse_Clickable, .Focusable, .Click_To_Focus })
|
||||
{
|
||||
frame_bar.layout.flags = {.Fixed_Height}
|
||||
frame_bar.layout.size.min.y = 50
|
||||
ui_parent(frame_bar)
|
||||
|
||||
ui_layout( UI_Layout {
|
||||
font_size = 18,
|
||||
})
|
||||
title := ui_text("Settings Menu: Title", str_intern("Settings Menu"), {.Disabled})
|
||||
{
|
||||
using title
|
||||
layout.margins = { 0, 0, 15, 0}
|
||||
layout.text_alignment = {0 , 0.5}
|
||||
layout.anchor.ratio.x = 1.0
|
||||
}
|
||||
|
||||
ui_layout( UI_Layout {
|
||||
font_size = 16,
|
||||
})
|
||||
|
||||
ui_style(ui_style_peek())
|
||||
style := ui_style_ref()
|
||||
maximize_btn := ui_button("Settings Menu: Maximize Btn")
|
||||
{
|
||||
using maximize_btn
|
||||
layout.flags = {.Fixed_Width}
|
||||
layout.size.min = {50, 50}
|
||||
layout.text_alignment = {0.5, 0.5}
|
||||
layout.anchor.ratio.x = 1.0
|
||||
if maximize_btn.pressed {
|
||||
settings_menu.is_maximized = ~settings_menu.is_maximized
|
||||
should_raise = true
|
||||
}
|
||||
if settings_menu.is_maximized do text = str_intern("min")
|
||||
else do text = str_intern("max")
|
||||
}
|
||||
close_btn := ui_button("Settings Menu: Close Btn")
|
||||
{
|
||||
using close_btn
|
||||
text = str_intern("close")
|
||||
layout.flags = {.Fixed_Width}
|
||||
layout.size.min = {50, 0}
|
||||
layout.text_alignment = {0.5, 0.5}
|
||||
layout.anchor.ratio.x = 1.0
|
||||
if close_btn.pressed {
|
||||
settings_menu.is_open = false
|
||||
}
|
||||
}
|
||||
|
||||
ui_hbox_end(frame_bar, compute_layout = true)
|
||||
}
|
||||
if frame_bar.active {
|
||||
pos += input.mouse.delta
|
||||
should_raise = true
|
||||
}
|
||||
|
||||
// Populate settings with values from config (hardcoded for now)
|
||||
ui_layout(UI_Layout {
|
||||
flags = {
|
||||
// .Origin_At_Anchor_Center,
|
||||
// .Fixed_Height,
|
||||
},
|
||||
// pos = {0, 50},
|
||||
// size = range2({100, 100},{}),
|
||||
// alignment = {0,0},
|
||||
})
|
||||
ui_style( UI_Style {
|
||||
// bg_color = Color_GreyRed
|
||||
})
|
||||
drop_down_bar := ui_hbox_begin(.Left_To_Right, "settings_menu.vbox: config drop_down_bar", {.Mouse_Clickable})
|
||||
btn : UI_Widget
|
||||
{
|
||||
drop_down_bar.layout.anchor.ratio.y = 0.1
|
||||
{
|
||||
using drop_down_bar
|
||||
text = str_intern("drop_down_bar")
|
||||
// style.bg_color = { 55, 55, 55, 100 }
|
||||
style.font = default_font
|
||||
style.text_color = Color_White
|
||||
layout.flags = {.Fixed_Height}
|
||||
layout.font_size = 12
|
||||
layout.text_alignment = {1, 0}
|
||||
layout.size.min.y = 35
|
||||
}
|
||||
ui_parent(drop_down_bar)
|
||||
|
||||
btn = ui_text("pls", str_intern("Lets figure this out..."))
|
||||
{
|
||||
using btn
|
||||
text = str_intern("Config")
|
||||
style.font = default_font
|
||||
style.text_color = Color_White
|
||||
// layout.flags = {.Origin_At_Anchor_Center}
|
||||
layout.alignment = {0.0, 0.0}
|
||||
layout.anchor.ratio.x = 1.0
|
||||
layout.font_size = 12
|
||||
layout.margins = {0,0, 15, 0}
|
||||
layout.size.min.y = 35
|
||||
}
|
||||
um := ui_spacer("um...")
|
||||
um.layout.anchor.ratio.x = 1.0
|
||||
ui_hbox_end(drop_down_bar, compute_layout = true)
|
||||
}
|
||||
|
||||
// ui_layout(UI_Layout {
|
||||
|
||||
// })
|
||||
// ui_style( UI_Style {
|
||||
|
||||
// })
|
||||
// res_width_hbox := ui_hbox_begin(.Left_To_Right, "settings_menu.vbox: config.resolution_width: hbox", {})
|
||||
// ui_parent(res_width_hbox)
|
||||
|
||||
spacer := ui_spacer("Settings Menu: Spacer")
|
||||
spacer.layout.anchor.ratio.y = 1.0
|
||||
|
||||
ui_vbox_end(vbox, compute_layout = false )
|
||||
}
|
||||
return
|
||||
}
|
2
code/sectr/app/serialize.odin
Normal file
2
code/sectr/app/serialize.odin
Normal file
@ -0,0 +1,2 @@
|
||||
package sectr
|
||||
|
260
code/sectr/app/state.odin
Normal file
260
code/sectr/app/state.odin
Normal file
@ -0,0 +1,260 @@
|
||||
package sectr
|
||||
|
||||
import "base:runtime"
|
||||
import "core:fmt"
|
||||
import "core:mem"
|
||||
import "core:mem/virtual"
|
||||
import "core:os"
|
||||
|
||||
import rl "vendor:raylib"
|
||||
|
||||
Str_App_State := "App State"
|
||||
|
||||
#region("Memory")
|
||||
|
||||
Memory_App : Memory
|
||||
|
||||
Memory_Base_Address_Persistent :: Terabyte * 1
|
||||
Memory_Base_Address_Frame :: Memory_Base_Address_Persistent + Memory_Reserve_Persistent * 2
|
||||
Memory_Base_Address_Transient :: Memory_Base_Address_Frame + Memory_Reserve_Frame * 2
|
||||
Memory_Base_Address_Files_Buffer :: Memory_Base_Address_Transient + Memory_Reserve_Transient * 2
|
||||
|
||||
// This reserve goes beyond the typical amount of ram the user has,
|
||||
// TODO(Ed): Setup warnings when the amount is heading toward half the ram size
|
||||
Memory_Reserve_Persistent :: 32 * Gigabyte
|
||||
Memory_Reserve_Frame :: 16 * Gigabyte
|
||||
Memory_Reserve_Transient :: 16 * Gigabyte
|
||||
Memory_Reserve_FilesBuffer :: 64 * Gigabyte
|
||||
|
||||
Memory_Commit_Initial_Persistent :: 4 * Kilobyte
|
||||
Memory_Commit_Initial_Frame :: 4 * Kilobyte
|
||||
Memory_Commit_Initial_Transient :: 4 * Kilobyte
|
||||
Memory_Commit_Initial_Filebuffer :: 4 * Kilobyte
|
||||
|
||||
MemorySnapshot :: struct {
|
||||
persistent : []u8,
|
||||
frame : []u8,
|
||||
transient : []u8,
|
||||
// files_buffer cannot be restored from snapshot
|
||||
}
|
||||
|
||||
Memory :: struct {
|
||||
persistent : ^VArena,
|
||||
frame : ^VArena,
|
||||
transient : ^VArena,
|
||||
files_buffer : ^VArena,
|
||||
|
||||
state : ^State,
|
||||
|
||||
// Should only be used for small memory allocation iterations
|
||||
// Not for large memory env states
|
||||
snapshot : MemorySnapshot,
|
||||
|
||||
replay : ReplayState,
|
||||
logger : Logger,
|
||||
profiler : ^SpallProfiler
|
||||
}
|
||||
|
||||
persistent_allocator :: proc() -> Allocator {
|
||||
result := varena_allocator( Memory_App.persistent )
|
||||
return result
|
||||
}
|
||||
|
||||
frame_allocator :: proc() -> Allocator {
|
||||
result := varena_allocator( Memory_App.frame )
|
||||
return result
|
||||
}
|
||||
|
||||
transient_allocator :: proc() -> Allocator {
|
||||
result := varena_allocator( Memory_App.transient )
|
||||
return result
|
||||
}
|
||||
|
||||
files_buffer_allocator :: proc() -> Allocator {
|
||||
result := varena_allocator( Memory_App.files_buffer )
|
||||
return result
|
||||
}
|
||||
|
||||
persistent_slab_allocator :: proc() -> Allocator {
|
||||
state := get_state()
|
||||
result := slab_allocator( state.persistent_slab )
|
||||
return result
|
||||
}
|
||||
|
||||
frame_slab_allocator :: proc() -> Allocator {
|
||||
result := slab_allocator( get_state().frame_slab )
|
||||
return result
|
||||
}
|
||||
|
||||
transient_slab_allocator :: proc() -> Allocator {
|
||||
result := slab_allocator( get_state().transient_slab )
|
||||
return result
|
||||
}
|
||||
|
||||
// TODO(Ed) : Implment host memory mapping api
|
||||
save_snapshot :: proc( snapshot : ^MemorySnapshot )
|
||||
{
|
||||
// Make sure the snapshot size is able to hold the current size of the arenas
|
||||
// Grow the files & mapping otherwise
|
||||
{
|
||||
// TODO(Ed) : Implement eventually
|
||||
}
|
||||
|
||||
persistent := Memory_App.persistent
|
||||
mem.copy_non_overlapping( & snapshot.persistent[0], persistent.reserve_start, int(persistent.commit_used) )
|
||||
|
||||
frame := Memory_App.frame
|
||||
mem.copy_non_overlapping( & snapshot.frame[0], frame.reserve_start, int(frame.commit_used) )
|
||||
|
||||
transient := Memory_App.transient
|
||||
mem.copy_non_overlapping( & snapshot.transient[0], transient.reserve_start, int(transient.commit_used) )
|
||||
}
|
||||
|
||||
// TODO(Ed) : Implment host memory mapping api
|
||||
load_snapshot :: proc( snapshot : ^MemorySnapshot ) {
|
||||
persistent := Memory_App.persistent
|
||||
mem.copy_non_overlapping( persistent.reserve_start, & snapshot.persistent[0], int(persistent.commit_used) )
|
||||
|
||||
frame := Memory_App.frame
|
||||
mem.copy_non_overlapping( frame.reserve_start, & snapshot.frame[0], int(frame.commit_used) )
|
||||
|
||||
transient := Memory_App.transient
|
||||
mem.copy_non_overlapping( transient.reserve_start, & snapshot.transient[0], int(transient.commit_used) )
|
||||
}
|
||||
|
||||
// TODO(Ed) : Implement usage of this
|
||||
MemoryConfig :: struct {
|
||||
reserve_persistent : uint,
|
||||
reserve_frame : uint,
|
||||
reserve_transient : uint,
|
||||
reserve_filebuffer : uint,
|
||||
|
||||
commit_initial_persistent : uint,
|
||||
commit_initial_frame : uint,
|
||||
commit_initial_transient : uint,
|
||||
commit_initial_filebuffer : uint,
|
||||
}
|
||||
|
||||
#endregion("Memory")
|
||||
|
||||
#region("State")
|
||||
|
||||
// ALl nobs available for this application
|
||||
AppConfig :: struct {
|
||||
using memory : MemoryConfig,
|
||||
|
||||
resolution_width : uint,
|
||||
resolution_height : uint,
|
||||
refresh_rate : uint,
|
||||
|
||||
cam_min_zoom : f32,
|
||||
cam_max_zoom : f32,
|
||||
cam_zoom_mode : CameraZoomMode,
|
||||
cam_zoom_smooth_snappiness : f32,
|
||||
cam_zoom_sensitivity_smooth : f32,
|
||||
cam_zoom_sensitivity_digital : f32,
|
||||
|
||||
engine_refresh_hz : uint,
|
||||
|
||||
timing_fps_moving_avg_alpha : f32,
|
||||
|
||||
ui_resize_border_width : f32,
|
||||
}
|
||||
|
||||
AppWindow :: struct {
|
||||
extent : Extents2, // Window half-size
|
||||
dpi_scale : f32, // Dots per inch scale (provided by raylib via glfw)
|
||||
ppcm : f32, // Dots per centimetre
|
||||
}
|
||||
|
||||
FontData :: struct {
|
||||
provider : FontProviderData,
|
||||
|
||||
// TODO(Ed): We can have font constants here I guess but eventually
|
||||
// I rather have fonts configurable for a 'theme' combo
|
||||
// So that way which IDs are picked depends on runtime
|
||||
firacode : FontID,
|
||||
squidgy_slimes : FontID,
|
||||
rec_mono_semicasual_reg : FontID,
|
||||
|
||||
default_font : FontID,
|
||||
}
|
||||
|
||||
FrameTime :: struct {
|
||||
sleep_is_granular : b32,
|
||||
|
||||
delta_seconds : f64,
|
||||
delta_ms : f64,
|
||||
delta_ns : Duration,
|
||||
target_ms : f64,
|
||||
elapsed_ms : f64,
|
||||
avg_ms : f64,
|
||||
fps_avg : f64,
|
||||
}
|
||||
|
||||
// Global Singleton stored in the persistent virtual arena, the first allocated data.
|
||||
// Use get_state() to conviently retrieve at any point for the program's lifetime
|
||||
State :: struct {
|
||||
default_slab_policy : SlabPolicy,
|
||||
persistent_slab : Slab,
|
||||
frame_slab : Slab,
|
||||
transient_slab : Slab, // TODO(Ed): This needs to be recreated per transient wipe
|
||||
transinet_clear_lock : b32, // Pravents auto-free of transient at designated intervals
|
||||
transient_clear_time : f32, // Time in seconds for the usual period to clear transient
|
||||
transient_clear_elapsed : f32, // Time since last clear
|
||||
|
||||
string_cache : StringCache,
|
||||
|
||||
input_data : [2]InputState,
|
||||
input_prev : ^InputState,
|
||||
input : ^InputState,
|
||||
|
||||
debug : DebugData,
|
||||
|
||||
project : Project,
|
||||
|
||||
config : AppConfig,
|
||||
app_window : AppWindow,
|
||||
screen_ui : UI_ScreenState,
|
||||
|
||||
monitor_id : i32,
|
||||
monitor_refresh_hz : i32,
|
||||
|
||||
// using frametime : FrameTime,
|
||||
sleep_is_granular : b32,
|
||||
|
||||
frametime_delta_seconds : f64,
|
||||
frametime_delta_ms : f64,
|
||||
frametime_delta_ns : Duration,
|
||||
frametime_target_ms : f64,
|
||||
frametime_elapsed_ms : f64,
|
||||
frametime_avg_ms : f64,
|
||||
fps_avg : f64,
|
||||
|
||||
// fonts : FontData,
|
||||
font_provider_data : FontProviderData,
|
||||
|
||||
font_firacode : FontID,
|
||||
font_squidgy_slimes : FontID,
|
||||
font_rec_mono_semicasual_reg : FontID,
|
||||
default_font : FontID,
|
||||
|
||||
// There are two potential UI contextes for this prototype so far,
|
||||
// the screen-space UI and the current workspace UI.
|
||||
// This is used so that the ui api doesn't need to have the user pass the context every single time.
|
||||
ui_context : ^UI_State,
|
||||
ui_floating_context : ^UI_FloatingManager,
|
||||
|
||||
// The camera is considered the "context" for coodrinate space operations in rendering
|
||||
cam_context : Camera,
|
||||
}
|
||||
|
||||
get_state :: #force_inline proc "contextless" () -> ^ State {
|
||||
return cast( ^ State ) Memory_App.persistent.reserve_start
|
||||
}
|
||||
|
||||
// get_frametime :: #force_inline proc "contextless" () -> FrameTime {
|
||||
// return get_state().frametime
|
||||
// }
|
||||
|
||||
#endregion("State")
|
114
code/sectr/app/ui_theme_default.odin
Normal file
114
code/sectr/app/ui_theme_default.odin
Normal file
@ -0,0 +1,114 @@
|
||||
package sectr
|
||||
|
||||
/*
|
||||
UI Themes: Comprise of UI_Box's layout & style
|
||||
|
||||
Provides presets for themes and their interface for manipulating the combo stacks in UI_State in pairs
|
||||
*/
|
||||
// TODO(Ed): Eventually this will have a configuration wizard, and we'll save the presets
|
||||
|
||||
@(deferred_none = ui_theme_pop)
|
||||
ui_theme_btn_default :: proc()
|
||||
{
|
||||
@static theme : UI_Theme
|
||||
@static loaded : b32 = false
|
||||
if ! loaded
|
||||
{
|
||||
layout := UI_Layout {
|
||||
flags = {},
|
||||
anchor = range2({},{}),
|
||||
alignment = {0, 0},
|
||||
text_alignment = {0.5, 0.5},
|
||||
font_size = 16,
|
||||
margins = {0, 0, 0, 0},
|
||||
padding = {0, 0, 0, 0},
|
||||
border_width = 1,
|
||||
pos = {0, 0},
|
||||
size = range2({},{})
|
||||
}
|
||||
style := UI_Style {
|
||||
bg_color = Color_ThmDark_Btn_BG_Default,
|
||||
border_color = Color_ThmDark_Border_Default,
|
||||
corner_radii = {},
|
||||
blur_size = 0,
|
||||
font = get_state().default_font,
|
||||
text_color = Color_ThmDark_Text_Default,
|
||||
cursor = {},
|
||||
}
|
||||
layout_combo := to_ui_layout_combo(layout)
|
||||
style_combo := to_ui_style_combo(style)
|
||||
{
|
||||
using layout_combo.hot
|
||||
using style_combo.hot
|
||||
bg_color = Color_ThmDark_Btn_BG_Hot
|
||||
text_color = Color_ThmDark_Text_Hot
|
||||
margins = {2, 2, 2, 2}
|
||||
}
|
||||
{
|
||||
using layout_combo.active
|
||||
using style_combo.active
|
||||
bg_color = Color_ThmDark_Btn_BG_Active
|
||||
text_color = Color_ThmDark_Text_Active
|
||||
margins = {2, 2, 2, 2}
|
||||
}
|
||||
theme = UI_Theme {
|
||||
layout_combo, style_combo
|
||||
}
|
||||
loaded = true
|
||||
}
|
||||
ui_layout_push(theme.layout)
|
||||
ui_style_push(theme.style)
|
||||
}
|
||||
|
||||
@(deferred_none = ui_theme_pop)
|
||||
ui_theme_transparent :: proc()
|
||||
{
|
||||
@static theme : UI_Theme
|
||||
@static loaded : b32 = false
|
||||
if ! loaded || true
|
||||
{
|
||||
layout := UI_Layout {
|
||||
flags = {},
|
||||
anchor = range2({},{}),
|
||||
alignment = {0, 0},
|
||||
text_alignment = {0.0, 0.0},
|
||||
font_size = 16,
|
||||
margins = {0, 0, 0, 0},
|
||||
padding = {0, 0, 0, 0},
|
||||
border_width = 0,
|
||||
pos = {0, 0},
|
||||
size = range2({},{})
|
||||
}
|
||||
style := UI_Style {
|
||||
bg_color = Color_Transparent,
|
||||
border_color = Color_Transparent,
|
||||
corner_radii = {},
|
||||
blur_size = 0,
|
||||
font = get_state().default_font,
|
||||
text_color = Color_ThmDark_Text_Default,
|
||||
cursor = {},
|
||||
}
|
||||
|
||||
layout_combo := to_ui_layout_combo(layout)
|
||||
style_combo := to_ui_style_combo(style)
|
||||
{
|
||||
using layout_combo.disabled
|
||||
using style_combo.disabled
|
||||
}
|
||||
{
|
||||
using layout_combo.hot
|
||||
using style_combo.hot
|
||||
}
|
||||
{
|
||||
using layout_combo.active
|
||||
using style_combo.active
|
||||
}
|
||||
|
||||
theme = UI_Theme {
|
||||
layout_combo, style_combo
|
||||
}
|
||||
loaded = true
|
||||
}
|
||||
ui_layout_push(theme.layout)
|
||||
ui_style_push(theme.style)
|
||||
}
|
35
code/sectr/chrono.odin
Normal file
35
code/sectr/chrono.odin
Normal file
@ -0,0 +1,35 @@
|
||||
package sectr
|
||||
|
||||
Nanosecond_To_Microsecond :: 1.0 / (1000.0)
|
||||
Nanosecond_To_Millisecond :: 1.0 / (1000.0 * 1000.0)
|
||||
Nanosecond_To_Second :: 1.0 / (1000.0 * 1000.0 * 1000.0)
|
||||
|
||||
Microsecond_To_Nanosecond :: 1000.0
|
||||
Microsecond_To_Millisecond :: 1.0 / 1000.0
|
||||
Microsecond_To_Second :: 1.0 / (1000.0 * 1000.0)
|
||||
|
||||
Millisecond_To_Nanosecond :: 1000.0 * 1000.0
|
||||
Millisecond_To_Microsecond :: 1000.0
|
||||
Millisecond_To_Second :: 1.0 / 1000.0
|
||||
|
||||
Second_To_Nanosecond :: 1000.0 * 1000.0 * 1000.0
|
||||
Second_To_Microsecnd :: 1000.0 * 1000.0
|
||||
Second_To_Millisecond :: 1000.0
|
||||
|
||||
NS_To_MS :: Nanosecond_To_Millisecond
|
||||
NS_To_US :: Nanosecond_To_Microsecond
|
||||
NS_To_S :: Nanosecond_To_Second
|
||||
|
||||
US_To_NS :: Microsecond_To_Nanosecond
|
||||
US_To_MS :: Microsecond_To_Millisecond
|
||||
US_To_S :: Microsecond_To_Second
|
||||
|
||||
MS_To_NS :: Millisecond_To_Nanosecond
|
||||
MS_To_US :: Millisecond_To_Microsecond
|
||||
MS_To_S :: Millisecond_To_Second
|
||||
|
||||
S_To_NS :: Second_To_Nanosecond
|
||||
S_To_US :: Second_To_Microsecnd
|
||||
S_To_MS :: Second_To_Millisecond
|
||||
|
||||
Frametime_High_Perf_Threshold_MS :: 1 / 240.0
|
42
code/sectr/collision.odin
Normal file
42
code/sectr/collision.odin
Normal file
@ -0,0 +1,42 @@
|
||||
// Goal is for any Position or 'Shape' intersections used by the prototype to be defined here for centeralization
|
||||
|
||||
package sectr
|
||||
|
||||
import "core:math/linalg"
|
||||
|
||||
// AABB: Separating Axis Theorem
|
||||
intersects_range2 :: #force_inline proc "contextless" ( a, b: Range2 ) -> bool
|
||||
{
|
||||
// Check if there's no overlap on the x-axis
|
||||
if a.max.x < b.min.x || b.max.x < a.min.x {
|
||||
return false; // No overlap on x-axis means no intersection
|
||||
}
|
||||
// Check if there's no overlap on the y-axis
|
||||
if a.max.y < b.min.y || b.max.y < a.min.y {
|
||||
return false; // No overlap on y-axis means no intersection
|
||||
}
|
||||
// If neither of the above conditions are true, there's at least a partial overlap
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO(Ed): Do we need this? Also does it even work (looks unfinished)?
|
||||
is_within_screenspace :: #force_inline proc "contextless" ( pos : Vec2 ) -> b32 {
|
||||
state := get_state(); using state
|
||||
screen_extent := state.app_window.extent
|
||||
cam := & project.workspace.cam
|
||||
within_x_bounds : b32 = pos.x >= -screen_extent.x && pos.x <= screen_extent.x
|
||||
within_y_bounds : b32 = pos.y >= -screen_extent.y && pos.y <= screen_extent.y
|
||||
return within_x_bounds && within_y_bounds
|
||||
}
|
||||
|
||||
within_range2 :: #force_inline proc "contextless" ( a, b : Range2 ) -> bool {
|
||||
within_x := b.min.x >= a.min.x && b.max.x <= a.max.x
|
||||
within_y := b.min.y >= a.min.y && b.max.y <= a.max.y
|
||||
return within_x && within_y
|
||||
}
|
||||
|
||||
pos_within_range2 :: #force_inline proc "contextless" ( pos : Vec2, range : Range2 ) -> b32 {
|
||||
within_x := pos.x > range.min.x && pos.x < range.max.x
|
||||
within_y := pos.y > range.min.y && pos.y < range.max.y
|
||||
return b32(within_x && within_y)
|
||||
}
|
62
code/sectr/colors.odin
Normal file
62
code/sectr/colors.odin
Normal file
@ -0,0 +1,62 @@
|
||||
package sectr
|
||||
|
||||
import rl "vendor:raylib"
|
||||
|
||||
Color :: rl.Color
|
||||
Color_Blue :: rl.BLUE
|
||||
// Color_Green :: rl.GREEN
|
||||
Color_Red :: rl.RED
|
||||
Color_White :: rl.WHITE
|
||||
|
||||
Color_Transparent :: Color { 0, 0, 0, 0 }
|
||||
Color_BG :: Color { 55, 55, 60, 255 }
|
||||
Color_BG_TextBox :: Color { 32, 32, 32, 180 }
|
||||
Color_BG_Panel :: Color { 32, 32, 32, 255 }
|
||||
Color_BG_Panel_Translucent :: Color { 32, 32, 32, 220 }
|
||||
Color_BG_TextBox_Green :: Color { 102, 102, 110, 255 }
|
||||
Color_Frame_Disabled :: Color { 22, 22, 22, 120 }
|
||||
Color_Frame_Hover :: Color { 122, 122, 125, 200 }
|
||||
Color_Frame_Select :: Color { 188, 188, 188, 220 }
|
||||
Color_GreyRed :: Color { 220, 100, 100, 50 }
|
||||
Color_White_A125 :: Color { 255, 255, 255, 165 }
|
||||
Color_Black :: Color { 0, 0, 0, 255 }
|
||||
Color_Green :: Color { 0, 180, 0, 255 }
|
||||
Color_ResizeHandle :: Color { 80, 80, 90, 180 }
|
||||
|
||||
Color_3D_BG :: Color { 188, 182 , 170, 255 }
|
||||
|
||||
Color_Debug_UI_Padding_Bounds :: Color { 40, 195, 170, 160 }
|
||||
Color_Debug_UI_Content_Bounds :: Color { 170, 120, 240, 160 }
|
||||
|
||||
// TODO(Ed): The entire rendering pass should be post-processed by a tone curve configurable for the user
|
||||
// This is how you properly support any tonality of light or dark themes and not have it be base don the monitors raw output.
|
||||
|
||||
// Dark Theme
|
||||
|
||||
// Brightest value limited to (text is the only exception):
|
||||
Color_ThmDark_BrightLimit :: Color {230, 230, 230, 255}
|
||||
// Darkness value limited to (text is the only exception):
|
||||
Color_ThmDark_DarkLimit :: Color {10, 10, 10, 255}
|
||||
|
||||
|
||||
Color_ThmDark_BG :: Color {33, 33, 33, 255}
|
||||
|
||||
Color_ThmDark_Translucent_Panel :: Color { 0, 0, 0, 60}
|
||||
|
||||
Color_ThmDark_ResizeHandle_Default :: Color_Transparent
|
||||
Color_ThmDark_ResizeHandle_Hot :: Color { 72, 72, 72, 90}
|
||||
Color_ThmDark_ResizeHandle_Active :: Color { 88, 88, 88, 90}
|
||||
|
||||
Color_ThmDark_Border_Default :: Color { 64, 64, 64, 255}
|
||||
|
||||
Color_ThmDark_Btn_BG_Default :: Color { 40, 40, 40, 255}
|
||||
Color_ThmDark_Btn_BG_Hot :: Color { 60, 60, 70, 255}
|
||||
Color_ThmDark_Btn_BG_Active :: Color { 90, 100, 130, 255}
|
||||
|
||||
Color_ThmDark_Text_Default :: Color {120, 117, 115, 255}
|
||||
Color_ThmDark_Text_Hot :: Color {180, 180, 180, 255}
|
||||
Color_ThmDark_Text_Active :: Color {240, 240, 240, 255}
|
||||
|
||||
// Light Theme
|
||||
|
||||
// LightTheme_BG :: Color { 120, 120, 120, 255 }
|
400
code/sectr/engine/client_api.odin
Normal file
400
code/sectr/engine/client_api.odin
Normal file
@ -0,0 +1,400 @@
|
||||
package sectr
|
||||
|
||||
import "base:runtime"
|
||||
import c "core:c/libc"
|
||||
import "core:dynlib"
|
||||
import "core:mem"
|
||||
import "core:mem/virtual"
|
||||
import "core:os"
|
||||
import "core:slice"
|
||||
import "core:strings"
|
||||
import "core:time"
|
||||
import "core:prof/spall"
|
||||
import rl "vendor:raylib"
|
||||
|
||||
Path_Assets :: "../assets/"
|
||||
Path_Shaders :: "../shaders/"
|
||||
Path_Input_Replay :: "scratch.sectr_replay"
|
||||
|
||||
Persistent_Slab_DBG_Name := "Peristent Slab"
|
||||
Frame_Slab_DBG_Name := "Frame Slab"
|
||||
Transient_Slab_DBG_Name := "Transient Slab"
|
||||
|
||||
ModuleAPI :: struct {
|
||||
lib : dynlib.Library,
|
||||
write_time : FileTime,
|
||||
lib_version : i32,
|
||||
|
||||
startup : type_of( startup ),
|
||||
shutdown : type_of( sectr_shutdown ),
|
||||
reload : type_of( reload ),
|
||||
tick : type_of( tick ),
|
||||
clean_frame : type_of( clean_frame ),
|
||||
}
|
||||
|
||||
@export
|
||||
startup :: proc( prof : ^SpallProfiler, persistent_mem, frame_mem, transient_mem, files_buffer_mem : ^VArena, host_logger : ^Logger )
|
||||
{
|
||||
spall.SCOPED_EVENT( & prof.ctx, & prof.buffer, #procedure )
|
||||
Memory_App.profiler = prof
|
||||
|
||||
startup_tick := time.tick_now()
|
||||
|
||||
logger_init( & Memory_App.logger, "Sectr", host_logger.file_path, host_logger.file )
|
||||
context.logger = to_odin_logger( & Memory_App.logger )
|
||||
|
||||
// Setup memory for the first time
|
||||
{
|
||||
using Memory_App;
|
||||
persistent = persistent_mem
|
||||
frame = frame_mem
|
||||
transient = transient_mem
|
||||
files_buffer = files_buffer_mem
|
||||
|
||||
context.allocator = persistent_allocator()
|
||||
context.temp_allocator = transient_allocator()
|
||||
// TODO(Ed) : Put on the transient allocator a slab allocator (transient slab)
|
||||
}
|
||||
|
||||
state := new( State, persistent_allocator() )
|
||||
Memory_App.state = state
|
||||
using state
|
||||
|
||||
// Setup Persistent Slab
|
||||
{
|
||||
alignment := uint(mem.DEFAULT_ALIGNMENT)
|
||||
|
||||
policy_ptr := & default_slab_policy
|
||||
push( policy_ptr, SlabSizeClass { 128 * Kilobyte, 1 * Kilobyte, alignment })
|
||||
push( policy_ptr, SlabSizeClass { 256 * Kilobyte, 2 * Kilobyte, alignment })
|
||||
push( policy_ptr, SlabSizeClass { 512 * Kilobyte, 4 * Kilobyte, alignment })
|
||||
push( policy_ptr, SlabSizeClass { 1 * Megabyte, 16 * Kilobyte, alignment })
|
||||
push( policy_ptr, SlabSizeClass { 1 * Megabyte, 32 * Kilobyte, alignment })
|
||||
push( policy_ptr, SlabSizeClass { 1 * Megabyte, 64 * Kilobyte, alignment })
|
||||
push( policy_ptr, SlabSizeClass { 2 * Megabyte, 128 * Kilobyte, alignment })
|
||||
push( policy_ptr, SlabSizeClass { 2 * Megabyte, 256 * Kilobyte, alignment })
|
||||
push( policy_ptr, SlabSizeClass { 2 * Megabyte, 512 * Kilobyte, alignment })
|
||||
push( policy_ptr, SlabSizeClass { 2 * Megabyte, 1 * Megabyte, alignment })
|
||||
push( policy_ptr, SlabSizeClass { 2 * Megabyte, 2 * Megabyte, alignment })
|
||||
push( policy_ptr, SlabSizeClass { 4 * Megabyte, 4 * Megabyte, alignment })
|
||||
push( policy_ptr, SlabSizeClass { 8 * Megabyte, 8 * Megabyte, alignment })
|
||||
push( policy_ptr, SlabSizeClass { 16 * Megabyte, 16 * Megabyte, alignment })
|
||||
push( policy_ptr, SlabSizeClass { 32 * Megabyte, 32 * Megabyte, alignment })
|
||||
push( policy_ptr, SlabSizeClass { 64 * Megabyte, 64 * Megabyte, alignment })
|
||||
// push( policy_ptr, SlabSizeClass { 128 * Megabyte, 128 * Megabyte, alignment })
|
||||
// push( policy_ptr, SlabSizeClass { 256 * Megabyte, 256 * Megabyte, alignment })
|
||||
// push( policy_ptr, SlabSizeClass { 512 * Megabyte, 512 * Megabyte, alignment })
|
||||
|
||||
alloc_error : AllocatorError
|
||||
persistent_slab, alloc_error = slab_init( policy_ptr, allocator = persistent_allocator(), dbg_name = Persistent_Slab_DBG_Name )
|
||||
verify( alloc_error == .None, "Failed to allocate the persistent slab" )
|
||||
|
||||
transient_slab, alloc_error = slab_init( & default_slab_policy, allocator = transient_allocator(), dbg_name = Transient_Slab_DBG_Name )
|
||||
verify( alloc_error == .None, "Failed to allocate transient slab" )
|
||||
|
||||
transient_clear_time = 120 // Seconds, 2 Minutes
|
||||
}
|
||||
|
||||
string_cache = str_cache_init()
|
||||
|
||||
input = & input_data[1]
|
||||
input_prev = & input_data[0]
|
||||
|
||||
// Configuration Load
|
||||
{
|
||||
using config
|
||||
resolution_width = 1000
|
||||
resolution_height = 600
|
||||
refresh_rate = 0
|
||||
|
||||
cam_min_zoom = 0.10
|
||||
cam_max_zoom = 30.0
|
||||
cam_zoom_mode = .Smooth
|
||||
cam_zoom_smooth_snappiness = 4.0
|
||||
cam_zoom_sensitivity_digital = 0.2
|
||||
cam_zoom_sensitivity_smooth = 4.0
|
||||
|
||||
engine_refresh_hz = 30
|
||||
|
||||
timing_fps_moving_avg_alpha = 0.9
|
||||
|
||||
ui_resize_border_width = 5
|
||||
}
|
||||
|
||||
Desired_OS_Scheduler_MS :: 1
|
||||
sleep_is_granular = set__scheduler_granularity( Desired_OS_Scheduler_MS )
|
||||
|
||||
// Rough setup of window with rl stuff
|
||||
{
|
||||
// rl.Odin_SetMalloc( RL_MALLOC )
|
||||
|
||||
rl.SetConfigFlags( {
|
||||
rl.ConfigFlag.WINDOW_RESIZABLE,
|
||||
// rl.ConfigFlag.WINDOW_TOPMOST,
|
||||
})
|
||||
|
||||
window_width : i32 = cast(i32) config.resolution_width
|
||||
window_height : i32 = cast(i32) config.resolution_height
|
||||
win_title : cstring = "Sectr Prototype"
|
||||
rl.InitWindow( window_width, window_height, win_title )
|
||||
log( "Raylib initialized and window opened" )
|
||||
|
||||
window := & state.app_window
|
||||
window.extent.x = f32(window_width) * 0.5
|
||||
window.extent.y = f32(window_height) * 0.5
|
||||
|
||||
// We do not support non-uniform DPI.
|
||||
window.dpi_scale = rl.GetWindowScaleDPI().x
|
||||
window.ppcm = os_default_ppcm * window.dpi_scale
|
||||
|
||||
// Determining current monitor and setting the target frametime based on it..
|
||||
monitor_id = rl.GetCurrentMonitor()
|
||||
monitor_refresh_hz = rl.GetMonitorRefreshRate( monitor_id )
|
||||
log( str_fmt_tmp( "Set target FPS to: %v", monitor_refresh_hz ) )
|
||||
}
|
||||
|
||||
// Basic Font Setup
|
||||
{
|
||||
font_provider_startup()
|
||||
// path_rec_mono_semicasual_reg := strings.concatenate( { Path_Assets, "RecMonoSemicasual-Regular-1.084.ttf" })
|
||||
// font_rec_mono_semicasual_reg = font_load( path_rec_mono_semicasual_reg, 24.0, "RecMonoSemiCasual_Regular" )
|
||||
|
||||
// path_squidgy_slimes := strings.concatenate( { Path_Assets, "Squidgy Slimes.ttf" } )
|
||||
// font_squidgy_slimes = font_load( path_squidgy_slimes, 24.0, "Squidgy_Slime" )
|
||||
|
||||
path_firacode := strings.concatenate( { Path_Assets, "FiraCode-Regular.ttf" }, transient_allocator() )
|
||||
font_firacode = font_load( path_firacode, 24.0, "FiraCode" )
|
||||
default_font = font_firacode
|
||||
log( "Default font loaded" )
|
||||
}
|
||||
|
||||
// Setup the screen ui state
|
||||
{
|
||||
ui_startup( & screen_ui.base, cache_allocator = persistent_slab_allocator() )
|
||||
ui_floating_startup( & screen_ui.floating, persistent_slab_allocator(), 1 * Kilobyte, 1 * Kilobyte, "screen ui floating manager" )
|
||||
|
||||
using screen_ui
|
||||
menu_bar.pos = { -60, 0 }
|
||||
// menu_bar.pos = Vec2(app_window.extent) * { -1, 1 }
|
||||
menu_bar.size = {200, 40}
|
||||
|
||||
settings_menu.min_size = {250, 200}
|
||||
}
|
||||
|
||||
// Demo project setup
|
||||
{
|
||||
using project
|
||||
path = str_intern("./")
|
||||
name = str_intern( "First Project" )
|
||||
workspace.name = str_intern( "First Workspace" )
|
||||
{
|
||||
using project.workspace
|
||||
cam = {
|
||||
target = { 0, 0 },
|
||||
offset = transmute(Vec2) app_window.extent,
|
||||
rotation = 0,
|
||||
zoom = 1.0,
|
||||
}
|
||||
// cam = {
|
||||
// position = { 0, 0, -100 },
|
||||
// target = { 0, 0, 0 },
|
||||
// up = { 0, 1, 0 },
|
||||
// fovy = 90,
|
||||
// projection = rl.CameraProjection.ORTHOGRAPHIC,
|
||||
// }
|
||||
|
||||
// Setup workspace UI state
|
||||
ui_startup( & workspace.ui, cache_allocator = persistent_slab_allocator() )
|
||||
}
|
||||
|
||||
debug.path_lorem = str_fmt_alloc("C:/projects/SectrPrototype/examples/Lorem Ipsum.txt", allocator = persistent_slab_allocator())
|
||||
|
||||
alloc_error : AllocatorError; success : bool
|
||||
debug.lorem_content, success = os.read_entire_file( debug.path_lorem, persistent_slab_allocator() )
|
||||
|
||||
debug.lorem_parse, alloc_error = pws_parser_parse( transmute(string) debug.lorem_content, persistent_slab_allocator() )
|
||||
verify( alloc_error == .None, "Faield to parse due to allocation failure" )
|
||||
|
||||
// Render texture test
|
||||
// debug.viewport_rt = rl.LoadRenderTexture( 1280, 720 )
|
||||
|
||||
// debug.proto_text_shader = rl.LoadShader( "C:/projects/SectrPrototype/code/shaders/text_shader.vs", "C:/projects/SectrPrototype/code/shaders/text_shader.fs" )
|
||||
}
|
||||
|
||||
startup_ms := duration_ms( time.tick_lap_time( & startup_tick))
|
||||
log( str_fmt_tmp("Startup time: %v ms", startup_ms) )
|
||||
|
||||
// Make sure to cleanup transient before continuing...
|
||||
// From here on, tarnsinet usage has to be done with care.
|
||||
// For most cases, the frame allocator should be more than enough.
|
||||
}
|
||||
|
||||
// For some reason odin's symbols conflict with native foreign symbols...
|
||||
@export
|
||||
sectr_shutdown :: proc()
|
||||
{
|
||||
context.logger = to_odin_logger( & Memory_App.logger )
|
||||
|
||||
if Memory_App.persistent == nil {
|
||||
return
|
||||
}
|
||||
state := get_state()
|
||||
|
||||
// Replay
|
||||
{
|
||||
file_close( Memory_App.replay.active_file )
|
||||
}
|
||||
|
||||
font_provider_shutdown()
|
||||
|
||||
log("Module shutdown complete")
|
||||
}
|
||||
|
||||
@export
|
||||
reload :: proc( prof : ^SpallProfiler, persistent_mem, frame_mem, transient_mem, files_buffer_mem : ^VArena, host_logger : ^ Logger )
|
||||
{
|
||||
spall.SCOPED_EVENT( & prof.ctx, & prof.buffer, #procedure )
|
||||
Memory_App.profiler = prof
|
||||
|
||||
context.logger = to_odin_logger( & Memory_App.logger )
|
||||
using Memory_App;
|
||||
|
||||
persistent = persistent_mem
|
||||
frame = frame_mem
|
||||
transient = transient_mem
|
||||
files_buffer = files_buffer_mem
|
||||
|
||||
context.allocator = persistent_allocator()
|
||||
context.temp_allocator = transient_allocator()
|
||||
|
||||
Memory_App.state = get_state()
|
||||
using state
|
||||
|
||||
// Procedure Addresses are not preserved on hot-reload. They must be restored for persistent data.
|
||||
// The only way to alleviate this is to either do custom handles to allocators
|
||||
// Or as done below, correct containers using allocators on reload.
|
||||
// Thankfully persistent dynamic allocations are rare, and thus we know exactly which ones they are.
|
||||
|
||||
slab_reload( persistent_slab, persistent_allocator() )
|
||||
|
||||
hmap_chained_reload( font_provider_data.font_cache, persistent_allocator())
|
||||
|
||||
slab_reload( string_cache.slab, persistent_allocator() )
|
||||
zpl_hmap_reload( & string_cache.table, persistent_slab_allocator())
|
||||
|
||||
slab_reload( frame_slab, frame_allocator())
|
||||
slab_reload( transient_slab, transient_allocator())
|
||||
|
||||
ui_reload( & get_state().project.workspace.ui, cache_allocator = persistent_slab_allocator() )
|
||||
|
||||
log("Module reloaded")
|
||||
}
|
||||
|
||||
@export
|
||||
tick :: proc( host_delta_time : f64, host_delta_ns : Duration ) -> b32
|
||||
{
|
||||
profile( "Client Tick" )
|
||||
context.logger = to_odin_logger( & Memory_App.logger )
|
||||
state := get_state(); using state
|
||||
|
||||
should_close : b32
|
||||
|
||||
client_tick := time.tick_now()
|
||||
{
|
||||
profile("Work frame")
|
||||
|
||||
// Setup Frame Slab
|
||||
{
|
||||
alloc_error : AllocatorError
|
||||
frame_slab, alloc_error = slab_init( & default_slab_policy, bucket_reserve_num = 0,
|
||||
allocator = frame_allocator(),
|
||||
dbg_name = Frame_Slab_DBG_Name,
|
||||
should_zero_buckets = true )
|
||||
verify( alloc_error == .None, "Failed to allocate frame slab" )
|
||||
}
|
||||
|
||||
context.allocator = frame_allocator()
|
||||
context.temp_allocator = transient_allocator()
|
||||
|
||||
rl.PollInputEvents()
|
||||
|
||||
debug.draw_ui_box_bounds_points = true
|
||||
debug.draw_UI_padding_bounds = false
|
||||
debug.draw_ui_content_bounds = true
|
||||
|
||||
should_close = update( host_delta_time )
|
||||
render()
|
||||
|
||||
rl.SwapScreenBuffer()
|
||||
}
|
||||
|
||||
// Timing
|
||||
{
|
||||
// profile("Client tick timing processing")
|
||||
config.engine_refresh_hz = uint(monitor_refresh_hz)
|
||||
// config.engine_refresh_hz = 6
|
||||
frametime_target_ms = 1.0 / f64(config.engine_refresh_hz) * S_To_MS
|
||||
sub_ms_granularity_required := frametime_target_ms <= Frametime_High_Perf_Threshold_MS
|
||||
|
||||
frametime_delta_ns = time.tick_lap_time( & client_tick )
|
||||
frametime_delta_ms = duration_ms( frametime_delta_ns )
|
||||
frametime_delta_seconds = duration_seconds( frametime_delta_ns )
|
||||
frametime_elapsed_ms = frametime_delta_ms + host_delta_time
|
||||
|
||||
if frametime_elapsed_ms < frametime_target_ms
|
||||
{
|
||||
sleep_ms := frametime_target_ms - frametime_elapsed_ms
|
||||
pre_sleep_tick := time.tick_now()
|
||||
|
||||
if sleep_ms > 0 {
|
||||
thread_sleep( cast(Duration) sleep_ms * MS_To_NS )
|
||||
// thread__highres_wait( sleep_ms )
|
||||
}
|
||||
|
||||
sleep_delta_ns := time.tick_lap_time( & pre_sleep_tick)
|
||||
sleep_delta_ms := duration_ms( sleep_delta_ns )
|
||||
|
||||
if sleep_delta_ms < sleep_ms {
|
||||
// log( str_fmt_tmp("frametime sleep was off by: %v ms", sleep_delta_ms - sleep_ms ))
|
||||
}
|
||||
|
||||
frametime_elapsed_ms += sleep_delta_ms
|
||||
for ; frametime_elapsed_ms < frametime_target_ms; {
|
||||
sleep_delta_ns = time.tick_lap_time( & pre_sleep_tick)
|
||||
sleep_delta_ms = duration_ms( sleep_delta_ns )
|
||||
|
||||
frametime_elapsed_ms += sleep_delta_ms
|
||||
}
|
||||
}
|
||||
|
||||
config.timing_fps_moving_avg_alpha = 0.99
|
||||
frametime_avg_ms = mov_avg_exp( f64(config.timing_fps_moving_avg_alpha), frametime_elapsed_ms, frametime_avg_ms )
|
||||
fps_avg = 1 / (frametime_avg_ms * MS_To_S)
|
||||
|
||||
if frametime_elapsed_ms > 60.0 {
|
||||
log( str_fmt_tmp("Big tick! %v ms", frametime_elapsed_ms), LogLevel.Warning )
|
||||
}
|
||||
}
|
||||
return should_close
|
||||
}
|
||||
|
||||
@export
|
||||
clean_frame :: proc()
|
||||
{
|
||||
// profile( #procedure)
|
||||
state := get_state(); using state
|
||||
context.logger = to_odin_logger( & Memory_App.logger )
|
||||
|
||||
free_all( frame_allocator() )
|
||||
|
||||
transient_clear_elapsed += frametime_delta32()
|
||||
if transient_clear_elapsed >= transient_clear_time && ! transinet_clear_lock
|
||||
{
|
||||
transient_clear_elapsed = 0
|
||||
free_all( transient_allocator() )
|
||||
|
||||
alloc_error : AllocatorError
|
||||
transient_slab, alloc_error = slab_init( & default_slab_policy, allocator = transient_allocator(), dbg_name = Transient_Slab_DBG_Name )
|
||||
verify( alloc_error == .None, "Failed to allocate transient slab" )
|
||||
}
|
||||
}
|
127
code/sectr/engine/logger.odin
Normal file
127
code/sectr/engine/logger.odin
Normal file
@ -0,0 +1,127 @@
|
||||
package sectr
|
||||
|
||||
import "base:runtime"
|
||||
import "core:fmt"
|
||||
import "core:mem"
|
||||
import "core:os"
|
||||
import str "core:strings"
|
||||
import "core:time"
|
||||
import core_log "core:log"
|
||||
|
||||
Max_Logger_Message_Width :: 300
|
||||
|
||||
LogLevel :: core_log.Level
|
||||
|
||||
Logger :: struct {
|
||||
file_path : string,
|
||||
file : os.Handle,
|
||||
id : string,
|
||||
}
|
||||
|
||||
to_odin_logger :: proc( logger : ^ Logger ) -> core_log.Logger {
|
||||
return { logger_interface, logger, core_log.Level.Debug, core_log.Default_File_Logger_Opts }
|
||||
}
|
||||
|
||||
logger_init :: proc( logger : ^ Logger, id : string, file_path : string, file := os.INVALID_HANDLE )
|
||||
{
|
||||
if file == os.INVALID_HANDLE
|
||||
{
|
||||
logger_file, result_code := file_open( file_path, os.O_RDWR | os.O_CREATE )
|
||||
if result_code != os.ERROR_NONE {
|
||||
// Log failures are fatal and must never occur at runtime (there is no logging)
|
||||
runtime.debug_trap()
|
||||
os.exit( -1 )
|
||||
// TODO(Ed) : Figure out the error code enums..
|
||||
}
|
||||
logger.file = logger_file
|
||||
}
|
||||
else {
|
||||
logger.file = file
|
||||
}
|
||||
logger.file_path = file_path
|
||||
logger.id = id
|
||||
|
||||
context.logger = { logger_interface, logger, core_log.Level.Debug, core_log.Default_File_Logger_Opts }
|
||||
log("Initialized Logger")
|
||||
when false {
|
||||
log("This sentence is over 80 characters long on purpose to test the ability of this logger to properfly wrap long as logs with a new line and then at the end of that pad it with the appropraite signature.")
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
logger_interface :: proc(
|
||||
logger_data : rawptr,
|
||||
level : core_log.Level,
|
||||
text : string,
|
||||
options : core_log.Options,
|
||||
location := #caller_location )
|
||||
{
|
||||
logger := cast(^ Logger) logger_data
|
||||
|
||||
@static builder_backing : [16 * Kilobyte] byte; {
|
||||
mem.set( raw_data( builder_backing[:] ), 0, len(builder_backing) )
|
||||
}
|
||||
builder := str.builder_from_bytes( builder_backing[:] )
|
||||
|
||||
first_line_length := len(text) > Max_Logger_Message_Width ? Max_Logger_Message_Width : len(text)
|
||||
first_line := transmute(string) text[ 0 : first_line_length ]
|
||||
str_fmt_builder( & builder, "%-*s ", Max_Logger_Message_Width, first_line )
|
||||
|
||||
// Signature
|
||||
{
|
||||
when time.IS_SUPPORTED
|
||||
{
|
||||
if core_log.Full_Timestamp_Opts & options != nil {
|
||||
str_fmt_builder( & builder, "[")
|
||||
|
||||
t := time.now()
|
||||
year, month, day := time.date(t)
|
||||
hour, minute, second := time.clock(t)
|
||||
|
||||
if .Date in options {
|
||||
str_fmt_builder( & builder, "%d-%02d-%02d ", year, month, day )
|
||||
}
|
||||
if .Time in options {
|
||||
str_fmt_builder( & builder, "%02d:%02d:%02d", hour, minute, second)
|
||||
}
|
||||
|
||||
str_fmt_builder( & builder, "] ")
|
||||
}
|
||||
}
|
||||
core_log.do_level_header( options, level, & builder )
|
||||
|
||||
if logger.id != "" {
|
||||
str_fmt_builder( & builder, "[%s] ", logger.id )
|
||||
}
|
||||
core_log.do_location_header( options, & builder, location )
|
||||
}
|
||||
|
||||
// Oversized message handling
|
||||
if len(text) > Max_Logger_Message_Width
|
||||
{
|
||||
offset := Max_Logger_Message_Width
|
||||
bytes := transmute( []u8 ) text
|
||||
for left := len(bytes) - Max_Logger_Message_Width; left > 0; left -= Max_Logger_Message_Width
|
||||
{
|
||||
str_fmt_builder( & builder, "\n" )
|
||||
subset_length := len(text) - offset
|
||||
if subset_length > Max_Logger_Message_Width {
|
||||
subset_length = Max_Logger_Message_Width
|
||||
}
|
||||
subset := slice_ptr( ptr_offset( raw_data(bytes), offset), subset_length )
|
||||
str_fmt_builder( & builder, "%s", transmute(string) subset )
|
||||
offset += Max_Logger_Message_Width
|
||||
}
|
||||
}
|
||||
|
||||
str_to_file_ln( logger.file, to_string(builder) )
|
||||
}
|
||||
|
||||
log :: proc( msg : string, level := LogLevel.Info, loc := #caller_location ) {
|
||||
core_log.log( level, msg, location = loc )
|
||||
}
|
||||
|
||||
logf :: proc( fmt : string, args : ..any, level := LogLevel.Info, loc := #caller_location ) {
|
||||
// context.allocator = transient_allocator()
|
||||
core_log.logf( level, fmt, args, location = loc )
|
||||
}
|
2
code/sectr/engine/render_gl.odin
Normal file
2
code/sectr/engine/render_gl.odin
Normal file
@ -0,0 +1,2 @@
|
||||
package sectr
|
||||
|
406
code/sectr/engine/render_raylib.odin
Normal file
406
code/sectr/engine/render_raylib.odin
Normal file
@ -0,0 +1,406 @@
|
||||
package sectr
|
||||
|
||||
import "core:fmt"
|
||||
|
||||
import rl "vendor:raylib"
|
||||
|
||||
range2_to_rl_rect :: #force_inline proc "contextless"( range : Range2 ) -> rl.Rectangle
|
||||
{
|
||||
rect := rl.Rectangle {
|
||||
range.min.x,
|
||||
range.max.y,
|
||||
abs(range.max.x - range.min.x),
|
||||
abs(range.max.y - range.min.y),
|
||||
}
|
||||
return rect
|
||||
}
|
||||
|
||||
draw_rectangle :: #force_inline proc "contextless" ( rect : rl.Rectangle, box : ^UI_RenderBoxInfo ) {
|
||||
using box
|
||||
if style.corner_radii[0] > 0 {
|
||||
rl.DrawRectangleRounded( rect, style.corner_radii[0], 9, style.bg_color )
|
||||
}
|
||||
else {
|
||||
rl.DrawRectangleRec( rect, style.bg_color )
|
||||
}
|
||||
}
|
||||
|
||||
draw_rectangle_lines :: #force_inline proc "contextless" ( rect : rl.Rectangle, box : ^UI_RenderBoxInfo, color : Color, thickness : f32 ) {
|
||||
using box
|
||||
if style.corner_radii[0] > 0 {
|
||||
rl.DrawRectangleRoundedLines( rect, style.corner_radii[0], 9, thickness, color )
|
||||
}
|
||||
else {
|
||||
rl.DrawRectangleLinesEx( rect, thickness, color )
|
||||
}
|
||||
}
|
||||
|
||||
render :: proc()
|
||||
{
|
||||
profile(#procedure)
|
||||
state := get_state(); using state
|
||||
|
||||
render_mode_3d()
|
||||
|
||||
rl.BeginDrawing()
|
||||
rl.ClearBackground( Color_ThmDark_BG )
|
||||
|
||||
render_mode_2d_workspace()
|
||||
render_mode_screenspace()
|
||||
|
||||
rl.EndDrawing()
|
||||
}
|
||||
|
||||
// Experimental 3d viewport, not really the focus of this prototype
|
||||
// Until we can have a native or interpreted program render to it its not very useful.
|
||||
// Note(Ed): Other usecase could be 3d vis notes & math/graphical debug
|
||||
render_mode_3d :: proc()
|
||||
{
|
||||
profile(#procedure)
|
||||
|
||||
state := get_state(); using state
|
||||
|
||||
rl.BeginDrawing()
|
||||
rl.BeginTextureMode( debug.viewport_rt )
|
||||
rl.BeginMode3D( debug.cam_vp )
|
||||
rl.ClearBackground( Color_3D_BG )
|
||||
|
||||
rl.EndMode3D()
|
||||
rl.EndTextureMode()
|
||||
rl.EndDrawing()
|
||||
}
|
||||
|
||||
// TODO(Ed): Eventually this needs to become a 'viewport within a UI'
|
||||
// This would allow the user to have more than one workspace open at the same time
|
||||
render_mode_2d_workspace :: proc()
|
||||
{
|
||||
profile(#procedure)
|
||||
state := get_state(); using state
|
||||
cam := & project.workspace.cam
|
||||
|
||||
win_extent := state.app_window.extent
|
||||
|
||||
rl.BeginMode2D( project.workspace.cam )
|
||||
|
||||
// Draw 3D Viewport
|
||||
when false
|
||||
{
|
||||
viewport_size := Vec2 { 1280.0, 720.0 }
|
||||
vp_half_size := viewport_size * 0.5
|
||||
viewport_box := range2( -vp_half_size, vp_half_size )
|
||||
viewport_render := range2(
|
||||
ws_view_to_render_pos( viewport_box.min),
|
||||
ws_view_to_render_pos( viewport_box.max),
|
||||
)
|
||||
viewport_rect := range2_to_rl_rect( viewport_render )
|
||||
rl.DrawTextureRec( debug.viewport_rt.texture, viewport_rect, -vp_half_size, Color_White )
|
||||
}
|
||||
|
||||
// draw_text( "This is text in world space", { 0, 200 }, 16.0 )
|
||||
|
||||
cam_zoom_ratio := 1.0 / cam.zoom
|
||||
|
||||
view_bounds := view_get_bounds()
|
||||
when false
|
||||
{
|
||||
render_view := Range2 { pts = {
|
||||
ws_view_to_render_pos( view_bounds.min),
|
||||
ws_view_to_render_pos( view_bounds.max),
|
||||
}}
|
||||
view_rect := rl.Rectangle {
|
||||
render_view.min.x,
|
||||
render_view.max.y,
|
||||
abs(render_view.max.x - render_view.min.x),
|
||||
abs(render_view.max.y - render_view.min.y),
|
||||
}
|
||||
rl.DrawRectangleRounded( view_rect, 0.3, 9, { 255, 0, 0, 20 } )
|
||||
}
|
||||
|
||||
ImguiRender:
|
||||
{
|
||||
profile("Imgui Render")
|
||||
ui := & state.project.workspace.ui
|
||||
root := ui.root
|
||||
if root == nil || root.num_children == 0 {
|
||||
break ImguiRender
|
||||
}
|
||||
state.ui_context = ui
|
||||
|
||||
current := root.first
|
||||
for & current in array_to_slice(ui.render_queue)
|
||||
{
|
||||
profile("Box")
|
||||
style := current.style
|
||||
computed := current.computed
|
||||
|
||||
if ! intersects_range2( view_bounds, computed.bounds ) {
|
||||
continue
|
||||
}
|
||||
|
||||
profile_begin("render space calc")
|
||||
render_bounds := range2(
|
||||
ws_view_to_render_pos(computed.bounds.min),
|
||||
ws_view_to_render_pos(computed.bounds.max),
|
||||
)
|
||||
render_padding := range2(
|
||||
ws_view_to_render_pos(computed.padding.min),
|
||||
ws_view_to_render_pos(computed.padding.max),
|
||||
)
|
||||
render_content := range2(
|
||||
ws_view_to_render_pos(computed.content.min),
|
||||
ws_view_to_render_pos(computed.content.max),
|
||||
)
|
||||
|
||||
rect_bounds := range2_to_rl_rect( render_bounds )
|
||||
rect_padding := range2_to_rl_rect( render_padding )
|
||||
rect_content := range2_to_rl_rect( render_content )
|
||||
profile_end()
|
||||
|
||||
profile_begin("raylib drawing")
|
||||
if style.bg_color.a != 0
|
||||
{
|
||||
draw_rectangle( rect_bounds, & current )
|
||||
}
|
||||
if current.border_width > 0 {
|
||||
draw_rectangle_lines( rect_bounds, & current, style.border_color, current.border_width )
|
||||
}
|
||||
|
||||
line_thickness := 1 * cam_zoom_ratio
|
||||
if debug.draw_UI_padding_bounds && equal_range2(computed.content, computed.padding) {
|
||||
draw_rectangle_lines( rect_padding, & current, Color_Debug_UI_Padding_Bounds, line_thickness )
|
||||
}
|
||||
else if debug.draw_ui_content_bounds {
|
||||
draw_rectangle_lines( rect_content, & current, Color_Debug_UI_Content_Bounds, line_thickness )
|
||||
}
|
||||
|
||||
point_radius := 3 * cam_zoom_ratio
|
||||
|
||||
// profile_begin("circles")
|
||||
if debug.draw_ui_box_bounds_points
|
||||
{
|
||||
computed_size := computed.bounds.p1 - computed.bounds.p0
|
||||
// center := Vec2 {
|
||||
// render_bounds.p0.x + computed_size.x * 0.5,
|
||||
// render_bounds.p0.y - computed_size.y * 0.5,
|
||||
// }
|
||||
// rl.DrawCircleV( center, point_radius, Color_White )
|
||||
|
||||
rl.DrawCircleV( render_bounds.p0, point_radius, Color_Red )
|
||||
rl.DrawCircleV( render_bounds.p1, point_radius, Color_Blue )
|
||||
}
|
||||
// profile_end()
|
||||
|
||||
profile_end()
|
||||
|
||||
if len(current.text.str) > 0 {
|
||||
ws_view_draw_text( current.text, ws_view_to_render_pos(computed.text_pos * {1, -1}), current.font_size, style.text_color )
|
||||
}
|
||||
}
|
||||
}
|
||||
//endregion Imgui Render
|
||||
|
||||
|
||||
if debug.mouse_vis {
|
||||
cursor_world_pos := screen_to_ws_view_pos(input.mouse.pos)
|
||||
rl.DrawCircleV( ws_view_to_render_pos(cursor_world_pos), 5, Color_GreyRed )
|
||||
}
|
||||
|
||||
rl.DrawCircleV( { 0, 0 }, 1 * cam_zoom_ratio, Color_White )
|
||||
|
||||
rl.EndMode2D()
|
||||
}
|
||||
|
||||
render_mode_screenspace :: proc ()
|
||||
{
|
||||
profile("Render Screenspace")
|
||||
|
||||
state := get_state(); using state
|
||||
replay := & Memory_App.replay
|
||||
cam := & project.workspace.cam
|
||||
win_extent := state.app_window.extent
|
||||
|
||||
render_screen_ui()
|
||||
|
||||
fps_msg := str_fmt_tmp( "FPS: %f", fps_avg)
|
||||
fps_msg_width := measure_text_size( fps_msg, default_font, 12.0, 0.0 ).x
|
||||
fps_msg_pos := screen_get_corners().top_right - { fps_msg_width, 0 } - { 5, 5 }
|
||||
debug_draw_text( fps_msg, fps_msg_pos, 12.0, color = rl.GREEN )
|
||||
|
||||
debug_text :: proc( format : string, args : ..any )
|
||||
{
|
||||
@static draw_text_scratch : [Kilobyte * 64]u8
|
||||
|
||||
state := get_state(); using state
|
||||
if debug.draw_debug_text_y > 800 {
|
||||
debug.draw_debug_text_y = 0
|
||||
}
|
||||
|
||||
cam := & project.workspace.cam
|
||||
screen_corners := screen_get_corners()
|
||||
|
||||
position := screen_corners.top_right
|
||||
position.x -= app_window.extent.x
|
||||
position.y -= debug.draw_debug_text_y
|
||||
|
||||
content := str_fmt_buffer( draw_text_scratch[:], format, ..args )
|
||||
debug_draw_text( content, position, 12.0 )
|
||||
|
||||
debug.draw_debug_text_y += 14
|
||||
}
|
||||
|
||||
// Debug Text
|
||||
{
|
||||
// debug_text( "Screen Width : %v", rl.GetScreenWidth () )
|
||||
// debug_text( "Screen Height: %v", rl.GetScreenHeight() )
|
||||
debug_text( "frametime_target_ms : %f ms", frametime_target_ms )
|
||||
debug_text( "frametime : %f ms", frametime_delta_ms )
|
||||
// debug_text( "frametime_last_elapsed_ms : %f ms", frametime_elapsed_ms )
|
||||
if replay.mode == ReplayMode.Record {
|
||||
debug_text( "Recording Input")
|
||||
}
|
||||
if replay.mode == ReplayMode.Playback {
|
||||
debug_text( "Replaying Input")
|
||||
}
|
||||
}
|
||||
|
||||
debug_text("Zoom Target: %v", project.workspace.zoom_target)
|
||||
|
||||
if debug.mouse_vis {
|
||||
debug_text("Mouse Vertical Wheel: %v", input.mouse.vertical_wheel )
|
||||
debug_text("Mouse Delta : %v", input.mouse.delta )
|
||||
debug_text("Mouse Position (Render) : %v", input.mouse.raw_pos )
|
||||
debug_text("Mouse Position (Screen) : %v", input.mouse.pos )
|
||||
debug_text("Mouse Position (Workspace View): %v", screen_to_ws_view_pos(input.mouse.pos) )
|
||||
rl.DrawCircleV( input.mouse.raw_pos, 10, Color_White_A125 )
|
||||
rl.DrawCircleV( screen_to_render_pos(input.mouse.pos), 2, Color_BG )
|
||||
}
|
||||
|
||||
ui := & project.workspace.ui
|
||||
|
||||
debug_text("Box Count (Workspace): %v", ui.built_box_count )
|
||||
|
||||
hot_box := ui_box_from_key( ui.curr_cache, ui.hot )
|
||||
active_box := ui_box_from_key( ui.curr_cache, ui.active )
|
||||
if hot_box != nil {
|
||||
debug_text("Worksapce Hot Box : %v", hot_box.label.str )
|
||||
debug_text("Workspace Hot Range2: %v", hot_box.computed.bounds.pts)
|
||||
}
|
||||
if active_box != nil{
|
||||
debug_text("Workspace Active Box: %v", active_box.label.str )
|
||||
}
|
||||
|
||||
ui = & screen_ui
|
||||
|
||||
debug_text("Box Count: %v", ui.built_box_count )
|
||||
|
||||
hot_box = ui_box_from_key( ui.curr_cache, ui.hot )
|
||||
active_box = ui_box_from_key( ui.curr_cache, ui.active )
|
||||
if hot_box != nil {
|
||||
debug_text("Hot Box : %v", hot_box.label.str )
|
||||
debug_text("Hot Range2: %v", hot_box.computed.bounds.pts)
|
||||
}
|
||||
if active_box != nil{
|
||||
debug_text("Active Box: %v", active_box.label.str )
|
||||
}
|
||||
|
||||
view := view_get_bounds()
|
||||
|
||||
debug.draw_debug_text_y = 14
|
||||
|
||||
// Define the triangle vertices and colors
|
||||
vertices := []f32{
|
||||
// Positions // Colors (RGBA)
|
||||
-0.5, -0.5, 0.0, 1.0, 0.0, 0.0, 1.0, // Vertex 1: Red
|
||||
0.5, -0.5, 0.0, 0.0, 1.0, 0.0, 1.0, // Vertex 2: Green
|
||||
0.0, 0.5, 0.0, 0.0, 0.0, 1.0, 1.0 // Vertex 3: Blue
|
||||
}
|
||||
}
|
||||
|
||||
// A non-zoomable static-view for ui
|
||||
// Only a scalar factor may be applied to the size of widgets & fonts
|
||||
// 'Window tiled' panels reside here
|
||||
render_screen_ui :: proc()
|
||||
{
|
||||
profile(#procedure)
|
||||
|
||||
using state := get_state()
|
||||
|
||||
//region App UI
|
||||
Render_App_UI:
|
||||
{
|
||||
profile("App UI")
|
||||
ui := & state.screen_ui
|
||||
state.ui_context = ui
|
||||
root := ui.root
|
||||
if root.num_children == 0 {
|
||||
break Render_App_UI
|
||||
}
|
||||
|
||||
for & current in array_to_slice(ui.render_queue)
|
||||
{
|
||||
profile("Box")
|
||||
|
||||
style := current.style
|
||||
computed := & current.computed
|
||||
|
||||
profile_begin("Coordinate space conversion")
|
||||
render_bounds := range2(
|
||||
screen_to_render_pos(computed.bounds.min),
|
||||
screen_to_render_pos(computed.bounds.max),
|
||||
)
|
||||
render_padding := range2(
|
||||
screen_to_render_pos(computed.padding.min),
|
||||
screen_to_render_pos(computed.padding.max),
|
||||
)
|
||||
render_content := range2(
|
||||
screen_to_render_pos(computed.content.min),
|
||||
screen_to_render_pos(computed.content.max),
|
||||
)
|
||||
rect_bounds := range2_to_rl_rect( render_bounds )
|
||||
rect_padding := range2_to_rl_rect( render_padding )
|
||||
rect_content := range2_to_rl_rect( render_content )
|
||||
profile_end()
|
||||
|
||||
profile_begin("raylib drawing")
|
||||
if style.bg_color.a != 0
|
||||
{
|
||||
draw_rectangle( rect_bounds, & current )
|
||||
}
|
||||
if current.border_width > 0 {
|
||||
draw_rectangle_lines( rect_bounds, & current, style.border_color, current.border_width )
|
||||
}
|
||||
|
||||
line_thickness : f32 = 1
|
||||
|
||||
if debug.draw_UI_padding_bounds && equal_range2(computed.content, computed.padding) {
|
||||
draw_rectangle_lines( rect_padding, & current, Color_Debug_UI_Padding_Bounds, line_thickness )
|
||||
}
|
||||
else if debug.draw_ui_content_bounds {
|
||||
draw_rectangle_lines( rect_content, & current, Color_Debug_UI_Content_Bounds, line_thickness )
|
||||
}
|
||||
|
||||
point_radius : f32 = 3
|
||||
|
||||
if debug.draw_ui_box_bounds_points
|
||||
{
|
||||
computed_size := computed.bounds.p1 - computed.bounds.p0
|
||||
if false
|
||||
{
|
||||
center := Vec2 {
|
||||
render_bounds.p0.x + computed_size.x * 0.5,
|
||||
render_bounds.p0.y - computed_size.y * 0.5,
|
||||
}
|
||||
rl.DrawCircleV( center, point_radius, Color_White )
|
||||
}
|
||||
rl.DrawCircleV( render_bounds.p0, point_radius, Color_Red )
|
||||
rl.DrawCircleV( render_bounds.p1, point_radius, Color_Blue )
|
||||
}
|
||||
|
||||
if len(current.text.str) > 0 && style.font.key != 0 {
|
||||
draw_text_screenspace( current.text, screen_to_render_pos(computed.text_pos), current.font_size, style.text_color )
|
||||
}
|
||||
profile_end()
|
||||
}
|
||||
}
|
||||
//endregion App UI
|
||||
}
|
224
code/sectr/engine/render_text_raylib.odin
Normal file
224
code/sectr/engine/render_text_raylib.odin
Normal file
@ -0,0 +1,224 @@
|
||||
package sectr
|
||||
|
||||
import "core:math"
|
||||
import "core:strings"
|
||||
import "core:unicode/utf8"
|
||||
import rl "vendor:raylib"
|
||||
|
||||
debug_draw_text :: proc( content : string, pos : Vec2, size : f32, color : rl.Color = rl.WHITE, font : FontID = Font_Default )
|
||||
{
|
||||
// profile(#procedure)
|
||||
state := get_state(); using state
|
||||
|
||||
if len( content ) == 0 {
|
||||
return
|
||||
}
|
||||
runes, alloc_error := to_runes( content, frame_allocator() )
|
||||
// runes, alloc_error := to_runes( content, context.temp_allocator )
|
||||
// verify( alloc_error == AllocatorError.None, "Failed to temp allocate runes" )
|
||||
|
||||
font := font
|
||||
if font.key == Font_Default.key {
|
||||
// if ( len(font) == 0 ) {
|
||||
font = default_font
|
||||
}
|
||||
pos := screen_to_render_pos(pos)
|
||||
|
||||
px_size := size
|
||||
|
||||
rl_font := to_rl_Font(font, px_size )
|
||||
rl.SetTextureFilter(rl_font.texture, rl.TextureFilter.POINT)
|
||||
rl.DrawTextCodepoints( rl_font,
|
||||
raw_data(runes), cast(i32) len(runes),
|
||||
position = transmute(rl.Vector2) pos,
|
||||
fontSize = px_size,
|
||||
spacing = 0.0,
|
||||
tint = color );
|
||||
rl.SetTextureFilter(rl_font.texture, rl.TextureFilter.POINT)
|
||||
}
|
||||
|
||||
draw_text_screenspace :: proc( content : StrRunesPair, pos : Vec2, size : f32, color : rl.Color = rl.WHITE, font : FontID = Font_Default )
|
||||
{
|
||||
// profile(#procedure)
|
||||
state := get_state(); using state
|
||||
|
||||
if len( content.str ) == 0 {
|
||||
return
|
||||
}
|
||||
font := font
|
||||
if font.key == Font_Default.key {
|
||||
font = default_font
|
||||
}
|
||||
pos := pos
|
||||
|
||||
rl_font := to_rl_Font(font, size )
|
||||
runes := content.runes
|
||||
|
||||
rl.SetTextureFilter(rl_font.texture, rl.TextureFilter.POINT)
|
||||
rl.DrawTextCodepoints( rl_font,
|
||||
raw_data(runes), cast(i32) len(runes),
|
||||
position = transmute(rl.Vector2) pos,
|
||||
fontSize = size,
|
||||
spacing = 0.0,
|
||||
tint = color );
|
||||
rl.SetTextureFilter(rl_font.texture, rl.TextureFilter.POINT)
|
||||
}
|
||||
|
||||
ws_view_draw_text_string :: proc( content : string, pos : Vec2, size : f32, color : rl.Color = rl.WHITE, font : FontID = Font_Default )
|
||||
{
|
||||
// profile(#procedure)
|
||||
state := get_state(); using state
|
||||
|
||||
if len( content ) == 0 {
|
||||
return
|
||||
}
|
||||
runes, alloc_error := to_runes( content, frame_allocator() )
|
||||
verify( alloc_error == AllocatorError.None, "Failed to temp allocate runes" )
|
||||
|
||||
font := font
|
||||
if font.key == Font_Default.key {
|
||||
// if len(font) == 0 {
|
||||
font = default_font
|
||||
}
|
||||
pos := ws_view_to_render_pos(pos)
|
||||
|
||||
px_size := size
|
||||
zoom_adjust := px_size * project.workspace.cam.zoom
|
||||
|
||||
rl_font := to_rl_Font(font, zoom_adjust )
|
||||
rl.SetTextureFilter(rl_font.texture, rl.TextureFilter.POINT)
|
||||
rl.DrawTextCodepoints( rl_font,
|
||||
raw_data(runes), cast(i32) len(runes),
|
||||
position = transmute(rl.Vector2) pos,
|
||||
fontSize = px_size,
|
||||
spacing = 0.0,
|
||||
tint = color );
|
||||
rl.SetTextureFilter(rl_font.texture, rl.TextureFilter.POINT)
|
||||
}
|
||||
|
||||
when true
|
||||
{
|
||||
ws_view_draw_text_StrRunesPair :: proc( content : StrRunesPair, pos : Vec2, size : f32, color : rl.Color = rl.WHITE, font : FontID = Font_Default )
|
||||
{
|
||||
profile(#procedure)
|
||||
state := get_state(); using state
|
||||
|
||||
if len( content.str ) == 0 {
|
||||
return
|
||||
}
|
||||
font := font
|
||||
if font.key == Font_Default.key {
|
||||
font = default_font
|
||||
}
|
||||
pos := ws_view_to_render_pos(pos)
|
||||
|
||||
px_size := size
|
||||
zoom_adjust := px_size * project.workspace.cam.zoom
|
||||
rl_font := to_rl_Font(font, zoom_adjust )
|
||||
runes := content.runes
|
||||
|
||||
profile_begin("raylib draw codepoints related")
|
||||
// rl.DrawTextCodepoints( rl_font,
|
||||
// raw_data(runes), cast(i32) len(runes),
|
||||
// position = transmute(rl.Vector2) pos,
|
||||
// fontSize = px_size,
|
||||
// spacing = 0.0,
|
||||
// tint = color );
|
||||
rl.DrawTextEx(rl_font,
|
||||
strings.clone_to_cstring(content.str),
|
||||
position = transmute(rl.Vector2) pos,
|
||||
fontSize = px_size,
|
||||
spacing = 0.0,
|
||||
tint = color
|
||||
)
|
||||
profile_end()
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ws_view_draw_text_StrRunesPair :: proc( content : StrRunesPair, pos : Vec2, size : f32, color : rl.Color = rl.WHITE, font : FontID = Font_Default )
|
||||
{
|
||||
profile(#procedure)
|
||||
state := get_state(); using state
|
||||
|
||||
// We need an alternative way to draw text to the screen (the above is way to expensive)
|
||||
// Possibly need to watch handmade hero...
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Raylib's equivalent doesn't take a length for the string (making it a pain in the ass)
|
||||
// So this is a 1:1 copy except it takes Odin strings
|
||||
measure_text_size_raylib :: proc( text : string, font : FontID, font_size := Font_Use_Default_Size, spacing : f32 ) -> Vec2
|
||||
{
|
||||
// profile(#procedure)
|
||||
px_size := math.round( points_to_pixels( font_size ) )
|
||||
rl_font := to_rl_Font( font, font_size )
|
||||
|
||||
// This is a static var within raylib. We don't have getter access to it.
|
||||
// Note(Ed) : raylib font size is in pixels so this is also.
|
||||
@static text_line_spacing : f32 = 15
|
||||
|
||||
text_size : Vec2
|
||||
|
||||
if rl_font.texture.id == 0 || len(text) == 0 {
|
||||
return text_size
|
||||
}
|
||||
|
||||
temp_byte_counter : i32 = 0 // Used to count longer text line num chars
|
||||
byte_counter : i32 = 0
|
||||
|
||||
text_width : f32 = 0.0
|
||||
temp_text_width : f32 = 0.0 // Used to counter longer text line width
|
||||
|
||||
text_height := cast(f32) rl_font.baseSize
|
||||
scale_factor := px_size / text_height
|
||||
|
||||
letter : rune
|
||||
index : i32 = 0
|
||||
|
||||
for id : i32 = 0; id < i32(len(text));
|
||||
{
|
||||
byte_counter += 1
|
||||
|
||||
next : i32 = 0
|
||||
|
||||
ctext := cast(cstring) ( & raw_data( text )[id] )
|
||||
letter = rl.GetCodepointNext( ctext, & next )
|
||||
index = rl.GetGlyphIndex( rl_font, letter )
|
||||
|
||||
id += 1
|
||||
|
||||
if letter != rune('\n')
|
||||
{
|
||||
if rl_font.glyphs[index].advanceX != 0 {
|
||||
text_width += f32(rl_font.glyphs[index].advanceX)
|
||||
}
|
||||
else {
|
||||
text_width += rl_font.recs[index].width + f32(rl_font.glyphs[index].offsetX)
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if temp_text_width < text_width {
|
||||
temp_text_width = text_width
|
||||
}
|
||||
byte_counter = 0
|
||||
text_width = 0
|
||||
|
||||
text_height += text_line_spacing
|
||||
|
||||
if temp_byte_counter < byte_counter {
|
||||
temp_byte_counter = byte_counter
|
||||
}
|
||||
}
|
||||
}
|
||||
if temp_text_width < text_width {
|
||||
temp_text_width = text_width
|
||||
}
|
||||
text_size.x = temp_text_width * scale_factor + f32(temp_byte_counter - 1) * spacing
|
||||
text_size.y = text_height * scale_factor
|
||||
|
||||
return text_size
|
||||
}
|
62
code/sectr/engine/replay.odin
Normal file
62
code/sectr/engine/replay.odin
Normal file
@ -0,0 +1,62 @@
|
||||
package sectr
|
||||
|
||||
import "core:os"
|
||||
|
||||
ReplayMode :: enum {
|
||||
Off,
|
||||
Record,
|
||||
Playback,
|
||||
}
|
||||
|
||||
ReplayState :: struct {
|
||||
loop_active : b32,
|
||||
mode : ReplayMode,
|
||||
active_file : os.Handle
|
||||
}
|
||||
|
||||
replay_recording_begin :: proc( path : string )
|
||||
{
|
||||
if file_exists( path ) {
|
||||
result := file_remove( path )
|
||||
verify( result != os.ERROR_NONE, "Failed to delete replay file before beginning a new one" )
|
||||
}
|
||||
|
||||
replay_file, open_error := file_open( path, FileFlag_ReadWrite | FileFlag_Create )
|
||||
verify( open_error != os.ERROR_NONE, "Failed to create or open the replay file" )
|
||||
|
||||
file_seek( replay_file, 0, 0 )
|
||||
|
||||
replay := & Memory_App.replay
|
||||
replay.active_file = replay_file
|
||||
replay.mode = ReplayMode.Record
|
||||
}
|
||||
|
||||
replay_recording_end :: proc() {
|
||||
replay := & Memory_App.replay
|
||||
replay.mode = ReplayMode.Off
|
||||
|
||||
file_seek( replay.active_file, 0, 0 )
|
||||
file_close( replay.active_file )
|
||||
}
|
||||
|
||||
replay_playback_begin :: proc( path : string )
|
||||
{
|
||||
verify( ! file_exists( path ), "Failed to find replay file" )
|
||||
|
||||
replay_file, open_error := file_open( path, FileFlag_ReadWrite | FileFlag_Create )
|
||||
verify( open_error != os.ERROR_NONE, "Failed to create or open the replay file" )
|
||||
|
||||
file_seek( replay_file, 0, 0 )
|
||||
|
||||
replay := & Memory_App.replay
|
||||
replay.active_file = replay_file
|
||||
replay.mode = ReplayMode.Playback
|
||||
}
|
||||
|
||||
replay_playback_end :: proc() {
|
||||
input := get_state().input
|
||||
replay := & Memory_App.replay
|
||||
replay.mode = ReplayMode.Off
|
||||
file_seek( replay.active_file, 0, 0 )
|
||||
file_close( replay.active_file )
|
||||
}
|
5
code/sectr/engine/startup.odin
Normal file
5
code/sectr/engine/startup.odin
Normal file
@ -0,0 +1,5 @@
|
||||
package sectr
|
||||
|
||||
|
||||
|
||||
|
251
code/sectr/engine/update.odin
Normal file
251
code/sectr/engine/update.odin
Normal file
@ -0,0 +1,251 @@
|
||||
package sectr
|
||||
|
||||
import "base:runtime"
|
||||
import "core:math"
|
||||
import "core:math/linalg"
|
||||
import "core:os"
|
||||
import str "core:strings"
|
||||
|
||||
import rl "vendor:raylib"
|
||||
|
||||
DebugActions :: struct {
|
||||
load_project : b32,
|
||||
save_project : b32,
|
||||
pause_renderer : b32,
|
||||
|
||||
load_auto_snapshot : b32,
|
||||
record_replay : b32,
|
||||
play_replay : b32,
|
||||
|
||||
show_mouse_pos : b32,
|
||||
|
||||
mouse_select : b32,
|
||||
|
||||
cam_move_up : b32,
|
||||
cam_move_left : b32,
|
||||
cam_move_down : b32,
|
||||
cam_move_right : b32,
|
||||
cam_mouse_pan : b32,
|
||||
}
|
||||
|
||||
poll_debug_actions :: proc( actions : ^ DebugActions, input : ^ InputState )
|
||||
{
|
||||
// profile(#procedure)
|
||||
using actions
|
||||
using input
|
||||
|
||||
modifier_active := keyboard.right_alt.ended_down ||
|
||||
keyboard.right_control.ended_down ||
|
||||
keyboard.right_shift.ended_down ||
|
||||
keyboard.left_alt.ended_down ||
|
||||
keyboard.left_control.ended_down ||
|
||||
keyboard.left_shift.ended_down
|
||||
|
||||
load_project = keyboard.left_control.ended_down && pressed( keyboard.O )
|
||||
save_project = keyboard.left_control.ended_down && pressed( keyboard.S )
|
||||
|
||||
base_replay_bind := keyboard.right_alt.ended_down && pressed( keyboard.L)
|
||||
record_replay = base_replay_bind && keyboard.right_shift.ended_down
|
||||
play_replay = base_replay_bind && ! keyboard.right_shift.ended_down
|
||||
|
||||
show_mouse_pos = keyboard.right_alt.ended_down && pressed(keyboard.M)
|
||||
|
||||
mouse_select = pressed(mouse.left)
|
||||
|
||||
cam_move_up = keyboard.W.ended_down && ( ! modifier_active || keyboard.left_shift.ended_down )
|
||||
cam_move_left = keyboard.A.ended_down && ( ! modifier_active || keyboard.left_shift.ended_down )
|
||||
cam_move_down = keyboard.S.ended_down && ( ! modifier_active || keyboard.left_shift.ended_down )
|
||||
cam_move_right = keyboard.D.ended_down && ( ! modifier_active || keyboard.left_shift.ended_down )
|
||||
|
||||
cam_mouse_pan = mouse.right.ended_down && ! pressed(mouse.right)
|
||||
}
|
||||
|
||||
frametime_delta32 :: #force_inline proc "contextless" () -> f32 {
|
||||
return cast(f32) get_state().frametime_delta_seconds
|
||||
}
|
||||
|
||||
update :: proc( delta_time : f64 ) -> b32
|
||||
{
|
||||
profile(#procedure)
|
||||
state := get_state(); using state
|
||||
replay := & Memory_App.replay
|
||||
workspace := & project.workspace
|
||||
cam := & workspace.cam
|
||||
|
||||
if rl.IsWindowResized() {
|
||||
window := & state.app_window
|
||||
window.extent.x = f32(rl.GetScreenWidth()) * 0.5
|
||||
window.extent.y = f32(rl.GetScreenHeight()) * 0.5
|
||||
|
||||
project.workspace.cam.offset = transmute(Vec2) window.extent
|
||||
}
|
||||
|
||||
state.input, state.input_prev = swap( state.input, state.input_prev )
|
||||
poll_input( state.input_prev, state.input )
|
||||
|
||||
debug_actions : DebugActions = {}
|
||||
poll_debug_actions( & debug_actions, state.input )
|
||||
|
||||
// Saving & Loading
|
||||
{
|
||||
if debug_actions.save_project {
|
||||
project_save( & project )
|
||||
}
|
||||
if debug_actions.load_project {
|
||||
project_load( str_tmp_from_any( project.path, project.name, ".sectr_proj", sep = "" ), & project )
|
||||
}
|
||||
}
|
||||
|
||||
//region Input Replay
|
||||
// TODO(Ed) : Implment host memory mapping api
|
||||
when false
|
||||
{
|
||||
if debug_actions.record_replay { #partial switch replay.mode
|
||||
{
|
||||
case ReplayMode.Off : {
|
||||
save_snapshot( & Memory_App.snapshot )
|
||||
replay_recording_begin( Path_Input_Replay )
|
||||
}
|
||||
case ReplayMode.Record : {
|
||||
replay_recording_end()
|
||||
}
|
||||
}}
|
||||
|
||||
if debug_actions.play_replay { switch replay.mode
|
||||
{
|
||||
case ReplayMode.Off : {
|
||||
if ! file_exists( Path_Input_Replay ) {
|
||||
save_snapshot( & Memory_App.snapshot )
|
||||
replay_recording_begin( Path_Input_Replay )
|
||||
}
|
||||
else {
|
||||
load_snapshot( & Memory_App.snapshot )
|
||||
replay_playback_begin( Path_Input_Replay )
|
||||
}
|
||||
}
|
||||
case ReplayMode.Playback : {
|
||||
replay_playback_end()
|
||||
load_snapshot( & Memory_App.snapshot )
|
||||
}
|
||||
case ReplayMode.Record : {
|
||||
replay_recording_end()
|
||||
load_snapshot( & Memory_App.snapshot )
|
||||
replay_playback_begin( Path_Input_Replay )
|
||||
}
|
||||
}}
|
||||
|
||||
if replay.mode == ReplayMode.Record {
|
||||
record_input( replay.active_file, input )
|
||||
}
|
||||
else if replay.mode == ReplayMode.Playback {
|
||||
play_input( replay.active_file, input )
|
||||
}
|
||||
}
|
||||
//endregion Input Replay
|
||||
|
||||
if debug_actions.show_mouse_pos {
|
||||
debug.mouse_vis = !debug.mouse_vis
|
||||
}
|
||||
|
||||
//region 2D Camera Manual Nav
|
||||
// TODO(Ed): This should be per workspace view
|
||||
{
|
||||
// profile("Camera Manual Nav")
|
||||
digital_move_speed : f32 = 1000.0
|
||||
|
||||
if workspace.zoom_target == 0.0 {
|
||||
workspace.zoom_target = cam.zoom
|
||||
}
|
||||
|
||||
config.cam_max_zoom = 30
|
||||
config.cam_zoom_sensitivity_digital = 0.04
|
||||
config.cam_min_zoom = 0.04
|
||||
config.cam_zoom_mode = .Digital
|
||||
switch config.cam_zoom_mode
|
||||
{
|
||||
case .Smooth:
|
||||
zoom_delta := input.mouse.vertical_wheel * config.cam_zoom_sensitivity_smooth
|
||||
workspace.zoom_target *= 1 + zoom_delta * f32(delta_time)
|
||||
workspace.zoom_target = clamp(workspace.zoom_target, config.cam_min_zoom, config.cam_max_zoom)
|
||||
|
||||
// Linearly interpolate cam.zoom towards zoom_target
|
||||
lerp_factor := config.cam_zoom_smooth_snappiness // Adjust this value to control the interpolation speed
|
||||
cam.zoom += (workspace.zoom_target - cam.zoom) * lerp_factor * f32(delta_time)
|
||||
cam.zoom = clamp(cam.zoom, config.cam_min_zoom, config.cam_max_zoom) // Ensure cam.zoom stays within bounds
|
||||
case .Digital:
|
||||
zoom_delta := input.mouse.vertical_wheel * config.cam_zoom_sensitivity_digital
|
||||
workspace.zoom_target = clamp(workspace.zoom_target + zoom_delta, config.cam_min_zoom, config.cam_max_zoom)
|
||||
cam.zoom = workspace.zoom_target
|
||||
}
|
||||
|
||||
move_velocity : Vec2 = {
|
||||
- cast(f32) i32(debug_actions.cam_move_left) + cast(f32) i32(debug_actions.cam_move_right),
|
||||
- cast(f32) i32(debug_actions.cam_move_up) + cast(f32) i32(debug_actions.cam_move_down),
|
||||
}
|
||||
move_velocity *= digital_move_speed * f32(delta_time)
|
||||
cam.target += move_velocity
|
||||
|
||||
if debug_actions.cam_mouse_pan
|
||||
{
|
||||
if is_within_screenspace(input.mouse.pos) {
|
||||
pan_velocity := input.mouse.delta * vec2(1, -1) * ( 1 / cam.zoom )
|
||||
cam.target -= pan_velocity
|
||||
}
|
||||
}
|
||||
}
|
||||
//endregion 2D Camera Manual Nav
|
||||
|
||||
// TODO(Ed): We need input buffer so that we can consume input actions based on the UI with priority
|
||||
|
||||
ui_screen_tick()
|
||||
|
||||
//region WorkspaceImgui Tick
|
||||
{
|
||||
profile("Workspace Imgui")
|
||||
|
||||
// Creates the root box node, set its as the first parent.
|
||||
ui_graph_build( & state.project.workspace.ui )
|
||||
ui := ui_context
|
||||
|
||||
frame_style_flags : UI_LayoutFlags = {
|
||||
.Fixed_Position_X, .Fixed_Position_Y,
|
||||
.Fixed_Width, .Fixed_Height,
|
||||
.Origin_At_Anchor_Center,
|
||||
}
|
||||
default_layout := UI_Layout {
|
||||
flags = frame_style_flags,
|
||||
anchor = {},
|
||||
// alignment = { 0.5, 0.5 },
|
||||
font_size = 30,
|
||||
text_alignment = { 0.0, 0.0 },
|
||||
// corner_radii = { 0.2, 0.2, 0.2, 0.2 },
|
||||
pos = { 0, 0 },
|
||||
// size = range2( { 1000, 1000 }, {}),
|
||||
// padding = { 20, 20, 20, 20 }
|
||||
}
|
||||
ui_layout( default_layout )
|
||||
frame_style_default := UI_Style {
|
||||
bg_color = Color_BG_TextBox,
|
||||
font = default_font,
|
||||
text_color = Color_White,
|
||||
}
|
||||
frame_theme := to_ui_style_combo(frame_style_default)
|
||||
frame_theme.disabled.bg_color = Color_Frame_Disabled
|
||||
frame_theme.hot. bg_color = Color_Frame_Hover
|
||||
frame_theme.active. bg_color = Color_Frame_Select
|
||||
ui_style( frame_theme )
|
||||
|
||||
config.ui_resize_border_width = 2.5
|
||||
// test_hover_n_click()
|
||||
// test_draggable()
|
||||
// test_text_box()
|
||||
test_parenting( & default_layout, & frame_style_default )
|
||||
// test_whitespace_ast( & default_layout, & frame_style_default )
|
||||
}
|
||||
//endregion Workspace Imgui Tick
|
||||
|
||||
debug.last_mouse_pos = input.mouse.pos
|
||||
|
||||
should_shutdown : b32 = ! cast(b32) rl.WindowShouldClose()
|
||||
return should_shutdown
|
||||
}
|
193
code/sectr/font/provider.odin
Normal file
193
code/sectr/font/provider.odin
Normal file
@ -0,0 +1,193 @@
|
||||
package sectr
|
||||
|
||||
import "core:fmt"
|
||||
import "core:math"
|
||||
import "core:mem"
|
||||
import "core:path/filepath"
|
||||
import "core:os"
|
||||
|
||||
import rl "vendor:raylib"
|
||||
|
||||
Font_Largest_Px_Size :: 32
|
||||
|
||||
Font_Size_Interval :: 2
|
||||
|
||||
// Font_Default :: ""
|
||||
Font_Default :: FontID { 0, "" }
|
||||
Font_Default_Point_Size :: 18.0
|
||||
|
||||
Font_TTF_Default_Chars_Padding :: 4
|
||||
|
||||
Font_Load_Use_Default_Size :: -1
|
||||
Font_Load_Gen_ID :: ""
|
||||
|
||||
Font_Atlas_Packing_Method :: enum u32 {
|
||||
Raylib_Basic = 0, // Basic packing algo
|
||||
Skyeline_Rect = 1, // stb_pack_rect
|
||||
}
|
||||
|
||||
FontID :: struct {
|
||||
key : u64,
|
||||
label : string,
|
||||
}
|
||||
FontTag :: struct {
|
||||
key : FontID,
|
||||
point_size : f32
|
||||
}
|
||||
|
||||
FontGlyphsRender :: struct {
|
||||
size : i32,
|
||||
count : i32,
|
||||
padding : i32,
|
||||
texture : rl.Texture2D,
|
||||
recs : [^]rl.Rectangle, // Characters rectangles in texture
|
||||
glyphs : [^]rl.GlyphInfo, // Characters info data
|
||||
}
|
||||
|
||||
FontDef :: struct {
|
||||
path_file : string,
|
||||
|
||||
// TODO(Ed) : you may have to store font data in the future if we render on demand
|
||||
// data : []u8,
|
||||
|
||||
default_size : i32,
|
||||
size_table : [Font_Largest_Px_Size / Font_Size_Interval] FontGlyphsRender,
|
||||
}
|
||||
|
||||
FontProviderData :: struct {
|
||||
// font_cache : HMapZPL(FontDef),
|
||||
font_cache : HMapChainedPtr(FontDef),
|
||||
}
|
||||
|
||||
font_provider_startup :: proc()
|
||||
{
|
||||
profile(#procedure)
|
||||
state := get_state()
|
||||
font_provider_data := & get_state().font_provider_data; using font_provider_data
|
||||
|
||||
font_cache_alloc_error : AllocatorError
|
||||
font_cache, font_cache_alloc_error = hmap_chained_init(FontDef, hmap_closest_prime(1 * Kilo), persistent_allocator(), dbg_name = "font_cache" )
|
||||
verify( font_cache_alloc_error == AllocatorError.None, "Failed to allocate font_cache" )
|
||||
|
||||
log("font_cache created")
|
||||
log("font_provider initialized")
|
||||
}
|
||||
|
||||
font_provider_shutdown :: proc()
|
||||
{
|
||||
font_provider_data := & get_state().font_provider_data; using font_provider_data
|
||||
|
||||
for & entry in font_cache.lookup
|
||||
{
|
||||
if entry == nil do continue
|
||||
|
||||
def := entry.value
|
||||
for & px_render in def.size_table {
|
||||
using px_render
|
||||
rl.UnloadFontData( glyphs, count )
|
||||
rl.UnloadTexture ( texture )
|
||||
rl.MemFree( recs )
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
font_load :: proc( path_file : string,
|
||||
default_size : f32 = Font_Load_Use_Default_Size,
|
||||
desired_id : string = Font_Load_Gen_ID
|
||||
) -> FontID
|
||||
{
|
||||
profile(#procedure)
|
||||
log( str_fmt_tmp("Loading font: %v", path_file))
|
||||
font_provider_data := & get_state().font_provider_data; using font_provider_data
|
||||
|
||||
font_data, read_succeded : = os.read_entire_file( path_file, context.temp_allocator )
|
||||
verify( b32(read_succeded), str_fmt_tmp("Failed to read font file for: %v", path_file) )
|
||||
font_data_size := cast(i32) len(font_data)
|
||||
|
||||
desired_id := desired_id
|
||||
// Use file name as key
|
||||
if len(desired_id) == 0 {
|
||||
// NOTE(Ed): This should never be used except for laziness so I'll be throwing a warning everytime.
|
||||
log("desired_key not provided, using file name. Give it a proper name!", LogLevel.Warning)
|
||||
// desired_id = cast(FontID) file_name_from_path(path_file)
|
||||
desired_id = file_name_from_path(path_file)
|
||||
}
|
||||
|
||||
default_size := default_size
|
||||
if default_size == Font_Load_Use_Default_Size {
|
||||
default_size = Font_Default_Point_Size
|
||||
}
|
||||
|
||||
key := cast(u64) crc32( transmute([]byte) desired_id )
|
||||
def, set_error := hmap_chained_set(font_cache, key, FontDef{})
|
||||
verify( set_error == AllocatorError.None, "Failed to add new font entry to cache" )
|
||||
|
||||
def.path_file = path_file
|
||||
def.default_size = i32(points_to_pixels(default_size))
|
||||
|
||||
// TODO(Ed): this is slow & eats quite a bit of memory early on. Setup a more on demand load for a specific size.
|
||||
// Also, we need to eventually switch to a SDF shader for rendering
|
||||
|
||||
// Render all sizes at once
|
||||
// Note(Ed) : We only generate textures for even multiples of the font.
|
||||
for font_size : i32 = Font_Size_Interval; font_size <= Font_Largest_Px_Size; font_size += Font_Size_Interval
|
||||
{
|
||||
profile("font size render")
|
||||
id := (font_size / Font_Size_Interval) + (font_size % Font_Size_Interval)
|
||||
|
||||
px_render := & def.size_table[id - 1]
|
||||
using px_render
|
||||
size = font_size
|
||||
count = 95 // This is the default codepoint count from raylib when loading a font.
|
||||
padding = Font_TTF_Default_Chars_Padding
|
||||
glyphs = rl.LoadFontData( raw_data(font_data), font_data_size,
|
||||
fontSize = size,
|
||||
codepoints = nil,
|
||||
codepointCount = count,
|
||||
type = rl.FontType.DEFAULT )
|
||||
verify( glyphs != nil, str_fmt_tmp("Failed to load glyphs for font: %v at desired size: %v", desired_id, size ) )
|
||||
|
||||
atlas := rl.GenImageFontAtlas( glyphs, & recs, count, size, padding, i32(Font_Atlas_Packing_Method.Raylib_Basic) )
|
||||
texture = rl.LoadTextureFromImage( atlas )
|
||||
|
||||
// glyphs_slice := slice_ptr( glyphs, count )
|
||||
// for glyph in glyphs_slice {
|
||||
// TODO(Ed) : See if above can properly reference
|
||||
|
||||
// NOTE(raylib): Update glyphs[i].image to use alpha, required to be used on image_draw_text()
|
||||
for glyph_id : i32 = 0; glyph_id < count; glyph_id += 1 {
|
||||
glyph := & glyphs[glyph_id]
|
||||
|
||||
rl.UnloadImage( glyph.image )
|
||||
glyph.image = rl.ImageFromImage( atlas, recs[glyph_id] )
|
||||
}
|
||||
rl.UnloadImage( atlas )
|
||||
}
|
||||
|
||||
free_all( context.temp_allocator )
|
||||
return { key, desired_id }
|
||||
}
|
||||
|
||||
Font_Use_Default_Size :: f32(0.0)
|
||||
|
||||
to_rl_Font :: proc( id : FontID, size := Font_Use_Default_Size ) -> rl.Font
|
||||
{
|
||||
font_provider_data := & get_state().font_provider_data; using font_provider_data
|
||||
|
||||
even_size := math.round(size * (1.0/f32(Font_Size_Interval))) * f32(Font_Size_Interval)
|
||||
size := clamp( i32( even_size), 4, Font_Largest_Px_Size )
|
||||
def := hmap_chained_get( font_cache, id.key )
|
||||
size = size if size != i32(Font_Use_Default_Size) else def.default_size
|
||||
|
||||
id := (size / Font_Size_Interval) + (size % Font_Size_Interval)
|
||||
px_render := & def.size_table[ id - 1 ]
|
||||
|
||||
rl_font : rl.Font
|
||||
rl_font.baseSize = px_render.size
|
||||
rl_font.glyphCount = px_render.count
|
||||
rl_font.glyphPadding = px_render.padding
|
||||
rl_font.glyphs = px_render.glyphs
|
||||
rl_font.recs = px_render.recs
|
||||
rl_font.texture = px_render.texture
|
||||
return rl_font
|
||||
}
|
62
code/sectr/grime/arena.odin
Normal file
62
code/sectr/grime/arena.odin
Normal file
@ -0,0 +1,62 @@
|
||||
/*
|
||||
The default arena allocator Odin provides does fragmented resizes even for the last most allocated block getting resized.
|
||||
This is an alternative to alleviates that.
|
||||
|
||||
TODO(Ed): Implement?
|
||||
*/
|
||||
package sectr
|
||||
|
||||
import "core:mem"
|
||||
|
||||
// Initialize a sub-section of our virtual memory as a sub-arena
|
||||
sub_arena_init :: proc( address : ^byte, size : int ) -> ( ^ Arena) {
|
||||
Arena :: mem.Arena
|
||||
|
||||
arena_size :: size_of( Arena)
|
||||
sub_arena := cast( ^ Arena ) address
|
||||
mem_slice := slice_ptr( ptr_offset( address, arena_size), size )
|
||||
arena_init( sub_arena, mem_slice )
|
||||
return sub_arena
|
||||
}
|
||||
|
||||
// TODO(Ed) : Once this is done (ArenaFixed), rename to just Arena as we're not going to use the core implementation
|
||||
|
||||
ArenaFixedHeader :: struct {
|
||||
data : []byte,
|
||||
offset : uint,
|
||||
peak_used : uint,
|
||||
}
|
||||
|
||||
ArenaFixed :: struct {
|
||||
using header : ^ArenaFixedHeader,
|
||||
}
|
||||
|
||||
arena_fixed_init :: proc( backing : []byte ) -> (arena : ArenaFixed) {
|
||||
header_size := size_of(ArenaFixedHeader)
|
||||
|
||||
verify(len(backing) >= (header_size + Kilobyte), "Attempted to init an arena with less than kilobyte of memory...")
|
||||
|
||||
arena.header = cast(^ArenaFixedHeader) raw_data(backing)
|
||||
using arena.header
|
||||
data_ptr := cast([^]byte) (cast( [^]ArenaFixedHeader) arena.header)[ 1:]
|
||||
data = slice_ptr( data_ptr, len(backing) - header_size )
|
||||
offset = 0
|
||||
peak_used = 0
|
||||
return
|
||||
}
|
||||
|
||||
arena_fixed_allocator_proc :: proc(
|
||||
allocator_data : rawptr,
|
||||
mode : AllocatorMode,
|
||||
size : int,
|
||||
alignment : int,
|
||||
old_memory : rawptr,
|
||||
old_size : int,
|
||||
location := #caller_location
|
||||
) -> ([]byte, AllocatorError)
|
||||
{
|
||||
|
||||
|
||||
return nil, .Out_Of_Memory
|
||||
}
|
||||
|
308
code/sectr/grime/array.odin
Normal file
308
code/sectr/grime/array.odin
Normal file
@ -0,0 +1,308 @@
|
||||
// Based on gencpp's and thus zpl's Array implementation
|
||||
// Made becasue of the map issue with fonts during hot-reload.
|
||||
// I didn't want to make the HMapZPL impl with the [dynamic] array for now to isolate the hot-reload issue (when I was diagnoising)
|
||||
package sectr
|
||||
|
||||
import "core:c/libc"
|
||||
import "core:mem"
|
||||
import "core:slice"
|
||||
|
||||
// Array :: struct ( $ Type : typeid ) {
|
||||
// bakcing : Allocator,
|
||||
// capacity : u64,
|
||||
// num : u64,
|
||||
// data : [^]Type,
|
||||
// }
|
||||
|
||||
ArrayHeader :: struct ( $ Type : typeid ) {
|
||||
backing : Allocator,
|
||||
dbg_name : string,
|
||||
fixed_cap : b32,
|
||||
capacity : u64,
|
||||
num : u64,
|
||||
data : [^]Type,
|
||||
}
|
||||
|
||||
Array :: struct ( $ Type : typeid ) {
|
||||
using header : ^ArrayHeader(Type),
|
||||
}
|
||||
|
||||
array_underlying_slice :: proc(slice: []($ Type)) -> Array(Type)
|
||||
{
|
||||
if len(slice) == 0 {
|
||||
return nil
|
||||
}
|
||||
array_size := size_of( Array(Type))
|
||||
raw_data := & slice[0]
|
||||
array_ptr := cast( ^Array(Type)) ( uintptr(first_element_ptr) - uintptr(array_size))
|
||||
return array_ptr ^
|
||||
}
|
||||
|
||||
array_to_slice :: proc( using self : Array($ Type) ) -> []Type {
|
||||
return slice_ptr( data, int(num) )
|
||||
}
|
||||
|
||||
array_to_slice_capacity :: proc( using self : Array($ Type) ) -> []Type {
|
||||
return slice_ptr( data, int(capacity))
|
||||
}
|
||||
|
||||
array_grow_formula :: proc( value : u64 ) -> u64 {
|
||||
result := (2 * value) + 8
|
||||
return result
|
||||
}
|
||||
|
||||
array_init :: proc( $ Type : typeid, allocator : Allocator ) -> ( Array(Type), AllocatorError ) {
|
||||
return array_init_reserve( Type, allocator, array_grow_formula(0) )
|
||||
}
|
||||
|
||||
array_init_reserve :: proc
|
||||
( $ Type : typeid, allocator : Allocator, capacity : u64, fixed_cap : b32 = false, dbg_name : string = "" ) -> ( result : Array(Type), alloc_error : AllocatorError )
|
||||
{
|
||||
header_size := size_of(ArrayHeader(Type))
|
||||
array_size := header_size + int(capacity) * size_of(Type)
|
||||
|
||||
raw_mem : rawptr
|
||||
raw_mem, alloc_error = alloc( array_size, allocator = allocator )
|
||||
// log( str_fmt_tmp("array reserved: %d", header_size + int(capacity) * size_of(Type) ))
|
||||
if alloc_error != AllocatorError.None do return
|
||||
|
||||
result.header = cast( ^ArrayHeader(Type)) raw_mem
|
||||
result.backing = allocator
|
||||
// result.dbg_name = dbg_name
|
||||
result.fixed_cap = fixed_cap
|
||||
result.capacity = capacity
|
||||
result.data = cast( [^]Type ) (cast( [^]ArrayHeader(Type)) result.header)[ 1:]
|
||||
return
|
||||
}
|
||||
|
||||
array_append :: proc( self : ^Array( $ Type), value : Type ) -> AllocatorError
|
||||
{
|
||||
// profile(#procedure)
|
||||
if self.header.num == self.header.capacity
|
||||
{
|
||||
grow_result := array_grow( self, self.header.capacity )
|
||||
if grow_result != AllocatorError.None {
|
||||
return grow_result
|
||||
}
|
||||
}
|
||||
|
||||
self.header.data[ self.header.num ] = value
|
||||
self.header.num += 1
|
||||
return AllocatorError.None
|
||||
}
|
||||
|
||||
array_append_slice :: proc( using self : ^Array( $ Type ), items : []Type ) -> AllocatorError
|
||||
{
|
||||
if num + len(items) > capacity
|
||||
{
|
||||
grow_result := array_grow( self, capacity )
|
||||
if grow_result != AllocatorError.None {
|
||||
return grow_result
|
||||
}
|
||||
}
|
||||
|
||||
// Note(Ed) : Original code from gencpp
|
||||
// libc.memcpy( ptr_offset(data, num), raw_data(items), len(items) * size_of(Type) )
|
||||
|
||||
// TODO(Ed) : VERIFY VIA DEBUG THIS COPY IS FINE.
|
||||
target := ptr_offset( data, num )
|
||||
copy( slice_ptr(target, capacity - num), items )
|
||||
|
||||
num += len(items)
|
||||
return AllocatorError.None
|
||||
}
|
||||
|
||||
array_append_at :: proc( using self : ^Array( $ Type ), item : Type, id : u64 ) -> AllocatorError
|
||||
{
|
||||
id := id
|
||||
if id >= num {
|
||||
id = num - 1
|
||||
}
|
||||
if id < 0 {
|
||||
id = 0
|
||||
}
|
||||
|
||||
if capacity < num + 1
|
||||
{
|
||||
grow_result := array_grow( self, capacity )
|
||||
if grow_result != AllocatorError.None {
|
||||
return grow_result
|
||||
}
|
||||
}
|
||||
|
||||
target := & data[id]
|
||||
libc.memmove( ptr_offset(target, 1), target, uint(num - id) * size_of(Type) )
|
||||
|
||||
data[id] = item
|
||||
num += 1
|
||||
return AllocatorError.None
|
||||
}
|
||||
|
||||
array_append_at_slice :: proc( using self : ^Array( $ Type ), items : []Type, id : u64 ) -> AllocatorError
|
||||
{
|
||||
id := id
|
||||
if id >= num {
|
||||
return array_append_slice( items )
|
||||
}
|
||||
if len(items) > capacity
|
||||
{
|
||||
grow_result := array_grow( self, capacity )
|
||||
if grow_result != AllocatorError.None {
|
||||
return grow_result
|
||||
}
|
||||
}
|
||||
|
||||
// Note(Ed) : Original code from gencpp
|
||||
// target := ptr_offset( data, id + len(items) )
|
||||
// src := ptr_offset( data, id )
|
||||
// libc.memmove( target, src, num - id * size_of(Type) )
|
||||
// libc.memcpy ( src, raw_data(items), len(items) * size_of(Type) )
|
||||
|
||||
// TODO(Ed) : VERIFY VIA DEBUG THIS COPY IS FINE
|
||||
target := & data[id + len(items)]
|
||||
dst := slice_ptr( target, num - id - len(items) )
|
||||
src := slice_ptr( & data[id], num - id )
|
||||
copy( dst, src )
|
||||
copy( src, items )
|
||||
|
||||
num += len(items)
|
||||
return AllocatorError.None
|
||||
}
|
||||
|
||||
// array_back :: proc( )
|
||||
|
||||
array_push_back :: proc( using self : Array( $ Type)) -> b32 {
|
||||
if num == capacity {
|
||||
return false
|
||||
}
|
||||
|
||||
data[ num ] = value
|
||||
num += 1
|
||||
return true
|
||||
}
|
||||
|
||||
array_clear :: proc "contextless" ( using self : Array( $ Type ), zero_data : b32 = false ) {
|
||||
if zero_data {
|
||||
mem.set( data, 0, int(num * size_of(Type)) )
|
||||
}
|
||||
header.num = 0
|
||||
}
|
||||
|
||||
array_fill :: proc( using self : Array( $ Type ), begin, end : u64, value : Type ) -> b32
|
||||
{
|
||||
if begin < 0 || end >= num {
|
||||
return false
|
||||
}
|
||||
|
||||
// data_slice := slice_ptr( ptr_offset( data, begin ), end - begin )
|
||||
// slice.fill( data_slice, cast(int) value )
|
||||
|
||||
for id := begin; id < end; id += 1 {
|
||||
data[ id ] = value
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
array_free :: proc( using self : Array( $ Type ) ) {
|
||||
free( self.header, backing )
|
||||
self.data = nil
|
||||
}
|
||||
|
||||
array_grow :: proc( using self : ^Array( $ Type ), min_capacity : u64 ) -> AllocatorError
|
||||
{
|
||||
// profile(#procedure)
|
||||
new_capacity := array_grow_formula( capacity )
|
||||
|
||||
if new_capacity < min_capacity {
|
||||
new_capacity = min_capacity
|
||||
}
|
||||
return array_set_capacity( self, new_capacity )
|
||||
}
|
||||
|
||||
array_pop :: proc( using self : Array( $ Type ) ) {
|
||||
verify( num != 0, "Attempted to pop an array with no elements" )
|
||||
num -= 1
|
||||
}
|
||||
|
||||
array_remove_at :: proc( using self : Array( $ Type ), id : u64 )
|
||||
{
|
||||
verify( id < header.num, "Attempted to remove from an index larger than the array" )
|
||||
|
||||
left := & data[id]
|
||||
right := & data[id + 1]
|
||||
libc.memmove( left, right, uint(num - id) * size_of(Type) )
|
||||
|
||||
header.num -= 1
|
||||
}
|
||||
|
||||
array_reserve :: proc( using self : ^Array( $ Type ), new_capacity : u64 ) -> AllocatorError
|
||||
{
|
||||
if capacity < new_capacity {
|
||||
return array_set_capacity( self, new_capacity )
|
||||
}
|
||||
return AllocatorError.None
|
||||
}
|
||||
|
||||
array_resize :: proc( array : ^Array( $ Type ), num : u64 ) -> AllocatorError
|
||||
{
|
||||
if array.capacity < num
|
||||
{
|
||||
grow_result := array_grow( array, array.capacity )
|
||||
if grow_result != AllocatorError.None {
|
||||
return grow_result
|
||||
}
|
||||
}
|
||||
|
||||
array.num = num
|
||||
return AllocatorError.None
|
||||
}
|
||||
|
||||
array_set_capacity :: proc( self : ^Array( $ Type ), new_capacity : u64 ) -> AllocatorError
|
||||
{
|
||||
if new_capacity == self.capacity {
|
||||
return AllocatorError.None
|
||||
}
|
||||
if new_capacity < self.num {
|
||||
self.num = new_capacity
|
||||
return AllocatorError.None
|
||||
}
|
||||
|
||||
header_size :: size_of(ArrayHeader(Type))
|
||||
|
||||
new_size := header_size + (cast(int) new_capacity ) * size_of(Type)
|
||||
old_size := header_size + (cast(int) self.capacity) * size_of(Type)
|
||||
|
||||
new_mem, result_code := resize_non_zeroed( self.header, old_size, new_size, mem.DEFAULT_ALIGNMENT, allocator = self.backing )
|
||||
|
||||
if result_code != AllocatorError.None {
|
||||
ensure( false, "Failed to allocate for new array capacity" )
|
||||
return result_code
|
||||
}
|
||||
if new_mem == nil {
|
||||
ensure(false, "new_mem is nil but no allocation error")
|
||||
return result_code
|
||||
}
|
||||
|
||||
self.header = cast( ^ArrayHeader(Type)) raw_data(new_mem);
|
||||
self.header.data = cast( [^]Type ) (cast( [^]ArrayHeader(Type)) self.header)[ 1:]
|
||||
self.header.capacity = new_capacity
|
||||
self.header.num = self.num
|
||||
return result_code
|
||||
}
|
||||
|
||||
array_block_size :: proc "contextless" ( self : Array( $Type ) ) -> u64 {
|
||||
header_size :: size_of(ArrayHeader(Type))
|
||||
block_size := cast(u64) (header_size + self.capacity * size_of(Type))
|
||||
return block_size
|
||||
}
|
||||
|
||||
array_memtracker_entry :: proc( self : Array( $Type ), name : string ) -> MemoryTrackerEntry {
|
||||
header_size :: size_of(ArrayHeader(Type))
|
||||
block_size := cast(uintptr) (header_size + (cast(uintptr) self.capacity) * size_of(Type))
|
||||
|
||||
block_start := transmute(^u8) self.header
|
||||
block_end := ptr_offset( block_start, block_size )
|
||||
|
||||
tracker_entry := MemoryTrackerEntry { name, block_start, block_end }
|
||||
return tracker_entry
|
||||
}
|
62
code/sectr/grime/assert.odin
Normal file
62
code/sectr/grime/assert.odin
Normal file
@ -0,0 +1,62 @@
|
||||
package sectr
|
||||
|
||||
import "base:runtime"
|
||||
import "core:io"
|
||||
import "core:os"
|
||||
import "core:text/table"
|
||||
|
||||
dump_stacktrace :: proc( allocator := context.temp_allocator ) -> string
|
||||
{
|
||||
trace_result := stacktrace()
|
||||
lines, error := stacktrace_lines( trace_result )
|
||||
|
||||
padding := " "
|
||||
|
||||
log_table := table.init( & table.Table{}, context.temp_allocator, context.temp_allocator )
|
||||
for line in lines {
|
||||
table.row( log_table, padding, line.symbol, " - ", line.location )
|
||||
}
|
||||
table.build(log_table)
|
||||
|
||||
// writer_builder_backing : [Kilobyte * 16] u8
|
||||
// writer_builder := from_bytes( writer_builder_backing[:] )
|
||||
writer_builder : StringBuilder
|
||||
str_builder_init( & writer_builder, allocator = allocator )
|
||||
|
||||
writer := to_writer( & writer_builder )
|
||||
for row in 2 ..< log_table.nr_rows {
|
||||
for col in 0 ..< log_table.nr_cols {
|
||||
table.write_table_cell( writer, log_table, row, col )
|
||||
}
|
||||
io.write_byte( writer, '\n' )
|
||||
}
|
||||
|
||||
return to_string( writer_builder )
|
||||
}
|
||||
|
||||
ensure :: proc( condition : b32, msg : string, location := #caller_location )
|
||||
{
|
||||
if condition {
|
||||
return
|
||||
}
|
||||
log( msg, LogLevel.Warning, location )
|
||||
runtime.debug_trap()
|
||||
}
|
||||
|
||||
// TODO(Ed) : Setup exit codes!
|
||||
fatal :: proc( msg : string, exit_code : int = -1, location := #caller_location )
|
||||
{
|
||||
log( msg, LogLevel.Fatal, location )
|
||||
runtime.debug_trap()
|
||||
os.exit( exit_code )
|
||||
}
|
||||
|
||||
verify :: proc( condition : b32, msg : string, exit_code : int = -1, location := #caller_location )
|
||||
{
|
||||
if condition {
|
||||
return
|
||||
}
|
||||
log( msg, LogLevel.Fatal, location )
|
||||
runtime.debug_trap()
|
||||
os.exit( exit_code )
|
||||
}
|
6
code/sectr/grime/context.odin
Normal file
6
code/sectr/grime/context.odin
Normal file
@ -0,0 +1,6 @@
|
||||
package sectr
|
||||
|
||||
context_ext :: proc( $ Type : typeid ) -> (^Type) {
|
||||
return cast(^Type) context.user_ptr
|
||||
}
|
||||
|
67
code/sectr/grime/filesystem.odin
Normal file
67
code/sectr/grime/filesystem.odin
Normal file
@ -0,0 +1,67 @@
|
||||
// TODO(Ed) : Move this to a grime package
|
||||
package sectr
|
||||
|
||||
import "core:fmt"
|
||||
import "core:os"
|
||||
import "base:runtime"
|
||||
|
||||
// Test
|
||||
|
||||
file_copy_sync :: proc( path_src, path_dst: string, allocator := context.temp_allocator ) -> b32
|
||||
{
|
||||
file_size : i64
|
||||
{
|
||||
path_info, result := file_status( path_src, allocator )
|
||||
if result != os.ERROR_NONE {
|
||||
logf("Could not get file info: %v", result, LogLevel.Error )
|
||||
return false
|
||||
}
|
||||
file_size = path_info.size
|
||||
}
|
||||
|
||||
src_content, result := os.read_entire_file( path_src, allocator )
|
||||
if ! result {
|
||||
logf( "Failed to read file to copy: %v", path_src, LogLevel.Error )
|
||||
runtime.debug_trap()
|
||||
return false
|
||||
}
|
||||
|
||||
result = os.write_entire_file( path_dst, src_content, false )
|
||||
if ! result {
|
||||
logf( "Failed to copy file: %v", path_dst, LogLevel.Error )
|
||||
runtime.debug_trap()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
file_exists :: proc( file_path : string ) -> b32 {
|
||||
path_info, result := file_status( file_path, frame_allocator() )
|
||||
if result != os.ERROR_NONE {
|
||||
return false
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
file_is_locked :: proc( file_path : string ) -> b32 {
|
||||
handle, err := file_open(file_path, os.O_RDONLY)
|
||||
if err != os.ERROR_NONE {
|
||||
// If the error indicates the file is in use, return true.
|
||||
return true
|
||||
}
|
||||
|
||||
// If the file opens successfully, close it and return false.
|
||||
file_close(handle)
|
||||
return false
|
||||
}
|
||||
|
||||
file_rewind :: proc( file : os.Handle ) {
|
||||
file_seek( file, 0, 0 )
|
||||
}
|
||||
|
||||
file_read_looped :: proc( file : os.Handle, data : []byte ) {
|
||||
total_read, result_code := file_read( file, data )
|
||||
if result_code == os.ERROR_HANDLE_EOF {
|
||||
file_rewind( file )
|
||||
}
|
||||
}
|
324
code/sectr/grime/grime.odin
Normal file
324
code/sectr/grime/grime.odin
Normal file
@ -0,0 +1,324 @@
|
||||
|
||||
package sectr
|
||||
|
||||
#region("Import Aliases")
|
||||
|
||||
import "base:builtin"
|
||||
copy :: builtin.copy
|
||||
import "base:intrinsics"
|
||||
mem_zero :: intrinsics.mem_zero
|
||||
ptr_sub :: intrinsics.ptr_sub
|
||||
type_has_field :: intrinsics.type_has_field
|
||||
type_elem_type :: intrinsics.type_elem_type
|
||||
import "base:runtime"
|
||||
Byte :: runtime.Byte
|
||||
Kilobyte :: runtime.Kilobyte
|
||||
Megabyte :: runtime.Megabyte
|
||||
Gigabyte :: runtime.Gigabyte
|
||||
Terabyte :: runtime.Terabyte
|
||||
Petabyte :: runtime.Petabyte
|
||||
Exabyte :: runtime.Exabyte
|
||||
resize_non_zeroed :: runtime.non_zero_mem_resize
|
||||
SourceCodeLocation :: runtime.Source_Code_Location
|
||||
import c "core:c/libc"
|
||||
import "core:dynlib"
|
||||
import "core:hash"
|
||||
crc32 :: hash.crc32
|
||||
import "core:hash/xxhash"
|
||||
xxh32 :: xxhash.XXH32
|
||||
import fmt_io "core:fmt"
|
||||
str_fmt :: fmt_io.printf
|
||||
str_fmt_tmp :: fmt_io.tprintf
|
||||
str_fmt_alloc :: fmt_io.aprintf
|
||||
str_fmt_builder :: fmt_io.sbprintf
|
||||
str_fmt_buffer :: fmt_io.bprintf
|
||||
str_to_file_ln :: fmt_io.fprintln
|
||||
str_tmp_from_any :: fmt_io.tprint
|
||||
import "core:math"
|
||||
import "core:mem"
|
||||
align_forward_int :: mem.align_forward_int
|
||||
align_forward_uint :: mem.align_forward_uint
|
||||
align_forward_uintptr :: mem.align_forward_uintptr
|
||||
Allocator :: mem.Allocator
|
||||
AllocatorError :: mem.Allocator_Error
|
||||
AllocatorMode :: mem.Allocator_Mode
|
||||
AllocatorModeSet :: mem.Allocator_Mode_Set
|
||||
alloc :: mem.alloc
|
||||
alloc_bytes :: mem.alloc_bytes
|
||||
alloc_bytes_non_zeroed :: mem.alloc_bytes_non_zeroed
|
||||
Arena :: mem.Arena
|
||||
arena_allocator :: mem.arena_allocator
|
||||
arena_init :: mem.arena_init
|
||||
byte_slice :: mem.byte_slice
|
||||
copy_non_overlapping :: mem.copy_non_overlapping
|
||||
free :: mem.free
|
||||
is_power_of_two_uintptr :: mem.is_power_of_two
|
||||
ptr_offset :: mem.ptr_offset
|
||||
resize :: mem.resize
|
||||
slice_ptr :: mem.slice_ptr
|
||||
TrackingAllocator :: mem.Tracking_Allocator
|
||||
tracking_allocator :: mem.tracking_allocator
|
||||
tracking_allocator_init :: mem.tracking_allocator_init
|
||||
import "core:mem/virtual"
|
||||
VirtualProtectFlags :: virtual.Protect_Flags
|
||||
// import "core:odin"
|
||||
import "core:os"
|
||||
FileFlag_Create :: os.O_CREATE
|
||||
FileFlag_ReadWrite :: os.O_RDWR
|
||||
FileTime :: os.File_Time
|
||||
file_close :: os.close
|
||||
file_open :: os.open
|
||||
file_read :: os.read
|
||||
file_remove :: os.remove
|
||||
file_seek :: os.seek
|
||||
file_status :: os.stat
|
||||
file_write :: os.write
|
||||
import "core:path/filepath"
|
||||
file_name_from_path :: filepath.short_stem
|
||||
import str "core:strings"
|
||||
StringBuilder :: str.Builder
|
||||
str_builder_from_bytes :: str.builder_from_bytes
|
||||
str_builder_init :: str.builder_init
|
||||
str_builder_to_writer :: str.to_writer
|
||||
str_builder_to_string :: str.to_string
|
||||
import "core:time"
|
||||
Duration :: time.Duration
|
||||
duration_seconds :: time.duration_seconds
|
||||
duration_ms :: time.duration_milliseconds
|
||||
thread_sleep :: time.sleep
|
||||
import "core:unicode"
|
||||
is_white_space :: unicode.is_white_space
|
||||
import "core:unicode/utf8"
|
||||
str_rune_count :: utf8.rune_count_in_string
|
||||
runes_to_string :: utf8.runes_to_string
|
||||
// string_to_runes :: utf8.string_to_runes
|
||||
import "thirdparty:backtrace"
|
||||
StackTraceData :: backtrace.Trace_Const
|
||||
stacktrace :: backtrace.trace
|
||||
stacktrace_lines :: backtrace.lines
|
||||
|
||||
#endregion("Import Aliases")
|
||||
|
||||
#region("Proc overload mappings")
|
||||
|
||||
// This has to be done on a per-module basis.
|
||||
|
||||
add :: proc {
|
||||
add_range2,
|
||||
}
|
||||
|
||||
bivec3 :: proc {
|
||||
bivec3_via_f32s,
|
||||
vec3_to_bivec3,
|
||||
}
|
||||
|
||||
cm_to_pixels :: proc {
|
||||
f32_cm_to_pixels,
|
||||
vec2_cm_to_pixels,
|
||||
range2_cm_to_pixels,
|
||||
}
|
||||
|
||||
regress :: proc {
|
||||
regress_bivec3,
|
||||
}
|
||||
|
||||
cross :: proc {
|
||||
cross_vec3,
|
||||
}
|
||||
|
||||
dot :: proc {
|
||||
dot_vec2,
|
||||
dot_vec3,
|
||||
dot_v3_unitv3,
|
||||
dot_unitv3_vs,
|
||||
}
|
||||
|
||||
ws_view_draw_text :: proc {
|
||||
ws_view_draw_text_string,
|
||||
ws_view_draw_text_StrRunesPair,
|
||||
}
|
||||
|
||||
from_bytes :: proc {
|
||||
str_builder_from_bytes,
|
||||
}
|
||||
|
||||
get_bounds :: proc {
|
||||
view_get_bounds,
|
||||
}
|
||||
|
||||
inverse_mag :: proc {
|
||||
inverse_mag_vec3,
|
||||
// inverse_mag_rotor3,
|
||||
}
|
||||
|
||||
is_power_of_two :: proc {
|
||||
is_power_of_two_u32,
|
||||
is_power_of_two_uintptr,
|
||||
}
|
||||
|
||||
measure_text_size :: proc {
|
||||
measure_text_size_raylib,
|
||||
}
|
||||
|
||||
mov_avg_exp :: proc {
|
||||
mov_avg_exp_f32,
|
||||
mov_avg_exp_f64,
|
||||
}
|
||||
|
||||
pixels_to_cm :: proc {
|
||||
f32_pixels_to_cm,
|
||||
vec2_pixels_to_cm,
|
||||
range2_pixels_to_cm,
|
||||
}
|
||||
|
||||
points_to_pixels :: proc {
|
||||
f32_points_to_pixels,
|
||||
vec2_points_to_pixels,
|
||||
}
|
||||
|
||||
pop :: proc {
|
||||
stack_pop,
|
||||
stack_allocator_pop,
|
||||
}
|
||||
|
||||
pow :: proc{
|
||||
math.pow_f16,
|
||||
math.pow_f16le,
|
||||
math.pow_f16be,
|
||||
math.pow_f32,
|
||||
math.pow_f32le,
|
||||
math.pow_f32be,
|
||||
math.pow_f64,
|
||||
math.pow_f64le,
|
||||
math.pow_f64be,
|
||||
}
|
||||
|
||||
pow2 :: proc {
|
||||
pow2_vec3,
|
||||
}
|
||||
|
||||
pressed :: proc {
|
||||
btn_pressed,
|
||||
}
|
||||
|
||||
push :: proc {
|
||||
stack_push,
|
||||
stack_allocator_push,
|
||||
}
|
||||
|
||||
rotor3 :: proc {
|
||||
rotor3_via_comps,
|
||||
rotor3_via_bv_s,
|
||||
rotor3_via_from_to,
|
||||
}
|
||||
|
||||
released :: proc {
|
||||
btn_released,
|
||||
}
|
||||
|
||||
sqrt :: proc{
|
||||
math.sqrt_f16,
|
||||
math.sqrt_f16le,
|
||||
math.sqrt_f16be,
|
||||
math.sqrt_f32,
|
||||
math.sqrt_f32le,
|
||||
math.sqrt_f32be,
|
||||
math.sqrt_f64,
|
||||
math.sqrt_f64le,
|
||||
math.sqrt_f64be,
|
||||
}
|
||||
|
||||
inverse_sqrt :: proc {
|
||||
inverse_sqrt_f32,
|
||||
}
|
||||
|
||||
sub :: proc {
|
||||
sub_point3,
|
||||
sub_range2,
|
||||
sub_bivec3,
|
||||
}
|
||||
|
||||
to_quat128 :: proc {
|
||||
rotor3_to_quat128,
|
||||
}
|
||||
|
||||
to_rl_rect :: proc {
|
||||
range2_to_rl_rect,
|
||||
}
|
||||
|
||||
to_runes :: proc {
|
||||
string_to_runes,
|
||||
}
|
||||
|
||||
to_string :: proc {
|
||||
runes_to_string,
|
||||
str_builder_to_string,
|
||||
}
|
||||
|
||||
vec3 :: proc {
|
||||
vec3_via_f32s,
|
||||
bivec3_to_vec3,
|
||||
point3_to_vec3,
|
||||
pointflat3_to_vec3,
|
||||
unitvec3_to_vec3,
|
||||
}
|
||||
|
||||
vec4 :: proc {
|
||||
unitvec4_to_vec4,
|
||||
}
|
||||
|
||||
to_writer :: proc {
|
||||
str_builder_to_writer,
|
||||
}
|
||||
|
||||
to_ui_layout_side :: proc {
|
||||
to_ui_layout_side_f32,
|
||||
to_ui_layout_side_vec2,
|
||||
}
|
||||
|
||||
ui_compute_layout :: proc {
|
||||
ui_core_compute_layout,
|
||||
ui_box_compute_layout,
|
||||
}
|
||||
|
||||
ui_floating :: proc {
|
||||
ui_floating_just_builder,
|
||||
ui_floating_with_capture,
|
||||
}
|
||||
|
||||
ui_layout_push :: proc {
|
||||
ui_layout_push_layout,
|
||||
ui_layout_push_theme,
|
||||
}
|
||||
|
||||
ui_layout :: proc {
|
||||
ui_layout_via_layout,
|
||||
ui_layout_via_combo,
|
||||
}
|
||||
|
||||
ui_style_push :: proc {
|
||||
ui_style_push_style,
|
||||
ui_style_push_combo,
|
||||
}
|
||||
|
||||
ui_style :: proc {
|
||||
ui_style_via_style,
|
||||
ui_style_via_combo,
|
||||
}
|
||||
|
||||
ui_theme :: proc {
|
||||
ui_theme_via_layout_style,
|
||||
ui_theme_via_combos,
|
||||
ui_theme_via_theme,
|
||||
}
|
||||
|
||||
wedge :: proc {
|
||||
wedge_vec3,
|
||||
wedge_bivec3,
|
||||
}
|
||||
|
||||
#endregion("Proc overload mappings")
|
||||
|
||||
OS_Type :: type_of(ODIN_OS)
|
||||
|
||||
swap :: #force_inline proc( a, b : ^ $Type ) -> ( ^ Type, ^ Type ) { return b, a }
|
225
code/sectr/grime/hashmap_chained.odin
Normal file
225
code/sectr/grime/hashmap_chained.odin
Normal file
@ -0,0 +1,225 @@
|
||||
/*
|
||||
Separate chaining hashtable with tombstone (vacancy aware)
|
||||
|
||||
This is an alternative to odin's map and the zpl hashtable I first used for this codebase.
|
||||
I haven't felt the need to go back to dealing with odin's map for my edge case hot reload/memory replay failure.
|
||||
|
||||
So this is a hahstable loosely based at what I saw in the raddbg codebase.
|
||||
It uses a fixed-size lookup table for the base layer of entries that can be chained.
|
||||
Each slot keeps track of its vacancy (tombstone, is occupied).
|
||||
If its occupied a new slot is chained using the fixed bucket-size pool allocator which will have its blocks sized to the type of the table.
|
||||
|
||||
This is ideal for tables have an indeterminate scope for how entires are added,
|
||||
and direct pointers are kept across the codebase instead of a key to the slot.
|
||||
*/
|
||||
package sectr
|
||||
|
||||
import "core:mem"
|
||||
|
||||
HTable_Minimum_Capacity :: 4 * Kilobyte
|
||||
|
||||
HMapChainedSlot :: struct( $Type : typeid ) {
|
||||
using links : DLL_NodePN(HMapChainedSlot(Type)),
|
||||
value : Type,
|
||||
key : u64,
|
||||
occupied : b32,
|
||||
}
|
||||
|
||||
HMapChained :: struct( $ Type : typeid ) {
|
||||
pool : Pool,
|
||||
lookup : [] ^HMapChainedSlot(Type),
|
||||
}
|
||||
|
||||
HMapChainedPtr :: struct( $ Type : typeid) {
|
||||
using header : ^HMapChained(Type),
|
||||
}
|
||||
|
||||
// Provides the nearest prime number value for the given capacity
|
||||
hmap_closest_prime :: proc( capacity : uint ) -> uint
|
||||
{
|
||||
prime_table : []uint = {
|
||||
53, 97, 193, 389, 769, 1543, 3079, 6151, 12289, 24593,
|
||||
49157, 98317, 196613, 393241, 786433, 1572869, 3145739,
|
||||
6291469, 12582917, 25165843, 50331653, 100663319,
|
||||
201326611, 402653189, 805306457, 1610612741, 3221225473, 6442450941
|
||||
};
|
||||
for slot in prime_table {
|
||||
if slot >= capacity {
|
||||
return slot
|
||||
}
|
||||
}
|
||||
return prime_table[len(prime_table) - 1]
|
||||
}
|
||||
|
||||
hmap_chained_init :: proc( $Type : typeid, lookup_capacity : uint, allocator : Allocator,
|
||||
pool_bucket_cap : uint = 1 * Kilo,
|
||||
pool_bucket_reserve_num : uint = 0,
|
||||
pool_alignment : uint = mem.DEFAULT_ALIGNMENT,
|
||||
dbg_name : string = ""
|
||||
) -> (table : HMapChainedPtr(Type), error : AllocatorError)
|
||||
{
|
||||
header_size := size_of(HMapChained(Type))
|
||||
size := header_size + int(lookup_capacity) * size_of( ^HMapChainedSlot(Type)) + size_of(int)
|
||||
|
||||
raw_mem : rawptr
|
||||
raw_mem, error = alloc( size, allocator = allocator )
|
||||
if error != AllocatorError.None do return
|
||||
|
||||
table.header = cast( ^HMapChained(Type)) raw_mem
|
||||
table.pool, error = pool_init(
|
||||
should_zero_buckets = false,
|
||||
block_size = size_of(HMapChainedSlot(Type)),
|
||||
bucket_capacity = pool_bucket_cap,
|
||||
bucket_reserve_num = pool_bucket_reserve_num,
|
||||
alignment = pool_alignment,
|
||||
allocator = allocator,
|
||||
dbg_name = str_intern(str_fmt_tmp("%v: pool", dbg_name)).str
|
||||
)
|
||||
data := transmute([^] ^HMapChainedSlot(Type)) (transmute( [^]HMapChained(Type)) table.header)[1:]
|
||||
table.lookup = slice_ptr( data, int(lookup_capacity) )
|
||||
return
|
||||
}
|
||||
|
||||
hmap_chained_clear :: proc( using self : HMapChainedPtr($Type))
|
||||
{
|
||||
for slot in lookup
|
||||
{
|
||||
if slot == nil {
|
||||
continue
|
||||
}
|
||||
for probe_slot = slot.next; probe_slot != nil; probe_slot = probe_slot.next {
|
||||
slot.occupied = false
|
||||
}
|
||||
slot.occupied = false
|
||||
}
|
||||
}
|
||||
|
||||
hmap_chained_destroy :: proc( using self : ^HMapChainedPtr($Type)) {
|
||||
pool_destroy( pool )
|
||||
free( self.header, backing)
|
||||
self = nil
|
||||
}
|
||||
|
||||
hmap_chained_lookup_id :: #force_inline proc( using self : HMapChainedPtr($Type), key : u64 ) -> u64
|
||||
{
|
||||
hash_index := key % u64( len(lookup) )
|
||||
return hash_index
|
||||
}
|
||||
|
||||
hmap_chained_get :: proc( using self : HMapChainedPtr($Type), key : u64) -> ^Type
|
||||
{
|
||||
// profile(#procedure)
|
||||
surface_slot := lookup[hmap_chained_lookup_id(self, key)]
|
||||
|
||||
if surface_slot == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if surface_slot.occupied && surface_slot.key == key {
|
||||
return & surface_slot.value
|
||||
}
|
||||
|
||||
for slot := surface_slot.next; slot != nil; slot = slot.next {
|
||||
if slot.occupied && slot.key == key {
|
||||
return & surface_slot.value
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
hmap_chained_reload :: proc( self : HMapChainedPtr($Type), allocator : Allocator )
|
||||
{
|
||||
pool_reload(self.pool, allocator)
|
||||
}
|
||||
|
||||
// Returns true if an slot was actually found and marked as vacant
|
||||
// Entries already found to be vacant will not return true
|
||||
hmap_chained_remove :: proc( self : HMapChainedPtr($Type), key : u64 ) -> b32
|
||||
{
|
||||
surface_slot := lookup[hmap_chained_lookup_id(self, key)]
|
||||
|
||||
if surface_slot == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if surface_slot.occupied && surface_slot.key == key {
|
||||
surface_slot.occupied = false
|
||||
return true
|
||||
}
|
||||
|
||||
for slot := surface_slot.next; slot != nil; slot.next
|
||||
{
|
||||
if slot.occupied && slot.key == key {
|
||||
slot.occupied = false
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Sets the value to a vacant slot
|
||||
// Will preemptively allocate the next slot in the hashtable if its null for the slot.
|
||||
hmap_chained_set :: proc( using self : HMapChainedPtr($Type), key : u64, value : Type ) -> (^ Type, AllocatorError)
|
||||
{
|
||||
// profile(#procedure)
|
||||
hash_index := hmap_chained_lookup_id(self, key)
|
||||
surface_slot := lookup[hash_index]
|
||||
set_slot :: #force_inline proc( using self : HMapChainedPtr(Type),
|
||||
slot : ^HMapChainedSlot(Type),
|
||||
key : u64,
|
||||
value : Type
|
||||
) -> (^ Type, AllocatorError )
|
||||
{
|
||||
error := AllocatorError.None
|
||||
if slot.next == nil {
|
||||
block : []byte
|
||||
block, error = pool_grab(pool)
|
||||
next := transmute( ^HMapChainedSlot(Type)) & block[0]
|
||||
slot.next = next
|
||||
next.prev = slot
|
||||
}
|
||||
slot.key = key
|
||||
slot.value = value
|
||||
slot.occupied = true
|
||||
return & slot.value, error
|
||||
}
|
||||
|
||||
if surface_slot == nil {
|
||||
block, error := pool_grab(pool)
|
||||
surface_slot := transmute( ^HMapChainedSlot(Type)) & block[0]
|
||||
surface_slot.key = key
|
||||
surface_slot.value = value
|
||||
surface_slot.occupied = true
|
||||
if error != AllocatorError.None {
|
||||
ensure(error != AllocatorError.None, "Allocation failure for chained slot in hash table")
|
||||
return nil, error
|
||||
}
|
||||
lookup[hash_index] = surface_slot
|
||||
|
||||
block, error = pool_grab(pool)
|
||||
next := transmute( ^HMapChainedSlot(Type)) & block[0]
|
||||
surface_slot.next = next
|
||||
next.prev = surface_slot
|
||||
return & surface_slot.value, error
|
||||
}
|
||||
|
||||
if ! surface_slot.occupied
|
||||
{
|
||||
result, error := set_slot( self, surface_slot, key, value)
|
||||
return result, error
|
||||
}
|
||||
|
||||
slot := surface_slot.next
|
||||
for ; slot != nil; slot = slot.next
|
||||
{
|
||||
if !slot.occupied
|
||||
{
|
||||
result, error := set_slot( self, surface_slot, key, value)
|
||||
return result, error
|
||||
}
|
||||
}
|
||||
ensure(false, "Somehow got to a null slot that wasn't preemptively allocated from a previus set")
|
||||
return nil, AllocatorError.None
|
||||
}
|
274
code/sectr/grime/hashmap_zpl.odin
Normal file
274
code/sectr/grime/hashmap_zpl.odin
Normal file
@ -0,0 +1,274 @@
|
||||
/*
|
||||
This is an alternative to Odin's default map type.
|
||||
The only reason I may need this is due to issues with allocator callbacks or something else going on
|
||||
with hot-reloads...
|
||||
|
||||
This implementation uses two ZPL-Based Arrays to hold entires and the actual hash table.
|
||||
Instead of using separate chains, it maintains linked entries within the array.
|
||||
Each entry contains a next field, which is an index pointing to the next entry in the same array.
|
||||
|
||||
Growing this hashtable is destructive, so it should usually be kept to a fixed-size unless
|
||||
the populating operations only occur in one place and from then on its read-only.
|
||||
*/
|
||||
package sectr
|
||||
|
||||
import "core:slice"
|
||||
|
||||
// Note(Ed) : See core:hash for hasing procs.
|
||||
|
||||
HMapZPL_MapProc :: #type proc( $ Type : typeid, key : u64, value : Type )
|
||||
HMapZPL_MapMutProc :: #type proc( $ Type : typeid, key : u64, value : ^ Type )
|
||||
|
||||
HMapZPL_CritialLoadScale :: 0.70
|
||||
HMapZPL_HashToEntryRatio :: 1.50
|
||||
|
||||
HMapZPL_FindResult :: struct {
|
||||
hash_index : i64,
|
||||
prev_index : i64,
|
||||
entry_index : i64,
|
||||
}
|
||||
|
||||
HMapZPL_Entry :: struct ( $ Type : typeid) {
|
||||
key : u64,
|
||||
next : i64,
|
||||
value : Type,
|
||||
}
|
||||
|
||||
HMapZPL :: struct ( $ Type : typeid ) {
|
||||
table : Array( i64 ),
|
||||
entries : Array( HMapZPL_Entry(Type) ),
|
||||
}
|
||||
|
||||
zpl_hmap_init :: proc( $ Type : typeid, allocator : Allocator ) -> ( HMapZPL( Type), AllocatorError ) {
|
||||
return zpl_hmap_init_reserve( Type, allocator )
|
||||
}
|
||||
|
||||
zpl_hmap_init_reserve :: proc
|
||||
( $ Type : typeid, allocator : Allocator, num : u64, dbg_name : string = "" ) -> ( HMapZPL( Type), AllocatorError )
|
||||
{
|
||||
result : HMapZPL(Type)
|
||||
table_result, entries_result : AllocatorError
|
||||
|
||||
result.table, table_result = array_init_reserve( i64, allocator, num, dbg_name = dbg_name )
|
||||
if table_result != AllocatorError.None {
|
||||
ensure( false, "Failed to allocate table array" )
|
||||
return result, table_result
|
||||
}
|
||||
array_resize( & result.table, num )
|
||||
slice.fill( slice_ptr( result.table.data, cast(int) result.table.num), -1 )
|
||||
|
||||
result.entries, entries_result = array_init_reserve( HMapZPL_Entry(Type), allocator, num, dbg_name = dbg_name )
|
||||
if entries_result != AllocatorError.None {
|
||||
ensure( false, "Failed to allocate entries array" )
|
||||
return result, entries_result
|
||||
}
|
||||
return result, AllocatorError.None
|
||||
}
|
||||
|
||||
zpl_hmap_clear :: proc( using self : ^ HMapZPL( $ Type ) ) {
|
||||
for id := 0; id < table.num; id += 1 {
|
||||
table[id] = -1
|
||||
}
|
||||
|
||||
array_clear( table )
|
||||
array_clear( entries )
|
||||
}
|
||||
|
||||
zpl_hmap_destroy :: proc( using self : ^ HMapZPL( $ Type ) ) {
|
||||
if table.data != nil && table.capacity > 0 {
|
||||
array_free( table )
|
||||
array_free( entries )
|
||||
}
|
||||
}
|
||||
|
||||
zpl_hmap_get :: proc ( using self : ^ HMapZPL( $ Type ), key : u64 ) -> ^ Type
|
||||
{
|
||||
// profile(#procedure)
|
||||
id := zpl_hmap_find( self, key ).entry_index
|
||||
if id >= 0 {
|
||||
return & entries.data[id].value
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
zpl_hmap_map :: proc( using self : ^ HMapZPL( $ Type), map_proc : HMapZPL_MapProc ) {
|
||||
ensure( map_proc != nil, "Mapping procedure must not be null" )
|
||||
for id := 0; id < entries.num; id += 1 {
|
||||
map_proc( Type, entries[id].key, entries[id].value )
|
||||
}
|
||||
}
|
||||
|
||||
zpl_hmap_map_mut :: proc( using self : ^ HMapZPL( $ Type), map_proc : HMapZPL_MapMutProc ) {
|
||||
ensure( map_proc != nil, "Mapping procedure must not be null" )
|
||||
for id := 0; id < entries.num; id += 1 {
|
||||
map_proc( Type, entries[id].key, & entries[id].value )
|
||||
}
|
||||
}
|
||||
|
||||
zpl_hmap_grow :: proc( using self : ^ HMapZPL( $ Type ) ) -> AllocatorError {
|
||||
new_num := array_grow_formula( entries.num )
|
||||
return zpl_hmap_rehash( self, new_num )
|
||||
}
|
||||
|
||||
zpl_hmap_rehash :: proc( ht : ^ HMapZPL( $ Type ), new_num : u64 ) -> AllocatorError
|
||||
{
|
||||
profile(#procedure)
|
||||
// For now the prototype should never allow this to happen.
|
||||
ensure( false, "ZPL HMAP IS REHASHING" )
|
||||
last_added_index : i64
|
||||
|
||||
new_ht, init_result := zpl_hmap_init_reserve( Type, ht.table.backing, new_num, ht.table.dbg_name )
|
||||
if init_result != AllocatorError.None {
|
||||
ensure( false, "New zpl_hmap failed to allocate" )
|
||||
return init_result
|
||||
}
|
||||
|
||||
for id : u64 = 0; id < ht.entries.num; id += 1 {
|
||||
find_result : HMapZPL_FindResult
|
||||
|
||||
entry := & ht.entries.data[id]
|
||||
find_result = zpl_hmap_find( & new_ht, entry.key )
|
||||
last_added_index = zpl_hmap_add_entry( & new_ht, entry.key )
|
||||
|
||||
if find_result.prev_index < 0 {
|
||||
new_ht.table.data[ find_result.hash_index ] = last_added_index
|
||||
}
|
||||
else {
|
||||
new_ht.entries.data[ find_result.prev_index ].next = last_added_index
|
||||
}
|
||||
|
||||
new_ht.entries.data[ last_added_index ].next = find_result.entry_index
|
||||
new_ht.entries.data[ last_added_index ].value = entry.value
|
||||
}
|
||||
|
||||
zpl_hmap_destroy( ht )
|
||||
|
||||
(ht ^) = new_ht
|
||||
return AllocatorError.None
|
||||
}
|
||||
|
||||
zpl_hmap_rehash_fast :: proc( using self : ^ HMapZPL( $ Type ) )
|
||||
{
|
||||
for id := 0; id < entries.num; id += 1 {
|
||||
entries[id].Next = -1;
|
||||
}
|
||||
for id := 0; id < table.num; id += 1 {
|
||||
table[id] = -1
|
||||
}
|
||||
for id := 0; id < entries.num; id += 1 {
|
||||
entry := & entries[id]
|
||||
find_result := zpl_hmap_find( entry.key )
|
||||
|
||||
if find_result.prev_index < 0 {
|
||||
table[ find_result.hash_index ] = id
|
||||
}
|
||||
else {
|
||||
entries[ find_result.prev_index ].next = id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Used when the address space of the allocator changes and the backing reference must be updated
|
||||
zpl_hmap_reload :: proc( using self : ^HMapZPL($Type), new_backing : Allocator ) {
|
||||
table.backing = new_backing
|
||||
entries.backing = new_backing
|
||||
}
|
||||
|
||||
zpl_hmap_remove :: proc( self : ^ HMapZPL( $ Type ), key : u64 ) {
|
||||
find_result := zpl_hmap_find( key )
|
||||
|
||||
if find_result.entry_index >= 0 {
|
||||
array_remove_at( & entries, find_result.entry_index )
|
||||
zpl_hmap_rehash_fast( self )
|
||||
}
|
||||
}
|
||||
|
||||
zpl_hmap_remove_entry :: proc( using self : ^ HMapZPL( $ Type ), id : i64 ) {
|
||||
array_remove_at( & entries, id )
|
||||
}
|
||||
|
||||
zpl_hmap_set :: proc( using self : ^ HMapZPL( $ Type), key : u64, value : Type ) -> (^ Type, AllocatorError)
|
||||
{
|
||||
// profile(#procedure)
|
||||
id : i64 = 0
|
||||
find_result : HMapZPL_FindResult
|
||||
|
||||
if zpl_hmap_full( self )
|
||||
{
|
||||
grow_result := zpl_hmap_grow( self )
|
||||
if grow_result != AllocatorError.None {
|
||||
return nil, grow_result
|
||||
}
|
||||
}
|
||||
|
||||
find_result = zpl_hmap_find( self, key )
|
||||
if find_result.entry_index >= 0 {
|
||||
id = find_result.entry_index
|
||||
}
|
||||
else
|
||||
{
|
||||
id = zpl_hmap_add_entry( self, key )
|
||||
if find_result.prev_index >= 0 {
|
||||
entries.data[ find_result.prev_index ].next = id
|
||||
}
|
||||
else {
|
||||
table.data[ find_result.hash_index ] = id
|
||||
}
|
||||
}
|
||||
|
||||
entries.data[id].value = value
|
||||
|
||||
if zpl_hmap_full( self ) {
|
||||
alloc_error := zpl_hmap_grow( self )
|
||||
return & entries.data[id].value, alloc_error
|
||||
}
|
||||
|
||||
return & entries.data[id].value, AllocatorError.None
|
||||
}
|
||||
|
||||
zpl_hmap_slot :: proc( using self : ^ HMapZPL( $ Type), key : u64 ) -> i64 {
|
||||
for id : i64 = 0; id < table.num; id += 1 {
|
||||
if table.data[id] == key {
|
||||
return id
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
zpl_hmap_add_entry :: proc( using self : ^ HMapZPL( $ Type), key : u64 ) -> i64 {
|
||||
entry : HMapZPL_Entry(Type) = { key, -1, {} }
|
||||
id := cast(i64) entries.num
|
||||
array_append( & entries, entry )
|
||||
return id
|
||||
}
|
||||
|
||||
zpl_hmap_find :: proc( using self : ^ HMapZPL( $ Type), key : u64 ) -> HMapZPL_FindResult
|
||||
{
|
||||
// profile(#procedure)
|
||||
result : HMapZPL_FindResult = { -1, -1, -1 }
|
||||
|
||||
if table.num > 0 {
|
||||
result.hash_index = cast(i64)( key % table.num )
|
||||
result.entry_index = table.data[ result.hash_index ]
|
||||
|
||||
verify( result.entry_index < i64(entries.num), "Entry index is larger than the number of entries" )
|
||||
|
||||
for ; result.entry_index >= 0; {
|
||||
entry := & entries.data[ result.entry_index ]
|
||||
if entry.key == key {
|
||||
break
|
||||
}
|
||||
|
||||
result.prev_index = result.entry_index
|
||||
result.entry_index = entry.next
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
zpl_hmap_full :: proc( using self : ^ HMapZPL( $ Type) ) -> b32 {
|
||||
critical_load := u64(HMapZPL_CritialLoadScale * cast(f64) table.num)
|
||||
result : b32 = entries.num > critical_load
|
||||
return result
|
||||
}
|
190
code/sectr/grime/linked_list.odin
Normal file
190
code/sectr/grime/linked_list.odin
Normal file
@ -0,0 +1,190 @@
|
||||
package sectr
|
||||
|
||||
LL_Node :: struct ( $ Type : typeid ) {
|
||||
next : ^Type,
|
||||
}
|
||||
|
||||
// ll_push :: proc( list_ptr : ^(^ ($ Type)), node : ^Type ) {
|
||||
ll_push :: #force_inline proc "contextless" ( list_ptr : ^(^ ($ Type)), node : ^Type ) {
|
||||
list : ^Type = (list_ptr^)
|
||||
node.next = list
|
||||
(list_ptr^) = node
|
||||
}
|
||||
|
||||
ll_pop :: #force_inline proc "contextless" ( list_ptr : ^(^ ($ Type)) ) -> ( node : ^Type ) {
|
||||
list : ^Type = (list_ptr^)
|
||||
(list_ptr^) = list.next
|
||||
return list
|
||||
}
|
||||
|
||||
//region Intrusive Doubly-Linked-List
|
||||
|
||||
DLL_Node :: struct ( $ Type : typeid ) #raw_union {
|
||||
using _ : struct {
|
||||
left, right : ^Type,
|
||||
},
|
||||
using _ : struct {
|
||||
prev, next : ^Type,
|
||||
},
|
||||
using _ : struct {
|
||||
first, last : ^Type,
|
||||
},
|
||||
using _ : struct {
|
||||
bottom, top : ^Type,
|
||||
}
|
||||
}
|
||||
|
||||
DLL_NodeFull :: struct ( $ Type : typeid ) {
|
||||
// using _ : DLL_NodeFL(Type),
|
||||
first, last : ^Type,
|
||||
prev, next : ^Type,
|
||||
}
|
||||
|
||||
DLL_NodePN :: struct ( $ Type : typeid ) {
|
||||
// using _ : struct {
|
||||
prev, next : ^Type,
|
||||
// },
|
||||
// using _ : struct {
|
||||
// left, right : ^Type,
|
||||
// },
|
||||
}
|
||||
|
||||
DLL_NodeFL :: struct ( $ Type : typeid ) {
|
||||
// using _ : struct {
|
||||
first, last : ^Type,
|
||||
// },
|
||||
|
||||
// TODO(Ed): Review this
|
||||
// using _ : struct {
|
||||
// bottom, top: ^Type,
|
||||
// },
|
||||
}
|
||||
|
||||
type_is_node :: #force_inline proc "contextless" ( $ Type : typeid ) -> bool
|
||||
{
|
||||
// elem_type := type_elem_type(Type)
|
||||
return type_has_field( type_elem_type(Type), "prev" ) && type_has_field( type_elem_type(Type), "next" )
|
||||
}
|
||||
|
||||
// First/Last append
|
||||
dll_fl_append :: proc ( list : ^( $TypeList), node : ^( $TypeNode) )
|
||||
{
|
||||
if list.first == nil {
|
||||
list.first = node
|
||||
list.last = node
|
||||
}
|
||||
else {
|
||||
list.last = node
|
||||
}
|
||||
}
|
||||
|
||||
dll_push_back :: proc "contextless" ( current_ptr : ^(^ ($ TypeCurr)), node : ^$TypeNode )
|
||||
{
|
||||
current : ^TypeCurr = (current_ptr ^)
|
||||
|
||||
if current == nil
|
||||
{
|
||||
(current_ptr ^) = node
|
||||
node.prev = nil
|
||||
}
|
||||
else
|
||||
{
|
||||
node.prev = current
|
||||
(current_ptr^) = node
|
||||
current.next = node
|
||||
}
|
||||
|
||||
node.next = nil
|
||||
}
|
||||
|
||||
dll_pn_pop :: proc "contextless" ( node : ^$Type )
|
||||
{
|
||||
if node == nil {
|
||||
return
|
||||
}
|
||||
if node.prev != nil {
|
||||
node.prev.next = nil
|
||||
node.prev = nil
|
||||
}
|
||||
if node.next != nil {
|
||||
node.next.prev = nil
|
||||
node.next = nil
|
||||
}
|
||||
}
|
||||
|
||||
dll_pop_back :: #force_inline proc "contextless" ( current_ptr : ^(^ ($ Type)) )
|
||||
{
|
||||
to_remove : ^Type = (current_ptr ^)
|
||||
if to_remove == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if to_remove.prev == nil {
|
||||
(current_ptr ^) = nil
|
||||
}
|
||||
else {
|
||||
(current_ptr ^) = to_remove.prev
|
||||
(current_ptr ^).next = nil
|
||||
}
|
||||
}
|
||||
|
||||
dll_full_insert_raw :: proc "contextless" ( null : ^($ Type), parent : ^$ParentType, pos, node : ^Type )
|
||||
{
|
||||
if parent.first == null {
|
||||
parent.first = node
|
||||
parent.last = node
|
||||
node.next = null
|
||||
node.prev = null
|
||||
}
|
||||
else if pos == null {
|
||||
// Position is not set, insert at beginning
|
||||
node.next = parent.first
|
||||
parent.first.prev = node
|
||||
parent.first = node
|
||||
node.prev = null
|
||||
}
|
||||
else if pos == parent.last {
|
||||
// Positin is set to last, insert at end
|
||||
parent.last.next = node
|
||||
node.prev = parent.last
|
||||
parent.last = node
|
||||
node.next = null
|
||||
}
|
||||
else
|
||||
{
|
||||
if pos.next != null {
|
||||
pos.next.prev = node
|
||||
}
|
||||
node.next = pos.next
|
||||
pos.next = node
|
||||
node.prev = pos
|
||||
}
|
||||
}
|
||||
|
||||
dll_full_pop :: proc "contextless" ( node : ^$NodeType, parent : ^$ParentType ) {
|
||||
if node == nil {
|
||||
return
|
||||
}
|
||||
if parent.first == node {
|
||||
parent.first = node.next
|
||||
}
|
||||
if parent.last == node {
|
||||
parent.last = node.prev
|
||||
}
|
||||
prev := node.prev
|
||||
next := node.next
|
||||
if prev != nil {
|
||||
prev.next = next
|
||||
node.prev = nil
|
||||
}
|
||||
if next != nil {
|
||||
next.prev = prev
|
||||
node.next = nil
|
||||
}
|
||||
}
|
||||
|
||||
dll_full_push_back :: proc "contextless" ( parent : ^$ParentType, node : ^$Type, null : ^Type ) {
|
||||
dll_full_insert_raw( null, parent, parent.last, node )
|
||||
}
|
||||
|
||||
//endregion Intrusive Doubly-Linked-List
|
91
code/sectr/grime/memory.odin
Normal file
91
code/sectr/grime/memory.odin
Normal file
@ -0,0 +1,91 @@
|
||||
// TODO(Ed) : Move this to a grime package problably
|
||||
package sectr
|
||||
|
||||
import "core:fmt"
|
||||
import "core:mem"
|
||||
import "core:mem/virtual"
|
||||
import "base:runtime"
|
||||
import "core:os"
|
||||
|
||||
kilobytes :: #force_inline proc "contextless" ( kb : $ integer_type ) -> integer_type {
|
||||
return kb * Kilobyte
|
||||
}
|
||||
megabytes :: #force_inline proc "contextless" ( mb : $ integer_type ) -> integer_type {
|
||||
return mb * Megabyte
|
||||
}
|
||||
gigabytes :: #force_inline proc "contextless" ( gb : $ integer_type ) -> integer_type {
|
||||
return gb * Gigabyte
|
||||
}
|
||||
terabytes :: #force_inline proc "contextless" ( tb : $ integer_type ) -> integer_type {
|
||||
return tb * Terabyte
|
||||
}
|
||||
|
||||
//region Memory Math
|
||||
|
||||
// See: core/mem.odin, I wanted to study it an didn't like the naming.
|
||||
@(require_results)
|
||||
calc_padding_with_header :: proc "contextless" (pointer: uintptr, alignment: uintptr, header_size: int) -> int
|
||||
{
|
||||
alignment_offset := pointer & (alignment - 1)
|
||||
|
||||
initial_padding := uintptr(0)
|
||||
if alignment_offset != 0 {
|
||||
initial_padding = alignment - alignment_offset
|
||||
}
|
||||
|
||||
header_space_adjustment := uintptr(header_size)
|
||||
if initial_padding < header_space_adjustment
|
||||
{
|
||||
additional_space_needed := header_space_adjustment - initial_padding
|
||||
unaligned_extra_space := additional_space_needed & (alignment - 1)
|
||||
|
||||
if unaligned_extra_space > 0 {
|
||||
initial_padding += alignment * (1 + (additional_space_needed / alignment))
|
||||
}
|
||||
else {
|
||||
initial_padding += alignment * (additional_space_needed / alignment)
|
||||
}
|
||||
}
|
||||
|
||||
return int(initial_padding)
|
||||
}
|
||||
|
||||
// Helper to get the the beginning of memory after a slice
|
||||
memory_after :: #force_inline proc "contextless" ( slice : []byte ) -> ( ^ byte) {
|
||||
return ptr_offset( & slice[0], len(slice) )
|
||||
}
|
||||
|
||||
memory_after_header :: #force_inline proc "contextless" ( header : ^($ Type) ) -> ( [^]byte) {
|
||||
result := cast( [^]byte) ptr_offset( header, 1 )
|
||||
// result := cast( [^]byte) (cast( [^]Type) header)[ 1:]
|
||||
return result
|
||||
}
|
||||
|
||||
@(require_results)
|
||||
memory_align_formula :: #force_inline proc "contextless" ( size, align : uint) -> uint {
|
||||
result := size + align - 1
|
||||
return result - result % align
|
||||
}
|
||||
|
||||
// This is here just for docs
|
||||
memory_misalignment :: #force_inline proc ( address, alignment : uintptr) -> uint {
|
||||
// address % alignment
|
||||
assert(is_power_of_two(alignment))
|
||||
return uint( address & (alignment - 1) )
|
||||
}
|
||||
|
||||
// This is here just for docs
|
||||
@(require_results)
|
||||
memory_aign_forward :: #force_inline proc( address, alignment : uintptr) -> uintptr
|
||||
{
|
||||
assert(is_power_of_two(alignment))
|
||||
|
||||
aligned_address := address
|
||||
misalignment := cast(uintptr) memory_misalignment( address, alignment )
|
||||
if misalignment != 0 {
|
||||
aligned_address += alignment - misalignment
|
||||
}
|
||||
return aligned_address
|
||||
}
|
||||
|
||||
//endregion Memory Math
|
172
code/sectr/grime/memory_tracker.odin
Normal file
172
code/sectr/grime/memory_tracker.odin
Normal file
@ -0,0 +1,172 @@
|
||||
/*
|
||||
This was a tracking allocator made to kill off various bugs left with grime's pool & slab allocators
|
||||
It doesn't perform that well on a per-frame basis and should be avoided for general memory debugging
|
||||
|
||||
It only makes sure that memory allocations don't collide in the allocator and deallocations don't occur for memory never allocated.
|
||||
|
||||
I'm keeping it around as an artifact & for future allocators I may make.
|
||||
*/
|
||||
package sectr
|
||||
|
||||
MemoryTrackerEntry :: struct {
|
||||
start, end : rawptr,
|
||||
}
|
||||
|
||||
MemoryTracker :: struct {
|
||||
name : string,
|
||||
entries : Array(MemoryTrackerEntry),
|
||||
}
|
||||
|
||||
Track_Memory :: false
|
||||
|
||||
tracker_msg_buffer : [Kilobyte * 16]u8
|
||||
|
||||
memtracker_clear :: proc ( tracker : MemoryTracker ) {
|
||||
when ! Track_Memory {
|
||||
return
|
||||
}
|
||||
temp_arena : Arena; arena_init(& temp_arena, tracker_msg_buffer[:])
|
||||
context.temp_allocator = arena_allocator(& temp_arena)
|
||||
|
||||
logf("Clearing tracker: %v", tracker.name)
|
||||
memtracker_dump_entries(tracker);
|
||||
array_clear(tracker.entries)
|
||||
}
|
||||
|
||||
memtracker_init :: proc ( tracker : ^MemoryTracker, allocator : Allocator, num_entries : u64, name : string )
|
||||
{
|
||||
when ! Track_Memory {
|
||||
return
|
||||
}
|
||||
temp_arena : Arena; arena_init(& temp_arena, tracker_msg_buffer[:])
|
||||
context.temp_allocator = arena_allocator(& temp_arena)
|
||||
|
||||
tracker.name = name
|
||||
|
||||
error : AllocatorError
|
||||
tracker.entries, error = array_init_reserve( MemoryTrackerEntry, allocator, num_entries, dbg_name = name )
|
||||
if error != AllocatorError.None {
|
||||
fatal("Failed to allocate memory tracker's hashmap");
|
||||
}
|
||||
}
|
||||
|
||||
memtracker_register :: proc( tracker : ^MemoryTracker, new_entry : MemoryTrackerEntry )
|
||||
{
|
||||
when ! Track_Memory {
|
||||
return
|
||||
}
|
||||
profile(#procedure)
|
||||
temp_arena : Arena; arena_init(& temp_arena, tracker_msg_buffer[:])
|
||||
context.temp_allocator = arena_allocator(& temp_arena)
|
||||
|
||||
if tracker.entries.num == tracker.entries.capacity {
|
||||
ensure(false, "Memory tracker entries array full, can no longer register any more allocations")
|
||||
return
|
||||
}
|
||||
|
||||
for idx in 0..< tracker.entries.num
|
||||
{
|
||||
entry := & tracker.entries.data[idx]
|
||||
if new_entry.start > entry.start {
|
||||
continue
|
||||
}
|
||||
|
||||
if (entry.end < new_entry.start)
|
||||
{
|
||||
msg := str_fmt_tmp("Memory tracker(%v) detected a collision:\nold_entry: %v\nnew_entry: %v", tracker.name, entry, new_entry)
|
||||
ensure( false, msg )
|
||||
memtracker_dump_entries(tracker ^)
|
||||
}
|
||||
array_append_at( & tracker.entries, new_entry, idx )
|
||||
log(str_fmt_tmp("%v : Registered: %v", tracker.name, new_entry) )
|
||||
return
|
||||
}
|
||||
|
||||
array_append( & tracker.entries, new_entry )
|
||||
log(str_fmt_tmp("%v : Registered: %v", tracker.name, new_entry) )
|
||||
}
|
||||
|
||||
memtracker_register_auto_name :: proc( tracker : ^MemoryTracker, start, end : rawptr )
|
||||
{
|
||||
when ! Track_Memory {
|
||||
return
|
||||
}
|
||||
memtracker_register( tracker, {start, end})
|
||||
}
|
||||
|
||||
memtracker_register_auto_name_slice :: proc( tracker : ^MemoryTracker, slice : []byte )
|
||||
{
|
||||
when ! Track_Memory {
|
||||
return
|
||||
}
|
||||
start := raw_data(slice)
|
||||
end := & slice[ len(slice) - 1 ]
|
||||
memtracker_register( tracker, {start, end})
|
||||
}
|
||||
|
||||
memtracker_unregister :: proc( tracker : MemoryTracker, to_remove : MemoryTrackerEntry )
|
||||
{
|
||||
when ! Track_Memory {
|
||||
return
|
||||
}
|
||||
profile(#procedure)
|
||||
temp_arena : Arena; arena_init(& temp_arena, tracker_msg_buffer[:])
|
||||
context.temp_allocator = arena_allocator(& temp_arena)
|
||||
|
||||
entries := array_to_slice(tracker.entries)
|
||||
for idx in 0..< tracker.entries.num
|
||||
{
|
||||
entry := & entries[idx]
|
||||
if entry.start == to_remove.start {
|
||||
if (entry.end == to_remove.end || to_remove.end == nil) {
|
||||
log(str_fmt_tmp("%v: Unregistered: %v", tracker.name, to_remove));
|
||||
array_remove_at(tracker.entries, idx)
|
||||
return
|
||||
}
|
||||
|
||||
ensure(false, str_fmt_tmp("%v: Found an entry with the same start address but end address was different:\nentry : %v\nto_remove: %v", tracker.name, entry, to_remove))
|
||||
memtracker_dump_entries(tracker)
|
||||
}
|
||||
}
|
||||
|
||||
ensure(false, str_fmt_tmp("%v: Attempted to unregister an entry that was not tracked: %v", tracker.name, to_remove))
|
||||
memtracker_dump_entries(tracker)
|
||||
}
|
||||
|
||||
memtracker_check_for_collisions :: proc ( tracker : MemoryTracker )
|
||||
{
|
||||
when ! Track_Memory {
|
||||
return
|
||||
}
|
||||
profile(#procedure)
|
||||
temp_arena : Arena; arena_init(& temp_arena, tracker_msg_buffer[:])
|
||||
context.temp_allocator = arena_allocator(& temp_arena)
|
||||
|
||||
entries := array_to_slice(tracker.entries)
|
||||
for idx in 1 ..< tracker.entries.num {
|
||||
// Check to make sure each allocations adjacent entries do not intersect
|
||||
left := & entries[idx - 1]
|
||||
right := & entries[idx]
|
||||
|
||||
collided := left.start > right.start || left.end > right.end
|
||||
if collided {
|
||||
msg := str_fmt_tmp("%v: Memory tracker detected a collision:\nleft: %v\nright: %v", tracker.name, left, right)
|
||||
memtracker_dump_entries(tracker)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
memtracker_dump_entries :: proc( tracker : MemoryTracker )
|
||||
{
|
||||
when ! Track_Memory {
|
||||
return
|
||||
}
|
||||
temp_arena : Arena; arena_init(& temp_arena, tracker_msg_buffer[:])
|
||||
context.temp_allocator = arena_allocator(& temp_arena)
|
||||
|
||||
log( "Dumping Memory Tracker:")
|
||||
for idx in 0 ..< tracker.entries.num {
|
||||
entry := & tracker.entries.data[idx]
|
||||
log( str_fmt_tmp("%v", entry) )
|
||||
}
|
||||
}
|
361
code/sectr/grime/pool_allocator.odin
Normal file
361
code/sectr/grime/pool_allocator.odin
Normal file
@ -0,0 +1,361 @@
|
||||
/*
|
||||
This is a pool allocator setup to grow incrementally via buckets.
|
||||
Buckets are stored in singly-linked lists so that allocations aren't necessrily contiguous.
|
||||
|
||||
The pool is setup with the intention to only grab single entires from the bucket,
|
||||
not for a contiguous array of them.
|
||||
Thus the free-list only tracks the last free entries thrown out by the user,
|
||||
irrespective of the bucket the originated from.
|
||||
This means if there is a heavy recyling of entires in a pool
|
||||
there can be a large discrepancy of memory localicty if buckets are small.
|
||||
|
||||
The pool doesn't allocate any buckets on initialization unless the user specifies.
|
||||
*/
|
||||
package sectr
|
||||
|
||||
import "base:intrinsics"
|
||||
import "base:runtime"
|
||||
import "core:mem"
|
||||
import "core:slice"
|
||||
|
||||
Pool :: struct {
|
||||
using header : ^PoolHeader,
|
||||
}
|
||||
|
||||
PoolHeader :: struct {
|
||||
backing : Allocator,
|
||||
dbg_name : string,
|
||||
tracker : MemoryTracker,
|
||||
|
||||
zero_bucket : b32,
|
||||
block_size : uint,
|
||||
bucket_capacity : uint,
|
||||
alignment : uint,
|
||||
|
||||
free_list_head : ^Pool_FreeBlock,
|
||||
current_bucket : ^PoolBucket,
|
||||
bucket_list : DLL_NodeFL( PoolBucket),
|
||||
}
|
||||
|
||||
PoolBucket :: struct {
|
||||
using nodes : DLL_NodePN( PoolBucket),
|
||||
next_block : uint,
|
||||
blocks : [^]byte,
|
||||
}
|
||||
|
||||
Pool_FreeBlock :: struct {
|
||||
next : ^Pool_FreeBlock,
|
||||
}
|
||||
|
||||
Pool_Check_Release_Object_Validity :: true
|
||||
|
||||
pool_init :: proc (
|
||||
should_zero_buckets : b32,
|
||||
block_size : uint,
|
||||
bucket_capacity : uint,
|
||||
bucket_reserve_num : uint = 0,
|
||||
alignment : uint = mem.DEFAULT_ALIGNMENT,
|
||||
allocator : Allocator = context.allocator,
|
||||
dbg_name : string,
|
||||
) -> ( pool : Pool, alloc_error : AllocatorError )
|
||||
{
|
||||
header_size := align_forward_int( size_of(PoolHeader), int(alignment) )
|
||||
|
||||
raw_mem : rawptr
|
||||
raw_mem, alloc_error = alloc( header_size, int(alignment), allocator )
|
||||
if alloc_error != .None do return
|
||||
|
||||
ensure(block_size > 0, "Bad block size provided")
|
||||
ensure(bucket_capacity > 0, "Bad bucket capacity provided")
|
||||
|
||||
pool.header = cast( ^PoolHeader) raw_mem
|
||||
pool.zero_bucket = should_zero_buckets
|
||||
pool.backing = allocator
|
||||
pool.dbg_name = dbg_name
|
||||
pool.block_size = align_forward_uint(block_size, alignment)
|
||||
pool.bucket_capacity = bucket_capacity
|
||||
pool.alignment = alignment
|
||||
|
||||
when ODIN_DEBUG {
|
||||
memtracker_init( & pool.tracker, allocator, Kilobyte * 96, dbg_name )
|
||||
}
|
||||
|
||||
if bucket_reserve_num > 0 {
|
||||
alloc_error = pool_allocate_buckets( pool, bucket_reserve_num )
|
||||
}
|
||||
|
||||
pool.current_bucket = pool.bucket_list.first
|
||||
return
|
||||
}
|
||||
|
||||
pool_reload :: proc( pool : Pool, allocator : Allocator ) {
|
||||
pool.backing = allocator
|
||||
}
|
||||
|
||||
pool_destroy :: proc ( using self : Pool )
|
||||
{
|
||||
if bucket_list.first != nil
|
||||
{
|
||||
bucket := bucket_list.first
|
||||
for ; bucket != nil; bucket = bucket.next {
|
||||
free( bucket, backing )
|
||||
}
|
||||
}
|
||||
|
||||
free( self.header, backing )
|
||||
|
||||
when ODIN_DEBUG {
|
||||
memtracker_clear( self.tracker )
|
||||
}
|
||||
}
|
||||
|
||||
pool_allocate_buckets :: proc( pool : Pool, num_buckets : uint ) -> AllocatorError
|
||||
{
|
||||
profile(#procedure)
|
||||
if num_buckets == 0 {
|
||||
return .Invalid_Argument
|
||||
}
|
||||
header_size := cast(uint) align_forward_int( size_of(PoolBucket), int(pool.alignment))
|
||||
bucket_size := header_size + pool.bucket_capacity
|
||||
to_allocate := cast(int) (bucket_size * num_buckets)
|
||||
|
||||
// log(str_fmt_tmp("Allocating %d bytes for %d buckets with header_size %d bytes & bucket_size %d", to_allocate, num_buckets, header_size, bucket_size ))
|
||||
|
||||
bucket_memory : []byte
|
||||
alloc_error : AllocatorError
|
||||
|
||||
pool_validate( pool )
|
||||
if pool.zero_bucket {
|
||||
bucket_memory, alloc_error = alloc_bytes( to_allocate, int(pool.alignment), pool.backing )
|
||||
}
|
||||
else {
|
||||
bucket_memory, alloc_error = alloc_bytes_non_zeroed( to_allocate, int(pool.alignment), pool.backing )
|
||||
}
|
||||
pool_validate( pool )
|
||||
|
||||
// log(str_fmt_tmp("Bucket memory size: %d bytes, without header: %d", len(bucket_memory), len(bucket_memory) - int(header_size)))
|
||||
|
||||
if alloc_error != .None {
|
||||
return alloc_error
|
||||
}
|
||||
verify( bucket_memory != nil, "Bucket memory is null")
|
||||
|
||||
next_bucket_ptr := cast( [^]byte) raw_data(bucket_memory)
|
||||
for index in 0 ..< num_buckets
|
||||
{
|
||||
bucket := cast( ^PoolBucket) next_bucket_ptr
|
||||
bucket.blocks = memory_after_header(bucket)
|
||||
bucket.next_block = 0
|
||||
// log( str_fmt_tmp("\tPool (%d) allocated bucket: %p start %p capacity: %d (raw: %d)",
|
||||
// pool.block_size,
|
||||
// raw_data(bucket_memory),
|
||||
// bucket.blocks,
|
||||
// pool.bucket_capacity / pool.block_size,
|
||||
// pool.bucket_capacity ))
|
||||
|
||||
if pool.bucket_list.first == nil {
|
||||
pool.bucket_list.first = bucket
|
||||
pool.bucket_list.last = bucket
|
||||
}
|
||||
else {
|
||||
dll_push_back( & pool.bucket_list.last, bucket )
|
||||
}
|
||||
// log( str_fmt_tmp("Bucket List First: %p", self.bucket_list.first))
|
||||
|
||||
next_bucket_ptr = next_bucket_ptr[ bucket_size: ]
|
||||
}
|
||||
return alloc_error
|
||||
}
|
||||
|
||||
pool_grab :: proc( pool : Pool, zero_memory := false ) -> ( block : []byte, alloc_error : AllocatorError )
|
||||
{
|
||||
pool := pool
|
||||
if pool.current_bucket != nil {
|
||||
if ( pool.current_bucket.blocks == nil ) {
|
||||
ensure( false, str_fmt_tmp("(corruption) current_bucket was wiped %p", pool.current_bucket) )
|
||||
}
|
||||
// verify( pool.current_bucket.blocks != nil, str_fmt_tmp("(corruption) current_bucket was wiped %p", pool.current_bucket) )
|
||||
}
|
||||
// profile(#procedure)
|
||||
alloc_error = .None
|
||||
|
||||
// Check the free-list first for a block
|
||||
if pool.free_list_head != nil
|
||||
{
|
||||
head := & pool.free_list_head
|
||||
|
||||
// Compiler Bug? Fails to compile
|
||||
// last_free := ll_pop( & pool.free_list_head )
|
||||
last_free : ^Pool_FreeBlock = pool.free_list_head
|
||||
pool.free_list_head = pool.free_list_head.next
|
||||
|
||||
block = byte_slice( cast([^]byte) last_free, int(pool.block_size) )
|
||||
// log( str_fmt_tmp("\tReturning free block: %p %d", raw_data(block), pool.block_size))
|
||||
if zero_memory {
|
||||
slice.zero(block)
|
||||
}
|
||||
|
||||
when ODIN_DEBUG {
|
||||
memtracker_register_auto_name_slice( & pool.tracker, block)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if pool.current_bucket == nil
|
||||
{
|
||||
alloc_error = pool_allocate_buckets( pool, 1 )
|
||||
if alloc_error != .None {
|
||||
ensure(false, "Failed to allocate bucket")
|
||||
return
|
||||
}
|
||||
pool.current_bucket = pool.bucket_list.first
|
||||
// log( "First bucket allocation")
|
||||
}
|
||||
|
||||
next := uintptr(pool.current_bucket.blocks) + uintptr(pool.current_bucket.next_block)
|
||||
end := uintptr(pool.current_bucket.blocks) + uintptr(pool.bucket_capacity)
|
||||
|
||||
blocks_left, overflow_signal := intrinsics.overflow_sub( end, next )
|
||||
if blocks_left == 0 || overflow_signal
|
||||
{
|
||||
// Compiler Bug
|
||||
// if current_bucket.next != nil {
|
||||
if pool.current_bucket.next != nil {
|
||||
// current_bucket = current_bucket.next
|
||||
// log( str_fmt_tmp("\tBucket %p exhausted using %p", pool.current_bucket, pool.current_bucket.next))
|
||||
pool.current_bucket = pool.current_bucket.next
|
||||
verify( pool.current_bucket.blocks != nil, "New current_bucket's blocks are null (new current_bucket is corrupted)" )
|
||||
}
|
||||
else
|
||||
{
|
||||
// log( "\tAll previous buckets exhausted, allocating new bucket")
|
||||
alloc_error := pool_allocate_buckets( pool, 1 )
|
||||
if alloc_error != .None {
|
||||
ensure(false, "Failed to allocate bucket")
|
||||
return
|
||||
}
|
||||
pool.current_bucket = pool.current_bucket.next
|
||||
verify( pool.current_bucket.blocks != nil, "Next's blocks are null (Post new bucket alloc)" )
|
||||
}
|
||||
}
|
||||
|
||||
verify( pool.current_bucket != nil, "Attempted to grab a block from a null bucket reference" )
|
||||
|
||||
// Compiler Bug
|
||||
// block = slice_ptr( current_bucket.blocks[ current_bucket.next_block:], int(block_size) )
|
||||
// self.current_bucket.next_block += block_size
|
||||
|
||||
block_ptr := cast(rawptr) (uintptr(pool.current_bucket.blocks) + uintptr(pool.current_bucket.next_block))
|
||||
|
||||
block = byte_slice( block_ptr, int(pool.block_size) )
|
||||
pool.current_bucket.next_block += pool.block_size
|
||||
|
||||
next = uintptr(pool.current_bucket.blocks) + uintptr(pool.current_bucket.next_block)
|
||||
// log( str_fmt_tmp("\tgrabbing block: %p from %p blocks left: %d", raw_data(block), pool.current_bucket.blocks, (end - next) / uintptr(pool.block_size) ))
|
||||
|
||||
if zero_memory {
|
||||
slice.zero(block)
|
||||
// log( str_fmt_tmp("Zeroed memory - Range(%p to %p)", block_ptr, cast(rawptr) (uintptr(block_ptr) + uintptr(pool.block_size))))
|
||||
}
|
||||
|
||||
when ODIN_DEBUG {
|
||||
memtracker_register_auto_name_slice( & pool.tracker, block)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
pool_release :: proc( self : Pool, block : []byte, loc := #caller_location )
|
||||
{
|
||||
// profile(#procedure)
|
||||
if Pool_Check_Release_Object_Validity {
|
||||
within_bucket := pool_validate_ownership( self, block )
|
||||
verify( within_bucket, "Attempted to release data that is not within a bucket of this pool", location = loc )
|
||||
}
|
||||
|
||||
// Compiler bug
|
||||
// ll_push( & self.free_list_head, cast(^Pool_FreeBlock) raw_data(block) )
|
||||
|
||||
pool_watch := self
|
||||
head_watch := & self.free_list_head
|
||||
|
||||
// ll_push:
|
||||
new_free_block := cast(^Pool_FreeBlock) raw_data(block)
|
||||
(new_free_block ^) = {}
|
||||
new_free_block.next = self.free_list_head
|
||||
self.free_list_head = new_free_block
|
||||
|
||||
// new_free_block = new_free_block
|
||||
// log( str_fmt_tmp("Released block: %p %d", new_free_block, self.block_size))
|
||||
|
||||
start := new_free_block
|
||||
end := transmute(rawptr) (uintptr(new_free_block) + uintptr(self.block_size) - 1)
|
||||
when ODIN_DEBUG {
|
||||
memtracker_unregister( self.tracker, { start, end } )
|
||||
}
|
||||
}
|
||||
|
||||
pool_reset :: proc( using pool : Pool )
|
||||
{
|
||||
bucket : ^PoolBucket = bucket_list.first // TODO(Ed): Compiler bug? Build fails unless ^PoolBucket is explcitly specified.
|
||||
for ; bucket != nil; {
|
||||
bucket.next_block = 0
|
||||
}
|
||||
|
||||
pool.free_list_head = nil
|
||||
pool.current_bucket = bucket_list.first
|
||||
}
|
||||
|
||||
pool_validate :: proc( pool : Pool )
|
||||
{
|
||||
when !ODIN_DEBUG do return
|
||||
pool := pool
|
||||
// Make sure all buckets don't show any indication of corruption
|
||||
bucket : ^PoolBucket = pool.bucket_list.first
|
||||
|
||||
if bucket != nil && uintptr(bucket) < 0x10000000000 {
|
||||
ensure(false, str_fmt_tmp("Found a corrupted bucket %p", bucket ))
|
||||
}
|
||||
// Compiler bug ^^ same as pool_reset
|
||||
for ; bucket != nil; bucket = bucket.next
|
||||
{
|
||||
if bucket != nil && uintptr(bucket) < 0x10000000000 {
|
||||
ensure(false, str_fmt_tmp("Found a corrupted bucket %p", bucket ))
|
||||
}
|
||||
|
||||
if ( bucket.blocks == nil ) {
|
||||
ensure(false, str_fmt_tmp("Found a corrupted bucket %p", bucket ))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pool_validate_ownership :: proc( using self : Pool, block : [] byte ) -> b32
|
||||
{
|
||||
profile(#procedure)
|
||||
within_bucket := b32(false)
|
||||
|
||||
// Compiler Bug : Same as pool_reset
|
||||
bucket : ^PoolBucket = bucket_list.first
|
||||
for ; bucket != nil; bucket = bucket.next
|
||||
{
|
||||
start := uintptr( bucket.blocks )
|
||||
end := start + uintptr(bucket_capacity)
|
||||
block_address := uintptr(raw_data(block))
|
||||
|
||||
if start <= block_address && block_address < end
|
||||
{
|
||||
misalignment := (block_address - start) % uintptr(block_size)
|
||||
if misalignment != 0 {
|
||||
ensure(false, "pool_validate_ownership: This data is within this pool's buckets, however its not aligned to the start of a block")
|
||||
log(str_fmt_tmp("Block address: %p Misalignment: %p closest: %p",
|
||||
transmute(rawptr)block_address,
|
||||
transmute(rawptr)misalignment,
|
||||
rawptr(block_address - misalignment)))
|
||||
}
|
||||
|
||||
within_bucket = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return within_bucket
|
||||
}
|
22
code/sectr/grime/profiler.odin
Normal file
22
code/sectr/grime/profiler.odin
Normal file
@ -0,0 +1,22 @@
|
||||
package sectr
|
||||
|
||||
import "base:runtime"
|
||||
import "core:prof/spall"
|
||||
|
||||
SpallProfiler :: struct {
|
||||
ctx : spall.Context,
|
||||
buffer : spall.Buffer,
|
||||
}
|
||||
|
||||
@(deferred_none=profile_end)
|
||||
profile :: #force_inline proc "contextless" ( name : string, loc := #caller_location ) {
|
||||
spall._buffer_begin( & Memory_App.profiler.ctx, & Memory_App.profiler.buffer, name, "", loc )
|
||||
}
|
||||
|
||||
profile_begin :: #force_inline proc "contextless" ( name : string, loc := #caller_location ) {
|
||||
spall._buffer_begin( & Memory_App.profiler.ctx, & Memory_App.profiler.buffer, name, "", loc )
|
||||
}
|
||||
|
||||
profile_end :: #force_inline proc "contextless" () {
|
||||
spall._buffer_end( & Memory_App.profiler.ctx, & Memory_App.profiler.buffer)
|
||||
}
|
30
code/sectr/grime/ptr.odin
Normal file
30
code/sectr/grime/ptr.odin
Normal file
@ -0,0 +1,30 @@
|
||||
package sectr
|
||||
|
||||
// Provides an alternative syntax for pointers
|
||||
|
||||
Ptr :: struct( $ Type : typeid ) {
|
||||
v : Type,
|
||||
}
|
||||
|
||||
exmaple_ptr :: proc()
|
||||
{
|
||||
a, b : int
|
||||
var : ^Ptr(int)
|
||||
reg : ^int
|
||||
|
||||
a = 1
|
||||
b = 1
|
||||
|
||||
var = &{a}
|
||||
var.v = 2
|
||||
var = &{b}
|
||||
var.v = 3
|
||||
|
||||
a = 1
|
||||
b = 1
|
||||
|
||||
reg = (& a)
|
||||
(reg^) = 2
|
||||
reg = (& b)
|
||||
(reg^) = 3
|
||||
}
|
335
code/sectr/grime/slab_allocator.odin
Normal file
335
code/sectr/grime/slab_allocator.odin
Normal file
@ -0,0 +1,335 @@
|
||||
/* Slab Allocator
|
||||
These are a collection of pool allocators serving as a general way
|
||||
to allocate a large amount of dynamic sized data.
|
||||
|
||||
The usual use case for this is an arena, stack,
|
||||
or dedicated pool allocator fail to be enough to handle a data structure
|
||||
that either is too random with its size (ex: strings)
|
||||
or is intended to grow an abitrary degree with an unknown upper bound (dynamic arrays, and hashtables).
|
||||
|
||||
The protototype will use slab allocators for two purposes:
|
||||
* String interning
|
||||
* General purpose set for handling large arrays & hash tables within some underlying arena or stack.
|
||||
|
||||
Technically speaking the general purpose situations can instead be grown on demand
|
||||
with a dedicated segement of vmem, however this might be overkill
|
||||
if the worst case buckets allocated are < 500 mb for most app usage.
|
||||
|
||||
The slab allocators are expected to hold growable pool allocators,
|
||||
where each pool stores a 'bucket' of fixed-sized blocks of memory.
|
||||
When a pools bucket is full it will request another bucket from its arena
|
||||
for permanent usage within the arena's lifetime.
|
||||
|
||||
A freelist is tracked for free-blocks for each pool (provided by the underlying pool allocator)
|
||||
|
||||
A slab starts out with pools initialized with no buckets and grows as needed.
|
||||
When a slab is initialized the slab policy is provided to know how many size-classes there should be
|
||||
which each contain the ratio of bucket to block size.
|
||||
*/
|
||||
package sectr
|
||||
|
||||
import "base:runtime"
|
||||
import "core:mem"
|
||||
import "core:slice"
|
||||
|
||||
SlabSizeClass :: struct {
|
||||
bucket_capacity : uint,
|
||||
block_size : uint,
|
||||
block_alignment : uint,
|
||||
}
|
||||
|
||||
Slab_Max_Size_Classes :: 64
|
||||
|
||||
SlabPolicy :: StackFixed(SlabSizeClass, Slab_Max_Size_Classes)
|
||||
|
||||
SlabHeader :: struct {
|
||||
dbg_name : string,
|
||||
tracker : MemoryTracker,
|
||||
backing : Allocator,
|
||||
pools : StackFixed(Pool, Slab_Max_Size_Classes),
|
||||
}
|
||||
|
||||
Slab :: struct {
|
||||
using header : ^SlabHeader,
|
||||
}
|
||||
|
||||
slab_allocator :: proc( slab : Slab ) -> ( allocator : Allocator ) {
|
||||
allocator.procedure = slab_allocator_proc
|
||||
allocator.data = slab.header
|
||||
return
|
||||
}
|
||||
|
||||
slab_init :: proc( policy : ^SlabPolicy, bucket_reserve_num : uint = 0, allocator : Allocator, dbg_name : string = "", should_zero_buckets : b32 = false ) -> ( slab : Slab, alloc_error : AllocatorError )
|
||||
{
|
||||
header_size :: size_of( SlabHeader )
|
||||
|
||||
raw_mem : rawptr
|
||||
raw_mem, alloc_error = alloc( header_size, mem.DEFAULT_ALIGNMENT, allocator )
|
||||
if alloc_error != .None do return
|
||||
|
||||
slab.header = cast( ^SlabHeader) raw_mem
|
||||
slab.backing = allocator
|
||||
slab.dbg_name = dbg_name
|
||||
when ODIN_DEBUG {
|
||||
memtracker_init( & slab.tracker, allocator, Kilobyte * 256, dbg_name )
|
||||
}
|
||||
alloc_error = slab_init_pools( slab, policy, bucket_reserve_num, should_zero_buckets )
|
||||
return
|
||||
}
|
||||
|
||||
slab_init_pools :: proc ( using self : Slab, policy : ^SlabPolicy, bucket_reserve_num : uint = 0, should_zero_buckets : b32 ) -> AllocatorError
|
||||
{
|
||||
profile(#procedure)
|
||||
|
||||
for id in 0 ..< policy.idx {
|
||||
using size_class := policy.items[id]
|
||||
|
||||
pool_dbg_name := str_fmt_alloc("%v pool[%v]", dbg_name, block_size, allocator = backing)
|
||||
pool, alloc_error := pool_init( should_zero_buckets, block_size, bucket_capacity, bucket_reserve_num, block_alignment, backing, pool_dbg_name )
|
||||
if alloc_error != .None do return alloc_error
|
||||
|
||||
push( & self.pools, pool )
|
||||
}
|
||||
return .None
|
||||
}
|
||||
|
||||
slab_reload :: proc ( slab : Slab, allocator : Allocator )
|
||||
{
|
||||
slab.backing = allocator
|
||||
|
||||
for id in 0 ..< slab.pools.idx {
|
||||
pool := slab.pools.items[id]
|
||||
pool_reload( pool, slab.backing )
|
||||
}
|
||||
}
|
||||
|
||||
slab_destroy :: proc( using self : Slab )
|
||||
{
|
||||
for id in 0 ..< pools.idx {
|
||||
pool := pools.items[id]
|
||||
pool_destroy( pool )
|
||||
}
|
||||
|
||||
free( self.header, backing )
|
||||
when ODIN_DEBUG {
|
||||
memtracker_clear(tracker)
|
||||
}
|
||||
}
|
||||
|
||||
slab_alloc :: proc( self : Slab,
|
||||
size : uint,
|
||||
alignment : uint,
|
||||
zero_memory := true,
|
||||
loc := #caller_location
|
||||
) -> ( data : []byte, alloc_error : AllocatorError )
|
||||
{
|
||||
// profile(#procedure)
|
||||
pool : Pool
|
||||
id : u32 = 0
|
||||
for ; id < self.pools.idx; id += 1 {
|
||||
pool = self.pools.items[id]
|
||||
|
||||
if pool.block_size >= size && pool.alignment >= alignment {
|
||||
break
|
||||
}
|
||||
}
|
||||
verify( id < self.pools.idx, "There is not a size class in the slab's policy to satisfy the requested allocation", location = loc )
|
||||
verify( pool.header != nil, "Requested alloc not supported by the slab allocator", location = loc )
|
||||
|
||||
block : []byte
|
||||
slab_validate_pools( self )
|
||||
block, alloc_error = pool_grab(pool)
|
||||
slab_validate_pools( self )
|
||||
|
||||
if block == nil || alloc_error != .None {
|
||||
ensure(false, "Bad block from pool")
|
||||
return nil, alloc_error
|
||||
}
|
||||
// log( str_fmt_tmp("%v: Retrieved block: %p %d", self.dbg_name, raw_data(block), len(block) ))
|
||||
|
||||
data = byte_slice(raw_data(block), size)
|
||||
if zero_memory {
|
||||
slice.zero(data)
|
||||
}
|
||||
|
||||
when ODIN_DEBUG {
|
||||
memtracker_register_auto_name( & self.tracker, raw_data(block), & block[ len(block) - 1 ] )
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
slab_free :: proc( using self : Slab, data : []byte, loc := #caller_location )
|
||||
{
|
||||
// profile(#procedure)
|
||||
pool : Pool
|
||||
for id in 0 ..< pools.idx
|
||||
{
|
||||
pool = pools.items[id]
|
||||
if pool_validate_ownership( pool, data ) {
|
||||
start := raw_data(data)
|
||||
end := ptr_offset(start, pool.block_size - 1)
|
||||
|
||||
when ODIN_DEBUG {
|
||||
memtracker_unregister( self.tracker, { start, end } )
|
||||
}
|
||||
|
||||
pool_release( pool, data, loc )
|
||||
return
|
||||
}
|
||||
}
|
||||
verify(false, "Attempted to free a block not within a pool of this slab", location = loc)
|
||||
}
|
||||
|
||||
slab_resize :: proc( using self : Slab,
|
||||
data : []byte,
|
||||
new_size : uint,
|
||||
alignment : uint,
|
||||
zero_memory := true,
|
||||
loc := #caller_location
|
||||
) -> ( new_data : []byte, alloc_error : AllocatorError )
|
||||
{
|
||||
// profile(#procedure)
|
||||
old_size := uint( len(data))
|
||||
|
||||
pool_resize, pool_old : Pool
|
||||
for id in 0 ..< pools.idx
|
||||
{
|
||||
pool := pools.items[id]
|
||||
|
||||
if pool.block_size >= new_size && pool.alignment >= alignment {
|
||||
pool_resize = pool
|
||||
}
|
||||
if pool_validate_ownership( pool, data ) {
|
||||
pool_old = pool
|
||||
}
|
||||
if pool_resize.header != nil && pool_old.header != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
verify( pool_resize.header != nil, "Requested resize not supported by the slab allocator", location = loc )
|
||||
|
||||
// Resize will keep block in the same size_class, just give it more of its already allocated block
|
||||
if pool_old.block_size == pool_resize.block_size
|
||||
{
|
||||
new_data_ptr := memory_after(data)
|
||||
new_data = byte_slice( raw_data(data), new_size )
|
||||
// log( dump_stacktrace() )
|
||||
// log( str_fmt_tmp("%v: Resize via expanding block space allocation %p %d", dbg_name, new_data_ptr, int(new_size - old_size)))
|
||||
|
||||
if zero_memory && new_size > old_size {
|
||||
to_zero := byte_slice( new_data_ptr, int(new_size - old_size) )
|
||||
|
||||
slab_validate_pools( self )
|
||||
slice.zero( to_zero )
|
||||
slab_validate_pools( self )
|
||||
|
||||
// log( str_fmt_tmp("Zeroed memory - Range(%p to %p)", new_data_ptr, cast(rawptr) (uintptr(new_data_ptr) + uintptr(new_size - old_size))))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// We'll need to provide an entirely new block, so the data will need to be copied over.
|
||||
new_block : []byte
|
||||
|
||||
slab_validate_pools( self )
|
||||
new_block, alloc_error = pool_grab( pool_resize )
|
||||
slab_validate_pools( self )
|
||||
|
||||
if new_block == nil {
|
||||
ensure(false, "Retreived a null block")
|
||||
return
|
||||
}
|
||||
|
||||
if alloc_error != .None do return
|
||||
|
||||
// TODO(Ed): Reapply this when safe.
|
||||
if zero_memory {
|
||||
slice.zero( new_block )
|
||||
// log( str_fmt_tmp("Zeroed memory - Range(%p to %p)", raw_data(new_block), cast(rawptr) (uintptr(raw_data(new_block)) + uintptr(new_size))))
|
||||
}
|
||||
|
||||
// log( str_fmt_tmp("Resize via new block: %p %d (old : %p $d )", raw_data(new_block), len(new_block), raw_data(data), old_size ))
|
||||
|
||||
if raw_data(data) != raw_data(new_block) {
|
||||
// log( str_fmt_tmp("%v: Resize via new block, copying from old data block to new block: (%p %d), (%p %d)", dbg_name, raw_data(data), len(data), raw_data(new_block), len(new_block)))
|
||||
copy_non_overlapping( raw_data(new_block), raw_data(data), int(old_size) )
|
||||
pool_release( pool_old, data )
|
||||
|
||||
start := raw_data( data )
|
||||
end := rawptr(uintptr(start) + uintptr(pool_old.block_size) - 1)
|
||||
|
||||
when ODIN_DEBUG {
|
||||
memtracker_unregister( self.tracker, { start, end } )
|
||||
}
|
||||
}
|
||||
|
||||
new_data = new_block[ :new_size]
|
||||
when ODIN_DEBUG {
|
||||
memtracker_register_auto_name( & self.tracker, raw_data(new_block), & new_block[ len(new_block) - 1 ] )
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
slab_reset :: proc( slab : Slab )
|
||||
{
|
||||
for id in 0 ..< slab.pools.idx {
|
||||
pool := slab.pools.items[id]
|
||||
pool_reset( pool )
|
||||
}
|
||||
when ODIN_DEBUG {
|
||||
memtracker_clear(slab.tracker)
|
||||
}
|
||||
}
|
||||
|
||||
slab_validate_pools :: proc( slab : Slab )
|
||||
{
|
||||
slab := slab
|
||||
for id in 0 ..< slab.pools.idx {
|
||||
pool := slab.pools.items[id]
|
||||
pool_validate( pool )
|
||||
}
|
||||
}
|
||||
|
||||
slab_allocator_proc :: proc(
|
||||
allocator_data : rawptr,
|
||||
mode : AllocatorMode,
|
||||
size : int,
|
||||
alignment : int,
|
||||
old_memory : rawptr,
|
||||
old_size : int,
|
||||
loc := #caller_location
|
||||
) -> ( data : []byte, alloc_error : AllocatorError)
|
||||
{
|
||||
slab : Slab
|
||||
slab.header = cast( ^SlabHeader) allocator_data
|
||||
|
||||
size := uint(size)
|
||||
alignment := uint(alignment)
|
||||
old_size := uint(old_size)
|
||||
|
||||
switch mode
|
||||
{
|
||||
case .Alloc, .Alloc_Non_Zeroed:
|
||||
return slab_alloc( slab, size, alignment, (mode != .Alloc_Non_Zeroed), loc)
|
||||
|
||||
case .Free:
|
||||
slab_free( slab, byte_slice( old_memory, int(old_size)), loc )
|
||||
|
||||
case .Free_All:
|
||||
slab_reset( slab )
|
||||
|
||||
case .Resize, .Resize_Non_Zeroed:
|
||||
return slab_resize( slab, byte_slice(old_memory, int(old_size)), size, alignment, (mode != .Resize_Non_Zeroed), loc)
|
||||
|
||||
case .Query_Features:
|
||||
set := cast( ^AllocatorModeSet) old_memory
|
||||
if set != nil {
|
||||
(set ^) = {.Alloc, .Alloc_Non_Zeroed, .Free_All, .Resize, .Query_Features}
|
||||
}
|
||||
|
||||
case .Query_Info:
|
||||
alloc_error = .Mode_Not_Implemented
|
||||
}
|
||||
return
|
||||
}
|
280
code/sectr/grime/stack.odin
Normal file
280
code/sectr/grime/stack.odin
Normal file
@ -0,0 +1,280 @@
|
||||
package sectr
|
||||
|
||||
import "core:mem"
|
||||
import "core:slice"
|
||||
|
||||
//region Fixed Stack
|
||||
|
||||
StackFixed :: struct ( $ Type : typeid, $ Size : u32 ) {
|
||||
idx : u32,
|
||||
items : [ Size ] Type,
|
||||
}
|
||||
|
||||
stack_clear :: #force_inline proc ( using stack : ^StackFixed( $Type, $Size)) {
|
||||
idx = 0
|
||||
}
|
||||
|
||||
stack_push :: #force_inline proc( using stack : ^ StackFixed( $ Type, $ Size ), value : Type ) {
|
||||
verify( idx < len( items ), "Attempted to push on a full stack" )
|
||||
|
||||
items[ idx ] = value
|
||||
idx += 1
|
||||
}
|
||||
|
||||
stack_pop :: #force_inline proc( using stack : ^StackFixed( $ Type, $ Size ) ) {
|
||||
verify( idx > 0, "Attempted to pop an empty stack" )
|
||||
|
||||
idx -= 1
|
||||
if idx == 0 {
|
||||
items[idx] = {}
|
||||
}
|
||||
}
|
||||
|
||||
stack_peek_ref :: #force_inline proc "contextless" ( using stack : ^StackFixed( $ Type, $ Size ) ) -> ( ^Type) {
|
||||
last_idx := max( 0, idx - 1 ) if idx > 0 else 0
|
||||
last := & items[last_idx]
|
||||
return last
|
||||
}
|
||||
|
||||
stack_peek :: #force_inline proc "contextless" ( using stack : ^StackFixed( $ Type, $ Size ) ) -> Type {
|
||||
last := max( 0, idx - 1 ) if idx > 0 else 0
|
||||
return items[last]
|
||||
}
|
||||
|
||||
//endregion Fixed Stack
|
||||
|
||||
//region Stack Allocator
|
||||
|
||||
// TODO(Ed) : This is untested and problably filled with bugs.
|
||||
/* Growing Stack allocator
|
||||
This implementation can support growing if the backing allocator supports
|
||||
it without fragmenting the backing allocator.
|
||||
|
||||
Each block in the stack is tracked with a doubly-linked list to have debug stats.
|
||||
(It could be removed for non-debug builds)
|
||||
*/
|
||||
|
||||
StackAllocatorBase :: struct {
|
||||
backing : Allocator,
|
||||
|
||||
using links : DLL_NodeFL(StackAllocatorHeader),
|
||||
peak_used : int,
|
||||
size : int,
|
||||
data : [^]byte,
|
||||
}
|
||||
|
||||
StackAllocator :: struct {
|
||||
using base : ^StackAllocatorBase,
|
||||
}
|
||||
|
||||
StackAllocatorHeader :: struct {
|
||||
using links : DLL_NodePN(StackAllocatorHeader),
|
||||
block_size : int,
|
||||
padding : int,
|
||||
}
|
||||
|
||||
stack_allocator :: proc( using self : StackAllocator ) -> ( allocator : Allocator ) {
|
||||
allocator.procedure = stack_allocator_proc
|
||||
allocator.data = self.base
|
||||
return
|
||||
}
|
||||
|
||||
stack_allocator_init :: proc( size : int, allocator := context.allocator ) -> ( stack : StackAllocator, alloc_error : AllocatorError )
|
||||
{
|
||||
header_size := size_of(StackAllocatorBase)
|
||||
|
||||
raw_mem : rawptr
|
||||
raw_mem, alloc_error = alloc( header_size + size, mem.DEFAULT_ALIGNMENT )
|
||||
if alloc_error != AllocatorError.None do return
|
||||
|
||||
stack.base = cast( ^StackAllocatorBase) raw_mem
|
||||
stack.size = size
|
||||
stack.data = cast( [^]byte) (cast( [^]StackAllocatorBase) stack.base)[ 1:]
|
||||
|
||||
stack.last = cast(^StackAllocatorHeader) stack.data
|
||||
stack.first = stack.last
|
||||
return
|
||||
}
|
||||
|
||||
stack_allocator_destroy :: proc( using self : StackAllocator )
|
||||
{
|
||||
free( self.base, backing )
|
||||
}
|
||||
|
||||
stack_allocator_init_via_memory :: proc( memory : []byte ) -> ( stack : StackAllocator )
|
||||
{
|
||||
header_size := size_of(StackAllocatorBase)
|
||||
|
||||
if len(memory) < (header_size + Kilobyte) {
|
||||
verify(false, "Assigning a stack allocator less than a kilobyte of space")
|
||||
return
|
||||
}
|
||||
|
||||
stack.base = cast( ^StackAllocatorBase) & memory[0]
|
||||
stack.size = len(memory) - header_size
|
||||
stack.data = cast( [^]byte ) (cast( [^]StackAllocatorBase) stack.base)[ 1:]
|
||||
|
||||
stack.last = cast( ^StackAllocatorHeader) stack.data
|
||||
stack.first = stack.last
|
||||
return
|
||||
}
|
||||
|
||||
stack_allocator_push :: proc( using self : StackAllocator, block_size, alignment : int, zero_memory : bool ) -> ( []byte, AllocatorError )
|
||||
{
|
||||
// TODO(Ed): Make sure first push is fine.
|
||||
verify( block_size > Kilobyte, "Attempted to push onto the stack less than a Kilobyte")
|
||||
top_block_ptr := memory_after_header( last )
|
||||
|
||||
theoretical_size := cast(int) (uintptr(top_block_ptr) + uintptr(block_size) - uintptr(first))
|
||||
if theoretical_size > size {
|
||||
// TODO(Ed) : Check if backing allocator supports resize, if it does attempt to grow.
|
||||
return nil, .Out_Of_Memory
|
||||
}
|
||||
|
||||
top_block_slice := slice_ptr( top_block_ptr, last.block_size )
|
||||
next_spot := uintptr( top_block_ptr) + uintptr(last.block_size)
|
||||
|
||||
header_offset_pad := calc_padding_with_header( uintptr(next_spot), uintptr(alignment), size_of(StackAllocatorHeader) )
|
||||
header := cast( ^StackAllocatorHeader) (next_spot + uintptr(header_offset_pad) - uintptr(size_of( StackAllocatorHeader)))
|
||||
header.padding = header_offset_pad
|
||||
header.prev = last
|
||||
header.block_size = block_size
|
||||
|
||||
curr_block_ptr := memory_after_header( header )
|
||||
curr_block := slice_ptr( curr_block_ptr, block_size )
|
||||
|
||||
curr_used := cast(int) (uintptr(curr_block_ptr) + uintptr(block_size) - uintptr(self.last))
|
||||
self.peak_used += max( peak_used, curr_used )
|
||||
|
||||
dll_push_back( & base.links.last, header )
|
||||
|
||||
if zero_memory {
|
||||
slice.zero( curr_block )
|
||||
}
|
||||
|
||||
return curr_block, .None
|
||||
}
|
||||
|
||||
stack_allocator_resize_top :: proc( using self : StackAllocator, new_block_size, alignment : int, zero_memory : bool ) -> AllocatorError
|
||||
{
|
||||
verify( new_block_size > Kilobyte, "Attempted to resize the last pushed on the stack to less than a Kilobyte")
|
||||
top_block_ptr := memory_after_header( last )
|
||||
|
||||
theoretical_size := cast(int) (uintptr(top_block_ptr) + uintptr(last.block_size) - uintptr(first))
|
||||
if theoretical_size > size {
|
||||
// TODO(Ed) : Check if backing allocator supports resize, if it does attempt to grow.
|
||||
return .Out_Of_Memory
|
||||
}
|
||||
|
||||
if zero_memory && new_block_size > last.block_size {
|
||||
added_ptr := top_block_ptr[ last.block_size:]
|
||||
added_slice := slice_ptr( added_ptr, new_block_size - last.block_size )
|
||||
slice.zero( added_slice )
|
||||
}
|
||||
|
||||
last.block_size = new_block_size
|
||||
return .None
|
||||
}
|
||||
|
||||
stack_allocator_pop :: proc( using self : StackAllocator ) {
|
||||
base.links.last = last.prev
|
||||
base.links.last.next = nil
|
||||
}
|
||||
|
||||
|
||||
stack_allocator_proc :: proc(
|
||||
allocator_data : rawptr,
|
||||
mode : AllocatorMode,
|
||||
block_size : int,
|
||||
alignment : int,
|
||||
old_memory : rawptr,
|
||||
old_size : int,
|
||||
location : SourceCodeLocation = #caller_location
|
||||
) -> ([]byte, AllocatorError)
|
||||
{
|
||||
stack := StackAllocator { cast( ^StackAllocatorBase) allocator_data }
|
||||
|
||||
if stack.data == nil {
|
||||
return nil, AllocatorError.Invalid_Argument
|
||||
}
|
||||
|
||||
switch mode
|
||||
{
|
||||
case .Alloc, .Alloc_Non_Zeroed:
|
||||
{
|
||||
return stack_allocator_push( stack, block_size, alignment, mode == .Alloc )
|
||||
}
|
||||
case .Free:
|
||||
{
|
||||
if old_memory == nil {
|
||||
return nil, .None
|
||||
}
|
||||
|
||||
start := uintptr(stack.data)
|
||||
end := start + uintptr(block_size)
|
||||
curr_addr := uintptr(old_memory)
|
||||
|
||||
verify( start <= curr_addr && curr_addr < end, "Out of bounds memory address passed to stack allocator (free)" )
|
||||
|
||||
block_ptr := memory_after_header( stack.last )
|
||||
|
||||
if curr_addr >= start + uintptr(block_ptr) {
|
||||
return nil, .None
|
||||
}
|
||||
|
||||
dll_pop_back( & stack.last )
|
||||
}
|
||||
case .Free_All:
|
||||
// TODO(Ed) : Review that we don't have any header issues with the reset.
|
||||
stack.first = stack.last
|
||||
stack.last.next = nil
|
||||
stack.last.block_size = 0
|
||||
|
||||
case .Resize, .Resize_Non_Zeroed:
|
||||
{
|
||||
// Check if old_memory is at the first on the stack, if it is, just grow its size
|
||||
// Otherwise, log that the user cannot resize stack items that are not at the top of the stack allocated.
|
||||
if old_memory == nil {
|
||||
return stack_allocator_push(stack, block_size, alignment, mode == .Resize )
|
||||
}
|
||||
if block_size == 0 {
|
||||
return nil, .None
|
||||
}
|
||||
|
||||
start := uintptr(stack.data)
|
||||
end := start + uintptr(block_size)
|
||||
curr_addr := uintptr(old_memory)
|
||||
|
||||
verify( start <= curr_addr && curr_addr < end, "Out of bounds memory address passed to stack allocator (resize)" )
|
||||
|
||||
block_ptr := memory_after_header( stack.last )
|
||||
if block_ptr != old_memory {
|
||||
ensure( false, "Attempted to reszie a block of memory on the stack other than top most" )
|
||||
return nil, .None
|
||||
}
|
||||
|
||||
if old_size == block_size {
|
||||
return byte_slice( old_memory, block_size ), .None
|
||||
}
|
||||
|
||||
stack_allocator_resize_top( stack, block_size, alignment, mode == .Resize )
|
||||
return byte_slice( block_ptr, block_size ), .None
|
||||
}
|
||||
case .Query_Features:
|
||||
{
|
||||
feature_flags := ( ^AllocatorModeSet)(old_memory)
|
||||
if feature_flags != nil {
|
||||
(feature_flags ^) = {.Alloc, .Alloc_Non_Zeroed, .Free, .Free_All, .Resize, .Resize_Non_Zeroed, .Query_Features}
|
||||
}
|
||||
return nil, .None
|
||||
}
|
||||
case .Query_Info:
|
||||
{
|
||||
return nil, .Mode_Not_Implemented
|
||||
}
|
||||
}
|
||||
|
||||
return nil, .None
|
||||
}
|
||||
|
||||
//endregion Stack Allocator
|
11
code/sectr/grime/string_format.odin
Normal file
11
code/sectr/grime/string_format.odin
Normal file
@ -0,0 +1,11 @@
|
||||
// This provides a string generator using a token replacement approach instead of a %<id> verb-syntax to parse.
|
||||
// This was done just for preference as I personally don't like the c-printf-like syntax.
|
||||
package sectr
|
||||
|
||||
|
||||
|
||||
// str_format :: proc ( format : string, tokens : ..args ) {
|
||||
|
||||
// }
|
||||
|
||||
|
117
code/sectr/grime/string_interning.odin
Normal file
117
code/sectr/grime/string_interning.odin
Normal file
@ -0,0 +1,117 @@
|
||||
/*
|
||||
This is a quick and dirty string table.
|
||||
IT uses the HMapZPL for the hashtable of strings, and the string's content is stored in a dedicated slab.
|
||||
|
||||
Future Plans (IF needed for performance):
|
||||
The goal is to eventually swap out the slab with possilby a dedicated growing vmem arena for the strings.
|
||||
The table would be swapped with a table stored in the general slab and uses either linear probing or open addressing
|
||||
|
||||
If linear probing, the hash node list per table bucket is store with the strigns in the same arena.
|
||||
If open addressing, we just keep the open addressed array of node slots in the general slab (but hopefully better perf)
|
||||
*/
|
||||
package sectr
|
||||
|
||||
import "base:runtime"
|
||||
import "core:mem"
|
||||
import "core:slice"
|
||||
import "core:strings"
|
||||
|
||||
StringKey :: distinct u64
|
||||
RunesCached :: []rune
|
||||
|
||||
StrRunesPair :: struct {
|
||||
str : string,
|
||||
runes : []rune,
|
||||
}
|
||||
to_str_runes_pair :: proc ( content : string ) -> StrRunesPair {
|
||||
return { content, to_runes(content) }
|
||||
}
|
||||
|
||||
StringCache :: struct {
|
||||
slab : Slab,
|
||||
table : HMapZPL(StrRunesPair),
|
||||
}
|
||||
|
||||
str_cache_init :: proc( /*allocator : Allocator*/ ) -> ( cache : StringCache ) {
|
||||
alignment := uint(mem.DEFAULT_ALIGNMENT)
|
||||
|
||||
policy : SlabPolicy
|
||||
policy_ptr := & policy
|
||||
push( policy_ptr, SlabSizeClass { 64 * Kilobyte, 16, alignment })
|
||||
push( policy_ptr, SlabSizeClass { 64 * Kilobyte, 32, alignment })
|
||||
push( policy_ptr, SlabSizeClass { 64 * Kilobyte, 64, alignment })
|
||||
push( policy_ptr, SlabSizeClass { 64 * Kilobyte, 128, alignment })
|
||||
push( policy_ptr, SlabSizeClass { 64 * Kilobyte, 256, alignment })
|
||||
push( policy_ptr, SlabSizeClass { 64 * Kilobyte, 512, alignment })
|
||||
push( policy_ptr, SlabSizeClass { 1 * Megabyte, 1 * Kilobyte, alignment })
|
||||
push( policy_ptr, SlabSizeClass { 4 * Megabyte, 4 * Kilobyte, alignment })
|
||||
push( policy_ptr, SlabSizeClass { 16 * Megabyte, 16 * Kilobyte, alignment })
|
||||
push( policy_ptr, SlabSizeClass { 32 * Megabyte, 32 * Kilobyte, alignment })
|
||||
// push( policy_ptr, SlabSizeClass { 64 * Megabyte, 64 * Kilobyte, alignment })
|
||||
// push( policy_ptr, SlabSizeClass { 64 * Megabyte, 128 * Kilobyte, alignment })
|
||||
// push( policy_ptr, SlabSizeClass { 64 * Megabyte, 256 * Kilobyte, alignment })
|
||||
// push( policy_ptr, SlabSizeClass { 64 * Megabyte, 512 * Kilobyte, alignment })
|
||||
// push( policy_ptr, SlabSizeClass { 64 * Megabyte, 1 * Megabyte, alignment })
|
||||
|
||||
header_size :: size_of( Slab )
|
||||
|
||||
@static dbg_name := "StringCache slab"
|
||||
|
||||
state := get_state()
|
||||
|
||||
alloc_error : AllocatorError
|
||||
cache.slab, alloc_error = slab_init( & policy, allocator = persistent_allocator(), dbg_name = dbg_name )
|
||||
verify(alloc_error == .None, "Failed to initialize the string cache" )
|
||||
|
||||
cache.table, alloc_error = zpl_hmap_init_reserve( StrRunesPair, persistent_allocator(), 4 * Megabyte, dbg_name )
|
||||
return
|
||||
}
|
||||
|
||||
str_intern_key :: #force_inline proc( content : string ) -> StringKey { return cast(StringKey) crc32( transmute([]byte) content ) }
|
||||
str_intern_lookup :: #force_inline proc( key : StringKey ) -> (^StrRunesPair) { return zpl_hmap_get( & get_state().string_cache.table, transmute(u64) key ) }
|
||||
|
||||
str_intern :: proc(
|
||||
content : string
|
||||
) -> StrRunesPair
|
||||
{
|
||||
// profile(#procedure)
|
||||
cache := & get_state().string_cache
|
||||
|
||||
key := str_intern_key(content)
|
||||
result := zpl_hmap_get( & cache.table, transmute(u64) key )
|
||||
if result != nil {
|
||||
return (result ^)
|
||||
}
|
||||
|
||||
// profile_begin("new entry")
|
||||
{
|
||||
length := len(content)
|
||||
// str_mem, alloc_error := alloc( length, mem.DEFAULT_ALIGNMENT )
|
||||
str_mem, alloc_error := slab_alloc( cache.slab, uint(length), uint(mem.DEFAULT_ALIGNMENT), zero_memory = false )
|
||||
verify( alloc_error == .None, "String cache had a backing allocator error" )
|
||||
|
||||
// copy_non_overlapping( str_mem, raw_data(content), length )
|
||||
copy_non_overlapping( raw_data(str_mem), raw_data(content), length )
|
||||
|
||||
runes : []rune
|
||||
// runes, alloc_error = to_runes( content, persistent_allocator() )
|
||||
runes, alloc_error = to_runes( content, slab_allocator(cache.slab) )
|
||||
verify( alloc_error == .None, "String cache had a backing allocator error" )
|
||||
|
||||
slab_validate_pools( get_state().persistent_slab )
|
||||
|
||||
// result, alloc_error = zpl_hmap_set( & cache.table, key, StrRunesPair { transmute(string) byte_slice(str_mem, length), runes } )
|
||||
result, alloc_error = zpl_hmap_set( & cache.table, transmute(u64) key, StrRunesPair { transmute(string) str_mem, runes } )
|
||||
verify( alloc_error == .None, "String cache had a backing allocator error" )
|
||||
|
||||
slab_validate_pools( get_state().persistent_slab )
|
||||
}
|
||||
// profile_end()
|
||||
|
||||
return (result ^)
|
||||
}
|
||||
|
||||
// runes_intern :: proc( content : []rune ) -> StrRunesPair
|
||||
// {
|
||||
// cache := get_state().string_cache
|
||||
// }
|
43
code/sectr/grime/unicode.odin
Normal file
43
code/sectr/grime/unicode.odin
Normal file
@ -0,0 +1,43 @@
|
||||
package sectr
|
||||
|
||||
rune16 :: distinct u16
|
||||
|
||||
|
||||
|
||||
|
||||
// Exposing the alloc_error
|
||||
@(require_results)
|
||||
string_to_runes :: proc ( content : string, allocator := context.allocator) -> (runes : []rune, alloc_error : AllocatorError) #optional_allocator_error {
|
||||
num := str_rune_count(content)
|
||||
|
||||
runes, alloc_error = make([]rune, num, allocator)
|
||||
if runes == nil || alloc_error != AllocatorError.None {
|
||||
return
|
||||
}
|
||||
|
||||
idx := 0
|
||||
for codepoint in content {
|
||||
runes[idx] = codepoint
|
||||
idx += 1
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
string_to_runes_array :: proc( content : string, allocator := context.allocator ) -> ( []rune, AllocatorError )
|
||||
{
|
||||
num := cast(u64) str_rune_count(content)
|
||||
|
||||
runes_array, alloc_error := array_init_reserve( rune, allocator, num )
|
||||
if alloc_error != AllocatorError.None {
|
||||
return nil, alloc_error
|
||||
}
|
||||
|
||||
runes := array_to_slice_capacity(runes_array)
|
||||
|
||||
idx := 0
|
||||
for codepoint in content {
|
||||
runes[idx] = codepoint
|
||||
idx += 1
|
||||
}
|
||||
return runes, alloc_error
|
||||
}
|
312
code/sectr/grime/virtual_arena.odin
Normal file
312
code/sectr/grime/virtual_arena.odin
Normal file
@ -0,0 +1,312 @@
|
||||
/*
|
||||
Odin's virtual arena allocator doesn't do what I ideally want for allocation resizing.
|
||||
(It was also a nice exercise along with making the other allocators)
|
||||
|
||||
So this is a virtual memory backed arena allocator designed
|
||||
to take advantage of one large contigous reserve of memory.
|
||||
With the expectation that resizes with its interface will only occur using the last allocated block.
|
||||
|
||||
All virtual address space memory for this application is managed by a virtual arena.
|
||||
No other part of the program will directly touch the vitual memory interface direclty other than it.
|
||||
|
||||
Thus for the scope of this prototype the Virtual Arena are the only interfaces to dynamic address spaces for the runtime of the client app.
|
||||
The host application as well ideally (although this may not be the case for a while)
|
||||
*/
|
||||
package sectr
|
||||
|
||||
import "base:intrinsics"
|
||||
import "base:runtime"
|
||||
import "core:mem"
|
||||
import "core:os"
|
||||
import "core:slice"
|
||||
import "core:sync"
|
||||
|
||||
VArena_GrowthPolicyProc :: #type proc( commit_used, committed, reserved, requested_size : uint ) -> uint
|
||||
|
||||
VArena :: struct {
|
||||
using vmem : VirtualMemoryRegion,
|
||||
dbg_name : string,
|
||||
tracker : MemoryTracker,
|
||||
commit_used : uint,
|
||||
growth_policy : VArena_GrowthPolicyProc,
|
||||
allow_any_reize : b32,
|
||||
mutex : sync.Mutex,
|
||||
}
|
||||
|
||||
varena_default_growth_policy :: proc( commit_used, committed, reserved, requested_size : uint ) -> uint
|
||||
{
|
||||
@static commit_limit := uint(1 * Megabyte)
|
||||
@static increment := uint(16 * Kilobyte)
|
||||
page_size := uint(virtual_get_page_size())
|
||||
|
||||
if increment < Gigabyte && committed > commit_limit {
|
||||
commit_limit *= 2
|
||||
increment *= 2
|
||||
|
||||
increment = clamp( increment, Megabyte, Gigabyte )
|
||||
}
|
||||
|
||||
remaining_reserve := reserved - committed
|
||||
growth_increment := max( increment, requested_size )
|
||||
growth_increment = clamp( growth_increment, page_size, remaining_reserve )
|
||||
next_commit_size := memory_align_formula( committed + growth_increment, page_size )
|
||||
return next_commit_size
|
||||
}
|
||||
|
||||
varena_allocator :: proc( arena : ^VArena ) -> ( allocator : Allocator ) {
|
||||
allocator.procedure = varena_allocator_proc
|
||||
allocator.data = arena
|
||||
return
|
||||
}
|
||||
|
||||
// Default growth_policy is nil
|
||||
varena_init :: proc( base_address : uintptr, to_reserve, to_commit : uint,
|
||||
growth_policy : VArena_GrowthPolicyProc, allow_any_reize : b32 = false, dbg_name : string
|
||||
) -> ( arena : VArena, alloc_error : AllocatorError)
|
||||
{
|
||||
page_size := uint(virtual_get_page_size())
|
||||
verify( page_size > size_of(VirtualMemoryRegion), "Make sure page size is not smaller than a VirtualMemoryRegion?")
|
||||
verify( to_reserve >= page_size, "Attempted to reserve less than a page size" )
|
||||
verify( to_commit >= page_size, "Attempted to commit less than a page size")
|
||||
verify( to_reserve >= to_commit, "Attempted to commit more than there is to reserve" )
|
||||
|
||||
vmem : VirtualMemoryRegion
|
||||
vmem, alloc_error = virtual_reserve_and_commit( base_address, to_reserve, to_commit )
|
||||
if vmem.base_address == nil || alloc_error != .None {
|
||||
ensure(false, "Failed to allocate requested virtual memory for virtual arena")
|
||||
return
|
||||
}
|
||||
|
||||
arena.vmem = vmem
|
||||
arena.commit_used = 0
|
||||
|
||||
if growth_policy == nil {
|
||||
arena.growth_policy = varena_default_growth_policy
|
||||
}
|
||||
else {
|
||||
arena.growth_policy = growth_policy
|
||||
}
|
||||
arena.allow_any_reize = allow_any_reize
|
||||
|
||||
when ODIN_DEBUG {
|
||||
memtracker_init( & arena.tracker, runtime.heap_allocator(), Kilobyte * 128, dbg_name )
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
varena_alloc :: proc( using self : ^VArena,
|
||||
size : uint,
|
||||
alignment : uint = mem.DEFAULT_ALIGNMENT,
|
||||
zero_memory := true,
|
||||
location := #caller_location
|
||||
) -> ( data : []byte, alloc_error : AllocatorError )
|
||||
{
|
||||
verify( alignment & (alignment - 1) == 0, "Non-power of two alignment", location = location )
|
||||
page_size := uint(virtual_get_page_size())
|
||||
|
||||
requested_size := size
|
||||
if requested_size == 0 {
|
||||
ensure(false, "Requested 0 size")
|
||||
return nil, .Invalid_Argument
|
||||
}
|
||||
// ensure( requested_size > page_size, "Requested less than a page size, going to allocate a page size")
|
||||
// requested_size = max(requested_size, page_size)
|
||||
|
||||
sync.mutex_guard( & mutex )
|
||||
|
||||
alignment_offset := uint(0)
|
||||
current_offset := uintptr(self.reserve_start) + uintptr(commit_used)
|
||||
mask := uintptr(alignment - 1)
|
||||
|
||||
if current_offset & mask != 0 {
|
||||
alignment_offset = alignment - uint(current_offset & mask)
|
||||
}
|
||||
|
||||
size_to_allocate, overflow_signal := intrinsics.overflow_add( requested_size, alignment_offset )
|
||||
if overflow_signal {
|
||||
alloc_error = .Out_Of_Memory
|
||||
return
|
||||
}
|
||||
|
||||
to_be_used : uint
|
||||
to_be_used, overflow_signal = intrinsics.overflow_add( commit_used, size_to_allocate )
|
||||
if overflow_signal || to_be_used > reserved {
|
||||
alloc_error = .Out_Of_Memory
|
||||
return
|
||||
}
|
||||
|
||||
header_offset := uint( uintptr(reserve_start) - uintptr(base_address) )
|
||||
|
||||
commit_left := committed - commit_used - header_offset
|
||||
needs_more_committed := commit_left < size_to_allocate
|
||||
if needs_more_committed
|
||||
{
|
||||
profile("VArena Growing")
|
||||
next_commit_size := growth_policy( commit_used, committed, reserved, size_to_allocate )
|
||||
alloc_error = virtual_commit( vmem, next_commit_size )
|
||||
if alloc_error != .None {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
data_ptr := rawptr(current_offset + uintptr(alignment_offset))
|
||||
data = byte_slice( data_ptr, int(requested_size) )
|
||||
self.commit_used += size_to_allocate
|
||||
alloc_error = .None
|
||||
|
||||
// log_backing : [Kilobyte * 16]byte
|
||||
// backing_slice := byte_slice( & log_backing[0], len(log_backing))
|
||||
// log( str_fmt_buffer( backing_slice, "varena alloc - BASE: %p PTR: %X, SIZE: %d", cast(rawptr) self.base_address, & data[0], requested_size) )
|
||||
|
||||
if zero_memory
|
||||
{
|
||||
// log( str_fmt_buffer( backing_slice, "Zeroring data (Range: %p to %p)", raw_data(data), cast(rawptr) (uintptr(raw_data(data)) + uintptr(requested_size))))
|
||||
// slice.zero( data )
|
||||
mem_zero( data_ptr, int(requested_size) )
|
||||
}
|
||||
|
||||
when ODIN_DEBUG {
|
||||
memtracker_register_auto_name( & tracker, & data[0], & data[len(data) - 1] )
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
varena_free_all :: proc( using self : ^VArena )
|
||||
{
|
||||
sync.mutex_guard( & mutex )
|
||||
commit_used = 0
|
||||
|
||||
when ODIN_DEBUG && Track_Memory {
|
||||
array_clear(tracker.entries)
|
||||
}
|
||||
}
|
||||
|
||||
varena_release :: proc( using self : ^VArena )
|
||||
{
|
||||
sync.mutex_guard( & mutex )
|
||||
|
||||
virtual_release( vmem )
|
||||
commit_used = 0
|
||||
}
|
||||
|
||||
varena_allocator_proc :: proc(
|
||||
allocator_data : rawptr,
|
||||
mode : AllocatorMode,
|
||||
size : int,
|
||||
alignment : int,
|
||||
old_memory : rawptr,
|
||||
old_size : int,
|
||||
location : SourceCodeLocation = #caller_location
|
||||
) -> ( data : []byte, alloc_error : AllocatorError)
|
||||
{
|
||||
arena := cast( ^VArena) allocator_data
|
||||
|
||||
size := uint(size)
|
||||
alignment := uint(alignment)
|
||||
old_size := uint(old_size)
|
||||
|
||||
page_size := uint(virtual_get_page_size())
|
||||
|
||||
switch mode
|
||||
{
|
||||
case .Alloc, .Alloc_Non_Zeroed:
|
||||
data, alloc_error = varena_alloc( arena, size, alignment, (mode != .Alloc_Non_Zeroed), location )
|
||||
return
|
||||
|
||||
case .Free:
|
||||
alloc_error = .Mode_Not_Implemented
|
||||
|
||||
case .Free_All:
|
||||
varena_free_all( arena )
|
||||
|
||||
case .Resize, .Resize_Non_Zeroed:
|
||||
if old_memory == nil {
|
||||
ensure(false, "Resizing without old_memory?")
|
||||
data, alloc_error = varena_alloc( arena, size, alignment, (mode != .Resize_Non_Zeroed), location )
|
||||
return
|
||||
}
|
||||
|
||||
if size == old_size {
|
||||
ensure(false, "Requested resize when none needed")
|
||||
data = byte_slice( old_memory, old_size )
|
||||
return
|
||||
}
|
||||
|
||||
alignment_offset := uintptr(old_memory) & uintptr(alignment - 1)
|
||||
if alignment_offset == 0 && size < old_size {
|
||||
ensure(false, "Requested a shrink from a virtual arena")
|
||||
data = byte_slice( old_memory, size )
|
||||
return
|
||||
}
|
||||
|
||||
old_memory_offset := uintptr(old_memory) + uintptr(old_size)
|
||||
current_offset := uintptr(arena.reserve_start) + uintptr(arena.commit_used)
|
||||
|
||||
// if old_size < page_size {
|
||||
// // We're dealing with an allocation that requested less than the minimum allocated on vmem.
|
||||
// // Provide them more of their actual memory
|
||||
// data = byte_slice( old_memory, size )
|
||||
// return
|
||||
// }
|
||||
|
||||
verify( old_memory_offset == current_offset || arena.allow_any_reize,
|
||||
"Cannot resize existing allocation in vitual arena to a larger size unless it was the last allocated" )
|
||||
|
||||
log_backing : [Kilobyte * 16]byte
|
||||
backing_slice := byte_slice( & log_backing[0], len(log_backing))
|
||||
|
||||
if old_memory_offset != current_offset && arena.allow_any_reize
|
||||
{
|
||||
// Give it new memory and copy the old over. Old memory is unrecoverable until clear.
|
||||
new_region : []byte
|
||||
new_region, alloc_error = varena_alloc( arena, size, alignment, (mode != .Resize_Non_Zeroed), location )
|
||||
if new_region == nil || alloc_error != .None {
|
||||
ensure(false, "Failed to grab new region")
|
||||
data = byte_slice( old_memory, old_size )
|
||||
|
||||
when ODIN_DEBUG {
|
||||
memtracker_register_auto_name( & arena.tracker, & data[0], & data[len(data) - 1] )
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
copy_non_overlapping( raw_data(new_region), old_memory, int(old_size) )
|
||||
data = new_region
|
||||
// log( str_fmt_tmp("varena resize (new): old: %p %v new: %p %v", old_memory, old_size, (& data[0]), size))
|
||||
|
||||
when ODIN_DEBUG {
|
||||
memtracker_register_auto_name( & arena.tracker, & data[0], & data[len(data) - 1] )
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
new_region : []byte
|
||||
new_region, alloc_error = varena_alloc( arena, size - old_size, alignment, (mode != .Resize_Non_Zeroed), location )
|
||||
if new_region == nil || alloc_error != .None {
|
||||
ensure(false, "Failed to grab new region")
|
||||
data = byte_slice( old_memory, old_size )
|
||||
return
|
||||
}
|
||||
|
||||
data = byte_slice( old_memory, size )
|
||||
// log( str_fmt_tmp("varena resize (expanded): old: %p %v new: %p %v", old_memory, old_size, (& data[0]), size))
|
||||
|
||||
when ODIN_DEBUG {
|
||||
memtracker_register_auto_name( & arena.tracker, & data[0], & data[len(data) - 1] )
|
||||
}
|
||||
return
|
||||
|
||||
case .Query_Features:
|
||||
{
|
||||
set := cast( ^AllocatorModeSet) old_memory
|
||||
if set != nil {
|
||||
(set ^) = {.Alloc, .Alloc_Non_Zeroed, .Free_All, .Resize, .Query_Features}
|
||||
}
|
||||
}
|
||||
case .Query_Info:
|
||||
{
|
||||
alloc_error = .Mode_Not_Implemented
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
116
code/sectr/grime/virtual_memory.odin
Normal file
116
code/sectr/grime/virtual_memory.odin
Normal file
@ -0,0 +1,116 @@
|
||||
/* Virtual Memory OS Interface
|
||||
This is an alternative to the virtual core library provided by odin, suppport setting the base address among other things.
|
||||
*/
|
||||
package sectr
|
||||
|
||||
import core_virtual "core:mem/virtual"
|
||||
import "core:os"
|
||||
|
||||
VirtualMemoryRegionHeader :: struct {
|
||||
committed : uint,
|
||||
reserved : uint,
|
||||
reserve_start : [^]byte,
|
||||
}
|
||||
|
||||
VirtualMemoryRegion :: struct {
|
||||
using base_address : ^VirtualMemoryRegionHeader
|
||||
}
|
||||
|
||||
virtual_get_page_size :: proc "contextless" () -> int {
|
||||
@static page_size := 0
|
||||
if page_size == 0 {
|
||||
page_size = os.get_page_size()
|
||||
}
|
||||
return page_size
|
||||
}
|
||||
|
||||
virtual_reserve_remaining :: proc "contextless" ( using vmem : VirtualMemoryRegion ) -> uint {
|
||||
header_offset := cast(uint) (uintptr(reserve_start) - uintptr(vmem.base_address))
|
||||
return reserved - header_offset
|
||||
}
|
||||
|
||||
@(require_results)
|
||||
virtual_commit :: proc "contextless" ( using vmem : VirtualMemoryRegion, size : uint ) -> ( alloc_error : AllocatorError )
|
||||
{
|
||||
if size < committed {
|
||||
return .None
|
||||
}
|
||||
|
||||
header_size := size_of(VirtualMemoryRegionHeader)
|
||||
page_size := uint(virtual_get_page_size())
|
||||
to_commit := memory_align_formula( size, page_size )
|
||||
|
||||
alloc_error = core_virtual.commit( base_address, to_commit )
|
||||
if alloc_error != .None {
|
||||
return alloc_error
|
||||
}
|
||||
|
||||
base_address.committed = size
|
||||
return alloc_error
|
||||
}
|
||||
|
||||
virtual_decommit :: proc "contextless" ( vmem : VirtualMemoryRegion, size : uint ) {
|
||||
core_virtual.decommit( vmem.base_address, size )
|
||||
}
|
||||
|
||||
virtual_protect :: proc "contextless" ( vmem : VirtualMemoryRegion, region : []byte, flags : VirtualProtectFlags ) -> b32
|
||||
{
|
||||
page_size := virtual_get_page_size()
|
||||
|
||||
if len(region) % page_size != 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
return cast(b32) core_virtual.protect( raw_data(region), len(region), flags )
|
||||
}
|
||||
|
||||
@(require_results)
|
||||
virtual_reserve :: proc "contextless" ( base_address : uintptr, size : uint ) -> ( VirtualMemoryRegion, AllocatorError ) {
|
||||
page_size := uint(virtual_get_page_size())
|
||||
to_reserve := memory_align_formula( size, page_size )
|
||||
return virtual__reserve( base_address, to_reserve )
|
||||
}
|
||||
|
||||
@(require_results)
|
||||
virtual_reserve_and_commit :: proc "contextless" (
|
||||
base_address : uintptr, reserve_size, commit_size : uint
|
||||
) -> ( vmem : VirtualMemoryRegion, alloc_error : AllocatorError )
|
||||
{
|
||||
if reserve_size < commit_size {
|
||||
alloc_error = .Invalid_Argument
|
||||
return
|
||||
}
|
||||
|
||||
vmem, alloc_error = virtual_reserve( base_address, reserve_size )
|
||||
if alloc_error != .None {
|
||||
return
|
||||
}
|
||||
|
||||
alloc_error = virtual_commit( vmem, commit_size )
|
||||
return
|
||||
}
|
||||
|
||||
virtual_release :: proc "contextless" ( vmem : VirtualMemoryRegion ) {
|
||||
core_virtual.release( vmem.base_address, vmem.reserved )
|
||||
}
|
||||
|
||||
// If the OS is not windows, we just use the library's interface which does not support base_address.
|
||||
when ODIN_OS != OS_Type.Windows {
|
||||
|
||||
virtual__reserve :: proc "contextless" ( base_address : uintptr, size : uint ) -> ( vmem : VirtualMemoryRegion, alloc_error : AllocatorError )
|
||||
{
|
||||
header_size := memory_align_formula(size_of(VirtualMemoryRegionHeader), mem.DEFAULT_ALIGNMENT)
|
||||
|
||||
// Ignoring the base address, add an os specific impl if you want it.
|
||||
data : []byte
|
||||
data, alloc_error := core_virtual.reserve( header_size + size ) or_return
|
||||
alloc_error := core_virtual.commit( header_size )
|
||||
|
||||
vmem.base_address := cast( ^VirtualMemoryRegionHeader ) raw_data(data)
|
||||
vmem.reserve_start = cast([^]byte) (uintptr(vmem.base_address) + uintptr(header_size))
|
||||
vmem.reserved = len(data)
|
||||
vmem.committed = header_size
|
||||
return
|
||||
}
|
||||
|
||||
} // END: ODIN_OS != runtime.Odin_OS_Type.Windows
|
112
code/sectr/grime/windows.odin
Normal file
112
code/sectr/grime/windows.odin
Normal file
@ -0,0 +1,112 @@
|
||||
package sectr
|
||||
|
||||
import "core:c"
|
||||
import "core:c/libc"
|
||||
import "core:fmt"
|
||||
import "core:mem"
|
||||
import core_virtual "core:mem/virtual"
|
||||
import "core:strings"
|
||||
import win32 "core:sys/windows"
|
||||
|
||||
when ODIN_OS == OS_Type.Windows {
|
||||
|
||||
thread__highres_wait :: proc( desired_ms : f64, loc := #caller_location ) -> b32
|
||||
{
|
||||
// label_backing : [1 * Megabyte]u8
|
||||
// label_arena : Arena
|
||||
// arena_init( & label_arena, slice_ptr( & label_backing[0], len(label_backing)) )
|
||||
// label_u8 := str_fmt_tmp( "SECTR: WAIT TIMER")//, allocator = arena_allocator( &label_arena) )
|
||||
// label_u16 := win32.utf8_to_utf16( label_u8, context.temp_allocator) //arena_allocator( & label_arena) )
|
||||
|
||||
timer := win32.CreateWaitableTimerExW( nil, nil, win32.CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, win32.TIMER_ALL_ACCESS )
|
||||
if timer == nil {
|
||||
msg := str_fmt_tmp("Failed to create win32 timer - ErrorCode: %v", win32.GetLastError() )
|
||||
log( msg, LogLevel.Warning, loc)
|
||||
return false
|
||||
}
|
||||
|
||||
due_time := win32.LARGE_INTEGER(desired_ms * MS_To_NS)
|
||||
result := win32.SetWaitableTimerEx( timer, & due_time, 0, nil, nil, nil, 0 )
|
||||
if ! result {
|
||||
msg := str_fmt_tmp("Failed to set win32 timer - ErrorCode: %v", win32.GetLastError() )
|
||||
log( msg, LogLevel.Warning, loc)
|
||||
return false
|
||||
}
|
||||
|
||||
WAIT_ABANDONED : win32.DWORD : 0x00000080
|
||||
WAIT_IO_COMPLETION : win32.DWORD : 0x000000C0
|
||||
WAIT_OBJECT_0 : win32.DWORD : 0x00000000
|
||||
WAIT_TIMEOUT : win32.DWORD : 0x00000102
|
||||
WAIT_FAILED : win32.DWORD : 0xFFFFFFFF
|
||||
|
||||
wait_result := win32.WaitForSingleObjectEx( timer, win32.INFINITE, win32.BOOL(true) )
|
||||
switch wait_result
|
||||
{
|
||||
case WAIT_ABANDONED:
|
||||
msg := str_fmt_tmp("Failed to wait for win32 timer - Error: WAIT_ABANDONED" )
|
||||
log( msg, LogLevel.Error, loc)
|
||||
return false
|
||||
|
||||
case WAIT_IO_COMPLETION:
|
||||
msg := str_fmt_tmp("Waited for win32 timer: Ended by APC queued to the thread" )
|
||||
log( msg, LogLevel.Error, loc)
|
||||
return false
|
||||
|
||||
case WAIT_OBJECT_0:
|
||||
msg := str_fmt_tmp("Waited for win32 timer- Reason : WAIT_OBJECT_0" )
|
||||
log( msg, loc = loc)
|
||||
return false
|
||||
|
||||
case WAIT_FAILED:
|
||||
msg := str_fmt_tmp("Waited for win32 timer failed - ErrorCode: $v", win32.GetLastError() )
|
||||
log( msg, LogLevel.Error, loc)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
set__scheduler_granularity :: proc "contextless" ( desired_ms : u32 ) -> b32 {
|
||||
return win32.timeBeginPeriod( desired_ms ) == win32.TIMERR_NOERROR
|
||||
}
|
||||
|
||||
WIN32_ERROR_INVALID_ADDRESS :: 487
|
||||
WIN32_ERROR_COMMITMENT_LIMIT :: 1455
|
||||
|
||||
@(require_results)
|
||||
virtual__reserve :: proc "contextless" ( base_address : uintptr, size : uint ) -> ( vmem : VirtualMemoryRegion, alloc_error : AllocatorError )
|
||||
{
|
||||
header_size := cast(uint) memory_align_formula(size_of(VirtualMemoryRegionHeader), mem.DEFAULT_ALIGNMENT)
|
||||
|
||||
result := win32.VirtualAlloc( rawptr(base_address), header_size + size, win32.MEM_RESERVE, win32.PAGE_READWRITE )
|
||||
if result == nil {
|
||||
alloc_error = .Out_Of_Memory
|
||||
return
|
||||
}
|
||||
result = win32.VirtualAlloc( rawptr(base_address), header_size, win32.MEM_COMMIT, win32.PAGE_READWRITE )
|
||||
if result == nil
|
||||
{
|
||||
switch err := win32.GetLastError(); err
|
||||
{
|
||||
case 0:
|
||||
alloc_error = .Invalid_Argument
|
||||
return
|
||||
|
||||
case WIN32_ERROR_INVALID_ADDRESS, WIN32_ERROR_COMMITMENT_LIMIT:
|
||||
alloc_error = .Out_Of_Memory
|
||||
return
|
||||
}
|
||||
|
||||
alloc_error = .Out_Of_Memory
|
||||
return
|
||||
}
|
||||
|
||||
vmem.base_address = cast(^VirtualMemoryRegionHeader) result
|
||||
vmem.reserve_start = cast([^]byte) (uintptr(vmem.base_address) + uintptr(header_size))
|
||||
vmem.reserved = size
|
||||
vmem.committed = header_size
|
||||
alloc_error = .None
|
||||
return
|
||||
}
|
||||
|
||||
} // END: ODIN_OS == runtime.Odin_OS_Type.Windows
|
1
code/sectr/input/actions.odin
Normal file
1
code/sectr/input/actions.odin
Normal file
@ -0,0 +1 @@
|
||||
package sectr
|
1
code/sectr/input/event.odin
Normal file
1
code/sectr/input/event.odin
Normal file
@ -0,0 +1 @@
|
||||
package sectr
|
501
code/sectr/input/input.odin
Normal file
501
code/sectr/input/input.odin
Normal file
@ -0,0 +1,501 @@
|
||||
// TODO(Ed) : This if its gets larget can be moved to its own package
|
||||
package sectr
|
||||
|
||||
import "base:runtime"
|
||||
|
||||
AnalogAxis :: f32
|
||||
AnalogStick :: struct {
|
||||
X, Y : f32
|
||||
}
|
||||
|
||||
DigitalBtn :: struct {
|
||||
half_transitions : i32,
|
||||
ended_down : b32
|
||||
}
|
||||
|
||||
btn_pressed :: proc( btn : DigitalBtn ) -> b32 {
|
||||
return btn.ended_down && btn.half_transitions > 0
|
||||
}
|
||||
|
||||
btn_released :: proc ( btn : DigitalBtn ) -> b32 {
|
||||
return btn.ended_down == false && btn.half_transitions > 0
|
||||
}
|
||||
|
||||
MaxKeyboardKeys :: 256
|
||||
KeyboardKey :: enum u32 {
|
||||
null = 0x00,
|
||||
|
||||
enter = 0x01,
|
||||
tab = 0x02,
|
||||
space = 0x03,
|
||||
bracket_open = 0x04,
|
||||
bracket_close = 0x05,
|
||||
semicolon = 0x06,
|
||||
apostrophe = 0x07,
|
||||
comma = 0x08,
|
||||
period = 0x09,
|
||||
|
||||
// 0x0A
|
||||
// 0x0B
|
||||
// 0x0C
|
||||
// 0x0D
|
||||
// 0x0E
|
||||
// 0x0F
|
||||
|
||||
caps_lock = 0x10,
|
||||
scroll_lock = 0x11,
|
||||
num_lock = 0x12,
|
||||
left_alt = 0x13,
|
||||
left_shit = 0x14,
|
||||
left_control = 0x15,
|
||||
right_alt = 0x16,
|
||||
right_shift = 0x17,
|
||||
right_control = 0x18,
|
||||
|
||||
// 0x19
|
||||
// 0x1A
|
||||
// 0x1B
|
||||
// 0x1C
|
||||
// 0x1D
|
||||
// 0x1C
|
||||
// 0x1D
|
||||
|
||||
escape = 0x1F,
|
||||
F1 = 0x20,
|
||||
F2 = 0x21,
|
||||
F3 = 0x22,
|
||||
F4 = 0x23,
|
||||
F5 = 0x24,
|
||||
F6 = 0x25,
|
||||
F7 = 0x26,
|
||||
F8 = 0x27,
|
||||
F9 = 0x28,
|
||||
F10 = 0x29,
|
||||
F11 = 0x2A,
|
||||
F12 = 0x2B,
|
||||
|
||||
print_screen = 0x2C,
|
||||
pause = 0x2D,
|
||||
// = 0x2E,
|
||||
|
||||
backtick = 0x2F,
|
||||
nrow_0 = 0x30,
|
||||
nrow_1 = 0x31,
|
||||
nrow_2 = 0x32,
|
||||
nrow_3 = 0x33,
|
||||
nrow_4 = 0x34,
|
||||
nrow_5 = 0x35,
|
||||
nrow_6 = 0x36,
|
||||
nrow_7 = 0x37,
|
||||
nrow_8 = 0x38,
|
||||
nrow_9 = 0x39,
|
||||
hyphen = 0x3A,
|
||||
equals = 0x3B,
|
||||
backspace = 0x3C,
|
||||
|
||||
backslash = 0x3D,
|
||||
slash = 0x3E,
|
||||
// = 0x3F,
|
||||
// = 0x40,
|
||||
|
||||
A = 0x41,
|
||||
B = 0x42,
|
||||
C = 0x43,
|
||||
D = 0x44,
|
||||
E = 0x45,
|
||||
F = 0x46,
|
||||
G = 0x47,
|
||||
H = 0x48,
|
||||
I = 0x49,
|
||||
J = 0x4A,
|
||||
K = 0x4B,
|
||||
L = 0x4C,
|
||||
M = 0x4D,
|
||||
N = 0x4E,
|
||||
O = 0x4F,
|
||||
P = 0x50,
|
||||
Q = 0x51,
|
||||
R = 0x52,
|
||||
S = 0x53,
|
||||
T = 0x54,
|
||||
U = 0x55,
|
||||
V = 0x56,
|
||||
W = 0x57,
|
||||
X = 0x58,
|
||||
Y = 0x59,
|
||||
Z = 0x5A,
|
||||
|
||||
insert = 0x5B,
|
||||
delete = 0x5C,
|
||||
home = 0x5D,
|
||||
end = 0x5E,
|
||||
page_up = 0x5F,
|
||||
page_down = 0x60,
|
||||
|
||||
npad_0 = 0x61,
|
||||
npad_1 = 0x62,
|
||||
npad_2 = 0x63,
|
||||
npad_3 = 0x64,
|
||||
npad_4 = 0x65,
|
||||
npad_5 = 0x66,
|
||||
npad_6 = 0x67,
|
||||
npad_7 = 0x68,
|
||||
npad_8 = 0x69,
|
||||
npad_9 = 0x6A,
|
||||
npad_decimal = 0x6B,
|
||||
npad_equals = 0x6C,
|
||||
npad_plus = 0x6D,
|
||||
npad_minus = 0x6E,
|
||||
npad_multiply = 0x6F,
|
||||
npad_divide = 0x70,
|
||||
npad_enter = 0x71,
|
||||
|
||||
count = 0x72
|
||||
}
|
||||
|
||||
KeyboardState :: struct #raw_union {
|
||||
keys : [MaxKeyboardKeys] DigitalBtn,
|
||||
using individual : struct {
|
||||
enter,
|
||||
tab,
|
||||
space,
|
||||
bracket_open,
|
||||
bracket_close,
|
||||
semicolon,
|
||||
apostrophe,
|
||||
comma,
|
||||
period : DigitalBtn,
|
||||
|
||||
__0x0A_0x0F_Unassigned__ : [ 6 * size_of( DigitalBtn )] u8,
|
||||
|
||||
caps_lock,
|
||||
scroll_lock,
|
||||
num_lock,
|
||||
left_alt,
|
||||
left_shift,
|
||||
left_control,
|
||||
right_alt,
|
||||
right_shift,
|
||||
right_control : DigitalBtn,
|
||||
|
||||
__0x19_0x1D_Unassigned__ : [ 6 * size_of( DigitalBtn )] u8,
|
||||
|
||||
escape,
|
||||
F1,
|
||||
F2,
|
||||
F3,
|
||||
F4,
|
||||
F5,
|
||||
F6,
|
||||
F7,
|
||||
F8,
|
||||
F9,
|
||||
F10,
|
||||
F11,
|
||||
F12 : DigitalBtn,
|
||||
|
||||
print_screen,
|
||||
pause : DigitalBtn,
|
||||
|
||||
__0x2E_Unassigned__ : [size_of(DigitalBtn)] u8,
|
||||
|
||||
backtick,
|
||||
nrow_0,
|
||||
nrow_1,
|
||||
nrow_2,
|
||||
nrow_3,
|
||||
nrow_4,
|
||||
nrow_5,
|
||||
nrow_6,
|
||||
nrow_7,
|
||||
nrow_8,
|
||||
nrow_9,
|
||||
hyphen,
|
||||
equals,
|
||||
backspace : DigitalBtn,
|
||||
|
||||
backslash,
|
||||
slash : DigitalBtn,
|
||||
|
||||
__0x3F_0x40_Unassigned__ : [ 2 * size_of(DigitalBtn)] u8,
|
||||
|
||||
A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z : DigitalBtn,
|
||||
|
||||
insert,
|
||||
delete,
|
||||
home,
|
||||
end,
|
||||
page_up,
|
||||
page_down : DigitalBtn,
|
||||
|
||||
npad_0,
|
||||
npad_1,
|
||||
npad_2,
|
||||
npad_3,
|
||||
npad_4,
|
||||
npad_5,
|
||||
npad_6,
|
||||
npad_7,
|
||||
npad_8,
|
||||
npad_9,
|
||||
npad_decimal,
|
||||
npad_equals,
|
||||
npad_plus,
|
||||
npad_minus,
|
||||
npad_multiply,
|
||||
npad_divide,
|
||||
npad_enter : DigitalBtn
|
||||
}
|
||||
}
|
||||
|
||||
MaxMouseBtns :: 16
|
||||
MouseBtn :: enum u32 {
|
||||
Left = 0x0,
|
||||
Middle = 0x1,
|
||||
Right = 0x2,
|
||||
Side = 0x3,
|
||||
Forward = 0x4,
|
||||
Back = 0x5,
|
||||
Extra = 0x6,
|
||||
|
||||
count
|
||||
}
|
||||
|
||||
MouseState :: struct {
|
||||
using _ : struct #raw_union {
|
||||
btns : [16] DigitalBtn,
|
||||
using individual : struct {
|
||||
left, middle, right : DigitalBtn,
|
||||
side, forward, back, extra : DigitalBtn
|
||||
}
|
||||
},
|
||||
raw_pos, pos, delta : Vec2,
|
||||
vertical_wheel, horizontal_wheel : AnalogAxis
|
||||
}
|
||||
|
||||
mouse_world_delta :: #force_inline proc "contextless" () -> Vec2 {
|
||||
using state := get_state()
|
||||
cam := & state.project.workspace.cam
|
||||
return input.mouse.delta * ( 1 / cam.zoom )
|
||||
}
|
||||
|
||||
InputState :: struct {
|
||||
keyboard : KeyboardState,
|
||||
mouse : MouseState
|
||||
}
|
||||
|
||||
import "core:os"
|
||||
import c "core:c/libc"
|
||||
import rl "vendor:raylib"
|
||||
|
||||
poll_input :: proc( old, new : ^ InputState )
|
||||
{
|
||||
profile(#procedure)
|
||||
input_process_digital_btn :: proc( old_state, new_state : ^ DigitalBtn, is_down : b32 )
|
||||
{
|
||||
new_state.ended_down = is_down
|
||||
had_transition := old_state.ended_down != new_state.ended_down
|
||||
if had_transition {
|
||||
new_state.half_transitions += 1
|
||||
}
|
||||
else {
|
||||
new_state.half_transitions = 0
|
||||
}
|
||||
}
|
||||
|
||||
// Keyboard
|
||||
{
|
||||
// profile("Keyboard")
|
||||
check_range :: proc( old, new : ^ InputState, start, end : i32 )
|
||||
{
|
||||
for id := start; id < end; id += 1
|
||||
{
|
||||
// TODO(Ed) : LOOK OVER THIS...
|
||||
entry_old := & old.keyboard.keys[id - 1]
|
||||
entry_new := & new.keyboard.keys[id - 1]
|
||||
|
||||
key_id := cast(KeyboardKey) id
|
||||
|
||||
is_down := cast(b32) rl.IsKeyDown( to_raylib_key(id) )
|
||||
input_process_digital_btn( entry_old, entry_new, is_down )
|
||||
}
|
||||
}
|
||||
|
||||
DeadBound_1 :: 0x0A
|
||||
DeadBound_2 :: 0x2E
|
||||
DeadBound_3 :: 0x19
|
||||
DeadBound_4 :: 0x3F
|
||||
check_range( old, new, cast(i32) KeyboardKey.enter, DeadBound_1 )
|
||||
check_range( old, new, cast(i32) KeyboardKey.caps_lock, DeadBound_2 )
|
||||
check_range( old, new, cast(i32) KeyboardKey.escape, DeadBound_3 )
|
||||
check_range( old, new, cast(i32) KeyboardKey.backtick, DeadBound_4 )
|
||||
check_range( old, new, cast(i32) KeyboardKey.A, cast(i32) KeyboardKey.count )
|
||||
}
|
||||
|
||||
// Mouse
|
||||
{
|
||||
// profile("Mouse")
|
||||
// Process Buttons
|
||||
for id : i32 = 0; id < i32(MouseBtn.count); id += 1
|
||||
{
|
||||
old_btn := & old.mouse.btns[id]
|
||||
new_btn := & new.mouse.btns[id]
|
||||
|
||||
mouse_id := cast(MouseBtn) id
|
||||
|
||||
is_down := cast(b32) rl.IsMouseButtonDown( to_raylib_mouse_btn(id) )
|
||||
input_process_digital_btn( old_btn, new_btn, is_down )
|
||||
}
|
||||
|
||||
new.mouse.raw_pos = rl.GetMousePosition()
|
||||
new.mouse.pos = render_to_screen_pos(new.mouse.raw_pos)
|
||||
new.mouse.delta = rl.GetMouseDelta() * {1, -1}
|
||||
new.mouse.vertical_wheel = rl.GetMouseWheelMove()
|
||||
}
|
||||
}
|
||||
|
||||
record_input :: proc( replay_file : os.Handle, input : ^ InputState ) {
|
||||
raw_data := slice_ptr( transmute(^ byte) input, size_of(InputState) )
|
||||
file_write( replay_file, raw_data )
|
||||
}
|
||||
|
||||
play_input :: proc( replay_file : os.Handle, input : ^ InputState ) {
|
||||
raw_data := slice_ptr( transmute(^ byte) input, size_of(InputState) )
|
||||
total_read, result_code := file_read( replay_file, raw_data )
|
||||
if result_code == os.ERROR_HANDLE_EOF {
|
||||
file_rewind( replay_file )
|
||||
load_snapshot( & Memory_App.snapshot )
|
||||
}
|
||||
}
|
||||
|
||||
to_raylib_key :: proc( key : i32 ) -> rl.KeyboardKey {
|
||||
@static raylib_key_lookup_table := [?] rl.KeyboardKey {
|
||||
rl.KeyboardKey.KEY_NULL,
|
||||
rl.KeyboardKey.ENTER,
|
||||
rl.KeyboardKey.TAB,
|
||||
rl.KeyboardKey.SPACE,
|
||||
rl.KeyboardKey.LEFT_BRACKET,
|
||||
rl.KeyboardKey.RIGHT_BRACKET,
|
||||
rl.KeyboardKey.SEMICOLON,
|
||||
rl.KeyboardKey.APOSTROPHE,
|
||||
rl.KeyboardKey.COMMA,
|
||||
rl.KeyboardKey.PERIOD,
|
||||
cast(rl.KeyboardKey) 0, // 0x0A
|
||||
cast(rl.KeyboardKey) 0, // 0x0B
|
||||
cast(rl.KeyboardKey) 0, // 0x0C
|
||||
cast(rl.KeyboardKey) 0, // 0x0D
|
||||
cast(rl.KeyboardKey) 0, // 0x0E
|
||||
cast(rl.KeyboardKey) 0, // 0x0F
|
||||
rl.KeyboardKey.CAPS_LOCK,
|
||||
rl.KeyboardKey.SCROLL_LOCK,
|
||||
rl.KeyboardKey.NUM_LOCK,
|
||||
rl.KeyboardKey.LEFT_ALT,
|
||||
rl.KeyboardKey.LEFT_SHIFT,
|
||||
rl.KeyboardKey.LEFT_CONTROL,
|
||||
rl.KeyboardKey.RIGHT_ALT,
|
||||
rl.KeyboardKey.RIGHT_SHIFT,
|
||||
rl.KeyboardKey.RIGHT_CONTROL,
|
||||
cast(rl.KeyboardKey) 0, // 0x0F
|
||||
cast(rl.KeyboardKey) 0, // 0x0F
|
||||
cast(rl.KeyboardKey) 0, // 0x0F
|
||||
cast(rl.KeyboardKey) 0, // 0x0F
|
||||
cast(rl.KeyboardKey) 0, // 0x0F
|
||||
cast(rl.KeyboardKey) 0, // 0x0F
|
||||
cast(rl.KeyboardKey) 0, // 0x0F
|
||||
rl.KeyboardKey.ESCAPE,
|
||||
rl.KeyboardKey.F1,
|
||||
rl.KeyboardKey.F2,
|
||||
rl.KeyboardKey.F3,
|
||||
rl.KeyboardKey.F4,
|
||||
rl.KeyboardKey.F5,
|
||||
rl.KeyboardKey.F7,
|
||||
rl.KeyboardKey.F8,
|
||||
rl.KeyboardKey.F9,
|
||||
rl.KeyboardKey.F10,
|
||||
rl.KeyboardKey.F11,
|
||||
rl.KeyboardKey.F12,
|
||||
rl.KeyboardKey.PRINT_SCREEN,
|
||||
rl.KeyboardKey.PAUSE,
|
||||
cast(rl.KeyboardKey) 0, // 0x2E
|
||||
rl.KeyboardKey.GRAVE,
|
||||
cast(rl.KeyboardKey) '0',
|
||||
cast(rl.KeyboardKey) '1',
|
||||
cast(rl.KeyboardKey) '2',
|
||||
cast(rl.KeyboardKey) '3',
|
||||
cast(rl.KeyboardKey) '4',
|
||||
cast(rl.KeyboardKey) '5',
|
||||
cast(rl.KeyboardKey) '6',
|
||||
cast(rl.KeyboardKey) '7',
|
||||
cast(rl.KeyboardKey) '8',
|
||||
cast(rl.KeyboardKey) '9',
|
||||
rl.KeyboardKey.MINUS,
|
||||
rl.KeyboardKey.EQUAL,
|
||||
rl.KeyboardKey.BACKSPACE,
|
||||
rl.KeyboardKey.BACKSLASH,
|
||||
rl.KeyboardKey.SLASH,
|
||||
cast(rl.KeyboardKey) 0, // 0x3F
|
||||
cast(rl.KeyboardKey) 0, // 0x40
|
||||
rl.KeyboardKey.A,
|
||||
rl.KeyboardKey.B,
|
||||
rl.KeyboardKey.C,
|
||||
rl.KeyboardKey.D,
|
||||
rl.KeyboardKey.E,
|
||||
rl.KeyboardKey.F,
|
||||
rl.KeyboardKey.G,
|
||||
rl.KeyboardKey.H,
|
||||
rl.KeyboardKey.I,
|
||||
rl.KeyboardKey.J,
|
||||
rl.KeyboardKey.K,
|
||||
rl.KeyboardKey.L,
|
||||
rl.KeyboardKey.M,
|
||||
rl.KeyboardKey.N,
|
||||
rl.KeyboardKey.O,
|
||||
rl.KeyboardKey.P,
|
||||
rl.KeyboardKey.Q,
|
||||
rl.KeyboardKey.R,
|
||||
rl.KeyboardKey.S,
|
||||
rl.KeyboardKey.T,
|
||||
rl.KeyboardKey.U,
|
||||
rl.KeyboardKey.V,
|
||||
rl.KeyboardKey.W,
|
||||
rl.KeyboardKey.X,
|
||||
rl.KeyboardKey.Y,
|
||||
rl.KeyboardKey.Z,
|
||||
rl.KeyboardKey.INSERT,
|
||||
rl.KeyboardKey.DELETE,
|
||||
rl.KeyboardKey.HOME,
|
||||
rl.KeyboardKey.END,
|
||||
rl.KeyboardKey.PAGE_UP,
|
||||
rl.KeyboardKey.PAGE_DOWN,
|
||||
rl.KeyboardKey.KP_0,
|
||||
rl.KeyboardKey.KP_1,
|
||||
rl.KeyboardKey.KP_2,
|
||||
rl.KeyboardKey.KP_3,
|
||||
rl.KeyboardKey.KP_4,
|
||||
rl.KeyboardKey.KP_5,
|
||||
rl.KeyboardKey.KP_6,
|
||||
rl.KeyboardKey.KP_7,
|
||||
rl.KeyboardKey.KP_8,
|
||||
rl.KeyboardKey.KP_9,
|
||||
rl.KeyboardKey.KP_DECIMAL,
|
||||
rl.KeyboardKey.KP_EQUAL,
|
||||
rl.KeyboardKey.KP_ADD,
|
||||
rl.KeyboardKey.KP_SUBTRACT,
|
||||
rl.KeyboardKey.KP_MULTIPLY,
|
||||
rl.KeyboardKey.KP_DIVIDE,
|
||||
rl.KeyboardKey.KP_ENTER }
|
||||
return raylib_key_lookup_table[ key ]
|
||||
}
|
||||
|
||||
to_raylib_mouse_btn :: proc( btn : i32 ) -> rl.MouseButton {
|
||||
@static raylib_mouse_btn_lookup_table := [?] rl.MouseButton {
|
||||
rl.MouseButton.LEFT,
|
||||
rl.MouseButton.MIDDLE,
|
||||
rl.MouseButton.RIGHT,
|
||||
rl.MouseButton.SIDE,
|
||||
rl.MouseButton.FORWARD,
|
||||
rl.MouseButton.BACK,
|
||||
rl.MouseButton.EXTRA,
|
||||
}
|
||||
return raylib_mouse_btn_lookup_table[ btn ]
|
||||
}
|
132
code/sectr/math/math.odin
Normal file
132
code/sectr/math/math.odin
Normal file
@ -0,0 +1,132 @@
|
||||
// General mathematical constructions used for the prototype
|
||||
|
||||
package sectr
|
||||
|
||||
import "core:math"
|
||||
|
||||
// These are the same as the runtime constants for memory units just using a more general name when not refering to bytes
|
||||
|
||||
Kilo :: Kilobyte
|
||||
Mega :: Megabyte
|
||||
Giga :: Gigabyte
|
||||
Tera :: Terabyte
|
||||
Peta :: Petabyte
|
||||
Exa :: Exabyte
|
||||
|
||||
Axis2 :: enum i32 {
|
||||
Invalid = -1,
|
||||
X = 0,
|
||||
Y = 1,
|
||||
Count,
|
||||
}
|
||||
|
||||
f32_Infinity :: 0x7F800000
|
||||
f32_Min :: 0x00800000
|
||||
|
||||
// Note(Ed) : I don't see an intrinsict available anywhere for this. So I'll be using the Terathon non-sse impl
|
||||
// Inverse Square Root
|
||||
// C++ Source https://github.com/EricLengyel/Terathon-Math-Library/blob/main/TSMath.cpp#L191
|
||||
inverse_sqrt_f32 :: proc "contextless" ( value : f32 ) -> f32
|
||||
{
|
||||
if ( value < f32_Min) {
|
||||
return f32_Infinity
|
||||
}
|
||||
|
||||
value_u32 := transmute(u32) value
|
||||
|
||||
initial_approx := 0x5F375A86 - (value_u32 >> 1)
|
||||
refined_approx := transmute(f32) initial_approx
|
||||
|
||||
// Newton–Raphson method for getting better approximations of square roots
|
||||
// Done twice for greater accuracy.
|
||||
refined_approx = refined_approx * (1.5 - value * 0.5 * refined_approx * refined_approx )
|
||||
refined_approx = refined_approx * (1.5 - value * 0.5 * refined_approx * refined_approx )
|
||||
// refined_approx = (0.5 * refined_approx) * (3.0 - value * refined_approx * refined_approx)
|
||||
// refined_approx = (0.5 * refined_approx) * (3.0 - value * refined_approx * refined_approx)
|
||||
return refined_approx
|
||||
}
|
||||
|
||||
is_power_of_two_u32 :: #force_inline proc "contextless" ( value : u32 ) -> b32
|
||||
{
|
||||
return value != 0 && ( value & ( value - 1 )) == 0
|
||||
}
|
||||
|
||||
mov_avg_exp_f32 := #force_inline proc "contextless" ( alpha, delta_interval, last_value : f32 ) -> f32
|
||||
{
|
||||
result := (delta_interval * alpha) + (delta_interval * (1.0 - alpha))
|
||||
return result
|
||||
}
|
||||
|
||||
mov_avg_exp_f64 := #force_inline proc "contextless" ( alpha, delta_interval, last_value : f64 ) -> f64
|
||||
{
|
||||
result := (delta_interval * alpha) + (delta_interval * (1.0 - alpha))
|
||||
return result
|
||||
}
|
||||
|
||||
import "core:math/linalg"
|
||||
|
||||
Quat128 :: quaternion128
|
||||
Matrix2 :: matrix [2, 2] f32
|
||||
Vec2i :: [2]i32
|
||||
Vec3i :: [3]i32
|
||||
|
||||
vec2i_to_vec2 :: #force_inline proc "contextless" (v : Vec2i) -> Vec2 {return transmute(Vec2) v}
|
||||
vec3i_to_vec3 :: #force_inline proc "contextless" (v : Vec3i) -> Vec3 {return transmute(Vec3) v}
|
||||
|
||||
#region("Range2")
|
||||
|
||||
Range2 :: struct #raw_union {
|
||||
using min_max : struct {
|
||||
min, max : Vec2
|
||||
},
|
||||
using pts : struct {
|
||||
p0, p1 : Vec2
|
||||
},
|
||||
using xy : struct {
|
||||
x0, y0 : f32,
|
||||
x1, y1 : f32,
|
||||
},
|
||||
using side : struct {
|
||||
left, bottom : f32,
|
||||
right, top : f32,
|
||||
},
|
||||
ratio : struct {
|
||||
x, y : f32,
|
||||
},
|
||||
|
||||
// TODO(Ed) : Test these
|
||||
array : [4]f32,
|
||||
mat : matrix[2, 2] f32,
|
||||
}
|
||||
|
||||
UnitRange2 :: distinct Range2
|
||||
|
||||
range2 :: #force_inline proc "contextless" ( a, b : Vec2 ) -> Range2 {
|
||||
result := Range2 { pts = { a, b } }
|
||||
return result
|
||||
}
|
||||
|
||||
add_range2 :: #force_inline proc "contextless" ( a, b : Range2 ) -> Range2 {
|
||||
result := Range2 { pts = {
|
||||
a.p0 + b.p0,
|
||||
a.p1 + b.p1,
|
||||
}}
|
||||
return result
|
||||
}
|
||||
|
||||
sub_range2 :: #force_inline proc "contextless" ( a, b : Range2 ) -> Range2 {
|
||||
// result := Range2 { array = a.array - b.array }
|
||||
result := Range2 { mat = a.mat - b.mat }
|
||||
return result
|
||||
}
|
||||
|
||||
equal_range2 :: #force_inline proc "contextless" ( a, b : Range2 ) -> b32 {
|
||||
result := a.p0 == b.p0 && a.p1 == b.p1
|
||||
return b32(result)
|
||||
}
|
||||
|
||||
size_range2 :: #force_inline proc "contextless" ( value : Range2 ) -> Vec2 {
|
||||
return { value.p1.x - value.p0.x, value.p0.y - value.p1.y }
|
||||
}
|
||||
|
||||
#endregion("Range2")
|
36
code/sectr/math/pga2.odin
Normal file
36
code/sectr/math/pga2.odin
Normal file
@ -0,0 +1,36 @@
|
||||
package sectr
|
||||
|
||||
/*
|
||||
Vec2 : 2D Vector 4D Extension (x, y, z : 0, w : 0)
|
||||
Bivec2 : 2D Bivector
|
||||
Transform2 : 3x3 Matrix where 3rd row is always (0, 0, 1)
|
||||
*/
|
||||
Vec2 :: [2]f32
|
||||
Bivec2 :: distinct f32
|
||||
Tansform2 :: matrix [3, 3] f32
|
||||
UnitVec2 :: distinct Vec2
|
||||
|
||||
Rotor2 :: struct {
|
||||
bv : Bivec2,
|
||||
s : f32, // Scalar
|
||||
}
|
||||
|
||||
rotor2_to_complex64 :: #force_inline proc( rotor : Rotor2 ) -> complex64 { return transmute(complex64) rotor; }
|
||||
|
||||
vec2 :: #force_inline proc "contextless" ( x, y : f32 ) -> Vec2 { return {x, y} }
|
||||
|
||||
dot_vec2 :: proc "contextless" ( a, b : Vec2 ) -> (s : f32) {
|
||||
x := a.x * b.x
|
||||
y := a.y + b.y
|
||||
s = x + y
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
PointFlat2 : CGA: 2D flat point (x, y, z)
|
||||
Line : PGA: 2D line (x, y, z)
|
||||
*/
|
||||
|
||||
Point2 :: distinct Vec2
|
||||
PointFlat2 :: distinct Vec3
|
||||
Line2 :: distinct Vec3
|
239
code/sectr/math/pga3.odin
Normal file
239
code/sectr/math/pga3.odin
Normal file
@ -0,0 +1,239 @@
|
||||
package sectr
|
||||
|
||||
/*
|
||||
Vec3 : 3D Vector (x, y, z) (3x1) 4D Expression : (x, y, z, 0)
|
||||
Bivec3 : 3D Bivector (yz, zx, xy) (3x1)
|
||||
Trivec3 : 3D Trivector (xyz) (1x1)
|
||||
Rotor3 : 3D Rotation Versor-Transform (4x1)
|
||||
Motor3 : 3D Rotation & Translation Transform (4x2)
|
||||
*/
|
||||
|
||||
Vec3 :: [3]f32
|
||||
Vec4 :: [4]f32
|
||||
|
||||
Bivec3 :: struct #raw_union {
|
||||
using _ : struct { yz, zx, xy : f32 },
|
||||
using xyz : Vec3,
|
||||
}
|
||||
|
||||
Trivec3 :: distinct f32
|
||||
|
||||
Rotor3 :: struct {
|
||||
using bv : Bivec3,
|
||||
s : f32, // Scalar
|
||||
}
|
||||
|
||||
Shifter3 :: struct {
|
||||
using bv : Bivec3,
|
||||
s : f32, // Scalar
|
||||
}
|
||||
|
||||
Motor3 :: struct {
|
||||
rotor : Rotor3,
|
||||
md : Shifter3,
|
||||
}
|
||||
|
||||
UnitVec3 :: distinct Vec3
|
||||
UnitVec4 :: distinct Vec4
|
||||
UnitBivec3 :: distinct Bivec3
|
||||
|
||||
//region Vec3
|
||||
|
||||
vec3_via_f32s :: #force_inline proc "contextless" (x, y, z : f32) -> Vec3 { return {x, y, z} }
|
||||
|
||||
// complement_vec3 :: #force_inline proc "contextless" ( v : Vec3 ) -> Bivec3 {return transmute(Bivec3) v}
|
||||
|
||||
cross_vec3 :: proc "contextless" (a, b : Vec3) -> (v : Vec3) {
|
||||
v = vec3( wedge(a, b))
|
||||
return
|
||||
}
|
||||
|
||||
dot_vec3 :: proc "contextless" ( a, b : Vec3 ) -> (s : f32) {
|
||||
mult := a * b // array multiply
|
||||
s = mult.x + mult.y + mult.z
|
||||
return
|
||||
}
|
||||
|
||||
inverse_mag_vec3 :: proc "contextless" (v : Vec3) -> (result : f32) {
|
||||
square := pow2(v)
|
||||
result = inverse_sqrt( square )
|
||||
return
|
||||
}
|
||||
|
||||
magnitude_vec3 :: proc "contextless" (v : Vec3) -> (mag : f32) {
|
||||
square := pow2(v)
|
||||
mag = sqrt(square)
|
||||
return
|
||||
}
|
||||
|
||||
normalize_vec3 :: proc "contextless" (v : Vec3) -> (unit_v : UnitVec3) {
|
||||
unit_v = transmute(UnitVec3) (v * inverse_mag(v))
|
||||
return
|
||||
}
|
||||
|
||||
pow2_vec3 :: #force_inline proc "contextless" ( v : Vec3 ) -> (s : f32) { return dot(v, v) }
|
||||
|
||||
project_vec3 :: proc "contextless" ( a, b : Vec3 ) -> ( a_to_b : Vec3 ) {
|
||||
return
|
||||
}
|
||||
|
||||
reject_vec3 :: proc "contextless" ( a, b : Vec3 ) -> ( a_from_b : Vec3 ) {
|
||||
return
|
||||
}
|
||||
|
||||
project_v3_unitv3 :: proc "contextless" ( v : Vec3, u : UnitVec3 ) -> (v_to_u : Vec3) {
|
||||
inner := dot(v, u)
|
||||
v_to_u = (transmute(Vec3) u) * inner
|
||||
return
|
||||
}
|
||||
project_unitv3_v3 :: #force_inline proc "contextless" (u : UnitVec3, v : Vec3) -> (u_to_v : Vec3) {
|
||||
inner := dot(u, v)
|
||||
u_to_v = v * inner
|
||||
return
|
||||
}
|
||||
|
||||
// Anti-wedge of vectors
|
||||
regress_vec3 :: proc "contextless" ( a, b : Vec3 ) -> f32 {
|
||||
return a.x * b.y - a.y * b.x
|
||||
}
|
||||
|
||||
reject_v3_unitv3 :: proc "contextless" ( v : Vec3, u : UnitVec3 ) -> ( v_from_u : Vec3) {
|
||||
inner := dot(v, u)
|
||||
v_from_u = (v - (transmute(Vec3) u)) * inner
|
||||
return
|
||||
}
|
||||
reject_unitv3_v3 :: proc "contextless" ( v : Vec3, u : UnitVec3 ) -> ( u_from_v : Vec3) {
|
||||
inner := dot(u, v)
|
||||
u_from_v = ((transmute(Vec3) u) - v) * inner
|
||||
return
|
||||
}
|
||||
|
||||
// Combines the deimensions that are present in a & b
|
||||
wedge_vec3 :: proc "contextless" (a, b : Vec3) -> (bv : Bivec3) {
|
||||
yzx_zxy := a.yzx * b.zxy
|
||||
zxy_yzx := a.zxy * b.yzx
|
||||
bv = transmute(Bivec3) (yzx_zxy - zxy_yzx)
|
||||
return
|
||||
}
|
||||
|
||||
//endregion Vec3
|
||||
|
||||
//region Bivec3
|
||||
bivec3_via_f32s :: #force_inline proc "contextless" (yz, zx, xy : f32) -> Bivec3 {return { xyz = {yz, zx, xy} }}
|
||||
|
||||
complement_bivec3 :: #force_inline proc "contextless" (b : Bivec3) -> Bivec3 {return transmute(Bivec3) b.xyz}
|
||||
|
||||
//region Operations isomoprhic to vectors
|
||||
negate_bivec3 :: #force_inline proc "contextless" (b : Bivec3) -> Bivec3 {return transmute(Bivec3) -b.xyz}
|
||||
add_bivec3 :: #force_inline proc "contextless" (a, b : Bivec3) -> Bivec3 {return transmute(Bivec3) (a.xyz + b.xyz)}
|
||||
sub_bivec3 :: #force_inline proc "contextless" (a, b : Bivec3) -> Bivec3 {return transmute(Bivec3) (a.xyz - b.xyz)}
|
||||
mul_bivec3 :: #force_inline proc "contextless" (a, b : Bivec3) -> Bivec3 {return transmute(Bivec3) (a.xyz * b.xyz)}
|
||||
mul_bivec3_f32 :: #force_inline proc "contextless" (b : Bivec3, s : f32) -> Bivec3 {return transmute(Bivec3) (b.xyz * s)}
|
||||
mul_f32_bivec3 :: #force_inline proc "contextless" (s : f32, b : Bivec3) -> Bivec3 {return transmute(Bivec3) (s * b.xyz)}
|
||||
div_bivec3_f32 :: #force_inline proc "contextless" (b : Bivec3, s : f32) -> Bivec3 {return transmute(Bivec3) (b.xyz / s)}
|
||||
inverse_mag_bivec3 :: #force_inline proc "contextless" (b : Bivec3) -> f32 {return inverse_mag_vec3(b.xyz)}
|
||||
magnitude_bivec3 :: #force_inline proc "contextless" (b : Bivec3) -> f32 {return magnitude_vec3 (b.xyz)}
|
||||
normalize_bivec3 :: #force_inline proc "contextless" (b : Bivec3) -> UnitBivec3 {return transmute(UnitBivec3) normalize_vec3(b.xyz)}
|
||||
squared_mag_bivec3 :: #force_inline proc "contextless" (b : Bivec3) -> f32 {return pow2_vec3(b.xyz)}
|
||||
//endregion Operations isomoprhic to vectors
|
||||
|
||||
// The wedge of a bi-vector in 3D vector space results in a Trivector represented as a scalar.
|
||||
// This scalar usually resolves to zero with six possible exceptions that lead to the negative volume element.
|
||||
wedge_bivec3 :: proc ( a, b : Bivec3 ) -> f32 {
|
||||
s := a.yz + b.yz + a.zx + b.zx + a.xy + b.xy
|
||||
return s
|
||||
}
|
||||
|
||||
// anti-wedge (Combines dimensions that are absent from a & b)
|
||||
regress_bivec3 :: #force_inline proc "contextless" ( a, b : Bivec3 ) -> Vec3 {return wedge_vec3(vec3(a), vec3(b))}
|
||||
regress_bivec3_v :: #force_inline proc "contextless" (b : Bivec3, v : Vec3) -> f32 {return regress_vec3(b.xyz, v)}
|
||||
regress_v3_bivec3 :: #force_inline proc "contextless" (v : Vec3, b : Bivec3) -> f32 {return regress_vec3(b.xyz, v)}
|
||||
|
||||
//endregion Bivec3
|
||||
|
||||
//region Rotor3
|
||||
|
||||
rotor3_via_comps :: proc "contextless" (yz, zx, xy, scalar : f32) -> (rotor : Rotor3) {
|
||||
rotor = Rotor3 {bivec3_via_f32s(yz, zx, xy), scalar}
|
||||
return
|
||||
}
|
||||
|
||||
rotor3_via_bv_s :: proc "contextless" (bv : Bivec3, scalar : f32) -> (rotor : Rotor3) {
|
||||
rotor = Rotor3 {bv, scalar}
|
||||
return
|
||||
}
|
||||
|
||||
rotor3_via_from_to :: proc "contextless" ( from, to : Vec3 ) -> (rotor : Rotor3) {
|
||||
scalar := 1 + dot( from, to )
|
||||
return
|
||||
}
|
||||
|
||||
inverse_mag_rotor3 :: proc "contextless" (rotor : Rotor3) -> (s : f32) {
|
||||
return
|
||||
}
|
||||
|
||||
magnitude_rotor3 :: proc "contextless" (rotor : Rotor3) -> (s : f32) {
|
||||
return
|
||||
}
|
||||
|
||||
squared_mag :: proc "contextless" (rotor : Rotor3) -> (s : f32) {
|
||||
return
|
||||
}
|
||||
|
||||
reverse_rotor3 :: proc "contextless" (rotor : Rotor3) -> (reversed : Rotor3) {
|
||||
reversed = { negate_bivec3(rotor.bv), rotor.s }
|
||||
return
|
||||
}
|
||||
|
||||
//endregion Rotor3
|
||||
|
||||
//region Flat Projective Geometry
|
||||
|
||||
Point3 :: distinct Vec3
|
||||
PointFlat3 :: distinct Vec4
|
||||
Line3 :: struct {
|
||||
weight : Vec3,
|
||||
bulk : Bivec3,
|
||||
}
|
||||
Plane3 :: distinct Vec4 // 4D Anti-vector
|
||||
|
||||
// aka: wedge operation for points
|
||||
join_point3 :: proc "contextless" (p, q : Point3) -> (l : Line3) {
|
||||
weight := sub(q, p)
|
||||
bulk := wedge(vec3(p), vec3(q))
|
||||
l = {weight, bulk}
|
||||
return
|
||||
}
|
||||
|
||||
join_pointflat3 :: proc "contextless" (p, q : PointFlat3) -> (l : Line3) {
|
||||
weight := vec3(
|
||||
p.w * q.x - p.x * q.w,
|
||||
p.w * q.y - p.y * q.w,
|
||||
p.w * q.z - p.z * q.w
|
||||
)
|
||||
bulk := wedge(vec3(p), vec3(q))
|
||||
l = { weight, bulk}
|
||||
return
|
||||
}
|
||||
|
||||
sub_point3 :: proc "contextless" (a, b : Point3) -> (v : Vec3) {
|
||||
v = vec3(a) - vec3(b)
|
||||
return
|
||||
}
|
||||
|
||||
//endregion Flat Projective Geometry
|
||||
|
||||
//region Rational Trig
|
||||
|
||||
quadrance :: proc "contextless" (a, b : Point3) -> (q : f32) {
|
||||
q = pow2( sub(a, b))
|
||||
return
|
||||
}
|
||||
|
||||
// Assumes the weight component is normalized.
|
||||
spread :: proc "contextless" (l, m : Line3) -> (s : f32) {
|
||||
s = dot(l.weight, m.weight)
|
||||
return
|
||||
}
|
||||
|
||||
//endregion Rational Trig
|
24
code/sectr/math/pga3_grime.odin
Normal file
24
code/sectr/math/pga3_grime.odin
Normal file
@ -0,0 +1,24 @@
|
||||
package sectr
|
||||
|
||||
// A dump of equivalent symbol generatioon (because the toolchain can't do it yet)
|
||||
// Symbol alias tables are in grim.odin
|
||||
|
||||
vec3_to_bivec3 :: #force_inline proc "contextless" (v : Vec3) -> Bivec3 {return transmute(Bivec3) v }
|
||||
bivec3_to_vec3 :: #force_inline proc "contextless" (bv : Bivec3) -> Vec3 {return transmute(Vec3) bv }
|
||||
rotor3_to_quat128 :: #force_inline proc "contextless" (rotor : Rotor3) -> Quat128 {return transmute(Quat128) rotor}
|
||||
unitvec3_to_vec3 :: #force_inline proc "contextless" (v : UnitVec3) -> Vec3 {return transmute(Vec3) v }
|
||||
unitvec4_to_vec4 :: #force_inline proc "contextless" (v : UnitVec4) -> Vec4 {return transmute(Vec4) v }
|
||||
|
||||
// plane_to_vec4 :: #force_inline proc "contextless" (p : Plane3) -> Vec4 {return transmute(Vec4) p}
|
||||
point3_to_vec3 :: #force_inline proc "contextless" (p : Point3) -> Vec3 {return transmute(Vec3) p}
|
||||
pointflat3_to_vec3 :: #force_inline proc "contextless" (p : PointFlat3) -> Vec3 {return { p.x, p.y, p.z }}
|
||||
vec3_to_point3 :: #force_inline proc "contextless" (v : Vec3) -> Point3 {return transmute(Point3) v}
|
||||
|
||||
cross_v3_unitv3 :: #force_inline proc "contextless" (v : Vec3, u : UnitVec3) -> Vec3 {return cross_vec3(v, transmute(Vec3) u)}
|
||||
cross_unitv3_vs :: #force_inline proc "contextless" (u : UnitVec3, v : Vec3) -> Vec3 {return cross_vec3(transmute(Vec3) u, v)}
|
||||
|
||||
dot_v3_unitv3 :: #force_inline proc "contextless" (v : Vec3, unit_v : UnitVec3) -> f32 {return dot_vec3(v, transmute(Vec3) unit_v)}
|
||||
dot_unitv3_vs :: #force_inline proc "contextless" (unit_v : UnitVec3, v : Vec3) -> f32 {return dot_vec3(v, transmute(Vec3) unit_v)}
|
||||
|
||||
wedge_v3_unitv3 :: #force_inline proc "contextless" (v : Vec3, unit_v : UnitVec3) -> Bivec3 {return wedge_vec3(v, transmute(Vec3) unit_v)}
|
||||
wedge_unitv3_vs :: #force_inline proc "contextless" (unit_v : UnitVec3, v : Vec3) -> Bivec3 {return wedge_vec3(transmute(Vec3) unit_v, v)}
|
244
code/sectr/parser/code_agnostic.odin
Normal file
244
code/sectr/parser/code_agnostic.odin
Normal file
@ -0,0 +1,244 @@
|
||||
/* Parser: Code Agnostic
|
||||
This is a 'coding langauge agnostic' parser.
|
||||
Its not meant to parse regular textual formats used in natural langauges (paragraphs, sentences, etc).
|
||||
It instead is meant to encode constructs significant to most programming languages.
|
||||
|
||||
AST Types:
|
||||
* Word
|
||||
* Operator
|
||||
* BracketsScope
|
||||
|
||||
This parser supports parsing whitepsace asts or raw text content.
|
||||
Operator tokens are not parsed into expressions (binary or polish) Thats beyond the scope of this parser.
|
||||
*/
|
||||
package sectr
|
||||
|
||||
PA_TokenType :: enum u32 {
|
||||
Invalid,
|
||||
|
||||
B_Literal_Begin,
|
||||
Integer, // 12345
|
||||
Deciaml, // 123.45
|
||||
Word, // Any string of visible characters that doesn't use an operator symbol.
|
||||
B_Literal_End,
|
||||
|
||||
B_Operator_Begin,
|
||||
Ampersand, // &
|
||||
Ampersand_Double, // &&
|
||||
Ampersand_Double_Equal, // &&=
|
||||
Ampersand_Equal, // &=
|
||||
And_Not, // &~
|
||||
And_Not_Equal, // &~=
|
||||
Arrow_Left, // <-
|
||||
Arrow_Right, // ->
|
||||
Asterisk, // *
|
||||
Asterisk_Equal, // *=
|
||||
At, // @
|
||||
Backslash, // \
|
||||
Backslash_Double, // \\
|
||||
Brace_Open, // {
|
||||
Brace_Close, // }
|
||||
Bracket_Open, // [
|
||||
Bracket_Close, // ]
|
||||
Caret, // ^
|
||||
Caret_Equal, // ^=
|
||||
Colon, // :
|
||||
Comma, // ,
|
||||
Dash_Triple, // ---
|
||||
Dollar, // $
|
||||
Ellispis_Dobule, // ..
|
||||
Ellipsis_Triple, // ...
|
||||
Equal, // =
|
||||
Equal_Double, // ==
|
||||
Exclamation, // !
|
||||
Exclamation_Equal, // !=
|
||||
Greater, // >
|
||||
Greater_Double, // >>
|
||||
Greater_Double_Equal, // >>=
|
||||
Greater_Equal, // >=
|
||||
Hash, // #
|
||||
Lesser, // <
|
||||
Lesser_Double, // <<
|
||||
Lesser_Double_Equal, // <<=
|
||||
Lesser_Equal, // <=
|
||||
Minus, // -
|
||||
Minus_Double, // --
|
||||
Minus_Equal, // -=
|
||||
Parenthesis_Open, // (
|
||||
Parenthesis_Close, // )
|
||||
Percent, // %
|
||||
Percent_Equal, // %=
|
||||
Percent_Double, // %%
|
||||
Percent_Dboule_Equal, // %%=
|
||||
Period, // .
|
||||
Plus, // +
|
||||
Plus_Dobule, // ++
|
||||
Plus_Equal, // +=
|
||||
Question, // ?
|
||||
Semi_Colon, // ;
|
||||
Slash, // /
|
||||
Slash_Equal, // /=
|
||||
Slash_Double, //
|
||||
Tilde, // ~
|
||||
Tilde_Equal, // ~=
|
||||
Vert_Bar, // |
|
||||
Vert_Bar_Double, // ||
|
||||
Vert_Bar_Equal, // |=
|
||||
Vert_Bar_Double_Equal, // |==
|
||||
B_Operator_End,
|
||||
|
||||
Count,
|
||||
}
|
||||
|
||||
PA_Token_Str_Table := [PA_TokenType.Count] string {
|
||||
"____Invalid____", // Invalid,
|
||||
|
||||
"____B_Literal_Begin____", // B_Literal_Begin,
|
||||
"____Integer____", // Integer, // 12345
|
||||
"____Deciaml____", // 123.45
|
||||
"____Word____", // Any string of visible characters that doesn't use an operator symbol.
|
||||
"____B_Literal_Begin____", // B_Literal_End,
|
||||
|
||||
"____B_Operator_Begin____", // B_Operator_Begin,
|
||||
"&", // Ampersand, // &
|
||||
"&&", // Ampersand_Double, // &&
|
||||
"&&=", // Ampersand_Double_Equal, // &&=
|
||||
"&=", // Ampersand_Equal, // &=
|
||||
"&~", // And_Not, // &~
|
||||
"&~=", // And_Not_Equal, // &~=
|
||||
"<-", // Arrow_Left, // <-
|
||||
"->", // Arrow_Right, // ->
|
||||
"*", // Asterisk, // *
|
||||
"*=", // Asterisk_Equal, // *=
|
||||
"@", // At, // @
|
||||
"\\", // Backslash, // \
|
||||
"\\\\", // Backslash_Double, // \\
|
||||
"{", // Brace_Open, // {
|
||||
"}", // Brace_Close, // }
|
||||
"[", // Bracket_Open, // [
|
||||
"]", // Bracket_Close, // ]
|
||||
"^", // Caret, // ^
|
||||
"^=", // Caret_Equal, // ^=
|
||||
":", // Colon, // :
|
||||
",", // Comma, // ,
|
||||
"---", // Dash_Triple, // ---
|
||||
"$", // Dollar, // $
|
||||
"..", // Ellispis_Dobule, // ..
|
||||
"...", // Ellipsis_Triple, // ...
|
||||
"=", // Equal, // =
|
||||
"==", // Equal_Double, // ==
|
||||
"!", // Exclamation, // !
|
||||
"!=", // Exclamation_Equal, // !=
|
||||
">", // Greater, // >
|
||||
">>", // Greater_Double, // >>
|
||||
">>=", // Greater_Double_Equal, // >>=
|
||||
">=", // Greater_Equal, // >=
|
||||
"#", // Hash, // #
|
||||
"<", // Lesser, // <
|
||||
"<<", // Lesser_Double, // <<
|
||||
"<<=", // Lesser_Double_Equal, // <<=
|
||||
"<=", // Lesser_Equal, // <=
|
||||
"-", // Minus, // -
|
||||
"--", // Minus_Double, // --
|
||||
"-=", // Minus_Equal, // -=
|
||||
"(", // Parenthesis_Open, // (
|
||||
")", // Parenthesis_Close, // )
|
||||
"%", // Percent, // %
|
||||
"%=", // Percent_Equal, // %=
|
||||
"%%", // Percent_Double, // %%
|
||||
"%%=", // Percent_Dboule_Equal, // %%=
|
||||
".", // Period, // .
|
||||
"+", // Plus, // +
|
||||
"++", // Plus_Dobule, // ++
|
||||
"+=", // Plus_Equal, // +=
|
||||
"?", // Question, // ?
|
||||
";", // Semi_Colon, // ;
|
||||
"/", // Slash, // /
|
||||
"/=", // Slash_Equal, // /=
|
||||
"//", // Slash_Double, //
|
||||
"~", // Tilde, // ~
|
||||
"~=", // Tilde_Equal, // ~=
|
||||
"|", // Vert_Bar, // |
|
||||
"||", // Vert_Bar_Double, // ||
|
||||
"|=", // Vert_Bar_Equal, // |=
|
||||
"//=", // Vert_Bar_Double_Equal, //=
|
||||
"____B_Operator_End____", // B_Operator_End,
|
||||
}
|
||||
|
||||
PA_Token :: struct {
|
||||
type : PA_TokenType,
|
||||
line, column : u32,
|
||||
ptr : ^rune,
|
||||
}
|
||||
|
||||
PA_LiteralType :: enum u32 {
|
||||
Integer,
|
||||
Decimal,
|
||||
Word,
|
||||
}
|
||||
|
||||
PA_Literal :: struct {
|
||||
type : PA_LiteralType,
|
||||
token : ^PA_Token,
|
||||
}
|
||||
|
||||
PA_OperatorType :: enum u32 {
|
||||
|
||||
}
|
||||
|
||||
PA_Operator :: struct {
|
||||
type : PA_OperatorType,
|
||||
token : ^PA_Token,
|
||||
}
|
||||
|
||||
PA_BracketScopeType :: enum u32 {
|
||||
Angled,
|
||||
Curly,
|
||||
Square,
|
||||
Round,
|
||||
}
|
||||
|
||||
PA_BracketScope :: struct {
|
||||
type : PA_BracketScopeType,
|
||||
token : ^PA_Token,
|
||||
body : ^PA_AST,
|
||||
}
|
||||
|
||||
PA_AST :: union {
|
||||
|
||||
}
|
||||
|
||||
// Changes parse behavior for specific tokens.
|
||||
PA_ParsePolicy :: struct {
|
||||
scope_detect_angled : b8,
|
||||
scope_detect_curly : b8,
|
||||
scope_detect_square : b8,
|
||||
scope_detect_round : b8,
|
||||
}
|
||||
|
||||
PA_ParseError :: struct {
|
||||
token : ^ PA_Token,
|
||||
msg : string,
|
||||
}
|
||||
|
||||
PA_ParseError_Max :: 32
|
||||
PA_NodeArray_ReserveSize :: 4 * Kilobyte
|
||||
|
||||
PA_ParseResult :: struct {
|
||||
content : string,
|
||||
runes : []rune,
|
||||
tokens : Array(PA_Token),
|
||||
pws_ast : ^PWS_AST,
|
||||
nodes : Array(PA_AST), // Switch this to a pool?
|
||||
errors : [PA_ParseError_Max] PA_ParseError
|
||||
}
|
||||
|
||||
pa_parse_text :: proc( content : string, allocator : Allocator ) -> ( PA_ParseResult, AllocatorError )
|
||||
{
|
||||
return {}, AllocatorError.None
|
||||
}
|
||||
|
||||
pa_parse_ws_ast :: proc( ast : ^PWS_AST, allocator : Allocator ) -> ( PA_ParseResult, AllocatorError )
|
||||
{
|
||||
return {}, AllocatorError.None
|
||||
}
|
14
code/sectr/parser/code_formatting.odin
Normal file
14
code/sectr/parser/code_formatting.odin
Normal file
@ -0,0 +1,14 @@
|
||||
/* Parser : Code Formatting
|
||||
This is a prototype parser meant to parse whitespace formatting constructs used in text based languages.
|
||||
These include indentation of a block, spacial alignment of similar statement components, etc.
|
||||
|
||||
This would be used to have awareness of constructs having associating with each other via formatting.
|
||||
|
||||
AST Types:
|
||||
|
||||
* Statement
|
||||
* Block-Indent Group
|
||||
* Aligned-Statements
|
||||
|
||||
*/
|
||||
package sectr
|
17
code/sectr/parser/odin_wysiwyg.odin
Normal file
17
code/sectr/parser/odin_wysiwyg.odin
Normal file
@ -0,0 +1,17 @@
|
||||
/* Parser: Odin Frontend AST (WYSIWYG)
|
||||
This is a parser to generate and manage a WYSIWYG variant of an Odin AST.
|
||||
The AST is naturally meant to be used for frontend interface, not backend.
|
||||
*/
|
||||
package sectr
|
||||
|
||||
PF_Odin_TokenType :: enum u32 {
|
||||
placeholder,
|
||||
}
|
||||
|
||||
POdin_Token :: struct {
|
||||
placeholder : int,
|
||||
}
|
||||
|
||||
POdin_AST :: struct {
|
||||
placeholder : int,
|
||||
}
|
374
code/sectr/parser/whitespace.odin
Normal file
374
code/sectr/parser/whitespace.odin
Normal file
@ -0,0 +1,374 @@
|
||||
/* Parser: Whitespace
|
||||
This is a prototype parser meant to only parse whitespace from visible blocks of code.
|
||||
Its meant to be the most minimal useful AST with coupling to traditional text file formatting.
|
||||
|
||||
All symbols related directly to the parser are prefixed with the PWS_ namespace.
|
||||
|
||||
The AST is composed of the following node types:
|
||||
* Visible
|
||||
* Spaces
|
||||
* Tabs
|
||||
* Line
|
||||
|
||||
AST_Visible tracks a slice of visible codepoints.
|
||||
It tracks a neighboring ASTs (left or right) which should always be Spaces, or Tabs.
|
||||
|
||||
AST_Spaces tracks a slice of singluar or consecutive Spaces.
|
||||
Neighboring ASTS should either be Visible, Tabs.
|
||||
|
||||
AST_Tabs tracks a slice of singlar or consectuive Tabs.
|
||||
Neighboring ASTS should be either Visible or Spaces.
|
||||
|
||||
AST_Line tracks a slice of AST nodes of Visible, Spaces, or Tabs that terminate with a New-Line token.
|
||||
Neighboring ASTS are only Lines.
|
||||
|
||||
The ParseData struct will contain an Array of AST_Line. This represents the entire AST where the root is the first entry.
|
||||
ASTs keep track of neighboring ASTs in double-linked list pattern for ease of use.
|
||||
This may be removed in the future for perforamance reasons,
|
||||
since this is a prototype it will only be removed if there is a performance issue.
|
||||
|
||||
Because this parser is so primtive, it can only be
|
||||
manually constructed via an AST editor or from parsed text.
|
||||
So there is only a parser directly dealing with text.
|
||||
|
||||
If its constructed from an AST-Editor. There will not be a content string referencable or runes derived fromt hat content string.
|
||||
Instead the AST's content will directly contain the runes associated.
|
||||
*/
|
||||
package sectr
|
||||
|
||||
import "core:os"
|
||||
|
||||
Rune_Space :: ' '
|
||||
Rune_Tab :: '\t'
|
||||
Rune_Carriage_Return :: '\r'
|
||||
Rune_Line_Feed :: '\n'
|
||||
// Rune_Tab_Vertical :: '\v'
|
||||
|
||||
PWS_TokenType :: enum u32 {
|
||||
Invalid,
|
||||
Visible,
|
||||
Spaces,
|
||||
Tabs,
|
||||
New_Line,
|
||||
End_Of_File,
|
||||
Count,
|
||||
}
|
||||
|
||||
// TODO(Ed) : The runes and token arrays should be handled by a slab allocator
|
||||
// This can grow in undeterministic ways, persistent will get very polluted otherwise.
|
||||
PWS_LexResult :: struct {
|
||||
tokens : Array(PWS_Token),
|
||||
}
|
||||
|
||||
PWS_Token :: struct {
|
||||
type : PWS_TokenType,
|
||||
line, column : u32,
|
||||
content : StrRunesPair,
|
||||
}
|
||||
|
||||
PWS_AST_Type :: enum u32 {
|
||||
Invalid,
|
||||
Visible,
|
||||
Spaces,
|
||||
Tabs,
|
||||
Line,
|
||||
Count,
|
||||
}
|
||||
|
||||
PWS_AST :: struct {
|
||||
using links : DLL_NodeFull(PWS_AST),
|
||||
type : PWS_AST_Type,
|
||||
|
||||
line, column : u32,
|
||||
content : StrRunesPair,
|
||||
}
|
||||
|
||||
PWS_ParseError :: struct {
|
||||
token : ^PWS_Token,
|
||||
msg : string,
|
||||
}
|
||||
|
||||
PWS_ParseError_Max :: 32
|
||||
PWS_TokenArray_ReserveSize :: 128
|
||||
PWS_NodeArray_ReserveSize :: 32 * Kilobyte
|
||||
PWS_LineArray_ReserveSize :: 32
|
||||
|
||||
// TODO(Ed) : The ast arrays should be handled by a slab allocator dedicated to PWS_ASTs
|
||||
// This can grow in undeterministic ways, persistent will get very polluted otherwise.
|
||||
PWS_ParseResult :: struct {
|
||||
content : string,
|
||||
tokens : Array(PWS_Token),
|
||||
nodes : Array(PWS_AST), // Nodes should be dumped in a pool.
|
||||
lines : Array( ^PWS_AST),
|
||||
errors : [PWS_ParseError_Max] PWS_ParseError,
|
||||
}
|
||||
|
||||
PWS_LexerData :: struct {
|
||||
using result : PWS_LexResult,
|
||||
|
||||
content : string,
|
||||
previous_rune : rune,
|
||||
current_rune : rune,
|
||||
previous : PWS_TokenType,
|
||||
line : u32,
|
||||
column : u32,
|
||||
start : int,
|
||||
length : int,
|
||||
current : PWS_Token,
|
||||
}
|
||||
|
||||
pws_parser_lex :: proc ( text : string, allocator : Allocator ) -> ( PWS_LexResult, AllocatorError )
|
||||
{
|
||||
bytes := transmute([]byte) text
|
||||
log( str_fmt_tmp( "lexing: %v ...", (len(text) > 30 ? transmute(string) bytes[ :30] : text) ))
|
||||
|
||||
profile(#procedure)
|
||||
using lexer : PWS_LexerData
|
||||
context.user_ptr = & lexer
|
||||
content = text
|
||||
|
||||
if len(text) == 0 {
|
||||
ensure( false, "Attempted to lex nothing")
|
||||
return result, .None
|
||||
}
|
||||
|
||||
rune_type :: proc( codepoint : rune ) -> PWS_TokenType
|
||||
{
|
||||
using self := context_ext( PWS_LexerData)
|
||||
|
||||
switch codepoint
|
||||
{
|
||||
case Rune_Space:
|
||||
return PWS_TokenType.Spaces
|
||||
|
||||
case Rune_Tab:
|
||||
return PWS_TokenType.Tabs
|
||||
|
||||
case Rune_Line_Feed:
|
||||
return PWS_TokenType.New_Line
|
||||
|
||||
// Support for CRLF format
|
||||
case Rune_Carriage_Return:
|
||||
{
|
||||
if previous_rune == 0 {
|
||||
return PWS_TokenType.Invalid
|
||||
}
|
||||
|
||||
// Assume for now its a new line
|
||||
return PWS_TokenType.New_Line
|
||||
}
|
||||
}
|
||||
|
||||
// Everything that isn't the supported whitespace code points is considered 'visible'
|
||||
// Eventually we should support other types of whitespace
|
||||
return PWS_TokenType.Visible
|
||||
}
|
||||
|
||||
alloc_error : AllocatorError
|
||||
// tokens, alloc_error = array_init_reserve( PWS_Token, allocator, Kilobyte * 4 )
|
||||
tokens, alloc_error = array_init_reserve( PWS_Token, allocator, PWS_TokenArray_ReserveSize )
|
||||
if alloc_error != AllocatorError.None {
|
||||
ensure(false, "Failed to allocate token's array")
|
||||
return result, alloc_error
|
||||
}
|
||||
|
||||
line = 0
|
||||
column = 0
|
||||
|
||||
make_token :: proc ( byte_offset : int ) -> AllocatorError
|
||||
{
|
||||
self := context_ext( PWS_LexerData); using self
|
||||
|
||||
if previous_rune == Rune_Carriage_Return && current_rune != Rune_Line_Feed {
|
||||
ensure(false, "Rouge Carriage Return")
|
||||
}
|
||||
|
||||
start_ptr := uintptr( raw_data(content)) + uintptr(start)
|
||||
token_slice := transmute(string) byte_slice( rawptr(start_ptr), length )
|
||||
|
||||
current.content = str_intern( token_slice )
|
||||
|
||||
start = byte_offset
|
||||
length = 0
|
||||
line += cast(u32) (current.type == .New_Line)
|
||||
column = 0
|
||||
|
||||
return array_append( & tokens, current )
|
||||
}
|
||||
|
||||
last_rune : rune
|
||||
last_byte_offset : int
|
||||
for codepoint, byte_offset in text
|
||||
{
|
||||
type := rune_type( codepoint )
|
||||
current_rune = codepoint
|
||||
|
||||
if (current.type != type && previous != .Invalid) ||
|
||||
( previous_rune != Rune_Carriage_Return && current.type == .New_Line )
|
||||
{
|
||||
alloc_error = make_token( byte_offset )
|
||||
if alloc_error != AllocatorError.None {
|
||||
ensure(false, "Failed to append token to token array")
|
||||
return lexer, alloc_error
|
||||
}
|
||||
}
|
||||
|
||||
current.type = type
|
||||
current.line = line
|
||||
current.column = column
|
||||
|
||||
column += 1
|
||||
length += 1
|
||||
previous = current.type
|
||||
previous_rune = codepoint
|
||||
last_byte_offset = byte_offset
|
||||
}
|
||||
|
||||
make_token( last_byte_offset )
|
||||
|
||||
return result, alloc_error
|
||||
}
|
||||
|
||||
PWS_ParseData :: struct {
|
||||
using result : PWS_ParseResult,
|
||||
|
||||
left : u32,
|
||||
head : [^]PWS_Token,
|
||||
line : PWS_AST,
|
||||
prev_line : ^PWS_AST,
|
||||
}
|
||||
|
||||
pws_parser_parse :: proc( text : string, allocator : Allocator ) -> ( PWS_ParseResult, AllocatorError )
|
||||
{
|
||||
bytes := transmute([]byte) text
|
||||
|
||||
profile(#procedure)
|
||||
using parser : PWS_ParseData
|
||||
context.user_ptr = & result
|
||||
|
||||
if len(text) == 0 {
|
||||
ensure( false, "Attempted to lex nothing")
|
||||
return result, .None
|
||||
}
|
||||
|
||||
lex, alloc_error := pws_parser_lex( text, allocator = allocator )
|
||||
verify( alloc_error == nil, "Allocation faiure in lex")
|
||||
|
||||
tokens = lex.tokens
|
||||
|
||||
log( str_fmt_tmp( "parsing: %v ...", (len(text) > 30 ? transmute(string) bytes[ :30] : text) ))
|
||||
|
||||
// TODO(Ed): Change this to use a node pool
|
||||
nodes, alloc_error = array_init_reserve( PWS_AST, allocator, PWS_NodeArray_ReserveSize )
|
||||
verify( alloc_error == nil, "Allocation failure creating nodes array")
|
||||
|
||||
parser.lines, alloc_error = array_init_reserve( ^PWS_AST, allocator, PWS_LineArray_ReserveSize )
|
||||
verify( alloc_error == nil, "Allocation failure creating line array")
|
||||
|
||||
//region Helper procs
|
||||
eat_line :: #force_inline proc()
|
||||
{
|
||||
self := context_ext( PWS_ParseData); using self
|
||||
tok := cast( ^PWS_Token) head
|
||||
|
||||
line.type = .Line
|
||||
line.line = tok.line
|
||||
line.column = tok.column
|
||||
line.content = tok.content
|
||||
|
||||
alloc_error := array_append( & nodes, line )
|
||||
verify( alloc_error == nil, "Allocation failure appending node")
|
||||
node := & nodes.data[ nodes.num - 1 ]
|
||||
|
||||
// TODO(Ed): Review this with multiple line test
|
||||
dll_push_back( & prev_line, node )
|
||||
prev_line = node
|
||||
|
||||
// Debug build compile error
|
||||
// alloc_error = array_append( & lines, prev_line )
|
||||
// verify( alloc_error == nil, "Allocation failure appending node")
|
||||
|
||||
line = {}
|
||||
}
|
||||
//endregion
|
||||
|
||||
head = & tokens.data[0]
|
||||
left = u32(tokens.num)
|
||||
|
||||
// Parse Line
|
||||
for ; left > 0;
|
||||
{
|
||||
type : PWS_AST_Type
|
||||
#partial switch head[0].type
|
||||
{
|
||||
case .Tabs:
|
||||
type = .Tabs
|
||||
|
||||
case .Spaces:
|
||||
type = .Spaces
|
||||
|
||||
case .Visible:
|
||||
type = .Visible
|
||||
|
||||
case .New_Line:
|
||||
eat_line()
|
||||
|
||||
alloc_error = array_append( & parser.lines, prev_line )
|
||||
verify( alloc_error == nil, "Allocation failure appending node")
|
||||
|
||||
case PWS_TokenType.End_Of_File:
|
||||
}
|
||||
|
||||
if type != .Line
|
||||
{
|
||||
tok := cast( ^PWS_Token) head
|
||||
ast : PWS_AST
|
||||
ast.type = type
|
||||
ast.line = tok.line
|
||||
ast.column = tok.column
|
||||
ast.content = tok.content
|
||||
|
||||
// Compiler Error (-Debug)
|
||||
// prev_node = array_back( nodes )
|
||||
prev_node : ^PWS_AST = nil
|
||||
if nodes.num > 0 {
|
||||
prev_node = & nodes.data[ nodes.num - 1 ]
|
||||
}
|
||||
|
||||
alloc_error := array_append( & nodes, ast )
|
||||
verify( alloc_error == nil, "Allocation failure appending node")
|
||||
|
||||
node := & nodes.data[ nodes.num - 1 ]
|
||||
|
||||
// dll_push_back( & prev_node, last_node )
|
||||
{
|
||||
if prev_node != nil
|
||||
{
|
||||
node.prev = prev_node
|
||||
prev_node.next = node
|
||||
}
|
||||
}
|
||||
|
||||
// dll_fl_append( & line, last_node )
|
||||
if line.first == nil {
|
||||
line.first = node
|
||||
line.last = node
|
||||
}
|
||||
else {
|
||||
line.last = node
|
||||
}
|
||||
}
|
||||
|
||||
head = head[ 1:]
|
||||
left -= 1
|
||||
}
|
||||
|
||||
if line.first != nil {
|
||||
eat_line()
|
||||
|
||||
alloc_error = array_append( & parser.lines, prev_line )
|
||||
verify( alloc_error == nil, "Allocation failure appending node")
|
||||
}
|
||||
|
||||
return result, alloc_error
|
||||
}
|
23
code/sectr/project/manual_serialization.odin
Normal file
23
code/sectr/project/manual_serialization.odin
Normal file
@ -0,0 +1,23 @@
|
||||
package sectr
|
||||
|
||||
import "core:encoding/json"
|
||||
import "core:reflect"
|
||||
|
||||
// TODO(Ed) : Generic Unmarshling of json objects (There should be a way I believe todo this generically but the reflect library is not well documented)
|
||||
|
||||
vec2_json_unmarshal :: proc( value : ^ json.Value ) -> Vec2 {
|
||||
json_v := value.(json.Array)
|
||||
return {
|
||||
f32(json_v[0].(json.Float)),
|
||||
f32(json_v[1].(json.Float)),
|
||||
}
|
||||
}
|
||||
|
||||
color_json_unmarshal :: proc( value : ^ json.Value ) -> Color {
|
||||
json_color := value.(json.Array)
|
||||
r := u8(json_color[0].(json.Float))
|
||||
g := u8(json_color[1].(json.Float))
|
||||
b := u8(json_color[2].(json.Float))
|
||||
a := u8(json_color[3].(json.Float))
|
||||
return { r, g, b, a }
|
||||
}
|
26
code/sectr/project/project.odin
Normal file
26
code/sectr/project/project.odin
Normal file
@ -0,0 +1,26 @@
|
||||
package sectr
|
||||
|
||||
/*
|
||||
Project: Encapsulation of all things a user can do separate from the core app behavior
|
||||
that is managed independetly of it.
|
||||
*/
|
||||
|
||||
// PMDB
|
||||
CodeBase :: struct {
|
||||
placeholder : int,
|
||||
}
|
||||
|
||||
ProjectConfig :: struct {
|
||||
placeholder : int,
|
||||
}
|
||||
|
||||
Project :: struct {
|
||||
path : StrRunesPair,
|
||||
name : StrRunesPair,
|
||||
|
||||
config : ProjectConfig,
|
||||
codebase : CodeBase,
|
||||
|
||||
// TODO(Ed) : Support multiple workspaces
|
||||
workspace : Workspace,
|
||||
}
|
186
code/sectr/project/serialize.odin
Normal file
186
code/sectr/project/serialize.odin
Normal file
@ -0,0 +1,186 @@
|
||||
package sectr
|
||||
|
||||
import "core:encoding/json"
|
||||
import "core:fmt"
|
||||
import "core:os"
|
||||
import "core:reflect"
|
||||
import "base:runtime"
|
||||
import "core:strings"
|
||||
|
||||
@(private="file")
|
||||
assign_int :: proc(val: any, i: $T) -> bool {
|
||||
v := reflect.any_core(val)
|
||||
switch &dst in v {
|
||||
case i8: dst = i8 (i)
|
||||
case i16: dst = i16 (i)
|
||||
case i16le: dst = i16le (i)
|
||||
case i16be: dst = i16be (i)
|
||||
case i32: dst = i32 (i)
|
||||
case i32le: dst = i32le (i)
|
||||
case i32be: dst = i32be (i)
|
||||
case i64: dst = i64 (i)
|
||||
case i64le: dst = i64le (i)
|
||||
case i64be: dst = i64be (i)
|
||||
case i128: dst = i128 (i)
|
||||
case i128le: dst = i128le (i)
|
||||
case i128be: dst = i128be (i)
|
||||
case u8: dst = u8 (i)
|
||||
case u16: dst = u16 (i)
|
||||
case u16le: dst = u16le (i)
|
||||
case u16be: dst = u16be (i)
|
||||
case u32: dst = u32 (i)
|
||||
case u32le: dst = u32le (i)
|
||||
case u32be: dst = u32be (i)
|
||||
case u64: dst = u64 (i)
|
||||
case u64le: dst = u64le (i)
|
||||
case u64be: dst = u64be (i)
|
||||
case u128: dst = u128 (i)
|
||||
case u128le: dst = u128le (i)
|
||||
case u128be: dst = u128be (i)
|
||||
case int: dst = int (i)
|
||||
case uint: dst = uint (i)
|
||||
case uintptr: dst = uintptr(i)
|
||||
case: return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
when false {
|
||||
unmarshal_from_object :: proc( $Type: typeid, object : json.Object ) -> Type
|
||||
{
|
||||
result : Type
|
||||
type_info := type_info_of(Type)
|
||||
#partial switch type in type_info.variant {
|
||||
case runtime.Type_Info_Union:
|
||||
ensure( false, "This proc doesn't support raw unions" )
|
||||
}
|
||||
|
||||
base_ptr := uintptr( & result )
|
||||
|
||||
field_infos := reflect.struct_fields_zipped(Type)
|
||||
for field_info in field_infos
|
||||
{
|
||||
field_type := field_info.type.id
|
||||
field_ptr := cast(field_type) rawptr( base_ptr + field_info.offset )
|
||||
|
||||
#partial switch type in field_info.type.variant {
|
||||
case runtime.Type_Info_Integer:
|
||||
field_ptr = object[filed_info.name].(json.Integer)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
Serializer_Version :: 1
|
||||
Serializer_Loading :: false
|
||||
|
||||
ArchiveData :: struct {
|
||||
data : [] byte,
|
||||
version : i32,
|
||||
}
|
||||
|
||||
archive_init_temp :: proc() -> ^ ArchiveData {
|
||||
archive := new( ArchiveData, context.temp_allocator )
|
||||
archive.version = Serializer_Version
|
||||
return archive
|
||||
}
|
||||
|
||||
state_serialize :: proc( archive : ^ ArchiveData = nil ) {
|
||||
// TODO(Ed): We'll need this for a better save/load snapshot setup.
|
||||
}
|
||||
|
||||
project_serialize :: proc( project : ^ Project, archive : ^ ArchiveData, is_writting : b32 = true )
|
||||
{
|
||||
options : json.Marshal_Options
|
||||
options.spec = json.Specification.MJSON
|
||||
options.indentation = 2
|
||||
options.pretty = true
|
||||
options.use_spaces = false
|
||||
|
||||
MarshalArchive :: struct {
|
||||
version : i32,
|
||||
project : Project
|
||||
}
|
||||
|
||||
if is_writting
|
||||
{
|
||||
marshal_archive := new(MarshalArchive, frame_slab_allocator())
|
||||
marshal_archive.version = archive.version
|
||||
marshal_archive.project = project^
|
||||
// TODO(Ed): In the future this will be more complicated, as serialization of workspaces and the code database won't be trivial
|
||||
|
||||
json_data, marshal_code := json.marshal( marshal_archive, options, allocator = context.temp_allocator )
|
||||
verify( marshal_code == json.Marshal_Data_Error.None, "Failed to marshal the project to JSON" )
|
||||
|
||||
archive.data = json_data
|
||||
}
|
||||
else
|
||||
{
|
||||
parsed_json, parse_code := json.parse( archive.data, json.Specification.MJSON, allocator = context.temp_allocator )
|
||||
verify( parse_code == json.Error.None, "Failed to parse project JSON")
|
||||
|
||||
archive_json := parsed_json.(json.Object)
|
||||
archive_version : i32 = cast(i32) archive_json["version"].(json.Float)
|
||||
verify( Serializer_Version == archive_version, "Version mismatch on archive!" )
|
||||
|
||||
// Note(Ed) : This works fine for now, but eventually it will most likely break with pointers...
|
||||
// We'll most likely set things up so that all refs in the project & workspace are handles.
|
||||
marshal_archive := new(MarshalArchive, frame_slab_allocator())
|
||||
json.unmarshal( archive.data, & marshal_archive, spec = json.Specification.MJSON, allocator = context.temp_allocator )
|
||||
if marshal_archive.version == Serializer_Version {
|
||||
project^ = marshal_archive.project
|
||||
}
|
||||
|
||||
// Manual unmarshal
|
||||
when false
|
||||
{
|
||||
project_json := archive_json["project"].(json.Object)
|
||||
project.name = project_json["name"].(json.String)
|
||||
|
||||
// TODO(Ed) : Make this a separate proc
|
||||
workspace_json := project_json["workspace"].(json.Object)
|
||||
{
|
||||
using project.workspace
|
||||
name = workspace_json["name"].(json.String)
|
||||
|
||||
// cam = unmarshal_from_object(Camera, workspace_json["camera"].(json.Object) )
|
||||
frame_1 = frame_json_unmarshal( & workspace_json["frame_1"] )
|
||||
}
|
||||
}
|
||||
|
||||
// DEBUG DUD
|
||||
options.use_spaces = false
|
||||
}
|
||||
}
|
||||
|
||||
project_save :: proc( project : ^ Project, archive : ^ ArchiveData = nil )
|
||||
{
|
||||
archive := archive
|
||||
if archive == nil {
|
||||
archive = archive_init_temp()
|
||||
}
|
||||
project_serialize( project, archive )
|
||||
|
||||
if ! os.is_dir( project.path.str ) {
|
||||
os.make_directory( project.path.str )
|
||||
verify( cast(b32) os.is_dir( project.path.str ), "Failed to create project path for saving" )
|
||||
}
|
||||
|
||||
os.write_entire_file( str_tmp_from_any( project.path.str, project.name.str, ".sectr_proj", sep = ""), archive.data )
|
||||
}
|
||||
|
||||
project_load :: proc( path : string, project : ^ Project, archive : ^ ArchiveData = nil )
|
||||
{
|
||||
archive := archive
|
||||
if archive == nil {
|
||||
archive = archive_init_temp()
|
||||
}
|
||||
|
||||
data, read_code := os.read_entire_file( path, context.temp_allocator )
|
||||
verify( b32(read_code), "Failed to read from project file" )
|
||||
|
||||
archive.data = data
|
||||
project_serialize( project, archive, Serializer_Loading )
|
||||
}
|
38
code/sectr/project/workspace.odin
Normal file
38
code/sectr/project/workspace.odin
Normal file
@ -0,0 +1,38 @@
|
||||
/*
|
||||
Workspace : A canvas for compositoning a view for the codebase along with notes.
|
||||
|
||||
Each workspace viewport supports both a canvas composition of code frames
|
||||
or frame tiling towards the application's screenspace.
|
||||
*/
|
||||
package sectr
|
||||
|
||||
Workspace :: struct {
|
||||
name : StrRunesPair,
|
||||
|
||||
cam : Camera,
|
||||
zoom_target : f32,
|
||||
|
||||
frames : Array(Frame),
|
||||
|
||||
test_frame : Frame,
|
||||
|
||||
// TODO(Ed) : The workspace is mainly a 'UI' conceptually...
|
||||
ui : UI_State,
|
||||
}
|
||||
|
||||
// Top level widgets for the workspace
|
||||
Frame :: struct {
|
||||
pos : Vec2,
|
||||
size : Vec2,
|
||||
|
||||
ui : UI_Widget,
|
||||
}
|
||||
|
||||
CodeFrame :: struct {
|
||||
readonly : b32, // Should this frame allow editing?
|
||||
|
||||
}
|
||||
|
||||
NoteFrame :: struct {
|
||||
|
||||
}
|
243
code/sectr/space.odin
Normal file
243
code/sectr/space.odin
Normal file
@ -0,0 +1,243 @@
|
||||
/* Space
|
||||
|
||||
Provides various definitions for converting from one standard of measurement to another.
|
||||
|
||||
Ultimately the user's window ppcm (pixels-per-centimeter) determins how all virtual metric conventions are handled.
|
||||
*/
|
||||
package sectr
|
||||
|
||||
import rl "vendor:raylib"
|
||||
|
||||
// The points to pixels and pixels to points are our only reference to accurately converting
|
||||
// an object from world space to screen-space.
|
||||
// This prototype engine will have all its spacial unit base for distances in virtual pixels.
|
||||
|
||||
Inches_To_CM :: cast(f32) 2.54
|
||||
Points_Per_CM :: cast(f32) 28.3465
|
||||
CM_Per_Point :: cast(f32) 1.0 / DPT_DPCM
|
||||
CM_Per_Pixel :: cast(f32) 1.0 / DPT_PPCM
|
||||
DPT_DPCM :: cast(f32) 72.0 * Inches_To_CM // 182.88 points/dots per cm
|
||||
DPT_PPCM :: cast(f32) 96.0 * Inches_To_CM // 243.84 pixels per cm
|
||||
|
||||
when ODIN_OS == OS_Type.Windows {
|
||||
op_default_dpcm :: 72.0 * Inches_To_CM
|
||||
os_default_ppcm :: 96.0 * Inches_To_CM
|
||||
// 1 inch = 2.54 cm, 96 inch * 2.54 = 243.84 DPCM
|
||||
}
|
||||
|
||||
//region Unit Conversion Impl
|
||||
|
||||
// cm_to_points :: proc( cm : f32 ) -> f32 {
|
||||
|
||||
// }
|
||||
|
||||
// points_to_cm :: proc( points : f32 ) -> f32 {
|
||||
// screen_dpc := get_state().app_window.dpc
|
||||
// cm_per_pixel := 1.0 / screen_dpc
|
||||
// pixels := points * DPT_DPC * cm_per_pixel
|
||||
// return points *
|
||||
// }
|
||||
|
||||
f32_cm_to_pixels :: #force_inline proc "contextless"(cm: f32) -> f32 {
|
||||
screen_ppcm := get_state().app_window.ppcm
|
||||
return cm * screen_ppcm
|
||||
}
|
||||
|
||||
f32_pixels_to_cm :: #force_inline proc "contextless"(pixels: f32) -> f32 {
|
||||
screen_ppcm := get_state().app_window.ppcm
|
||||
cm_per_pixel := 1.0 / screen_ppcm
|
||||
return pixels * cm_per_pixel
|
||||
}
|
||||
|
||||
f32_points_to_pixels :: #force_inline proc "contextless"(points: f32) -> f32 {
|
||||
screen_ppcm := get_state().app_window.ppcm
|
||||
cm_per_pixel := 1.0 / screen_ppcm
|
||||
return points * DPT_PPCM * cm_per_pixel
|
||||
}
|
||||
|
||||
f32_pixels_to_points :: #force_inline proc "contextless"(pixels: f32) -> f32 {
|
||||
screen_ppcm := get_state().app_window.ppcm
|
||||
cm_per_pixel := 1.0 / screen_ppcm
|
||||
return pixels * cm_per_pixel * Points_Per_CM
|
||||
}
|
||||
|
||||
vec2_cm_to_pixels :: #force_inline proc "contextless"(v: Vec2) -> Vec2 {
|
||||
screen_ppcm := get_state().app_window.ppcm
|
||||
return v * screen_ppcm
|
||||
}
|
||||
|
||||
vec2_pixels_to_cm :: #force_inline proc "contextless"(v: Vec2) -> Vec2 {
|
||||
screen_ppcm := get_state().app_window.ppcm
|
||||
cm_per_pixel := 1.0 / screen_ppcm
|
||||
return v * cm_per_pixel
|
||||
}
|
||||
|
||||
vec2_points_to_pixels :: #force_inline proc "contextless"(vpoints: Vec2) -> Vec2 {
|
||||
screen_ppcm := get_state().app_window.ppcm
|
||||
cm_per_pixel := 1.0 / screen_ppcm
|
||||
return vpoints * DPT_PPCM * cm_per_pixel
|
||||
}
|
||||
|
||||
range2_cm_to_pixels :: #force_inline proc "contextless"( range : Range2 ) -> Range2 {
|
||||
screen_ppcm := get_state().app_window.ppcm
|
||||
result := Range2 { pts = { range.min * screen_ppcm, range.max * screen_ppcm }}
|
||||
return result
|
||||
}
|
||||
|
||||
range2_pixels_to_cm :: #force_inline proc "contextless"( range : Range2 ) -> Range2 {
|
||||
screen_ppcm := get_state().app_window.ppcm
|
||||
cm_per_pixel := 1.0 / screen_ppcm
|
||||
result := Range2 { pts = { range.min * cm_per_pixel, range.max * cm_per_pixel }}
|
||||
return result
|
||||
}
|
||||
|
||||
// vec2_points_to_cm :: proc( vpoints : Vec2 ) -> Vec2 {
|
||||
|
||||
// }
|
||||
|
||||
//endregion
|
||||
|
||||
Camera :: rl.Camera2D
|
||||
|
||||
CameraZoomMode :: enum u32 {
|
||||
Digital,
|
||||
Smooth,
|
||||
}
|
||||
|
||||
// TODO(Ed) : I'm not sure making the size and extent types distinct has made things easier or more difficult in Odin..
|
||||
// The lack of operator overloads is going to make any sort of nice typesystem
|
||||
// for doing lots of math or phyiscs more error prone or filled with proc wrappers
|
||||
AreaSize :: distinct Vec2
|
||||
|
||||
Bounds2 :: struct {
|
||||
top_left, bottom_right: Vec2,
|
||||
}
|
||||
|
||||
BoundsCorners2 :: struct {
|
||||
top_left, top_right, bottom_left, bottom_right: Vec2,
|
||||
}
|
||||
|
||||
Extents2 :: distinct Vec2
|
||||
Extents2i :: distinct Vec2i
|
||||
|
||||
WS_Pos :: struct {
|
||||
tile_id : Vec2i,
|
||||
rel : Vec2,
|
||||
}
|
||||
|
||||
bounds2_radius :: proc(bounds: Bounds2) -> f32 {
|
||||
return max( bounds.bottom_right.x, bounds.top_left.y )
|
||||
}
|
||||
|
||||
extent_from_size :: proc(size: AreaSize) -> Extents2 {
|
||||
return transmute(Extents2) size * 2.0
|
||||
}
|
||||
|
||||
screen_size :: proc "contextless" () -> AreaSize {
|
||||
extent := get_state().app_window.extent
|
||||
return transmute(AreaSize) ( extent * 2.0 )
|
||||
}
|
||||
|
||||
screen_get_bounds :: #force_inline proc "contextless" () -> Range2 {
|
||||
state := get_state(); using state
|
||||
screen_extent := state.app_window.extent
|
||||
bottom_left := Vec2 { -screen_extent.x, -screen_extent.y}
|
||||
top_right := Vec2 { screen_extent.x, screen_extent.y}
|
||||
return range2( bottom_left, top_right )
|
||||
}
|
||||
|
||||
screen_get_corners :: #force_inline proc "contextless"() -> BoundsCorners2 {
|
||||
state := get_state(); using state
|
||||
screen_extent := state.app_window.extent
|
||||
top_left := Vec2 { -screen_extent.x, screen_extent.y }
|
||||
top_right := Vec2 { screen_extent.x, screen_extent.y }
|
||||
bottom_left := Vec2 { -screen_extent.x, -screen_extent.y }
|
||||
bottom_right := Vec2 { screen_extent.x, -screen_extent.y }
|
||||
return { top_left, top_right, bottom_left, bottom_right }
|
||||
}
|
||||
|
||||
// TODO(Ed): Use a cam/workspace context instead (when multiple workspaces viewproting supported)
|
||||
view_get_bounds :: #force_inline proc "contextless"() -> Range2 {
|
||||
state := get_state(); using state
|
||||
cam := & project.workspace.cam
|
||||
screen_extent := state.app_window.extent
|
||||
cam_zoom_ratio := 1.0 / cam.zoom
|
||||
bottom_left := Vec2 { cam.target.x, -cam.target.y } + Vec2 { -screen_extent.x, -screen_extent.y} * cam_zoom_ratio
|
||||
top_right := Vec2 { cam.target.x, -cam.target.y } + Vec2 { screen_extent.x, screen_extent.y} * cam_zoom_ratio
|
||||
return range2( bottom_left, top_right )
|
||||
}
|
||||
|
||||
// TODO(Ed): Use a cam/workspace context instead (when multiple workspace viewproting)
|
||||
view_get_corners :: #force_inline proc "contextless"() -> BoundsCorners2 {
|
||||
state := get_state(); using state
|
||||
cam := & project.workspace.cam
|
||||
cam_zoom_ratio := 1.0 / cam.zoom
|
||||
screen_extent := state.app_window.extent * cam_zoom_ratio
|
||||
top_left := cam.target + Vec2 { -screen_extent.x, screen_extent.y }
|
||||
top_right := cam.target + Vec2 { screen_extent.x, screen_extent.y }
|
||||
bottom_left := cam.target + Vec2 { -screen_extent.x, -screen_extent.y }
|
||||
bottom_right := cam.target + Vec2 { screen_extent.x, -screen_extent.y }
|
||||
return { top_left, top_right, bottom_left, bottom_right }
|
||||
}
|
||||
|
||||
render_to_screen_pos :: #force_inline proc "contextless" (pos : Vec2) -> Vec2 {
|
||||
extent := & get_state().app_window.extent
|
||||
result := Vec2 {
|
||||
pos.x - extent.x,
|
||||
pos.y * -1 + extent.y
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
render_to_ws_view_pos :: #force_inline proc "contextless" (pos : Vec2) -> Vec2 {
|
||||
return {}
|
||||
}
|
||||
|
||||
screen_to_ws_view_pos :: #force_inline proc "contextless" (pos: Vec2) -> Vec2 {
|
||||
state := get_state(); using state
|
||||
cam := & project.workspace.cam
|
||||
result := Vec2 { cam.target.x, -cam.target.y} + Vec2 { pos.x, pos.y } * (1 / cam.zoom)
|
||||
return result
|
||||
}
|
||||
|
||||
// Centered screen space to conventional screen space used for rendering
|
||||
screen_to_render_pos :: #force_inline proc "contextless" (pos : Vec2) -> Vec2 {
|
||||
screen_extent := transmute(Vec2) get_state().app_window.extent
|
||||
return pos * {1, -1} + { screen_extent.x, screen_extent.y }
|
||||
}
|
||||
|
||||
// TODO(Ed): These should assume a cam_context or have the ability to provide it in params
|
||||
|
||||
// Extent of workspace view (currently hardcoded to the app window's extent, eventually will be based on a viewport object's extent field)
|
||||
// TODO(Ed): Support a position which would not be centered on the screen if in a viewport
|
||||
ws_view_extent :: #force_inline proc "contextless"() -> Extents2 {
|
||||
state := get_state(); using state
|
||||
cam_zoom_ratio := 1.0 / project.workspace.cam.zoom
|
||||
return app_window.extent * cam_zoom_ratio
|
||||
}
|
||||
|
||||
// Workspace view to screen space position
|
||||
// TODO(Ed): Support a position which would not be centered on the screen if in a viewport
|
||||
ws_view_to_screen_pos :: #force_inline proc "contextless"(position: Vec2) -> Vec2 {
|
||||
return position
|
||||
}
|
||||
|
||||
ws_view_to_render_pos :: #force_inline proc "contextless"(position: Vec2) -> Vec2 {
|
||||
return { position.x, position.y * -1 }
|
||||
}
|
||||
|
||||
// Workspace view to screen space position (zoom agnostic)
|
||||
// TODO(Ed): Support a position which would not be centered on the screen if in a viewport
|
||||
ws_view_to_screen_pos_no_zoom :: #force_inline proc "contextless"(position: Vec2) -> Vec2 {
|
||||
state := get_state(); using state
|
||||
cam_zoom_ratio := 1.0 / state.project.workspace.cam.zoom
|
||||
return { position.x, position.y } * cam_zoom_ratio
|
||||
}
|
||||
|
||||
// Workspace view to render space position (zoom agnostic)
|
||||
// TODO(Ed): Support a position which would not be centered on the screen if in a viewport
|
||||
ws_view_to_render_pos_no_zoom :: #force_inline proc "contextless"(position: Vec2) -> Vec2 {
|
||||
state := get_state(); using state
|
||||
cam_zoom_ratio := 1.0 / state.project.workspace.cam.zoom
|
||||
return { position.x, position.y } * cam_zoom_ratio
|
||||
}
|
6
code/sectr/ui/canvas.odin
Normal file
6
code/sectr/ui/canvas.odin
Normal file
@ -0,0 +1,6 @@
|
||||
package sectr
|
||||
|
||||
// Specialization of floating that allows panning across the viewport in the space (space isn't clampped to a specific size))
|
||||
UI_Canvas :: struct {
|
||||
using floating : UI_Floating,
|
||||
}
|
262
code/sectr/ui/core.odin
Normal file
262
code/sectr/ui/core.odin
Normal file
@ -0,0 +1,262 @@
|
||||
package sectr
|
||||
|
||||
import "base:runtime"
|
||||
|
||||
// TODO(Ed) : This is in Raddbg base_types.h, consider moving outside of UI.
|
||||
|
||||
Corner :: enum i32 {
|
||||
Invalid = -1,
|
||||
_00,
|
||||
_01,
|
||||
_10,
|
||||
_11,
|
||||
TopLeft = _00,
|
||||
TopRight = _01,
|
||||
BottomLeft = _10,
|
||||
BottomRight = _11,
|
||||
Count = 4,
|
||||
}
|
||||
|
||||
Side :: enum i32 {
|
||||
Invalid = -1,
|
||||
Min = 0,
|
||||
Max = 1,
|
||||
Count
|
||||
}
|
||||
|
||||
// Side2 :: enum u32 {
|
||||
// Top,
|
||||
// Bottom,
|
||||
// Left,
|
||||
// Right,
|
||||
// Count,
|
||||
// }
|
||||
|
||||
// UI_AnchorPresets :: enum u32 {
|
||||
// Top_Left,
|
||||
// Top_Right,
|
||||
// Bottom_Right,
|
||||
// Bottom_Left,
|
||||
// Center_Left,
|
||||
// Center_Top,
|
||||
// Center_Right,
|
||||
// Center_Bottom,
|
||||
// Center,
|
||||
// Left_Wide,
|
||||
// Top_Wide,
|
||||
// Right_Wide,
|
||||
// Bottom_Wide,
|
||||
// VCenter_Wide,
|
||||
// HCenter_Wide,
|
||||
// Full,
|
||||
// Count,
|
||||
// }
|
||||
|
||||
UI_Cursor :: struct {
|
||||
placeholder : int,
|
||||
}
|
||||
|
||||
UI_FramePassKind :: enum {
|
||||
Generate,
|
||||
Compute,
|
||||
Logical,
|
||||
}
|
||||
|
||||
UI_InteractState :: struct {
|
||||
hot_time : f32,
|
||||
active_time : f32,
|
||||
disabled_time : f32,
|
||||
}
|
||||
|
||||
UI_Key :: distinct u64
|
||||
|
||||
UI_Scalar :: f32
|
||||
|
||||
UI_ScalarConstraint :: struct {
|
||||
min, max : UI_Scalar,
|
||||
}
|
||||
|
||||
UI_Scalar2 :: [Axis2.Count]UI_Scalar
|
||||
|
||||
|
||||
// UI_BoxFlags_Stack_Size :: 512
|
||||
UI_Layout_Stack_Size :: 512
|
||||
UI_Style_Stack_Size :: 512
|
||||
UI_Parent_Stack_Size :: 512
|
||||
// UI_Built_Boxes_Array_Size :: 8
|
||||
UI_Built_Boxes_Array_Size :: 128 * Kilobyte
|
||||
|
||||
UI_State :: struct {
|
||||
// TODO(Ed) : Use these
|
||||
// build_arenas : [2]Arena,
|
||||
// build_arena : ^ Arena,
|
||||
|
||||
built_box_count : i32,
|
||||
|
||||
caches : [2] HMapZPL( UI_Box ),
|
||||
prev_cache : ^HMapZPL( UI_Box ),
|
||||
curr_cache : ^HMapZPL( UI_Box ),
|
||||
|
||||
render_queue : Array(UI_RenderBoxInfo),
|
||||
|
||||
null_box : ^UI_Box, // This was used with the Linked list interface...
|
||||
// TODO(Ed): Should we change our convention for null boxes to use the above and nil as an invalid state?
|
||||
root : ^UI_Box,
|
||||
// Children of the root node are unique in that they have their order preserved per frame
|
||||
// This is to support overlapping frames
|
||||
// So long as their parent-index is non-negative they'll be rendered
|
||||
|
||||
// Do we need to recompute the layout?
|
||||
// layout_dirty : b32,
|
||||
|
||||
// TODO(Ed) : Look into using a build arena like Ryan does for these possibly (and thus have a linked-list stack)
|
||||
layout_combo_stack : StackFixed( UI_LayoutCombo, UI_Style_Stack_Size ),
|
||||
style_combo_stack : StackFixed( UI_StyleCombo, UI_Style_Stack_Size ),
|
||||
parent_stack : StackFixed( ^UI_Box, UI_Parent_Stack_Size ),
|
||||
// flag_stack : Stack( UI_BoxFlags, UI_BoxFlags_Stack_Size ),
|
||||
|
||||
hot : UI_Key,
|
||||
hot_start_style : UI_Style,
|
||||
|
||||
active_mouse : [MouseBtn.count] UI_Key,
|
||||
active : UI_Key,
|
||||
active_start_signal : UI_Signal,
|
||||
|
||||
clipboard_copy : UI_Key,
|
||||
last_clicked : UI_Key,
|
||||
|
||||
active_start_style : UI_Style,
|
||||
|
||||
last_pressed_key : [MouseBtn.count] UI_Key,
|
||||
last_pressed_key_us : [MouseBtn.count] f32,
|
||||
}
|
||||
|
||||
ui_startup :: proc( ui : ^ UI_State, cache_allocator : Allocator /* , cache_reserve_size : u64 */ )
|
||||
{
|
||||
ui := ui
|
||||
ui^ = {}
|
||||
|
||||
for & cache in ui.caches {
|
||||
box_cache, allocation_error := zpl_hmap_init_reserve( UI_Box, cache_allocator, UI_Built_Boxes_Array_Size )
|
||||
verify( allocation_error == AllocatorError.None, "Failed to allocate box cache" )
|
||||
cache = box_cache
|
||||
}
|
||||
ui.curr_cache = (& ui.caches[1])
|
||||
ui.prev_cache = (& ui.caches[0])
|
||||
|
||||
allocation_error : AllocatorError
|
||||
ui.render_queue, allocation_error = array_init_reserve( UI_RenderBoxInfo, cache_allocator, UI_Built_Boxes_Array_Size, fixed_cap = true )
|
||||
verify( allocation_error == AllocatorError.None, "Failed to allocate render queue" )
|
||||
|
||||
log("ui_startup completed")
|
||||
}
|
||||
|
||||
ui_reload :: proc( ui : ^ UI_State, cache_allocator : Allocator )
|
||||
{
|
||||
// We need to repopulate Allocator references
|
||||
for & cache in ui.caches {
|
||||
zpl_hmap_reload( & cache, cache_allocator)
|
||||
}
|
||||
ui.render_queue.backing = cache_allocator
|
||||
}
|
||||
|
||||
// TODO(Ed) : Is this even needed?
|
||||
ui_shutdown :: proc() {
|
||||
}
|
||||
|
||||
ui_cursor_pos :: #force_inline proc "contextless" () -> Vec2 {
|
||||
using state := get_state()
|
||||
if ui_context == & state.project.workspace.ui {
|
||||
return screen_to_ws_view_pos( input.mouse.pos )
|
||||
}
|
||||
else {
|
||||
return input.mouse.pos
|
||||
}
|
||||
}
|
||||
|
||||
ui_drag_delta :: #force_inline proc "contextless" () -> Vec2 {
|
||||
using state := get_state()
|
||||
return ui_cursor_pos() - state.ui_context.active_start_signal.cursor_pos
|
||||
}
|
||||
|
||||
ui_graph_build_begin :: proc( ui : ^ UI_State, bounds : Vec2 = {} )
|
||||
{
|
||||
profile(#procedure)
|
||||
|
||||
state := get_state()
|
||||
get_state().ui_context = ui
|
||||
using get_state().ui_context
|
||||
|
||||
stack_clear( & layout_combo_stack )
|
||||
stack_clear( & style_combo_stack )
|
||||
array_clear( render_queue )
|
||||
|
||||
curr_cache, prev_cache = swap( curr_cache, prev_cache )
|
||||
|
||||
if ui.active == UI_Key(0) {
|
||||
//ui.hot = UI_Key(0)
|
||||
ui.active_start_signal = {}
|
||||
}
|
||||
|
||||
ui.built_box_count = 0
|
||||
root = ui_box_make( {}, str_intern(str_fmt_tmp("%s: root#001", ui == & state.screen_ui ? "Screen" : "Workspace" )).str)
|
||||
if ui == & state.screen_ui {
|
||||
root.layout.size = range2(Vec2(state.app_window.extent) * 2, {})
|
||||
}
|
||||
ui_parent_push(root)
|
||||
}
|
||||
|
||||
ui_graph_build_end :: proc( ui : ^UI_State )
|
||||
{
|
||||
profile(#procedure)
|
||||
|
||||
ui_parent_pop() // Should be ui_context.root
|
||||
|
||||
// Regenerate the computed layout if dirty
|
||||
ui_compute_layout( ui )
|
||||
|
||||
get_state().ui_context = nil
|
||||
}
|
||||
|
||||
@(deferred_in = ui_graph_build_end)
|
||||
ui_graph_build :: #force_inline proc( ui : ^ UI_State ) { ui_graph_build_begin( ui ) }
|
||||
|
||||
ui_key_from_string :: #force_inline proc "contextless" ( value : string ) -> UI_Key
|
||||
{
|
||||
// profile(#procedure)
|
||||
USE_RAD_DEBUGGERS_METHOD :: true
|
||||
|
||||
key : UI_Key
|
||||
|
||||
when USE_RAD_DEBUGGERS_METHOD {
|
||||
hash : u64
|
||||
for str_byte in transmute([]byte) value {
|
||||
hash = ((hash << 5) + hash) + u64(str_byte)
|
||||
}
|
||||
key = cast(UI_Key) hash
|
||||
}
|
||||
|
||||
when ! USE_RAD_DEBUGGERS_METHOD {
|
||||
key = cast(UI_Key) crc32( transmute([]byte) value )
|
||||
}
|
||||
|
||||
return key
|
||||
}
|
||||
|
||||
ui_parent_push :: #force_inline proc( ui : ^ UI_Box ) { stack_push( & ui_context().parent_stack, ui ) }
|
||||
ui_parent_pop :: #force_inline proc() { stack_pop( & get_state().ui_context.parent_stack ) }
|
||||
|
||||
@(deferred_none = ui_parent_pop)
|
||||
ui_parent :: #force_inline proc( ui : ^UI_Box) { ui_parent_push( ui ) }
|
||||
|
||||
ui_prev_cached_box :: #force_inline proc( box : ^UI_Box ) -> ^UI_Box { return zpl_hmap_get( ui_context().prev_cache, cast(u64) box.key ) }
|
||||
|
||||
// Topmost ancestor that is not the root
|
||||
ui_top_ancestor :: #force_inline proc "contextless" ( box : ^UI_Box ) -> (^UI_Box) {
|
||||
using ui := get_state().ui_context
|
||||
ancestor := box
|
||||
for ; ancestor.parent != root; ancestor = ancestor.parent {}
|
||||
return ancestor
|
||||
}
|
||||
|
||||
ui_context :: #force_inline proc() -> ^UI_State { return get_state().ui_context }
|
153
code/sectr/ui/core_box.odin
Normal file
153
code/sectr/ui/core_box.odin
Normal file
@ -0,0 +1,153 @@
|
||||
package sectr
|
||||
|
||||
UI_BoxFlag :: enum u64
|
||||
{
|
||||
Disabled,
|
||||
|
||||
Focusable,
|
||||
Click_To_Focus,
|
||||
|
||||
Mouse_Clickable,
|
||||
Keyboard_Clickable,
|
||||
|
||||
Count,
|
||||
}
|
||||
UI_BoxFlags :: bit_set[UI_BoxFlag; u64]
|
||||
// UI_BoxFlag_Scroll :: UI_BoxFlags { .Scroll_X, .Scroll_Y }
|
||||
|
||||
UI_RenderBoxInfo :: struct {
|
||||
using computed : UI_Computed,
|
||||
using style : UI_Style,
|
||||
text : StrRunesPair,
|
||||
font_size : UI_Scalar,
|
||||
border_width : UI_Scalar,
|
||||
}
|
||||
|
||||
UI_Box :: struct {
|
||||
// Cache ID
|
||||
key : UI_Key,
|
||||
// label : string,
|
||||
label : StrRunesPair,
|
||||
text : StrRunesPair,
|
||||
|
||||
// Regenerated per frame.
|
||||
|
||||
// first, last : The first and last child of this box
|
||||
// prev, next : The adjacent neighboring boxes who are children of to the same parent
|
||||
using links : DLL_NodeFull( UI_Box ),
|
||||
parent : ^UI_Box,
|
||||
num_children : i32,
|
||||
ancestors : i32, // This value for rooted widgets gets set to -1 after rendering see ui_box_make() for the reason.
|
||||
parent_index : i32,
|
||||
|
||||
flags : UI_BoxFlags,
|
||||
computed : UI_Computed,
|
||||
|
||||
layout : UI_Layout,
|
||||
style : UI_Style,
|
||||
|
||||
// Persistent Data
|
||||
hot_delta : f32,
|
||||
active_delta : f32,
|
||||
disabled_delta : f32,
|
||||
style_delta : f32,
|
||||
first_frame : b8,
|
||||
// root_order_id : i16,
|
||||
|
||||
// mouse : UI_InteractState,
|
||||
// keyboard : UI_InteractState,
|
||||
}
|
||||
|
||||
ui_box_equal :: #force_inline proc "contextless" ( a, b : ^ UI_Box ) -> b32 {
|
||||
BoxSize :: size_of(UI_Box)
|
||||
|
||||
result : b32 = true
|
||||
result &= a.key == b.key // We assume for now the label is the same as the key, if not something is terribly wrong.
|
||||
result &= a.flags == b.flags
|
||||
return result
|
||||
}
|
||||
|
||||
ui_box_from_key :: #force_inline proc ( cache : ^HMapZPL(UI_Box), key : UI_Key ) -> (^UI_Box) {
|
||||
return zpl_hmap_get( cache, cast(u64) key )
|
||||
}
|
||||
|
||||
ui_box_make :: proc( flags : UI_BoxFlags, label : string ) -> (^ UI_Box)
|
||||
{
|
||||
// profile(#procedure)
|
||||
using ui := get_state().ui_context
|
||||
key := ui_key_from_string( label )
|
||||
|
||||
curr_box : (^ UI_Box)
|
||||
prev_box := zpl_hmap_get( prev_cache, cast(u64) key )
|
||||
{
|
||||
// profile("Assigning current box")
|
||||
set_result : ^ UI_Box
|
||||
set_error : AllocatorError
|
||||
if prev_box != nil
|
||||
{
|
||||
// Previous history was found, copy over previous state.
|
||||
set_result, set_error = zpl_hmap_set( curr_cache, cast(u64) key, (prev_box ^) )
|
||||
}
|
||||
else {
|
||||
box : UI_Box
|
||||
box.key = key
|
||||
box.label = str_intern( label )
|
||||
set_result, set_error = zpl_hmap_set( curr_cache, cast(u64) key, box )
|
||||
}
|
||||
|
||||
verify( set_error == AllocatorError.None, "Failed to set zpl_hmap due to allocator error" )
|
||||
curr_box = set_result
|
||||
curr_box.first_frame = prev_box == nil
|
||||
curr_box.flags = flags
|
||||
}
|
||||
|
||||
// Clear non-persistent data
|
||||
curr_box.computed.fresh = false
|
||||
curr_box.links = {}
|
||||
curr_box.num_children = 0
|
||||
|
||||
// If there is a parent, setup the relevant references
|
||||
parent := stack_peek( & parent_stack )
|
||||
if parent != nil
|
||||
{
|
||||
dll_full_push_back( parent, curr_box, nil )
|
||||
curr_box.parent_index = parent.num_children
|
||||
parent.num_children += 1
|
||||
curr_box.parent = parent
|
||||
curr_box.ancestors = parent.ancestors + 1
|
||||
}
|
||||
|
||||
ui.built_box_count += 1
|
||||
return curr_box
|
||||
}
|
||||
|
||||
ui_box_tranverse_next :: proc "contextless" ( box : ^ UI_Box ) -> (^ UI_Box)
|
||||
{
|
||||
using state := get_state()
|
||||
// If current has children, do them first
|
||||
if box.first != nil
|
||||
{
|
||||
// Check to make sure parent is present on the screen, if its not don't bother.
|
||||
is_app_ui := ui_context == & screen_ui
|
||||
if intersects_range2( view_get_bounds(), box.computed.bounds)
|
||||
{
|
||||
return box.first
|
||||
}
|
||||
}
|
||||
|
||||
if box.next != nil do return box.next
|
||||
// There is no more adjacent nodes
|
||||
|
||||
parent := box.parent
|
||||
// Attempt to find a parent with a next, otherwise we just return a parent with nil
|
||||
for ; parent.parent != nil;
|
||||
{
|
||||
if parent.next != nil {
|
||||
break
|
||||
}
|
||||
parent = parent.parent
|
||||
}
|
||||
|
||||
// Lift back up to parent, and set it to its next.
|
||||
return parent.next
|
||||
}
|
165
code/sectr/ui/core_layout.odin
Normal file
165
code/sectr/ui/core_layout.odin
Normal file
@ -0,0 +1,165 @@
|
||||
package sectr
|
||||
|
||||
import "core:math"
|
||||
import "core:math/linalg"
|
||||
|
||||
|
||||
// Anchor_
|
||||
|
||||
// Alignment presets
|
||||
|
||||
LayoutAlign_OriginTL_Top :: Vec2{0.5, 0}
|
||||
LayoutAlign_OriginTL_TopLeft :: Vec2{ 0, 0}
|
||||
LayoutAlign_OriginTL_TopRight :: Vec2{ 1, 0}
|
||||
LayoutAlign_OriginTL_Centered :: Vec2{0.5, 0.5}
|
||||
LayoutAlign_OriginTL_Bottom :: Vec2{0.5, 1}
|
||||
LayoutAlign_OriginTL_BottomLeft :: Vec2{ 0, 1}
|
||||
LayoutAlign_OriginTL_BottomRight :: Vec2{ 1, 1}
|
||||
|
||||
// LayoutAlign_OriginTL_
|
||||
|
||||
Layout_OriginCenter_Centered :: Vec2{0.5, 0.5}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// The UI_Box's actual positioning and sizing
|
||||
// There is an excess of rectangles here for debug puproses.
|
||||
UI_Computed :: struct {
|
||||
// anchors : Range2, // Bounds for anchors within parent
|
||||
// margins : Range2, // Bounds for margins within parent
|
||||
padding : Range2, // Bounds for padding's starting bounds (will be offset by border if there is one), only here for debug vis
|
||||
|
||||
bounds : Range2, // Bounds for box itself
|
||||
content : Range2, // Bounds for content (text or children)
|
||||
text_pos : Vec2, // Position of text within content
|
||||
text_size : Vec2, // Size of text within content
|
||||
fresh : b32, // If the auto-layout has been computed for the current frame
|
||||
}
|
||||
|
||||
UI_LayoutDirectionX :: enum(i32) {
|
||||
Left_To_Right,
|
||||
Right_To_Left,
|
||||
}
|
||||
|
||||
UI_LayoutDirectionY :: enum(i32) {
|
||||
Top_To_Bottom,
|
||||
Bottom_To_Top,
|
||||
}
|
||||
|
||||
UI_LayoutSide :: struct {
|
||||
// using _ : struct {
|
||||
top, bottom : UI_Scalar,
|
||||
left, right : UI_Scalar,
|
||||
// }
|
||||
}
|
||||
|
||||
UI_LayoutFlag :: enum u32 {
|
||||
|
||||
// Will perform scissor pass on children to their parent's bounds
|
||||
// (Specified in the parent)
|
||||
Clip_Children_To_Bounds,
|
||||
|
||||
// Enforces the box will always remain in a specific position relative to the parent.
|
||||
// Overriding the anchors and margins.
|
||||
Fixed_Position_X,
|
||||
Fixed_Position_Y,
|
||||
|
||||
// Enforces box will always be within the bounds of the parent box.
|
||||
Clamp_Position_X,
|
||||
Clamp_Position_Y,
|
||||
|
||||
// Enroces the widget will maintain its size reguardless of any constraints
|
||||
// Will override parent constraints (use the size.min.xy to specify the width & height)
|
||||
Fixed_Width,
|
||||
Fixed_Height,
|
||||
|
||||
// Enforces the widget will have a width specified as a ratio of its height (use the size.min/max.x to specify the scalar)
|
||||
// If you wish for the width to stay fixed couple with the Fixed_Width flag
|
||||
Scale_Width_By_Height_Ratio,
|
||||
// Enforces the widget will have a height specified as a ratio of its width (use the size.min/max.y to specify the scalar)
|
||||
// If you wish for the height to stay fixed couple with the Fixed_Height flag
|
||||
Scale_Height_By_Width_Ratio,
|
||||
|
||||
// Sets the (0, 0) position of the child box to the parents anchor's center (post-margins bounds)
|
||||
// By Default, the origin is at the top left of the anchor's bounds
|
||||
Origin_At_Anchor_Center,
|
||||
|
||||
// TODO(Ed): Implement this!
|
||||
// For this to work, the children must have a minimum size set & their size overall must be greater than the parent's minimum size
|
||||
Size_To_Content,
|
||||
|
||||
// Will size the box to its text.
|
||||
Size_To_Text,
|
||||
|
||||
// TODO(Ed): Implement this!
|
||||
// ?Note(Ed): This can get pretty complicated... Maybe its better to leave this to composition of boxes.
|
||||
// ?A text wrapping panel can organize text and wrap it via procedrually generated lines in a hbox/vbox.
|
||||
// ?It would be a non-issue so long as the text rendering bottleneck is resolved.
|
||||
// Wrap text around the box, text_alignment specifies the justification for its compostion when wrapping.
|
||||
Text_Wrap,
|
||||
|
||||
Count,
|
||||
}
|
||||
UI_LayoutFlags :: bit_set[UI_LayoutFlag; u32]
|
||||
|
||||
// Used within UI_Box, provides the layout (spacial constraints & specification) of the widget and
|
||||
UI_Layout :: struct {
|
||||
flags : UI_LayoutFlags,
|
||||
anchor : Range2,
|
||||
alignment : Vec2,
|
||||
text_alignment : Vec2,
|
||||
|
||||
font_size : UI_Scalar,
|
||||
|
||||
margins : UI_LayoutSide,
|
||||
padding : UI_LayoutSide,
|
||||
|
||||
border_width : UI_Scalar,
|
||||
|
||||
// Position in relative coordinate space.
|
||||
// If the box's flags has Fixed_Position, then this will be its aboslute position in the relative coordinate space
|
||||
pos : Vec2,
|
||||
size : Range2,
|
||||
|
||||
// TODO(Ed) : Should thsi just always be WS_Pos for workspace UI?
|
||||
// (We can union either varient and just know based on checking if its the screenspace UI)
|
||||
// If the box is a child of the root parent, its automatically in world space and thus will use the tile_pos.
|
||||
// tile_pos : WS_Pos,
|
||||
}
|
||||
|
||||
UI_LayoutCombo :: struct #raw_union {
|
||||
array : [UI_StylePreset.Count] UI_Layout,
|
||||
using layouts : struct {
|
||||
default, disabled, hot, active : UI_Layout,
|
||||
}
|
||||
}
|
||||
|
||||
to_ui_layout_side_f32 :: #force_inline proc( pixels : f32 ) -> UI_LayoutSide { return { pixels, pixels, pixels, pixels } }
|
||||
to_ui_layout_side_vec2 :: #force_inline proc( v : Vec2) -> UI_LayoutSide { return { v.x, v.x, v.y, v.y} }
|
||||
to_ui_layout_combo :: #force_inline proc( layout : UI_Layout ) -> UI_LayoutCombo { return { layouts = {layout, layout, layout, layout} } }
|
||||
|
||||
/*
|
||||
Layout Interface
|
||||
|
||||
Layout for UI_Boxes in the state graph is stored on a per-graph UI_State basis in the fixed sized stack called layout_combo_stack.
|
||||
The following provides a convient way to manipulate this stack from the assuption of the program's state.ui_context
|
||||
|
||||
The following procedure overloads are available from grime.odin:
|
||||
* ui_layout
|
||||
* ui_layout_push
|
||||
*/
|
||||
|
||||
ui_layout_peek :: #force_inline proc() -> UI_LayoutCombo { return stack_peek( & get_state().ui_context.layout_combo_stack) }
|
||||
ui_layout_ref :: #force_inline proc() -> ^UI_LayoutCombo { return stack_peek_ref( & get_state().ui_context.layout_combo_stack) }
|
||||
|
||||
ui_layout_push_layout :: #force_inline proc( layout : UI_Layout ) { push( & get_state().ui_context.layout_combo_stack, to_ui_layout_combo(layout)) }
|
||||
ui_layout_push_theme :: #force_inline proc( combo : UI_LayoutCombo ) { push( & get_state().ui_context.layout_combo_stack, combo ) }
|
||||
ui_layout_pop :: #force_inline proc() { pop( & get_state().ui_context.layout_combo_stack ) }
|
||||
|
||||
@(deferred_none = ui_layout_pop) ui_layout_via_layout :: #force_inline proc( layout : UI_Layout ) { ui_layout_push( layout) }
|
||||
@(deferred_none = ui_layout_pop) ui_layout_via_combo :: #force_inline proc( combo : UI_LayoutCombo ) { ui_layout_push( combo) }
|
||||
|
||||
ui_set_layout :: #force_inline proc( layout : UI_Layout, preset : UI_StylePreset ) { stack_peek_ref( & get_state().ui_context.layout_combo_stack).array[preset] = layout }
|
219
code/sectr/ui/core_layout_compute.odin
Normal file
219
code/sectr/ui/core_layout_compute.odin
Normal file
@ -0,0 +1,219 @@
|
||||
package sectr
|
||||
|
||||
ui_box_compute_layout :: proc( box : ^UI_Box,
|
||||
dont_mark_fresh : b32 = false,
|
||||
ancestors_layout_required : b32 = false,
|
||||
root_layout_required : b32 = false )
|
||||
{
|
||||
// profile("Layout Box")
|
||||
state := get_state()
|
||||
ui := state.ui_context
|
||||
using box
|
||||
|
||||
size_to_text : bool = .Size_To_Text in layout.flags
|
||||
|
||||
parent_content := parent.computed.content
|
||||
parent_content_size := parent_content.max - parent_content.min
|
||||
parent_center := parent_content.min + parent_content_size * 0.5
|
||||
|
||||
/*
|
||||
If fixed position (X or Y):
|
||||
* Ignore Margins
|
||||
* Ignore Anchors
|
||||
|
||||
If clampped position (X or Y):
|
||||
* Positon cannot exceed the anchors/margins bounds.
|
||||
|
||||
If fixed size (X or Y):
|
||||
* Ignore Parent constraints (can only be clipped)
|
||||
|
||||
If an axis is auto-sized by a ratio of the other axis
|
||||
* Using the referenced axis, set the size of the ratio'd axis by that ratio.
|
||||
|
||||
If auto-sized:
|
||||
* Enforce parent size constraint of bounds relative to
|
||||
where the adjusted content bounds are after applying margins & anchors.
|
||||
The 'side' conflicting with the bounds will end at that bound side instead of clipping.
|
||||
|
||||
If size.min is not 0:
|
||||
* Ignore parent constraints if the bounds go below that value.
|
||||
|
||||
If size.max is 0:
|
||||
* Allow the child box to spread to entire adjusted content bounds, otherwise clampped to max size.
|
||||
*/
|
||||
|
||||
// 1. Anchors
|
||||
anchor := & layout.anchor
|
||||
anchored_bounds := range2(
|
||||
parent_content.min + parent_content_size * anchor.min,
|
||||
parent_content.max - parent_content_size * anchor.max,
|
||||
)
|
||||
// anchored_bounds_origin := (anchored_bounds.min + anchored_bounds.max) * 0.5
|
||||
|
||||
// 2. Apply Margins
|
||||
margins := range2(
|
||||
{ layout.margins.left, layout.margins.bottom },
|
||||
{ layout.margins.right, layout.margins.top },
|
||||
)
|
||||
margined_bounds := range2(
|
||||
anchored_bounds.min + margins.min,
|
||||
anchored_bounds.max - margins.max,
|
||||
)
|
||||
margined_bounds_origin := (margined_bounds.min + margined_bounds.max) * 0.5
|
||||
margined_size := margined_bounds.max - margined_bounds.min
|
||||
|
||||
// 3. Enforce Min/Max Size Constraints
|
||||
adjusted_max_size_x := layout.size.max.x > 0 ? min( margined_size.x, layout.size.max.x ) : margined_size.x
|
||||
adjusted_max_size_y := layout.size.max.y > 0 ? min( margined_size.y, layout.size.max.y ) : margined_size.y
|
||||
|
||||
adjusted_size : Vec2
|
||||
adjusted_size.x = max( adjusted_max_size_x, layout.size.min.x)
|
||||
adjusted_size.y = max( adjusted_max_size_y, layout.size.min.y)
|
||||
|
||||
text_size : Vec2
|
||||
if layout.font_size == computed.text_size.y {
|
||||
text_size = computed.text_size
|
||||
}
|
||||
else {
|
||||
text_size = cast(Vec2) measure_text_size( box.text.str, style.font, layout.font_size, 0 )
|
||||
}
|
||||
|
||||
if size_to_text {
|
||||
adjusted_size = text_size
|
||||
}
|
||||
|
||||
if .Scale_Width_By_Height_Ratio in layout.flags {
|
||||
adjusted_size.x = adjusted_size.y * layout.size.min.x
|
||||
}
|
||||
if .Scale_Height_By_Width_Ratio in layout.flags {
|
||||
adjusted_size.y = adjusted_size.x * layout.size.min.y
|
||||
}
|
||||
|
||||
if .Size_To_Content in layout.flags {
|
||||
// Preemtively traverse the children of this parent and have them compute their layout.
|
||||
// This parent will just set its size to the max bounding area of those children.
|
||||
// This will recursively occur if child also depends on their content size from their children, etc.
|
||||
ui_box_compute_layout_children(box)
|
||||
//ui_compute_children_bounding_area(box)
|
||||
}
|
||||
|
||||
// TODO(Ed): Should this force override all of the previous auto-sizing possible?
|
||||
if .Fixed_Width in layout.flags {
|
||||
adjusted_size.x = layout.size.min.x
|
||||
}
|
||||
if .Fixed_Height in layout.flags {
|
||||
adjusted_size.y = layout.size.min.y
|
||||
}
|
||||
|
||||
// 5. Determine relative position
|
||||
|
||||
origin_center := margined_bounds_origin
|
||||
origin_top_left := Vec2 { margined_bounds.min.x, margined_bounds.max.y }
|
||||
|
||||
origin := .Origin_At_Anchor_Center in layout.flags ? origin_center : origin_top_left
|
||||
|
||||
rel_pos := origin + layout.pos
|
||||
|
||||
if .Fixed_Position_X in layout.flags {
|
||||
rel_pos.x = origin.x + layout.pos.x
|
||||
}
|
||||
if .Fixed_Position_Y in layout.flags {
|
||||
rel_pos.y = origin.y + layout.pos.y
|
||||
}
|
||||
|
||||
vec2_one := Vec2 { 1, 1 }
|
||||
|
||||
// 6. Determine the box bounds
|
||||
// Adjust Alignment of pivot position
|
||||
alignment := layout.alignment
|
||||
bounds : Range2
|
||||
if ! (.Origin_At_Anchor_Center in layout.flags) {
|
||||
// The convention offset adjust the box so that the top-left point is at the top left of the anchor's bounds
|
||||
tl_convention_offset := adjusted_size * {0, -1}
|
||||
bounds = range2(
|
||||
rel_pos - adjusted_size * alignment + tl_convention_offset,
|
||||
rel_pos + adjusted_size * (vec2_one - alignment) + tl_convention_offset,
|
||||
)
|
||||
}
|
||||
else {
|
||||
centered_convention_offset := adjusted_size * -0.5
|
||||
bounds = range2(
|
||||
(rel_pos + centered_convention_offset) - adjusted_size * -alignment ,
|
||||
(rel_pos + centered_convention_offset) + adjusted_size * (alignment + vec2_one),
|
||||
)
|
||||
}
|
||||
|
||||
// 7. Padding & Content
|
||||
// Determine Padding's outer bounds
|
||||
border_offset := Vec2 { layout.border_width, layout.border_width }
|
||||
|
||||
padding_bounds := range2(
|
||||
bounds.min + border_offset,
|
||||
bounds.min - border_offset,
|
||||
)
|
||||
|
||||
// Determine Content Bounds
|
||||
content_bounds := range2(
|
||||
bounds.min + { layout.padding.left, layout.padding.bottom } + border_offset,
|
||||
bounds.max - { layout.padding.right, layout.padding.top } - border_offset,
|
||||
)
|
||||
|
||||
computed.bounds = bounds
|
||||
computed.padding = padding_bounds
|
||||
computed.content = content_bounds
|
||||
|
||||
// 8. Text position & size
|
||||
if len(box.text.str) > 0
|
||||
{
|
||||
content_size := content_bounds.max - content_bounds.min
|
||||
text_pos : Vec2
|
||||
text_pos = content_bounds.min + { 0, text_size.y }
|
||||
text_pos += (content_size - text_size) * layout.text_alignment
|
||||
|
||||
computed.text_size = text_size
|
||||
computed.text_pos = text_pos
|
||||
}
|
||||
computed.fresh = true && !dont_mark_fresh
|
||||
}
|
||||
|
||||
ui_box_compute_layout_children :: proc( box : ^UI_Box )
|
||||
{
|
||||
for current := box.first; current != nil && current.prev != box; current = ui_box_tranverse_next( current )
|
||||
{
|
||||
if current == box do return
|
||||
if current.computed.fresh do continue
|
||||
ui_box_compute_layout( current )
|
||||
}
|
||||
}
|
||||
|
||||
ui_core_compute_layout :: proc( ui : ^UI_State )
|
||||
{
|
||||
profile(#procedure)
|
||||
state := get_state()
|
||||
|
||||
root := ui.root
|
||||
{
|
||||
computed := & root.computed
|
||||
style := root.style
|
||||
layout := & root.layout
|
||||
if ui == & state.screen_ui {
|
||||
computed.bounds.min = transmute(Vec2) state.app_window.extent * -1
|
||||
computed.bounds.max = transmute(Vec2) state.app_window.extent
|
||||
}
|
||||
computed.content = computed.bounds
|
||||
}
|
||||
|
||||
for current := root.first; current != nil; current = ui_box_tranverse_next( current )
|
||||
{
|
||||
if ! current.computed.fresh {
|
||||
ui_box_compute_layout( current )
|
||||
}
|
||||
array_append( & ui.render_queue, UI_RenderBoxInfo {
|
||||
current.computed,
|
||||
current.style,
|
||||
current.text,
|
||||
current.layout.font_size,
|
||||
current.layout.border_width,
|
||||
})
|
||||
}
|
||||
}
|
256
code/sectr/ui/core_signal.odin
Normal file
256
code/sectr/ui/core_signal.odin
Normal file
@ -0,0 +1,256 @@
|
||||
package sectr
|
||||
|
||||
import "base:runtime"
|
||||
|
||||
UI_Signal :: struct {
|
||||
cursor_pos : Vec2,
|
||||
drag_delta : Vec2,
|
||||
scroll : Vec2,
|
||||
|
||||
left_clicked : b8,
|
||||
right_clicked : b8,
|
||||
double_clicked : b8,
|
||||
keyboard_clicked : b8,
|
||||
left_shift_held : b8,
|
||||
left_ctrl_held : b8,
|
||||
|
||||
active : b8,
|
||||
hot : b8,
|
||||
disabled : b8,
|
||||
|
||||
was_active : b8,
|
||||
was_hot : b8,
|
||||
was_disabled : b8,
|
||||
|
||||
pressed : b8,
|
||||
released : b8,
|
||||
cursor_over : b8,
|
||||
commit : b8,
|
||||
}
|
||||
|
||||
ui_signal_from_box :: proc ( box : ^ UI_Box, update_style := true, update_deltas := true ) -> UI_Signal
|
||||
{
|
||||
// profile(#procedure)
|
||||
ui := get_state().ui_context
|
||||
input := get_state().input
|
||||
|
||||
frame_delta := frametime_delta32()
|
||||
|
||||
signal := UI_Signal {}
|
||||
|
||||
// Cursor Collision
|
||||
// profile_begin( "Cursor collision")
|
||||
signal.cursor_pos = ui_cursor_pos()
|
||||
signal.cursor_over = cast(b8) pos_within_range2( signal.cursor_pos, box.computed.bounds )
|
||||
|
||||
UnderCheck:
|
||||
{
|
||||
if ! signal.cursor_over do break UnderCheck
|
||||
|
||||
last_root := ui_box_from_key( ui.prev_cache, ui.root.key )
|
||||
if last_root == nil do break UnderCheck
|
||||
|
||||
top_ancestor := ui_top_ancestor(box)
|
||||
if top_ancestor.parent_index < last_root.parent_index
|
||||
{
|
||||
for curr := last_root.last; curr != nil && curr.key != box.key; curr = curr.prev {
|
||||
if pos_within_range2( signal.cursor_pos, curr.computed.bounds ) {
|
||||
signal.cursor_over = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// profile_end()
|
||||
|
||||
// profile_begin("misc")
|
||||
left_pressed := pressed( input.mouse.left )
|
||||
left_released := released( input.mouse.left )
|
||||
|
||||
signal.left_shift_held = b8(input.keyboard.left_shift.ended_down)
|
||||
|
||||
mouse_clickable := UI_BoxFlag.Mouse_Clickable in box.flags
|
||||
keyboard_clickable := UI_BoxFlag.Keyboard_Clickable in box.flags
|
||||
|
||||
was_hot := (box.hot_delta > 0)
|
||||
was_active := (ui.active == box.key) && (box.active_delta > 0)
|
||||
was_disabled := box.disabled_delta > 0
|
||||
// if was_hot {
|
||||
// runtime.debug_trap()
|
||||
// }
|
||||
|
||||
// Check to see if this box is active
|
||||
if mouse_clickable && signal.cursor_over && left_pressed && was_hot
|
||||
{
|
||||
// ui.hot = box.key
|
||||
ui.active = box.key
|
||||
ui.active_mouse[MouseBtn.Left] = box.key
|
||||
|
||||
ui.last_pressed_key = box.key
|
||||
ui.active_start_style = box.style
|
||||
|
||||
signal.pressed = true
|
||||
signal.left_clicked = b8(left_pressed)
|
||||
// TODO(Ed) : Support double-click detection
|
||||
}
|
||||
|
||||
if mouse_clickable && ! signal.cursor_over && left_released
|
||||
{
|
||||
box.active_delta = 0
|
||||
|
||||
ui.active = UI_Key(0)
|
||||
ui.active_mouse[MouseBtn.Left] = UI_Key(0)
|
||||
|
||||
signal.released = true
|
||||
}
|
||||
|
||||
if keyboard_clickable
|
||||
{
|
||||
// TODO(Ed) : Add keyboard interaction support
|
||||
}
|
||||
|
||||
// TODO(Ed): Should panning and scrolling get supported here? (problably not...)
|
||||
// TODO(Ed) : Add scrolling support
|
||||
// if UI_BoxFlag.Scroll_X in box.flags {
|
||||
|
||||
// }
|
||||
// if UI_BoxFlag.Scroll_Y in box.flags {
|
||||
|
||||
// }
|
||||
// TODO(Ed) : Add panning support
|
||||
// if UI_BoxFlag.Pan_X in box.flags {
|
||||
|
||||
// }
|
||||
// if UI_BoxFlag.Pan_Y in box.flags {
|
||||
// }
|
||||
|
||||
is_disabled := UI_BoxFlag.Disabled in box.flags
|
||||
is_hot := ui.hot == box.key
|
||||
is_active := ui.active == box.key
|
||||
|
||||
// TODO(Ed): It should be able to enter hot without mouse_clickable
|
||||
if mouse_clickable && signal.cursor_over && ! is_disabled
|
||||
{
|
||||
hot_vacant := ui.hot == UI_Key(0)
|
||||
active_vacant := ui.active == UI_Key(0)
|
||||
// (active_vacant is_active)
|
||||
if signal.cursor_over && active_vacant
|
||||
{
|
||||
if ! hot_vacant {
|
||||
prev := ui_box_from_key( ui.curr_cache, ui.hot )
|
||||
prev.hot_delta = 0
|
||||
}
|
||||
// prev_hot := zpl_hmap_get( ui.prev_cache, u64(ui.hot) )
|
||||
// prev_hot_label := prev_hot != nil ? prev_hot.label.str : ""
|
||||
// log( str_fmt_tmp("Detected HOT via CURSOR OVER: %v is_hot: %v is_active: %v prev_hot: %v", box.label.str, is_hot, is_active, prev_hot_label ))
|
||||
ui.hot = box.key
|
||||
is_hot = true
|
||||
|
||||
ui.hot_start_style = box.style
|
||||
}
|
||||
}
|
||||
else if ! signal.cursor_over && was_hot
|
||||
{
|
||||
ui.hot = UI_Key(0)
|
||||
is_hot = false
|
||||
box.hot_delta = 0
|
||||
}
|
||||
|
||||
if mouse_clickable && signal.cursor_over && left_released
|
||||
{
|
||||
box.active_delta = 0
|
||||
|
||||
ui.active = UI_Key(0)
|
||||
ui.active_mouse[MouseBtn.Left] = UI_Key(0)
|
||||
|
||||
signal.released = true
|
||||
|
||||
if was_active {
|
||||
signal.left_clicked = true
|
||||
ui.last_clicked = box.key
|
||||
}
|
||||
}
|
||||
// profile_end()
|
||||
|
||||
// State Deltas update
|
||||
// profile_begin( "state deltas upate")
|
||||
if is_hot
|
||||
{
|
||||
box.hot_delta += frame_delta
|
||||
if was_hot {
|
||||
box.style_delta += frame_delta
|
||||
}
|
||||
}
|
||||
if is_active
|
||||
{
|
||||
box.active_delta += frame_delta
|
||||
if was_active {
|
||||
box.style_delta += frame_delta
|
||||
}
|
||||
}
|
||||
else {
|
||||
box.active_delta = 0
|
||||
}
|
||||
if is_disabled
|
||||
{
|
||||
box.disabled_delta += frame_delta
|
||||
if was_hot {
|
||||
box.style_delta += frame_delta
|
||||
}
|
||||
}
|
||||
else {
|
||||
box.disabled_delta = 0
|
||||
}
|
||||
// profile_end()
|
||||
|
||||
signal.active = cast(b8) is_active
|
||||
signal.was_active = cast(b8) was_active
|
||||
// logf("was_active: %v", was_active)
|
||||
|
||||
// Update style if not in default state
|
||||
if update_style
|
||||
{
|
||||
// profile("Update style")
|
||||
|
||||
if is_hot
|
||||
{
|
||||
if ! was_hot {
|
||||
box.style_delta = 0
|
||||
}
|
||||
box.layout = ui_layout_peek().hot
|
||||
box.style = ui_style_peek().hot
|
||||
}
|
||||
if is_active
|
||||
{
|
||||
if ! was_active {
|
||||
box.style_delta = 0
|
||||
}
|
||||
box.layout = ui_layout_peek().active
|
||||
box.style = ui_style_peek().active
|
||||
}
|
||||
if is_disabled
|
||||
{
|
||||
if ! was_disabled {
|
||||
box.style_delta = 0
|
||||
}
|
||||
box.layout = ui_layout_peek().disabled
|
||||
box.style = ui_style_peek().disabled
|
||||
}
|
||||
|
||||
if ! is_disabled && ! is_active && ! is_hot {
|
||||
if was_disabled || was_active || was_hot {
|
||||
box.style_delta = 0
|
||||
}
|
||||
else {
|
||||
box.style_delta += frame_delta
|
||||
}
|
||||
box.layout = ui_layout_peek().default
|
||||
box.style = ui_style_peek().default
|
||||
}
|
||||
}
|
||||
|
||||
if is_active && ! was_active {
|
||||
ui.active_start_signal = signal
|
||||
}
|
||||
|
||||
return signal
|
||||
}
|
72
code/sectr/ui/core_style.odin
Normal file
72
code/sectr/ui/core_style.odin
Normal file
@ -0,0 +1,72 @@
|
||||
package sectr
|
||||
|
||||
// TODO(Ed): We problably can embedd this info into the UI_Layout with the regular text_alignment
|
||||
UI_TextAlign :: enum u32 {
|
||||
Left,
|
||||
Center,
|
||||
Right,
|
||||
Count
|
||||
}
|
||||
|
||||
UI_StylePreset :: enum u32 {
|
||||
Default,
|
||||
Disabled,
|
||||
Hot,
|
||||
Active,
|
||||
Count,
|
||||
}
|
||||
|
||||
UI_Style :: struct {
|
||||
bg_color : Color,
|
||||
border_color : Color,
|
||||
|
||||
// TODO(Ed): We cannot support individual corners unless we add it to raylib (or finally change the rendering backend)
|
||||
corner_radii : [Corner.Count]f32,
|
||||
|
||||
// TODO(Ed) : Add support for this eventually
|
||||
blur_size : f32,
|
||||
|
||||
// TODO(Ed): Add support for textures
|
||||
// texture : Texture2,
|
||||
|
||||
// TODO(Ed): Add support for custom shader
|
||||
// shader : UI_Shader,
|
||||
|
||||
font : FontID,
|
||||
text_color : Color,
|
||||
|
||||
// TODO(Ed) : Support setting the cursor state
|
||||
cursor : UI_Cursor,
|
||||
}
|
||||
|
||||
UI_StyleCombo :: struct #raw_union {
|
||||
array : [UI_StylePreset.Count] UI_Style,
|
||||
using styles : struct {
|
||||
default, disabled, hot, active : UI_Style,
|
||||
}
|
||||
}
|
||||
|
||||
to_ui_style_combo :: #force_inline proc( style : UI_Style ) -> UI_StyleCombo { return { styles = {style, style, style, style} } }
|
||||
|
||||
/*
|
||||
Style Interface
|
||||
|
||||
Style for UI_Boxes in the state graph is stored on a per-graph UI_State basis in the fixed sized stack called style_combo_stack.
|
||||
The following provides a convient way to manipulate this stack from the assuption of the program's state.ui_context
|
||||
|
||||
The following procedure overloads are available from grime.odin :
|
||||
* ui_style
|
||||
* ui_style_push
|
||||
*/
|
||||
|
||||
ui_style_peek :: #force_inline proc() -> UI_StyleCombo { return stack_peek( & get_state().ui_context.style_combo_stack ) }
|
||||
ui_style_ref :: #force_inline proc() -> (^ UI_StyleCombo) { return stack_peek_ref( & get_state().ui_context.style_combo_stack ) }
|
||||
|
||||
ui_style_push_style :: #force_inline proc( style : UI_Style ) { push( & get_state().ui_context.style_combo_stack, to_ui_style_combo(style)) }
|
||||
ui_style_push_combo :: #force_inline proc( combo : UI_StyleCombo ) { push( & get_state().ui_context.style_combo_stack, combo ) }
|
||||
ui_style_pop :: #force_inline proc() { pop( & get_state().ui_context.style_combo_stack ) }
|
||||
|
||||
@(deferred_none = ui_style_pop) ui_style_via_style :: #force_inline proc( style : UI_Style ) { ui_style_push( style) }
|
||||
@(deferred_none = ui_style_pop) ui_style_via_combo :: #force_inline proc( combo : UI_StyleCombo ) { ui_style_push( combo) }
|
||||
|
||||
ui_style_set :: #force_inline proc ( style : UI_Style, preset : UI_StylePreset ) { stack_peek_ref( & get_state().ui_context.style_combo_stack ).array[preset] = style }
|
12
code/sectr/ui/docking.odin
Normal file
12
code/sectr/ui/docking.odin
Normal file
@ -0,0 +1,12 @@
|
||||
package sectr
|
||||
|
||||
UI_DockedEntry :: struct {
|
||||
placeholder : int,
|
||||
}
|
||||
|
||||
// Non-overlapping, Tiled, window/frame manager
|
||||
// Has support for tabbing
|
||||
// AKA: Tiled Window Manager
|
||||
UI_Docking :: struct {
|
||||
placeholder : int,
|
||||
}
|
178
code/sectr/ui/floating.odin
Normal file
178
code/sectr/ui/floating.odin
Normal file
@ -0,0 +1,178 @@
|
||||
package sectr
|
||||
|
||||
UI_Floating :: struct {
|
||||
label : string,
|
||||
using links : DLL_NodePN(UI_Floating),
|
||||
captures : rawptr,
|
||||
builder : UI_FloatingBuilder,
|
||||
queued : b32,
|
||||
}
|
||||
UI_FloatingBuilder :: #type proc( captures : rawptr ) -> (became_active : b32)
|
||||
|
||||
// Overlapping/Stacking window/frame manager
|
||||
// AKA: Stacking or Floating Window Manager
|
||||
UI_FloatingManager :: struct {
|
||||
using links : DLL_NodeFL(UI_Floating),
|
||||
build_queue : Array(UI_Floating),
|
||||
tracked : HMapChainedPtr(UI_Floating),
|
||||
}
|
||||
|
||||
ui_floating_startup :: proc( self : ^UI_FloatingManager, allocator : Allocator, build_queue_cap, tracked_cap : u64, dbg_name : string = "" ) -> AllocatorError
|
||||
{
|
||||
error : AllocatorError
|
||||
|
||||
queue_dbg_name := str_intern(str_fmt_tmp("%s: build_queue", dbg_name))
|
||||
self.build_queue, error = array_init_reserve( UI_Floating, allocator, build_queue_cap, dbg_name = queue_dbg_name.str )
|
||||
if error != AllocatorError.None
|
||||
{
|
||||
ensure(false, "Failed to allocate the build_queue")
|
||||
return error
|
||||
}
|
||||
|
||||
tracked_dbg_name := str_intern(str_fmt_tmp("%s: tracked", dbg_name))
|
||||
self.tracked, error = hmap_chained_init(UI_Floating, uint(tracked_cap), allocator, dbg_name = tracked_dbg_name.str )
|
||||
if error != AllocatorError.None
|
||||
{
|
||||
ensure(false, "Failed to allocate tracking table")
|
||||
return error
|
||||
}
|
||||
return error
|
||||
}
|
||||
|
||||
ui_floating_reload :: proc( self : ^UI_FloatingManager, allocator : Allocator )
|
||||
{
|
||||
using self
|
||||
build_queue.backing = allocator
|
||||
hmap_chained_reload(tracked, allocator)
|
||||
}
|
||||
|
||||
ui_floating_just_builder :: #force_inline proc( label : string, builder : UI_FloatingBuilder ) -> ^UI_Floating
|
||||
{
|
||||
No_Captures : rawptr = nil
|
||||
return ui_floating_with_capture(label, No_Captures, builder)
|
||||
}
|
||||
|
||||
ui_floating_with_capture :: proc( label : string, captures : rawptr = nil, builder : UI_FloatingBuilder ) -> ^UI_Floating
|
||||
{
|
||||
entry := UI_Floating {
|
||||
label = label,
|
||||
captures = captures,
|
||||
builder = builder,
|
||||
}
|
||||
|
||||
floating := get_state().ui_floating_context
|
||||
array_append( & floating.build_queue, entry )
|
||||
return nil
|
||||
}
|
||||
|
||||
@(deferred_none = ui_floating_manager_end)
|
||||
ui_floating_manager :: proc ( manager : ^UI_FloatingManager )
|
||||
{
|
||||
ui_floating_manager_begin(manager)
|
||||
}
|
||||
|
||||
ui_floating_manager_begin :: proc ( manager : ^UI_FloatingManager )
|
||||
{
|
||||
state := get_state()
|
||||
state.ui_floating_context = manager
|
||||
}
|
||||
|
||||
ui_floating_manager_end :: proc()
|
||||
{
|
||||
state := get_state()
|
||||
floating := & state.ui_floating_context
|
||||
ui_floating_build()
|
||||
floating = nil
|
||||
}
|
||||
|
||||
ui_floating_build :: proc()
|
||||
{
|
||||
ui := ui_context()
|
||||
using floating := get_state().ui_floating_context
|
||||
|
||||
for to_enqueue in array_to_slice( build_queue)
|
||||
{
|
||||
key := ui_key_from_string(to_enqueue.label)
|
||||
lookup := hmap_chained_get( tracked, transmute(u64) key )
|
||||
|
||||
// Check if entry is already present
|
||||
if lookup != nil && (lookup.next != nil || lookup == last) {
|
||||
lookup.captures = to_enqueue.captures
|
||||
lookup.builder = to_enqueue.builder
|
||||
lookup.queued = true
|
||||
continue
|
||||
}
|
||||
|
||||
if lookup == nil {
|
||||
error : AllocatorError
|
||||
lookup, error = hmap_chained_set( tracked, transmute(u64) key, to_enqueue )
|
||||
if error != AllocatorError.None {
|
||||
ensure(false, "Failed to allocate entry to hashtable")
|
||||
continue
|
||||
}
|
||||
}
|
||||
else {
|
||||
lookup.captures = to_enqueue.captures
|
||||
lookup.builder = to_enqueue.builder
|
||||
}
|
||||
lookup.queued = true
|
||||
dll_full_push_back(floating, lookup, nil )
|
||||
// if first == nil {
|
||||
// first = lookup
|
||||
// last = lookup
|
||||
// continue
|
||||
// }
|
||||
// if first == last {
|
||||
// last = lookup
|
||||
// last.prev = first
|
||||
// first.next = last
|
||||
// continue
|
||||
// }
|
||||
// last.next = lookup
|
||||
// lookup.prev = last
|
||||
// last = lookup
|
||||
}
|
||||
array_clear(build_queue)
|
||||
|
||||
to_raise : ^UI_Floating
|
||||
for entry := first; entry != nil; entry = entry.next
|
||||
{
|
||||
if ! entry.queued
|
||||
{
|
||||
ensure(false, "There should be no queue failures yet")
|
||||
|
||||
if entry == first
|
||||
{
|
||||
first = entry.next
|
||||
entry.next = nil
|
||||
continue
|
||||
}
|
||||
if entry == last
|
||||
{
|
||||
last = last.prev
|
||||
last.prev = nil
|
||||
entry.prev = nil
|
||||
continue
|
||||
}
|
||||
|
||||
left := entry.prev
|
||||
right := entry.next
|
||||
|
||||
left.next = right
|
||||
right.prev = left
|
||||
entry.prev = nil
|
||||
entry.next = nil
|
||||
}
|
||||
|
||||
if entry.builder( entry.captures ) && entry != last && to_raise == nil
|
||||
{
|
||||
to_raise = entry
|
||||
}
|
||||
entry.queued = false
|
||||
}
|
||||
if to_raise != nil
|
||||
{
|
||||
dll_full_pop( to_raise, floating )
|
||||
dll_full_push_back( floating, to_raise, nil )
|
||||
}
|
||||
}
|
142
code/sectr/ui/layout_widget.odin
Normal file
142
code/sectr/ui/layout_widget.odin
Normal file
@ -0,0 +1,142 @@
|
||||
package sectr
|
||||
|
||||
/*
|
||||
Widget Layout Ops
|
||||
*/
|
||||
|
||||
ui_layout_children_horizontally :: proc( container : ^UI_Box, direction : UI_LayoutDirectionX, width_ref : ^f32 )
|
||||
{
|
||||
container_width : f32
|
||||
if width_ref != nil {
|
||||
container_width = width_ref ^
|
||||
}
|
||||
else {
|
||||
container_width = container.computed.content.max.x - container.computed.content.min.x
|
||||
}
|
||||
|
||||
// do layout calculations for the children
|
||||
total_stretch_ratio : f32 = 0.0
|
||||
size_req_children : f32 = 0
|
||||
for child := container.first; child != nil; child = child.next
|
||||
{
|
||||
using child.layout
|
||||
scaled_width_by_height : b32 = b32(.Scale_Width_By_Height_Ratio in flags)
|
||||
if .Fixed_Width in flags
|
||||
{
|
||||
if scaled_width_by_height {
|
||||
height := size.max.y != 0 ? size.max.y : container_width
|
||||
width := height * size.min.x
|
||||
|
||||
size_req_children += width
|
||||
continue
|
||||
}
|
||||
|
||||
size_req_children += size.min.x
|
||||
continue
|
||||
}
|
||||
|
||||
total_stretch_ratio += anchor.ratio.x
|
||||
}
|
||||
|
||||
avail_flex_space := container_width - size_req_children
|
||||
|
||||
allocate_space :: proc( child : ^UI_Box, total_stretch_ratio, avail_flex_space : f32 )
|
||||
{
|
||||
using child.layout
|
||||
if ! (.Fixed_Width in flags) {
|
||||
size.min.x = anchor.ratio.x * (1 / total_stretch_ratio) * avail_flex_space - child.layout.margins.left - child.layout.margins.right
|
||||
}
|
||||
flags |= {.Fixed_Width}
|
||||
}
|
||||
|
||||
space_used : f32 = 0.0
|
||||
switch direction{
|
||||
case .Right_To_Left:
|
||||
for child := container.last; child != nil; child = child.prev {
|
||||
allocate_space(child, total_stretch_ratio, avail_flex_space)
|
||||
using child.layout
|
||||
anchor = range2({0, 0}, {0, 0})
|
||||
pos.x = space_used
|
||||
space_used += size.min.x + child.layout.margins.left + child.layout.margins.right
|
||||
}
|
||||
case .Left_To_Right:
|
||||
for child := container.first; child != nil; child = child.next {
|
||||
allocate_space(child, total_stretch_ratio, avail_flex_space)
|
||||
using child.layout
|
||||
anchor = range2({0, 0}, {0, 0})
|
||||
pos.x = space_used
|
||||
space_used += size.min.x + child.layout.margins.left + child.layout.margins.right
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ui_layout_children_vertically :: proc( container : ^UI_Box, direction : UI_LayoutDirectionY, height_ref : ^f32 )
|
||||
{
|
||||
container_height : f32
|
||||
if height_ref != nil {
|
||||
container_height = height_ref ^
|
||||
}
|
||||
else {
|
||||
container_height = container.computed.content.max.y - container.computed.content.min.y
|
||||
}
|
||||
|
||||
// do layout calculations for the children
|
||||
total_stretch_ratio : f32 = 0.0
|
||||
size_req_children : f32 = 0
|
||||
for child := container.first; child != nil; child = child.next
|
||||
{
|
||||
using child.layout
|
||||
scaled_width_by_height : b32 = b32(.Scale_Width_By_Height_Ratio in flags)
|
||||
if .Fixed_Height in flags
|
||||
{
|
||||
if scaled_width_by_height {
|
||||
width := size.max.x != 0 ? size.max.x : container_height
|
||||
height := width * size.min.y
|
||||
|
||||
size_req_children += height
|
||||
continue
|
||||
}
|
||||
|
||||
size_req_children += size.min.y
|
||||
continue
|
||||
}
|
||||
|
||||
total_stretch_ratio += anchor.ratio.y
|
||||
}
|
||||
|
||||
avail_flex_space := container_height - size_req_children
|
||||
|
||||
allocate_space :: proc( child : ^UI_Box, total_stretch_ratio, avail_flex_space : f32 )
|
||||
{
|
||||
using child.layout
|
||||
if ! (.Fixed_Height in flags) {
|
||||
size.min.y = (anchor.ratio.y * (1 / total_stretch_ratio) * avail_flex_space)
|
||||
}
|
||||
flags |= {.Fixed_Height}
|
||||
}
|
||||
|
||||
space_used : f32 = 0.0
|
||||
switch direction
|
||||
{
|
||||
case .Bottom_To_Top:
|
||||
for child := container.last; child != nil; child = child.prev {
|
||||
allocate_space(child, total_stretch_ratio, avail_flex_space)
|
||||
using child.layout
|
||||
anchor = range2({0,0}, {0, 0})
|
||||
// alignment = {0, 0}
|
||||
pos.y = -space_used
|
||||
space_used += size.min.y
|
||||
size.min.x = container.computed.content.max.x + container.computed.content.min.x
|
||||
}
|
||||
case .Top_To_Bottom:
|
||||
for child := container.first; child != nil; child = child.next {
|
||||
allocate_space(child, total_stretch_ratio, avail_flex_space)
|
||||
using child.layout
|
||||
anchor = range2({0, 0}, {0, 0})
|
||||
// alignment = {0, 0}
|
||||
pos.y = -space_used
|
||||
space_used += size.min.y
|
||||
size.min.x = container.computed.content.max.x - container.computed.content.min.x
|
||||
}
|
||||
}
|
||||
}
|
299
code/sectr/ui/tests.odin
Normal file
299
code/sectr/ui/tests.odin
Normal file
@ -0,0 +1,299 @@
|
||||
package sectr
|
||||
|
||||
import "core:math/linalg"
|
||||
import str "core:strings"
|
||||
|
||||
test_hover_n_click :: proc()
|
||||
{
|
||||
state := get_state(); using state
|
||||
|
||||
first_btn := ui_button( "FIRST BOX!" )
|
||||
if first_btn.left_clicked || debug.frame_2_created {
|
||||
debug.frame_2_created = true
|
||||
|
||||
second_layout := first_btn.layout
|
||||
second_layout.pos = { 250, 0 }
|
||||
ui_layout( second_layout )
|
||||
|
||||
second_box := ui_button( "SECOND BOX!")
|
||||
}
|
||||
}
|
||||
|
||||
test_draggable :: proc()
|
||||
{
|
||||
state := get_state(); using state
|
||||
ui := ui_context
|
||||
|
||||
draggable_layout := UI_Layout {
|
||||
flags = {
|
||||
.Fixed_Position_X, .Fixed_Position_Y,
|
||||
.Fixed_Width, .Fixed_Height,
|
||||
.Origin_At_Anchor_Center,
|
||||
},
|
||||
// alignment = { 0.0, 0.5 },
|
||||
alignment = { 0.5, 0 },
|
||||
text_alignment = { 0.0, 0.0 },
|
||||
// alignment = { 1.0, 1.0 },
|
||||
pos = { 0, 0 },
|
||||
size = range2({ 200, 200 }, {}),
|
||||
}
|
||||
ui_layout( draggable_layout )
|
||||
ui_style( UI_Style {
|
||||
corner_radii = { 0.3, 0.3, 0.3, 0.3 },
|
||||
})
|
||||
|
||||
draggable := ui_widget( "Draggable Box!", UI_BoxFlags { .Mouse_Clickable } )
|
||||
if draggable.first_frame {
|
||||
debug.draggable_box_pos = draggable.layout.pos + { 0, -100 }
|
||||
debug.draggable_box_size = draggable.layout.size.min
|
||||
}
|
||||
|
||||
// Dragging
|
||||
if draggable.active {
|
||||
debug.draggable_box_pos += mouse_world_delta()
|
||||
}
|
||||
|
||||
if (ui.hot == draggable.key) {
|
||||
draggable.style.bg_color = Color_Blue
|
||||
}
|
||||
|
||||
draggable.layout.pos = debug.draggable_box_pos
|
||||
draggable.layout.size.min = debug.draggable_box_size
|
||||
|
||||
draggable.text = { str_fmt_alloc("%v", debug.draggable_box_pos), {} }
|
||||
draggable.text.runes = to_runes(draggable.text.str)
|
||||
}
|
||||
|
||||
test_parenting :: proc( default_layout : ^UI_Layout, frame_style_default : ^UI_Style )
|
||||
{
|
||||
state := get_state(); using state
|
||||
ui := ui_context
|
||||
|
||||
// frame := ui_widget( "Frame", {} )
|
||||
// ui_parent(frame)
|
||||
parent_layout := default_layout ^
|
||||
parent_layout.size = range2( { 300, 300 }, {} )
|
||||
parent_layout.alignment = { 0.0, 0.0 }
|
||||
// parent_layout.margins = { 100, 100, 100, 100 }
|
||||
parent_layout.padding = { 5, 10, 5, 5 }
|
||||
parent_layout.pos = { 0, 0 }
|
||||
parent_layout.flags = {
|
||||
.Fixed_Position_X, .Fixed_Position_Y,
|
||||
.Fixed_Width, .Fixed_Height,
|
||||
.Origin_At_Anchor_Center
|
||||
}
|
||||
ui_layout(parent_layout)
|
||||
|
||||
parent_style := frame_style_default ^
|
||||
ui_style(parent_style)
|
||||
|
||||
parent := ui_widget( "Parent", { .Mouse_Clickable })
|
||||
ui_parent_push(parent)
|
||||
{
|
||||
if parent.first_frame {
|
||||
debug.draggable_box_pos = parent.layout.pos
|
||||
debug.draggable_box_size = parent.layout.size.min
|
||||
}
|
||||
if parent.active {
|
||||
debug.draggable_box_pos += mouse_world_delta()
|
||||
}
|
||||
if (ui.hot == parent.key) {
|
||||
parent.style.bg_color = Color_Blue
|
||||
}
|
||||
parent.layout.pos = debug.draggable_box_pos
|
||||
parent.layout.size.min = debug.draggable_box_size
|
||||
}
|
||||
ui_resizable_handles( & parent, & debug.draggable_box_pos, & debug.draggable_box_size)
|
||||
|
||||
child_layout := default_layout ^
|
||||
child_layout.size = range2({ 100, 100 }, { 0, 0 })
|
||||
child_layout.alignment = { 0.0, 0.0 }
|
||||
// child_layout.margins = { 20, 20, 20, 20 }
|
||||
child_layout.padding = { 5, 5, 5, 5 }
|
||||
// child_layout.anchor = range2({ 0.2, 0.1 }, { 0.1, 0.15 })
|
||||
child_layout.pos = { 0, 0 }
|
||||
child_layout.flags = {
|
||||
.Fixed_Width, .Fixed_Height,
|
||||
// .Origin_At_Anchor_Center
|
||||
}
|
||||
|
||||
child_style := frame_style_default ^
|
||||
child_style.bg_color = Color_GreyRed
|
||||
ui_theme(child_layout, child_style)
|
||||
child := ui_widget( "Child", { .Mouse_Clickable })
|
||||
ui_parent_pop()
|
||||
}
|
||||
|
||||
test_text_box :: proc()
|
||||
{
|
||||
state := get_state(); using state
|
||||
ui := ui_context
|
||||
|
||||
@static pos : Vec2
|
||||
layout := ui_layout_peek().default
|
||||
layout.text_alignment = { 1.0, 1.0 }
|
||||
// style.flags = { .Size_To_Text }
|
||||
layout.padding = { 10, 10, 10, 10 }
|
||||
layout.font_size = 32
|
||||
ui_layout( layout)
|
||||
|
||||
text := str_intern( "Lorem ipsum dolor sit amet")
|
||||
|
||||
text_box := ui_text("TEXT BOX!", text, flags = { .Mouse_Clickable })
|
||||
if text_box.first_frame {
|
||||
pos = text_box.layout.pos
|
||||
}
|
||||
|
||||
if text_box.active {
|
||||
pos += mouse_world_delta()
|
||||
}
|
||||
|
||||
text_box.layout.pos = pos
|
||||
|
||||
text_box.layout.size.min = { text_box.computed.text_size.x * 1.5, text_box.computed.text_size.y * 3 }
|
||||
}
|
||||
|
||||
test_whitespace_ast :: proc( default_layout : ^UI_Layout, frame_style_default : ^UI_Style )
|
||||
{
|
||||
profile("Whitespace AST test")
|
||||
state := get_state(); using state
|
||||
ui := ui_context
|
||||
|
||||
text_layout := default_layout^
|
||||
text_layout.flags = {
|
||||
.Origin_At_Anchor_Center,
|
||||
.Fixed_Position_X, .Fixed_Position_Y,
|
||||
.Fixed_Width, .Fixed_Height,
|
||||
}
|
||||
text_layout.text_alignment = { 0.0, 0.5 }
|
||||
text_layout.alignment = { 0.0, 1.0 }
|
||||
text_layout.size.min = { 1600, 30 }
|
||||
text_style := frame_style_default ^
|
||||
text_style_combo := to_ui_style_combo(text_style)
|
||||
text_style_combo.default.bg_color = Color_Transparent
|
||||
text_style_combo.disabled.bg_color = Color_Frame_Disabled
|
||||
text_style_combo.hot.bg_color = Color_Frame_Hover
|
||||
text_style_combo.active.bg_color = Color_Frame_Select
|
||||
ui_theme( text_layout, text_style )
|
||||
|
||||
alloc_error : AllocatorError; success : bool
|
||||
// debug.lorem_content, success = os.read_entire_file( debug.path_lorem, frame_allocator() )
|
||||
|
||||
// debug.lorem_parse, alloc_error = pws_parser_parse( transmute(string) debug.lorem_content, frame_slab_allocator() )
|
||||
// verify( alloc_error == .None, "Faield to parse due to allocation failure" )
|
||||
|
||||
text_space := str_intern( " " )
|
||||
text_tab := str_intern( "\t")
|
||||
|
||||
// index := 0
|
||||
widgets : Array(UI_Widget)
|
||||
// widgets, alloc_error = array_init_reserve( UI_Widget, frame_slab_allocator(), 8 )
|
||||
widgets, alloc_error = array_init_reserve( UI_Widget, frame_slab_allocator(), 4 * Kilobyte )
|
||||
widgets_ptr := & widgets
|
||||
|
||||
label_id := 0
|
||||
|
||||
line_id := 0
|
||||
for line in array_to_slice( debug.lorem_parse.lines )
|
||||
{
|
||||
if line_id == 0 {
|
||||
line_id += 1
|
||||
continue
|
||||
}
|
||||
|
||||
ui_layout( text_layout )
|
||||
line_hbox := ui_widget(str_fmt_alloc( "line %v", line_id ), {.Mouse_Clickable})
|
||||
|
||||
if line_hbox.key == ui.hot
|
||||
{
|
||||
line_hbox.text = StrRunesPair {}
|
||||
ui_parent(line_hbox)
|
||||
|
||||
chunk_layout := text_layout
|
||||
chunk_layout.alignment = { 0.0, 1.0 }
|
||||
chunk_layout.anchor = range2({ 0.0, 0 }, { 0.0, 0 })
|
||||
chunk_layout.pos = {}
|
||||
chunk_layout.flags = { .Fixed_Position_X, .Size_To_Text }
|
||||
|
||||
chunk_style := text_style
|
||||
ui_theme( to_ui_layout_combo(chunk_layout), to_ui_style_combo(chunk_style) )
|
||||
|
||||
head := line.first
|
||||
for ; head != nil;
|
||||
{
|
||||
ui_layout( chunk_layout )
|
||||
widget : UI_Widget
|
||||
|
||||
#partial switch head.type
|
||||
{
|
||||
case .Visible:
|
||||
label := str_intern( str_fmt_alloc( "%v %v", head.content.str, label_id ))
|
||||
widget = ui_text( label.str, head.content )
|
||||
label_id += 1
|
||||
|
||||
chunk_layout.pos.x += size_range2( widget.computed.bounds ).x
|
||||
|
||||
case .Spaces:
|
||||
label := str_intern( str_fmt_alloc( "%v %v", "space", label_id ))
|
||||
widget = ui_text_spaces( label.str )
|
||||
label_id += 1
|
||||
|
||||
for idx in 1 ..< len( head.content.runes )
|
||||
{
|
||||
// TODO(Ed): VIRTUAL WHITESPACE
|
||||
// widget.style.layout.size.x += range2_size( widget.computed.bounds )
|
||||
}
|
||||
chunk_layout.pos.x += size_range2( widget.computed.bounds ).x
|
||||
|
||||
case .Tabs:
|
||||
label := str_intern( str_fmt_alloc( "%v %v", "tab", label_id ))
|
||||
widget = ui_text_tabs( label.str )
|
||||
label_id += 1
|
||||
|
||||
for idx in 1 ..< len( head.content.runes )
|
||||
{
|
||||
// widget.style.layout.size.x += range2_size( widget.computed.bounds )
|
||||
}
|
||||
chunk_layout.pos.x += size_range2( widget.computed.bounds ).x
|
||||
}
|
||||
|
||||
array_append( widgets_ptr, widget )
|
||||
head = head.next
|
||||
}
|
||||
|
||||
line_hbox.layout.size.min.x = chunk_layout.pos.x
|
||||
}
|
||||
else
|
||||
{
|
||||
builder_backing : [16 * Kilobyte] byte
|
||||
builder := str.builder_from_bytes( builder_backing[:] )
|
||||
|
||||
line_hbox.layout.flags |= { .Size_To_Text }
|
||||
|
||||
head := line.first.next
|
||||
for ; head != nil;
|
||||
{
|
||||
str.write_string( & builder, head.content.str )
|
||||
head = head.next
|
||||
}
|
||||
|
||||
line_hbox.text = str_intern( to_string( builder ) )
|
||||
// if len(line_hbox.text.str) == 0 {
|
||||
// line_hbox.text = str_intern( " " )
|
||||
// }
|
||||
}
|
||||
|
||||
if len(line_hbox.text.str) > 0 {
|
||||
array_append( widgets_ptr, line_hbox )
|
||||
text_layout.pos.x = text_layout.pos.x
|
||||
text_layout.pos.y += size_range2(line_hbox.computed.bounds).y
|
||||
}
|
||||
else {
|
||||
text_layout.pos.y += size_range2( (& widgets.data[ widgets.num - 1 ]).computed.bounds ).y
|
||||
}
|
||||
|
||||
line_id += 1
|
||||
}
|
||||
|
||||
label_id += 1 // Dummy action
|
||||
}
|
37
code/sectr/ui/theme.odin
Normal file
37
code/sectr/ui/theme.odin
Normal file
@ -0,0 +1,37 @@
|
||||
package sectr
|
||||
|
||||
UI_ThemePtr :: struct {
|
||||
layout : ^UI_LayoutCombo,
|
||||
style : ^UI_StyleCombo,
|
||||
}
|
||||
|
||||
UI_Theme :: struct {
|
||||
layout : UI_LayoutCombo,
|
||||
style : UI_StyleCombo,
|
||||
}
|
||||
|
||||
ui_theme_pop :: #force_inline proc() {
|
||||
ui_layout_pop()
|
||||
ui_style_pop()
|
||||
}
|
||||
|
||||
@(deferred_none = ui_theme_pop)
|
||||
ui_theme_via_layout_style :: #force_inline proc( layout : UI_Layout, style : UI_Style ) {
|
||||
using ui := get_state().ui_context
|
||||
ui_layout_push( layout )
|
||||
ui_style_push( style )
|
||||
}
|
||||
|
||||
@(deferred_none = ui_theme_pop)
|
||||
ui_theme_via_combos :: #force_inline proc( layout : UI_LayoutCombo, style : UI_StyleCombo ) {
|
||||
using ui := get_state().ui_context
|
||||
ui_layout_push( layout )
|
||||
ui_style_push( style )
|
||||
}
|
||||
|
||||
@(deferred_none = ui_theme_pop)
|
||||
ui_theme_via_theme :: #force_inline proc( theme : UI_Theme ) {
|
||||
using ui := get_state().ui_context
|
||||
ui_layout_push( theme.layout )
|
||||
ui_style_push( theme.style )
|
||||
}
|
3
code/sectr/ui/util.odin
Normal file
3
code/sectr/ui/util.odin
Normal file
@ -0,0 +1,3 @@
|
||||
package sectr
|
||||
|
||||
|
541
code/sectr/ui/widgets.odin
Normal file
541
code/sectr/ui/widgets.odin
Normal file
@ -0,0 +1,541 @@
|
||||
package sectr
|
||||
|
||||
import "base:runtime"
|
||||
import lalg "core:math/linalg"
|
||||
|
||||
UI_Widget :: struct {
|
||||
using box : ^UI_Box,
|
||||
using signal : UI_Signal,
|
||||
}
|
||||
|
||||
ui_widget :: proc( label : string, flags : UI_BoxFlags ) -> (widget : UI_Widget)
|
||||
{
|
||||
widget.box = ui_box_make( flags, label )
|
||||
widget.signal = ui_signal_from_box( widget.box )
|
||||
return
|
||||
}
|
||||
|
||||
ui_button :: proc( label : string, flags : UI_BoxFlags = {} ) -> (btn : UI_Widget)
|
||||
{
|
||||
btn_flags := UI_BoxFlags { .Mouse_Clickable, .Focusable, .Click_To_Focus }
|
||||
btn.box = ui_box_make( btn_flags | flags, label )
|
||||
btn.signal = ui_signal_from_box( btn.box )
|
||||
return
|
||||
}
|
||||
|
||||
#region("Horizontal Box")
|
||||
/*
|
||||
Horizontal Boxes automatically manage a collection of widgets and
|
||||
attempt to slot them adjacent to each other along the x-axis.
|
||||
|
||||
The user must provide the direction that the hbox will append entries.
|
||||
How the widgets will be scaled will be based on the individual entires style flags.
|
||||
|
||||
All the usual behaviors that the style and box flags do apply when managed by the box widget.
|
||||
Whether or not the horizontal box will scale the widget's width is if:
|
||||
fixed size or "scale by ratio" flags are not used for the width.
|
||||
The hbox will use the anchor's (range2) ratio.x value to determine the "stretch ratio".
|
||||
|
||||
Keep in mind the stretch ratio is only respected if no size.min.x value is violated for each of the widgets.
|
||||
*/
|
||||
|
||||
// Horizontal Widget
|
||||
UI_HBox :: struct {
|
||||
using widget : UI_Widget,
|
||||
direction : UI_LayoutDirectionX,
|
||||
}
|
||||
|
||||
// Boilerplate creation
|
||||
ui_hbox_begin :: proc( direction : UI_LayoutDirectionX, label : string, flags : UI_BoxFlags = {} ) -> (hbox : UI_HBox) {
|
||||
// profile(#procedure)
|
||||
hbox.direction = direction
|
||||
hbox.box = ui_box_make( flags, label )
|
||||
hbox.signal = ui_signal_from_box(hbox.box)
|
||||
// ui_box_compute_layout(hbox)
|
||||
return
|
||||
}
|
||||
|
||||
// Auto-layout children
|
||||
ui_hbox_end :: proc( hbox : UI_HBox, width_ref : ^f32 = nil, compute_layout := true )
|
||||
{
|
||||
// profile(#procedure)
|
||||
if compute_layout do ui_box_compute_layout(hbox.box, dont_mark_fresh = true)
|
||||
ui_layout_children_horizontally( hbox.box, hbox.direction, width_ref )
|
||||
}
|
||||
|
||||
@(deferred_out = ui_hbox_end_auto)
|
||||
ui_hbox :: #force_inline proc( direction : UI_LayoutDirectionX, label : string, flags : UI_BoxFlags = {} ) -> (hbox : UI_HBox) {
|
||||
hbox = ui_hbox_begin(direction, label, flags)
|
||||
ui_parent_push(hbox.box)
|
||||
return
|
||||
}
|
||||
|
||||
// Auto-layout children and pop parent from parent stack
|
||||
ui_hbox_end_auto :: proc( hbox : UI_HBox ) {
|
||||
ui_hbox_end(hbox)
|
||||
ui_parent_pop()
|
||||
}
|
||||
#endregion("Horizontal Box")
|
||||
|
||||
#region("Resizable")
|
||||
// Parameterized widget def for ui_resizable_handles
|
||||
UI_Resizable :: struct {
|
||||
using widget : UI_Widget,
|
||||
handle_width : f32,
|
||||
theme : ^UI_Theme,
|
||||
left : bool,
|
||||
right : bool,
|
||||
top : bool,
|
||||
bottom : bool,
|
||||
corner_tr : bool,
|
||||
corner_tl : bool,
|
||||
corner_br : bool,
|
||||
corner_bl : bool,
|
||||
compute_layout : bool
|
||||
}
|
||||
|
||||
ui_resizable_begin :: proc( label : string, flags : UI_BoxFlags = {},
|
||||
handle_width : f32 = 15,
|
||||
theme : ^UI_Theme,
|
||||
left := true,
|
||||
right := true,
|
||||
top := true,
|
||||
bottom := true,
|
||||
corner_tr := true,
|
||||
corner_tl := true,
|
||||
corner_br := true,
|
||||
corner_bl := true,
|
||||
compute_layout := true ) -> (resizable : UI_Resizable)
|
||||
{
|
||||
resizable.box = ui_box_make(flags, label)
|
||||
resizable.signal = ui_signal_from_box(resizable.box)
|
||||
|
||||
resizable.handle_width = handle_width
|
||||
resizable.theme = theme
|
||||
resizable.left = left
|
||||
resizable.right = right
|
||||
resizable.top = top
|
||||
resizable.bottom = bottom
|
||||
resizable.corner_tr = corner_tr
|
||||
resizable.corner_tl = corner_tl
|
||||
resizable.corner_br = corner_br
|
||||
resizable.corner_bl = corner_bl
|
||||
resizable.compute_layout = compute_layout
|
||||
return
|
||||
}
|
||||
|
||||
ui_resizable_end :: proc( resizable : ^UI_Resizable, pos, size : ^Vec2 ) {
|
||||
using resizable
|
||||
ui_resizable_handles( & widget, pos, size,
|
||||
handle_width,
|
||||
theme,
|
||||
left,
|
||||
right,
|
||||
top,
|
||||
bottom,
|
||||
corner_tr,
|
||||
corner_tl,
|
||||
corner_br,
|
||||
corner_bl,
|
||||
compute_layout)
|
||||
}
|
||||
|
||||
ui_resizable_begin_auto :: proc() {
|
||||
|
||||
}
|
||||
|
||||
ui_resizable_end_auto :: proc() {
|
||||
|
||||
}
|
||||
|
||||
// Adds resizable handles to a widget
|
||||
ui_resizable_handles :: proc( parent : ^UI_Widget, pos : ^Vec2, size : ^Vec2,
|
||||
handle_width : f32 = 15,
|
||||
theme : ^UI_Theme = nil,
|
||||
left := true,
|
||||
right := true,
|
||||
top := true,
|
||||
bottom := true,
|
||||
corner_tr := true,
|
||||
corner_tl := true,
|
||||
corner_br := true,
|
||||
corner_bl := true,
|
||||
compute_layout := true) -> (drag_signal : b32)
|
||||
{
|
||||
profile(#procedure)
|
||||
handle_left : UI_Widget
|
||||
handle_right : UI_Widget
|
||||
handle_top : UI_Widget
|
||||
handle_bottom : UI_Widget
|
||||
handle_corner_tr : UI_Widget
|
||||
handle_corner_tl : UI_Widget
|
||||
handle_corner_br : UI_Widget
|
||||
handle_corner_bl : UI_Widget
|
||||
ui_parent(parent)
|
||||
|
||||
@(deferred_none = ui_theme_pop)
|
||||
theme_handle :: proc( base : ^UI_Theme, margins, size : Vec2, flags : UI_LayoutFlags = {})
|
||||
{
|
||||
layout_combo : UI_LayoutCombo
|
||||
style_combo : UI_StyleCombo
|
||||
if base != nil
|
||||
{
|
||||
layout_combo = base.layout
|
||||
style_combo = base.style
|
||||
{
|
||||
layout_combo.default.margins = {margins.x, margins.x, margins.y, margins.y}
|
||||
layout_combo.default.size.min = size
|
||||
}
|
||||
{
|
||||
layout_combo.hot.margins = {margins.x, margins.x, margins.y, margins.y}
|
||||
layout_combo.hot.size.min = size
|
||||
}
|
||||
{
|
||||
layout_combo.active.margins = {margins.x, margins.x, margins.y, margins.y}
|
||||
layout_combo.active.size.min = size
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
layout := UI_Layout {
|
||||
flags = flags,
|
||||
anchor = range2({},{}),
|
||||
alignment = {0, 0},
|
||||
text_alignment = {0.0, 0.0},
|
||||
font_size = 16,
|
||||
margins = to_ui_layout_side(margins),
|
||||
padding = {0, 0, 0, 0},
|
||||
border_width = 0,
|
||||
pos = {0, 0},
|
||||
size = range2(size,{})
|
||||
}
|
||||
style := UI_Style {
|
||||
bg_color = Color_Transparent,
|
||||
border_color = Color_Transparent,
|
||||
corner_radii = {5, 0, 0, 0},
|
||||
blur_size = 0,
|
||||
font = get_state().default_font,
|
||||
text_color = Color_ThmDark_Text_Default,
|
||||
cursor = {},
|
||||
}
|
||||
layout_combo = to_ui_layout_combo(layout)
|
||||
style_combo = to_ui_style_combo(style)
|
||||
{
|
||||
using layout_combo.hot
|
||||
using style_combo.hot
|
||||
bg_color = Color_ThmDark_ResizeHandle_Hot
|
||||
}
|
||||
{
|
||||
using layout_combo.active
|
||||
using style_combo.active
|
||||
bg_color = Color_ThmDark_ResizeHandle_Active
|
||||
}
|
||||
}
|
||||
theme := UI_Theme {
|
||||
layout_combo, style_combo
|
||||
}
|
||||
ui_layout_push(theme.layout)
|
||||
ui_style_push(theme.style)
|
||||
}
|
||||
|
||||
flags := UI_BoxFlags { .Mouse_Clickable }
|
||||
|
||||
name :: proc( label : string ) -> string {
|
||||
parent_label := (transmute(^string) context.user_ptr) ^
|
||||
return str_intern(str_fmt_alloc("%v: %v", parent_label, label )).str
|
||||
}
|
||||
context.user_ptr = & parent.label
|
||||
|
||||
#region("Handle & Corner construction")
|
||||
theme_handle( theme, {handle_width, 0}, {handle_width,0})
|
||||
if left {
|
||||
handle_left = ui_widget(name("resize_handle_left"), flags )
|
||||
handle_left.layout.anchor.left = 0
|
||||
handle_left.layout.anchor.right = 1
|
||||
handle_left.layout.alignment = {1, 0}
|
||||
}
|
||||
if right {
|
||||
handle_right = ui_widget(name("resize_handle_right"), flags )
|
||||
handle_right.layout.anchor.left = 1
|
||||
}
|
||||
theme_handle( theme, {0, handle_width}, {0, handle_width})
|
||||
if top {
|
||||
handle_top = ui_widget(name("resize_handle_top"), flags )
|
||||
handle_top.layout.anchor.bottom = 1
|
||||
handle_top.layout.alignment = {0, -1}
|
||||
}
|
||||
if bottom {
|
||||
handle_bottom = ui_widget("resize_handle_bottom", flags)
|
||||
handle_bottom.layout.anchor.top = 1
|
||||
handle_bottom.layout.alignment = { 0, 0 }
|
||||
}
|
||||
theme_handle( theme, {0,0}, {handle_width, handle_width}, {.Fixed_Width, .Fixed_Height} )
|
||||
if corner_tl {
|
||||
handle_corner_tl = ui_widget(name("corner_top_left"), flags)
|
||||
handle_corner_tl.layout.alignment = {1, -1}
|
||||
}
|
||||
if corner_tr {
|
||||
handle_corner_tr = ui_widget(name("corner_top_right"), flags)
|
||||
handle_corner_tr.layout.anchor = range2({1, 0}, {})
|
||||
handle_corner_tr.layout.alignment = {0, -1}
|
||||
}
|
||||
if corner_bl {
|
||||
handle_corner_bl = ui_widget("corner_bottom_left", flags)
|
||||
handle_corner_bl.layout.anchor = range2({}, {0, 1})
|
||||
handle_corner_bl.layout.alignment = { 1, 0 }
|
||||
}
|
||||
if corner_br {
|
||||
handle_corner_br = ui_widget("corner_bottom_right", flags)
|
||||
handle_corner_br.layout.anchor = range2({1, 0}, {0, 1})
|
||||
handle_corner_br.layout.alignment = {0, 0}
|
||||
}
|
||||
#endregion("Handle & Corner construction")
|
||||
|
||||
process_handle_drag :: #force_inline proc ( handle : ^UI_Widget,
|
||||
direction : Vec2,
|
||||
target_alignment : Vec2,
|
||||
target_center_aligned : Vec2,
|
||||
pos : ^Vec2,
|
||||
size : ^Vec2,
|
||||
alignment : ^Vec2, ) -> b32
|
||||
{
|
||||
@static active_context : ^UI_State
|
||||
@static was_dragging : b32 = false
|
||||
@static start_size : Vec2
|
||||
@static prev_left_shift_held : b8
|
||||
@static prev_alignment : Vec2
|
||||
|
||||
ui := get_state().ui_context
|
||||
using handle
|
||||
if ui.last_pressed_key != key || (!active && (!released || !was_dragging)) do return false
|
||||
|
||||
direction := direction
|
||||
align_adjsutment := left_shift_held ? target_center_aligned : target_alignment
|
||||
|
||||
size_delta := ui_drag_delta()
|
||||
pos_adjust := size^ * (alignment^ - align_adjsutment)
|
||||
pos_reverse := size^ * (alignment^ - prev_alignment)
|
||||
|
||||
shift_changed := (left_shift_held != prev_left_shift_held)
|
||||
|
||||
need_to_change_alignment_and_pos := pressed || shift_changed
|
||||
|
||||
if active
|
||||
{
|
||||
if pressed
|
||||
{
|
||||
active_context = ui
|
||||
start_size = size^
|
||||
prev_left_shift_held = left_shift_held
|
||||
}
|
||||
if (.Origin_At_Anchor_Center in parent.layout.flags) && !left_shift_held {
|
||||
pos_adjust = size^ * 0.5 * direction
|
||||
pos_reverse = size^ * 0.5 * direction
|
||||
}
|
||||
|
||||
latest_size := start_size + size_delta * direction
|
||||
|
||||
if pressed
|
||||
{
|
||||
pos^ -= pos_adjust
|
||||
}
|
||||
else if shift_changed
|
||||
{
|
||||
if (.Origin_At_Anchor_Center in parent.layout.flags) {
|
||||
pos^ -= pos_reverse
|
||||
alignment^ = !left_shift_held ? target_center_aligned : target_alignment
|
||||
}
|
||||
else
|
||||
{
|
||||
if !left_shift_held {
|
||||
pos^ -= size^ * direction * 0.5
|
||||
alignment^ = target_center_aligned
|
||||
}
|
||||
else {
|
||||
pos^ += size^ * direction * 0.5 // Right
|
||||
alignment^ = target_alignment
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
size^ = latest_size
|
||||
alignment^ = align_adjsutment
|
||||
}
|
||||
was_dragging = true
|
||||
}
|
||||
else if released && was_dragging
|
||||
{
|
||||
// This needed to be added as for some reason, this was getting called in screen_ui even when we were resizing with a handle in a worksapce
|
||||
if active_context != ui do return false
|
||||
|
||||
if (.Origin_At_Anchor_Center in parent.layout.flags) && !left_shift_held {
|
||||
pos_adjust = size^ * 0.5 * direction
|
||||
pos_reverse = size^ * 0.5 * direction
|
||||
}
|
||||
pos^ += pos_adjust
|
||||
alignment^ = align_adjsutment
|
||||
was_dragging = false
|
||||
start_size = 0
|
||||
}
|
||||
// text = active_context.root.label
|
||||
// style.text_color = Color_White
|
||||
|
||||
prev_left_shift_held = handle.left_shift_held
|
||||
prev_alignment = align_adjsutment
|
||||
return was_dragging
|
||||
}
|
||||
|
||||
state := get_state()
|
||||
alignment := & parent.layout.alignment
|
||||
|
||||
if .Origin_At_Anchor_Center in parent.layout.flags
|
||||
{
|
||||
if right do drag_signal |= process_handle_drag( & handle_right, { 1, 0}, { 0.5, 0}, {0, 0}, pos, size, alignment )
|
||||
if left do drag_signal |= process_handle_drag( & handle_left, {-1, 0}, {-0.5, 0}, {0, 0}, pos, size, alignment )
|
||||
if top do drag_signal |= process_handle_drag( & handle_top, { 0, 1}, { 0, 0.5}, {0, 0}, pos, size, alignment )
|
||||
if bottom do drag_signal |= process_handle_drag( & handle_bottom, { 0, -1}, { 0, -0.5}, {0, 0}, pos, size, alignment )
|
||||
if corner_tr do drag_signal |= process_handle_drag( & handle_corner_tr, { 1, 1}, { 0.5, 0.5}, {0, 0}, pos, size, alignment )
|
||||
if corner_tl do drag_signal |= process_handle_drag( & handle_corner_tl, {-1, 1}, {-0.5, 0.5}, {0, 0}, pos, size, alignment )
|
||||
if corner_br do drag_signal |= process_handle_drag( & handle_corner_br, { 1, -1}, { 0.5, -0.5}, {0, 0}, pos, size, alignment )
|
||||
if corner_bl do drag_signal |= process_handle_drag( & handle_corner_bl, {-1, -1}, {-0.5, -0.5}, {0, 0}, pos, size, alignment )
|
||||
}
|
||||
else
|
||||
{
|
||||
if right do drag_signal |= process_handle_drag( & handle_right, { 1, 0 }, {0, 0}, { 0.5, 0}, pos, size, alignment )
|
||||
if left do drag_signal |= process_handle_drag( & handle_left, { -1, 0 }, {1, 0}, { 0.5, 0}, pos, size, alignment )
|
||||
if top do drag_signal |= process_handle_drag( & handle_top, { 0, 1 }, {0, -1}, { 0.0, -0.5}, pos, size, alignment )
|
||||
if bottom do drag_signal |= process_handle_drag( & handle_bottom, { 0, -1 }, {0, 0}, { 0.0, -0.5}, pos, size, alignment )
|
||||
if corner_tr do drag_signal |= process_handle_drag( & handle_corner_tr, { 1, 1 }, {0, -1}, { 0.5, -0.5}, pos, size, alignment )
|
||||
if corner_tl do drag_signal |= process_handle_drag( & handle_corner_tl, { -1, 1 }, {1, -1}, { 0.5, -0.5}, pos, size, alignment )
|
||||
if corner_br do drag_signal |= process_handle_drag( & handle_corner_br, { 1, -1 }, {0, 0}, { 0.5, -0.5}, pos, size, alignment )
|
||||
if corner_bl do drag_signal |= process_handle_drag( & handle_corner_bl, { -1, -1 }, {1, 0}, { 0.5, -0.5}, pos, size, alignment )
|
||||
}
|
||||
|
||||
if drag_signal && compute_layout do ui_box_compute_layout(parent)
|
||||
return
|
||||
}
|
||||
#endregion("Resizable")
|
||||
|
||||
ui_spacer :: proc( label : string ) -> (widget : UI_Widget) {
|
||||
widget.box = ui_box_make( {.Mouse_Clickable}, label )
|
||||
widget.signal = ui_signal_from_box( widget.box )
|
||||
|
||||
widget.style.bg_color = Color_Transparent
|
||||
return
|
||||
}
|
||||
|
||||
UI_ScrollBox :: struct {
|
||||
using widget : UI_Widget,
|
||||
scroll_bar : UI_Widget,
|
||||
content : UI_Widget,
|
||||
}
|
||||
|
||||
ui_scroll_box :: proc( label : string, flags : UI_BoxFlags ) -> (scroll_box : UI_ScrollBox) {
|
||||
fatal("NOT IMPLEMENTED")
|
||||
return
|
||||
}
|
||||
|
||||
// ui_scrollable_view( )
|
||||
|
||||
#region("Text")
|
||||
|
||||
ui_text :: proc( label : string, content : StrRunesPair, flags : UI_BoxFlags = {} ) -> UI_Widget
|
||||
{
|
||||
// profile(#procedure)
|
||||
state := get_state(); using state
|
||||
|
||||
box := ui_box_make( flags, label )
|
||||
signal := ui_signal_from_box( box )
|
||||
|
||||
box.text = content
|
||||
return { box, signal }
|
||||
}
|
||||
|
||||
ui_text_spaces :: proc( label : string, flags : UI_BoxFlags = {} ) -> UI_Widget
|
||||
{
|
||||
// profile(#procedure)
|
||||
state := get_state(); using state
|
||||
|
||||
// TODO(Ed) : Move this somwhere in state.
|
||||
space_str := str_intern( " " )
|
||||
|
||||
box := ui_box_make( flags, label )
|
||||
signal := ui_signal_from_box( box )
|
||||
|
||||
box.text = space_str
|
||||
return { box, signal }
|
||||
}
|
||||
|
||||
ui_text_tabs :: proc( label : string, flags : UI_BoxFlags = {} ) -> UI_Widget
|
||||
{
|
||||
// profile(#procedure)
|
||||
state := get_state(); using state
|
||||
|
||||
// TODO(Ed) : Move this somwhere in state.
|
||||
tab_str := str_intern( "\t" )
|
||||
|
||||
box := ui_box_make( flags, label )
|
||||
signal := ui_signal_from_box( box )
|
||||
|
||||
box.text = tab_str
|
||||
return { box, signal }
|
||||
}
|
||||
|
||||
ui_text_wrap_panel :: proc( parent : ^UI_Widget )
|
||||
{
|
||||
fatal("NOT IMPLEMENTED")
|
||||
}
|
||||
#endregion("Text")
|
||||
|
||||
#region("Vertical Box")
|
||||
/*
|
||||
Vertical Boxes automatically manage a collection of widgets and
|
||||
attempt to slot them adjacent to each other along the y-axis.
|
||||
|
||||
The user must provide the direction that the vbox will append entries.
|
||||
How the widgets will be scaled will be based on the individual entires style flags.
|
||||
|
||||
All the usual behaviors that the style and box flags do apply when managed by the box widget.
|
||||
Whether or not the horizontal box will scale the widget's width is if:
|
||||
fixed size or "scale by ratio" flags are not used for the width.
|
||||
The hbox will use the anchor's (range2) ratio.y value to determine the "stretch ratio".
|
||||
|
||||
Keep in mind the stretch ratio is only respected if no size.min.y value is violated for each of the widgets.
|
||||
*/
|
||||
|
||||
UI_VBox :: struct {
|
||||
using widget : UI_Widget,
|
||||
direction : UI_LayoutDirectionY,
|
||||
}
|
||||
|
||||
// Boilerplate creation
|
||||
ui_vbox_begin :: proc( direction : UI_LayoutDirectionY, label : string, flags : UI_BoxFlags = {}, compute_layout := false ) -> (vbox : UI_VBox) {
|
||||
// profile(#procedure)
|
||||
vbox.direction = direction
|
||||
vbox.box = ui_box_make( flags, label )
|
||||
vbox.signal = ui_signal_from_box( vbox.box )
|
||||
if compute_layout do ui_box_compute_layout(vbox, dont_mark_fresh = true)
|
||||
return
|
||||
}
|
||||
|
||||
// Auto-layout children
|
||||
ui_vbox_end :: proc( vbox : UI_VBox, height_ref : ^f32 = nil, compute_layout := true ) {
|
||||
// profile(#procedure)
|
||||
if compute_layout do ui_box_compute_layout(vbox, dont_mark_fresh = true)
|
||||
ui_layout_children_vertically( vbox.box, vbox.direction, height_ref )
|
||||
}
|
||||
|
||||
// Auto-layout children and pop parent from parent stack
|
||||
ui_vbox_end_pop_parent :: proc( vbox : UI_VBox ) {
|
||||
ui_parent_pop()
|
||||
ui_vbox_end(vbox)
|
||||
}
|
||||
|
||||
@(deferred_out = ui_vbox_end_pop_parent)
|
||||
ui_vbox :: #force_inline proc( direction : UI_LayoutDirectionY, label : string, flags : UI_BoxFlags = {} ) -> (vbox : UI_VBox) {
|
||||
vbox = ui_vbox_begin(direction, label, flags)
|
||||
ui_parent_push(vbox.widget)
|
||||
return
|
||||
}
|
||||
#endregion("Vertical Box")
|
Reference in New Issue
Block a user