From 1704e2a619f31b3114205f0d11e14a452c0a8b9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Mon, 5 Oct 2026 12:04:30 -0400 Subject: [PATCH] Web mirror, license, notices, README, compat --- .gitignore | 1 + LICENSE | 21 +++ README.md | 86 ++++++++++ THIRD_PARTY_NOTICES.md | 62 +++++++ docs/compat.md | 25 +++ examples/gallery/main.go | 9 +- examples/gallery/web.go | 284 ++++++++++++++++++++++++++++++++ examples/gallery/web/index.html | 178 ++++++++++++++++++++ examples/gallery/web_keys.go | 138 ++++++++++++++++ 9 files changed, 803 insertions(+), 1 deletion(-) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 THIRD_PARTY_NOTICES.md create mode 100644 docs/compat.md create mode 100644 examples/gallery/web.go create mode 100644 examples/gallery/web/index.html create mode 100644 examples/gallery/web_keys.go diff --git a/.gitignore b/.gitignore index 0fe7656..44041fd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.got.png .DS_Store /gallery +/.claude/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b332054 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 MujicaUI contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3746349 --- /dev/null +++ b/README.md @@ -0,0 +1,86 @@ +# MujicaUI + +宫廷哥特风格的 Go 桌面组件库,建立在 [MyGo](https://mygo.egoist.dev/docs/ui) 原生 UI 之上。象牙白与炭黑底色,酒红主色,旧金装饰线,衬线标题。100 个组件,浅深两套主题,三种密度。 + +界面由 MyGo 在 GPU 上自绘,不使用 HTML、JavaScript 或 WebView。 + +## 安装 + +模块路径暂为 `mujicaui`,尚未发布到代码托管地址。本地使用: + +```sh +go mod edit -require=mujicaui@v0.0.0 -replace=mujicaui=/path/to/MujicaUI +``` + +要求 Go 1.27.1 与 MyGo v0.2.10(见 [兼容表](docs/compat.md))。 + +## 最小示例 + +```go +package main + +import ( + "log" + + "github.com/egoist/mygo" + "github.com/egoist/mygo/ui" + + mujica "mujicaui" +) + +type app struct{ name string } + +func (a *app) view(c *ui.Context) { + mujica.Use(c, mujica.Settings{Locale: mujica.ZhCN}) + ui.Column(c).Fill().Center().Gap(12).Children(func() { + mujica.TextInput(c, &a.name, mujica.TextInputOptions{Placeholder: "姓名"}) + if mujica.Button(c, "觐见", mujica.ButtonOptions{}).Clicked() { + log.Println("hello", a.name) + } + }) +} + +func main() { + a := &app{} + mygo.App.WhenReady(func() { + mygo.NewWindow(mygo.WindowOptions{Title: "Mujica", Width: 480, Height: 320, Content: ui.View(a.view)}) + }) + if err := mygo.App.Run(); err != nil { + log.Fatal(err) + } +} +``` + +## 主题 + +每帧开头调用 `mujica.Use(c, settings)`。`Settings` 决定模式(`System`、`Light`、`Dark`)、密度(`theme.Compact`、`theme.Default`、`theme.Comfortable`)、语言与文案覆盖。每个窗口各自设置,切换不重置业务状态。 + +```go +mujica.Use(c, mujica.Settings{ + Mode: mujica.Dark, + Density: theme.Compact, + Locale: mujica.ZhCN, +}) +k := mujica.Tokens(c) // 当前窗口的颜色 +``` + +覆盖顺序:组件参数,窗口 `Settings`,库默认值。系统高对比度、文本缩放、减少动画始终生效。 + +## Gallery + +```sh +go run ./examples/gallery # 原生窗口 +go run ./examples/gallery -web :8080 # 浏览器镜像 +``` + +浏览器镜像不在浏览器里运行组件。Go 进程用 `ui.NewTester` 驱动同一个 Gallery 视图,把帧推给页面,再把指针、键盘、输入法和剪贴板事件送回 Go。 + +## 文档 + +- [组件文档](docs/components/):每个组件一页。 +- [兼容与平台依赖](docs/compat.md) +- [上游归并](docs/upstream.md) + +## 许可 + +MIT。第三方声明见 [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)。 diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md new file mode 100644 index 0000000..48da452 --- /dev/null +++ b/THIRD_PARTY_NOTICES.md @@ -0,0 +1,62 @@ +# Third-party notices + +## Lucide icons 1.52.0 (icons/svg) + +Copied unmodified except for the removed license comment line. Source: https://lucide.dev + +```text +ISC License + +Copyright (c) 2026 Lucide Icons and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +--- + +The following Lucide icons are derived from the Feather project: + +airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out + +The MIT License (MIT) (for the icons listed above) + +Copyright (c) 2013-present Cole Bemis + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` + +## Go module dependencies + +Linked, not copied. Licenses ship with each module. + +| Module | Version | License | +| --- | --- | --- | +| github.com/egoist/mygo | v0.2.10 | MIT | +| github.com/ebitengine/purego | v0.11.1 | Apache-2.0 | +| github.com/go-text/typesetting | v0.3.5 | BSD-3-Clause OR Unlicense | +| golang.org/x/image | v0.46.0 | BSD-3-Clause | diff --git a/docs/compat.md b/docs/compat.md new file mode 100644 index 0000000..44e807e --- /dev/null +++ b/docs/compat.md @@ -0,0 +1,25 @@ +# 兼容与平台依赖 + +## 版本 + +| 项 | 值 | +| --- | --- | +| Go | 1.27.1 及以上(MyGo v0.2.10 的 go.mod 要求) | +| MyGo | v0.2.10,commit 459b512,固定在 go.mod 与 go.sum | +| 运行时依赖 | macOS:系统框架。Windows:无需 WebView2。Linux:GTK 3(MyGo 通过 GtkGLArea 绘制) | + +## 已实测环境 + +| 系统 | 架构 | 会话 | 结果 | +| --- | --- | --- | --- | +| macOS(Darwin 27.2) | arm64 | Aqua | 无窗口测试、快照、Gallery 构建 | + +以下项目尚未实测,不宣称支持:Windows(x64、arm64)、Linux X11、原生 Wayland、XWayland;VoiceOver、Narrator、NVDA、Orca;跨缩放显示器移动窗口;100% 以外的真机显示缩放。无窗口测试已覆盖 1x 与 2x 渲染,不替代真机。 + +## 快照基准 + +快照按系统分目录:`testdata/snapshots//`。目前只有 `darwin` 基准。其他平台须在该平台运行 `go test -update .` 生成并人工检查后提交;在此之前用 `go test -skip Snapshot ./...` 运行其余测试。 + +## 离线 + +库与 Gallery 不发起网络请求,无遥测、账号或远程字体。图标与角饰随代码嵌入。浏览器镜像只在本机监听调用方给定的地址。 diff --git a/examples/gallery/main.go b/examples/gallery/main.go index ceb182f..7dfd576 100644 --- a/examples/gallery/main.go +++ b/examples/gallery/main.go @@ -1,9 +1,11 @@ // Gallery tours every MujicaUI component in a native MyGo window. // -// go run ./examples/gallery +// go run ./examples/gallery native window +// go run ./examples/gallery -web :8080 browser mirror package main import ( + "flag" "log" "github.com/egoist/mygo" @@ -11,7 +13,12 @@ import ( ) func main() { + web := flag.String("web", "", "serve the gallery to a browser at this address instead of opening a window") + flag.Parse() app := newGallery() + if *web != "" { + log.Fatal(serveWeb(*web, app.view)) + } mygo.App.WhenReady(func() { mygo.NewWindow(mygo.WindowOptions{ Title: "MujicaUI Gallery", diff --git a/examples/gallery/web.go b/examples/gallery/web.go new file mode 100644 index 0000000..fac1a9b --- /dev/null +++ b/examples/gallery/web.go @@ -0,0 +1,284 @@ +package main + +import ( + "bytes" + _ "embed" + "encoding/json" + "fmt" + "hash/maphash" + "image/png" + "log/slog" + "net/http" + "sync" + "time" + + "github.com/egoist/mygo/ui" +) + +//go:embed web/index.html +var indexHTML []byte + +// mirror runs the gallery view headless and streams its frames. +type mirror struct { + mu sync.Mutex + tt *ui.Tester + frame []byte + seq int + hash uint64 + seed maphash.Seed + subs map[chan []byte]struct{} + wake chan struct{} + urls int + told int + clip string + enc png.Encoder +} + +func serveWeb(addr string, view func(c *ui.Context)) error { + m := &mirror{ + tt: ui.NewTester(view, 1200, 800), + seed: maphash.MakeSeed(), + subs: map[chan []byte]struct{}{}, + wake: make(chan struct{}, 1), + enc: png.Encoder{CompressionLevel: png.BestSpeed}, + } + m.capture() + go m.loop() + mux := http.NewServeMux() + mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Write(indexHTML) + }) + mux.HandleFunc("GET /frame.png", m.serveFrame) + mux.HandleFunc("GET /events", m.serveEvents) + mux.HandleFunc("POST /input", m.serveInput) + slog.Info("gallery: web mirror listening", "addr", "http://"+addr) + return http.ListenAndServe(addr, mux) +} + +// loop draws frames while the picture keeps changing, then sleeps. +func (m *mirror) loop() { + for range m.wake { + for still := 0; still < 6; { + time.Sleep(33 * time.Millisecond) + m.mu.Lock() + m.tt.Frame() + changed := m.capture() + m.mu.Unlock() + if changed { + still = 0 + } else { + still++ + } + } + } +} + +func (m *mirror) kick() { + select { + case m.wake <- struct{}{}: + default: + } +} + +// capture encodes the frame and notifies browsers; the caller holds mu. +func (m *mirror) capture() bool { + img := m.tt.Image() + h := maphash.Bytes(m.seed, img.Pix) + notice := m.notices() + if h == m.hash && notice == nil { + return false + } + if h != m.hash { + var b bytes.Buffer + if err := m.enc.Encode(&b, img); err != nil { + panic(err) + } + m.frame, m.hash = b.Bytes(), h + m.seq++ + } + if notice == nil { + notice = map[string]any{} + } + notice["seq"] = m.seq + notice["cursor"] = cursorCSS(m.tt.Cursor()) + msg, err := json.Marshal(notice) + if err != nil { + panic(err) + } + for ch := range m.subs { + select { + case ch <- msg: + default: + slog.Warn("gallery: web client lagging, frame notice dropped", "seq", m.seq) + } + } + return true +} + +// notices gathers opened URLs, announcements and clipboard changes. +func (m *mirror) notices() map[string]any { + n := map[string]any{} + if urls := m.tt.OpenedURLs(); len(urls) > m.urls { + n["open"], m.urls = urls[m.urls:], len(urls) + } + if told := m.tt.Announcements(); len(told) > m.told { + n["announce"], m.told = told[m.told:], len(told) + } + if clip := m.tt.Clipboard(); clip != m.clip { + n["clipboard"], m.clip = clip, clip + } + if len(n) == 0 { + return nil + } + return n +} + +func (m *mirror) serveFrame(w http.ResponseWriter, r *http.Request) { + m.mu.Lock() + frame := m.frame + m.mu.Unlock() + w.Header().Set("Content-Type", "image/png") + w.Header().Set("Cache-Control", "no-store") + w.Write(frame) +} + +func (m *mirror) serveEvents(w http.ResponseWriter, r *http.Request) { + flusher, ok := w.(http.Flusher) + if !ok { + http.Error(w, "streaming unsupported", http.StatusInternalServerError) + return + } + ch := make(chan []byte, 8) + m.mu.Lock() + m.subs[ch] = struct{}{} + first := fmt.Sprintf(`{"seq":%d}`, m.seq) + m.mu.Unlock() + slog.Info("gallery: web client connected", "remote", r.RemoteAddr) + defer func() { + m.mu.Lock() + delete(m.subs, ch) + m.mu.Unlock() + slog.Info("gallery: web client left", "remote", r.RemoteAddr) + }() + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-store") + fmt.Fprintf(w, "data: %s\n\n", first) + flusher.Flush() + for { + select { + case msg := <-ch: + fmt.Fprintf(w, "data: %s\n\n", msg) + flusher.Flush() + case <-r.Context().Done(): + return + } + } +} + +// input is one browser event. +type input struct { + Type string `json:"type"` + X float32 `json:"x"` + Y float32 `json:"y"` + DX float32 `json:"dx"` + DY float32 `json:"dy"` + Button int `json:"button"` + Code string `json:"code"` + Text string `json:"text"` + Caret int `json:"caret"` + Mods []bool `json:"mods"` + W int `json:"w"` + H int `json:"h"` + Scale float32 `json:"scale"` + Dark bool `json:"dark"` + Motion bool `json:"reduceMotion"` + More bool `json:"moreContrast"` +} + +func (m *mirror) serveInput(w http.ResponseWriter, r *http.Request) { + var in input + if err := json.NewDecoder(r.Body).Decode(&in); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + m.mu.Lock() + err := m.apply(in) + if err == nil { + m.capture() + } + m.mu.Unlock() + if err != nil { + slog.Warn("gallery: web input rejected", "type", in.Type, "err", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + m.kick() + w.WriteHeader(http.StatusNoContent) +} + +// apply sends one event to the tester; the caller holds mu. +func (m *mirror) apply(in input) error { + mods := modifiers(in.Mods) + switch in.Type { + case "down": + if in.Button == 2 { + m.tt.RightClickAt(in.X, in.Y) + } else if mods != 0 { + m.tt.ClickAtWith(mods, in.X, in.Y) + } else { + m.tt.Press(in.X, in.Y) + } + case "up": + if in.Button == 0 && mods == 0 { + m.tt.Release(in.X, in.Y) + } + case "move": + m.tt.Move(in.X, in.Y) + case "wheel": + m.tt.Scroll(in.X, in.Y, in.DX, in.DY) + case "key": + k, ok := keyCodes[in.Code] + if !ok { + return fmt.Errorf("unmapped key code %q", in.Code) + } + if in.Text != "" && mods&^ui.Shift == 0 { + m.tt.TypeKey(mods, k, in.Text) + } else { + m.tt.Key(mods, k) + } + case "text": + m.tt.Type(in.Text) + case "compose": + m.tt.Compose(in.Text, in.Caret) + case "paste": + m.tt.SetClipboard(in.Text) + m.clip = in.Text + m.tt.Command("paste") + case "command": + m.tt.Command(in.Text) + case "resize": + if in.W < 320 || in.H < 240 || in.Scale <= 0 { + return fmt.Errorf("bad size %dx%d@%v", in.W, in.H, in.Scale) + } + m.tt.SetSize(in.W, in.H) + m.tt.SetScale(in.Scale) + m.tt.SetDark(in.Dark) + m.tt.SetPreferences(ui.Preferences{ReduceMotion: in.Motion, HighContrast: in.More, TextScale: 1}) + case "focus": + m.tt.SetFocused(in.Text == "in") + default: + return fmt.Errorf("unknown input type %q", in.Type) + } + return nil +} + +func modifiers(m []bool) ui.Modifiers { + var out ui.Modifiers + for i, mod := range []ui.Modifiers{ui.Shift, ui.Ctrl, ui.Alt, ui.Super} { + if i < len(m) && m[i] { + out |= mod + } + } + return out +} diff --git a/examples/gallery/web/index.html b/examples/gallery/web/index.html new file mode 100644 index 0000000..98572c4 --- /dev/null +++ b/examples/gallery/web/index.html @@ -0,0 +1,178 @@ + + + + + +MujicaUI Mirror + + + +
+ +

