components.go
1 package desktop 2 3 import ( 4 "fmt" 5 "image/color" 6 "time" 7 8 "fyne.io/fyne/v2" 9 "fyne.io/fyne/v2/canvas" 10 "fyne.io/fyne/v2/container" 11 "fyne.io/fyne/v2/driver/desktop" 12 "fyne.io/fyne/v2/layout" 13 "fyne.io/fyne/v2/theme" 14 "fyne.io/fyne/v2/widget" 15 ) 16 17 // 全局主题管理器实例 18 var globalThemeManager *ThemeManager 19 20 // SetGlobalThemeManager 设置全局主题管理器 21 func SetGlobalThemeManager(tm *ThemeManager) { 22 globalThemeManager = tm 23 } 24 25 func GetCurrentThemeIsDark() bool { 26 if globalThemeManager != nil { 27 return globalThemeManager.IsDarkMode() 28 } 29 return false 30 } 31 32 // FadeAnimation 淡入淡出动画 33 func FadeAnimation(content fyne.CanvasObject, duration time.Duration, startOpacity, endOpacity float64) { 34 rect := canvas.NewRectangle(color.NRGBA{R: 240, G: 246, B: 252, A: 0}) 35 rect.FillColor = color.NRGBA{R: 240, G: 246, B: 252, A: uint8(startOpacity * 255)} 36 37 anim := canvas.NewColorRGBAAnimation( 38 color.NRGBA{R: 240, G: 246, B: 252, A: uint8(startOpacity * 255)}, 39 color.NRGBA{R: 240, G: 246, B: 252, A: uint8(endOpacity * 255)}, 40 duration, 41 func(c color.Color) { 42 rect.FillColor = c 43 content.Refresh() 44 }) 45 46 anim.Start() 47 } 48 49 // GlassmorphismCard 毛玻璃效果卡片 50 func GlassmorphismCard(title, subtitle string, content fyne.CanvasObject, isDark bool) *fyne.Container { 51 var bgColor color.Color 52 var titleColor color.Color 53 var subtitleColor color.Color 54 var borderColor color.Color 55 56 if isDark { 57 // 夜晚主题毛玻璃效果 58 bgColor = color.NRGBA{R: 30, G: 41, B: 59, A: 180} // 半透明深色背景 59 titleColor = color.NRGBA{R: 248, G: 250, B: 252, A: 255} 60 subtitleColor = color.NRGBA{R: 148, G: 163, B: 184, A: 200} 61 borderColor = color.NRGBA{R: 51, G: 65, B: 85, A: 100} 62 } else { 63 // 明亮主题毛玻璃效果 64 bgColor = color.NRGBA{R: 255, G: 255, B: 255, A: 180} // 半透明白色背景 65 titleColor = color.NRGBA{R: 17, G: 24, B: 39, A: 255} 66 subtitleColor = color.NRGBA{R: 107, G: 114, B: 128, A: 200} 67 borderColor = color.NRGBA{R: 209, G: 213, B: 219, A: 100} 68 } 69 70 // 创建毛玻璃背景 71 glassBackground := canvas.NewRectangle(bgColor) 72 glassBackground.CornerRadius = 16 73 glassBackground.StrokeColor = borderColor 74 glassBackground.StrokeWidth = 1 75 76 // 标题 77 titleLabel := canvas.NewText(title, titleColor) 78 titleLabel.TextSize = 18 79 titleLabel.TextStyle = fyne.TextStyle{Bold: true} 80 81 // 副标题 82 var subtitleLabel *canvas.Text 83 if subtitle != "" { 84 subtitleLabel = canvas.NewText(subtitle, subtitleColor) 85 subtitleLabel.TextSize = 14 86 } 87 88 // 标题容器 89 var headerContainer *fyne.Container 90 if subtitleLabel != nil { 91 headerContainer = container.NewVBox(titleLabel, subtitleLabel) 92 } else { 93 headerContainer = container.NewVBox(titleLabel) 94 } 95 96 // 分隔线 97 dividerColor := color.NRGBA{R: 209, G: 213, B: 219, A: 100} 98 if isDark { 99 dividerColor = color.NRGBA{R: 51, G: 65, B: 85, A: 100} 100 } 101 divider := canvas.NewLine(dividerColor) 102 divider.StrokeWidth = 1 103 104 contentWithPadding := container.NewPadded(content) 105 106 // 布局 107 cardContent := container.NewBorder( 108 container.NewVBox(container.NewPadded(headerContainer), divider), 109 nil, nil, nil, 110 contentWithPadding, 111 ) 112 113 // 多层阴影效果 114 shadow1 := canvas.NewRectangle(color.NRGBA{R: 0, G: 0, B: 0, A: 10}) 115 shadow1.Move(fyne.NewPos(2, 2)) 116 shadow1.CornerRadius = 16 117 118 shadow2 := canvas.NewRectangle(color.NRGBA{R: 0, G: 0, B: 0, A: 5}) 119 shadow2.Move(fyne.NewPos(4, 4)) 120 shadow2.CornerRadius = 16 121 122 return container.NewStack(shadow2, shadow1, glassBackground, cardContent) 123 } 124 125 // TransparentCard 透明效果卡片 126 func TransparentCard(content fyne.CanvasObject, isDark bool) *fyne.Container { 127 var bgColor color.Color 128 var borderColor color.Color 129 130 if isDark { 131 bgColor = color.NRGBA{R: 30, G: 41, B: 59, A: 120} 132 borderColor = color.NRGBA{R: 51, G: 65, B: 85, A: 80} 133 } else { 134 bgColor = color.NRGBA{R: 255, G: 255, B: 255, A: 120} 135 borderColor = color.NRGBA{R: 209, G: 213, B: 219, A: 80} 136 } 137 138 background := canvas.NewRectangle(bgColor) 139 background.CornerRadius = 12 140 background.StrokeColor = borderColor 141 background.StrokeWidth = 1 142 143 return container.NewStack(background, container.NewPadded(content)) 144 } 145 146 func PrimaryButton(text string, icon fyne.Resource, action func()) *widget.Button { 147 btn := widget.NewButtonWithIcon(text, icon, action) 148 btn.Importance = widget.HighImportance 149 return btn 150 } 151 152 func SecondaryButton(text string, icon fyne.Resource, action func()) *widget.Button { 153 btn := widget.NewButtonWithIcon(text, icon, action) 154 btn.Importance = widget.MediumImportance 155 return btn 156 } 157 158 func GhostButton(text string, icon fyne.Resource, action func()) *widget.Button { 159 btn := widget.NewButtonWithIcon(text, icon, action) 160 btn.Importance = widget.LowImportance 161 return btn 162 } 163 164 func TitleText(text string) *canvas.Text { 165 title := canvas.NewText(text, theme.Color(theme.ColorNamePrimary)) 166 title.TextSize = 24 167 title.TextStyle = fyne.TextStyle{Bold: true} 168 title.Alignment = fyne.TextAlignCenter 169 return title 170 } 171 172 func SubtitleText(text string) *canvas.Text { 173 subtitle := canvas.NewText(text, theme.Color(theme.ColorNameForeground)) 174 subtitle.TextSize = 16 175 subtitle.TextStyle = fyne.TextStyle{Italic: true} 176 subtitle.Alignment = fyne.TextAlignCenter 177 return subtitle 178 } 179 180 func createShadowRectangle(fillColor color.Color, cornerRadius float32) *canvas.Rectangle { 181 rect := canvas.NewRectangle(fillColor) 182 rect.CornerRadius = cornerRadius 183 return rect 184 } 185 186 func GlassCard(title, subtitle string, content fyne.CanvasObject) *fyne.Container { 187 return GlassmorphismCard(title, subtitle, content, false) 188 } 189 190 // StyledCard 样式化卡片 - 优化版本 191 func StyledCard(title string, content fyne.CanvasObject) *fyne.Container { 192 bg := createShadowRectangle(color.NRGBA{R: 250, G: 251, B: 254, A: 255}, 12) 193 194 titleLabel := canvas.NewText(title, color.NRGBA{R: 17, G: 24, B: 39, A: 255}) 195 titleLabel.TextSize = 16 196 titleLabel.TextStyle = fyne.TextStyle{Bold: true} 197 198 divider := canvas.NewRectangle(color.NRGBA{R: 229, G: 231, B: 235, A: 255}) 199 divider.SetMinSize(fyne.NewSize(0, 1)) 200 201 // 组合 202 contentContainer := container.NewBorder( 203 container.NewVBox( 204 container.NewPadded(titleLabel), 205 divider, 206 ), 207 nil, nil, nil, 208 container.NewPadded(content), 209 ) 210 211 shadow := canvas.NewRectangle(color.NRGBA{R: 0, G: 0, B: 0, A: 15}) 212 shadow.Move(fyne.NewPos(2, 2)) 213 shadow.SetMinSize(fyne.NewSize(contentContainer.Size().Width+4, contentContainer.Size().Height+4)) 214 shadow.CornerRadius = 12 215 216 return container.NewStack(shadow, bg, contentContainer) 217 } 218 219 // StyledSelect 样式化选择器 220 func StyledSelect(options []string, selected func(string)) *widget.Select { 221 sel := widget.NewSelect(options, selected) 222 223 // 针对包含"翻译后字幕"的选项增加宽度 224 for _, option := range options { 225 if len(option) > 8 { 226 extraOptions := make([]string, len(options)) 227 copy(extraOptions, options) 228 229 maxOption := "" 230 for _, opt := range options { 231 if len(opt) > len(maxOption) { 232 maxOption = opt 233 } 234 } 235 236 // 添加额外空格来扩展宽度 237 padding := " " 238 if len(maxOption) < 20 { 239 maxOption = maxOption + padding 240 } 241 242 sel = widget.NewSelect(extraOptions, selected) 243 break 244 } 245 } 246 247 return sel 248 } 249 250 // StyledEntry 样式化输入框 251 func StyledEntry(placeholder string) *widget.Entry { 252 entry := widget.NewEntry() 253 entry.SetPlaceHolder(placeholder) 254 return entry 255 } 256 257 // StyledPasswordEntry 样式化密码输入框 258 func StyledPasswordEntry(placeholder string) *widget.Entry { 259 entry := widget.NewPasswordEntry() 260 entry.SetPlaceHolder(placeholder) 261 return entry 262 } 263 264 // DividedContainer 分隔容器 265 func DividedContainer(vertical bool, items ...fyne.CanvasObject) *fyne.Container { 266 if len(items) <= 1 { 267 if len(items) == 1 { 268 return container.NewPadded(items[0]) 269 } 270 return container.NewPadded() 271 } 272 273 var dividers []fyne.CanvasObject 274 for i := 0; i < len(items)-1; i++ { 275 dividers = append(dividers, createDivider(vertical)) 276 } 277 278 var objects []fyne.CanvasObject 279 for i, item := range items { 280 objects = append(objects, item) 281 if i < len(dividers) { 282 objects = append(objects, dividers[i]) 283 } 284 } 285 286 if vertical { 287 return container.New(layout.NewVBoxLayout(), objects...) 288 } 289 return container.New(layout.NewHBoxLayout(), objects...) 290 } 291 292 // createDivider 创建分隔线 293 func createDivider(vertical bool) fyne.CanvasObject { 294 divider := canvas.NewRectangle(color.NRGBA{R: 209, G: 213, B: 219, A: 255}) 295 if vertical { 296 divider.SetMinSize(fyne.NewSize(0, 1)) 297 } else { 298 divider.SetMinSize(fyne.NewSize(1, 0)) 299 } 300 return divider 301 } 302 303 // ProgressWithLabel 进度条带标签 304 func ProgressWithLabel(initial float64) (*widget.ProgressBar, *widget.Label, *fyne.Container) { 305 progress := widget.NewProgressBar() 306 progress.SetValue(initial) 307 308 label := widget.NewLabel("0%") 309 310 container := container.NewBorder(nil, nil, nil, label, progress) 311 312 return progress, label, container 313 } 314 315 // UpdateProgressLabel 更新进度条标签 316 func UpdateProgressLabel(progress *widget.ProgressBar, label *widget.Label) { 317 percentage := int(progress.Value * 100) 318 label.SetText(fmt.Sprintf("%d%%", percentage)) 319 } 320 321 // AnimatedContainer 动画容器 322 func AnimatedContainer() *fyne.Container { 323 return container.NewStack() 324 } 325 326 // SwitchContent 切换内容 327 func SwitchContent(container *fyne.Container, content fyne.CanvasObject, duration time.Duration) { 328 if container == nil || content == nil { 329 return 330 } 331 332 if len(container.Objects) > 0 { 333 oldContent := container.Objects[0] 334 FadeAnimation(oldContent, duration/2, 1.0, 0.0) 335 336 go func() { 337 defer func() { 338 if r := recover(); r != nil { 339 fmt.Println("内容切换时发生错误:", r) 340 } 341 }() 342 343 time.Sleep(duration / 2) 344 container.Objects = []fyne.CanvasObject{content} 345 container.Refresh() 346 FadeAnimation(content, duration/2, 0.0, 1.0) 347 }() 348 } else { 349 container.Objects = []fyne.CanvasObject{content} 350 container.Refresh() 351 FadeAnimation(content, duration/2, 0.0, 1.0) 352 } 353 } 354 355 // ModernCard 现代卡片组件 356 func ModernCard(title string, content fyne.CanvasObject, isDark bool) *fyne.Container { 357 var bgColor color.Color 358 var titleColor color.Color 359 var borderColor color.Color 360 361 if isDark { 362 bgColor = color.NRGBA{R: 30, G: 41, B: 59, A: 255} 363 titleColor = color.NRGBA{R: 248, G: 250, B: 252, A: 255} 364 borderColor = color.NRGBA{R: 51, G: 65, B: 85, A: 255} 365 } else { 366 bgColor = color.NRGBA{R: 255, G: 255, B: 255, A: 255} 367 titleColor = color.NRGBA{R: 17, G: 24, B: 39, A: 255} 368 borderColor = color.NRGBA{R: 209, G: 213, B: 219, A: 255} 369 } 370 371 background := canvas.NewRectangle(bgColor) 372 background.CornerRadius = 12 373 background.StrokeColor = borderColor 374 background.StrokeWidth = 1 375 376 titleLabel := canvas.NewText(title, titleColor) 377 titleLabel.TextSize = 16 378 titleLabel.TextStyle = fyne.TextStyle{Bold: true} 379 380 divider := canvas.NewRectangle(color.NRGBA{R: 229, G: 231, B: 235, A: 255}) 381 if isDark { 382 divider.FillColor = color.NRGBA{R: 51, G: 65, B: 85, A: 255} 383 } 384 divider.SetMinSize(fyne.NewSize(0, 1)) 385 386 contentContainer := container.NewBorder( 387 container.NewVBox( 388 container.NewPadded(titleLabel), 389 divider, 390 ), 391 nil, nil, nil, 392 container.NewPadded(content), 393 ) 394 395 // 阴影效果 396 shadow := canvas.NewRectangle(color.NRGBA{R: 0, G: 0, B: 0, A: 10}) 397 shadow.Move(fyne.NewPos(2, 2)) 398 shadow.CornerRadius = 12 399 400 return container.NewStack(shadow, background, contentContainer) 401 } 402 403 // ThemeToggleButton 主题切换按钮 404 func ThemeToggleButton(isDark bool, onToggle func()) *widget.Button { 405 var icon fyne.Resource 406 var text string 407 408 if isDark { 409 icon = theme.ColorPaletteIcon() 410 text = "明亮模式" 411 } else { 412 icon = theme.ColorPaletteIcon() 413 text = "夜晚模式" 414 } 415 416 btn := widget.NewButtonWithIcon(text, icon, onToggle) 417 btn.Importance = widget.MediumImportance 418 return btn 419 } 420 421 // 自定义可点击对象,避免按钮的默认样式 422 type tappableObject struct { 423 widget.BaseWidget 424 rect *canvas.Rectangle 425 onTap func() 426 onHover func(bool) // 悬停回调函数 427 } 428 429 func (t *tappableObject) CreateRenderer() fyne.WidgetRenderer { 430 return widget.NewSimpleRenderer(t.rect) 431 } 432 433 func (t *tappableObject) Tapped(*fyne.PointEvent) { 434 if t.onTap != nil { 435 t.onTap() 436 } 437 } 438 439 func (t *tappableObject) TappedSecondary(*fyne.PointEvent) {} 440 441 func (t *tappableObject) MouseIn(*desktop.MouseEvent) { 442 if t.onHover != nil { 443 t.onHover(true) 444 } 445 } 446 447 func (t *tappableObject) MouseOut() { 448 if t.onHover != nil { 449 t.onHover(false) 450 } 451 } 452 453 func (t *tappableObject) MouseMoved(*desktop.MouseEvent) {} 454 455 func (t *tappableObject) Resize(size fyne.Size) { 456 t.BaseWidget.Resize(size) 457 if t.rect != nil { 458 t.rect.Resize(size) 459 } 460 } 461 462 func (t *tappableObject) Move(pos fyne.Position) { 463 t.BaseWidget.Move(pos) 464 if t.rect != nil { 465 t.rect.Move(pos) 466 } 467 }