/ internal / desktop / components.go
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  }