MujicaUI

+ Go components, drawn by MyGo, mirrored here + +
+
+ + +
+
+ The browser runs no component code. Every pixel comes from ui.NewTester in the Go process; your pointer, keys, IME and clipboard travel back to it. + +
+
+ + + diff --git a/examples/gallery/web_keys.go b/examples/gallery/web_keys.go new file mode 100644 index 0000000..ae987ad --- /dev/null +++ b/examples/gallery/web_keys.go @@ -0,0 +1,138 @@ +package main + +import "github.com/egoist/mygo/ui" + +// keyCodes maps KeyboardEvent.code to MyGo keys. +var keyCodes = map[string]ui.Key{ + "Enter": ui.KeyEnter, + "NumpadEnter": ui.KeyEnter, + "Escape": ui.KeyEscape, + "Backspace": ui.KeyBackspace, + "Tab": ui.KeyTab, + "Space": ui.KeySpace, + "Delete": ui.KeyDelete, + "Insert": ui.KeyInsert, + "Home": ui.KeyHome, + "End": ui.KeyEnd, + "PageUp": ui.KeyPageUp, + "PageDown": ui.KeyPageDown, + "ArrowLeft": ui.KeyLeft, + "ArrowRight": ui.KeyRight, + "ArrowUp": ui.KeyUp, + "ArrowDown": ui.KeyDown, + "Minus": ui.KeyMinus, + "Equal": ui.KeyEqual, + "Comma": ui.KeyComma, + "Period": ui.KeyPeriod, + "Slash": ui.KeySlash, + "Semicolon": ui.KeySemicolon, + "Quote": ui.KeyQuote, + "BracketLeft": ui.KeyBracketLeft, + "BracketRight": ui.KeyBracketRight, + "Backslash": ui.KeyBackslash, + "Backquote": ui.KeyBackquote, + "ContextMenu": ui.KeyContextMenu, + "BrowserBack": ui.KeyBack, + "BrowserForward": ui.KeyForward, + "F1": ui.KeyF1, + "F2": ui.KeyF2, + "F3": ui.KeyF3, + "F4": ui.KeyF4, + "F5": ui.KeyF5, + "F6": ui.KeyF6, + "F7": ui.KeyF7, + "F8": ui.KeyF8, + "F9": ui.KeyF9, + "F10": ui.KeyF10, + "F11": ui.KeyF11, + "F12": ui.KeyF12, + "Digit0": ui.Key0, + "Digit1": ui.Key1, + "Digit2": ui.Key2, + "Digit3": ui.Key3, + "Digit4": ui.Key4, + "Digit5": ui.Key5, + "Digit6": ui.Key6, + "Digit7": ui.Key7, + "Digit8": ui.Key8, + "Digit9": ui.Key9, + "KeyA": ui.KeyA, + "KeyB": ui.KeyB, + "KeyC": ui.KeyC, + "KeyD": ui.KeyD, + "KeyE": ui.KeyE, + "KeyF": ui.KeyF, + "KeyG": ui.KeyG, + "KeyH": ui.KeyH, + "KeyI": ui.KeyI, + "KeyJ": ui.KeyJ, + "KeyK": ui.KeyK, + "KeyL": ui.KeyL, + "KeyM": ui.KeyM, + "KeyN": ui.KeyN, + "KeyO": ui.KeyO, + "KeyP": ui.KeyP, + "KeyQ": ui.KeyQ, + "KeyR": ui.KeyR, + "KeyS": ui.KeyS, + "KeyT": ui.KeyT, + "KeyU": ui.KeyU, + "KeyV": ui.KeyV, + "KeyW": ui.KeyW, + "KeyX": ui.KeyX, + "KeyY": ui.KeyY, + "KeyZ": ui.KeyZ, +} + +// cursorCSS names a MyGo cursor in CSS. +func cursorCSS(c ui.Cursor) string { + switch c { + case ui.CursorDefault: + return "default" + case ui.CursorPointer: + return "pointer" + case ui.CursorText: + return "text" + case ui.CursorMove: + return "move" + case ui.CursorResizeEW: + return "ew-resize" + case ui.CursorResizeNS: + return "ns-resize" + case ui.CursorResizeNWSE: + return "nwse-resize" + case ui.CursorResizeNESW: + return "nesw-resize" + case ui.CursorNotAllowed: + return "not-allowed" + case ui.CursorCrosshair: + return "crosshair" + case ui.CursorGrab: + return "grab" + case ui.CursorGrabbing: + return "grabbing" + case ui.CursorResizeN: + return "n-resize" + case ui.CursorResizeE: + return "e-resize" + case ui.CursorResizeS: + return "s-resize" + case ui.CursorResizeW: + return "w-resize" + case ui.CursorResizeColumn: + return "col-resize" + case ui.CursorResizeRow: + return "row-resize" + case ui.CursorVerticalText: + return "vertical-text" + case ui.CursorCopy: + return "copy" + case ui.CursorAlias: + return "alias" + case ui.CursorContextMenu: + return "context-menu" + case ui.CursorNone: + return "none" + } + panic("gallery: unmapped cursor") +}