├── .gitignore ├── .gitmodules ├── LICENSE ├── Makefile ├── README.md ├── build.bat ├── changelog.txt ├── current.todool ├── ols.json ├── showcase └── todool_preview.png └── src ├── assets ├── Kanban Mode Icon.png ├── Lato-Bold.ttf ├── Lato-Regular.ttf ├── List Mode Icon.png ├── NotoSans-Regular.ttf ├── Task Drag Icon.png ├── big.txt ├── comp_trie.bin ├── frag.glsl ├── hue.png ├── icons.ttf ├── logo.png ├── sv.png └── vert.glsl ├── bookmarks.odin ├── box.odin ├── btrie └── trie.odin ├── cam.odin ├── changelog.odin ├── color.odin ├── copy.odin ├── cutf8 └── utf8.odin ├── dialog.odin ├── element.odin ├── fuzz └── fuzz.odin ├── global.odin ├── hsluv.odin ├── keymap.odin ├── keymap_editor.odin ├── main.odin ├── pattern.odin ├── pomodoro.odin ├── pool.odin ├── power_mode.odin ├── rect.odin ├── renderer.odin ├── save.odin ├── search.odin ├── shortcuts.odin ├── sidebar.odin ├── small_string.odin ├── sounds ├── timer_ended.wav ├── timer_main.sfx ├── timer_resume.wav ├── timer_start.wav └── timer_stop.wav ├── spell_check.odin ├── statusbar.odin ├── task.odin ├── tfd ├── Makefile ├── build.bat ├── hello.c ├── main.a ├── main.c ├── main.lib ├── main.obj ├── tfd.odin ├── tinyfiledialogs.c └── tinyfiledialogs.h ├── theme.odin ├── timing.odin ├── undo.odin └── wrap.odin /.gitignore: -------------------------------------------------------------------------------- 1 | /target -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "nfd/nfd"] 2 | path = nfd/nfd 3 | url = https://github.com/mlabbe/nativefiledialog 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Michael Kutowski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | @odin build src -out:target/todool -thread-count:12 && cd target && ./todool 3 | 4 | release: 5 | @odin build src -out:target/todool -o:speed -thread-count:12 6 | 7 | debug: 8 | @odin build src -out:target/todool -debug -thread-count:12 9 | 10 | run: 11 | @cd target && ./todool 12 | 13 | check: 14 | @odin check src -thread-count:12 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Todool 2 | 3 | ![preview](showcase/todool_preview.png) 4 | 5 | I developed [Todool](https://todool.de/) fulltime from 2022-2023 in [odin](https://odin-lang.org/). I started selling it in November 2022 but quickly felt a bit of burnout. Lot's of reworks lead to the current state of things, which I feel like reworking again... 6 | 7 | Instead of letting the project die completly I'll share the source here, have fun with it! 8 | 9 | ## What I'd rework 10 | 11 | - Renderer: compute tiled sdf renderer to save up on rerendering 12 | - UI: immediate style to be more detatched from the data 13 | - DB: store the editor content in a database and write out a readable text file that ppl can have as a backup or git inspection 14 | - Kanban: remove kanban and focus solely on "List Mode" and make that better 15 | 16 | ## Tech 17 | 18 | - OpenGL 3.3 renderer similar to 4coder 19 | - SDL2 20 | - fontstash - glyph atlas [fontstash](https://github.com/memononen/fontstash) 21 | - RMGUI - similar to [luigi](https://github.com/nakst/luigi/blob/main/luigi.h) 22 | - Undo/Redo with callbacks (not that nice) 23 | - Custom binary file format (trash) 24 | 25 | ## How To Build 26 | 27 | Install [Odin](https://odin-lang.org/docs/install/) based on your platform. 28 | 29 | ### Prepare Static Libraries (Linux/Darwin) 30 | 31 | 1. Build stb truetype -> go to `odin/vendor/stb/src/` and run `make` in there 32 | 2. Build -> go to `src/tfd` and run `make` in there 33 | 34 | ### Todool 35 | 36 | 1. Clone this repository. 37 | 2. Create a `target` folder next to the `src` folder 38 | 3. On Windows: run `build.bat` - On Linux/Darwin: run `make` 39 | -------------------------------------------------------------------------------- /build.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | odin.exe build src -out:target/todool.exe -thread-count:12 -subsystem:windows && pushd target && todool.exe && popd 3 | -------------------------------------------------------------------------------- /changelog.txt: -------------------------------------------------------------------------------- 1 | Version 0.4.0 2 | Task Based 3 | rework task highlights & allow multiple ones 4 | panel highlight color now available 5 | task vertical shifting animation ignores separator 6 | UI 7 | scrollbars are now floating and disappear an non usage 8 | backing shadow when dialogs appear 9 | save/load dialogs work better in fullscreen 10 | windows reappear if they already exist 11 | dialogs can use text boxes -> pattern import dialog 12 | context menus now use menu buttons with keyboard command highlights 13 | sliders reworked internally and also show start/end of ranges now 14 | Menu Bar 15 | icons added on the left of items 16 | open locals folder 17 | about dialog with version number 18 | Shift Up / Down Rework 19 | single shifts will also shift children around 20 | multi shift will shift only the selected content 21 | automatically opens folded content when tasks would result being inside a folded task 22 | Based On Feedback 23 | box word movement improved to ignore spaces properly 24 | store undo/redo text harder when space is inserted 25 | camera zooming improved to zoom near selection and mouse 26 | Animations 27 | motion trailing caret 28 | caret alpha 29 | character selection animation 30 | task outline 31 | Keymap Rework 32 | show what a command does when hovering 33 | fuzzy search command filtering 34 | dialog that steals key combinations when up 35 | highlight conflicting key commands 36 | right click on lines to remove/add 37 | Focus System 38 | movement escapes focus region 39 | press escape to quit focus 40 | focus the same parent quits focus 41 | only focus region interactable via mouse 42 | focus new parent refocuses 43 | rendering and other functionality like search is localized to focus region 44 | Save File 45 | reworked entirely -> will not load old save files 46 | now saves all task data - visible content is only the data you see currently 47 | task scale added 48 | box head / tail now saved 49 | all cameras added 50 | extensible for new byte blobs 51 | highlighted tasks are now saved 52 | General 53 | copy paste now includes newer task states properly (highlights, links, separators) 54 | using hsluv color space for randomly generated colors 55 | first task is no longer forced to be indentation zero 56 | fallback fonts now added (noto-sans europe) 57 | search state now saved + case sensitivity 58 | case-insensitive search option added 59 | pattern import now supports lua pattern matching 60 | FPS 61 | now configurable in options from 10-240fps 62 | vsync frequency is prioritized if lower than wanted fps 63 | Theme Editor Rework 64 | new color "panel highlight" added 65 | use a grid for element layouts to reduce text 66 | randomize and reset buttons as a menu bar 67 | color picker is now a floating popup 68 | pickable root color that will be used to generate other colors in the groups 69 | variation in % to generate offspring from the root 70 | keyboard navigation removed 71 | added ~10 presets 72 | press 1-9 to toggle between lock regions 73 | press "space" to randomize all 74 | press "return" to randomize tags only 75 | Command Additions / Changes 76 | "duplicate_line" now duplicates additional state too 77 | "duplicate_line" nowworks with multi-selection 78 | "change_state" only pushes undo step on changesqwe 79 | "change_state" changes parent if all state changes would be the same 80 | "insert_sibling" now inserts at the next same indentation 81 | "insert_child" inserts at the end of the children 82 | "sort_locals" now animates transitions 83 | ctrl+j = "toggle_highlight" added - toggle a slight background highlight on task selection 84 | alt+k = "toggle_separator" added - toggle a separator line on task selection 85 | alt+h = "focus_parent" added - switch focus ranges from global to parent 86 | "move_start" added - move to the start of the file 87 | "move_end" added - move to the end of the file 88 | Bugs 89 | wrapped lines selection rect doesnt cover last codepoint in line 90 | rightclick date has weird color reset on lower task 91 | mouse selection not working on line breaks fixed 92 | dialog cancel twice crash fixed 93 | clipboard changes now properly reset the priority of the next copy 94 | closing windows out of order doesnt crash anymore 95 | 96 | --------------------------------------------------------------------------------------------------- 97 | Version 0.3.2 98 | Demo Mode 99 | Latest version builds are now added which can't save 100 | Dialog spawns when trying to save 101 | changelog.txt added which will contain all changes 102 | theme editor ~ button to reset to previous theme prior to entering the editor 103 | Bookmark Jump Highlights 104 | highlights the nearest bookmark jump 105 | render lines between bookmarks to show stepping 106 | Date / Time Management 107 | double click to open calendar popup on timestamp elements 108 | can be used for any timestamp 109 | UI 110 | change slider value by ctrl+scrolling up/down 111 | Fixes 112 | tag set 6 fixed -> ctrl + 6 or 0x20 was ignored 113 | link saving/loading fixed (wrong size read) 114 | link are now copy/pasted across as expected 115 | clamp movement for small_string insertions out of bounds 116 | undo/redo for text insertion out of bounds when out of bounds 117 | UI text boxes now properly scroll again 118 | 119 | --------------------------------------------------------------------------------------------------- 120 | Version 0.3.1 HOTFIX 121 | embed the spell checker binary properly - causes crashes on startup and on clicks 122 | 123 | --------------------------------------------------------------------------------------------------- 124 | Version 0.3.0 125 | global memory allocation for wrapped lines and rendered glyphs 126 | Keymap Window 127 | button to remove selected 128 | button to record next input 129 | vim add visual left/right movement for kanban, list would be normal 130 | Date / Time Management 131 | completion date on the left 132 | theme new colors for dates & links 133 | option to insert timestamp task 134 | UI 135 | toggle selector more space left/right 136 | implement menu bar & fields & menus 137 | revamped slider style 138 | dont let dialogs be escapable from focus anymore 139 | tags ui should be clickable 140 | window x/y clamped to current display total width/height 141 | sort children based on task state count and other orderings command 142 | seperators that can be moved through indexes that will stay static 143 | task drag onto a task that has children will indent + 1 144 | cut of link text with dots... 145 | progressbars on tasks 146 | double click adds a task under selection based on diff of mouse y 147 | check if clipboard content length has changed to change to text pasting mode 148 | short zoom level indicator in the top right 149 | goto to relative jumps in vim mode 150 | Power Mode 151 | subtle screenshake 152 | particles at cursor 153 | particles at state change 154 | several options for power mode 155 | use color from the text you're currently at with the cursor 156 | try left over of particles similar to games which stick around a task or the camera 157 | Spell Checking 158 | english dictionary based on an ebook 159 | compressed trie embedded in to executable 160 | User Based Dictionary 161 | all unkown words from .todool file are automatically assumed right 162 | resets on loading different .todool file 163 | adds unknown words once you leave the task 164 | Line Number Highlights 165 | adjustable alpha via options 166 | automatically render on goto 167 | Task String Optimization 168 | limit string size to 255 169 | optimized operations (insert, remove, delete, selection) 170 | Changelog Generator 171 | task indentation fixed when upper parent isnt done 172 | start & numbering option 173 | Progressbars + Options 174 | toggle on/off -> shortcut "toggle_progressbars" 175 | percentage based 176 | only render currently hovered task option 177 | Save File 178 | text based save format 179 | autosave should save or in general changing things on the sidebar should be automatic or separate? 180 | 8 tag strings & colors now saved in .todool file 181 | delete combo if it doesnt have a command associate 182 | Link As A Button Element 183 | link string stored in .todool save file as an optional tag 184 | Bugs Fixed 185 | Image Loading 186 | still saves the image path when closing the application instantly 187 | will save image at the wanted task (mouse position) 188 | dialog escaped focus now blocks key combinations 189 | task state saved when automatically set somewhere on shift up/down 190 | hover info blocking element interaction 191 | using start.exe on windows now for opening links 192 | select all + indent OOB error fixed 193 | link button clickable when removed 194 | full display image clips away sidebar 195 | changelog scrollable height fixed 196 | mouse word selection now based on non whitespace characters 197 | theme editor sliders reformat on reset light/black 198 | mouse behaves weird on different scales with selection 199 | -------------------------------------------------------------------------------- /current.todool: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Skytrias/todool/a913e71c064c6b721bdf517229c7c192471b3451/current.todool -------------------------------------------------------------------------------- /ols.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/DanielGavin/ols/master/misc/ols.schema.json", 3 | "enable_document_symbols": true, 4 | "enable_hover": true, 5 | "enable_snippets": true 6 | } -------------------------------------------------------------------------------- /showcase/todool_preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Skytrias/todool/a913e71c064c6b721bdf517229c7c192471b3451/showcase/todool_preview.png -------------------------------------------------------------------------------- /src/assets/Kanban Mode Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Skytrias/todool/a913e71c064c6b721bdf517229c7c192471b3451/src/assets/Kanban Mode Icon.png -------------------------------------------------------------------------------- /src/assets/Lato-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Skytrias/todool/a913e71c064c6b721bdf517229c7c192471b3451/src/assets/Lato-Bold.ttf -------------------------------------------------------------------------------- /src/assets/Lato-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Skytrias/todool/a913e71c064c6b721bdf517229c7c192471b3451/src/assets/Lato-Regular.ttf -------------------------------------------------------------------------------- /src/assets/List Mode Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Skytrias/todool/a913e71c064c6b721bdf517229c7c192471b3451/src/assets/List Mode Icon.png -------------------------------------------------------------------------------- /src/assets/NotoSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Skytrias/todool/a913e71c064c6b721bdf517229c7c192471b3451/src/assets/NotoSans-Regular.ttf -------------------------------------------------------------------------------- /src/assets/Task Drag Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Skytrias/todool/a913e71c064c6b721bdf517229c7c192471b3451/src/assets/Task Drag Icon.png -------------------------------------------------------------------------------- /src/assets/comp_trie.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Skytrias/todool/a913e71c064c6b721bdf517229c7c192471b3451/src/assets/comp_trie.bin -------------------------------------------------------------------------------- /src/assets/frag.glsl: -------------------------------------------------------------------------------- 1 | #version 330 2 | 3 | // input from vertex 4 | in vec2 v_pos; 5 | in vec2 v_uv; 6 | in vec4 v_color; 7 | in vec4 v_add; 8 | in vec2 v_adjusted_half_dimensions; 9 | in float v_roundness; 10 | in float v_thickness; 11 | flat in uint v_kind; 12 | 13 | // uniforms 14 | uniform mat4 u_projection; 15 | uniform sampler2D u_sampler_font; 16 | uniform sampler2D u_sampler_sv; 17 | uniform sampler2D u_sampler_hue; 18 | uniform sampler2D u_sampler_kanban; 19 | uniform sampler2D u_sampler_list; 20 | uniform sampler2D u_sampler_drag; 21 | uniform sampler2D u_sampler_search; 22 | uniform sampler2D u_sampler_custom; 23 | uniform vec4 u_shadow_color; 24 | 25 | // output 26 | out vec4 o_color; 27 | 28 | // distance from a rectangle 29 | // doesnt do rounding 30 | float sdBox(vec2 p, vec2 b) { 31 | vec2 d = abs(p) - b; 32 | return(length(max(d, vec2(0.0, 0.0))) + min(max(d.x, d.y), 0.0)); 33 | } 34 | 35 | float sdArc(in vec2 p, in vec2 sc, in float ra, float rb) { 36 | p.x = abs(p.x); 37 | // return ((sc.y*p.x>sc.x*p.y) ? length(p-sc*ra) : abs(length(p)-ra)) - rb; 38 | return (sc.y*p.x > sc.x*p.y) ? length(p - ra*sc) - rb : abs(length(p) - ra) - rb; 39 | } 40 | 41 | float sdCircle(vec2 p, float r) { 42 | return length(p)-r; 43 | } 44 | 45 | float opOnion(float distance, float r) { 46 | return abs(distance) - r; 47 | } 48 | 49 | float sigmoid(float t) { 50 | return 1.0 / (1.0 + exp(-t)); 51 | } 52 | 53 | float sdCircleWave(vec2 p, float tb, float ra) { 54 | tb = 3.1415927*5.0/6.0 * max(tb,0.0001); 55 | vec2 co = ra*vec2(sin(tb),cos(tb)); 56 | 57 | p.x = abs(mod(p.x,co.x*4.0)-co.x*2.0); 58 | 59 | vec2 p1 = p; 60 | vec2 p2 = vec2(abs(p.x-2.0*co.x),-p.y+2.0*co.y); 61 | float d1 = ((co.y*p1.x>co.x*p1.y) ? length(p1-co) : abs(length(p1)-ra)); 62 | float d2 = ((co.y*p2.x>co.x*p2.y) ? length(p2-co) : abs(length(p2)-ra)); 63 | 64 | return min(d1, d2); 65 | } 66 | 67 | float dot2( in vec2 v ) { return dot(v,v); } 68 | float cro( in vec2 a, in vec2 b ) { return a.x*b.y - a.y*b.x; } 69 | 70 | float sdBezier(vec2 p, vec2 v0, vec2 v1, vec2 v2) { 71 | vec2 i = v0 - v2; 72 | vec2 j = v2 - v1; 73 | vec2 k = v1 - v0; 74 | vec2 w = j-k; 75 | 76 | v0-= p; v1-= p; v2-= p; 77 | 78 | float x = cro(v0, v2); 79 | float y = cro(v1, v0); 80 | float z = cro(v2, v1); 81 | 82 | vec2 s = 2.0*(y*j+z*k)-x*i; 83 | 84 | float r = (y*z-x*x*0.25)/dot2(s); 85 | float t = clamp( (0.5*x+y+r*dot(s,w))/(x+y+z),0.0,1.0); 86 | 87 | return length( v0+t*(k+k+t*w) ); 88 | } 89 | 90 | // float sdSegment(in vec2 p, in vec2 a, in vec2 b) { 91 | // vec2 ba = b - a; 92 | // vec2 pa = p - a; 93 | // float h = clamp(dot(pa, ba) / dot(ba, ba), 0., 1.); 94 | // return length(pa - h * ba); 95 | // } 96 | 97 | vec3 sdgSegment(vec2 p, vec2 a, vec2 b) { 98 | vec2 ba = b-a; 99 | vec2 pa = p-a; 100 | float h = clamp( dot(pa,ba)/dot(ba,ba), 0.0, 1.0 ); 101 | vec2 q = pa-h*ba; 102 | float d = length(q); 103 | 104 | return vec3(d,q/d); 105 | } 106 | 107 | #define RK_Invalid uint(0) 108 | #define RK_Rect uint(1) 109 | #define RK_Glyph uint(2) 110 | #define RK_Drop_Shadow uint(3) 111 | #define RK_Circle uint(4) 112 | #define RK_Circle_Outline uint(5) 113 | #define RK_Sine uint(6) 114 | #define RK_Segment uint(7) 115 | 116 | #define RK_SV uint(8) 117 | #define RK_HUE uint(9) 118 | #define RK_Kanban uint(10) 119 | #define RK_List uint(11) 120 | #define RK_Drag uint(12) 121 | #define RK_TEXTURE uint(13) 122 | 123 | void main(void) { 124 | vec4 color_goal = v_color; 125 | 126 | if (v_kind == RK_Invalid) { 127 | 128 | } else if (v_kind == RK_Rect) { 129 | // calculate distance from center and dimensions 130 | vec2 center = v_uv; 131 | float distance = sdBox(v_pos - center, v_adjusted_half_dimensions); 132 | distance -= v_roundness; 133 | 134 | // add thickness if exists 135 | if (v_thickness >= 1) { 136 | distance = (abs(distance + v_thickness) - v_thickness); 137 | } 138 | 139 | float alpha = 1.0 - smoothstep(-1, 0, distance); 140 | color_goal.a *= alpha; 141 | } else if (v_kind == RK_Glyph) { 142 | float alpha = texture(u_sampler_font, v_uv).r; 143 | color_goal.a *= alpha; 144 | } else if (v_kind == RK_Drop_Shadow) { 145 | vec2 center = v_uv; 146 | vec2 drop_size = vec2(20, 20); 147 | 148 | float drop_distance = sdBox(v_pos - center - vec2(2, 2), v_adjusted_half_dimensions - drop_size); 149 | drop_distance -= v_roundness; 150 | drop_distance = sigmoid(drop_distance * 0.25); 151 | float drop_alpha = 1 - smoothstep(0, 1, drop_distance); 152 | 153 | float rect_distance = sdBox(v_pos - center, v_adjusted_half_dimensions - drop_size); 154 | rect_distance -= v_roundness; 155 | float rect_alpha = 1 - smoothstep(-1, 1, rect_distance); 156 | 157 | color_goal = u_shadow_color; 158 | color_goal.a = drop_alpha; 159 | color_goal = mix(color_goal, v_color, rect_alpha); 160 | color_goal.a *= v_color.a; // keep v_color alpha for transition 161 | } else if (v_kind == RK_Circle) { 162 | float distance = sdCircle(v_pos - v_uv, v_roundness / 2); 163 | float alpha = 1.0 - smoothstep(-1.0, 0.0, distance); 164 | color_goal.a *= alpha; 165 | } else if (v_kind == RK_Circle_Outline) { 166 | float thickness = v_thickness; 167 | float distance = opOnion(sdCircle(v_pos - v_uv, v_roundness / 2) + thickness, thickness); 168 | 169 | float alpha = 1.0 - smoothstep(-1.0, 0.0, distance); 170 | color_goal.a *= alpha; 171 | } else if (v_kind == RK_Sine) { 172 | // basic sine wave, inverted for only wave coloring 173 | float distance = sdCircleWave(v_pos - v_uv, 0.4, 2); 174 | // float alpha = 1.0 - smoothstep(0, 0.5, distance); 175 | // float alpha = distance; 176 | float alpha = 1 - distance; 177 | color_goal.a *= alpha; 178 | } else if (v_kind == RK_Segment) { 179 | vec2 p0 = vec2(v_add.x, v_add.y); 180 | vec2 p1 = vec2(v_add.z, v_add.w); 181 | vec3 res = sdgSegment(v_pos, p0, p1) - v_thickness; 182 | 183 | float alpha = 1.0 - smoothstep(-0.5, 0.5, res.x); 184 | color_goal.a *= alpha; 185 | 186 | // float alpha = color_goal.a; 187 | // color_goal = mix(vec4(1, 0, 0, 1), color_goal, res.x); 188 | // color_goal.xyz *= vec3(res.yz, 1); 189 | // color_goal *= alpha; 190 | 191 | // color_goal = vec4(1, 0, 0, 1); 192 | } else if (v_kind == RK_SV) { 193 | vec4 texture_color = texture(u_sampler_sv, v_uv); 194 | color_goal = mix(color_goal, texture_color, texture_color.a); 195 | color_goal.a = 1; 196 | } else if (v_kind == RK_HUE) { 197 | vec4 texture_color = texture(u_sampler_hue, v_uv); 198 | color_goal = texture_color; 199 | } else if (v_kind == RK_TEXTURE) { 200 | vec4 texture_color = texture(u_sampler_custom, v_uv); 201 | color_goal = texture_color; 202 | } else if (v_kind == RK_Kanban) { 203 | vec4 texture_color = texture(u_sampler_kanban, v_uv); 204 | color_goal *= texture_color; 205 | } else if (v_kind == RK_List) { 206 | vec4 texture_color = texture(u_sampler_list, v_uv); 207 | color_goal *= texture_color; 208 | } else if (v_kind == RK_Drag) { 209 | vec4 texture_color = texture(u_sampler_drag, v_uv); 210 | color_goal *= texture_color; 211 | } 212 | 213 | o_color = color_goal; 214 | } -------------------------------------------------------------------------------- /src/assets/hue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Skytrias/todool/a913e71c064c6b721bdf517229c7c192471b3451/src/assets/hue.png -------------------------------------------------------------------------------- /src/assets/icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Skytrias/todool/a913e71c064c6b721bdf517229c7c192471b3451/src/assets/icons.ttf -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Skytrias/todool/a913e71c064c6b721bdf517229c7c192471b3451/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/sv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Skytrias/todool/a913e71c064c6b721bdf517229c7c192471b3451/src/assets/sv.png -------------------------------------------------------------------------------- /src/assets/vert.glsl: -------------------------------------------------------------------------------- 1 | #version 330 2 | 3 | // input from vertex 4 | in vec2 i_pos; 5 | in vec2 i_uv; 6 | in uint i_color; 7 | in vec4 i_add; 8 | in uint i_roundness_and_thickness; 9 | in vec2 i_additional; 10 | in uint i_kind; 11 | 12 | // uniforms 13 | uniform mat4 u_projection; 14 | 15 | // output 16 | out vec2 v_pos; 17 | out vec2 v_uv; 18 | out vec4 v_color; 19 | out vec2 v_adjusted_half_dimensions; 20 | out vec4 v_add; 21 | out float v_roundness; 22 | out float v_thickness; 23 | out vec2 v_additional; 24 | flat out uint v_kind; 25 | 26 | // get u16 information out 27 | uint uint_get_lower(uint val) { return val & uint(0xFFFF); } 28 | uint uint_get_upper(uint val) { return val >> 16 & uint(0xFFFF); } 29 | 30 | void main(void) { 31 | gl_Position = u_projection * vec4(i_pos, 0, 1); 32 | v_additional = i_additional; 33 | v_pos = i_pos; 34 | v_uv = i_uv; 35 | 36 | // only available since glsl 4.0 37 | // v_color = unpackUnorm4x8(i_color); 38 | // unwrap color from uint 39 | v_color = vec4( 40 | float((i_color ) & 0xFFu) / 255.0, 41 | float((i_color >> 8u ) & 0xFFu) / 255.0, 42 | float((i_color >> 16u) & 0xFFu) / 255.0, 43 | float((i_color >> 24u) & 0xFFu) / 255.0 // alpha 44 | ); 45 | 46 | v_roundness = float(uint_get_lower(i_roundness_and_thickness)); 47 | v_thickness = float(uint_get_upper(i_roundness_and_thickness)) / 1.5; 48 | 49 | // calculate dimensions per vertex 50 | vec2 center = i_uv; 51 | // center = round(center); 52 | vec2 half_dimensions = abs(v_pos - center); 53 | v_adjusted_half_dimensions = half_dimensions - v_roundness + vec2(0.5, 0.5); 54 | 55 | v_kind = i_kind; 56 | v_add = i_add; 57 | } -------------------------------------------------------------------------------- /src/bookmarks.odin: -------------------------------------------------------------------------------- 1 | package src 2 | 3 | import "core:fmt" 4 | import "core:math" 5 | import "core:math/rand" 6 | 7 | GRAVITY :: 9.81 8 | BOOKMARK_SPLITS :: 8 9 | 10 | Bookmark_State :: struct { 11 | current_index: int, 12 | rows: [dynamic]^Task, 13 | alpha: f32, 14 | } 15 | bs: Bookmark_State 16 | 17 | bookmark_state_init :: proc() { 18 | using bs 19 | current_index = -1 20 | rows = make([dynamic]^Task, 0, 32) 21 | } 22 | 23 | bookmark_state_destroy :: proc() { 24 | using bs 25 | delete(rows) 26 | } 27 | 28 | bookmark_nearest_index :: proc(backward: bool) -> int { 29 | // on reset set to closest from current 30 | if app_filter_not_empty() { 31 | // look for anything higher than the current index 32 | filter_index := app_task_head().filter_index 33 | found: bool 34 | 35 | if backward { 36 | // backward 37 | for i := len(bs.rows) - 1; i >= 0; i -= 1 { 38 | if bs.rows[i].filter_index < filter_index { 39 | return i 40 | } 41 | } 42 | } else { 43 | // forward 44 | for task, i in &bs.rows { 45 | if task.filter_index > filter_index { 46 | return i 47 | } 48 | } 49 | } 50 | } 51 | 52 | return -1 53 | } 54 | 55 | // advance bookmark or jump to closest on reset 56 | bookmark_advance :: proc(backward: bool) { 57 | if app.task_head == -1 { 58 | return 59 | } 60 | 61 | if bs.current_index == -1 { 62 | nearest := bookmark_nearest_index(backward) 63 | 64 | if nearest != -1 { 65 | bs.current_index = nearest 66 | return 67 | } 68 | } 69 | 70 | // just normally set 71 | range_advance_index(&bs.current_index, len(bs.rows) - 1, backward) 72 | } 73 | 74 | bookmarks_clear_and_set :: proc() { 75 | // count first 76 | clear(&bs.rows) 77 | list := app_focus_list() 78 | 79 | for index in list { 80 | task := app_task_list(index) 81 | 82 | if task_bookmark_is_valid(task) { 83 | append(&bs.rows, task) 84 | } 85 | } 86 | } 87 | 88 | bookmarks_render_connections :: proc(target: ^Render_Target, clip: RectI) { 89 | if app.task_head == -1 || len(bs.rows) <= 1 || bs.alpha == 0 { 90 | return 91 | } 92 | 93 | render_push_clip(target, clip) 94 | p_last: [2]f32 95 | count := -1 96 | goal := bs.current_index 97 | 98 | if bs.current_index == -1 { 99 | goal = bookmark_nearest_index(true) 100 | } 101 | 102 | for task in bs.rows { 103 | alpha := (count == goal ? 0.5 : 0.25) * bs.alpha 104 | color := color_alpha(theme.text_default, alpha) 105 | 106 | x, y := rect_center(task.button_bookmark.bounds) 107 | p := [2]f32 { x, y } 108 | 109 | if p_last != {} { 110 | render_line(target, p_last, p, color) 111 | } 112 | 113 | p_last = p 114 | count += 1 115 | } 116 | } 117 | 118 | bookmark_alpha_animate :: proc() -> bool { 119 | return bs.alpha > 0 120 | } 121 | 122 | bookmark_alpha_update :: proc() { 123 | if bookmark_alpha_animate() { 124 | animate_to( 125 | &bs.alpha, 126 | 0, 127 | 0.25, 128 | 0.01, 129 | ) 130 | } 131 | } -------------------------------------------------------------------------------- /src/btrie/trie.odin: -------------------------------------------------------------------------------- 1 | package btrie 2 | 3 | import "core:os" 4 | import "core:mem" 5 | import "core:math/bits" 6 | import "core:fmt" 7 | import "core:strings" 8 | 9 | ALPHABET_SIZE :: 26 10 | SHORTCUT_VALUE :: 0xFC000000 //w upper 6 bits set from u32 11 | ALPHABET_MASK :: u32(1 << 26) - 1 12 | 13 | /* 14 | CTrie (104B) 15 | uses smallest index possible to reduce size constraints 16 | data allocated in array -> easy to free/clear/delete 17 | should be the same speed of the default Trie 18 | */ 19 | 20 | // treating 0 as nil as 0 is the root 21 | CTrie :: struct #packed { 22 | array: [ALPHABET_SIZE]u32, 23 | count: u8, // count of used array fields 24 | } 25 | 26 | ctrie_data: [dynamic]CTrie 27 | 28 | ctrie_init :: proc(cap: int) { 29 | ctrie_data = make([dynamic]CTrie, 0, cap) 30 | append(&ctrie_data, CTrie {}) 31 | } 32 | 33 | ctrie_destroy :: proc() { 34 | delete(ctrie_data) 35 | } 36 | 37 | // push a node to the ctrie array and return its index 38 | ctrie_push :: proc() -> u32 { 39 | append(&ctrie_data, CTrie {}) 40 | return u32(len(ctrie_data) - 1) 41 | } 42 | 43 | // simple helper 44 | ctrie_get :: #force_inline proc(index: u32) -> ^CTrie { 45 | return &ctrie_data[index] 46 | } 47 | 48 | // get the root of the ctrie tree 49 | ctrie_root :: #force_inline proc() -> ^CTrie { 50 | return &ctrie_data[0] 51 | } 52 | 53 | // insert a key 54 | ctrie_insert :: proc(key: string) { 55 | t := ctrie_root() 56 | 57 | for b in key { 58 | idx := b - 'a' 59 | 60 | if t.array[idx] == 0 { 61 | t.array[idx] = ctrie_push() 62 | t.count += 1 63 | } 64 | 65 | t = ctrie_get(t.array[idx]) 66 | } 67 | } 68 | 69 | // print the ctrie tree 70 | ctrie_print :: proc() { 71 | depth: int 72 | ctrie_print_recursive(ctrie_root(), &depth, 0) 73 | } 74 | 75 | // print the ctrie tree recursively by nodes 76 | ctrie_print_recursive :: proc(t: ^CTrie, depth: ^int, b: u8) { 77 | if depth^ != 0 { 78 | for i in 0.. (res: ^u32) { 122 | old := comp_index 123 | res = cast(^u32) &comp[old] 124 | comp_index += size_of(u32) 125 | return 126 | } 127 | 128 | // push trie children nodes as indexes 129 | comp_push_data :: proc(count: int) -> (res: []u32) { 130 | old := comp_index 131 | res = mem.slice_ptr(cast(^u32) &comp[old], count) 132 | comp_index += size_of(u32) * count 133 | return 134 | } 135 | 136 | // baking data when single lane only 137 | single_characters: [256]u8 138 | single_index: int 139 | 140 | ctrie_check_single_only :: proc(t: ^CTrie) -> bool { 141 | if t.count ==0 || t.count == 1 { 142 | for i in 0.. insert data 174 | if t.count != 0 { 175 | field: u32 176 | 177 | // check for single branches only 178 | if t.count == 1 { 179 | single_index = 0 180 | single_only := ctrie_check_single_only(t) 181 | // fmt.eprintln("single_only?", single_only) 182 | 183 | if single_only { 184 | // insert shortcut signal 185 | field = SHORTCUT_VALUE 186 | // insert string length 187 | field = bits.bitfield_insert_u32(field, u32(single_index), 0, 26) 188 | // insert characters 189 | comp_push_shortcut(t) 190 | } 191 | } 192 | 193 | // if nothing was set 194 | if field == 0 { 195 | data := comp_push_data(int(t.count)) 196 | index: int 197 | 198 | for i in 0.. 0; i += 1 { 243 | if b & 1 == 1 { 244 | // search for matching bits 245 | depth^ += 1 246 | for j in 0..>= 1 259 | } 260 | } 261 | 262 | // print logical size of the compressed trie 263 | comp_print_size :: proc() { 264 | size := comp_index 265 | fmt.eprintf("SIZE in %dB %dKB %dMB for compressed\n", size, size / 1024, size / 1024 / 1024) 266 | } 267 | 268 | // converts alphabetic byte index into the remapped bitfield space 269 | comp_bits_index_to_counted_one :: proc(field: u32, idx: u32) -> (res: u32, ok: bool) { 270 | b := field 271 | 272 | for i := u32(0); b > 0; i += 1 { 273 | if b & 1 == 1 { 274 | if idx == i { 275 | ok = true 276 | return 277 | } 278 | 279 | res += 1 280 | } 281 | 282 | b >>= 1 283 | } 284 | 285 | return 286 | } 287 | 288 | // DEBUG prints used characters in the bitset 289 | comp_print_characters :: proc(field: u32) { 290 | if field == 0 { 291 | fmt.eprintln("EMPTY BITS") 292 | return 293 | } 294 | 295 | b := field 296 | for i := u32(0); b > 0; i += 1 { 297 | if b & 1 == 1 { 298 | fmt.eprint(rune(i + 'a'), ' ') 299 | } 300 | 301 | b >>= 1 302 | } 303 | 304 | fmt.eprintln() 305 | } 306 | 307 | // TODO escape on utf8 byte? 308 | // lowercase valid alpha 309 | ascii_check_lower :: proc(b: u8) -> u8 { 310 | if 'A' <= b && b <= 'Z' { 311 | return b + 32 312 | } else { 313 | return b 314 | } 315 | } 316 | 317 | // wether the byte is a letter 318 | ascii_is_letter :: #force_inline proc(b: u8) -> bool { 319 | return 'a' <= b && b <= 'z' 320 | } 321 | 322 | // less pointer offseting sent by Jeroen :) 323 | comp_search :: proc(key: string) -> bool { 324 | alphabet_bits := cast(^u32) &comp[0] 325 | 326 | for i := 0; i < len(key); i += 1 { 327 | b := ascii_check_lower(key[i]) 328 | 329 | if ascii_is_letter(b) { 330 | letter := b - 'a' 331 | 332 | // check for shortcut first! 333 | if comp_bits_is_shortcut(alphabet_bits^) { 334 | // fmt.eprintln("check bits", key) 335 | rest := comp_bits_shortcut_text(alphabet_bits) 336 | assert(len(rest) != 0) 337 | rest_index: int 338 | 339 | // match the rest letters 340 | for j in i.. bool { 375 | return (field & SHORTCUT_VALUE) == SHORTCUT_VALUE 376 | } 377 | 378 | // extract shortcut length 379 | comp_bits_shortcut_length :: #force_inline proc(field: u32) -> u32 { 380 | return bits.bitfield_extract_u32(field, 0, 26) 381 | } 382 | 383 | // get the shortcut text as a string from the ptr 384 | comp_bits_shortcut_text :: proc(field: ^u32) -> string { 385 | length := comp_bits_shortcut_length(field^) 386 | 387 | return strings.string_from_ptr( 388 | cast(^u8) mem.ptr_offset(field, 1), 389 | int(length), 390 | ) 391 | } 392 | 393 | comp_write_to_file :: proc(path: string) -> bool { 394 | return os.write_entire_file(path, comp[:comp_index]) 395 | } 396 | 397 | comp_read_from_file :: proc(path: string) { 398 | content, ok := os.read_entire_file(path) 399 | 400 | if ok { 401 | comp = content 402 | comp_index = len(content) 403 | } 404 | } 405 | 406 | comp_read_from_data :: proc(data: []byte) { 407 | comp = data 408 | comp_index = len(data) 409 | } -------------------------------------------------------------------------------- /src/cam.odin: -------------------------------------------------------------------------------- 1 | package src 2 | 3 | import "core:fmt" 4 | import "core:log" 5 | import "core:math" 6 | import "core:strings" 7 | import "core:math/rand" 8 | import "core:math/ease" 9 | import "base:intrinsics" 10 | 11 | CAM_CENTER :: 100 12 | 13 | Pan_Camera_Animation :: struct { 14 | animating: bool, 15 | direction: int, 16 | goal: int, 17 | } 18 | 19 | Cam_Check_Type :: enum { 20 | Bounds, 21 | Centered, 22 | } 23 | 24 | Pan_Camera :: struct { 25 | start_x, start_y: int, // start of drag 26 | offset_x, offset_y: f32, 27 | margin_x, margin_y: int, 28 | 29 | freehand: bool, // disables auto centering while panning 30 | 31 | // check state 32 | check_next_index: int, 33 | check_next_type: Cam_Check_Type, 34 | 35 | ay: Pan_Camera_Animation, 36 | ax: Pan_Camera_Animation, 37 | 38 | // screenshake, running on power mode 39 | screenshake_counter: f32, 40 | screenshake_x, screenshake_y: f32, 41 | } 42 | 43 | cam_check :: proc(cam: ^Pan_Camera, type: Cam_Check_Type, frames := int(1)) { 44 | cam.check_next_index = frames 45 | cam.check_next_type = type 46 | } 47 | 48 | // update lifetime 49 | cam_update_screenshake :: proc(using cam: ^Pan_Camera, update: bool) { 50 | if !pm_screenshake_use() || !pm_show() { 51 | screenshake_x = 0 52 | screenshake_y = 0 53 | screenshake_counter = 0 54 | return 55 | } 56 | 57 | if update { 58 | // unit range nums 59 | x := (rand.float32() * 2 - 1) 60 | y := (rand.float32() * 2 - 1) 61 | shake := pm_screenshake_amount() // skake amount in px 62 | lifetime_opt := pm_screenshake_lifetime() 63 | screenshake_x = x * max(shake - screenshake_counter * shake * 2 * lifetime_opt, 0) 64 | screenshake_y = y * max(shake - screenshake_counter * shake * 2 * lifetime_opt, 0) 65 | screenshake_counter += gs.dt 66 | } else { 67 | screenshake_x = 0 68 | screenshake_y = 0 69 | screenshake_counter = 0 70 | } 71 | } 72 | 73 | cam_update_check :: proc(using cam: ^Pan_Camera) { 74 | if cam.check_next_index >= 0 { 75 | cam.check_next_index -= 1 76 | 77 | if cam.check_next_index == 0 { 78 | mode_panel_cam_freehand_off(cam) 79 | 80 | switch cam.check_next_type { 81 | case .Bounds: { 82 | mode_panel_cam_bounds_check_x(cam, app.caret.rect.l, app.caret.rect.r, false, true) 83 | mode_panel_cam_bounds_check_y(cam, app.caret.rect.t, app.caret.rect.b, true) 84 | } 85 | 86 | case .Centered: { 87 | cam_center_by_height_state(cam, app.mmpp.bounds, app.caret.rect.t) 88 | // fmt.eprintln(app.mmpp.bounds, app.caret.rect) 89 | } 90 | } 91 | } 92 | } 93 | } 94 | 95 | // return offsets + screenshake 96 | cam_offsets :: proc(cam: ^Pan_Camera) -> (f32, f32) { 97 | return cam.offset_x + cam.screenshake_x, cam.offset_y + cam.screenshake_y 98 | } 99 | 100 | cam_init :: proc(cam: ^Pan_Camera, margin_x, margin_y: int) { 101 | cam.offset_x = f32(margin_x) 102 | cam.margin_x = margin_x 103 | cam.offset_y = f32(margin_y) 104 | cam.margin_y = margin_y 105 | } 106 | 107 | cam_set_y :: proc(cam: ^Pan_Camera, to: int) { 108 | cam.offset_y = f32(to) 109 | scrollbar_position_set(app.custom_split.vscrollbar, f32(-cam.offset_y)) 110 | } 111 | 112 | cam_set_x :: proc(cam: ^Pan_Camera, to: int) { 113 | cam.offset_x = f32(to) 114 | scrollbar_position_set(app.custom_split.hscrollbar, f32(-cam.offset_x)) 115 | } 116 | 117 | cam_inc_y :: proc(cam: ^Pan_Camera, off: f32) { 118 | cam.offset_y += off 119 | scrollbar_position_set(app.custom_split.vscrollbar, f32(-cam.offset_y)) 120 | } 121 | 122 | cam_inc_x :: proc(cam: ^Pan_Camera, off: f32) { 123 | cam.offset_x += off 124 | scrollbar_position_set(app.custom_split.hscrollbar, f32(-cam.offset_x)) 125 | } 126 | 127 | // return the cam per mode 128 | mode_panel_cam :: #force_inline proc() -> ^Pan_Camera #no_bounds_check { 129 | return &app.mmpp.cam[app.mmpp.mode] 130 | } 131 | 132 | cam_animate :: proc(cam: ^Pan_Camera, x: bool) -> bool { 133 | a := x ? &cam.ax : &cam.ay 134 | off := x ? &cam.offset_x : &cam.offset_y 135 | lerp := x ? &app.caret.lerp_speed_x : &app.caret.lerp_speed_y 136 | using a 137 | 138 | if cam.freehand || !animating { 139 | return false 140 | } 141 | 142 | real_goal := direction == CAM_CENTER ? f32(goal) : off^ + f32(direction * goal) 143 | // fmt.eprintln("real_goal", x ? "x" : "y", direction == 0, real_goal, off^, direction) 144 | res := animate_to_state( 145 | &animating, 146 | off, 147 | real_goal, 148 | 1 + lerp^, 149 | 1, 150 | ) 151 | 152 | scrollbar_position_set(app.custom_split.vscrollbar, f32(-cam.offset_y)) 153 | scrollbar_position_set(app.custom_split.hscrollbar, f32(-cam.offset_x)) 154 | 155 | lerp^ = res ? lerp^ + 0.5 : 1 156 | 157 | // if !res { 158 | // fmt.eprintln("done", x ? "x" : "y", off^, goal) 159 | // } 160 | 161 | return res 162 | } 163 | 164 | // returns the wanted goal + direction if y is out of bounds of focus rect 165 | cam_bounds_check_y :: proc( 166 | cam: ^Pan_Camera, 167 | focus: RectI, 168 | to_top: int, 169 | to_bottom: int, 170 | ) -> (goal: int, direction: int) { 171 | if cam.margin_y * 2 > rect_height(focus) { 172 | return 173 | } 174 | 175 | if to_top < focus.t + cam.margin_y { 176 | goal = focus.t - to_top + cam.margin_y 177 | 178 | if goal != 0 { 179 | direction = 1 180 | return 181 | } 182 | } 183 | 184 | if to_bottom > focus.b - cam.margin_y { 185 | goal = to_bottom - focus.b + cam.margin_y 186 | 187 | if goal != 0 { 188 | direction = -1 189 | } 190 | } 191 | 192 | return 193 | } 194 | 195 | cam_bounds_check_x :: proc( 196 | cam: ^Pan_Camera, 197 | focus: RectI, 198 | to_left: int, 199 | to_right: int, 200 | ) -> (goal: int, direction: int) { 201 | if cam.margin_x * 2 >= rect_width(focus) { 202 | return 203 | } 204 | 205 | if to_left < focus.l + cam.margin_x { 206 | goal = focus.l - to_left + cam.margin_x 207 | 208 | if goal != 0 { 209 | direction = 1 210 | return 211 | } 212 | } 213 | 214 | if to_right >= focus.r - cam.margin_x { 215 | goal = to_right - focus.r + cam.margin_x 216 | 217 | if goal != 0 { 218 | direction = -1 219 | } 220 | } 221 | 222 | return 223 | } 224 | 225 | // check animation on caret bounds 226 | mode_panel_cam_bounds_check_y :: proc( 227 | cam: ^Pan_Camera, 228 | to_top: int, 229 | to_bottom: int, 230 | use_task: bool, // use task boundary 231 | ) { 232 | if cam.freehand { 233 | return 234 | } 235 | 236 | to_top := to_top 237 | to_bottom := to_bottom 238 | 239 | goal: int 240 | direction: int 241 | if app.task_head != -1 && use_task { 242 | task := app_task_head() 243 | to_top = task.element.bounds.t 244 | to_bottom = task.element.bounds.b 245 | } 246 | 247 | goal, direction = cam_bounds_check_y(cam, app.mmpp.bounds, to_top, to_bottom) 248 | 249 | if direction != 0 { 250 | element_animation_start(app.mmpp) 251 | cam.ay.animating = true 252 | cam.ay.direction = direction 253 | cam.ay.goal = goal 254 | } 255 | } 256 | 257 | // check animation on caret bounds 258 | mode_panel_cam_bounds_check_x :: proc( 259 | cam: ^Pan_Camera, 260 | to_left: int, 261 | to_right: int, 262 | check_stop: bool, 263 | use_kanban: bool, 264 | ) { 265 | if cam.freehand { 266 | return 267 | } 268 | 269 | goal: int 270 | direction: int 271 | to_left := to_left 272 | to_right := to_right 273 | 274 | switch app.mmpp.mode { 275 | case .List: { 276 | if app.task_head != -1 { 277 | t := app_task_head() 278 | 279 | // check if one liner 280 | if len(t.box.wrapped_lines) == 1 { 281 | fcs_element(&t.element) 282 | fcs_ahv(.LEFT, .TOP) 283 | text_width := string_width(ss_string(&t.box.ss)) 284 | 285 | // if rect_width(mode_panel.bounds) - cam.margin_x * 2 286 | 287 | to_left = t.element.bounds.l 288 | to_right = t.element.bounds.l + text_width 289 | // rect := rect_wh(t.bounds.l, t.bounds.t, text_width, text_width + LINE_WIDTH, scaled_size) 290 | } 291 | } 292 | 293 | goal, direction = cam_bounds_check_x(cam, app.mmpp.bounds, to_left, to_right) 294 | } 295 | 296 | case .Kanban: { 297 | // find indent 0 task and get its rect 298 | t: ^Task 299 | if app.task_head != -1 && use_kanban { 300 | index := app.task_head 301 | 302 | for t == nil || (t.indentation != 0 && index >= 0) { 303 | t = app_task_filter(index) 304 | index -= 1 305 | } 306 | } 307 | 308 | if t != nil && t.kanban_rect != {} && use_kanban { 309 | // check if larger than kanban size 310 | if rect_width(t.kanban_rect) < rect_width(app.mmpp.bounds) - cam.margin_x * 2 { 311 | to_left = t.kanban_rect.l 312 | to_right = t.kanban_rect.r 313 | } 314 | } 315 | 316 | goal, direction = cam_bounds_check_x(cam, app.mmpp.bounds, to_left, to_right) 317 | } 318 | } 319 | 320 | // fmt.eprintln(goal, direction) 321 | 322 | if check_stop { 323 | if direction == 0 { 324 | cam.ax.animating = false 325 | // fmt.eprintln("FORCE STOP") 326 | } else { 327 | // fmt.eprintln("HAD DIRECTION X", goal, direction) 328 | } 329 | } else if direction != 0 { 330 | element_animation_start(app.mmpp) 331 | cam.ax.animating = true 332 | cam.ax.direction = direction 333 | cam.ax.goal = goal 334 | } 335 | } 336 | 337 | cam_center_by_height_state :: proc( 338 | cam: ^Pan_Camera, 339 | focus: RectI, 340 | y: int, 341 | max_height: int = -1, 342 | ) { 343 | if cam.freehand { 344 | return 345 | } 346 | 347 | height := rect_height(focus) 348 | offset_goal: int 349 | 350 | switch app.mmpp.mode { 351 | case .List: { 352 | // center by view height max height is lower than view height 353 | if max_height != -1 && max_height < height { 354 | offset_goal = (height / 2 - max_height / 2) 355 | } else { 356 | top := y - int(cam.offset_y) 357 | offset_goal = (height / 2 - top) 358 | } 359 | } 360 | 361 | case .Kanban: { 362 | // center by view height max height is lower than view height 363 | if max_height != -1 && max_height < height { 364 | offset_goal = (height / 2 - max_height / 2) 365 | } else { 366 | top := y - int(cam.offset_y) 367 | // NOTE clamps to the top of the kanban region youd like to see at max 368 | offset_goal = min(height / 2 - top, cam.margin_y) 369 | // fmt.eprintln(top, height, offset_goal, cam.offset_y) 370 | } 371 | } 372 | } 373 | 374 | element_animation_start(app.mmpp) 375 | cam.ay.animating = true 376 | cam.ay.direction = CAM_CENTER 377 | cam.ay.goal = offset_goal 378 | } -------------------------------------------------------------------------------- /src/changelog.odin: -------------------------------------------------------------------------------- 1 | package src 2 | 3 | import "core:reflect" 4 | import "core:fmt" 5 | import "core:mem" 6 | import "core:math" 7 | import "core:strings" 8 | import "core:slice" 9 | 10 | //changelog generator output window 11 | // descritiption what this window does 12 | // button to update to task tree content (in case the window is kept alive) 13 | // display supposed generated output 14 | // checkboxes to decide where to output (Terminal, File, Clipboard) 15 | // checkbox to remove content from task tree or not 16 | // allow changing numbering scheme or inserting stars at the start of each textual line 17 | 18 | //LATER 19 | // skip folded 20 | 21 | Changelog_Task :: struct { 22 | task: ^Task, 23 | remove: bool, 24 | } 25 | 26 | Changelog_Indexing :: enum { 27 | None, 28 | Numbers, 29 | Stars_Non_Zero, 30 | Stars_All, 31 | } 32 | 33 | Changelog :: struct { 34 | window: ^Window, 35 | panel: ^Panel, 36 | td: ^Changelog_Text_Display, 37 | 38 | checkbox_skip_folded: ^Checkbox, 39 | checkbox_include_canceled: ^Checkbox, 40 | checkbox_pop_tasks: ^Checkbox, 41 | 42 | check_next: bool, 43 | 44 | qlist: [dynamic]Changelog_Task, 45 | qparents: map[^Task]u8, 46 | indexing: Changelog_Indexing, 47 | } 48 | changelog: Changelog 49 | 50 | changelog_window_message :: proc(element: ^Element, msg: Message, di: int, dp: rawptr) -> int { 51 | window := cast(^Window) element 52 | 53 | #partial switch msg { 54 | case .Destroy: { 55 | delete(changelog.qparents) 56 | delete(changelog.qlist) 57 | changelog = {} 58 | } 59 | } 60 | 61 | return 0 62 | } 63 | 64 | Changelog_Text_Display :: struct { 65 | using element: Element, 66 | builder: strings.Builder, 67 | vscrollbar: ^Scrollbar, 68 | hscrollbar: ^Scrollbar, 69 | } 70 | 71 | changelog_text_display_message :: proc(element: ^Element, msg: Message, di: int, dp: rawptr) -> int { 72 | td := cast(^Changelog_Text_Display) element 73 | margin_scaled := int(TEXT_PADDING * SCALE) 74 | tab_scaled := int(50 * SCALE) 75 | 76 | #partial switch msg { 77 | case .Layout: { 78 | bounds := element.bounds 79 | // measure max string width and lines 80 | iter := strings.to_string(td.builder) 81 | width: int 82 | line_count := 1 83 | scaled_size := fcs_element(element) 84 | for line in strings.split_lines_iterator(&iter) { 85 | tabs := tabs_count(line) 86 | width = max(width, string_width(line) + tabs * tab_scaled) 87 | line_count += 1 88 | } 89 | 90 | scrollbar_layout_help( 91 | td.hscrollbar, 92 | td.vscrollbar, 93 | element.bounds, 94 | width, 95 | line_count * scaled_size, 96 | ) 97 | } 98 | 99 | case .Paint_Recursive: { 100 | target := element.window.target 101 | render_rect(target, element.bounds, theme.background[1], ROUNDNESS) 102 | bounds := rect_margin(element.bounds, margin_scaled) 103 | 104 | text := strings.to_string(td.builder) 105 | scaled_size := fcs_element(element) 106 | 107 | if len(text) == 0 { 108 | fcs_color(theme.text_blank) 109 | fcs_ahv() 110 | render_string_rect(target, bounds, "no changes found") 111 | } else { 112 | // render each line, increasingly 113 | fcs_color(theme.text_default) 114 | fcs_ahv(.LEFT, .TOP) 115 | x := bounds.l - int(td.hscrollbar.position) 116 | y := bounds.t - int(td.vscrollbar.position) 117 | 118 | iter := text 119 | for line in strings.split_lines_iterator(&iter) { 120 | tabs := tabs_count(line) 121 | 122 | render_string( 123 | target, 124 | x + tabs * tab_scaled, y, 125 | line[tabs:], 126 | ) 127 | 128 | y += scaled_size 129 | } 130 | } 131 | } 132 | 133 | case .Mouse_Scroll_X: { 134 | if scrollbar_valid(td.hscrollbar) { 135 | return element_message(td.hscrollbar, msg, di, dp) 136 | } 137 | } 138 | 139 | case .Mouse_Scroll_Y: { 140 | if scrollbar_valid(td.vscrollbar) { 141 | return element_message(td.vscrollbar, msg, di, dp) 142 | } 143 | } 144 | 145 | case .Destroy: { 146 | delete(td.builder.buf) 147 | } 148 | } 149 | 150 | return 0 151 | } 152 | 153 | changelog_text_display_init :: proc( 154 | parent: ^Element, 155 | flags: Element_Flags, 156 | ) -> (res: ^Changelog_Text_Display) { 157 | res = element_init(Changelog_Text_Display, parent, flags, changelog_text_display_message, context.allocator) 158 | res.builder = strings.builder_make(0, mem.Kilobyte) 159 | res.hscrollbar = scrollbar_init(res, {}, true, context.allocator) 160 | res.vscrollbar = scrollbar_init(res, {}, false, context.allocator) 161 | return 162 | } 163 | 164 | changelog_text_display_set :: proc(td: ^Changelog_Text_Display) { 165 | b := &td.builder 166 | strings.builder_reset(b) 167 | 168 | write :: proc(b: ^strings.Builder, task: ^Task, indentation: int, count: int) { 169 | for i in 0.. string { 231 | return strings.to_string(changelog.td.builder) 232 | } 233 | 234 | changelog_update_safe :: proc() { 235 | if changelog.window == nil { 236 | return 237 | } 238 | 239 | changelog.check_next = true 240 | changelog.window.update_next = true 241 | } 242 | 243 | changelog_update_invoke :: proc(box: ^Checkbox) { 244 | changelog.check_next = true 245 | changelog.window.update_next = true 246 | } 247 | 248 | changelog_spawn :: proc(du: u32 = COMBO_EMPTY) { 249 | if changelog.window != nil { 250 | window_raise(changelog.window) 251 | return 252 | } 253 | 254 | changelog.qlist = make([dynamic]Changelog_Task, 0, 64) 255 | changelog.qparents = make(map[^Task]u8, 32) 256 | 257 | changelog.window = window_init(nil, {}, "Changelog Genrator", 700, 700, 8, 8) 258 | changelog.window.element.message_user = changelog_window_message 259 | changelog.window.update_before = proc(window: ^Window) { 260 | // only check once when window is updating 261 | if changelog.check_next { 262 | task_check_parent_states(nil) 263 | changelog_text_display_set(changelog.td) 264 | changelog.check_next = false 265 | } 266 | } 267 | changelog.window.on_focus_gained = proc(window: ^Window) { 268 | if changelog.td != nil { 269 | changelog_text_display_set(changelog.td) 270 | } 271 | window.update_next = true 272 | } 273 | 274 | changelog.panel = panel_init( 275 | &changelog.window.element, 276 | { .HF, .VF, .Panel_Default_Background }, 277 | 5, 278 | 5, 279 | ) 280 | changelog.panel.background_index = 0 281 | p := changelog.panel 282 | 283 | { 284 | p1 := panel_init(p, { .HF, .Panel_Default_Background }, 5, 5) 285 | p1.background_index = 1 286 | p1.rounded = true 287 | label_init(p1, { .HF, .Label_Center }, "Generates a Changelog from your Done/Canceled Tasks") 288 | 289 | { 290 | p2 := panel_init(p1, { .HF, .Panel_Horizontal }, 5, 5) 291 | label_init(p2, { .Label_Center }, "Generate to") 292 | p3 := panel_init(p2, { .HF, .Panel_Horizontal, .Panel_Default_Background }) 293 | p3.background_index = 2 294 | p3.rounded = true 295 | button_init(p3, { .HF }, "Clipboard").invoke = proc(button: ^Button, data: rawptr) { 296 | text := changelog_result() 297 | clipboard_set_with_builder(text) 298 | changelog_result_pop_tasks() 299 | } 300 | button_init(p3, { .HF }, "Terminal").invoke = proc(button: ^Button, data: rawptr) { 301 | fmt.println(changelog_result()) 302 | changelog_result_pop_tasks() 303 | } 304 | button_init(p3, { .HF }, "File").invoke = proc(button: ^Button, data: rawptr) { 305 | path := bpath_temp("changelog.txt") 306 | gs_write_safely(path, changelog.td.builder.buf[:]) 307 | changelog_result_pop_tasks() 308 | } 309 | } 310 | 311 | { 312 | toggle := toggle_panel_init(p1, { .HF }, { .Panel_Default_Background }, "Options", true) 313 | p2 := toggle.panel 314 | // p2 := panel_init(p1, { .HF, .Panel_Default_Background, .Panel_Horizontal }) 315 | 316 | p2.background_index = 2 317 | p2.margin = 5 318 | p2.rounded = true 319 | p2.gap = 5 320 | changelog.checkbox_skip_folded = checkbox_init(p2, { .HF }, "Skip Folded", true) 321 | changelog.checkbox_skip_folded.invoke = changelog_update_invoke 322 | changelog.checkbox_include_canceled = checkbox_init(p2, { .HF }, "Include Canceled Tasks", true) 323 | changelog.checkbox_include_canceled.invoke = changelog_update_invoke 324 | changelog.checkbox_pop_tasks = checkbox_init(p2, { .HF }, "Pop Tasks", true) 325 | 326 | indexing_names := reflect.enum_field_names(Changelog_Indexing) 327 | t := toggle_selector_init(p2, { .HF }, 0, len(Changelog_Indexing), indexing_names) 328 | t.changed = proc(toggle: ^Toggle_Selector) { 329 | changelog.indexing = Changelog_Indexing(toggle.value) 330 | changelog_text_display_set(changelog.td) 331 | } 332 | } 333 | } 334 | 335 | changelog.td = changelog_text_display_init(p, { .HF, .VF }) 336 | changelog_text_display_set(changelog.td) 337 | } 338 | 339 | changelog_find :: proc() { 340 | clear(&changelog.qlist) 341 | clear(&changelog.qparents) 342 | 343 | include := changelog.checkbox_include_canceled.state 344 | skip := changelog.checkbox_skip_folded.state 345 | 346 | for filter_index in 0.. bool { 384 | return a.task.filter_index < b.task.filter_index 385 | } 386 | slice.sort_by(changelog.qlist[:], sort_by) 387 | } -------------------------------------------------------------------------------- /src/color.odin: -------------------------------------------------------------------------------- 1 | package src 2 | 3 | import "core:math" 4 | import "core:math/rand" 5 | 6 | Color :: [4]u8 7 | RED :: Color { 255, 0, 0, 255 } 8 | GREEN :: Color { 0, 255, 0, 255 } 9 | BLUE :: Color { 0, 0, 255, 255 } 10 | BLACK :: Color { 0, 0, 0, 255 } 11 | WHITE :: Color { 255, 255, 255, 255 } 12 | TRANSPARENT :: Color { } 13 | GOLDEN_RATIO :: 0.618033988749895 14 | 15 | color_rgb_rand :: proc() -> Color { 16 | return { 17 | u8(rand.float32() * 255), 18 | u8(rand.float32() * 255), 19 | u8(rand.float32() * 255), 20 | 255, 21 | } 22 | } 23 | 24 | // color_hsl_rand :: proc(gen: ^rand.Rand = nil, s := f32(1), v := f32(1)) -> Color { 25 | // hue := rand.float32() 26 | // return color_hsv_to_rgb(hue, s, v) 27 | // } 28 | 29 | // color_hsl_golden_rand :: proc(gen: ^rand.Rand = nil, s := f32(1), v := f32(1)) -> Color { 30 | // hue := math.mod(rand.float32() + GOLDEN_RATIO, 1) 31 | // return color_hsv_to_rgb(hue, s, v) 32 | // } 33 | 34 | color_hsluv_rand :: proc(s := f64(1), l := f64(0.5)) -> Color { 35 | hue := rand.float64() * 360 36 | r, g, b := hsluv_to_rgb(hue, s * 100, l * 100) 37 | return { u8(r * 255), u8(g * 255), u8(b * 255), 255 } 38 | } 39 | 40 | color_hsluv_to_rgb :: proc(hue, s, l: f64) -> Color { 41 | hue := hue * 360 42 | r, g, b := hsluv_to_rgb(hue, s * 100, l * 100) 43 | return { u8(r * 255), u8(g * 255), u8(b * 255), 255 } 44 | } 45 | 46 | color_hsluv_golden_rand :: proc(s := f64(1), l := f64(0.5)) -> Color { 47 | hue := math.mod(rand.float64() + GOLDEN_RATIO, 1) * 360 48 | r, g, b := hsluv_to_rgb(hue, s * 100, l * 100) 49 | return { u8(r * 255), u8(g * 255), u8(b * 255), 255 } 50 | } 51 | 52 | color_alpha :: proc(color: Color, alpha: f32) -> (res: Color) { 53 | res = color 54 | res.a = u8(alpha * 255) 55 | return 56 | } 57 | 58 | color_blend_amount :: proc(a, b: Color, t: f32) -> (result: Color) { 59 | result.a = a.a 60 | result.r = u8((1.0 - t) * f32(b.r) + t * f32(a.r)) 61 | result.g = u8((1.0 - t) * f32(b.g) + t * f32(a.g)) 62 | result.b = u8((1.0 - t) * f32(b.b) + t * f32(a.b)) 63 | return 64 | } 65 | 66 | color_blend :: proc(c1: Color, c2: Color, amount: f32, use_alpha: bool) -> Color { 67 | r := amount * (f32(c1.r) / 255) + (1 - amount) * (f32(c2.r) / 255) 68 | g := amount * (f32(c1.g) / 255) + (1 - amount) * (f32(c2.g) / 255) 69 | b := amount * (f32(c1.b) / 255) + (1 - amount) * (f32(c2.b) / 255) 70 | a := amount * (f32(c1.a) / 255) + (1 - amount) * (f32(c2.a) / 255) 71 | 72 | return Color { 73 | u8(r * 255), 74 | u8(g * 255), 75 | u8(b * 255), 76 | u8(use_alpha ? u8(a * 255) : 255), 77 | } 78 | } 79 | 80 | color_from_f32 :: #force_inline proc(r, g, b, a: f32) -> Color { 81 | return { 82 | u8(r * 255), 83 | u8(g * 255), 84 | u8(b * 255), 85 | u8(a * 255), 86 | } 87 | } 88 | 89 | color_to_bw :: proc(a: Color) -> Color { 90 | return max(a.r, a.g, a.b) < 125 ? WHITE : BLACK 91 | } 92 | 93 | color_hsv_to_rgb :: proc(h, s, v: f32) -> (res: Color) { 94 | if s == 0 { 95 | return color_from_f32(v, v, v, 1) 96 | } 97 | 98 | i := int(h * 6) 99 | f := (h * 6) - f32(i) 100 | p := v * (1 - s) 101 | q := v * (1 - s * f) 102 | t := v * (1 - s * (1 - f)) 103 | i %= 6 104 | 105 | switch i { 106 | case 0: return color_from_f32(v, t, p, 1) 107 | case 1: return color_from_f32(q, v, p, 1) 108 | case 2: return color_from_f32(p, v, t, 1) 109 | case 3: return color_from_f32(p, q, v, 1) 110 | case 4: return color_from_f32(t, p, v, 1) 111 | case 5: return color_from_f32(v, p, q, 1) 112 | } 113 | 114 | unimplemented("yup") 115 | } 116 | 117 | color_rgb_to_hsv :: proc(col: Color) -> (f32, f32, f32, f32) { 118 | r := f32(col.r) / 255 119 | g := f32(col.g) / 255 120 | b := f32(col.b) / 255 121 | a := f32(col.a) / 255 122 | c_min := min(r, g, b) 123 | c_max := max(r, g, b) 124 | h, s, v: f32 125 | h = 0.0 126 | s = 0.0 127 | // v = (c_min + c_max) * 0.5 128 | v = c_max 129 | 130 | if c_max != c_min { 131 | delta := c_max - c_min 132 | // s = c_max == 0 ? 0 : 1 - (1 * c_min / c_max) 133 | s = c_max == 0 ? 0 : delta / c_max 134 | // s = d / (2.0 - c_max - c_min) if v > 0.5 else d / (c_max + c_min) 135 | switch { 136 | case c_max == r: { 137 | h = (g - b) / delta + (6.0 if g < b else 0.0) 138 | } 139 | 140 | case c_max == g: { 141 | h = (b - r) / delta + 2.0 142 | } 143 | 144 | case c_max == b: { 145 | h = (r - g) / delta + 4.0 146 | } 147 | } 148 | 149 | h *= 1.0 / 6.0 150 | } 151 | 152 | return h, s, v, a 153 | } -------------------------------------------------------------------------------- /src/copy.odin: -------------------------------------------------------------------------------- 1 | package src 2 | 3 | import "core:fmt" 4 | import "core:strings" 5 | import "core:mem" 6 | import "core:time" 7 | 8 | // helper call to build strings linearly 9 | string_list_push_indices :: proc(builder: ^strings.Builder, text: string) -> (start, end: int) { 10 | start = len(builder.buf) 11 | strings.write_string(builder, text) 12 | end = len(builder.buf) 13 | return 14 | } 15 | 16 | string_list_push_ptr :: proc(builder: ^strings.Builder, text: string) -> (res: string) { 17 | old := len(builder.buf) 18 | strings.write_string(builder, text) 19 | res = string(builder.buf[old:]) 20 | return 21 | } 22 | 23 | inv_lerp :: proc(a, b, v: f32) -> f32 { 24 | return (v - a) / (b - a) 25 | } 26 | 27 | // Bare data to copy task from 28 | Copy_Task :: struct #packed { 29 | text_start: u32, // offset into text_list 30 | text_end: u32, // offset into text_list 31 | indentation: u8, 32 | state: Task_State, 33 | tags: u8, 34 | bookmarked: bool, 35 | timestamp: time.Time, 36 | highlight: bool, 37 | separator: bool, 38 | stored_image: ^Stored_Image, 39 | link_start: u32, 40 | link_end: u32, 41 | fold_count: u32, 42 | fold_parent: int, 43 | } 44 | 45 | // whats necessary to produce valid copies that can persist or are temporary 46 | Copy_State :: struct { 47 | stored_text: strings.Builder, 48 | stored_links: strings.Builder, 49 | stored_tasks: [dynamic]Copy_Task, 50 | parent_count: int, 51 | } 52 | 53 | // allow to create temporary copy states 54 | copy_state_init :: proc( 55 | text_cap: int, 56 | task_cap: int, 57 | allocator := context.allocator, 58 | ) -> (res: Copy_State) { 59 | res.stored_text = strings.builder_make(0, text_cap, allocator) 60 | res.stored_links = strings.builder_make(0, mem.Kilobyte, allocator) 61 | res.stored_tasks = make([dynamic]Copy_Task, 0, task_cap, allocator) 62 | // res.stored_folded = make([dynamic]int, 0, 128, allocator) 63 | return 64 | } 65 | 66 | copy_state_destroy :: proc(state: Copy_State) { 67 | delete(state.stored_text.buf) 68 | delete(state.stored_links.buf) 69 | delete(state.stored_tasks) 70 | // delete(state.stored_folded) 71 | } 72 | 73 | // reset copy data 74 | copy_state_reset :: proc(state: ^Copy_State) { 75 | strings.builder_reset(&state.stored_text) 76 | clear(&state.stored_tasks) 77 | state.parent_count = 0 78 | } 79 | 80 | // just push text, e.g. from archive 81 | copy_state_push_empty :: proc(state: ^Copy_State, text: string) { 82 | start, end := string_list_push_indices(&state.stored_text, text) 83 | 84 | // copy crucial info of task 85 | append(&state.stored_tasks, Copy_Task { 86 | text_start = u32(start), 87 | text_end = u32(end), 88 | fold_parent = -1, 89 | }) 90 | } 91 | 92 | // push a task to copy list 93 | copy_state_push_task :: proc(state: ^Copy_State, task: ^Task, fold_parent: int) { 94 | // NOTE works with utf8 :) copies task text 95 | text_start, text_end := string_list_push_indices(&state.stored_text, ss_string(&task.box.ss)) 96 | link_start, link_end: int 97 | 98 | if task_link_is_valid(task) { 99 | link_start, link_end = string_list_push_indices( 100 | &state.stored_links, 101 | strings.to_string(task.button_link.builder), 102 | ) 103 | } 104 | 105 | // store that the next tasks are children 106 | fold_count: int 107 | if task.filter_folded { 108 | fold_count = len(task.filter_children) 109 | } 110 | 111 | // copy crucial info of task 112 | append(&state.stored_tasks, Copy_Task { 113 | u32(text_start), 114 | u32(text_end), 115 | u8(task.indentation), 116 | task.state, 117 | task.tags, 118 | task_bookmark_is_valid(task), 119 | task_time_date_is_valid(task) ? task.time_date.stamp : {}, 120 | task.highlight, 121 | task_separator_is_valid(task), 122 | task.image_display == nil ? nil : task.image_display.img, 123 | u32(link_start), 124 | u32(link_end), 125 | u32(fold_count), 126 | fold_parent, 127 | }) 128 | 129 | // have to copy folded content 130 | if task.filter_folded { 131 | parent := state.parent_count 132 | state.parent_count += 1 133 | 134 | for list_index in task.filter_children { 135 | child := app_task_list(list_index) 136 | fmt.eprintln("SAVE CHILD", task_string(child)) 137 | copy_state_push_task(state, child, parent) 138 | } 139 | } 140 | } 141 | 142 | copy_state_empty :: proc(state: Copy_State) -> bool { 143 | return len(state.stored_tasks) == 0 144 | } 145 | 146 | copy_state_paste_at :: proc( 147 | state: ^Copy_State, 148 | manager: ^Undo_Manager, 149 | real_index: int, 150 | indentation: int, 151 | ) -> (insert_count: int) { 152 | index_at := real_index 153 | 154 | // find lowest indentation 155 | lowest_indentation := max(int) 156 | for t, i in state.stored_tasks { 157 | lowest_indentation = min(lowest_indentation, int(t.indentation)) 158 | } 159 | 160 | // temp data 161 | parents := make([dynamic]^Task, 0, 32, context.temp_allocator) 162 | 163 | // copy into index range 164 | for t, i in state.stored_tasks { 165 | text := state.stored_text.buf[t.text_start:t.text_end] 166 | relative_indentation := indentation + int(t.indentation) - lowest_indentation 167 | 168 | task := task_init(relative_indentation, string(text), true) 169 | task.state = t.state 170 | task_set_bookmark(task, t.bookmarked) 171 | task.tags = t.tags 172 | task.highlight = t.highlight 173 | 174 | if t.separator { 175 | task_set_separator(task, true) 176 | } 177 | 178 | if t.timestamp != {} { 179 | task_set_time_date(task) 180 | task.time_date.stamp = t.timestamp 181 | } 182 | 183 | if t.stored_image != nil { 184 | task_set_img(task, t.stored_image) 185 | } 186 | 187 | if t.link_end != 0 { 188 | link := state.stored_links.buf[t.link_start:t.link_end] 189 | task_set_link(task, string(link)) 190 | } 191 | 192 | if t.fold_count != 0 { 193 | append(&parents, task) 194 | task.filter_folded = true 195 | reserve(&task.filter_children, int(t.fold_count)) 196 | } 197 | 198 | if t.fold_parent == -1 { 199 | task_insert_at(manager, index_at + insert_count, task) 200 | insert_count += 1 201 | } else { 202 | // get the parent at the expected relative index 203 | parent := parents[t.fold_parent] 204 | // dont insert but append to the parent 205 | append(&parent.filter_children, task.list_index) 206 | } 207 | } 208 | 209 | return 210 | } 211 | 212 | // copy selected task region 213 | copy_state_copy_selection :: proc(state: ^Copy_State, low, high: int) -> bool { 214 | if low != -1 && high != -1 { 215 | copy_state_reset(state) 216 | 217 | // copy each line 218 | for i in low.. bool { 34 | b := rune(b) 35 | type := utf8d[b] 36 | codep^ = (state^ != UTF8_ACCEPT) ? ((b & 0x3f) | (codep^ << 6)) : ((0xff >> type) & (b)) 37 | state^ = rune(utf8d[256 + state^ * 16 + rune(type)]) 38 | return state^ == UTF8_ACCEPT 39 | } 40 | 41 | // get codepoint count back 42 | count :: proc(text: string) -> (count: int) { 43 | codepoint, state: rune 44 | 45 | for i in 0.. ( 65 | codepoint: rune, 66 | codepoint_index: int, 67 | ok: bool, 68 | ) { 69 | byte_offset_old = byte_offset 70 | 71 | // advance till the next codepoint is done 72 | for byte_offset < len(text) { 73 | byte_offset += 1 74 | 75 | if decode(&state, &codepoint, text[byte_offset - 1]) { 76 | codepoint_index = codepoint_count 77 | codepoint_count += 1 78 | ok = true 79 | return 80 | } 81 | } 82 | 83 | return 84 | } 85 | 86 | ds_recount :: proc(using ds: ^Decode_State, text: string) -> int { 87 | ds^ = {} 88 | codepoint: rune 89 | 90 | for byte_offset < len(text) { 91 | if decode(&state, &codepoint, text[byte_offset]) { 92 | codepoint_count += 1 93 | } 94 | 95 | byte_offset += 1 96 | } 97 | 98 | return codepoint_count 99 | } 100 | 101 | ds_string_till_codepoint_index :: proc( 102 | using ds: ^Decode_State, 103 | text: string, 104 | codepoint_index: int, 105 | ) -> (res: string) { 106 | ds^ = {} 107 | codepoint: rune 108 | 109 | for byte_offset < len(text) { 110 | byte_offset += 1 111 | 112 | if decode(&state, &codepoint, text[byte_offset - 1]) { 113 | if codepoint_index == codepoint_count { 114 | res = text[:byte_offset - 1] 115 | return 116 | } 117 | 118 | codepoint_count += 1 119 | } 120 | } 121 | 122 | return text[:] 123 | } 124 | 125 | ds_byte_offset_till_codepoint_index :: proc( 126 | using ds: ^Decode_State, 127 | text: string, 128 | codepoint_index: int, 129 | ) -> (res: int) { 130 | ds^ = {} 131 | codepoint: rune 132 | 133 | for byte_offset < len(text) { 134 | if decode(&state, &codepoint, text[byte_offset]) { 135 | if codepoint_index == codepoint_count { 136 | res = byte_offset 137 | return 138 | } 139 | 140 | codepoint_count += 1 141 | } 142 | 143 | byte_offset += 1 144 | } 145 | 146 | return byte_offset 147 | } 148 | 149 | // decode until the word ended using state 150 | ds_string_selection :: proc( 151 | using ds: ^Decode_State, 152 | text: string, 153 | low, high: int, 154 | ) -> (res: string, ok: bool) { 155 | codepoint: rune 156 | start := -1 157 | end := -1 158 | 159 | for byte_offset < len(text) { 160 | if decode(&state, &codepoint, text[byte_offset]) { 161 | if low == codepoint_count { 162 | start = byte_offset 163 | } 164 | 165 | if high == codepoint_count { 166 | end = byte_offset 167 | } 168 | 169 | // codepoint_index = codepoint_count 170 | codepoint_count += 1 171 | } 172 | 173 | byte_offset += 1 174 | } 175 | 176 | if end == -1 && codepoint_count == high { 177 | end = codepoint_count 178 | } 179 | 180 | if start != -1 && end != -1 { 181 | res = text[start:end] 182 | ok = true 183 | } 184 | 185 | return 186 | } 187 | 188 | byte_indices_to_character_indices :: proc( 189 | text: string, 190 | byte_start: int, 191 | byte_end: int, 192 | head: ^int, 193 | tail: ^int, 194 | ) #no_bounds_check { 195 | codepoint: rune 196 | state: rune 197 | codepoint_offset: int 198 | byte_offset: int 199 | 200 | for byte_offset < len(text) { 201 | if decode(&state, &codepoint, text[byte_offset]) { 202 | if byte_offset == byte_start { 203 | tail^ = codepoint_offset 204 | } 205 | 206 | if byte_offset == byte_end { 207 | head^ = codepoint_offset 208 | } 209 | 210 | codepoint_offset += 1 211 | } 212 | 213 | byte_offset += 1 214 | } 215 | 216 | if byte_offset == byte_start { 217 | tail^ = codepoint_offset 218 | } 219 | 220 | if byte_offset == byte_end { 221 | head^ = codepoint_offset 222 | } 223 | } 224 | 225 | to_lower :: proc(builder: ^strings.Builder, text: string) -> string { 226 | state, codepoint: rune 227 | 228 | for byte_offset in 0.. []rune { 238 | temp := make([dynamic]rune, 0, 256, context.temp_allocator) 239 | state, codepoint: rune 240 | 241 | for byte_offset in 0.. (res: ^Dialog) { 50 | window := parent.window 51 | 52 | // disable other elements 53 | for element in window.element.children { 54 | element.flags += { Element_Flag.Disabled } 55 | } 56 | 57 | res = element_init(Dialog, parent, {}, dialog_message, context.allocator) 58 | res.z_index = 255 59 | res.on_finish = on_finish 60 | res.focus_start = window.focused 61 | res.width = width 62 | 63 | // flush old state 64 | window_flush_mouse_state(window) 65 | window.pressed = nil 66 | window.hovered = nil 67 | window.update_next = true 68 | 69 | // write content 70 | element_animation_start(res) 71 | // window_animate(parent.window, &res.shadow, 1, .Quadratic_Out, time.Millisecond * 200) 72 | undo_manager_init(&res.um, mem.Kilobyte * 2) 73 | 74 | // panel 75 | panel := panel_init(res, { .Tab_Movement_Allowed, .Panel_Default_Background }, 5, 5) 76 | panel.background_index = 2 77 | panel.shadow = true 78 | panel.rounded = true 79 | res.panel = panel 80 | 81 | dialog_build_elements(res, format, ..args) 82 | 83 | return 84 | } 85 | 86 | dialog_spawn :: proc( 87 | window: ^Window, 88 | on_finish: Dialog_Callback, 89 | width: f32, 90 | format: string, 91 | args: ..string, 92 | ) -> (res: ^Dialog) { 93 | dialog_close(window) 94 | res = dialog_init(&window.element, on_finish, width, format, ..args) 95 | window.dialog = res 96 | return 97 | } 98 | 99 | dialog_message :: proc(element: ^Element, msg: Message, di: int, dp: rawptr) -> int { 100 | dialog := cast(^Dialog) element 101 | 102 | #partial switch msg { 103 | case .Paint_Recursive: { 104 | target := element.window.target 105 | render_push_clip(target, element.window.rect) 106 | shadow := theme.shadow 107 | shadow.a = u8(dialog.shadow * 0.5 * 255) 108 | render_rect(target, element.window.rect, shadow) 109 | render_push_clip(target, element.bounds) 110 | } 111 | 112 | case .Layout: { 113 | w := element.window 114 | assert(len(element.children) != 0) 115 | 116 | panel := cast(^Panel) element.children[0] 117 | width := int(dialog.width * SCALE) 118 | height := element_message(panel, .Get_Height, 0) 119 | cx := (element.bounds.l + element.bounds.r) / 2 120 | cy := (element.bounds.t + element.bounds.b) / 2 121 | bounds := RectI { 122 | cx - (width + 1) / 2, 123 | cx + width / 2, 124 | cy - (height + 1) / 2, 125 | cy + height / 2, 126 | } 127 | element_move(panel, bounds) 128 | } 129 | 130 | case .Update: { 131 | panel := element.children[0] 132 | element_message(panel, .Update, di, dp) 133 | } 134 | 135 | case .Destroy: { 136 | undo_manager_destroy(&dialog.um) 137 | } 138 | 139 | case .Key_Combination: { 140 | combo := (cast(^string) dp)^ 141 | 142 | if combo == "escape" && dialog.button_cancel != nil { 143 | dialog_close(dialog.window, .Cancel) 144 | return 1 145 | } 146 | 147 | if combo == "return" && dialog.button_default != nil { 148 | dialog_close(dialog.window, .Default) 149 | return 1 150 | } 151 | 152 | if combo == "escape" { 153 | dialog_close(dialog.window) 154 | return 1 155 | } 156 | } 157 | 158 | case .Animate: { 159 | handled := dialog.shadow <= 1 160 | animate_to( 161 | &dialog.shadow, 162 | 1, 163 | 2, 164 | 0.01, 165 | ) 166 | return int(handled) 167 | } 168 | } 169 | 170 | return 0 171 | } 172 | 173 | dialog_build_elements :: proc(dialog: ^Dialog, format: string, args: ..string) { 174 | arg_index: int 175 | row: ^Panel = nil 176 | ds: cutf8.Decode_State 177 | focus_next: ^Element 178 | 179 | for codepoint, i in cutf8.ds_iter(&ds, format) { 180 | codepoint := codepoint 181 | i := i 182 | 183 | if i == 0 || codepoint == '\n' { 184 | row = panel_init(dialog.panel, { .Panel_Horizontal, .HF }, 0, 5) 185 | } 186 | 187 | if codepoint == ' ' || codepoint == '\n' { 188 | continue 189 | } 190 | 191 | if codepoint == '%' { 192 | // next 193 | codepoint, i, _ = cutf8.ds_iter(&ds, format) 194 | 195 | switch codepoint { 196 | case 'b', 'B', 'C': { 197 | text := args[arg_index] 198 | arg_index += 1 199 | b := button_init(row, { .HF }, text) 200 | b.message_user = dialog_button_message 201 | 202 | // default 203 | if codepoint == 'B' { 204 | dialog.button_default = b 205 | } 206 | 207 | // canceled 208 | if codepoint == 'C' { 209 | dialog.button_cancel = b 210 | } 211 | 212 | // set first focused 213 | if focus_next == nil { 214 | focus_next = b 215 | } 216 | } 217 | 218 | case 'f': { 219 | spacer_init(row, { .HF }, 0, int(10 * SCALE), .Empty) 220 | } 221 | 222 | case 'l': { 223 | spacer_init(row, { .HF }, 0, LINE_WIDTH, .Thin) 224 | } 225 | 226 | // text box 227 | case 't': { 228 | text := args[arg_index] 229 | arg_index += 1 230 | box := text_box_init(row, { .HF }, text) 231 | 232 | // box.um = &dialog.um 233 | element_message(box, .Value_Changed) 234 | dialog.text_box = box 235 | 236 | if focus_next == nil { 237 | focus_next = box 238 | } 239 | } 240 | 241 | // text line 242 | case 's': { 243 | text := args[arg_index] 244 | arg_index += 1 245 | label_init(row, { .HF }, text) 246 | } 247 | 248 | // keyboard input stealer 249 | case 'x': { 250 | text := args[arg_index] 251 | arg_index += 1 252 | 253 | stealer := ke_stealer_init(row, { .HF }, text) 254 | dialog.stealer = stealer 255 | 256 | if focus_next == nil { 257 | focus_next = stealer 258 | } 259 | } 260 | } 261 | } else { 262 | byte_start := ds.byte_offset_old 263 | byte_end := ds.byte_offset 264 | end_early: bool 265 | 266 | // advance till empty 267 | for other_codepoint in cutf8.ds_iter(&ds, format) { 268 | byte_end = ds.byte_offset 269 | 270 | if other_codepoint == '%' || other_codepoint == '\n' { 271 | end_early = true 272 | break 273 | } 274 | } 275 | 276 | text := format[byte_start:byte_end - (end_early ? 1 : 0)] 277 | label := label_init(row, { .Label_Center, .HF }, text) 278 | label.font_options = &app.font_options_bold 279 | 280 | if end_early { 281 | row = panel_init(dialog.panel, { .Panel_Horizontal, .HF }, 0, 5) 282 | } 283 | } 284 | } 285 | 286 | // force dialog to receive key combinations still 287 | if focus_next == nil { 288 | focus_next = dialog 289 | } 290 | 291 | element_focus(dialog.window, focus_next) 292 | } 293 | 294 | // close a window dialog 295 | dialog_close :: proc(window: ^Window, set: Maybe(Dialog_Result) = nil) -> bool { 296 | if window.dialog == nil { 297 | return false 298 | } 299 | 300 | if value, ok := set.?; ok { 301 | window.dialog.result = value 302 | } 303 | 304 | // reset state 305 | window.focused = window.dialog.focus_start 306 | for element in window.element.children { 307 | element.flags -= { Element_Flag.Disabled } 308 | } 309 | 310 | // reset state to default 311 | window_flush_mouse_state(window) 312 | window.pressed = nil 313 | window.hovered = nil 314 | 315 | if window.dialog.on_finish != nil { 316 | result := "" 317 | 318 | if window.dialog.stealer != nil { 319 | result = strings.to_string(window.dialog.stealer.builder) 320 | } else if window.dialog.text_box != nil { 321 | result = ss_string(&window.dialog.text_box.ss) 322 | } 323 | 324 | window.dialog.on_finish(window.dialog, result) 325 | } 326 | 327 | element_destroy(window.dialog) 328 | window_repaint(window) 329 | window.dialog = nil 330 | return true 331 | } 332 | 333 | dialog_button_message :: proc(element: ^Element, msg: Message, di: int, dp: rawptr) -> int { 334 | button := cast(^Button) element 335 | 336 | if msg == .Clicked { 337 | dialog := element.window.dialog 338 | 339 | if button == dialog.button_default { 340 | dialog.result = .Default 341 | } 342 | 343 | if button == dialog.button_cancel { 344 | dialog.result = .Cancel 345 | } 346 | 347 | dialog_close(element.window) 348 | } 349 | 350 | return 0 351 | } 352 | -------------------------------------------------------------------------------- /src/fuzz/fuzz.odin: -------------------------------------------------------------------------------- 1 | package fuzz 2 | 3 | import "core:unicode" 4 | import "core:unicode/utf8" 5 | 6 | SCORE_MATCH :: 16 7 | SCORE_GAP_START :: -3 8 | SCORE_GAP_EXTENSION :: -1 9 | BONUS_BOUNDARY :: SCORE_MATCH / 2 10 | BONUS_NON_WORD :: BONUS_BOUNDARY 11 | BONUS_CAMEL_123 :: BONUS_BOUNDARY + SCORE_GAP_EXTENSION 12 | BONUS_CONSECUTIVE :: -(SCORE_GAP_START + SCORE_GAP_EXTENSION) 13 | BONUS_FIRST_CHAR_MULTIPLIER :: 2 14 | 15 | Fuzz_Result :: struct { 16 | // byte offsets 17 | start, end: int, 18 | score: int, 19 | } 20 | 21 | // continuation byte? 22 | @private 23 | is_cont :: proc(b: byte) -> bool { 24 | return b & 0xc0 == 0x80 25 | } 26 | 27 | @private 28 | utf8_prev :: proc(bytes: string, a, b: int) -> int { 29 | b := b 30 | 31 | for a < b && is_cont(bytes[b - 1]) { 32 | b -= 1 33 | } 34 | 35 | return a < b ? b - 1 : a 36 | } 37 | 38 | @private 39 | utf8_next :: proc(bytes: string, a: int) -> int { 40 | a := a 41 | b := len(bytes) 42 | 43 | for a < b - 1 && is_cont(bytes[a + 1]) { 44 | a += 1 45 | } 46 | 47 | return a < b ? a + 1 : b 48 | } 49 | 50 | utf8_peek :: utf8.decode_rune_in_string 51 | 52 | @private 53 | fuzz_calculate_score :: proc( 54 | trunes, prunes: string, 55 | sidx, eidx: int, 56 | case_sensitive: bool, 57 | ) -> int { 58 | pidx, score, consecutive, first_bonus: int 59 | in_gap: bool 60 | //prev_class := ''; 61 | 62 | if sidx > 0 { 63 | //prev_class = trunes[sidx - 1]; 64 | } 65 | 66 | for idx := sidx; idx < eidx; { 67 | // for idx := sidx; idx < eidx; idx += 1 { 68 | schar, size := utf8_peek(trunes[idx:]) 69 | defer idx += size 70 | // c := trunes[idx] 71 | 72 | if !case_sensitive { 73 | schar = unicode.to_lower(schar) 74 | } 75 | 76 | pchar, _ := utf8_peek(prunes[pidx:]) 77 | if schar == pchar { 78 | score += SCORE_MATCH 79 | //bonus := bonusFor(prevClass, class) 80 | bonus := 0 81 | 82 | if consecutive == 0 { 83 | first_bonus = bonus 84 | } else { 85 | // break consecutive chunk 86 | if bonus == BONUS_BOUNDARY { 87 | first_bonus = bonus 88 | } 89 | 90 | bonus = max(max(bonus, first_bonus), BONUS_CONSECUTIVE) 91 | } 92 | 93 | if pidx == 0 { 94 | score += bonus * BONUS_FIRST_CHAR_MULTIPLIER; 95 | } else { 96 | score += bonus 97 | } 98 | 99 | in_gap = false 100 | consecutive += 1 101 | pidx += 1 102 | } else { 103 | if in_gap { 104 | score += SCORE_GAP_EXTENSION 105 | } else { 106 | score += SCORE_GAP_START 107 | } 108 | 109 | in_gap = true 110 | consecutive = 0 111 | first_bonus = 0 112 | } 113 | 114 | //prev_class = class 115 | } 116 | 117 | return score 118 | } 119 | 120 | match :: fuzz_match_v1 121 | 122 | fuzz_match_v1 :: proc( 123 | haystack, pattern: string, 124 | case_sensitive := false, 125 | ) -> (res: Fuzz_Result, ok: bool) { 126 | if len(pattern) == 0 { 127 | return 128 | } 129 | 130 | pidx := 0 131 | sidx := -1 132 | eidx := -1 133 | 134 | for index := 0; index < len(haystack); { 135 | schar, ssize := utf8_peek(haystack[index:]) 136 | defer index += ssize 137 | 138 | // if !case_sensitive { 139 | // c = unicode.to_lower(c) 140 | // } 141 | 142 | pchar, psize := utf8_peek(pattern[pidx:]) 143 | 144 | if schar == pchar { 145 | if sidx < 0 { 146 | sidx = index 147 | } 148 | 149 | pidx += psize 150 | 151 | if pidx == len(pattern) { 152 | eidx = index + ssize 153 | break 154 | } 155 | } 156 | } 157 | 158 | if sidx >= 0 && eidx >= 0 { 159 | pidx = utf8_prev(pattern, 0, pidx) 160 | 161 | for index := utf8_prev(haystack, 0, eidx); index >= sidx; { 162 | schar, _ := utf8_peek(haystack[index:]) 163 | 164 | // if !case_sensitive { 165 | // c = unicode.to_lower(c) 166 | // } 167 | 168 | pchar, _ := utf8_peek(pattern[pidx:]) 169 | 170 | if schar == pchar { 171 | pidx = utf8_prev(pattern, 0, pidx) 172 | 173 | if pidx < 0 { 174 | sidx = index 175 | break 176 | } 177 | } 178 | 179 | // stop at last index 180 | if index == 0 { 181 | break 182 | } 183 | 184 | index = utf8_prev(haystack, 0, index) 185 | } 186 | 187 | score := fuzz_calculate_score(haystack, pattern, sidx, eidx, case_sensitive) 188 | res = { sidx, eidx, score } 189 | ok = true 190 | return 191 | } 192 | 193 | return 194 | } -------------------------------------------------------------------------------- /src/hsluv.odin: -------------------------------------------------------------------------------- 1 | package src 2 | 3 | import "core:math" 4 | import "core:c/libc" 5 | 6 | Triplet :: struct { 7 | a, b, c: f64, 8 | } 9 | 10 | // for RGB 11 | m := [3]Triplet { 12 | { 3.24096994190452134377, -1.53738317757009345794, -0.49861076029300328366 }, 13 | { -0.96924363628087982613, 1.87596750150772066772, 0.04155505740717561247 }, 14 | { 0.05563007969699360846, -0.20397695888897656435, 1.05697151424287856072 }, 15 | } 16 | 17 | // for XYZ 18 | m_inv := [3]Triplet { 19 | { 0.41239079926595948129, 0.35758433938387796373, 0.18048078840183428751 }, 20 | { 0.21263900587151035754, 0.71516867876775592746, 0.07219231536073371500 }, 21 | { 0.01933081871559185069, 0.11919477979462598791, 0.95053215224966058086 }, 22 | } 23 | 24 | REF_U :: 0.19783000664283680764 25 | REF_V :: 0.46831999493879100370 26 | 27 | KAPPA :: 903.29629629629629629630 28 | EPSILON :: 0.00885645167903563082 29 | 30 | Bounds :: struct { 31 | a, b: f64, 32 | } 33 | 34 | get_bounds :: proc "contextless" (l: f64) -> (bounds: [6]Bounds) { 35 | tl := l + 16.0 36 | sub1 := (tl * tl * tl) / 1560896.0 37 | sub2 := sub1 > EPSILON ? sub1 : (l / KAPPA) 38 | 39 | for channel in 0..<3 { 40 | m1 := m[channel].a 41 | m2 := m[channel].b 42 | m3 := m[channel].c 43 | 44 | for t in 0.. f64 { 58 | return (line1.b - line2.b) / (line2.a - line1.a) 59 | } 60 | 61 | dist_from_pole_squared :: proc "contextless" (x, y: f64) -> f64 { 62 | return x * x + y * y 63 | } 64 | 65 | ray_length_until_intersect :: proc "contextless" (theta: f64, line: Bounds) -> f64 { 66 | return line.b / (math.sin(theta) - line.a * math.cos(theta)) 67 | } 68 | 69 | max_safe_chroma_for_l :: proc "contextless" (l: f64) -> f64 { 70 | min_len_squared := max(f64) 71 | bounds := get_bounds(l) 72 | 73 | for i in 0..<6 { 74 | m1 := bounds[i].a 75 | b1 := bounds[i].b 76 | // x where line intersects with perpendicular running though (0, 0) 77 | line2 := Bounds { -1.0 / m1, 0.0 } 78 | x := intersect_line_line(bounds[i], line2) 79 | distance := dist_from_pole_squared(x, b1 + x * m1) 80 | 81 | if distance < min_len_squared { 82 | min_len_squared = distance 83 | } 84 | } 85 | 86 | return math.sqrt(min_len_squared) 87 | } 88 | 89 | max_chroma_for_lh :: proc "contextless" (l, h: f64) -> f64 { 90 | min_len := max(f64) 91 | hrad := h * 0.01745329251994329577 // (2 * pi / 360) 92 | bounds := get_bounds(l) 93 | 94 | for i in 0..<6 { 95 | len := ray_length_until_intersect(hrad, bounds[i]) 96 | 97 | if len >= 0 && len < min_len { 98 | min_len = len 99 | } 100 | } 101 | 102 | return min_len 103 | } 104 | 105 | dot_product :: proc "contextless" (t1, t2: Triplet) -> f64 { 106 | return t1.a * t2.a + t1.b * t2.b + t1.c * t2.c 107 | } 108 | 109 | // Used for rgb conversions 110 | from_linear :: proc "contextless" (c: f64) -> f64 { 111 | if c <= 0.0031308 { 112 | return 12.92 * c 113 | } else { 114 | return 1.055 * math.pow(c, 1.0 / 2.4) - 0.055 115 | } 116 | } 117 | 118 | to_linear :: proc "contextless" (c: f64) -> f64 { 119 | if c > 0.04045 { 120 | return math.pow((c + 0.055) / 1.055, 2.4) 121 | } else { 122 | return c / 12.92 123 | } 124 | } 125 | 126 | xyz_to_rgb :: proc "contextless" (in_out: ^Triplet) { 127 | r := from_linear(dot_product(m[0], in_out^)) 128 | g := from_linear(dot_product(m[1], in_out^)) 129 | b := from_linear(dot_product(m[2], in_out^)) 130 | in_out.a = r 131 | in_out.b = g 132 | in_out.c = b 133 | } 134 | 135 | rgb_to_xyz :: proc "contextless" (in_out: ^Triplet) { 136 | rgbl := Triplet { to_linear(in_out.a), to_linear(in_out.b), to_linear(in_out.c) } 137 | x := dot_product(m_inv[0], rgbl) 138 | y := dot_product(m_inv[1], rgbl) 139 | z := dot_product(m_inv[2], rgbl) 140 | in_out.a = x 141 | in_out.b = y 142 | in_out.c = z 143 | } 144 | 145 | cbrt :: proc "contextless" (y: f64) -> f64 { 146 | return math.pow(y, 1.0 / 3.0) 147 | } 148 | 149 | /* 150 | https://en.wikipedia.org/wiki/CIELUV 151 | In these formulas, Yn refers to the reference white point. We are using 152 | illuminant D65, so Yn (see refY in Maxima file) equals 1. The formula is 153 | simplified accordingly. 154 | */ 155 | y_to_l :: proc "contextless" (y: f64) -> f64 { 156 | if y <= EPSILON { 157 | return y * KAPPA 158 | } else { 159 | return 116.0 * cbrt(y) - 16.0 160 | } 161 | } 162 | 163 | l_to_y :: proc "contextless" (l: f64) -> f64 { 164 | if l <= 8.0 { 165 | return l / KAPPA 166 | } else { 167 | x := (l + 16.0) / 116.0 168 | return (x * x * x) 169 | } 170 | } 171 | 172 | xyz_to_luv :: proc "contextless" (in_out: ^Triplet) { 173 | var_u := (4.0 * in_out.a) / (in_out.a + (15.0 * in_out.b) + (3.0 * in_out.c)) 174 | var_v := (9.0 * in_out.b) / (in_out.a + (15.0 * in_out.b) + (3.0 * in_out.c)) 175 | l := y_to_l(in_out.b) 176 | u := 13.0 * l * (var_u - REF_U) 177 | v := 13.0 * l * (var_v - REF_V) 178 | 179 | in_out.a = l 180 | if l < 0.00000001 { 181 | in_out.b = 0.0 182 | in_out.c = 0.0 183 | } else { 184 | in_out.b = u 185 | in_out.c = v 186 | } 187 | } 188 | 189 | luv_to_xyz :: proc "contextless" (in_out: ^Triplet) { 190 | if in_out.a <= 0.00000001 { 191 | // Black will create a divide-by-zero error. 192 | in_out.a = 0.0 193 | in_out.b = 0.0 194 | in_out.c = 0.0 195 | return 196 | } 197 | 198 | var_u := in_out.b / (13.0 * in_out.a) + REF_U 199 | var_v := in_out.c / (13.0 * in_out.a) + REF_V 200 | y := l_to_y(in_out.a) 201 | x := -(9.0 * y * var_u) / ((var_u - 4.0) * var_v - var_u * var_v) 202 | z := (9.0 * y - (15.0 * var_v * y) - (var_v * x)) / (3.0 * var_v) 203 | in_out.a = x 204 | in_out.b = y 205 | in_out.c = z 206 | } 207 | 208 | luv_to_lch :: proc "contextless" (in_out: ^Triplet) { 209 | l := in_out.a 210 | u := in_out.b 211 | v := in_out.c 212 | h: f64 213 | c := math.sqrt(u * u + v * v) 214 | 215 | // Grays: disambiguate hue 216 | if c < 0.00000001 { 217 | h = 0 218 | } else { 219 | h = math.atan2(v, u) * 57.29577951308232087680 // (180 / pi) 220 | 221 | if h < 0.0 { 222 | h += 360.0 223 | } 224 | } 225 | 226 | in_out.a = l 227 | in_out.b = c 228 | in_out.c = h 229 | } 230 | 231 | lch_to_luv :: proc "contextless" (in_out: ^Triplet) { 232 | hrad := in_out.c * 0.01745329251994329577 // (pi / 180.0) 233 | u := math.cos(hrad) * in_out.b 234 | v := math.sin(hrad) * in_out.b 235 | 236 | in_out.b = u 237 | in_out.c = v 238 | } 239 | 240 | hsluv_to_lch :: proc "contextless" (in_out: ^Triplet) { 241 | h := in_out.a 242 | s := in_out.b 243 | l := in_out.c 244 | c: f64 245 | 246 | // White and black: disambiguate chroma 247 | if l > 99.9999999 || l < 0.00000001 { 248 | c = 0.0 249 | } else { 250 | c = max_chroma_for_lh(l, h) / 100.0 * s 251 | } 252 | 253 | // Grays: disambiguate hue 254 | if s < 0.00000001 { 255 | h = 0.0 256 | } 257 | 258 | in_out.a = l 259 | in_out.b = c 260 | in_out.c = h 261 | } 262 | 263 | lch_to_hsluv :: proc "contextless" (in_out: ^Triplet) { 264 | l := in_out.a 265 | c := in_out.b 266 | h := in_out.c 267 | s: f64 268 | 269 | // White and black: disambiguate saturation 270 | if l > 99.9999999 || l < 0.00000001 { 271 | s = 0.0 272 | } else { 273 | s = c / max_chroma_for_lh(l, h) * 100.0 274 | } 275 | 276 | // Grays: disambiguate hue 277 | if c < 0.00000001 { 278 | h = 0.0 279 | } 280 | 281 | in_out.a = h 282 | in_out.b = s 283 | in_out.c = l 284 | } 285 | 286 | hpluv_to_lch :: proc "contextless" (in_out: ^Triplet) { 287 | h := in_out.a 288 | s := in_out.b 289 | l := in_out.c 290 | c: f64 291 | 292 | // White and black: disambiguate chroma 293 | if l > 99.9999999 || l < 0.00000001 { 294 | c = 0.0 295 | } else { 296 | c = max_safe_chroma_for_l(l) / 100.0 * s 297 | } 298 | 299 | // Grays: disambiguate hue 300 | if s < 0.00000001 { 301 | h = 0.0 302 | } 303 | 304 | in_out.a = l 305 | in_out.b = c 306 | in_out.c = h 307 | } 308 | 309 | lch_to_hpluv :: proc "contextless" (in_out: ^Triplet) { 310 | l := in_out.a 311 | c := in_out.b 312 | h := in_out.c 313 | s: f64 314 | 315 | // White and black: disambiguate saturation 316 | if l > 99.9999999 || l < 0.00000001 { 317 | s = 0.0 318 | } else { 319 | s = c / max_safe_chroma_for_l(l) * 100.0 320 | } 321 | 322 | // Grays: disambiguate hue 323 | if c < 0.00000001 { 324 | h = 0.0 325 | } 326 | 327 | in_out.a = h 328 | in_out.b = s 329 | in_out.c = l 330 | } 331 | 332 | 333 | /* 334 | Convert HSLuv to RGB. 335 | 336 | @param h Hue. Between 0.0 and 360.0. 337 | @param s Saturation. Between 0.0 and 100.0. 338 | @param l Lightness. Between 0.0 and 100.0. 339 | @param[out] pr Red component. Between 0.0 and 1.0. 340 | @param[out] pg Green component. Between 0.0 and 1.0. 341 | @param[out] pb Blue component. Between 0.0 and 1.0. 342 | */ 343 | hsluv_to_rgb :: proc "contextless" (h, s, l: f64) -> (pr, pg, pb: f64) { 344 | tmp := Triplet { h, s, l } 345 | hsluv_to_lch(&tmp) 346 | lch_to_luv(&tmp) 347 | luv_to_xyz(&tmp) 348 | xyz_to_rgb(&tmp) 349 | 350 | pr = tmp.a 351 | pg = tmp.b 352 | pb = tmp.c 353 | return 354 | } 355 | 356 | hpluv_to_rgb :: proc "contextless" (h, s, l: f64) -> (pr, pg, pb: f64) { 357 | tmp := Triplet { h, s, l } 358 | 359 | hpluv_to_lch(&tmp) 360 | lch_to_luv(&tmp) 361 | luv_to_xyz(&tmp) 362 | xyz_to_rgb(&tmp) 363 | 364 | pr = tmp.a 365 | pg = tmp.b 366 | pb = tmp.c 367 | return 368 | } 369 | 370 | /* 371 | Convert RGB to HSLuv. 372 | 373 | @param r Red component. Between 0.0 and 1.0. 374 | @param g Green component. Between 0.0 and 1.0. 375 | @param b Blue component. Between 0.0 and 1.0. 376 | @param[out] ph Hue. Between 0.0 and 360.0. 377 | @param[out] ps Saturation. Between 0.0 and 100.0. 378 | @param[out] pl Lightness. Between 0.0 and 100.0. 379 | */ 380 | rgb_to_hsluv :: proc "contextless" (r, g, b: f64) -> (ph, ps, pl: f64) { 381 | tmp := Triplet { r, g, b } 382 | 383 | rgb_to_xyz(&tmp) 384 | xyz_to_luv(&tmp) 385 | luv_to_lch(&tmp) 386 | lch_to_hsluv(&tmp) 387 | 388 | ph = tmp.a 389 | ps = tmp.b 390 | pl = tmp.c 391 | return 392 | } 393 | 394 | rgb_to_hpluv :: proc "contextless" (r, g, b: f64) -> (ph, ps, pl: f64) { 395 | tmp := Triplet { r, g, b } 396 | 397 | rgb_to_xyz(&tmp) 398 | xyz_to_luv(&tmp) 399 | luv_to_lch(&tmp) 400 | lch_to_hpluv(&tmp) 401 | 402 | ph = tmp.a 403 | ps = tmp.b 404 | pl = tmp.c 405 | return 406 | } 407 | -------------------------------------------------------------------------------- /src/main.odin: -------------------------------------------------------------------------------- 1 | package src 2 | 3 | import "base:runtime" 4 | import "core:math/bits" 5 | import "core:image" 6 | import "core:image/png" 7 | import "core:os" 8 | import "core:encoding/json" 9 | import "core:mem" 10 | import "core:math" 11 | import "core:time" 12 | import "core:strings" 13 | import "core:fmt" 14 | import "core:log" 15 | import "core:math/rand" 16 | import "core:unicode" 17 | import "core:thread" 18 | import "base:intrinsics" 19 | import sdl "vendor:sdl2" 20 | import "cutf8" 21 | import "btrie" 22 | 23 | SHOW_FPS :: false 24 | POOL_DEBUG :: false 25 | TRACK_MEMORY :: false 26 | 27 | when ODIN_DEBUG { 28 | VERSION :: "DEBUG" 29 | } else { 30 | VERSION :: "0.4.0" 31 | } 32 | 33 | main :: proc() { 34 | gs_init() 35 | context.logger = gs.logger 36 | context.allocator = gs_allocator() 37 | 38 | theme_presets_init() 39 | app = app_init() 40 | 41 | window := window_init(nil, {}, "Todool", 900, 900, 256, 256) 42 | window.on_resize = proc(window: ^Window) { 43 | cam := mode_panel_cam() 44 | mode_panel_cam_freehand_on(cam) 45 | } 46 | app.window_main = window 47 | window.element.message_user = window_main_message 48 | window.update_before = main_update 49 | window.update_check = proc(window: ^Window) -> (handled: bool) { 50 | handled |= power_mode_running() 51 | handled |= caret_state_update_motion(&app.caret, true) 52 | handled |= caret_state_update_alpha(&app.caret) 53 | handled |= caret_state_update_outline(&app.caret) 54 | handled |= mode_panel_zoom_running() 55 | handled |= app_focus_alpha_animate() != 0 56 | handled |= progressbars_animate() 57 | handled |= app_shadow_animate() 58 | handled |= bookmark_alpha_animate() 59 | return 60 | } 61 | window.name = "MAIN" 62 | 63 | { 64 | // keymap loading 65 | keymap_push_todool_commands(&app.window_main.keymap_custom) 66 | keymap_push_box_commands(&app.window_main.keymap_box) 67 | // NOTE push default todool to vim too 68 | keymap_push_todool_commands(&app.keymap_vim_normal) 69 | keymap_push_vim_normal_commands(&app.keymap_vim_normal) 70 | keymap_push_vim_insert_commands(&app.keymap_vim_insert) 71 | 72 | if loaded := keymap_load("save.keymap"); !loaded { 73 | // just clear since data could be loaded 74 | keymap_clear_combos(&app.window_main.keymap_custom) 75 | keymap_clear_combos(&app.window_main.keymap_box) 76 | keymap_clear_combos(&app.keymap_vim_normal) 77 | keymap_clear_combos(&app.keymap_vim_insert) 78 | 79 | keymap_push_todool_combos(&app.window_main.keymap_custom) 80 | keymap_push_box_combos(&app.window_main.keymap_box) 81 | keymap_push_vim_normal_combos(&app.keymap_vim_normal) 82 | keymap_push_vim_insert_combos(&app.keymap_vim_insert) 83 | 84 | keymap_force_push_latest(&app.window_main.keymap_custom) 85 | keymap_force_push_latest(&app.keymap_vim_normal) 86 | 87 | log.info("KEYMAP: Load failed -> Loading default") 88 | } else { 89 | log.info("KEYMAP: Load successful") 90 | } 91 | } 92 | 93 | { 94 | // add_shortcuts(window) 95 | menu_split, menu_bar := todool_menu_bar(&window.element) 96 | app.task_menu_bar = menu_bar 97 | panel := panel_init(menu_split, { .Panel_Horizontal, .Tab_Movement_Allowed }) 98 | statusbar_init(&statusbar, menu_split) 99 | sidebar_panel_init(panel) 100 | 101 | { 102 | rect := window.rect 103 | split := split_pane_init(panel, { .Split_Pane_Hidable, .VF, .HF, .Tab_Movement_Allowed }, 300, 300) 104 | split.pixel_based = true 105 | sb.split = split 106 | } 107 | 108 | sidebar_enum_panel_init(sb.split) 109 | task_panel_init(sb.split) 110 | 111 | goto_init(window) 112 | } 113 | 114 | { 115 | if loaded := json_load_misc("save.sjson"); loaded { 116 | log.info("JSON: Load Successful") 117 | } else { 118 | log.info("JSON: Load failed -> Using default") 119 | } 120 | } 121 | 122 | // tasks_load_tutorial() 123 | tasks_load_file() 124 | 125 | // do actual loading later because options might change the path 126 | gs_update_after_load() 127 | 128 | defer { 129 | intrinsics.atomic_store(&app.main_thread_running, false) 130 | } 131 | 132 | gs_message_loop() 133 | } 134 | 135 | main_box_key_combination :: proc(window: ^Window, msg: Message, di: int, dp: rawptr) -> int { 136 | task_head_tail_clamp() 137 | if app.task_head != -1 && app_has_no_selection() && app_filter_not_empty() { 138 | box := app_task_filter(app.task_head).box 139 | 140 | if element_message(box, msg, di, dp) == 1 { 141 | cam := mode_panel_cam() 142 | mode_panel_cam_freehand_off(cam) 143 | mode_panel_cam_bounds_check_x(cam, app.caret.rect.l, app.caret.rect.r, false, false) 144 | mode_panel_cam_bounds_check_y(cam, app.caret.rect.t, app.caret.rect.b, true) 145 | return 1 146 | } 147 | } 148 | 149 | return 0 150 | } 151 | 152 | main_update :: proc(window: ^Window) { 153 | // fmt.eprintln(font_regular, font_bold, font_icon) 154 | 155 | rendered_glyphs_clear() 156 | wrapped_lines_clear() 157 | 158 | task_set_children_info() 159 | task_check_parent_states(&app.um_task) 160 | 161 | switch app.task_state_progression { 162 | case .Idle: {} 163 | case .Update_Instant: { 164 | for index in app.pool.filter { 165 | task := app_task_list(index) 166 | 167 | if task_has_children(task) { 168 | task_progress_state_set(task) 169 | } 170 | } 171 | } 172 | case .Update_Animated: { 173 | for index in app.pool.filter { 174 | task := app_task_list(index) 175 | 176 | if task_has_children(task) { 177 | element_animation_start(&task.element) 178 | } else { 179 | task.progress_animation = {} 180 | } 181 | } 182 | } 183 | } 184 | app.task_state_progression = .Idle 185 | 186 | // just set the font options once here 187 | for index in app.pool.filter { 188 | task := app_task_list(index) 189 | task.element.font_options = task_has_children(task) ? &app.font_options_bold : nil 190 | } 191 | 192 | task_dragging_check_find(window) 193 | bookmarks_clear_and_set() 194 | 195 | // title building 196 | { 197 | b := &window.title_builder 198 | strings.builder_reset(b) 199 | strings.write_string(b, "Todool: ") 200 | strings.write_string(b, strings.to_string(app.last_save_location)) 201 | strings.write_string(b, app.dirty != app.dirty_saved ? " * " : " ") 202 | window_title_push_builder(window, b) 203 | } 204 | 205 | task_head_tail_clamp() 206 | 207 | // keep the head / tail at the position of the task you set it to at some point 208 | if task, ok := app.keep_task_position.?; ok { 209 | if app_filter_not_empty() && !task.removed { 210 | app.task_head = task.filter_index 211 | app.task_tail = app.task_head 212 | } 213 | 214 | app.keep_task_position = nil 215 | } 216 | 217 | // line changed 218 | if app.old_task_head != app.task_head || app.old_task_tail != app.task_tail { 219 | // call box changes immediatly when leaving task head / tail 220 | if app_filter_not_empty() && app.old_task_head != -1 && app.old_task_head < len(app.pool.filter) { 221 | cam := mode_panel_cam() 222 | 223 | mode_panel_cam_freehand_off(cam) 224 | task := app_task_filter(app.old_task_head) 225 | box_force_changes(&app.um_task, task.box) 226 | 227 | // add spell checking results to user dictionary 228 | spell_check_mapping_words_add(ss_string(&task.box.ss)) 229 | 230 | // save last state 231 | // app.caret.last_state = task.state 232 | } 233 | } 234 | 235 | app.old_task_head = app.task_head 236 | app.old_task_tail = app.task_tail 237 | 238 | pomodoro_update() 239 | image_load_process_texture_handles(window) 240 | 241 | statusbar_update(&statusbar) 242 | power_mode_set_caret_color() 243 | 244 | for &cam in &app.mmpp.cam { 245 | cam_update_check(&cam) 246 | } 247 | 248 | // check focus outside 249 | if app_filter_not_empty() && app.focus.root != nil { 250 | app_focus_bounds() 251 | 252 | if !(app.focus.start <= app.task_head && app.task_head < app.focus.end) || !task_has_children(app.focus.root) { 253 | app.focus.root = nil 254 | } 255 | } 256 | 257 | app_focus_alpha_update() 258 | mode_panel_zoom_update() 259 | progressbars_update() 260 | app_shadow_update() 261 | bookmark_alpha_update() 262 | } 263 | 264 | window_main_message :: proc(element: ^Element, msg: Message, di: int, dp: rawptr) -> int { 265 | window := cast(^Window) element 266 | 267 | #partial switch msg { 268 | case .Key_Combination: { 269 | handled := true 270 | combo := (cast(^string) dp)^ 271 | 272 | if window_focused_shown(window) || window.dialog != nil { 273 | return 0 274 | } 275 | 276 | if options_vim_use() { 277 | if vim.insert_mode { 278 | if main_box_key_combination(window, msg, di, dp) == 1 { 279 | return 1 280 | } 281 | 282 | // allow unicode insertion 283 | if keymap_combo_execute(&app.keymap_vim_insert, combo) { 284 | return 1 285 | } 286 | } else { 287 | // ignore unicode insertion always 288 | keymap_combo_execute(&app.keymap_vim_normal, combo) 289 | return 1 290 | } 291 | } else { 292 | if main_box_key_combination(window, msg, di, dp) == 1 { 293 | return 1 294 | } 295 | 296 | { 297 | if keymap_combo_execute(&window.keymap_custom, combo) { 298 | return 1 299 | } 300 | } 301 | } 302 | 303 | return 0 304 | } 305 | 306 | case .Unicode_Insertion: { 307 | if window_focused_shown(window) || window.dialog != nil { 308 | return 0 309 | } 310 | 311 | if app_filter_not_empty() { 312 | task_focused := app_task_head() 313 | res := element_message(task_focused.box, msg, di, dp) 314 | 315 | if res == 1 { 316 | cam := mode_panel_cam() 317 | mode_panel_cam_freehand_off(cam) 318 | mode_panel_cam_bounds_check_x(cam, app.caret.rect.l, app.caret.rect.r, false, true) 319 | mode_panel_cam_bounds_check_y(cam, app.caret.rect.t, app.caret.rect.b, true) 320 | app.task_tail = app.task_head 321 | } 322 | return res 323 | } 324 | } 325 | 326 | case .Window_Close_Interrupt: { 327 | return int(app_save_close()) 328 | } 329 | 330 | case .Window_Close: { 331 | gs_destroy_all_windows() 332 | } 333 | 334 | case .Dropped_Files: { 335 | if app_filter_empty() { 336 | return 0 337 | } 338 | 339 | // check the content to load 340 | load_images: bool 341 | window_drop_init(window) 342 | for file_path in window_drop_iter(window) { 343 | // image dropping 344 | if strings.has_suffix(file_path, ".png") { 345 | load_images = true 346 | break 347 | } 348 | } 349 | 350 | // load images only 351 | if load_images { 352 | window_drop_init(window) 353 | for file_path in window_drop_iter(window) { 354 | handle := image_load_push(file_path) 355 | 356 | if handle != nil { 357 | // find task by mouse intersection 358 | task := app_task_head() 359 | x, y := global_mouse_position() 360 | window_x, window_y := window_get_position(window) 361 | x -= window_x 362 | y -= window_y 363 | 364 | list := app_focus_list() 365 | for index in list { 366 | t := app_task_list(index) 367 | 368 | if rect_contains(t.element.bounds, x, y) { 369 | task = t 370 | break 371 | } 372 | } 373 | 374 | task_set_img(task, handle) 375 | } 376 | } 377 | } else { 378 | // spawn dialog with pattern question 379 | dialog_spawn( 380 | window, 381 | proc(dialog: ^Dialog, result: string) { 382 | // on success load with result string 383 | if dialog.result == .Default && result != "" { 384 | // save last result 385 | strings.builder_reset(&app.pattern_load_pattern) 386 | strings.write_string(&app.pattern_load_pattern, result) 387 | 388 | task := app_task_head() 389 | task_insert_offset := task.filter_index + 1 390 | task_indentation := task.indentation 391 | had_imports: bool 392 | manager := mode_panel_manager_begin() 393 | 394 | // read all files 395 | window_drop_init(app.window_main) 396 | for file_path in window_drop_iter(app.window_main) { 397 | // import from code 398 | content, ok := os.read_entire_file(file_path) 399 | defer delete(content) 400 | 401 | if ok { 402 | had_imports |= pattern_load_content_simple(manager, string(content), result, task_indentation, &task_insert_offset) 403 | } 404 | } 405 | 406 | if had_imports { 407 | task_head_tail_push(manager) 408 | undo_group_end(manager) 409 | } 410 | } 411 | }, 412 | 300, 413 | "Code Import: Lua Pattern\n%l\n%f\n%t\n%f\n%C%B", 414 | strings.to_string(app.pattern_load_pattern), 415 | "Cancel", 416 | "Import", 417 | ) 418 | } 419 | 420 | window_repaint(app.window_main) 421 | } 422 | } 423 | 424 | return 0 425 | } 426 | -------------------------------------------------------------------------------- /src/pattern.odin: -------------------------------------------------------------------------------- 1 | package src 2 | 3 | import "core:log" 4 | import "core:fmt" 5 | import "core:time" 6 | import "core:mem" 7 | import "core:os" 8 | import "core:unicode" 9 | import "core:unicode/utf8" 10 | import "core:slice" 11 | import "core:strings" 12 | import "core:hash" 13 | import "core:text/match" 14 | 15 | pattern_load_content_simple :: proc( 16 | manager: ^Undo_Manager, 17 | content: string, 18 | pattern: string, 19 | indentation: int, 20 | index_at: ^int, 21 | ) -> (found_any: bool) { 22 | temp := content 23 | 24 | for line in strings.split_lines_iterator(&temp) { 25 | m := match.matcher_init(line, pattern) 26 | 27 | res, ok := match.matcher_match(&m) 28 | 29 | if ok && m.captures_length > 1 { 30 | word := match.matcher_capture(&m, 0) 31 | task_push_undoable(manager, indentation, word, index_at^) 32 | index_at^ += 1 33 | found_any = true 34 | } 35 | } 36 | 37 | return 38 | } 39 | 40 | // pattern_read_dir :: proc( 41 | // path: string, 42 | // call: proc(string, ^Task, ^history.Batch), 43 | // parent: ^Task, 44 | // batch: ^history.Batch, 45 | // allocator := context.allocator, 46 | // ) { 47 | // if handle, err := os.open(path); err == os.ERROR_NONE { 48 | // if file_infos, err := os.read_dir(handle, 100, allocator); err == os.ERROR_NONE { 49 | // for file in file_infos { 50 | // if file.is_dir { 51 | // // recursively read inner directories 52 | // pattern_read_dir(file.fullpath, call, parent, batch, allocator) 53 | // } else { 54 | // if bytes, ok := os.read_entire_file(file.fullpath, allocator); ok { 55 | // // append(&ims.loaded_files, string(bytes)) 56 | // call(string(bytes[:]), parent, batch) 57 | // } 58 | // } 59 | // } 60 | // } else { 61 | // // try normal read 62 | // if bytes, ok := os.read_entire_file(path, allocator); ok { 63 | // // append(&ims.loaded_files, string(bytes)) 64 | // call(string(bytes[:]), parent, batch) 65 | // } 66 | // } 67 | // } else { 68 | // log.error("failed to open file %v", err) 69 | // } 70 | // } 71 | -------------------------------------------------------------------------------- /src/pomodoro.odin: -------------------------------------------------------------------------------- 1 | package src 2 | 3 | import "core:log" 4 | import "core:fmt" 5 | import "core:math/rand" 6 | import "core:math/ease" 7 | import "base:runtime" 8 | import "core:strings" 9 | import "core:time" 10 | import sdl "vendor:sdl2" 11 | 12 | Pomodoro_Celebration :: struct { 13 | x, y: f32, 14 | color: Color, 15 | skip: bool, 16 | } 17 | 18 | Pomodoro :: struct { 19 | index: int, // 0-2 20 | timer_id: sdl.TimerID, 21 | stopwatch: time.Stopwatch, 22 | accumulated: time.Duration, 23 | 24 | celebration_goal_reached: bool, 25 | celebrating: bool, 26 | celebration: []Pomodoro_Celebration, 27 | } 28 | pomodoro: Pomodoro 29 | 30 | pomodoro_init :: proc() { 31 | pomodoro.timer_id = sdl.AddTimer(500, pomodoro_timer_callback, nil) 32 | pomodoro.celebration = make([]Pomodoro_Celebration, 256) 33 | } 34 | 35 | pomodoro_destroy :: proc() { 36 | delete(pomodoro.celebration) 37 | sdl.RemoveTimer(pomodoro.timer_id) 38 | } 39 | 40 | // spawn & move particles down 41 | pomodoro_celebration_spawn :: proc(x, y: f32) { 42 | // if !pomodoro.celebrating { 43 | // pomodoro.celebrating = true 44 | // // fmt.eprintln("called", mmpp.bounds) 45 | 46 | // for c in &pomodoro.celebration { 47 | // c.skip = false 48 | // c.x = x 49 | // c.y = y 50 | // c.color = color_rgb_rand() 51 | 52 | // WIDTH :: 400 53 | // x_goal := x + rand.float32() * WIDTH - WIDTH / 2 54 | // anim_duration := time.Millisecond * time.Duration(rand.float32() * 4000 + 500) 55 | // anim_wait := rand.float64() * 2 56 | // window_animate(app.window_main, &c.y, f32(app.mmpp.bounds.b + 50), .Quadratic_In_Out, anim_duration, anim_wait) 57 | // window_animate(app.window_main, &c.x, x_goal, .Quadratic_Out, anim_duration, anim_wait) 58 | // } 59 | // } 60 | } 61 | 62 | // render particles as circles 63 | pomodoro_celebration_render :: proc(target: ^Render_Target) { 64 | if pomodoro.celebrating { 65 | draw_count := 0 66 | 67 | for &c in &pomodoro.celebration { 68 | if c.skip { 69 | continue 70 | } 71 | 72 | draw_count += 1 73 | rect := rect_wh(int(c.x), int(c.y), 10, 10) 74 | render_rect(target, rect, c.color, ROUNDNESS) 75 | 76 | if int(c.y) >= app.mmpp.bounds.b { 77 | c.skip = true 78 | } 79 | } 80 | 81 | // clear 82 | if draw_count == 0 { 83 | pomodoro.celebrating = false 84 | } 85 | } 86 | } 87 | 88 | // NOTE same as before, just return diff 89 | time_stop_stopwatch :: proc(using stopwatch: ^time.Stopwatch) -> (diff: time.Duration) { 90 | if running { 91 | diff = time.tick_diff(_start_time, time.tick_now()) 92 | _accumulation += diff 93 | running = false 94 | } 95 | 96 | return 97 | } 98 | 99 | pomodoro_stopwatch_stop_add :: proc() { 100 | diff := time_stop_stopwatch(&pomodoro.stopwatch) 101 | pomodoro.accumulated += diff 102 | // pomodoro.accumulated += time.Minute * 61 103 | } 104 | 105 | pomodoro_stopwatch_toggle :: proc() { 106 | if pomodoro.stopwatch.running { 107 | pomodoro_stopwatch_stop_add() 108 | sound_play(.Timer_Stop) 109 | } else { 110 | if pomodoro.stopwatch._accumulation != {} { 111 | sound_play(.Timer_Resume) 112 | } else { 113 | sound_play(.Timer_Start) 114 | } 115 | 116 | time.stopwatch_start(&pomodoro.stopwatch) 117 | } 118 | } 119 | 120 | pomodoro_stopwatch_reset :: #force_inline proc() { 121 | element_hide(sb.stats.pomodoro_reset, true) 122 | 123 | if pomodoro.stopwatch.running { 124 | pomodoro_stopwatch_stop_add() 125 | pomodoro.stopwatch._accumulation = {} 126 | } 127 | } 128 | 129 | // toggle stopwatch on or off based on index 130 | pomodoro_stopwatch_hot_toggle :: proc(du: u32) { 131 | defer { 132 | element_hide(sb.stats.pomodoro_reset, !pomodoro.stopwatch.running) 133 | element_repaint(app.mmpp) 134 | } 135 | 136 | value, ok := du_value(du) 137 | if !ok { 138 | return 139 | } 140 | 141 | index := int(value) 142 | if index == pomodoro.index { 143 | pomodoro_stopwatch_toggle() 144 | return 145 | } 146 | 147 | pomodoro.index = index 148 | 149 | if pomodoro.stopwatch.running { 150 | pomodoro_stopwatch_reset() 151 | } 152 | 153 | time.stopwatch_start(&pomodoro.stopwatch) 154 | pomodoro_label_format() 155 | } 156 | 157 | pomodoro_stopwatch_diff :: proc() -> time.Duration { 158 | accumulated := time.stopwatch_duration(pomodoro.stopwatch) 159 | wanted_minutes := pomodoro_time_index(pomodoro.index) 160 | // log.info("WANTED", wanted_minutes, accumulated) 161 | return (time.Minute * time.Duration(wanted_minutes)) - accumulated 162 | } 163 | 164 | // writes the pomodoro label 165 | pomodoro_label_format :: proc() { 166 | duration := pomodoro_stopwatch_diff() 167 | _, minutes, seconds := duration_clock(duration) 168 | 169 | // TODO could check for diff and only repaint then! 170 | b := &sb.pomodoro_label.builder 171 | strings.builder_reset(b) 172 | text := fmt.sbprintf(b, "%2d:%2d", int(minutes), int(seconds)) 173 | element_repaint(sb.pomodoro_label) 174 | // log.info("PRINTED", text, duration) 175 | } 176 | 177 | // on interval update the pomodoro label 178 | pomodoro_timer_callback :: proc "c" (interval: u32, data: rawptr) -> u32 { 179 | context = runtime.default_context() 180 | context.logger = gs.logger 181 | 182 | if pomodoro.stopwatch.running { 183 | pomodoro_label_format() 184 | sdl_push_empty_event() 185 | } 186 | 187 | return interval 188 | } 189 | 190 | // get time from slider 191 | pomodoro_time_index :: proc(index: int) -> (position: int) { 192 | index := clamp(index, 0, 2) 193 | switch index { 194 | case 0: position = sb.stats.work.position 195 | case 1: position = sb.stats.short_break.position 196 | case 2: position = sb.stats.long_break.position 197 | } 198 | return 199 | } 200 | 201 | pomodoro_button_message :: proc(element: ^Element, msg: Message, di: int, dp: rawptr) -> int { 202 | button := cast(^Button) element 203 | 204 | pomodoro_index_from :: proc(builder: strings.Builder) -> int { 205 | text := strings.to_string(builder) 206 | 207 | switch text { 208 | case "1": return 0 209 | case "2": return 1 210 | case "3": return 2 211 | } 212 | 213 | unimplemented("gotta add pomodoro index") 214 | } 215 | 216 | #partial switch msg { 217 | case .Button_Highlight: { 218 | color := cast(^Color) dp 219 | index := pomodoro_index_from(button.builder) 220 | selected := index == pomodoro.index 221 | color^ = selected ? theme.text_default : theme.text_blank 222 | return selected ? 1 : 2 223 | } 224 | 225 | case .Clicked: { 226 | pomodoro.index = pomodoro_index_from(button.builder) 227 | pomodoro_stopwatch_reset() 228 | pomodoro_label_format() 229 | element_repaint(element) 230 | } 231 | } 232 | 233 | return 0 234 | } 235 | 236 | pomodoro_update :: proc() { 237 | // check for duration diff > 0 238 | { 239 | if pomodoro.stopwatch.running { 240 | diff := pomodoro_stopwatch_diff() 241 | 242 | if diff < 0 { 243 | pomodoro_stopwatch_reset() 244 | sound_play(.Timer_Ended) 245 | } 246 | } 247 | } 248 | 249 | // set work today position 250 | { 251 | goal_today := max(time.Duration(sb.stats.work_today.position * 24), 1) * time.Hour 252 | sb.stats.gauge_work_today.position = f32(pomodoro.accumulated) / f32(goal_today) 253 | } 254 | 255 | // just update every frame 256 | { 257 | b := &sb.stats.label_time_accumulated.builder 258 | strings.builder_reset(b) 259 | hours, minutes, seconds := duration_clock(pomodoro.accumulated) 260 | fmt.sbprintf(b, "Total: %dh %dm %ds", hours, minutes, seconds) 261 | } 262 | 263 | { 264 | // if 265 | // sb.stats.gauge_work_today.position > 1.0 && 266 | // !pomodoro.celebration_goal_reached && 267 | // sb.stats.gauge_work_today.bounds != {} && 268 | // (.Hide not_in sb.enum_panel.flags) { 269 | // pomodoro.celebration_goal_reached = true 270 | // x := sb.stats.gauge_work_today.bounds.l + rect_width_halfed(sb.stats.gauge_work_today.bounds) 271 | // y := sb.stats.gauge_work_today.bounds.t 272 | // pomodoro_celebration_spawn(f32(x), f32(y)) 273 | // } 274 | } 275 | } -------------------------------------------------------------------------------- /src/pool.odin: -------------------------------------------------------------------------------- 1 | package src 2 | 3 | import "core:mem" 4 | import "core:fmt" 5 | import "base:runtime" 6 | 7 | // TASK LAYOUT DATA 8 | // dynamic array to store new tasks -> no new(Task) required anymore 9 | // free list to store which tasks were previously "removed" for undo/redo capability 10 | // (maybe) free list for folded content 11 | // "visible" list is fed manually 12 | 13 | // MANUAL TASK ACTIONS 14 | // deleting 15 | // remove handle from the "filter" 16 | // push handle to the free list 17 | // folding 18 | // pop all children handles 19 | // push to a folded list which stores all handles 20 | // swapping 21 | // happens in visible space only - no complexity at all 22 | // pushing new 23 | // just push to the pool 24 | // insert the handle at any "filter" index 25 | 26 | Task_Pool :: struct { 27 | list: [dynamic]Task, // storage for tasks, never change layout here 28 | filter: [dynamic]int, // what to use 29 | free_list: [dynamic]int, // empty indices that are allocated but unused from a save file 30 | } 31 | 32 | task_pool_push_new :: proc(pool: ^Task_Pool, check_freed: bool) -> (task: ^Task) { 33 | if check_freed && len(pool.free_list) > 0 { 34 | freed_index := pool.free_list[len(pool.free_list) - 1] 35 | pop(&pool.free_list) 36 | 37 | // NOTE we need to set the removed flag to false again, as they could still be marked! 38 | task = &pool.list[freed_index] 39 | task.removed = false 40 | 41 | return 42 | } 43 | 44 | index := len(pool.list) 45 | 46 | // keep track of resizes 47 | resized: bool 48 | if cap(pool.list) < len(pool.list) + 1 { 49 | resized = true 50 | } 51 | 52 | append(&pool.list, Task { 53 | list_index = index, 54 | }) 55 | 56 | if resized { 57 | // NOTE stupid fix to keep pointers to task parents sane 58 | for &task in &pool.list { 59 | for &child in &task.element.children { 60 | child.parent = &task.element 61 | } 62 | } 63 | } 64 | 65 | return &pool.list[index] 66 | } 67 | 68 | task_pool_init :: proc() -> (res: Task_Pool) { 69 | res.list = make([dynamic]Task, 0, 256) 70 | res.filter = make([dynamic]int, 0, 256) 71 | res.free_list = make([dynamic]int, 0, 64) 72 | return 73 | } 74 | 75 | task_pool_clear :: proc(pool: ^Task_Pool) { 76 | for &task in &pool.list { 77 | element_destroy_and_deallocate(&task.element) 78 | } 79 | 80 | // TODO clear other data 81 | clear(&pool.list) 82 | clear(&pool.filter) 83 | clear(&pool.free_list) 84 | } 85 | 86 | task_pool_destroy :: proc(pool: ^Task_Pool) { 87 | for &task in &pool.list { 88 | element_destroy_and_deallocate(&task.element) 89 | } 90 | 91 | // TODO clear other data 92 | delete(pool.list) 93 | delete(pool.filter) 94 | delete(pool.free_list) 95 | } -------------------------------------------------------------------------------- /src/power_mode.odin: -------------------------------------------------------------------------------- 1 | package src 2 | 3 | import "base:intrinsics" 4 | import "core:fmt" 5 | import "core:strings" 6 | import "core:math/rand" 7 | import "core:math/noise" 8 | import "core:math/ease" 9 | import "vendor:fontstash" 10 | import "cutf8" 11 | 12 | // NOTE noise return -1 to 1 range 13 | 14 | // options 15 | // lifetime scale 0.5-4 16 | // alpha max 0.1-1 17 | // screenshake amount 18 | // screenshake lifetime (how quick it ends) 19 | 20 | P_SPAWN_HIGH :: 10 21 | P_SPAWN_LOW :: 4 22 | 23 | PM_State :: struct { 24 | particles: [dynamic]PM_Particle, 25 | 26 | spawn_next: bool, 27 | 28 | // coloring 29 | color_seed: i64, 30 | color_count: f64, 31 | 32 | // caret color 33 | caret_color: Color, 34 | } 35 | pm_state: PM_State 36 | 37 | PM_Particle :: struct { 38 | lifetime: f32, 39 | lifetime_count: f32, 40 | delay: f32, // delay until drawn 41 | x, y: f32, 42 | xoff, yoff: f32, // camera offset at the spawn time 43 | radius: f32, 44 | color: Color, 45 | seed: i64, 46 | } 47 | 48 | power_mode_init :: proc() { 49 | using pm_state 50 | particles = make([dynamic]PM_Particle, 0, 256) 51 | color_seed = intrinsics.read_cycle_counter() 52 | } 53 | 54 | power_mode_destroy :: proc() { 55 | using pm_state 56 | delete(particles) 57 | } 58 | 59 | power_mode_clear :: proc() { 60 | using pm_state 61 | clear(&particles) 62 | color_count = 0 63 | } 64 | 65 | power_mode_check_spawn :: proc() { 66 | using pm_state 67 | 68 | if !pm_show() { 69 | return 70 | } 71 | 72 | if spawn_next { 73 | power_mode_spawn_at_caret() 74 | spawn_next = false 75 | } 76 | } 77 | 78 | // simple line spawn per glyph 79 | power_mode_spawn_along_text :: proc(text: string, x, y: f32, color: Color) { 80 | if !pm_show() { 81 | return 82 | } 83 | 84 | fcs_ahv(.LEFT, .TOP) 85 | fcs_size(DEFAULT_FONT_SIZE * TASK_SCALE) 86 | fcs_font(font_regular) 87 | iter := fontstash.TextIterInit(&gs.fc, x, y, text) 88 | q: fontstash.Quad 89 | 90 | cam := mode_panel_cam() 91 | cam_screenshake_reset(cam) 92 | 93 | for fontstash.TextIterNext(&gs.fc, &iter, &q) { 94 | power_mode_spawn_at(iter.x, iter.y, cam.offset_x, cam.offset_y, P_SPAWN_LOW, color) 95 | } 96 | } 97 | 98 | // NOTE using rendered glyphs only 99 | power_mode_spawn_along_task_text :: proc(task: ^Task, task_count: int) { 100 | if !pm_show() { 101 | return 102 | } 103 | 104 | if task.box.rendered_glyphs != nil { 105 | text := ss_string(&task.box.ss) 106 | color := theme_task_text(task.state) 107 | cam := mode_panel_cam() 108 | cam_screenshake_reset(cam) 109 | ds: cutf8.Decode_State 110 | count: int 111 | 112 | for codepoint, i in cutf8.ds_iter(&ds, text) { 113 | glyph := task.box.rendered_glyphs[i] 114 | delay := f32(count) * 0.002 + f32(task_count) * 0.02 115 | power_mode_spawn_at(glyph.x, glyph.y, cam.offset_x, cam.offset_y, P_SPAWN_LOW / 2, color, delay) 116 | count += 1 117 | } 118 | } 119 | } 120 | 121 | // spawn at the global caret 122 | power_mode_spawn_at_caret :: proc() { 123 | if !pm_show() { 124 | return 125 | } 126 | 127 | cam := mode_panel_cam() 128 | x := f32(app.caret.rect.l) 129 | y := f32(app.caret.rect.t) + rect_heightf_halfed(app.caret.rect) 130 | color: Color = pm_particle_colored() ? {} : pm_state.caret_color 131 | power_mode_spawn_at(x, y, cam.offset_x, cam.offset_y, P_SPAWN_HIGH, color) 132 | } 133 | 134 | // spawn particles through random points of a rectangle 135 | power_mode_spawn_rect :: proc( 136 | rect: RectI, 137 | count: int, 138 | color: Color = {}, 139 | ) { 140 | cam := mode_panel_cam() 141 | cam_screenshake_reset(cam) 142 | 143 | for i in 0.. 1 181 | value := (noise.noise_2d(color_seed, { color_count * 0.01, 0 }) + 1) / 2 182 | c = color_hsv_to_rgb(value, 1, 1) 183 | } 184 | 185 | append(&particles, PM_Particle { 186 | lifetime = life, 187 | lifetime_count = life, 188 | delay = d, 189 | 190 | x = x + rand.float32() * width - width / 2, 191 | y = y + rand.float32() * height - height / 2, 192 | radius = 2 + rand.float32() * size, 193 | color = c, 194 | xoff = -xoff, 195 | yoff = -yoff, 196 | 197 | // random seed 198 | seed = intrinsics.read_cycle_counter(), 199 | }) 200 | 201 | color_count += 1 202 | } 203 | } 204 | 205 | power_mode_update :: proc() { 206 | using pm_state 207 | 208 | if !pm_show() { 209 | return 210 | } 211 | 212 | for i := len(particles) - 1; i >= 0; i -= 1 { 213 | p := &particles[i] 214 | 215 | if p.delay > 0 { 216 | p.delay -= gs.dt 217 | continue 218 | } 219 | 220 | if p.lifetime_count > 0 { 221 | p.lifetime_count -= gs.dt 222 | x_dir := noise.noise_2d(p.seed, { f64(p.lifetime_count) / 2, 0 }) 223 | y_dir := noise.noise_2d(p.seed, { f64(p.lifetime_count) / 2, 1 }) 224 | p.x += x_dir * TASK_SCALE * TASK_SCALE 225 | p.y += y_dir * TASK_SCALE * TASK_SCALE 226 | } else { 227 | unordered_remove(&particles, i) 228 | } 229 | } 230 | } 231 | 232 | power_mode_render :: proc(target: ^Render_Target) { 233 | using pm_state 234 | 235 | if !pm_show() { 236 | return 237 | } 238 | 239 | cam := mode_panel_cam() 240 | xoff, yoff: f32 241 | alpha_opt := pm_particle_alpha_scale() 242 | 243 | for i := len(particles) - 1; i >= 0; i -= 1 { 244 | p := &particles[i] 245 | 246 | if p.delay > 0 { 247 | continue 248 | } 249 | 250 | alpha := clamp((p.lifetime_count / p.lifetime) * alpha_opt, 0, 1) 251 | 252 | // when alpha has reached 0 we can shortcut here 253 | if alpha == 0 { 254 | unordered_remove(&particles, i) 255 | continue 256 | } 257 | 258 | xoff = p.xoff + cam.offset_x 259 | yoff = p.yoff + cam.offset_y 260 | color := color_alpha(p.color, alpha) 261 | radius := max(1, p.radius * ease.cubic_out(alpha)) 262 | render_circle(target, p.x + xoff, p.y + yoff, radius, color, true) 263 | } 264 | } 265 | 266 | power_mode_running :: #force_inline proc() -> bool { 267 | return len(pm_state.particles) != 0 268 | } 269 | 270 | power_mode_issue_spawn :: #force_inline proc() { 271 | using pm_state 272 | 273 | if !pm_show() { 274 | return 275 | } 276 | 277 | spawn_next = true 278 | 279 | cam := mode_panel_cam() 280 | cam_screenshake_reset(cam) 281 | } 282 | 283 | cam_screenshake_reset :: #force_inline proc(cam: ^Pan_Camera) { 284 | cam.screenshake_counter = 0 285 | } 286 | 287 | power_mode_set_caret_color :: proc() { 288 | using pm_state 289 | 290 | if app.task_head != -1 { 291 | task := app_task_head() 292 | // TODO make this syntax based instead 293 | caret_color = theme_task_text(task.state) 294 | } 295 | } -------------------------------------------------------------------------------- /src/rect.odin: -------------------------------------------------------------------------------- 1 | package src 2 | 3 | import "core:math" 4 | 5 | RectF :: struct { 6 | l, r, t, b: f32, 7 | } 8 | 9 | RECT_LERP_INIT :: RectF { 10 | max(f32), 11 | -max(f32), 12 | max(f32), 13 | -max(f32), 14 | } 15 | 16 | RectI :: struct { 17 | l, r, t, b: int, 18 | } 19 | 20 | RECT_INF :: RectI { 21 | max(int), 22 | -max(int), 23 | max(int), 24 | -max(int), 25 | } 26 | 27 | // build a rectangle from multiple 28 | rect_inf_push :: proc(rect: ^RectI, other: RectI) { 29 | rect.t = min(rect.t, other.t) 30 | rect.l = min(rect.l, other.l) 31 | rect.b = max(rect.b, other.b) 32 | rect.r = max(rect.r, other.r) 33 | } 34 | 35 | rect_one :: #force_inline proc(a: int) -> RectI { 36 | return { a, a, a, a } 37 | } 38 | 39 | rect_one_inv :: #force_inline proc(a: int) -> RectI { 40 | return { a, -a, a, -a } 41 | } 42 | 43 | rect_negate :: #force_inline proc(a: RectI) -> RectI { 44 | return { 45 | -a.l, 46 | -a.r, 47 | -a.t, 48 | -a.b, 49 | } 50 | } 51 | 52 | rect_valid :: #force_inline proc(a: RectI) -> bool { 53 | return a.r > a.l && a.b > a.t 54 | } 55 | 56 | rect_invalid :: #force_inline proc(rect: RectI) -> bool { 57 | return !rect_valid(rect) 58 | } 59 | 60 | rect_wh :: #force_inline proc(x, y, w, h: int) -> RectI { 61 | return { x, x + w, y, y + h } 62 | } 63 | 64 | rect_center :: #force_inline proc(a: RectI) -> (x, y: f32) { 65 | return f32(a.l) + f32(a.r - a.l) / 2, f32(a.t) + f32(a.b - a.t) / 2 66 | } 67 | 68 | // width 69 | rect_width :: #force_inline proc(a: RectI) -> int { 70 | return (a.r - a.l) 71 | } 72 | rect_widthf :: #force_inline proc(a: RectI) -> f32 { 73 | return f32(a.r - a.l) 74 | } 75 | rect_width_halfed :: #force_inline proc(a: RectI) -> int { 76 | return (a.r - a.l) / 2 77 | } 78 | rect_widthf_halfed :: #force_inline proc(a: RectI) -> f32 { 79 | return f32(a.r - a.l) / 2 80 | } 81 | 82 | // height 83 | rect_height :: #force_inline proc(a: RectI) -> int { 84 | return (a.b - a.t) 85 | } 86 | rect_heightf :: #force_inline proc(a: RectI) -> f32 { 87 | return f32(a.b - a.t) 88 | } 89 | rect_height_halfed :: #force_inline proc(a: RectI) -> int { 90 | return (a.b - a.t) / 2 91 | } 92 | rect_heightf_halfed :: #force_inline proc(a: RectI) -> f32 { 93 | return f32(a.b - a.t) / 2 94 | } 95 | 96 | // width / height by option 97 | rect_opt_v :: #force_inline proc(a: RectI, vertical: bool) -> int { 98 | return vertical ? rect_height(a) : rect_width(a) 99 | } 100 | rect_opt_h :: #force_inline proc(a: RectI, horizontal: bool) -> int { 101 | return horizontal ? rect_width(a) : rect_height(a) 102 | } 103 | rect_opt_vf :: #force_inline proc(a: RectI, vertical: bool) -> f32 { 104 | return vertical ? rect_heightf(a) : rect_widthf(a) 105 | } 106 | rect_opt_hf :: #force_inline proc(a: RectI, horizontal: bool) -> f32 { 107 | return horizontal ? rect_widthf(a) : rect_heightf(a) 108 | } 109 | 110 | rect_xxyy :: #force_inline proc(x, y: int) -> RectI { 111 | return { x, x, y, y } 112 | } 113 | 114 | rect_intersection :: proc(a, b: RectI) -> RectI { 115 | a := a 116 | if a.l < b.l do a.l = b.l 117 | if a.t < b.t do a.t = b.t 118 | if a.r > b.r do a.r = b.r 119 | if a.b > b.b do a.b = b.b 120 | return a 121 | } 122 | 123 | // smallest rectangle 124 | rect_bounding :: proc(a, b: RectI) -> RectI { 125 | a := a 126 | if a.l > b.l do a.l = b.l 127 | if a.t > b.t do a.t = b.t 128 | if a.r < b.r do a.r = b.r 129 | if a.b < b.b do a.b = b.b 130 | return a 131 | } 132 | 133 | rect_contains :: proc(a: RectI, x, y: int) -> bool { 134 | return a.l <= x && a.r > x && a.t <= y && a.b > y 135 | } 136 | 137 | // rect cutting with HARD CUT, will result in invalid rectangles when out of size 138 | 139 | rect_cut_left :: proc(rect: ^RectI, a: int) -> (res: RectI) { 140 | res = rect^ 141 | res.r = rect.l + a 142 | rect.l = res.r 143 | return 144 | } 145 | 146 | rect_cut_right :: proc(rect: ^RectI, a: int) -> (res: RectI) { 147 | res = rect^ 148 | res.l = rect.r - a 149 | rect.r = res.l 150 | return 151 | } 152 | 153 | rect_cut_top :: proc(rect: ^RectI, a: int) -> (res: RectI) { 154 | res = rect^ 155 | res.b = rect.t + a 156 | rect.t = res.b 157 | return 158 | } 159 | 160 | rect_cut_bottom :: proc(rect: ^RectI, a: int) -> (res: RectI) { 161 | res = rect^ 162 | res.t = rect.b - a 163 | rect.b = res.t 164 | return 165 | } 166 | 167 | // add another rect as padding 168 | rect_padding :: proc(a, b: RectI) -> RectI { 169 | a := a 170 | a.l += b.l 171 | a.t += b.t 172 | a.r -= b.r 173 | a.b -= b.b 174 | return a 175 | } 176 | 177 | // add another rect as padding 178 | rect_margin :: proc(a: RectI, value: int) -> RectI { 179 | a := a 180 | a.l += value 181 | a.t += value 182 | a.r -= value 183 | a.b -= value 184 | return a 185 | } 186 | 187 | rect_add :: proc(a, b: RectI) -> RectI { 188 | a := a 189 | a.l += b.l 190 | a.t += b.t 191 | a.r += b.r 192 | a.b += b.b 193 | return a 194 | } 195 | 196 | rect_translate :: proc(a, b: RectI) -> RectI { 197 | a := a 198 | a.l += b.l 199 | a.t += b.t 200 | a.r += b.l 201 | a.b += b.t 202 | return a 203 | } 204 | 205 | rect_overlap :: proc(a, b: RectI) -> bool { 206 | return b.r >= a.l && b.l <= a.r && b.b >= a.t && b.t <= a.b 207 | } 208 | 209 | // cuts out rect b from a and returns the left regions 210 | rect_cut_out_rect :: proc(a, b: RectI) -> (res: [4]RectI) { 211 | // top 212 | res[0] = a 213 | res[0].b = b.t 214 | 215 | // bottom 216 | res[1] = a 217 | res[1].t = b.b 218 | 219 | // middle 220 | last := rect_intersection(res[0], res[1]) 221 | 222 | // left 223 | res[2] = last 224 | res[2].r = b.l 225 | 226 | // right 227 | res[3] = last 228 | res[3].l = b.r 229 | return 230 | } 231 | 232 | rect_lerp :: proc(a: ^RectF, b: RectI, rate: f32) { 233 | if a^ == RECT_LERP_INIT { 234 | a^ = rect_itof(b) 235 | } else { 236 | a.l = math.lerp(a.l, f32(b.l), rate) 237 | a.r = math.lerp(a.r, f32(b.r), rate) 238 | a.t = math.lerp(a.t, f32(b.t), rate) 239 | a.b = math.lerp(a.b, f32(b.b), rate) 240 | } 241 | } 242 | 243 | rect_animate_to :: proc(a: ^RectF, b: RectI, rate: f32 = 1, cuttoff: f32 = 0.001) { 244 | if a^ == RECT_LERP_INIT { 245 | a^ = rect_itof(b) 246 | } else { 247 | animate_to(&a.l, f32(b.l), rate, cuttoff) 248 | animate_to(&a.r, f32(b.r), rate, cuttoff) 249 | animate_to(&a.t, f32(b.t), rate, cuttoff) 250 | animate_to(&a.b, f32(b.b), rate, cuttoff) 251 | } 252 | } 253 | 254 | rect_ftoi :: proc(a: RectF) -> RectI { 255 | return { 256 | int(a.l), 257 | int(a.r), 258 | int(a.t), 259 | int(a.b), 260 | } 261 | } 262 | 263 | rect_itof :: proc(a: RectI) -> RectF { 264 | return { 265 | f32(a.l), 266 | f32(a.r), 267 | f32(a.t), 268 | f32(a.b), 269 | } 270 | } -------------------------------------------------------------------------------- /src/search.odin: -------------------------------------------------------------------------------- 1 | package src 2 | 3 | import "core:math" 4 | import "core:mem" 5 | import "core:fmt" 6 | import "core:strings" 7 | import "vendor:fontstash" 8 | import "cutf8" 9 | 10 | // THOUGHTS 11 | // check task search_updateidity? or update search results based on validity 12 | // what if the task is now invisible due to folding and search result still includes that 13 | 14 | // goals 15 | // space effefciency 16 | // linearity(?) loop through the results from index to another forward/backward 17 | // easy clearing 18 | // fast task lookup 19 | 20 | Search_Entry :: struct #packed { 21 | ptr: ^Task, 22 | length: u16, 23 | result_offset: int, 24 | } 25 | 26 | Search_Result :: struct { 27 | start, end: u16, 28 | } 29 | 30 | Search_State :: struct { 31 | results: [dynamic]Search_Result, 32 | entries: [dynamic]Search_Entry, 33 | 34 | // pointers from backing data 35 | result_count: ^u16, // current entry 36 | pattern_length: int, 37 | 38 | // task and box position saved 39 | saved_task_head: int, 40 | saved_task_tail: int, 41 | saved_box_head: int, 42 | saved_box_tail: int, 43 | 44 | // current index for searching 45 | current_index: int, 46 | 47 | // ui related 48 | text_box: ^Text_Box, 49 | 50 | // persistent state 51 | case_insensitive: bool, 52 | } 53 | search: Search_State 54 | panel_search: ^Panel 55 | 56 | // init to cap 57 | search_state_init :: proc() { 58 | search.entries = make([dynamic]Search_Entry, 0, 128) 59 | search.results = make([dynamic]Search_Result, 0, 1028) 60 | search_clear() 61 | } 62 | 63 | search_state_destroy :: proc() { 64 | using search 65 | delete(entries) 66 | delete(results) 67 | result_count = nil 68 | } 69 | 70 | // clear count and reset write slice 71 | search_clear :: proc() { 72 | using search 73 | clear(&entries) 74 | clear(&results) 75 | result_count = nil 76 | current_index = -1 77 | } 78 | 79 | search_has_results :: proc() -> bool { 80 | return len(search.entries) != 0 81 | } 82 | 83 | // push a ptr and set current counter 84 | search_push_task :: proc(task: ^Task) { 85 | using search 86 | append(&entries, Search_Entry { 87 | task, 88 | 0, 89 | len(results), 90 | }) 91 | entry := &entries[len(entries) - 1] 92 | result_count = &entry.length 93 | } 94 | 95 | search_pop_task :: proc() { 96 | pop(&search.entries) 97 | } 98 | 99 | // push a search result 100 | search_push_result :: proc(start, end: int) { 101 | search.result_count^ += 1 102 | append(&search.results, Search_Result { u16(start), u16(end) }) 103 | } 104 | 105 | // update serach state and find new results 106 | search_update :: proc(pattern: string) { 107 | search_clear() 108 | search.pattern_length = cutf8.count(pattern) 109 | 110 | if len(pattern) == 0 { 111 | return 112 | } 113 | 114 | if search.case_insensitive { 115 | // TODO any kind of optimization? 116 | builder := strings.builder_make(0, 256, context.temp_allocator) 117 | sf := string_finder_init(cutf8.to_lower(&builder, pattern)) 118 | defer string_finder_destroy(sf) 119 | 120 | // find results 121 | list := app_focus_list() 122 | for index in list { 123 | task := app_task_list(index) 124 | text := ss_string(&task.box.ss) 125 | task_pushed: bool 126 | index: int 127 | strings.builder_reset(&builder) 128 | text_lowered := cutf8.to_lower(&builder, text) 129 | 130 | for { 131 | res := string_finder_next(&sf, text_lowered[index:]) 132 | 133 | if res == -1 { 134 | break 135 | } 136 | 137 | if !task_pushed { 138 | search_push_task(task) 139 | task_pushed = true 140 | } 141 | 142 | index += res 143 | count := cutf8.count(text_lowered[:index]) 144 | search_push_result(count, count + search.pattern_length) 145 | index += len(pattern) 146 | } 147 | } 148 | } else { 149 | sf := string_finder_init(pattern) 150 | defer string_finder_destroy(sf) 151 | 152 | // find results 153 | list := app_focus_list() 154 | for index in list { 155 | task := app_task_list(index) 156 | text := ss_string(&task.box.ss) 157 | task_pushed: bool 158 | index: int 159 | 160 | for { 161 | res := string_finder_next(&sf, text[index:]) 162 | if res == -1 { 163 | break 164 | } 165 | 166 | if !task_pushed { 167 | search_push_task(task) 168 | task_pushed = true 169 | } 170 | 171 | index += res 172 | count := cutf8.count(text[:index]) 173 | search_push_result(count, count + search.pattern_length) 174 | index += len(pattern) 175 | } 176 | } 177 | } 178 | 179 | search_find_next() 180 | } 181 | 182 | search_find :: proc(backwards: bool) { 183 | using search 184 | 185 | if len(results) == 0 { 186 | return 187 | } 188 | 189 | range_advance_index(¤t_index, len(results) - 1, backwards) 190 | 191 | task: ^Task 192 | result_index: int 193 | length_sum: int 194 | for entry, i in entries { 195 | // in correct space 196 | if length_sum + int(entry.length) > current_index { 197 | task = entry.ptr 198 | result_index = entry.result_offset + (current_index - length_sum) 199 | break 200 | } 201 | 202 | length_sum += int(entry.length) 203 | } 204 | 205 | app.task_head = task.filter_index 206 | app.task_tail = task.filter_index 207 | 208 | result := results[result_index] 209 | text := task_string(task) 210 | task.box.head = int(result.end) 211 | task.box.tail = int(result.start) 212 | 213 | element_repaint(app.mmpp) 214 | } 215 | 216 | search_find_next :: proc() { 217 | search_find(false) 218 | } 219 | 220 | search_find_prev :: proc() { 221 | search_find(true) 222 | } 223 | 224 | // draw the search results outline 225 | search_draw_highlights :: proc(target: ^Render_Target, panel: ^Mode_Panel) { 226 | if (.Hide in panel_search.flags) || !search_has_results() { 227 | return 228 | } 229 | 230 | render_push_clip(target, panel.clip) 231 | ds: cutf8.Decode_State 232 | color := theme.text_good 233 | search_draw_index: int 234 | GRAY :: Color { 100, 100, 100, 255 } 235 | 236 | for entry in search.entries { 237 | task := entry.ptr 238 | length := entry.length 239 | top := task.box.bounds.t 240 | height := rect_heightf(task.box.bounds) 241 | scaled_size := f32(fcs_task(&task.element)) 242 | 243 | for i in 0.. int { 267 | button := cast(^Button) element 268 | 269 | #partial switch msg{ 270 | case .Paint_Recursive: { 271 | assert(button.data != nil) 272 | enabled := cast(^bool) button.data 273 | 274 | target := element.window.target 275 | pressed := element.window.pressed == element 276 | hovered := element.window.hovered == element 277 | text_color := hovered || pressed ? theme.text_default : theme.text_blank 278 | 279 | if enabled^ { 280 | render_hovered_highlight(target, element.bounds) 281 | } 282 | 283 | fcs_element(button) 284 | fcs_ahv() 285 | fcs_color(text_color) 286 | text := strings.to_string(button.builder) 287 | render_string_rect(target, element.bounds, text) 288 | return 1 289 | } 290 | 291 | case .Clicked: { 292 | assert(button.data != nil) 293 | enabled := cast(^bool) button.data 294 | enabled^ = !enabled^ 295 | element_message(search.text_box, .Value_Changed) 296 | return 1 297 | } 298 | } 299 | 300 | return 0 301 | } 302 | 303 | search_init :: proc(parent: ^Element) { 304 | margin_scaled := int(TEXT_PADDING * SCALE) 305 | height := int(DEFAULT_FONT_SIZE * SCALE) + margin_scaled * 2 306 | p := panel_init(parent, { .Panel_Default_Background, .Panel_Horizontal }, margin_scaled, 5) 307 | p.background_index = 2 308 | // p.shadow = true 309 | p.z_index = 2 310 | 311 | { 312 | button := button_init(p, {}, "aA", button_state_message) 313 | button.hover_info = "Case Insensitive Search" 314 | button.data = &search.case_insensitive 315 | } 316 | 317 | box := text_box_init(p, { .HF }) 318 | search.text_box = box 319 | box.um = &app.um_search 320 | box.message_user = proc(element: ^Element, msg: Message, di: int, dp: rawptr) -> int { 321 | box := cast(^Text_Box) element 322 | 323 | #partial switch msg { 324 | case .Value_Changed: { 325 | query := ss_string(&box.ss) 326 | search_update(query) 327 | } 328 | 329 | case .Key_Combination: { 330 | combo := (cast(^string) dp)^ 331 | handled := true 332 | 333 | switch combo { 334 | case "escape": { 335 | element_hide(panel_search, true) 336 | element_repaint(panel_search) 337 | app.task_head = search.saved_task_head 338 | app.task_tail = search.saved_task_tail 339 | } 340 | 341 | case "return": { 342 | element_hide(panel_search, true) 343 | element_repaint(panel_search) 344 | } 345 | 346 | // next 347 | case "f3", "ctrl n": { 348 | search_find_next() 349 | } 350 | 351 | // prev 352 | case "shift f3", "ctrl shift n": { 353 | search_find_prev() 354 | } 355 | 356 | case: { 357 | handled = false 358 | } 359 | } 360 | 361 | return int(handled) 362 | } 363 | 364 | case .Update: { 365 | if di == UPDATE_FOCUS_LOST { 366 | element_hide(panel_search, true) 367 | } 368 | } 369 | } 370 | 371 | return 0 372 | } 373 | 374 | b1 := button_init(p, {}, "Find Next") 375 | b1.invoke = proc(button: ^Button, data: rawptr) { 376 | search_find_next() 377 | } 378 | b2 := button_init(p, {}, "Find Prev") 379 | b2.invoke = proc(button: ^Button, data: rawptr) { 380 | search_find_prev() 381 | } 382 | 383 | panel_search = p 384 | element_hide(panel_search, true) 385 | } 386 | 387 | String_Finder :: struct { 388 | pattern: string, 389 | pattern_hash: u32, 390 | pattern_pow: u32, 391 | } 392 | 393 | string_finder_init :: proc(pattern: string) -> (res: String_Finder) { 394 | res.pattern = strings.clone(pattern) 395 | 396 | hash_str_rabin_karp :: proc(s: string) -> (hash: u32 = 0, pow: u32 = 1) { 397 | for i := 0; i < len(s); i += 1 { 398 | hash = hash*PRIME_RABIN_KARP + u32(s[i]) 399 | } 400 | sq := u32(PRIME_RABIN_KARP) 401 | for i := len(s); i > 0; i >>= 1 { 402 | if (i & 1) != 0 { 403 | pow *= sq 404 | } 405 | sq *= sq 406 | } 407 | return 408 | } 409 | 410 | res.pattern_hash, res.pattern_pow = hash_str_rabin_karp(pattern) 411 | return 412 | } 413 | 414 | string_finder_destroy :: proc(sf: String_Finder) { 415 | delete(sf.pattern) 416 | } 417 | 418 | @private PRIME_RABIN_KARP :: 16777619 419 | 420 | string_finder_next :: proc(sf: ^String_Finder, text: string) -> int { 421 | n := len(sf.pattern) 422 | switch { 423 | case n == 0: 424 | return 0 425 | case n == 1: 426 | return strings.index_byte(text, sf.pattern[0]) 427 | case n == len(text): 428 | if text == sf.pattern { 429 | return 0 430 | } 431 | return -1 432 | case n > len(text): 433 | return -1 434 | } 435 | 436 | hash, pow := sf.pattern_hash, sf.pattern_pow 437 | h: u32 438 | for i := 0; i < n; i += 1 { 439 | h = h*PRIME_RABIN_KARP + u32(text[i]) 440 | } 441 | if h == hash && text[:n] == sf.pattern { 442 | return 0 443 | } 444 | for i := n; i < len(text); /**/ { 445 | h *= PRIME_RABIN_KARP 446 | h += u32(text[i]) 447 | h -= pow * u32(text[i-n]) 448 | i += 1 449 | if h == hash && text[i-n:i] == sf.pattern { 450 | return i - n 451 | } 452 | } 453 | return -1 454 | } -------------------------------------------------------------------------------- /src/small_string.odin: -------------------------------------------------------------------------------- 1 | package src 2 | 3 | import "core:unicode" 4 | import "core:unicode/utf8" 5 | import "core:fmt" 6 | import "cutf8" 7 | 8 | SS_SIZE :: 255 9 | 10 | ss_temp_chars: [SS_SIZE]u8 11 | ss_temp_runes: [SS_SIZE]rune 12 | 13 | // static sized string with no magic going on 14 | // insert & pop are fast and utf8 based 15 | // 16 | // insert_at & remove_at are a bit more involved 17 | Small_String :: struct { 18 | buf: [SS_SIZE]u8, 19 | length: u8, // used up content 20 | } 21 | 22 | // actual string space left 23 | ss_size :: #force_inline proc(ss: ^Small_String) -> u8 { 24 | return ss.length 25 | } 26 | 27 | // true if the there is still space left 28 | ss_has_space :: #force_inline proc(ss: ^Small_String) -> bool { 29 | return ss.length != SS_SIZE 30 | } 31 | 32 | // return the actual string 33 | ss_string :: #force_inline proc(ss: ^Small_String) -> string { 34 | return string(ss.buf[:ss.length]) 35 | } 36 | 37 | // true if the string has a length 38 | ss_has_content :: #force_inline proc(ss: ^Small_String) -> bool { 39 | return ss.length != 0 40 | } 41 | 42 | // true when empty length 43 | ss_empty :: #force_inline proc(ss: ^Small_String) -> bool { 44 | return ss.length == 0 45 | } 46 | 47 | // true when empty length 48 | ss_full :: #force_inline proc(ss: ^Small_String) -> bool { 49 | return ss.length == 255 50 | } 51 | 52 | // clear the small string 53 | ss_clear :: #force_inline proc(ss: ^Small_String) { 54 | ss.length = 0 55 | } 56 | 57 | ss_copy :: proc(a, b: ^Small_String) { 58 | a.length = b.length 59 | copy(a.buf[:], b.buf[:a.length]) 60 | } 61 | 62 | // append the rune to the buffer 63 | // true on success 64 | ss_append :: proc(ss: ^Small_String, c: rune) -> bool { 65 | data, size := utf8.encode_rune(c) 66 | 67 | if int(ss.length) + size <= SS_SIZE { 68 | for i in 0.. (c: rune, ok: bool) { 81 | if ss.length != 0 { 82 | size: int 83 | c, size = utf8.decode_last_rune(ss.buf[:ss.length]) 84 | 85 | if c != utf8.RUNE_ERROR { 86 | // pop of the rune 87 | ss.length -= u8(size) 88 | ok = true 89 | return 90 | } 91 | } 92 | 93 | return 94 | } 95 | 96 | SS_Byte_Info :: struct { 97 | byte_index: u8, 98 | codepoint: rune, 99 | size: u8, 100 | } 101 | 102 | // find the byte index at codepoint index 103 | // codepoint codepoint result and its size 104 | _ss_find_byte_index_info :: proc( 105 | ss: ^Small_String, 106 | index: int, 107 | ) -> ( 108 | info: SS_Byte_Info, 109 | found: bool, 110 | ) { 111 | ds: cutf8.Decode_State 112 | 113 | for codepoint, i in cutf8.ds_iter(&ds, ss_string(ss)) { 114 | if i == index { 115 | info.codepoint = codepoint 116 | info.byte_index = u8(ds.byte_offset_old) 117 | info.size = u8(ds.byte_offset - ds.byte_offset_old) 118 | found = true 119 | return 120 | } 121 | } 122 | 123 | info.byte_index = u8(ds.byte_offset) 124 | return 125 | } 126 | 127 | // NOTE in utf8 indexing space! 128 | // insert a rune at a wanted index 129 | ss_insert_at :: proc(ss: ^Small_String, index: int, c: rune) -> bool { 130 | if ss.length == 0 { 131 | return ss_append(ss, c) 132 | } else { 133 | data, size := utf8.encode_rune(c) 134 | 135 | // check if we even have enough space 136 | if int(ss.length) + size <= SS_SIZE { 137 | undex := u8(index) 138 | info, found := _ss_find_byte_index_info(ss, index) 139 | 140 | // check for append situation still since its faster then copy 141 | if ss.length == info.byte_index || !found { 142 | for i in 0.. (c: rune, ok: bool) { 168 | if ss.length == 0 { 169 | return 170 | } 171 | 172 | info, found := _ss_find_byte_index_info(ss, index) 173 | 174 | if !found { 175 | return 176 | } 177 | 178 | // no need to copy if at end 179 | // if info.byte_index != ss.length { 180 | copy(ss.buf[info.byte_index:ss.length], ss.buf[info.byte_index + info.size:ss.length]) 181 | // } 182 | 183 | ss.length -= info.size 184 | c = info.codepoint 185 | ok = true 186 | return 187 | } 188 | 189 | // NOTE in utf8 indexing space! 190 | // removes a rune backwards at the wanted codepoint index 191 | ss_delete_at :: proc(ss: ^Small_String, index: int) -> (c: rune, ok: bool) { 192 | if ss.length == 0 { 193 | return 194 | } 195 | 196 | info, found := _ss_find_byte_index_info(ss, index) 197 | 198 | // leave early when out of bounds 199 | if !found { 200 | return 201 | } 202 | 203 | // skip empty anyway 204 | if ss.length == u8(info.size) { 205 | // fmt.eprintln("skip") 206 | } else { 207 | copy(ss.buf[info.byte_index:ss.length], ss.buf[info.byte_index + info.size:ss.length]) 208 | } 209 | 210 | c = info.codepoint 211 | ss.length -= info.size 212 | ok = true 213 | return 214 | } 215 | 216 | // init with string 217 | ss_init_string :: proc(text: string) -> (ss: Small_String) { 218 | if len(text) != 0 { 219 | length := min(len(text), SS_SIZE) 220 | copy(ss.buf[:length], text[:length]) 221 | ss.length = u8(length) 222 | } 223 | 224 | return 225 | } 226 | 227 | // set the small string to your wanted text 228 | ss_set_string :: proc(ss: ^Small_String, text: string) { 229 | if len(text) == 0 { 230 | ss.length = 0 231 | } else { 232 | length := min(len(text), SS_SIZE) 233 | copy(ss.buf[:length], text[:length]) 234 | ss.length = u8(length) 235 | } 236 | } 237 | 238 | // remove the selection into the temp buffer 239 | // false if diff is low 240 | ss_remove_selection :: proc( 241 | ss: ^Small_String, 242 | low, high: int, 243 | temp: []u8, 244 | ) -> ( 245 | temp_size: int, 246 | ok: bool, 247 | ) { 248 | diff := high - low 249 | if diff == 0 { 250 | return 251 | } 252 | 253 | // could be optimized to inline find both instantly 254 | info1, found1 := _ss_find_byte_index_info(ss, low) 255 | info2, found2 := _ss_find_byte_index_info(ss, high) 256 | 257 | size := info2.byte_index - info1.byte_index 258 | // copy into temp buffer 259 | copy(temp[:], ss.buf[info1.byte_index:info2.byte_index]) 260 | // move upper to lower info 261 | copy(ss.buf[info1.byte_index:ss.length], ss.buf[info2.byte_index:ss.length]) 262 | ss.length -= size 263 | temp_size = int(size) 264 | ok = true 265 | 266 | return 267 | } 268 | 269 | // insert string at index if it has enough space 270 | ss_insert_string_at :: proc( 271 | ss: ^Small_String, 272 | index: int, 273 | text: string, 274 | ) -> bool { 275 | if len(text) == 0 { 276 | return false 277 | } 278 | 279 | info, found := _ss_find_byte_index_info(ss, index) 280 | 281 | new_size := min(int(ss.length) + len(text), SS_SIZE) 282 | 283 | if new_size - int(ss.length) != len(text) { 284 | return false 285 | } 286 | 287 | diff_size := u8(len(text)) 288 | ss.length += diff_size 289 | 290 | // copy forward 291 | copy( 292 | ss.buf[info.byte_index + diff_size:ss.length], 293 | ss.buf[info.byte_index:ss.length], 294 | ) 295 | // copy from thingy 296 | copy( 297 | ss.buf[info.byte_index:info.byte_index + diff_size], 298 | text[:diff_size], 299 | ) 300 | 301 | return true 302 | } 303 | 304 | 305 | // write uppercased version of the string 306 | ss_uppercased_string :: proc(ss: ^Small_String) { 307 | // copy to temp and iterate through it 308 | copy(ss_temp_chars[:ss.length], ss.buf[:ss.length]) 309 | input := string(ss_temp_chars[:ss.length]) 310 | ds: cutf8.Decode_State 311 | prev: rune 312 | 313 | // append lower case version 314 | ss_clear(ss) 315 | for codepoint, i in cutf8.ds_iter(&ds, input) { 316 | codepoint := codepoint 317 | 318 | if i == 0 || (prev != 0 && prev == ' ') { 319 | codepoint = unicode.to_upper(codepoint) 320 | } 321 | 322 | ss_append(ss, codepoint) 323 | prev = codepoint 324 | } 325 | } 326 | 327 | // write uppercased version of the string 328 | ss_lowercased_string :: proc(ss: ^Small_String) { 329 | // copy to temp and iterate through it 330 | copy(ss_temp_chars[:ss.length], ss.buf[:ss.length]) 331 | input := string(ss_temp_chars[:ss.length]) 332 | ds: cutf8.Decode_State 333 | 334 | // append lower case version 335 | ss_clear(ss) 336 | for codepoint, i in cutf8.ds_iter(&ds, input) { 337 | ss_append(ss, unicode.to_lower(codepoint)) 338 | } 339 | } 340 | 341 | // to temp runes 342 | ss_to_runes_temp :: proc(ss: ^Small_String) -> []rune { 343 | state, codepoint: rune 344 | codepoint_index: int 345 | 346 | for i in 0.. []Word_Result { 40 | clear(words) 41 | ds: cutf8.Decode_State 42 | index_codepoint_start := -1 43 | index_byte_start := -1 44 | 45 | word_push_check :: proc( 46 | words: ^[dynamic]Word_Result, 47 | text: string, 48 | ds: cutf8.Decode_State, 49 | index_codepoint_current: int, 50 | index_codepoint_start: ^int, 51 | index_byte_start: int, 52 | ) { 53 | if index_codepoint_start^ != -1 { 54 | append(words, Word_Result { 55 | text = text[index_byte_start:ds.byte_offset_old], 56 | index_codepoint_start = index_codepoint_start^, 57 | index_codepoint_end = index_codepoint_current, 58 | }) 59 | 60 | index_codepoint_start^ = -1 61 | } 62 | } 63 | 64 | for codepoint, index in cutf8.ds_iter(&ds, text) { 65 | if unicode.is_alpha(codepoint) { 66 | if index_codepoint_start == -1 { 67 | index_codepoint_start = index 68 | index_byte_start = ds.byte_offset_old 69 | } 70 | } else { 71 | word_push_check(words, text, ds, index, &index_codepoint_start, index_byte_start) 72 | } 73 | } 74 | 75 | word_push_check(words, text, ds, ds.codepoint_count, &index_codepoint_start, index_byte_start) 76 | return words[:] 77 | } 78 | 79 | words_extract_test :: proc() { 80 | words := make([dynamic]Word_Result, 0, 32) 81 | w1 := "testing this out man" 82 | words_extract(&words, w1) 83 | fmt.eprintln(words[:], "\n") 84 | w2 := "test" 85 | words_extract(&words, w2) 86 | fmt.eprintln(words[:], "\n") 87 | } 88 | 89 | spell_check_render_missing_words :: proc(target: ^Render_Target, task: ^Task) { 90 | text := ss_string(&task.box.ss) 91 | words := words_extract(&sc.word_results, text) 92 | 93 | builder := strings.builder_make(0, 256, context.temp_allocator) 94 | ds: cutf8.Decode_State 95 | 96 | for word in words { 97 | // lower case each word 98 | strings.builder_reset(&builder) 99 | ds = {} 100 | for codepoint in cutf8.ds_iter(&ds, word.text) { 101 | // TODO CHECK UTF8 WORD HERE? 102 | strings.write_rune(&builder, codepoint) 103 | } 104 | // res := rax.CustomFind(rt, raw_data(builder.buf), len(builder.buf)) 105 | exists := spell_check_mapping(strings.to_string(builder)) 106 | 107 | // render the result when not found 108 | if !exists { 109 | fcs_task(&task.element) 110 | state := wrap_state_init( 111 | &gs.fc, 112 | task.box.wrapped_lines, 113 | word.index_codepoint_start, 114 | word.index_codepoint_end, 115 | ) 116 | scaled_size := f32(state.isize / 10) 117 | line_width := LINE_WIDTH + int(4 * TASK_SCALE) 118 | 119 | for wrap_state_iter(&gs.fc, &state) { 120 | y := task.box.bounds.t + int(f32(state.y) * scaled_size) - line_width / 2 121 | 122 | rect := RectI { 123 | task.box.bounds.l + int(state.x_from), 124 | task.box.bounds.l + int(state.x_to), 125 | y, 126 | y + line_width, 127 | } 128 | 129 | render_sine(target, rect, theme.text_bad) 130 | } 131 | } 132 | } 133 | } 134 | 135 | // build the ebook to a compressed format 136 | compressed_trie_build :: proc() { 137 | btrie.ctrie_init(80000) 138 | defer btrie.ctrie_destroy() 139 | 140 | bytes, ok := os.read_entire_file("assets/big.txt", context.allocator) 141 | defer delete(bytes) 142 | 143 | // NOTE ASSUMING ASCII ENCODING 144 | // check for words in file, to lower all 145 | word: [256]u8 146 | word_index: uint 147 | for i in 0.. (res: bool) { 209 | res = btrie.comp_search(key) 210 | 211 | if !res { 212 | when !DISABLE_USER { 213 | _, found := sc.user_intern.entries[key] 214 | res = found 215 | } 216 | } 217 | 218 | return 219 | } 220 | 221 | // check to add non existent words to the spell checker user dictionary 222 | spell_check_mapping_words_add :: proc(word: string) { 223 | words := words_extract(&sc.word_results, word) 224 | 225 | for word_result in words { 226 | spell_check_mapping_add(word_result.text) 227 | } 228 | } 229 | 230 | spell_check_mapping_add :: proc(key: string) -> bool { 231 | // only if the key doesnt exist in the compressed trie 232 | if !btrie.comp_search(key) { 233 | _, err := strings.intern_get(&sc.user_intern, key) 234 | // fmt.eprintln("added --->", key) 235 | return err == nil 236 | } 237 | 238 | return false 239 | } -------------------------------------------------------------------------------- /src/statusbar.odin: -------------------------------------------------------------------------------- 1 | package src 2 | 3 | import "core:reflect" 4 | import "core:strings" 5 | import "core:fmt" 6 | 7 | Statusbar :: struct { 8 | stat: ^Element, 9 | label_info: ^Label, 10 | 11 | task_panel: ^Panel, 12 | label_task_state: [Task_State]^Label, 13 | 14 | label_task_count: ^Label, 15 | 16 | vim_panel: ^Panel, 17 | vim_mode_label: ^Vim_Label, 18 | // label_vim_buffer: ^Label, 19 | } 20 | statusbar: Statusbar 21 | 22 | Vim_Label :: struct { 23 | using element: Element, 24 | } 25 | 26 | vim_label_init :: proc( 27 | parent: ^Element, 28 | flags: Element_Flags, 29 | ) -> (res: ^Vim_Label) { 30 | res = element_init(Vim_Label, parent, flags, vim_label_message, context.allocator) 31 | return 32 | } 33 | 34 | vim_label_message :: proc(element: ^Element, msg: Message, di: int, dp: rawptr) -> int { 35 | v := cast(^Vim_Label) element 36 | text := vim.insert_mode ? "-- INSERT --" : "NORMAL" 37 | 38 | #partial switch msg { 39 | case .Paint_Recursive: { 40 | target := element.window.target 41 | fcs_element(element) 42 | fcs_ahv() 43 | fcs_color(theme.panel[0]) 44 | render_string_rect(target, element.bounds, text) 45 | } 46 | 47 | case .Get_Width: { 48 | fcs_element(element) 49 | return int(string_width(text)) 50 | } 51 | 52 | case .Get_Height: { 53 | return efont_size(element) 54 | } 55 | } 56 | 57 | return 0 58 | } 59 | 60 | statusbar_init :: proc(using statusbar: ^Statusbar, parent: ^Element) { 61 | stat = element_init(Element, parent, {}, statusbar_message, context.allocator) 62 | label_info = label_init(stat, { .Label_Center }) 63 | 64 | task_panel = panel_init(stat, { .HF, .Panel_Horizontal }, 5, 5) 65 | task_panel.color = &theme.panel[1] 66 | task_panel.rounded = true 67 | 68 | for i in 0.. int { 87 | #partial switch msg { 88 | case .Paint_Recursive: { 89 | target := element.window.target 90 | render_rect(target, element.bounds, theme.background[2]) 91 | } 92 | 93 | case .Layout: { 94 | bounds := element.bounds 95 | bounds = rect_margin(bounds, int(TEXT_PADDING * SCALE)) 96 | 97 | // custom layout based on data 98 | for child in element.children { 99 | w := element_message(child, .Get_Width) 100 | 101 | if .HF in child.flags { 102 | // right 103 | element_move(child, rect_cut_right(&bounds, w)) 104 | bounds.r -= int(TEXT_PADDING * SCALE) 105 | } else { 106 | element_move(child, rect_cut_left(&bounds, w)) 107 | bounds.l += int(TEXT_PADDING * SCALE) 108 | } 109 | } 110 | } 111 | } 112 | 113 | return 0 114 | } 115 | 116 | statusbar_update :: proc(using statusbar: ^Statusbar) { 117 | // update checkbox if hidden by key command 118 | // { 119 | // checkbox := &sb.options.checkbox_hide_statusbar 120 | // if checkbox.state != (.Hide in s.state.flags) { 121 | // checkbox_set(checkbox, (.Hide not_in s.state.flags)) 122 | // } 123 | // } 124 | 125 | if .Hide in stat.flags { 126 | return 127 | } 128 | 129 | element_hide(vim_panel, !options_vim_use()) 130 | 131 | // info 132 | { 133 | b := &label_info.builder 134 | strings.builder_reset(b) 135 | 136 | if app.task_head == -1 { 137 | fmt.sbprintf(b, "~") 138 | } else { 139 | if app.task_head != app.task_tail { 140 | low, high := task_low_and_high() 141 | fmt.sbprintf(b, "Lines %d - %d selected", low + 1, high + 1) 142 | } else { 143 | task := app_task_head() 144 | 145 | if .Hide not_in panel_search.flags { 146 | index := search.current_index 147 | amt := len(search.results) 148 | 149 | if amt == 0 { 150 | fmt.sbprintf(b, "No matches found") 151 | } else if amt == 1 { 152 | fmt.sbprintf(b, "1 match") 153 | } else { 154 | fmt.sbprintf(b, "%d of %d matches", index + 1, amt) 155 | } 156 | } else { 157 | if task.box.head != task.box.tail { 158 | low, high := box_low_and_high(task.box) 159 | fmt.sbprintf(b, "%d characters selected", high - low) 160 | } else { 161 | // default 162 | fmt.sbprintf(b, "Line %d, Column %d", app.task_head + 1, task.box.head + 1) 163 | } 164 | } 165 | } 166 | } 167 | } 168 | 169 | // count states 170 | count: [Task_State]int 171 | for list_index in app.pool.filter { 172 | task := app_task_list(list_index) 173 | count[task.state] += 1 174 | } 175 | task_names := reflect.enum_field_names(Task_State) 176 | 177 | // tasks 178 | for state, i in Task_State { 179 | label := label_task_state[state] 180 | b := &label.builder 181 | strings.builder_reset(b) 182 | strings.write_string(b, task_names[i]) 183 | strings.write_byte(b, ' ') 184 | strings.write_int(b, count[state]) 185 | } 186 | 187 | { 188 | total := len(app.pool.filter) 189 | shown := len(app.pool.filter) 190 | 191 | hidden := 0 192 | deleted: int 193 | for task in app.pool.list { 194 | if task.removed { 195 | deleted += 1 196 | } else { 197 | if task.filter_folded { 198 | hidden += len(task.filter_children) 199 | } 200 | } 201 | } 202 | total += hidden 203 | 204 | b := &label_task_count.builder 205 | strings.builder_reset(b) 206 | 207 | when POOL_DEBUG { 208 | strings.write_string(b, "T! ") 209 | strings.write_int(b, len(app.pool.list)) 210 | strings.write_string(b, ", ") 211 | 212 | if len(app.pool.free_list) != 0 { 213 | strings.write_string(b, "A! ") 214 | strings.write_int(b, len(app.pool.list) - len(app.pool.free_list)) 215 | strings.write_string(b, ", ") 216 | 217 | strings.write_string(b, "F! ") 218 | strings.write_int(b, len(app.pool.free_list)) 219 | strings.write_string(b, ", ") 220 | } 221 | } 222 | 223 | strings.write_string(b, "Total ") 224 | strings.write_int(b, total) 225 | strings.write_string(b, ", ") 226 | 227 | if hidden != 0 { 228 | strings.write_string(b, "Shown ") 229 | strings.write_int(b, shown) 230 | strings.write_string(b, ", ") 231 | 232 | strings.write_string(b, "Hidden ") 233 | strings.write_int(b, hidden) 234 | strings.write_string(b, ", ") 235 | } 236 | 237 | strings.write_string(b, "Deleted ") 238 | strings.write_int(b, deleted) 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /src/tfd/Makefile: -------------------------------------------------------------------------------- 1 | OS=$(shell uname) 2 | 3 | ifeq ($(OS), Darwin) 4 | all: darwin 5 | else 6 | all: unix 7 | endif 8 | 9 | unix: 10 | $(CC) -c -O2 -Os -fPIC main.c 11 | $(AR) rcs main.a main.o 12 | rm *.o 13 | 14 | darwin: 15 | $(CC) -arch x86_64 -c -O2 -Os -fPIC main.c -o main-x86_64.o -mmacosx-version-min=10.12 16 | $(CC) -arch arm64 -c -O2 -Os -fPIC main.c -o main-arm64.o -mmacosx-version-min=10.12 17 | lipo -create main-x86_64.o main-arm64.o -output main.a 18 | rm *.o 19 | -------------------------------------------------------------------------------- /src/tfd/build.bat: -------------------------------------------------------------------------------- 1 | cl -c main.c /MT && lib main.obj -------------------------------------------------------------------------------- /src/tfd/hello.c: -------------------------------------------------------------------------------- 1 | /*_________ 2 | / \ hello.c v3.8.8 [Apr 22, 2021] zlib licence 3 | |tiny file| Hello World file created [November 9, 2014] 4 | | dialogs | Copyright (c) 2014 - 2021 Guillaume Vareille http://ysengrin.com 5 | \____ ___/ http://tinyfiledialogs.sourceforge.net 6 | \| git clone http://git.code.sf.net/p/tinyfiledialogs/code tinyfd 7 | ____________________________________________ 8 | | | 9 | | email: tinyfiledialogs at ysengrin.com | 10 | |____________________________________________| 11 | _________________________________________________________________________________ 12 | | | 13 | | the windows only wchar_t UTF-16 prototypes are at the bottom of the header file | 14 | |_________________________________________________________________________________| 15 | _________________________________________________________ 16 | | | 17 | | on windows: - since v3.6 char is UTF-8 by default | 18 | | - if you want MBCS set tinyfd_winUtf8 to 0 | 19 | | - functions like fopen expect MBCS | 20 | |_________________________________________________________| 21 | 22 | If you like tinyfiledialogs, please upvote my stackoverflow answer 23 | https://stackoverflow.com/a/47651444 24 | 25 | - License - 26 | 27 | This software is provided 'as-is', without any express or implied 28 | warranty. In no event will the authors be held liable for any damages 29 | arising from the use of this software. 30 | 31 | including commercial applications, and to alter it and redistribute it 32 | freely, subject to the following restrictions: 33 | 34 | 1. The origin of this software must not be misrepresented; you must not 35 | claim that you wrote the original software. If you use this software 36 | in a product, an acknowledgment in the product documentation would be 37 | appreciated but is not required. 38 | 2. Altered source versions must be plainly marked as such, and must not be 39 | misrepresented as being the original software. 40 | 3. This notice may not be removed or altered from any source distribution. 41 | */ 42 | 43 | 44 | /* 45 | - Here is the Hello World: 46 | if a console is missing, it will use graphic dialogs 47 | if a graphical display is absent, it will use console dialogs 48 | (on windows the input box may take some time to open the first time) 49 | */ 50 | 51 | 52 | #include 53 | #include 54 | #include "tinyfiledialogs.h" 55 | 56 | #ifdef _MSC_VER 57 | #pragma warning(disable:4996) /* silences warnings about strcpy strcat fopen*/ 58 | #endif 59 | 60 | int main( int argc , char * argv[] ) 61 | { 62 | int lIntValue; 63 | char const * lPassword; 64 | char const * lTheSaveFileName; 65 | char const * lTheOpenFileName; 66 | char const * lTheSelectFolderName; 67 | char const * lTheHexColor; 68 | char const * lWillBeGraphicMode; 69 | unsigned char lRgbColor[3]; 70 | FILE * lIn; 71 | char lBuffer[1024]; 72 | char const * lFilterPatterns[2] = { "*.txt", "*.text" }; 73 | 74 | (void)argv; /*to silence stupid visual studio warning*/ 75 | 76 | tinyfd_verbose = argc - 1; /* default is 0 */ 77 | tinyfd_silent = 1; /* default is 1 */ 78 | 79 | tinyfd_forceConsole = 0; /* default is 0 */ 80 | tinyfd_assumeGraphicDisplay = 0; /* default is 0 */ 81 | 82 | #ifdef _WIN32 83 | tinyfd_winUtf8 = 1; /* default is 1 */ 84 | /* On windows, you decide if char holds 1:UTF-8(default) or 0:MBCS */ 85 | /* Windows is not ready to handle UTF-8 as many char functions like fopen() expect MBCS filenames.*/ 86 | /* This hello.c file has been prepared, on windows, to convert the filenames from UTF-8 to UTF-16 87 | and pass them passed to _wfopen() instead of fopen() */ 88 | #endif 89 | 90 | /*tinyfd_beep();*/ 91 | 92 | lWillBeGraphicMode = tinyfd_inputBox("tinyfd_query", NULL, NULL); 93 | 94 | strcpy(lBuffer, "tinyfiledialogs\nv"); 95 | strcat(lBuffer, tinyfd_version); 96 | if (lWillBeGraphicMode) 97 | { 98 | strcat(lBuffer, "\ngraphic mode: "); 99 | } 100 | else 101 | { 102 | strcat(lBuffer, "\nconsole mode: "); 103 | } 104 | strcat(lBuffer, tinyfd_response); 105 | tinyfd_messageBox("hello", lBuffer, "ok", "info", 0); 106 | 107 | tinyfd_notifyPopup("the title", "the message\n\tfrom outer-space", "info"); 108 | 109 | if ( lWillBeGraphicMode && ! tinyfd_forceConsole ) 110 | { 111 | #if 0 112 | lIntValue = tinyfd_messageBox("Hello World", "\ 113 | graphic dialogs [Yes]\n\ 114 | console mode [No]\n\ 115 | quit [Cancel]", 116 | "yesnocancel", "question", 1); 117 | if (!lIntValue) return 1; 118 | tinyfd_forceConsole = (lIntValue == 2); 119 | #else 120 | lIntValue = tinyfd_messageBox("Hello World", "graphic dialogs [Yes] / console mode [No]", "yesno", "question", 1); 121 | tinyfd_forceConsole = ! lIntValue; 122 | #endif 123 | } 124 | 125 | lPassword = tinyfd_inputBox( 126 | "a password box", "your password will be revealed later", NULL); 127 | 128 | if (!lPassword) return 1; 129 | 130 | tinyfd_messageBox("your password as read", lPassword, "ok", "info", 1); 131 | 132 | lTheSaveFileName = tinyfd_saveFileDialog( 133 | "let us save this password", 134 | "passwordFile.txt", 135 | 2, 136 | lFilterPatterns, 137 | NULL); 138 | 139 | if (! lTheSaveFileName) 140 | { 141 | tinyfd_messageBox( 142 | "Error", 143 | "Save file name is NULL", 144 | "ok", 145 | "error", 146 | 1); 147 | return 1 ; 148 | } 149 | 150 | #ifdef _WIN32 151 | if (tinyfd_winUtf8) 152 | lIn = _wfopen(tinyfd_utf8to16(lTheSaveFileName), L"w"); /* the UTF-8 filename is converted to UTF-16 to open the file*/ 153 | else 154 | #endif 155 | lIn = fopen(lTheSaveFileName, "w"); 156 | 157 | if (!lIn) 158 | { 159 | tinyfd_messageBox( 160 | "Error", 161 | "Can not open this file in write mode", 162 | "ok", 163 | "error", 164 | 1); 165 | return 1 ; 166 | } 167 | fputs(lPassword, lIn); 168 | fclose(lIn); 169 | 170 | lTheOpenFileName = tinyfd_openFileDialog( 171 | "let us read the password back", 172 | "", 173 | 2, 174 | lFilterPatterns, 175 | NULL, 176 | 0); 177 | 178 | if (! lTheOpenFileName) 179 | { 180 | tinyfd_messageBox( 181 | "Error", 182 | "Open file name is NULL", 183 | "ok", 184 | "error", 185 | 0); 186 | return 1 ; 187 | } 188 | 189 | #ifdef _WIN32 190 | if (tinyfd_winUtf8) 191 | lIn = _wfopen(tinyfd_utf8to16(lTheOpenFileName), L"r"); /* the UTF-8 filename is converted to UTF-16 */ 192 | else 193 | #endif 194 | lIn = fopen(lTheOpenFileName, "r"); 195 | 196 | if (!lIn) 197 | { 198 | tinyfd_messageBox( 199 | "Error", 200 | "Can not open this file in read mode", 201 | "ok", 202 | "error", 203 | 1); 204 | return(1); 205 | } 206 | 207 | lBuffer[0] = '\0'; 208 | fgets(lBuffer, sizeof(lBuffer), lIn); 209 | fclose(lIn); 210 | 211 | tinyfd_messageBox("your password as it was saved", lBuffer, "ok", "info", 1); 212 | 213 | lTheSelectFolderName = tinyfd_selectFolderDialog( 214 | "let us just select a directory", NULL); 215 | 216 | if (!lTheSelectFolderName) 217 | { 218 | tinyfd_messageBox( 219 | "Error", 220 | "Select folder name is NULL", 221 | "ok", 222 | "error", 223 | 1); 224 | return 1; 225 | } 226 | 227 | tinyfd_messageBox("The selected folder is", lTheSelectFolderName, "ok", "info", 1); 228 | 229 | lTheHexColor = tinyfd_colorChooser( 230 | "choose a nice color", 231 | "#FF0077", 232 | lRgbColor, 233 | lRgbColor); 234 | 235 | if (!lTheHexColor) 236 | { 237 | tinyfd_messageBox( 238 | "Error", 239 | "hexcolor is NULL", 240 | "ok", 241 | "error", 242 | 1); 243 | return 1; 244 | } 245 | 246 | tinyfd_messageBox("The selected hexcolor is", lTheHexColor, "ok", "info", 1); 247 | 248 | tinyfd_messageBox("your read password was", lPassword, "ok", "info", 1); 249 | 250 | tinyfd_beep(); 251 | 252 | return 0; 253 | } 254 | 255 | #ifdef _MSC_VER 256 | #pragma warning(default:4996) 257 | #endif 258 | 259 | /* 260 | OSX : 261 | $ clang -o hello.app hello.c tinyfiledialogs.c 262 | ( or gcc ) 263 | 264 | UNIX : 265 | $ gcc -o hello hello.c tinyfiledialogs.c 266 | ( or clang tcc owcc cc CC ) 267 | 268 | Windows : 269 | MinGW needs gcc >= v4.9 otherwise some headers are incomplete 270 | > gcc -o hello.exe hello.c tinyfiledialogs.c -LC:/mingw/lib -lcomdlg32 -lole32 271 | 272 | TinyCC needs >= v0.9.27 (+ tweaks - contact me) otherwise some headers are missing 273 | > tcc -o hello.exe hello.c tinyfiledialogs.c ^ 274 | -isystem C:\tcc\winapi-full-for-0.9.27\include\winapi ^ 275 | -lcomdlg32 -lole32 -luser32 -lshell32 276 | 277 | Borland C: > bcc32c -o hello.exe hello.c tinyfiledialogs.c 278 | OpenWatcom v2: create a character-mode executable project. 279 | 280 | VisualStudio : 281 | Create a console application project, 282 | it links against comdlg32.lib & ole32.lib. 283 | 284 | VisualStudio command line : 285 | > cl hello.c tinyfiledialogs.c comdlg32.lib ole32.lib user32.lib shell32.lib /W4 286 | */ 287 | -------------------------------------------------------------------------------- /src/tfd/main.a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Skytrias/todool/a913e71c064c6b721bdf517229c7c192471b3451/src/tfd/main.a -------------------------------------------------------------------------------- /src/tfd/main.c: -------------------------------------------------------------------------------- 1 | #include "tinyfiledialogs.c" 2 | #include "tinyfiledialogs.h" -------------------------------------------------------------------------------- /src/tfd/main.lib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Skytrias/todool/a913e71c064c6b721bdf517229c7c192471b3451/src/tfd/main.lib -------------------------------------------------------------------------------- /src/tfd/main.obj: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Skytrias/todool/a913e71c064c6b721bdf517229c7c192471b3451/src/tfd/main.obj -------------------------------------------------------------------------------- /src/tfd/tfd.odin: -------------------------------------------------------------------------------- 1 | package tfd 2 | 3 | import "core:c" 4 | 5 | when ODIN_OS == .Windows { 6 | foreign import lib { "main.lib", "system:user32.lib", "system:ole32.lib", "system:Comdlg32.lib", "system:shell32.lib" } 7 | } 8 | when ODIN_OS == .Linux || ODIN_OS == .Darwin { 9 | foreign import lib { "main.a" } 10 | } 11 | 12 | @(default_calling_convention="c", link_prefix="tinyfd_") 13 | foreign lib { 14 | saveFileDialog :: proc( 15 | title: cstring, // nil or "" 16 | defaultPathAndFile: cstring, // nil or "" 17 | numOfFilterPatterns: c.int, 18 | filterPatterns: [^]cstring, 19 | singleFilterDescription: cstring, // nil or "text files" 20 | ) -> cstring --- 21 | 22 | openFileDialog :: proc( 23 | title: cstring, // nil or "" 24 | defaultPathAndFile: cstring, // nil or "" 25 | numOfFilterPatterns: c.int, 26 | filterPatterns: [^]cstring, 27 | singleFilterDescription: cstring, // nil or "image files" 28 | allowMultipleSelects: c.int, // 0 / 1 29 | // in case of multiple files, the separator is | 30 | // returns NULL on cancel 31 | ) -> cstring --- 32 | } 33 | 34 | save_file_dialog :: proc( 35 | title: cstring, 36 | default_path_and_file: cstring, 37 | file_patterns: []cstring, 38 | single_filter_description: cstring = "", 39 | ) -> cstring { 40 | return saveFileDialog(title, default_path_and_file, i32(len(file_patterns)), auto_cast raw_data(file_patterns), single_filter_description) 41 | } 42 | 43 | open_file_dialog :: proc( 44 | title: cstring, 45 | default_path_and_file: cstring, 46 | file_patterns: []cstring, 47 | single_filter_description: cstring = "", 48 | allow_multiple_selectss: bool = false, 49 | ) -> cstring { 50 | return openFileDialog( 51 | title, 52 | default_path_and_file, 53 | i32(len(file_patterns)), 54 | auto_cast raw_data(file_patterns), 55 | single_filter_description, 56 | i32(allow_multiple_selectss), 57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /src/tfd/tinyfiledialogs.h: -------------------------------------------------------------------------------- 1 | /* If you are using a C++ compiler to compile tinyfiledialogs.c (maybe renamed with an extension ".cpp") 2 | then comment out << extern "C" >> bellow in this header file) */ 3 | 4 | /*_________ 5 | / \ tinyfiledialogs.h v3.8.8 [Apr 22, 2021] zlib licence 6 | |tiny file| Unique header file created [November 9, 2014] 7 | | dialogs | Copyright (c) 2014 - 2021 Guillaume Vareille http://ysengrin.com 8 | \____ ___/ http://tinyfiledialogs.sourceforge.net 9 | \| git clone http://git.code.sf.net/p/tinyfiledialogs/code tinyfd 10 | ____________________________________________ 11 | | | 12 | | email: tinyfiledialogs at ysengrin.com | 13 | |____________________________________________| 14 | ________________________________________________________________________________ 15 | | ____________________________________________________________________________ | 16 | | | | | 17 | | | on windows: | | 18 | | | - for UTF-16, use the wchar_t functions at the bottom of the header file | | 19 | | | - _wfopen() requires wchar_t | | 20 | | | | | 21 | | | - in tinyfiledialogs, char is UTF-8 by default (since v3.6) | | 22 | | | - but fopen() expects MBCS (not UTF-8) | | 23 | | | - if you want char to be MBCS: set tinyfd_winUtf8 to 0 | | 24 | | | | | 25 | | | - alternatively, tinyfiledialogs provides | | 26 | | | functions to convert between UTF-8, UTF-16 and MBCS | | 27 | | |____________________________________________________________________________| | 28 | |________________________________________________________________________________| 29 | 30 | If you like tinyfiledialogs, please upvote my stackoverflow answer 31 | https://stackoverflow.com/a/47651444 32 | 33 | - License - 34 | This software is provided 'as-is', without any express or implied 35 | warranty. In no event will the authors be held liable for any damages 36 | arising from the use of this software. 37 | Permission is granted to anyone to use this software for any purpose, 38 | including commercial applications, and to alter it and redistribute it 39 | freely, subject to the following restrictions: 40 | 1. The origin of this software must not be misrepresented; you must not 41 | claim that you wrote the original software. If you use this software 42 | in a product, an acknowledgment in the product documentation would be 43 | appreciated but is not required. 44 | 2. Altered source versions must be plainly marked as such, and must not be 45 | misrepresented as being the original software. 46 | 3. This notice may not be removed or altered from any source distribution. 47 | */ 48 | 49 | #ifndef TINYFILEDIALOGS_H 50 | #define TINYFILEDIALOGS_H 51 | 52 | #ifdef __cplusplus 53 | /* if tinydialogs.c is compiled as C++ code rather than C code, you may need to comment this out 54 | and the corresponding closing bracket near the end of this file. */ 55 | extern "C" { 56 | #endif 57 | 58 | /******************************************************************************************************/ 59 | /**************************************** UTF-8 on Windows ********************************************/ 60 | /******************************************************************************************************/ 61 | #ifdef _WIN32 62 | /* On windows, if you want to use UTF-8 ( instead of the UTF-16/wchar_t functions at the end of this file ) 63 | Make sure your code is really prepared for UTF-8 (on windows, functions like fopen() expect MBCS and not UTF-8) */ 64 | extern int tinyfd_winUtf8; /* on windows char strings can be 1:UTF-8(default) or 0:MBCS */ 65 | /* for MBCS change this to 0, in tinyfiledialogs.c or in your code */ 66 | 67 | /* Here are some functions to help you convert between UTF-16 UTF-8 MBSC */ 68 | char * tinyfd_utf8toMbcs(char const * aUtf8string); 69 | char * tinyfd_utf16toMbcs(wchar_t const * aUtf16string); 70 | wchar_t * tinyfd_mbcsTo16(char const * aMbcsString); 71 | char * tinyfd_mbcsTo8(char const * aMbcsString); 72 | wchar_t * tinyfd_utf8to16(char const * aUtf8string); 73 | char * tinyfd_utf16to8(wchar_t const * aUtf16string); 74 | #endif 75 | /******************************************************************************************************/ 76 | /******************************************************************************************************/ 77 | /******************************************************************************************************/ 78 | 79 | /************* 3 funtions for C# (you don't need this in C or C++) : */ 80 | char const * tinyfd_getGlobalChar(char const * aCharVariableName); /* returns NULL on error */ 81 | int tinyfd_getGlobalInt(char const * aIntVariableName); /* returns -1 on error */ 82 | int tinyfd_setGlobalInt(char const * aIntVariableName, int aValue); /* returns -1 on error */ 83 | /* aCharVariableName: "tinyfd_version" "tinyfd_needs" "tinyfd_response" 84 | aIntVariableName : "tinyfd_verbose" "tinyfd_silent" "tinyfd_allowCursesDialogs" 85 | "tinyfd_forceConsole" "tinyfd_assumeGraphicDisplay" "tinyfd_winUtf8" 86 | **************/ 87 | 88 | 89 | extern char tinyfd_version[8]; /* contains tinyfd current version number */ 90 | extern char tinyfd_needs[]; /* info about requirements */ 91 | extern int tinyfd_verbose; /* 0 (default) or 1 : on unix, prints the command line calls */ 92 | extern int tinyfd_silent; /* 1 (default) or 0 : on unix, hide errors and warnings from called dialogs */ 93 | 94 | /* Curses dialogs are difficult to use, on windows they are only ascii and uses the unix backslah */ 95 | extern int tinyfd_allowCursesDialogs; /* 0 (default) or 1 */ 96 | 97 | extern int tinyfd_forceConsole; /* 0 (default) or 1 */ 98 | /* for unix & windows: 0 (graphic mode) or 1 (console mode). 99 | 0: try to use a graphic solution, if it fails then it uses console mode. 100 | 1: forces all dialogs into console mode even when an X server is present, 101 | it can use the package dialog or dialog.exe. 102 | on windows it only make sense for console applications */ 103 | 104 | extern int tinyfd_assumeGraphicDisplay; /* 0 (default) or 1 */ 105 | /* some systems don't set the environment variable DISPLAY even when a graphic display is present. 106 | set this to 1 to tell tinyfiledialogs to assume the existence of a graphic display */ 107 | 108 | extern char tinyfd_response[1024]; 109 | /* if you pass "tinyfd_query" as aTitle, 110 | the functions will not display the dialogs 111 | but will return 0 for console mode, 1 for graphic mode. 112 | tinyfd_response is then filled with the retain solution. 113 | possible values for tinyfd_response are (all lowercase) 114 | for graphic mode: 115 | windows_wchar windows applescript kdialog zenity zenity3 matedialog 116 | shellementary qarma yad python2-tkinter python3-tkinter python-dbus 117 | perl-dbus gxmessage gmessage xmessage xdialog gdialog 118 | for console mode: 119 | dialog whiptail basicinput no_solution */ 120 | 121 | void tinyfd_beep(void); 122 | 123 | int tinyfd_notifyPopup( 124 | char const * aTitle, /* NULL or "" */ 125 | char const * aMessage, /* NULL or "" may contain \n \t */ 126 | char const * aIconType); /* "info" "warning" "error" */ 127 | /* return has only meaning for tinyfd_query */ 128 | 129 | int tinyfd_messageBox( 130 | char const * aTitle , /* NULL or "" */ 131 | char const * aMessage , /* NULL or "" may contain \n \t */ 132 | char const * aDialogType , /* "ok" "okcancel" "yesno" "yesnocancel" */ 133 | char const * aIconType , /* "info" "warning" "error" "question" */ 134 | int aDefaultButton ) ; 135 | /* 0 for cancel/no , 1 for ok/yes , 2 for no in yesnocancel */ 136 | 137 | char * tinyfd_inputBox( 138 | char const * aTitle , /* NULL or "" */ 139 | char const * aMessage , /* NULL or "" (\n and \t have no effect) */ 140 | char const * aDefaultInput ) ; /* NULL passwordBox, "" inputbox */ 141 | /* returns NULL on cancel */ 142 | 143 | char * tinyfd_saveFileDialog( 144 | char const * aTitle , /* NULL or "" */ 145 | char const * aDefaultPathAndFile , /* NULL or "" */ 146 | int aNumOfFilterPatterns , /* 0 (1 in the following example) */ 147 | char const * const * aFilterPatterns , /* NULL or char const * lFilterPatterns[1]={"*.txt"} */ 148 | char const * aSingleFilterDescription ) ; /* NULL or "text files" */ 149 | /* returns NULL on cancel */ 150 | 151 | char * tinyfd_openFileDialog( 152 | char const * aTitle, /* NULL or "" */ 153 | char const * aDefaultPathAndFile, /* NULL or "" */ 154 | int aNumOfFilterPatterns , /* 0 (2 in the following example) */ 155 | char const * const * aFilterPatterns, /* NULL or char const * lFilterPatterns[2]={"*.png","*.jpg"}; */ 156 | char const * aSingleFilterDescription, /* NULL or "image files" */ 157 | int aAllowMultipleSelects ) ; /* 0 or 1 */ 158 | /* in case of multiple files, the separator is | */ 159 | /* returns NULL on cancel */ 160 | 161 | char * tinyfd_selectFolderDialog( 162 | char const * aTitle, /* NULL or "" */ 163 | char const * aDefaultPath); /* NULL or "" */ 164 | /* returns NULL on cancel */ 165 | 166 | char * tinyfd_colorChooser( 167 | char const * aTitle, /* NULL or "" */ 168 | char const * aDefaultHexRGB, /* NULL or "#FF0000" */ 169 | unsigned char const aDefaultRGB[3] , /* unsigned char lDefaultRGB[3] = { 0 , 128 , 255 }; */ 170 | unsigned char aoResultRGB[3] ) ; /* unsigned char lResultRGB[3]; */ 171 | /* returns the hexcolor as a string "#FF0000" */ 172 | /* aoResultRGB also contains the result */ 173 | /* aDefaultRGB is used only if aDefaultHexRGB is NULL */ 174 | /* aDefaultRGB and aoResultRGB can be the same array */ 175 | /* returns NULL on cancel */ 176 | 177 | 178 | /************ WINDOWS ONLY SECTION ************************/ 179 | #ifdef _WIN32 180 | 181 | /* windows only - utf-16 version */ 182 | int tinyfd_notifyPopupW( 183 | wchar_t const * aTitle, /* NULL or L"" */ 184 | wchar_t const * aMessage, /* NULL or L"" may contain \n \t */ 185 | wchar_t const * aIconType); /* L"info" L"warning" L"error" */ 186 | 187 | /* windows only - utf-16 version */ 188 | int tinyfd_messageBoxW( 189 | wchar_t const * aTitle, /* NULL or L"" */ 190 | wchar_t const * aMessage, /* NULL or L"" may contain \n \t */ 191 | wchar_t const * aDialogType, /* L"ok" L"okcancel" L"yesno" */ 192 | wchar_t const * aIconType, /* L"info" L"warning" L"error" L"question" */ 193 | int aDefaultButton ); /* 0 for cancel/no , 1 for ok/yes */ 194 | /* returns 0 for cancel/no , 1 for ok/yes */ 195 | 196 | /* windows only - utf-16 version */ 197 | wchar_t * tinyfd_inputBoxW( 198 | wchar_t const * aTitle, /* NULL or L"" */ 199 | wchar_t const * aMessage, /* NULL or L"" (\n nor \t not respected) */ 200 | wchar_t const * aDefaultInput); /* NULL passwordBox, L"" inputbox */ 201 | 202 | /* windows only - utf-16 version */ 203 | wchar_t * tinyfd_saveFileDialogW( 204 | wchar_t const * aTitle, /* NULL or L"" */ 205 | wchar_t const * aDefaultPathAndFile, /* NULL or L"" */ 206 | int aNumOfFilterPatterns, /* 0 (1 in the following example) */ 207 | wchar_t const * const * aFilterPatterns, /* NULL or wchar_t const * lFilterPatterns[1]={L"*.txt"} */ 208 | wchar_t const * aSingleFilterDescription); /* NULL or L"text files" */ 209 | /* returns NULL on cancel */ 210 | 211 | /* windows only - utf-16 version */ 212 | wchar_t * tinyfd_openFileDialogW( 213 | wchar_t const * aTitle, /* NULL or L"" */ 214 | wchar_t const * aDefaultPathAndFile, /* NULL or L"" */ 215 | int aNumOfFilterPatterns , /* 0 (2 in the following example) */ 216 | wchar_t const * const * aFilterPatterns, /* NULL or wchar_t const * lFilterPatterns[2]={L"*.png","*.jpg"} */ 217 | wchar_t const * aSingleFilterDescription, /* NULL or L"image files" */ 218 | int aAllowMultipleSelects ) ; /* 0 or 1 */ 219 | /* in case of multiple files, the separator is | */ 220 | /* returns NULL on cancel */ 221 | 222 | /* windows only - utf-16 version */ 223 | wchar_t * tinyfd_selectFolderDialogW( 224 | wchar_t const * aTitle, /* NULL or L"" */ 225 | wchar_t const * aDefaultPath); /* NULL or L"" */ 226 | /* returns NULL on cancel */ 227 | 228 | /* windows only - utf-16 version */ 229 | wchar_t * tinyfd_colorChooserW( 230 | wchar_t const * aTitle, /* NULL or L"" */ 231 | wchar_t const * aDefaultHexRGB, /* NULL or L"#FF0000" */ 232 | unsigned char const aDefaultRGB[3], /* unsigned char lDefaultRGB[3] = { 0 , 128 , 255 }; */ 233 | unsigned char aoResultRGB[3]); /* unsigned char lResultRGB[3]; */ 234 | /* returns the hexcolor as a string L"#FF0000" */ 235 | /* aoResultRGB also contains the result */ 236 | /* aDefaultRGB is used only if aDefaultHexRGB is NULL */ 237 | /* aDefaultRGB and aoResultRGB can be the same array */ 238 | /* returns NULL on cancel */ 239 | 240 | #endif /*_WIN32 */ 241 | 242 | #ifdef __cplusplus 243 | } /*extern "C"*/ 244 | #endif 245 | 246 | #endif /* TINYFILEDIALOGS_H */ 247 | 248 | /* 249 | ________________________________________________________________________________ 250 | | ____________________________________________________________________________ | 251 | | | | | 252 | | | on windows: | | 253 | | | - for UTF-16, use the wchar_t functions at the bottom of the header file | | 254 | | | - _wfopen() requires wchar_t | | 255 | | | | | 256 | | | - in tinyfiledialogs, char is UTF-8 by default (since v3.6) | | 257 | | | - but fopen() expects MBCS (not UTF-8) | | 258 | | | - if you want char to be MBCS: set tinyfd_winUtf8 to 0 | | 259 | | | | | 260 | | | - alternatively, tinyfiledialogs provides | | 261 | | | functions to convert between UTF-8, UTF-16 and MBCS | | 262 | | |____________________________________________________________________________| | 263 | |________________________________________________________________________________| 264 | 265 | - This is not for ios nor android (it works in termux though). 266 | - The files can be renamed with extension ".cpp" as the code is 100% compatible C C++ 267 | (just comment out << extern "C" >> in the header file) 268 | - Windows is fully supported from XP to 10 (maybe even older versions) 269 | - C# & LUA via dll, see files in the folder EXTRAS 270 | - OSX supported from 10.4 to latest (maybe even older versions) 271 | - Do not use " and ' as the dialogs will be displayed with a warning 272 | instead of the title, message, etc... 273 | - There's one file filter only, it may contain several patterns. 274 | - If no filter description is provided, 275 | the list of patterns will become the description. 276 | - On windows link against Comdlg32.lib and Ole32.lib 277 | (on windows the no linking claim is a lie) 278 | - On unix: it tries command line calls, so no such need (NO LINKING). 279 | - On unix you need one of the following: 280 | applescript, kdialog, zenity, matedialog, shellementary, qarma, yad, 281 | python (2 or 3)/tkinter/python-dbus (optional), Xdialog 282 | or curses dialogs (opens terminal if running without console). 283 | - One of those is already included on most (if not all) desktops. 284 | - In the absence of those it will use gdialog, gxmessage or whiptail 285 | with a textinputbox. If nothing is found, it switches to basic console input, 286 | it opens a console if needed (requires xterm + bash). 287 | - for curses dialogs you must set tinyfd_allowCursesDialogs=1 288 | - You can query the type of dialog that will be used (pass "tinyfd_query" as aTitle) 289 | - String memory is preallocated statically for all the returned values. 290 | - File and path names are tested before return, they should be valid. 291 | - tinyfd_forceConsole=1; at run time, forces dialogs into console mode. 292 | - On windows, console mode only make sense for console applications. 293 | - On windows, console mode is not implemented for wchar_T UTF-16. 294 | - Mutiple selects are not possible in console mode. 295 | - The package dialog must be installed to run in curses dialogs in console mode. 296 | It is already installed on most unix systems. 297 | - On osx, the package dialog can be installed via 298 | http://macappstore.org/dialog or http://macports.org 299 | - On windows, for curses dialogs console mode, 300 | dialog.exe should be copied somewhere on your executable path. 301 | It can be found at the bottom of the following page: 302 | http://andrear.altervista.org/home/cdialog.php 303 | */ 304 | -------------------------------------------------------------------------------- /src/undo.odin: -------------------------------------------------------------------------------- 1 | package src 2 | 3 | import "base:runtime" 4 | import "core:fmt" 5 | import "core:log" 6 | import "core:mem" 7 | 8 | // TODO multiple managers and 1 active only 9 | 10 | Undo_State :: enum { 11 | Normal, 12 | Undoing, 13 | Redoing, 14 | } 15 | 16 | Undo_Manager :: struct { 17 | undo: [dynamic]byte, 18 | redo: [dynamic]byte, 19 | state: Undo_State, 20 | } 21 | 22 | undo_manager_init :: proc(manager: ^Undo_Manager, cap: int = mem.Kilobyte * 10) { 23 | manager.undo = make([dynamic]byte, 0, cap) 24 | manager.redo = make([dynamic]byte, 0, cap) 25 | } 26 | 27 | undo_manager_reset :: proc(manager: ^Undo_Manager) { 28 | clear(&manager.undo) 29 | clear(&manager.redo) 30 | manager.state = .Normal 31 | } 32 | 33 | undo_manager_destroy :: proc(manager: ^Undo_Manager) { 34 | delete(manager.undo) 35 | delete(manager.redo) 36 | } 37 | 38 | // callback type 39 | Undo_Callback :: proc(manager: ^Undo_Manager, item: rawptr) 40 | 41 | // footer used to describe the item region coming before this footer in bytes 42 | Undo_Item_Footer :: struct { 43 | callback: Undo_Callback, // callback that will be called on invoke 44 | byte_count: int, // item byte count 45 | group_end: bool, // wether this item means the end of undo steps 46 | } 47 | 48 | // push an item with its size and a callback 49 | undo_push :: proc( 50 | manager: ^Undo_Manager, 51 | callback: Undo_Callback, 52 | item: rawptr, 53 | item_bytes: int, 54 | ) -> []byte { 55 | stack := manager.state == .Undoing ? &manager.redo : &manager.undo 56 | footer := Undo_Item_Footer { 57 | callback = callback, 58 | // TODO align? 59 | byte_count = item_bytes, 60 | } 61 | 62 | // copy item content and footer 63 | old_length := len(stack) 64 | resize(stack, old_length + item_bytes + size_of(Undo_Item_Footer)) 65 | root := uintptr(raw_data(stack^)) + uintptr(old_length) 66 | mem.copy(rawptr(root), item, item_bytes) 67 | mem.copy(rawptr(root + uintptr(item_bytes)), &footer, size_of(Undo_Item_Footer)) 68 | 69 | if manager.state == .Normal { 70 | clear(&manager.redo) 71 | } 72 | 73 | return mem.slice_ptr(cast(^byte) root, item_bytes) 74 | } 75 | 76 | // set group_end to true 77 | undo_group_end :: proc(manager: ^Undo_Manager) -> bool { 78 | assert(manager.state == .Normal) 79 | stack := &manager.undo 80 | 81 | if len(stack) == 0 { 82 | return false 83 | } 84 | 85 | footer := cast(^Undo_Item_Footer) &stack[len(stack) - size_of(Undo_Item_Footer)] 86 | footer.group_end = true 87 | return true 88 | } 89 | 90 | // set group_end to false 91 | undo_group_continue :: proc(manager: ^Undo_Manager) -> bool { 92 | if manager == nil { 93 | return false 94 | } 95 | 96 | assert(manager.state == .Normal) 97 | stack := &manager.undo 98 | 99 | if len(stack) == 0 { 100 | return false 101 | } 102 | 103 | footer := cast(^Undo_Item_Footer) &stack[len(stack) - size_of(Undo_Item_Footer)] 104 | footer.group_end = false 105 | return true 106 | } 107 | 108 | // check if undo / redo is empty 109 | undo_is_empty :: proc(manager: ^Undo_Manager, redo: bool) -> bool { 110 | stack := redo ? &manager.redo : &manager.undo 111 | return len(stack) == 0 112 | } 113 | 114 | // invoke the undo / redo action 115 | undo_invoke :: proc(manager: ^Undo_Manager, redo: bool) { 116 | assert(manager.state == .Normal) 117 | manager.state = redo ? .Redoing : .Undoing 118 | stack := redo ? &manager.redo : &manager.undo 119 | assert(len(stack) != 0) 120 | 121 | first := true 122 | count: int 123 | for len(stack) != 0 { 124 | old_length := len(stack) 125 | footer := cast(^Undo_Item_Footer) &stack[old_length - size_of(Undo_Item_Footer)] 126 | 127 | if !first && footer.group_end { 128 | break 129 | } 130 | first = false 131 | 132 | item_root := &stack[old_length - footer.byte_count - size_of(Undo_Item_Footer)] 133 | footer.callback(manager, item_root) 134 | resize(stack, old_length - footer.byte_count - size_of(Undo_Item_Footer)) 135 | count += 1 136 | } 137 | 138 | // set oposite stack latest footer to group_end = true 139 | { 140 | stack := redo ? &manager.undo : &manager.redo 141 | assert(len(stack) != 0) 142 | footer := cast(^Undo_Item_Footer) &stack[len(stack) - size_of(Undo_Item_Footer)] 143 | footer.group_end = true 144 | } 145 | 146 | manager.state = .Normal 147 | } 148 | 149 | undo_is_in_undo :: #force_inline proc(manager: ^Undo_Manager) -> bool { 150 | return manager.state != .Normal 151 | } 152 | 153 | undo_is_in_normal :: #force_inline proc(manager: ^Undo_Manager) -> bool { 154 | return manager.state == .Normal 155 | } 156 | 157 | // // peek the latest undo step in the queue 158 | // undo_peek :: proc(manager: ^Undo_Manager) -> ( 159 | // callback: Undo_Callback, 160 | // item: rawptr, 161 | // ok: bool, 162 | // ) { 163 | // stack := manager.state == .Undoing ? &manager.redo : &manager.undo 164 | // if len(stack) == 0 { 165 | // return 166 | // } 167 | 168 | // footer := cast(^Undo_Item_Footer) &stack[len(stack) - size_of(Undo_Item_Footer)] 169 | // callback = footer.callback 170 | // item = &stack[len(stack) - size_of(Undo_Item_Footer) - footer.byte_count] 171 | // ok = true 172 | // return 173 | // } -------------------------------------------------------------------------------- /src/wrap.odin: -------------------------------------------------------------------------------- 1 | package src 2 | 3 | import "core:unicode" 4 | import "core:fmt" 5 | import "vendor:fontstash" 6 | import "cutf8" 7 | 8 | ////////////////////////////////////////////// 9 | // line wrapping helpers 10 | ////////////////////////////////////////////// 11 | 12 | // wrap a string to a width limit where the result are the strings seperated to the width limit 13 | wrap_format_to_lines :: proc( 14 | ctx: ^fontstash.FontContext, 15 | text: string, 16 | width_limit: f32, 17 | lines: ^[dynamic]string, 18 | ) { 19 | // clear(lines) 20 | iter := fontstash.TextIterInit(ctx, 0, 0, text) 21 | q: fontstash.Quad 22 | last_byte_offset: int 23 | 24 | index_last: int 25 | index_line_start: int 26 | index_word_start: int = -1 27 | 28 | // widths 29 | width_codepoint: f32 30 | width_word: f32 31 | width_line: f32 32 | 33 | for fontstash.TextIterNext(ctx, &iter, &q) { 34 | width_codepoint = iter.nextx - iter.x 35 | 36 | // set first valid index 37 | if index_word_start == -1 { 38 | index_word_start = last_byte_offset 39 | } 40 | 41 | // set the word index, reset width 42 | if index_word_start != -1 && iter.codepoint == ' ' { 43 | index_word_start = -1 44 | width_word = 0 45 | } 46 | 47 | // add widths 48 | width_line += width_codepoint 49 | width_word += width_codepoint 50 | 51 | if width_line > width_limit { 52 | if !unicode.is_space(iter.codepoint) { 53 | // when full word is longer than limit, just seperate like whitespace 54 | if width_word > width_limit { 55 | append(lines, text[index_line_start:iter.str]) 56 | index_line_start = iter.str 57 | width_line = 0 58 | } else { 59 | append(lines, text[index_line_start:index_word_start]) 60 | index_line_start = index_word_start 61 | width_line = width_word 62 | } 63 | } else { 64 | append(lines, text[index_line_start:iter.str]) 65 | index_line_start = iter.str 66 | width_line = width_word 67 | } 68 | } 69 | 70 | index_last = last_byte_offset 71 | last_byte_offset = iter.str 72 | } 73 | 74 | if width_line <= width_limit { 75 | append(lines, text[index_line_start:]) 76 | } 77 | } 78 | 79 | // getting the right index into the now cut lines of strings 80 | wrap_codepoint_index_to_line :: proc( 81 | lines: []string, 82 | codepoint_index: int, 83 | loc := #caller_location, 84 | ) -> ( 85 | y: int, 86 | line_byte_offset: int, 87 | line_codepoint_index: int, 88 | ) { 89 | assert(len(lines) > 0, "Lines should have valid content of lines > 0", loc) 90 | 91 | if codepoint_index == 0 || len(lines) <= 1 { 92 | return 93 | } 94 | 95 | // need to care about utf8 sized codepoints 96 | total_byte_offset: int 97 | last_byte_offset: int 98 | total_codepoint_count: int 99 | last_codepoint_count: int 100 | 101 | for line, i in lines { 102 | codepoint_count := cutf8.count(line) 103 | 104 | if codepoint_index < total_codepoint_count + codepoint_count { 105 | y = i 106 | line_byte_offset = total_byte_offset 107 | line_codepoint_index = total_codepoint_count 108 | return 109 | } 110 | 111 | last_codepoint_count = total_codepoint_count 112 | total_codepoint_count += codepoint_count 113 | last_byte_offset = total_byte_offset 114 | total_byte_offset += len(line) 115 | } 116 | 117 | // last line 118 | line_byte_offset = last_byte_offset 119 | line_codepoint_index = last_codepoint_count 120 | y = len(lines) - 1 121 | 122 | return 123 | } 124 | 125 | // returns the logical position of a caret without offsetting 126 | // can be used on single lines of text or wrapped lines of text 127 | wrap_layout_caret :: proc( 128 | ctx: ^fontstash.FontContext, 129 | wrapped_lines: []string, // using the resultant lines 130 | codepoint_index: int, // in codepoint_index, not byte_offset 131 | loc := #caller_location, 132 | ) -> (x_offset: int, line: int) { 133 | assert(len(wrapped_lines) > 0, "Lines should have valid content of lines > 0", loc) 134 | 135 | // get wanted line and byte index offset 136 | y, byte_offset, codepoint_offset := wrap_codepoint_index_to_line( 137 | wrapped_lines, 138 | codepoint_index, 139 | ) 140 | line = y 141 | 142 | q: fontstash.Quad 143 | text := wrapped_lines[line] 144 | goal := codepoint_index - codepoint_offset 145 | iter := fontstash.TextIterInit(ctx, 0, 0, text) 146 | 147 | // still till the goal position is reached and use x position 148 | for fontstash.TextIterNext(ctx, &iter, &q) { 149 | if iter.codepointCount >= goal { 150 | break 151 | } 152 | } 153 | 154 | // anything hitting the count 155 | if goal == iter.codepointCount { 156 | x_offset = int(iter.nextx) 157 | } else { 158 | // get the first index 159 | x_offset = int(iter.x) 160 | } 161 | 162 | return 163 | } 164 | 165 | // line wrapping iteration when you want to do things like selection highlighting 166 | // which could span across multiple lines 167 | Wrap_State :: struct { 168 | // font options 169 | font: ^Font, 170 | isize: i16, 171 | iblur: i16, 172 | scale: f32, 173 | spacing: f32, 174 | 175 | // formatted lines 176 | lines: []string, 177 | 178 | // wanted from / to 179 | codepoint_offset: int, 180 | codepoint_index_low: int, 181 | codepoint_index_high: int, 182 | 183 | // output used 184 | x_from: f32, 185 | x_to: f32, 186 | y: int, // always +1 in lines 187 | } 188 | 189 | wrap_state_init :: proc( 190 | ctx: ^fontstash.FontContext, 191 | lines: []string, 192 | codepoint_index_from: int, 193 | codepoint_index_to: int, 194 | ) -> (res: Wrap_State) { 195 | state := fontstash.__getState(ctx) 196 | res.font = fontstash.__getFont(ctx, state.font) 197 | res.isize = i16(state.size * 10) 198 | res.iblur = i16(state.blur) 199 | res.scale = fontstash.__getPixelHeightScale(res.font, f32(res.isize / 10)) 200 | res.lines = lines 201 | res.spacing = state.spacing 202 | 203 | // do min / max here for safety instead of reyling on the user 204 | res.codepoint_index_low = min(codepoint_index_from, codepoint_index_to) 205 | res.codepoint_index_high = max(codepoint_index_from, codepoint_index_to) 206 | 207 | return 208 | } 209 | 210 | wrap_state_iter :: proc( 211 | ctx: ^fontstash.FontContext, 212 | w: ^Wrap_State, 213 | ) -> bool { 214 | w.x_from = -1 215 | q: fontstash.Quad 216 | 217 | // NOTE could be optimized to only search the wanted lines 218 | // would need to count the glyphs anyway though hmm 219 | 220 | for w.x_from == -1 && w.y < len(w.lines) { 221 | line := w.lines[w.y] 222 | ds: cutf8.Decode_State 223 | previous_glyph_index: fontstash.Glyph_Index = -1 224 | temp_x: f32 225 | temp_y: f32 226 | 227 | // step through each line to find selection area 228 | for codepoint, codepoint_index in cutf8.ds_iter(&ds, line) { 229 | glyph, ok := fontstash.__getGlyph(ctx, w.font, codepoint, w.isize, w.iblur) 230 | index := w.codepoint_offset + codepoint_index 231 | old := temp_x 232 | 233 | if glyph != nil { 234 | fontstash.__getQuad(ctx, w.font, previous_glyph_index, glyph, w.scale, w.spacing, &temp_x, &temp_y, &q) 235 | } 236 | 237 | if w.codepoint_index_low <= index && index < w.codepoint_index_high { 238 | w.x_to = temp_x 239 | 240 | if w.x_from == -1 { 241 | w.x_from = old 242 | } 243 | } 244 | 245 | previous_glyph_index = glyph == nil ? -1 : glyph.index 246 | } 247 | 248 | w.y += 1 249 | w.codepoint_offset += ds.codepoint_count 250 | } 251 | 252 | return w.x_from != -1 253 | } --------------------------------------------------------------------------------