cronixui 1.1.2 → 1.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +71 -71
- package/packages/flutter/.qwen/settings.json +7 -0
- package/packages/flutter/pubspec.yaml +20 -20
- package/packages/go/cronixui/cronixui.go +926 -926
- package/packages/python/README.md +142 -0
- package/packages/python/cronixui/__init__.py +15 -6
- package/packages/python/cronixui/__pycache__/__init__.cpython-314.pyc +0 -0
- package/packages/python/cronixui/__pycache__/accordion.cpython-314.pyc +0 -0
- package/packages/python/cronixui/__pycache__/alert.cpython-314.pyc +0 -0
- package/packages/python/cronixui/__pycache__/avatar.cpython-314.pyc +0 -0
- package/packages/python/cronixui/__pycache__/badge.cpython-314.pyc +0 -0
- package/packages/python/cronixui/__pycache__/button.cpython-314.pyc +0 -0
- package/packages/python/cronixui/__pycache__/card.cpython-314.pyc +0 -0
- package/packages/python/cronixui/__pycache__/command_palette.cpython-314.pyc +0 -0
- package/packages/python/cronixui/__pycache__/core.cpython-314.pyc +0 -0
- package/packages/python/cronixui/__pycache__/dropdown.cpython-314.pyc +0 -0
- package/packages/python/cronixui/__pycache__/form.cpython-314.pyc +0 -0
- package/packages/python/cronixui/__pycache__/layout.cpython-314.pyc +0 -0
- package/packages/python/cronixui/__pycache__/list.cpython-314.pyc +0 -0
- package/packages/python/cronixui/__pycache__/loading.cpython-314.pyc +0 -0
- package/packages/python/cronixui/__pycache__/modal.cpython-314.pyc +0 -0
- package/packages/python/cronixui/__pycache__/nav.cpython-314.pyc +0 -0
- package/packages/python/cronixui/__pycache__/pagination.cpython-314.pyc +0 -0
- package/packages/python/cronixui/__pycache__/progress.cpython-314.pyc +0 -0
- package/packages/python/cronixui/__pycache__/search.cpython-314.pyc +0 -0
- package/packages/python/cronixui/__pycache__/table.cpython-314.pyc +0 -0
- package/packages/python/cronixui/__pycache__/tabs.cpython-314.pyc +0 -0
- package/packages/python/cronixui/__pycache__/toast.cpython-314.pyc +0 -0
- package/packages/python/cronixui/__pycache__/toggle.cpython-314.pyc +0 -0
- package/packages/python/cronixui/__pycache__/tokens.cpython-314.pyc +0 -0
- package/packages/python/cronixui/__pycache__/tooltip.cpython-314.pyc +0 -0
- package/packages/python/cronixui/alert.py +119 -36
- package/packages/python/cronixui/avatar.py +129 -22
- package/packages/python/cronixui/badge.py +161 -24
- package/packages/python/cronixui/button.py +96 -27
- package/packages/python/cronixui/card.py +206 -33
- package/packages/python/cronixui/core.py +212 -23
- package/packages/python/cronixui/form.py +552 -141
- package/packages/python/cronixui/layout.py +358 -96
- package/packages/python/cronixui/list.py +140 -37
- package/packages/python/cronixui/loading.py +107 -17
- package/packages/python/cronixui/progress.py +189 -47
- package/packages/python/cronixui/table.py +118 -31
- package/packages/python/cronixui/tooltip.py +117 -15
- package/packages/react/src/components/Accordion.tsx +82 -82
- package/packages/react/src/components/Button.tsx +47 -47
- package/packages/react/src/components/Card.tsx +69 -69
- package/packages/react/src/components/CommandPalette.tsx +131 -131
- package/packages/react/src/components/Dropdown.tsx +88 -88
- package/packages/react/src/components/FileInput.tsx +86 -86
- package/packages/react/src/components/FormGroup.tsx +36 -36
- package/packages/react/src/components/List.tsx +55 -55
- package/packages/react/src/components/Pagination.tsx +107 -107
- package/packages/react/src/components/Progress.tsx +49 -49
- package/packages/react/src/components/Search.tsx +95 -95
- package/packages/react/src/components/Sidebar.tsx +64 -64
- package/packages/react/src/components/Stack.tsx +69 -69
- package/packages/react/src/components/Table.tsx +90 -90
- package/packages/react/src/components/Toast.tsx +134 -134
- package/packages/react/src/components/Typography.tsx +66 -66
- package/packages/react/src/index.ts +40 -40
- package/packages/react/src/styles.css +2039 -2039
- package/packages/rust/cronixui/src/components/avatar.rs +85 -85
- package/packages/rust/cronixui/src/components/breadcrumb.rs +58 -58
- package/packages/rust/cronixui/src/components/card.rs +259 -259
- package/packages/rust/cronixui/src/components/command_palette.rs +254 -254
- package/packages/rust/cronixui/src/components/dropdown.rs +179 -179
- package/packages/rust/cronixui/src/components/file_input.rs +74 -74
- package/packages/rust/cronixui/src/components/mod.rs +51 -51
- package/packages/rust/cronixui/src/components/search.rs +185 -185
- package/packages/rust/cronixui/src/components/skeleton.rs +63 -63
- package/packages/rust/cronixui/src/components/table.rs +56 -56
- package/packages/rust/cronixui/src/lib.rs +128 -128
- package/packages/web/dist/cronixui.css +97 -93
- package/packages/web/dist/cronixui.min.css +1 -1
|
@@ -1,926 +1,926 @@
|
|
|
1
|
-
package cronixui
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"image/color"
|
|
5
|
-
"strings"
|
|
6
|
-
|
|
7
|
-
"fyne.io/fyne/v2"
|
|
8
|
-
"fyne.io/fyne/v2/app"
|
|
9
|
-
"fyne.io/fyne/v2/canvas"
|
|
10
|
-
"fyne.io/fyne/v2/container"
|
|
11
|
-
"fyne.io/fyne/v2/layout"
|
|
12
|
-
"fyne.io/fyne/v2/theme"
|
|
13
|
-
"fyne.io/fyne/v2/widget"
|
|
14
|
-
)
|
|
15
|
-
|
|
16
|
-
const Version = "1.0.6"
|
|
17
|
-
|
|
18
|
-
// =============================================================================
|
|
19
|
-
// DESIGN TOKENS
|
|
20
|
-
// =============================================================================
|
|
21
|
-
|
|
22
|
-
type Colors struct {
|
|
23
|
-
BG color.Color
|
|
24
|
-
Surface color.Color
|
|
25
|
-
Surface2 color.Color
|
|
26
|
-
Surface3 color.Color
|
|
27
|
-
Surface4 color.Color
|
|
28
|
-
Text color.Color
|
|
29
|
-
TextMuted color.Color
|
|
30
|
-
TextDim color.Color
|
|
31
|
-
Accent color.Color
|
|
32
|
-
AccentHover color.Color
|
|
33
|
-
AccentLight color.Color
|
|
34
|
-
AccentGlow color.Color
|
|
35
|
-
AccentText color.Color
|
|
36
|
-
Success color.Color
|
|
37
|
-
SuccessBorder color.Color
|
|
38
|
-
SuccessText color.Color
|
|
39
|
-
Warning color.Color
|
|
40
|
-
WarningBorder color.Color
|
|
41
|
-
WarningText color.Color
|
|
42
|
-
Error color.Color
|
|
43
|
-
ErrorBorder color.Color
|
|
44
|
-
ErrorText color.Color
|
|
45
|
-
Info color.Color
|
|
46
|
-
InfoBorder color.Color
|
|
47
|
-
InfoText color.Color
|
|
48
|
-
Border color.Color
|
|
49
|
-
BorderHover color.Color
|
|
50
|
-
BorderFocus color.Color
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
func DefaultColors() *Colors {
|
|
54
|
-
return &Colors{
|
|
55
|
-
BG: color.RGBA{R: 10, G: 10, B: 10, A: 255},
|
|
56
|
-
Surface: color.RGBA{R: 17, G: 17, B: 17, A: 255},
|
|
57
|
-
Surface2: color.RGBA{R: 26, G: 26, B: 26, A: 255},
|
|
58
|
-
Surface3: color.RGBA{R: 34, G: 34, B: 34, A: 255},
|
|
59
|
-
Surface4: color.RGBA{R: 42, G: 42, B: 42, A: 255},
|
|
60
|
-
Text: color.RGBA{R: 240, G: 237, B: 232, A: 255},
|
|
61
|
-
TextMuted: color.RGBA{R: 240, G: 237, B: 232, A: 128},
|
|
62
|
-
TextDim: color.RGBA{R: 240, G: 237, B: 232, A: 64},
|
|
63
|
-
Accent: color.RGBA{R: 107, G: 35, B: 35, A: 255},
|
|
64
|
-
AccentHover: color.RGBA{R: 125, G: 42, B: 42, A: 255},
|
|
65
|
-
AccentLight: color.RGBA{R: 138, G: 53, B: 53, A: 255},
|
|
66
|
-
AccentGlow: color.RGBA{R: 107, G: 35, B: 35, A: 77},
|
|
67
|
-
AccentText: color.RGBA{R: 201, G: 122, B: 122, A: 255},
|
|
68
|
-
Success: color.RGBA{R: 30, G: 80, B: 40, A: 255},
|
|
69
|
-
SuccessBorder: color.RGBA{R: 60, G: 140, B: 70, A: 102},
|
|
70
|
-
SuccessText: color.RGBA{R: 107, G: 196, B: 122, A: 255},
|
|
71
|
-
Warning: color.RGBA{R: 80, G: 60, B: 20, A: 255},
|
|
72
|
-
WarningBorder: color.RGBA{R: 150, G: 110, B: 30, A: 102},
|
|
73
|
-
WarningText: color.RGBA{R: 196, G: 164, B: 58, A: 255},
|
|
74
|
-
Error: color.RGBA{R: 80, G: 20, B: 20, A: 255},
|
|
75
|
-
ErrorBorder: color.RGBA{R: 180, G: 60, B: 60, A: 102},
|
|
76
|
-
ErrorText: color.RGBA{R: 196, G: 107, B: 107, A: 255},
|
|
77
|
-
Info: color.RGBA{R: 20, G: 53, B: 80, A: 255},
|
|
78
|
-
InfoBorder: color.RGBA{R: 60, G: 140, B: 200, A: 102},
|
|
79
|
-
InfoText: color.RGBA{R: 107, G: 168, B: 196, A: 255},
|
|
80
|
-
Border: color.RGBA{R: 255, G: 255, B: 255, A: 20},
|
|
81
|
-
BorderHover: color.RGBA{R: 255, G: 255, B: 255, A: 38},
|
|
82
|
-
BorderFocus: color.RGBA{R: 255, G: 255, B: 255, A: 64},
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
type Typography struct {
|
|
87
|
-
FontFamily string
|
|
88
|
-
FontMono string
|
|
89
|
-
FontSizeXS float32
|
|
90
|
-
FontSizeSM float32
|
|
91
|
-
FontSizeBase float32
|
|
92
|
-
FontSizeMD float32
|
|
93
|
-
FontSizeLG float32
|
|
94
|
-
FontSizeXL float32
|
|
95
|
-
FontSize2XL float32
|
|
96
|
-
FontSize3XL float32
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
func DefaultTypography() *Typography {
|
|
100
|
-
return &Typography{
|
|
101
|
-
FontFamily: "Outfit",
|
|
102
|
-
FontMono: "JetBrains Mono",
|
|
103
|
-
FontSizeXS: 11,
|
|
104
|
-
FontSizeSM: 12,
|
|
105
|
-
FontSizeBase: 13,
|
|
106
|
-
FontSizeMD: 14,
|
|
107
|
-
FontSizeLG: 16,
|
|
108
|
-
FontSizeXL: 20,
|
|
109
|
-
FontSize2XL: 28,
|
|
110
|
-
FontSize3XL: 36,
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
type Spacing struct {
|
|
115
|
-
Space1, Space2, Space3, Space4, Space5, Space6, Space8, Space10, Space12 float32
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
func DefaultSpacing() *Spacing {
|
|
119
|
-
return &Spacing{4, 8, 12, 16, 20, 24, 32, 40, 48}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
type RadiusTokens struct {
|
|
123
|
-
SM, Default, LG, XL, Full float32
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
func DefaultRadiusTokens() *RadiusTokens {
|
|
127
|
-
return &RadiusTokens{6, 10, 14, 20, 9999}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
type Shadow struct {
|
|
131
|
-
SM, Default, LG, Glow string
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
func DefaultShadow() *Shadow {
|
|
135
|
-
return &Shadow{
|
|
136
|
-
SM: "0 1px 2px rgba(0, 0, 0, 0.3)",
|
|
137
|
-
Default: "0 4px 12px rgba(0, 0, 0, 0.4)",
|
|
138
|
-
LG: "0 8px 24px rgba(0, 0, 0, 0.5)",
|
|
139
|
-
Glow: "0 0 20px rgba(107, 35, 35, 0.3)",
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
type Transition struct {
|
|
144
|
-
Fast, Default, Slow string
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
func DefaultTransition() *Transition {
|
|
148
|
-
return &Transition{"0.1s ease", "0.15s ease", "0.25s ease"}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
type ZIndexTokens struct {
|
|
152
|
-
Dropdown, Sticky, Fixed, ModalBackdrop, Modal, Popover, Tooltip, Toast int
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
func DefaultZIndexTokens() *ZIndexTokens {
|
|
156
|
-
return &ZIndexTokens{100, 200, 300, 400, 500, 600, 700, 800}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
type LayoutTokens struct {
|
|
160
|
-
ContainerMax, SidebarWidth, HeaderHeight float32
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
func DefaultLayoutTokens() *LayoutTokens {
|
|
164
|
-
return &LayoutTokens{1200, 260, 56}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// =============================================================================
|
|
168
|
-
// THEME
|
|
169
|
-
// =============================================================================
|
|
170
|
-
|
|
171
|
-
type Theme struct {
|
|
172
|
-
colors *Colors
|
|
173
|
-
spacing *Spacing
|
|
174
|
-
radius *RadiusTokens
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
func NewTheme() fyne.Theme {
|
|
178
|
-
return &Theme{
|
|
179
|
-
colors: DefaultColors(),
|
|
180
|
-
spacing: DefaultSpacing(),
|
|
181
|
-
radius: DefaultRadiusTokens(),
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
func (t *Theme) Color(name fyne.ThemeColorName, variant fyne.ThemeVariant) color.Color {
|
|
186
|
-
switch name {
|
|
187
|
-
case theme.ColorNameBackground:
|
|
188
|
-
return t.colors.BG
|
|
189
|
-
case theme.ColorNameButton:
|
|
190
|
-
return t.colors.Surface2
|
|
191
|
-
case theme.ColorNameDisabledButton:
|
|
192
|
-
return t.colors.Surface3
|
|
193
|
-
case theme.ColorNameInputBackground:
|
|
194
|
-
return t.colors.Surface
|
|
195
|
-
case theme.ColorNameInputBorder:
|
|
196
|
-
return t.colors.Border
|
|
197
|
-
case theme.ColorNameDisabled:
|
|
198
|
-
return t.colors.TextDim
|
|
199
|
-
case theme.ColorNameForeground:
|
|
200
|
-
return t.colors.Text
|
|
201
|
-
case theme.ColorNamePlaceHolder:
|
|
202
|
-
return t.colors.TextMuted
|
|
203
|
-
case theme.ColorNamePrimary:
|
|
204
|
-
return t.colors.Accent
|
|
205
|
-
case theme.ColorNameHover:
|
|
206
|
-
return t.colors.Surface3
|
|
207
|
-
case theme.ColorNamePressed:
|
|
208
|
-
return t.colors.Surface4
|
|
209
|
-
case theme.ColorNameScrollBar:
|
|
210
|
-
return t.colors.Surface3
|
|
211
|
-
case theme.ColorNameShadow:
|
|
212
|
-
return color.RGBA{R: 0, G: 0, B: 0, A: 50}
|
|
213
|
-
case theme.ColorNameSuccess:
|
|
214
|
-
return t.colors.Success
|
|
215
|
-
case theme.ColorNameWarning:
|
|
216
|
-
return t.colors.Warning
|
|
217
|
-
case theme.ColorNameError:
|
|
218
|
-
return t.colors.Error
|
|
219
|
-
default:
|
|
220
|
-
return t.colors.Text
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
func (t *Theme) Font(style fyne.TextStyle) fyne.Resource {
|
|
225
|
-
return theme.DefaultTheme().Font(style)
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
func (t *Theme) Icon(name fyne.ThemeIconName) fyne.Resource {
|
|
229
|
-
return theme.DefaultTheme().Icon(name)
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
func (t *Theme) Size(name fyne.ThemeSizeName) float32 {
|
|
233
|
-
switch name {
|
|
234
|
-
case theme.SizeNamePadding:
|
|
235
|
-
return t.spacing.Space2
|
|
236
|
-
case theme.SizeNameInlinePadding:
|
|
237
|
-
return t.spacing.Space3
|
|
238
|
-
case theme.SizeNameScrollBar:
|
|
239
|
-
return 8
|
|
240
|
-
case theme.SizeNameScrollBarSmall:
|
|
241
|
-
return 4
|
|
242
|
-
case theme.SizeNameSeparatorThickness:
|
|
243
|
-
return 1
|
|
244
|
-
case theme.SizeNameText:
|
|
245
|
-
return t.spacing.Space4
|
|
246
|
-
case theme.SizeNameHeadingText:
|
|
247
|
-
return t.spacing.Space6
|
|
248
|
-
case theme.SizeNameSubHeadingText:
|
|
249
|
-
return t.spacing.Space5
|
|
250
|
-
case theme.SizeNameCaptionText:
|
|
251
|
-
return t.spacing.Space2
|
|
252
|
-
case theme.SizeNameInputBorder:
|
|
253
|
-
return 1
|
|
254
|
-
case theme.SizeNameInputRadius:
|
|
255
|
-
return t.radius.Default
|
|
256
|
-
case theme.SizeNameSelectionRadius:
|
|
257
|
-
return t.radius.SM
|
|
258
|
-
default:
|
|
259
|
-
return 0
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// =============================================================================
|
|
264
|
-
// BUTTONS
|
|
265
|
-
// =============================================================================
|
|
266
|
-
|
|
267
|
-
type ButtonVariant int
|
|
268
|
-
|
|
269
|
-
const (
|
|
270
|
-
ButtonDefault ButtonVariant = iota
|
|
271
|
-
ButtonPrimary
|
|
272
|
-
ButtonGhost
|
|
273
|
-
ButtonOutline
|
|
274
|
-
ButtonDanger
|
|
275
|
-
ButtonSuccess
|
|
276
|
-
)
|
|
277
|
-
|
|
278
|
-
func NewButton(text string, variant ButtonVariant, onTap func()) *widget.Button {
|
|
279
|
-
btn := widget.NewButton(text, onTap)
|
|
280
|
-
switch variant {
|
|
281
|
-
case ButtonPrimary:
|
|
282
|
-
btn.Importance = widget.HighImportance
|
|
283
|
-
case ButtonDanger:
|
|
284
|
-
btn.Importance = widget.DangerImportance
|
|
285
|
-
case ButtonSuccess:
|
|
286
|
-
btn.Importance = widget.HighImportance
|
|
287
|
-
case ButtonGhost, ButtonOutline:
|
|
288
|
-
btn.Importance = widget.LowImportance
|
|
289
|
-
default:
|
|
290
|
-
btn.Importance = widget.MediumImportance
|
|
291
|
-
}
|
|
292
|
-
return btn
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
func NewButtonGroup(buttons ...*widget.Button) *fyne.Container {
|
|
296
|
-
return container.NewHBox(buttons...)
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// =============================================================================
|
|
300
|
-
// CARD
|
|
301
|
-
// =============================================================================
|
|
302
|
-
|
|
303
|
-
// CardOption is a function that configures a card.
|
|
304
|
-
type CardOption func(*CardConfig)
|
|
305
|
-
|
|
306
|
-
// CardConfig holds card configuration.
|
|
307
|
-
type CardConfig struct {
|
|
308
|
-
Title string
|
|
309
|
-
Subtitle string
|
|
310
|
-
Icon fyne.CanvasObject
|
|
311
|
-
Footer fyne.CanvasObject
|
|
312
|
-
Clickable bool
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// WithCardTitle sets the card title.
|
|
316
|
-
func WithCardTitle(title string) CardOption {
|
|
317
|
-
return func(c *CardConfig) { c.Title = title }
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// WithCardSubtitle sets the card subtitle.
|
|
321
|
-
func WithCardSubtitle(subtitle string) CardOption {
|
|
322
|
-
return func(c *CardConfig) { c.Subtitle = subtitle }
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// WithCardIcon sets the card icon.
|
|
326
|
-
func WithCardIcon(icon fyne.CanvasObject) CardOption {
|
|
327
|
-
return func(c *CardConfig) { c.Icon = icon }
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// WithCardFooter sets the card footer.
|
|
331
|
-
func WithCardFooter(footer fyne.CanvasObject) CardOption {
|
|
332
|
-
return func(c *CardConfig) { c.Footer = footer }
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// WithCardClickable makes the card clickable.
|
|
336
|
-
func WithCardClickable(clickable bool) CardOption {
|
|
337
|
-
return func(c *CardConfig) { c.Clickable = clickable }
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// NewCard creates a card widget with optional icon, title, subtitle, and footer.
|
|
341
|
-
func NewCard(content fyne.CanvasObject, options ...CardOption) *fyne.Container {
|
|
342
|
-
config := &CardConfig{}
|
|
343
|
-
for _, opt := range options {
|
|
344
|
-
opt(config)
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
c := DefaultColors()
|
|
348
|
-
|
|
349
|
-
var headerObjects []fyne.CanvasObject
|
|
350
|
-
if config.Icon != nil {
|
|
351
|
-
headerObjects = append(headerObjects, config.Icon)
|
|
352
|
-
}
|
|
353
|
-
if config.Title != "" {
|
|
354
|
-
titleText := canvas.NewText(config.Title, c.Text)
|
|
355
|
-
titleText.TextSize = 14
|
|
356
|
-
titleText.TextStyle = fyne.TextStyle{Bold: true}
|
|
357
|
-
headerObjects = append(headerObjects, titleText)
|
|
358
|
-
}
|
|
359
|
-
if config.Subtitle != "" {
|
|
360
|
-
subtitleText := canvas.NewText(config.Subtitle, c.TextMuted)
|
|
361
|
-
subtitleText.TextSize = 12
|
|
362
|
-
headerObjects = append(headerObjects, subtitleText)
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
var header *fyne.Container
|
|
366
|
-
if len(headerObjects) > 0 {
|
|
367
|
-
header = container.NewVBox(headerObjects...)
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
var body fyne.CanvasObject
|
|
371
|
-
if header != nil {
|
|
372
|
-
body = container.NewVBox(header, content)
|
|
373
|
-
} else {
|
|
374
|
-
body = content
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
var finalContent *fyne.Container
|
|
378
|
-
if config.Footer != nil {
|
|
379
|
-
finalContent = container.NewVBox(body, canvas.NewRectangle(c.Border), config.Footer)
|
|
380
|
-
} else {
|
|
381
|
-
if c, ok := body.(*fyne.Container); ok {
|
|
382
|
-
finalContent = c
|
|
383
|
-
} else {
|
|
384
|
-
finalContent = container.NewMax(body)
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
bg := canvas.NewRectangle(c.Surface)
|
|
389
|
-
bg.StrokeColor = c.Border
|
|
390
|
-
bg.StrokeWidth = 1
|
|
391
|
-
bg.CornerRadius = 14
|
|
392
|
-
|
|
393
|
-
return container.NewStack(bg, container.NewPadded(finalContent))
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// =============================================================================
|
|
397
|
-
// INPUTS
|
|
398
|
-
// =============================================================================
|
|
399
|
-
|
|
400
|
-
func NewInput(placeholder string) *widget.Entry {
|
|
401
|
-
entry := widget.NewEntry()
|
|
402
|
-
entry.SetPlaceHolder(placeholder)
|
|
403
|
-
return entry
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
func NewTextarea(placeholder string) *widget.Entry {
|
|
407
|
-
entry := widget.NewMultiLineEntry()
|
|
408
|
-
entry.SetPlaceHolder(placeholder)
|
|
409
|
-
return entry
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
func NewPassword(placeholder string) *widget.Entry {
|
|
413
|
-
entry := widget.NewPasswordEntry()
|
|
414
|
-
entry.SetPlaceHolder(placeholder)
|
|
415
|
-
return entry
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// =============================================================================
|
|
419
|
-
// FORM ELEMENTS
|
|
420
|
-
// =============================================================================
|
|
421
|
-
|
|
422
|
-
func NewCheckbox(label string, onChanged func(bool)) *widget.Check {
|
|
423
|
-
return widget.NewCheck(label, onChanged)
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
func NewRadio(options []string, onChanged func(string)) *widget.RadioGroup {
|
|
427
|
-
return widget.NewRadioGroup(options, onChanged)
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
func NewSelect(options []string, onChanged func(string)) *widget.Select {
|
|
431
|
-
return widget.NewSelect(options, onChanged)
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
func NewSlider(min, max float64) *widget.Slider {
|
|
435
|
-
return widget.NewSlider(min, max)
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
// =============================================================================
|
|
439
|
-
// PROGRESS & LOADING
|
|
440
|
-
// =============================================================================
|
|
441
|
-
|
|
442
|
-
func NewProgress() *widget.ProgressBar {
|
|
443
|
-
return widget.NewProgressBar()
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
func NewProgressInfinite() *widget.ProgressBarInfinite {
|
|
447
|
-
return widget.NewProgressBarInfinite()
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
// =============================================================================
|
|
451
|
-
// TOGGLE
|
|
452
|
-
// =============================================================================
|
|
453
|
-
|
|
454
|
-
type Toggle struct {
|
|
455
|
-
widget.BaseWidget
|
|
456
|
-
on bool
|
|
457
|
-
onToggle func(bool)
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
func NewToggle(onToggle func(bool)) *Toggle {
|
|
461
|
-
t := &Toggle{onToggle: onToggle}
|
|
462
|
-
t.ExtendBaseWidget(t)
|
|
463
|
-
return t
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
func (t *Toggle) Toggle() {
|
|
467
|
-
t.on = !t.on
|
|
468
|
-
if t.onToggle != nil {
|
|
469
|
-
t.onToggle(t.on)
|
|
470
|
-
}
|
|
471
|
-
t.Refresh()
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
func (t *Toggle) IsOn() bool { return t.on }
|
|
475
|
-
func (t *Toggle) SetOn(value bool) { t.on = value; t.Refresh() }
|
|
476
|
-
|
|
477
|
-
// =============================================================================
|
|
478
|
-
// MODAL
|
|
479
|
-
// =============================================================================
|
|
480
|
-
|
|
481
|
-
type Modal struct {
|
|
482
|
-
widget.BaseWidget
|
|
483
|
-
content fyne.CanvasObject
|
|
484
|
-
open bool
|
|
485
|
-
window fyne.Window
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
func NewModal(content fyne.CanvasObject) *Modal {
|
|
489
|
-
m := &Modal{content: content}
|
|
490
|
-
m.ExtendBaseWidget(m)
|
|
491
|
-
return m
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
func (m *Modal) Open(window fyne.Window) {
|
|
495
|
-
m.window = window
|
|
496
|
-
m.open = true
|
|
497
|
-
m.Show()
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
func (m *Modal) Close() { m.open = false; m.Hide() }
|
|
501
|
-
func (m *Modal) IsOpen() bool { return m.open }
|
|
502
|
-
func (m *Modal) Show() {
|
|
503
|
-
if m.window != nil {
|
|
504
|
-
popup := widget.NewModalPopUp(m.content, m.window.Canvas())
|
|
505
|
-
popup.Show()
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
func (m *Modal) Hide() {}
|
|
509
|
-
|
|
510
|
-
// =============================================================================
|
|
511
|
-
// DROPDOWN
|
|
512
|
-
// =============================================================================
|
|
513
|
-
|
|
514
|
-
// Dropdown creates a dropdown selection widget using Fyne's Select widget.
|
|
515
|
-
func NewDropdown(items []string, onSelect func(string)) *widget.Select {
|
|
516
|
-
s := widget.NewSelect(items, onSelect)
|
|
517
|
-
return s
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
// =============================================================================
|
|
521
|
-
// TABS
|
|
522
|
-
// =============================================================================
|
|
523
|
-
|
|
524
|
-
func NewTabs(items ...*widget.TabItem) *container.AppTabs {
|
|
525
|
-
return container.NewAppTabs(items...)
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
// =============================================================================
|
|
529
|
-
// ACCORDION
|
|
530
|
-
// =============================================================================
|
|
531
|
-
|
|
532
|
-
type AccordionItem struct {
|
|
533
|
-
Title string
|
|
534
|
-
Content fyne.CanvasObject
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
type Accordion struct {
|
|
538
|
-
widget.BaseWidget
|
|
539
|
-
items []AccordionItem
|
|
540
|
-
openIndices map[int]bool
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
func NewAccordion(items ...AccordionItem) *Accordion {
|
|
544
|
-
a := &Accordion{items: items, openIndices: make(map[int]bool)}
|
|
545
|
-
a.ExtendBaseWidget(a)
|
|
546
|
-
return a
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
func (a *Accordion) Toggle(index int) { a.openIndices[index] = !a.openIndices[index]; a.Refresh() }
|
|
550
|
-
func (a *Accordion) Open(index int) { a.openIndices[index] = true; a.Refresh() }
|
|
551
|
-
func (a *Accordion) Close(index int) { delete(a.openIndices, index); a.Refresh() }
|
|
552
|
-
func (a *Accordion) OpenAll() {
|
|
553
|
-
for i := range a.items {
|
|
554
|
-
a.openIndices[i] = true
|
|
555
|
-
}
|
|
556
|
-
a.Refresh()
|
|
557
|
-
}
|
|
558
|
-
func (a *Accordion) CloseAll() { a.openIndices = make(map[int]bool); a.Refresh() }
|
|
559
|
-
func (a *Accordion) IsOpen(index int) bool { return a.openIndices[index] }
|
|
560
|
-
|
|
561
|
-
// =============================================================================
|
|
562
|
-
// PAGINATION
|
|
563
|
-
// =============================================================================
|
|
564
|
-
|
|
565
|
-
type Pagination struct {
|
|
566
|
-
widget.BaseWidget
|
|
567
|
-
total int
|
|
568
|
-
current int
|
|
569
|
-
onChange func(int)
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
func NewPagination(total, current int, onChange func(int)) *Pagination {
|
|
573
|
-
p := &Pagination{total: total, current: current, onChange: onChange}
|
|
574
|
-
p.ExtendBaseWidget(p)
|
|
575
|
-
return p
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
func (p *Pagination) GoTo(page int) {
|
|
579
|
-
if page >= 1 && page <= p.total {
|
|
580
|
-
p.current = page
|
|
581
|
-
if p.onChange != nil {
|
|
582
|
-
p.onChange(page)
|
|
583
|
-
}
|
|
584
|
-
p.Refresh()
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
func (p *Pagination) Next() {
|
|
588
|
-
if p.current < p.total {
|
|
589
|
-
p.GoTo(p.current + 1)
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
func (p *Pagination) Prev() {
|
|
593
|
-
if p.current > 1 {
|
|
594
|
-
p.GoTo(p.current - 1)
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
func (p *Pagination) Current() int { return p.current }
|
|
598
|
-
func (p *Pagination) Total() int { return p.total }
|
|
599
|
-
|
|
600
|
-
// =============================================================================
|
|
601
|
-
// TOAST
|
|
602
|
-
// =============================================================================
|
|
603
|
-
|
|
604
|
-
type ToastType int
|
|
605
|
-
|
|
606
|
-
const (
|
|
607
|
-
ToastSuccess ToastType = iota
|
|
608
|
-
ToastError
|
|
609
|
-
ToastWarning
|
|
610
|
-
ToastInfo
|
|
611
|
-
)
|
|
612
|
-
|
|
613
|
-
func ShowToast(window fyne.Window, message string, toastType ToastType) {
|
|
614
|
-
c := DefaultColors()
|
|
615
|
-
var bgColor color.Color
|
|
616
|
-
var prefix string
|
|
617
|
-
switch toastType {
|
|
618
|
-
case ToastSuccess:
|
|
619
|
-
bgColor = c.Success
|
|
620
|
-
prefix = "✓ "
|
|
621
|
-
case ToastError:
|
|
622
|
-
bgColor = c.Error
|
|
623
|
-
prefix = "✕ "
|
|
624
|
-
case ToastWarning:
|
|
625
|
-
bgColor = c.Warning
|
|
626
|
-
prefix = "⚠ "
|
|
627
|
-
default:
|
|
628
|
-
bgColor = c.Info
|
|
629
|
-
prefix = "ℹ "
|
|
630
|
-
}
|
|
631
|
-
label := canvas.NewText(prefix+message, c.Text)
|
|
632
|
-
label.TextSize = 13
|
|
633
|
-
bg := canvas.NewRectangle(bgColor)
|
|
634
|
-
bg.CornerRadius = 10
|
|
635
|
-
cont := container.NewStack(bg, container.NewPadded(label))
|
|
636
|
-
popover := widget.NewPopUp(cont, window.Canvas())
|
|
637
|
-
popover.Move(fyne.NewPos(window.Canvas().Size().Width-320, window.Canvas().Size().Height-60))
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
// =============================================================================
|
|
641
|
-
// COMMAND PALETTE
|
|
642
|
-
// =============================================================================
|
|
643
|
-
|
|
644
|
-
type CommandItem struct {
|
|
645
|
-
Title, Subtitle, Kbd string
|
|
646
|
-
Action func()
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
// CommandPalette creates a modal command palette with search and filtering.
|
|
650
|
-
func NewCommandPalette(window fyne.Window, items []CommandItem) *fyne.Container {
|
|
651
|
-
c := DefaultColors()
|
|
652
|
-
searchEntry := widget.NewEntry()
|
|
653
|
-
searchEntry.SetPlaceHolder("Type to search commands...")
|
|
654
|
-
|
|
655
|
-
list := widget.NewList(
|
|
656
|
-
func() int { return len(items) },
|
|
657
|
-
func() fyne.CanvasObject {
|
|
658
|
-
return container.NewVBox(
|
|
659
|
-
widget.NewLabel("Command"),
|
|
660
|
-
widget.NewLabel("Description"),
|
|
661
|
-
)
|
|
662
|
-
},
|
|
663
|
-
func(id widget.ListItemID, item fyne.CanvasObject) {
|
|
664
|
-
if id < len(items) {
|
|
665
|
-
vbox := item.(*fyne.Container)
|
|
666
|
-
vbox.Objects[0].(*widget.Label).SetText(items[id].Title)
|
|
667
|
-
vbox.Objects[1].(*widget.Label).SetText(items[id].Subtitle)
|
|
668
|
-
}
|
|
669
|
-
},
|
|
670
|
-
)
|
|
671
|
-
|
|
672
|
-
searchEntry.OnChanged = func(query string) {
|
|
673
|
-
// Filter logic can be added here
|
|
674
|
-
list.Refresh()
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
list.OnSelected = func(id widget.ListItemID) {
|
|
678
|
-
if id < len(items) && items[id].Action != nil {
|
|
679
|
-
items[id].Action()
|
|
680
|
-
window.Hide()
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
content := container.NewVBox(
|
|
685
|
-
searchEntry,
|
|
686
|
-
container.NewScroll(list),
|
|
687
|
-
)
|
|
688
|
-
|
|
689
|
-
bg := canvas.NewRectangle(c.Surface)
|
|
690
|
-
bg.CornerRadius = 14
|
|
691
|
-
|
|
692
|
-
return container.NewStack(bg, container.NewPadded(content))
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
// =============================================================================
|
|
696
|
-
// SEARCH
|
|
697
|
-
// =============================================================================
|
|
698
|
-
|
|
699
|
-
type SearchItem struct {
|
|
700
|
-
Title, Subtitle string
|
|
701
|
-
Action func()
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
// Search creates a search widget with input and results list.
|
|
705
|
-
func NewSearch(items []SearchItem, onSelect func(SearchItem)) *fyne.Container {
|
|
706
|
-
c := DefaultColors()
|
|
707
|
-
searchEntry := widget.NewEntry()
|
|
708
|
-
searchEntry.SetPlaceHolder("Search...")
|
|
709
|
-
|
|
710
|
-
results := make([]SearchItem, 0)
|
|
711
|
-
resultsList := widget.NewList(
|
|
712
|
-
func() int { return len(results) },
|
|
713
|
-
func() fyne.CanvasObject {
|
|
714
|
-
return container.NewVBox(
|
|
715
|
-
widget.NewLabel("Title"),
|
|
716
|
-
widget.NewLabel("Subtitle"),
|
|
717
|
-
)
|
|
718
|
-
},
|
|
719
|
-
func(id widget.ListItemID, item fyne.CanvasObject) {
|
|
720
|
-
if id < len(results) {
|
|
721
|
-
vbox := item.(*fyne.Container)
|
|
722
|
-
vbox.Objects[0].(*widget.Label).SetText(results[id].Title)
|
|
723
|
-
vbox.Objects[1].(*widget.Label).SetText(results[id].Subtitle)
|
|
724
|
-
}
|
|
725
|
-
},
|
|
726
|
-
)
|
|
727
|
-
|
|
728
|
-
resultsList.OnSelected = func(id widget.ListItemID) {
|
|
729
|
-
if id < len(results) && onSelect != nil {
|
|
730
|
-
onSelect(results[id])
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
searchEntry.OnChanged = func(query string) {
|
|
735
|
-
results = make([]SearchItem, 0)
|
|
736
|
-
if query == "" {
|
|
737
|
-
resultsList.Refresh()
|
|
738
|
-
return
|
|
739
|
-
}
|
|
740
|
-
for _, item := range items {
|
|
741
|
-
// Simple case-insensitive search
|
|
742
|
-
if containsIgnoreCase(item.Title, query) || containsIgnoreCase(item.Subtitle, query) {
|
|
743
|
-
results = append(results, item)
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
resultsList.Refresh()
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
content := container.NewVBox(
|
|
750
|
-
searchEntry,
|
|
751
|
-
container.NewScroll(resultsList),
|
|
752
|
-
)
|
|
753
|
-
|
|
754
|
-
return content
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
func containsIgnoreCase(s, substr string) bool {
|
|
758
|
-
return strings.Contains(strings.ToLower(s), strings.ToLower(substr))
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
// =============================================================================
|
|
762
|
-
// TABLE
|
|
763
|
-
// =============================================================================
|
|
764
|
-
|
|
765
|
-
func NewTable(rows, cols int, cellFunc func(row, col int) fyne.CanvasObject) *widget.Table {
|
|
766
|
-
return widget.NewTable(
|
|
767
|
-
func() (int, int) { return rows, cols },
|
|
768
|
-
func() fyne.CanvasObject { return widget.NewLabel("") },
|
|
769
|
-
func(id widget.TableCellID, cell fyne.CanvasObject) {
|
|
770
|
-
if obj := cellFunc(id.Row, id.Col); obj != nil {
|
|
771
|
-
cell.(*widget.Label).SetText(obj.(*widget.Label).Text)
|
|
772
|
-
}
|
|
773
|
-
},
|
|
774
|
-
)
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
// =============================================================================
|
|
778
|
-
// LIST
|
|
779
|
-
// =============================================================================
|
|
780
|
-
|
|
781
|
-
type ListItem struct {
|
|
782
|
-
Title, Subtitle string
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
func NewList(items []ListItem) *widget.List {
|
|
786
|
-
return widget.NewList(
|
|
787
|
-
func() int { return len(items) },
|
|
788
|
-
func() fyne.CanvasObject { return widget.NewLabel("") },
|
|
789
|
-
func(id widget.ListItemID, item fyne.CanvasObject) {
|
|
790
|
-
item.(*widget.Label).SetText(items[id].Title)
|
|
791
|
-
},
|
|
792
|
-
)
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
// =============================================================================
|
|
796
|
-
// LAYOUT HELPERS
|
|
797
|
-
// =============================================================================
|
|
798
|
-
|
|
799
|
-
func NewContainer(content fyne.CanvasObject) fyne.CanvasObject {
|
|
800
|
-
return container.NewMax(content)
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
func NewFlex(objects ...fyne.CanvasObject) fyne.CanvasObject {
|
|
804
|
-
return container.NewHBox(objects...)
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
func NewStack(objects ...fyne.CanvasObject) fyne.CanvasObject {
|
|
808
|
-
return container.NewVBox(objects...)
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
func NewGrid(cols int, objects ...fyne.CanvasObject) fyne.CanvasObject {
|
|
812
|
-
return container.NewGridWithColumns(cols, objects...)
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
func NewForm(items ...*widget.FormItem) *widget.Form {
|
|
816
|
-
return widget.NewForm(items...)
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
// =============================================================================
|
|
820
|
-
// NAV
|
|
821
|
-
// =============================================================================
|
|
822
|
-
|
|
823
|
-
// Nav creates a navigation sidebar widget.
|
|
824
|
-
func NewNav(items []string, active int, onSelect func(int)) *widget.List {
|
|
825
|
-
list := widget.NewList(
|
|
826
|
-
func() int { return len(items) },
|
|
827
|
-
func() fyne.CanvasObject {
|
|
828
|
-
return widget.NewLabel("Nav Item")
|
|
829
|
-
},
|
|
830
|
-
func(id widget.ListItemID, item fyne.CanvasObject) {
|
|
831
|
-
if id < len(items) {
|
|
832
|
-
item.(*widget.Label).SetText(items[id])
|
|
833
|
-
}
|
|
834
|
-
},
|
|
835
|
-
)
|
|
836
|
-
list.OnSelected = func(id widget.ListItemID) {
|
|
837
|
-
if onSelect != nil {
|
|
838
|
-
onSelect(id)
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
if active >= 0 && active < len(items) {
|
|
842
|
-
list.Select(active)
|
|
843
|
-
}
|
|
844
|
-
return list
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
// =============================================================================
|
|
848
|
-
// STAT
|
|
849
|
-
// =============================================================================
|
|
850
|
-
|
|
851
|
-
type StatDeltaType int
|
|
852
|
-
|
|
853
|
-
const (
|
|
854
|
-
StatDeltaUp StatDeltaType = iota
|
|
855
|
-
StatDeltaDown
|
|
856
|
-
)
|
|
857
|
-
|
|
858
|
-
// Stat creates a stat/metric display widget with value, label, and optional delta.
|
|
859
|
-
func NewStat(value string, label string, delta string, deltaType StatDeltaType) *fyne.Container {
|
|
860
|
-
c := DefaultColors()
|
|
861
|
-
|
|
862
|
-
valueLabel := canvas.NewText(value, c.Text)
|
|
863
|
-
valueLabel.TextSize = 28
|
|
864
|
-
valueLabel.TextStyle = fyne.TextStyle{Bold: true}
|
|
865
|
-
|
|
866
|
-
labelWidget := canvas.NewText(label, c.TextMuted)
|
|
867
|
-
labelWidget.TextSize = 11
|
|
868
|
-
|
|
869
|
-
var deltaWidget *canvas.Text
|
|
870
|
-
if delta != "" {
|
|
871
|
-
deltaColor := c.SuccessText
|
|
872
|
-
if deltaType == StatDeltaDown {
|
|
873
|
-
deltaColor = c.ErrorText
|
|
874
|
-
}
|
|
875
|
-
prefix := "↑ "
|
|
876
|
-
if deltaType == StatDeltaDown {
|
|
877
|
-
prefix = "↓ "
|
|
878
|
-
}
|
|
879
|
-
deltaWidget = canvas.NewText(prefix+delta, deltaColor)
|
|
880
|
-
deltaWidget.TextSize = 11
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
content := container.NewVBox(
|
|
884
|
-
valueLabel,
|
|
885
|
-
labelWidget,
|
|
886
|
-
)
|
|
887
|
-
|
|
888
|
-
if deltaWidget != nil {
|
|
889
|
-
content = container.NewVBox(
|
|
890
|
-
valueLabel,
|
|
891
|
-
labelWidget,
|
|
892
|
-
deltaWidget,
|
|
893
|
-
)
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
bg := canvas.NewRectangle(c.Surface)
|
|
897
|
-
bg.StrokeColor = c.Border
|
|
898
|
-
bg.StrokeWidth = 1
|
|
899
|
-
bg.CornerRadius = 10
|
|
900
|
-
|
|
901
|
-
return container.NewStack(bg, container.NewPadded(content))
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
// =============================================================================
|
|
905
|
-
// APPLICATION
|
|
906
|
-
// =============================================================================
|
|
907
|
-
|
|
908
|
-
type App struct {
|
|
909
|
-
fyne.App
|
|
910
|
-
Theme fyne.Theme
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
func NewApp() *App {
|
|
914
|
-
a := app.New()
|
|
915
|
-
t := NewTheme()
|
|
916
|
-
a.Settings().SetTheme(t)
|
|
917
|
-
return &App{App: a, Theme: t}
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
func (a *App) NewWindow(title string) fyne.Window {
|
|
921
|
-
return a.App.NewWindow(title)
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
func Init() fyne.Theme {
|
|
925
|
-
return NewTheme()
|
|
926
|
-
}
|
|
1
|
+
package cronixui
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"image/color"
|
|
5
|
+
"strings"
|
|
6
|
+
|
|
7
|
+
"fyne.io/fyne/v2"
|
|
8
|
+
"fyne.io/fyne/v2/app"
|
|
9
|
+
"fyne.io/fyne/v2/canvas"
|
|
10
|
+
"fyne.io/fyne/v2/container"
|
|
11
|
+
"fyne.io/fyne/v2/layout"
|
|
12
|
+
"fyne.io/fyne/v2/theme"
|
|
13
|
+
"fyne.io/fyne/v2/widget"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
const Version = "1.0.6"
|
|
17
|
+
|
|
18
|
+
// =============================================================================
|
|
19
|
+
// DESIGN TOKENS
|
|
20
|
+
// =============================================================================
|
|
21
|
+
|
|
22
|
+
type Colors struct {
|
|
23
|
+
BG color.Color
|
|
24
|
+
Surface color.Color
|
|
25
|
+
Surface2 color.Color
|
|
26
|
+
Surface3 color.Color
|
|
27
|
+
Surface4 color.Color
|
|
28
|
+
Text color.Color
|
|
29
|
+
TextMuted color.Color
|
|
30
|
+
TextDim color.Color
|
|
31
|
+
Accent color.Color
|
|
32
|
+
AccentHover color.Color
|
|
33
|
+
AccentLight color.Color
|
|
34
|
+
AccentGlow color.Color
|
|
35
|
+
AccentText color.Color
|
|
36
|
+
Success color.Color
|
|
37
|
+
SuccessBorder color.Color
|
|
38
|
+
SuccessText color.Color
|
|
39
|
+
Warning color.Color
|
|
40
|
+
WarningBorder color.Color
|
|
41
|
+
WarningText color.Color
|
|
42
|
+
Error color.Color
|
|
43
|
+
ErrorBorder color.Color
|
|
44
|
+
ErrorText color.Color
|
|
45
|
+
Info color.Color
|
|
46
|
+
InfoBorder color.Color
|
|
47
|
+
InfoText color.Color
|
|
48
|
+
Border color.Color
|
|
49
|
+
BorderHover color.Color
|
|
50
|
+
BorderFocus color.Color
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
func DefaultColors() *Colors {
|
|
54
|
+
return &Colors{
|
|
55
|
+
BG: color.RGBA{R: 10, G: 10, B: 10, A: 255},
|
|
56
|
+
Surface: color.RGBA{R: 17, G: 17, B: 17, A: 255},
|
|
57
|
+
Surface2: color.RGBA{R: 26, G: 26, B: 26, A: 255},
|
|
58
|
+
Surface3: color.RGBA{R: 34, G: 34, B: 34, A: 255},
|
|
59
|
+
Surface4: color.RGBA{R: 42, G: 42, B: 42, A: 255},
|
|
60
|
+
Text: color.RGBA{R: 240, G: 237, B: 232, A: 255},
|
|
61
|
+
TextMuted: color.RGBA{R: 240, G: 237, B: 232, A: 128},
|
|
62
|
+
TextDim: color.RGBA{R: 240, G: 237, B: 232, A: 64},
|
|
63
|
+
Accent: color.RGBA{R: 107, G: 35, B: 35, A: 255},
|
|
64
|
+
AccentHover: color.RGBA{R: 125, G: 42, B: 42, A: 255},
|
|
65
|
+
AccentLight: color.RGBA{R: 138, G: 53, B: 53, A: 255},
|
|
66
|
+
AccentGlow: color.RGBA{R: 107, G: 35, B: 35, A: 77},
|
|
67
|
+
AccentText: color.RGBA{R: 201, G: 122, B: 122, A: 255},
|
|
68
|
+
Success: color.RGBA{R: 30, G: 80, B: 40, A: 255},
|
|
69
|
+
SuccessBorder: color.RGBA{R: 60, G: 140, B: 70, A: 102},
|
|
70
|
+
SuccessText: color.RGBA{R: 107, G: 196, B: 122, A: 255},
|
|
71
|
+
Warning: color.RGBA{R: 80, G: 60, B: 20, A: 255},
|
|
72
|
+
WarningBorder: color.RGBA{R: 150, G: 110, B: 30, A: 102},
|
|
73
|
+
WarningText: color.RGBA{R: 196, G: 164, B: 58, A: 255},
|
|
74
|
+
Error: color.RGBA{R: 80, G: 20, B: 20, A: 255},
|
|
75
|
+
ErrorBorder: color.RGBA{R: 180, G: 60, B: 60, A: 102},
|
|
76
|
+
ErrorText: color.RGBA{R: 196, G: 107, B: 107, A: 255},
|
|
77
|
+
Info: color.RGBA{R: 20, G: 53, B: 80, A: 255},
|
|
78
|
+
InfoBorder: color.RGBA{R: 60, G: 140, B: 200, A: 102},
|
|
79
|
+
InfoText: color.RGBA{R: 107, G: 168, B: 196, A: 255},
|
|
80
|
+
Border: color.RGBA{R: 255, G: 255, B: 255, A: 20},
|
|
81
|
+
BorderHover: color.RGBA{R: 255, G: 255, B: 255, A: 38},
|
|
82
|
+
BorderFocus: color.RGBA{R: 255, G: 255, B: 255, A: 64},
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
type Typography struct {
|
|
87
|
+
FontFamily string
|
|
88
|
+
FontMono string
|
|
89
|
+
FontSizeXS float32
|
|
90
|
+
FontSizeSM float32
|
|
91
|
+
FontSizeBase float32
|
|
92
|
+
FontSizeMD float32
|
|
93
|
+
FontSizeLG float32
|
|
94
|
+
FontSizeXL float32
|
|
95
|
+
FontSize2XL float32
|
|
96
|
+
FontSize3XL float32
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
func DefaultTypography() *Typography {
|
|
100
|
+
return &Typography{
|
|
101
|
+
FontFamily: "Outfit",
|
|
102
|
+
FontMono: "JetBrains Mono",
|
|
103
|
+
FontSizeXS: 11,
|
|
104
|
+
FontSizeSM: 12,
|
|
105
|
+
FontSizeBase: 13,
|
|
106
|
+
FontSizeMD: 14,
|
|
107
|
+
FontSizeLG: 16,
|
|
108
|
+
FontSizeXL: 20,
|
|
109
|
+
FontSize2XL: 28,
|
|
110
|
+
FontSize3XL: 36,
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
type Spacing struct {
|
|
115
|
+
Space1, Space2, Space3, Space4, Space5, Space6, Space8, Space10, Space12 float32
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
func DefaultSpacing() *Spacing {
|
|
119
|
+
return &Spacing{4, 8, 12, 16, 20, 24, 32, 40, 48}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
type RadiusTokens struct {
|
|
123
|
+
SM, Default, LG, XL, Full float32
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
func DefaultRadiusTokens() *RadiusTokens {
|
|
127
|
+
return &RadiusTokens{6, 10, 14, 20, 9999}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
type Shadow struct {
|
|
131
|
+
SM, Default, LG, Glow string
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
func DefaultShadow() *Shadow {
|
|
135
|
+
return &Shadow{
|
|
136
|
+
SM: "0 1px 2px rgba(0, 0, 0, 0.3)",
|
|
137
|
+
Default: "0 4px 12px rgba(0, 0, 0, 0.4)",
|
|
138
|
+
LG: "0 8px 24px rgba(0, 0, 0, 0.5)",
|
|
139
|
+
Glow: "0 0 20px rgba(107, 35, 35, 0.3)",
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
type Transition struct {
|
|
144
|
+
Fast, Default, Slow string
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
func DefaultTransition() *Transition {
|
|
148
|
+
return &Transition{"0.1s ease", "0.15s ease", "0.25s ease"}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
type ZIndexTokens struct {
|
|
152
|
+
Dropdown, Sticky, Fixed, ModalBackdrop, Modal, Popover, Tooltip, Toast int
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
func DefaultZIndexTokens() *ZIndexTokens {
|
|
156
|
+
return &ZIndexTokens{100, 200, 300, 400, 500, 600, 700, 800}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
type LayoutTokens struct {
|
|
160
|
+
ContainerMax, SidebarWidth, HeaderHeight float32
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
func DefaultLayoutTokens() *LayoutTokens {
|
|
164
|
+
return &LayoutTokens{1200, 260, 56}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// =============================================================================
|
|
168
|
+
// THEME
|
|
169
|
+
// =============================================================================
|
|
170
|
+
|
|
171
|
+
type Theme struct {
|
|
172
|
+
colors *Colors
|
|
173
|
+
spacing *Spacing
|
|
174
|
+
radius *RadiusTokens
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
func NewTheme() fyne.Theme {
|
|
178
|
+
return &Theme{
|
|
179
|
+
colors: DefaultColors(),
|
|
180
|
+
spacing: DefaultSpacing(),
|
|
181
|
+
radius: DefaultRadiusTokens(),
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
func (t *Theme) Color(name fyne.ThemeColorName, variant fyne.ThemeVariant) color.Color {
|
|
186
|
+
switch name {
|
|
187
|
+
case theme.ColorNameBackground:
|
|
188
|
+
return t.colors.BG
|
|
189
|
+
case theme.ColorNameButton:
|
|
190
|
+
return t.colors.Surface2
|
|
191
|
+
case theme.ColorNameDisabledButton:
|
|
192
|
+
return t.colors.Surface3
|
|
193
|
+
case theme.ColorNameInputBackground:
|
|
194
|
+
return t.colors.Surface
|
|
195
|
+
case theme.ColorNameInputBorder:
|
|
196
|
+
return t.colors.Border
|
|
197
|
+
case theme.ColorNameDisabled:
|
|
198
|
+
return t.colors.TextDim
|
|
199
|
+
case theme.ColorNameForeground:
|
|
200
|
+
return t.colors.Text
|
|
201
|
+
case theme.ColorNamePlaceHolder:
|
|
202
|
+
return t.colors.TextMuted
|
|
203
|
+
case theme.ColorNamePrimary:
|
|
204
|
+
return t.colors.Accent
|
|
205
|
+
case theme.ColorNameHover:
|
|
206
|
+
return t.colors.Surface3
|
|
207
|
+
case theme.ColorNamePressed:
|
|
208
|
+
return t.colors.Surface4
|
|
209
|
+
case theme.ColorNameScrollBar:
|
|
210
|
+
return t.colors.Surface3
|
|
211
|
+
case theme.ColorNameShadow:
|
|
212
|
+
return color.RGBA{R: 0, G: 0, B: 0, A: 50}
|
|
213
|
+
case theme.ColorNameSuccess:
|
|
214
|
+
return t.colors.Success
|
|
215
|
+
case theme.ColorNameWarning:
|
|
216
|
+
return t.colors.Warning
|
|
217
|
+
case theme.ColorNameError:
|
|
218
|
+
return t.colors.Error
|
|
219
|
+
default:
|
|
220
|
+
return t.colors.Text
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
func (t *Theme) Font(style fyne.TextStyle) fyne.Resource {
|
|
225
|
+
return theme.DefaultTheme().Font(style)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
func (t *Theme) Icon(name fyne.ThemeIconName) fyne.Resource {
|
|
229
|
+
return theme.DefaultTheme().Icon(name)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
func (t *Theme) Size(name fyne.ThemeSizeName) float32 {
|
|
233
|
+
switch name {
|
|
234
|
+
case theme.SizeNamePadding:
|
|
235
|
+
return t.spacing.Space2
|
|
236
|
+
case theme.SizeNameInlinePadding:
|
|
237
|
+
return t.spacing.Space3
|
|
238
|
+
case theme.SizeNameScrollBar:
|
|
239
|
+
return 8
|
|
240
|
+
case theme.SizeNameScrollBarSmall:
|
|
241
|
+
return 4
|
|
242
|
+
case theme.SizeNameSeparatorThickness:
|
|
243
|
+
return 1
|
|
244
|
+
case theme.SizeNameText:
|
|
245
|
+
return t.spacing.Space4
|
|
246
|
+
case theme.SizeNameHeadingText:
|
|
247
|
+
return t.spacing.Space6
|
|
248
|
+
case theme.SizeNameSubHeadingText:
|
|
249
|
+
return t.spacing.Space5
|
|
250
|
+
case theme.SizeNameCaptionText:
|
|
251
|
+
return t.spacing.Space2
|
|
252
|
+
case theme.SizeNameInputBorder:
|
|
253
|
+
return 1
|
|
254
|
+
case theme.SizeNameInputRadius:
|
|
255
|
+
return t.radius.Default
|
|
256
|
+
case theme.SizeNameSelectionRadius:
|
|
257
|
+
return t.radius.SM
|
|
258
|
+
default:
|
|
259
|
+
return 0
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// =============================================================================
|
|
264
|
+
// BUTTONS
|
|
265
|
+
// =============================================================================
|
|
266
|
+
|
|
267
|
+
type ButtonVariant int
|
|
268
|
+
|
|
269
|
+
const (
|
|
270
|
+
ButtonDefault ButtonVariant = iota
|
|
271
|
+
ButtonPrimary
|
|
272
|
+
ButtonGhost
|
|
273
|
+
ButtonOutline
|
|
274
|
+
ButtonDanger
|
|
275
|
+
ButtonSuccess
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
func NewButton(text string, variant ButtonVariant, onTap func()) *widget.Button {
|
|
279
|
+
btn := widget.NewButton(text, onTap)
|
|
280
|
+
switch variant {
|
|
281
|
+
case ButtonPrimary:
|
|
282
|
+
btn.Importance = widget.HighImportance
|
|
283
|
+
case ButtonDanger:
|
|
284
|
+
btn.Importance = widget.DangerImportance
|
|
285
|
+
case ButtonSuccess:
|
|
286
|
+
btn.Importance = widget.HighImportance
|
|
287
|
+
case ButtonGhost, ButtonOutline:
|
|
288
|
+
btn.Importance = widget.LowImportance
|
|
289
|
+
default:
|
|
290
|
+
btn.Importance = widget.MediumImportance
|
|
291
|
+
}
|
|
292
|
+
return btn
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
func NewButtonGroup(buttons ...*widget.Button) *fyne.Container {
|
|
296
|
+
return container.NewHBox(buttons...)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// =============================================================================
|
|
300
|
+
// CARD
|
|
301
|
+
// =============================================================================
|
|
302
|
+
|
|
303
|
+
// CardOption is a function that configures a card.
|
|
304
|
+
type CardOption func(*CardConfig)
|
|
305
|
+
|
|
306
|
+
// CardConfig holds card configuration.
|
|
307
|
+
type CardConfig struct {
|
|
308
|
+
Title string
|
|
309
|
+
Subtitle string
|
|
310
|
+
Icon fyne.CanvasObject
|
|
311
|
+
Footer fyne.CanvasObject
|
|
312
|
+
Clickable bool
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// WithCardTitle sets the card title.
|
|
316
|
+
func WithCardTitle(title string) CardOption {
|
|
317
|
+
return func(c *CardConfig) { c.Title = title }
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// WithCardSubtitle sets the card subtitle.
|
|
321
|
+
func WithCardSubtitle(subtitle string) CardOption {
|
|
322
|
+
return func(c *CardConfig) { c.Subtitle = subtitle }
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// WithCardIcon sets the card icon.
|
|
326
|
+
func WithCardIcon(icon fyne.CanvasObject) CardOption {
|
|
327
|
+
return func(c *CardConfig) { c.Icon = icon }
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// WithCardFooter sets the card footer.
|
|
331
|
+
func WithCardFooter(footer fyne.CanvasObject) CardOption {
|
|
332
|
+
return func(c *CardConfig) { c.Footer = footer }
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// WithCardClickable makes the card clickable.
|
|
336
|
+
func WithCardClickable(clickable bool) CardOption {
|
|
337
|
+
return func(c *CardConfig) { c.Clickable = clickable }
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// NewCard creates a card widget with optional icon, title, subtitle, and footer.
|
|
341
|
+
func NewCard(content fyne.CanvasObject, options ...CardOption) *fyne.Container {
|
|
342
|
+
config := &CardConfig{}
|
|
343
|
+
for _, opt := range options {
|
|
344
|
+
opt(config)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
c := DefaultColors()
|
|
348
|
+
|
|
349
|
+
var headerObjects []fyne.CanvasObject
|
|
350
|
+
if config.Icon != nil {
|
|
351
|
+
headerObjects = append(headerObjects, config.Icon)
|
|
352
|
+
}
|
|
353
|
+
if config.Title != "" {
|
|
354
|
+
titleText := canvas.NewText(config.Title, c.Text)
|
|
355
|
+
titleText.TextSize = 14
|
|
356
|
+
titleText.TextStyle = fyne.TextStyle{Bold: true}
|
|
357
|
+
headerObjects = append(headerObjects, titleText)
|
|
358
|
+
}
|
|
359
|
+
if config.Subtitle != "" {
|
|
360
|
+
subtitleText := canvas.NewText(config.Subtitle, c.TextMuted)
|
|
361
|
+
subtitleText.TextSize = 12
|
|
362
|
+
headerObjects = append(headerObjects, subtitleText)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
var header *fyne.Container
|
|
366
|
+
if len(headerObjects) > 0 {
|
|
367
|
+
header = container.NewVBox(headerObjects...)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
var body fyne.CanvasObject
|
|
371
|
+
if header != nil {
|
|
372
|
+
body = container.NewVBox(header, content)
|
|
373
|
+
} else {
|
|
374
|
+
body = content
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
var finalContent *fyne.Container
|
|
378
|
+
if config.Footer != nil {
|
|
379
|
+
finalContent = container.NewVBox(body, canvas.NewRectangle(c.Border), config.Footer)
|
|
380
|
+
} else {
|
|
381
|
+
if c, ok := body.(*fyne.Container); ok {
|
|
382
|
+
finalContent = c
|
|
383
|
+
} else {
|
|
384
|
+
finalContent = container.NewMax(body)
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
bg := canvas.NewRectangle(c.Surface)
|
|
389
|
+
bg.StrokeColor = c.Border
|
|
390
|
+
bg.StrokeWidth = 1
|
|
391
|
+
bg.CornerRadius = 14
|
|
392
|
+
|
|
393
|
+
return container.NewStack(bg, container.NewPadded(finalContent))
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// =============================================================================
|
|
397
|
+
// INPUTS
|
|
398
|
+
// =============================================================================
|
|
399
|
+
|
|
400
|
+
func NewInput(placeholder string) *widget.Entry {
|
|
401
|
+
entry := widget.NewEntry()
|
|
402
|
+
entry.SetPlaceHolder(placeholder)
|
|
403
|
+
return entry
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
func NewTextarea(placeholder string) *widget.Entry {
|
|
407
|
+
entry := widget.NewMultiLineEntry()
|
|
408
|
+
entry.SetPlaceHolder(placeholder)
|
|
409
|
+
return entry
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
func NewPassword(placeholder string) *widget.Entry {
|
|
413
|
+
entry := widget.NewPasswordEntry()
|
|
414
|
+
entry.SetPlaceHolder(placeholder)
|
|
415
|
+
return entry
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// =============================================================================
|
|
419
|
+
// FORM ELEMENTS
|
|
420
|
+
// =============================================================================
|
|
421
|
+
|
|
422
|
+
func NewCheckbox(label string, onChanged func(bool)) *widget.Check {
|
|
423
|
+
return widget.NewCheck(label, onChanged)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
func NewRadio(options []string, onChanged func(string)) *widget.RadioGroup {
|
|
427
|
+
return widget.NewRadioGroup(options, onChanged)
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
func NewSelect(options []string, onChanged func(string)) *widget.Select {
|
|
431
|
+
return widget.NewSelect(options, onChanged)
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
func NewSlider(min, max float64) *widget.Slider {
|
|
435
|
+
return widget.NewSlider(min, max)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// =============================================================================
|
|
439
|
+
// PROGRESS & LOADING
|
|
440
|
+
// =============================================================================
|
|
441
|
+
|
|
442
|
+
func NewProgress() *widget.ProgressBar {
|
|
443
|
+
return widget.NewProgressBar()
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
func NewProgressInfinite() *widget.ProgressBarInfinite {
|
|
447
|
+
return widget.NewProgressBarInfinite()
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// =============================================================================
|
|
451
|
+
// TOGGLE
|
|
452
|
+
// =============================================================================
|
|
453
|
+
|
|
454
|
+
type Toggle struct {
|
|
455
|
+
widget.BaseWidget
|
|
456
|
+
on bool
|
|
457
|
+
onToggle func(bool)
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
func NewToggle(onToggle func(bool)) *Toggle {
|
|
461
|
+
t := &Toggle{onToggle: onToggle}
|
|
462
|
+
t.ExtendBaseWidget(t)
|
|
463
|
+
return t
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
func (t *Toggle) Toggle() {
|
|
467
|
+
t.on = !t.on
|
|
468
|
+
if t.onToggle != nil {
|
|
469
|
+
t.onToggle(t.on)
|
|
470
|
+
}
|
|
471
|
+
t.Refresh()
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
func (t *Toggle) IsOn() bool { return t.on }
|
|
475
|
+
func (t *Toggle) SetOn(value bool) { t.on = value; t.Refresh() }
|
|
476
|
+
|
|
477
|
+
// =============================================================================
|
|
478
|
+
// MODAL
|
|
479
|
+
// =============================================================================
|
|
480
|
+
|
|
481
|
+
type Modal struct {
|
|
482
|
+
widget.BaseWidget
|
|
483
|
+
content fyne.CanvasObject
|
|
484
|
+
open bool
|
|
485
|
+
window fyne.Window
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
func NewModal(content fyne.CanvasObject) *Modal {
|
|
489
|
+
m := &Modal{content: content}
|
|
490
|
+
m.ExtendBaseWidget(m)
|
|
491
|
+
return m
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
func (m *Modal) Open(window fyne.Window) {
|
|
495
|
+
m.window = window
|
|
496
|
+
m.open = true
|
|
497
|
+
m.Show()
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
func (m *Modal) Close() { m.open = false; m.Hide() }
|
|
501
|
+
func (m *Modal) IsOpen() bool { return m.open }
|
|
502
|
+
func (m *Modal) Show() {
|
|
503
|
+
if m.window != nil {
|
|
504
|
+
popup := widget.NewModalPopUp(m.content, m.window.Canvas())
|
|
505
|
+
popup.Show()
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
func (m *Modal) Hide() {}
|
|
509
|
+
|
|
510
|
+
// =============================================================================
|
|
511
|
+
// DROPDOWN
|
|
512
|
+
// =============================================================================
|
|
513
|
+
|
|
514
|
+
// Dropdown creates a dropdown selection widget using Fyne's Select widget.
|
|
515
|
+
func NewDropdown(items []string, onSelect func(string)) *widget.Select {
|
|
516
|
+
s := widget.NewSelect(items, onSelect)
|
|
517
|
+
return s
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// =============================================================================
|
|
521
|
+
// TABS
|
|
522
|
+
// =============================================================================
|
|
523
|
+
|
|
524
|
+
func NewTabs(items ...*widget.TabItem) *container.AppTabs {
|
|
525
|
+
return container.NewAppTabs(items...)
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// =============================================================================
|
|
529
|
+
// ACCORDION
|
|
530
|
+
// =============================================================================
|
|
531
|
+
|
|
532
|
+
type AccordionItem struct {
|
|
533
|
+
Title string
|
|
534
|
+
Content fyne.CanvasObject
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
type Accordion struct {
|
|
538
|
+
widget.BaseWidget
|
|
539
|
+
items []AccordionItem
|
|
540
|
+
openIndices map[int]bool
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
func NewAccordion(items ...AccordionItem) *Accordion {
|
|
544
|
+
a := &Accordion{items: items, openIndices: make(map[int]bool)}
|
|
545
|
+
a.ExtendBaseWidget(a)
|
|
546
|
+
return a
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
func (a *Accordion) Toggle(index int) { a.openIndices[index] = !a.openIndices[index]; a.Refresh() }
|
|
550
|
+
func (a *Accordion) Open(index int) { a.openIndices[index] = true; a.Refresh() }
|
|
551
|
+
func (a *Accordion) Close(index int) { delete(a.openIndices, index); a.Refresh() }
|
|
552
|
+
func (a *Accordion) OpenAll() {
|
|
553
|
+
for i := range a.items {
|
|
554
|
+
a.openIndices[i] = true
|
|
555
|
+
}
|
|
556
|
+
a.Refresh()
|
|
557
|
+
}
|
|
558
|
+
func (a *Accordion) CloseAll() { a.openIndices = make(map[int]bool); a.Refresh() }
|
|
559
|
+
func (a *Accordion) IsOpen(index int) bool { return a.openIndices[index] }
|
|
560
|
+
|
|
561
|
+
// =============================================================================
|
|
562
|
+
// PAGINATION
|
|
563
|
+
// =============================================================================
|
|
564
|
+
|
|
565
|
+
type Pagination struct {
|
|
566
|
+
widget.BaseWidget
|
|
567
|
+
total int
|
|
568
|
+
current int
|
|
569
|
+
onChange func(int)
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
func NewPagination(total, current int, onChange func(int)) *Pagination {
|
|
573
|
+
p := &Pagination{total: total, current: current, onChange: onChange}
|
|
574
|
+
p.ExtendBaseWidget(p)
|
|
575
|
+
return p
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
func (p *Pagination) GoTo(page int) {
|
|
579
|
+
if page >= 1 && page <= p.total {
|
|
580
|
+
p.current = page
|
|
581
|
+
if p.onChange != nil {
|
|
582
|
+
p.onChange(page)
|
|
583
|
+
}
|
|
584
|
+
p.Refresh()
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
func (p *Pagination) Next() {
|
|
588
|
+
if p.current < p.total {
|
|
589
|
+
p.GoTo(p.current + 1)
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
func (p *Pagination) Prev() {
|
|
593
|
+
if p.current > 1 {
|
|
594
|
+
p.GoTo(p.current - 1)
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
func (p *Pagination) Current() int { return p.current }
|
|
598
|
+
func (p *Pagination) Total() int { return p.total }
|
|
599
|
+
|
|
600
|
+
// =============================================================================
|
|
601
|
+
// TOAST
|
|
602
|
+
// =============================================================================
|
|
603
|
+
|
|
604
|
+
type ToastType int
|
|
605
|
+
|
|
606
|
+
const (
|
|
607
|
+
ToastSuccess ToastType = iota
|
|
608
|
+
ToastError
|
|
609
|
+
ToastWarning
|
|
610
|
+
ToastInfo
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
func ShowToast(window fyne.Window, message string, toastType ToastType) {
|
|
614
|
+
c := DefaultColors()
|
|
615
|
+
var bgColor color.Color
|
|
616
|
+
var prefix string
|
|
617
|
+
switch toastType {
|
|
618
|
+
case ToastSuccess:
|
|
619
|
+
bgColor = c.Success
|
|
620
|
+
prefix = "✓ "
|
|
621
|
+
case ToastError:
|
|
622
|
+
bgColor = c.Error
|
|
623
|
+
prefix = "✕ "
|
|
624
|
+
case ToastWarning:
|
|
625
|
+
bgColor = c.Warning
|
|
626
|
+
prefix = "⚠ "
|
|
627
|
+
default:
|
|
628
|
+
bgColor = c.Info
|
|
629
|
+
prefix = "ℹ "
|
|
630
|
+
}
|
|
631
|
+
label := canvas.NewText(prefix+message, c.Text)
|
|
632
|
+
label.TextSize = 13
|
|
633
|
+
bg := canvas.NewRectangle(bgColor)
|
|
634
|
+
bg.CornerRadius = 10
|
|
635
|
+
cont := container.NewStack(bg, container.NewPadded(label))
|
|
636
|
+
popover := widget.NewPopUp(cont, window.Canvas())
|
|
637
|
+
popover.Move(fyne.NewPos(window.Canvas().Size().Width-320, window.Canvas().Size().Height-60))
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// =============================================================================
|
|
641
|
+
// COMMAND PALETTE
|
|
642
|
+
// =============================================================================
|
|
643
|
+
|
|
644
|
+
type CommandItem struct {
|
|
645
|
+
Title, Subtitle, Kbd string
|
|
646
|
+
Action func()
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// CommandPalette creates a modal command palette with search and filtering.
|
|
650
|
+
func NewCommandPalette(window fyne.Window, items []CommandItem) *fyne.Container {
|
|
651
|
+
c := DefaultColors()
|
|
652
|
+
searchEntry := widget.NewEntry()
|
|
653
|
+
searchEntry.SetPlaceHolder("Type to search commands...")
|
|
654
|
+
|
|
655
|
+
list := widget.NewList(
|
|
656
|
+
func() int { return len(items) },
|
|
657
|
+
func() fyne.CanvasObject {
|
|
658
|
+
return container.NewVBox(
|
|
659
|
+
widget.NewLabel("Command"),
|
|
660
|
+
widget.NewLabel("Description"),
|
|
661
|
+
)
|
|
662
|
+
},
|
|
663
|
+
func(id widget.ListItemID, item fyne.CanvasObject) {
|
|
664
|
+
if id < len(items) {
|
|
665
|
+
vbox := item.(*fyne.Container)
|
|
666
|
+
vbox.Objects[0].(*widget.Label).SetText(items[id].Title)
|
|
667
|
+
vbox.Objects[1].(*widget.Label).SetText(items[id].Subtitle)
|
|
668
|
+
}
|
|
669
|
+
},
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
searchEntry.OnChanged = func(query string) {
|
|
673
|
+
// Filter logic can be added here
|
|
674
|
+
list.Refresh()
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
list.OnSelected = func(id widget.ListItemID) {
|
|
678
|
+
if id < len(items) && items[id].Action != nil {
|
|
679
|
+
items[id].Action()
|
|
680
|
+
window.Hide()
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
content := container.NewVBox(
|
|
685
|
+
searchEntry,
|
|
686
|
+
container.NewScroll(list),
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
bg := canvas.NewRectangle(c.Surface)
|
|
690
|
+
bg.CornerRadius = 14
|
|
691
|
+
|
|
692
|
+
return container.NewStack(bg, container.NewPadded(content))
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// =============================================================================
|
|
696
|
+
// SEARCH
|
|
697
|
+
// =============================================================================
|
|
698
|
+
|
|
699
|
+
type SearchItem struct {
|
|
700
|
+
Title, Subtitle string
|
|
701
|
+
Action func()
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// Search creates a search widget with input and results list.
|
|
705
|
+
func NewSearch(items []SearchItem, onSelect func(SearchItem)) *fyne.Container {
|
|
706
|
+
c := DefaultColors()
|
|
707
|
+
searchEntry := widget.NewEntry()
|
|
708
|
+
searchEntry.SetPlaceHolder("Search...")
|
|
709
|
+
|
|
710
|
+
results := make([]SearchItem, 0)
|
|
711
|
+
resultsList := widget.NewList(
|
|
712
|
+
func() int { return len(results) },
|
|
713
|
+
func() fyne.CanvasObject {
|
|
714
|
+
return container.NewVBox(
|
|
715
|
+
widget.NewLabel("Title"),
|
|
716
|
+
widget.NewLabel("Subtitle"),
|
|
717
|
+
)
|
|
718
|
+
},
|
|
719
|
+
func(id widget.ListItemID, item fyne.CanvasObject) {
|
|
720
|
+
if id < len(results) {
|
|
721
|
+
vbox := item.(*fyne.Container)
|
|
722
|
+
vbox.Objects[0].(*widget.Label).SetText(results[id].Title)
|
|
723
|
+
vbox.Objects[1].(*widget.Label).SetText(results[id].Subtitle)
|
|
724
|
+
}
|
|
725
|
+
},
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
resultsList.OnSelected = func(id widget.ListItemID) {
|
|
729
|
+
if id < len(results) && onSelect != nil {
|
|
730
|
+
onSelect(results[id])
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
searchEntry.OnChanged = func(query string) {
|
|
735
|
+
results = make([]SearchItem, 0)
|
|
736
|
+
if query == "" {
|
|
737
|
+
resultsList.Refresh()
|
|
738
|
+
return
|
|
739
|
+
}
|
|
740
|
+
for _, item := range items {
|
|
741
|
+
// Simple case-insensitive search
|
|
742
|
+
if containsIgnoreCase(item.Title, query) || containsIgnoreCase(item.Subtitle, query) {
|
|
743
|
+
results = append(results, item)
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
resultsList.Refresh()
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
content := container.NewVBox(
|
|
750
|
+
searchEntry,
|
|
751
|
+
container.NewScroll(resultsList),
|
|
752
|
+
)
|
|
753
|
+
|
|
754
|
+
return content
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
func containsIgnoreCase(s, substr string) bool {
|
|
758
|
+
return strings.Contains(strings.ToLower(s), strings.ToLower(substr))
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// =============================================================================
|
|
762
|
+
// TABLE
|
|
763
|
+
// =============================================================================
|
|
764
|
+
|
|
765
|
+
func NewTable(rows, cols int, cellFunc func(row, col int) fyne.CanvasObject) *widget.Table {
|
|
766
|
+
return widget.NewTable(
|
|
767
|
+
func() (int, int) { return rows, cols },
|
|
768
|
+
func() fyne.CanvasObject { return widget.NewLabel("") },
|
|
769
|
+
func(id widget.TableCellID, cell fyne.CanvasObject) {
|
|
770
|
+
if obj := cellFunc(id.Row, id.Col); obj != nil {
|
|
771
|
+
cell.(*widget.Label).SetText(obj.(*widget.Label).Text)
|
|
772
|
+
}
|
|
773
|
+
},
|
|
774
|
+
)
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// =============================================================================
|
|
778
|
+
// LIST
|
|
779
|
+
// =============================================================================
|
|
780
|
+
|
|
781
|
+
type ListItem struct {
|
|
782
|
+
Title, Subtitle string
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
func NewList(items []ListItem) *widget.List {
|
|
786
|
+
return widget.NewList(
|
|
787
|
+
func() int { return len(items) },
|
|
788
|
+
func() fyne.CanvasObject { return widget.NewLabel("") },
|
|
789
|
+
func(id widget.ListItemID, item fyne.CanvasObject) {
|
|
790
|
+
item.(*widget.Label).SetText(items[id].Title)
|
|
791
|
+
},
|
|
792
|
+
)
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// =============================================================================
|
|
796
|
+
// LAYOUT HELPERS
|
|
797
|
+
// =============================================================================
|
|
798
|
+
|
|
799
|
+
func NewContainer(content fyne.CanvasObject) fyne.CanvasObject {
|
|
800
|
+
return container.NewMax(content)
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
func NewFlex(objects ...fyne.CanvasObject) fyne.CanvasObject {
|
|
804
|
+
return container.NewHBox(objects...)
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
func NewStack(objects ...fyne.CanvasObject) fyne.CanvasObject {
|
|
808
|
+
return container.NewVBox(objects...)
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
func NewGrid(cols int, objects ...fyne.CanvasObject) fyne.CanvasObject {
|
|
812
|
+
return container.NewGridWithColumns(cols, objects...)
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
func NewForm(items ...*widget.FormItem) *widget.Form {
|
|
816
|
+
return widget.NewForm(items...)
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// =============================================================================
|
|
820
|
+
// NAV
|
|
821
|
+
// =============================================================================
|
|
822
|
+
|
|
823
|
+
// Nav creates a navigation sidebar widget.
|
|
824
|
+
func NewNav(items []string, active int, onSelect func(int)) *widget.List {
|
|
825
|
+
list := widget.NewList(
|
|
826
|
+
func() int { return len(items) },
|
|
827
|
+
func() fyne.CanvasObject {
|
|
828
|
+
return widget.NewLabel("Nav Item")
|
|
829
|
+
},
|
|
830
|
+
func(id widget.ListItemID, item fyne.CanvasObject) {
|
|
831
|
+
if id < len(items) {
|
|
832
|
+
item.(*widget.Label).SetText(items[id])
|
|
833
|
+
}
|
|
834
|
+
},
|
|
835
|
+
)
|
|
836
|
+
list.OnSelected = func(id widget.ListItemID) {
|
|
837
|
+
if onSelect != nil {
|
|
838
|
+
onSelect(id)
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
if active >= 0 && active < len(items) {
|
|
842
|
+
list.Select(active)
|
|
843
|
+
}
|
|
844
|
+
return list
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// =============================================================================
|
|
848
|
+
// STAT
|
|
849
|
+
// =============================================================================
|
|
850
|
+
|
|
851
|
+
type StatDeltaType int
|
|
852
|
+
|
|
853
|
+
const (
|
|
854
|
+
StatDeltaUp StatDeltaType = iota
|
|
855
|
+
StatDeltaDown
|
|
856
|
+
)
|
|
857
|
+
|
|
858
|
+
// Stat creates a stat/metric display widget with value, label, and optional delta.
|
|
859
|
+
func NewStat(value string, label string, delta string, deltaType StatDeltaType) *fyne.Container {
|
|
860
|
+
c := DefaultColors()
|
|
861
|
+
|
|
862
|
+
valueLabel := canvas.NewText(value, c.Text)
|
|
863
|
+
valueLabel.TextSize = 28
|
|
864
|
+
valueLabel.TextStyle = fyne.TextStyle{Bold: true}
|
|
865
|
+
|
|
866
|
+
labelWidget := canvas.NewText(label, c.TextMuted)
|
|
867
|
+
labelWidget.TextSize = 11
|
|
868
|
+
|
|
869
|
+
var deltaWidget *canvas.Text
|
|
870
|
+
if delta != "" {
|
|
871
|
+
deltaColor := c.SuccessText
|
|
872
|
+
if deltaType == StatDeltaDown {
|
|
873
|
+
deltaColor = c.ErrorText
|
|
874
|
+
}
|
|
875
|
+
prefix := "↑ "
|
|
876
|
+
if deltaType == StatDeltaDown {
|
|
877
|
+
prefix = "↓ "
|
|
878
|
+
}
|
|
879
|
+
deltaWidget = canvas.NewText(prefix+delta, deltaColor)
|
|
880
|
+
deltaWidget.TextSize = 11
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
content := container.NewVBox(
|
|
884
|
+
valueLabel,
|
|
885
|
+
labelWidget,
|
|
886
|
+
)
|
|
887
|
+
|
|
888
|
+
if deltaWidget != nil {
|
|
889
|
+
content = container.NewVBox(
|
|
890
|
+
valueLabel,
|
|
891
|
+
labelWidget,
|
|
892
|
+
deltaWidget,
|
|
893
|
+
)
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
bg := canvas.NewRectangle(c.Surface)
|
|
897
|
+
bg.StrokeColor = c.Border
|
|
898
|
+
bg.StrokeWidth = 1
|
|
899
|
+
bg.CornerRadius = 10
|
|
900
|
+
|
|
901
|
+
return container.NewStack(bg, container.NewPadded(content))
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// =============================================================================
|
|
905
|
+
// APPLICATION
|
|
906
|
+
// =============================================================================
|
|
907
|
+
|
|
908
|
+
type App struct {
|
|
909
|
+
fyne.App
|
|
910
|
+
Theme fyne.Theme
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
func NewApp() *App {
|
|
914
|
+
a := app.New()
|
|
915
|
+
t := NewTheme()
|
|
916
|
+
a.Settings().SetTheme(t)
|
|
917
|
+
return &App{App: a, Theme: t}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
func (a *App) NewWindow(title string) fyne.Window {
|
|
921
|
+
return a.App.NewWindow(title)
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
func Init() fyne.Theme {
|
|
925
|
+
return NewTheme()
|
|
926
|
+
}
|