package gio import ( "fmt" "image" "image/color" "gioui.org/io/semantic" "gioui.org/layout" "gioui.org/unit" "gioui.org/widget" "gioui.org/widget/material" "softbox.local/core/application" ) type rowControls struct { open widget.Clickable } func (shell *AppShell) layoutContent( gtx layout.Context, theme *material.Theme, ) layout.Dimensions { return layout.Flex{}.Layout( gtx, layout.Rigid(func(gtx layout.Context) layout.Dimensions { width := gtx.Dp(unit.Dp(152)) gtx.Constraints.Min.X = width gtx.Constraints.Max.X = width return panel( gtx, shellColors.muted, unit.Dp(6), layout.UniformInset(unit.Dp(10)), func(gtx layout.Context) layout.Dimensions { return layout.Flex{Axis: layout.Vertical}.Layout( gtx, layout.Rigid(func(gtx layout.Context) layout.Dimensions { return shell.layoutViewButton( gtx, theme, &shell.viewAll, "全部软件", application.CatalogViewAll, ) }), layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { return shell.layoutViewButton( gtx, theme, &shell.viewInstalled, "已安装", application.CatalogViewInstalled, ) }), layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { return shell.layoutViewButton( gtx, theme, &shell.viewUpdates, "可更新", application.CatalogViewUpdates, ) }), ) }, ) }), layout.Rigid(layout.Spacer{Width: unit.Dp(12)}.Layout), layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { selected, hasSelection := shell.model.SelectedItem() return layout.Flex{}.Layout( gtx, layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { return panel( gtx, shellColors.surface, unit.Dp(6), layout.UniformInset(unit.Dp(12)), func(gtx layout.Context) layout.Dimensions { visible := shell.model.VisibleItems() return layout.Flex{Axis: layout.Vertical}.Layout( gtx, layout.Rigid(func(gtx layout.Context) layout.Dimensions { return layout.Flex{Alignment: layout.Middle}.Layout( gtx, layout.Rigid(material.H6(theme, viewTitle(shell.model.View())).Layout), layout.Flexed(1, layout.Spacer{}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { label := material.Caption( theme, fmt.Sprintf( "%d / %d 项", len(visible), shell.model.TotalCount(), ), ) label.Color = shellColors.secondary return label.Layout(gtx) }), ) }), layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { if len(visible) == 0 { return shell.layoutEmptyState(gtx, theme) } return shell.appList.Layout(gtx, len(visible), func( gtx layout.Context, index int, ) layout.Dimensions { shell.lastRendered++ return shell.layoutAppRow(gtx, theme, visible[index]) }) }), ) }, ) }), layout.Rigid(func(gtx layout.Context) layout.Dimensions { if !hasSelection { return layout.Dimensions{} } return layout.Spacer{Width: unit.Dp(8)}.Layout(gtx) }), layout.Rigid(func(gtx layout.Context) layout.Dimensions { if !hasSelection { return layout.Dimensions{} } width := gtx.Dp(unit.Dp(280)) gtx.Constraints.Min.X = width gtx.Constraints.Max.X = width return shell.layoutDetail(gtx, theme, selected) }), ) }), ) } func (shell *AppShell) layoutAppRow( gtx layout.Context, theme *material.Theme, item application.CatalogListItem, ) layout.Dimensions { controls := shell.rows[item.ID] if controls == nil { return layout.Dimensions{} } return layout.Inset{Bottom: unit.Dp(6)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions { gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(80)) return controls.open.Layout(gtx, func(gtx layout.Context) layout.Dimensions { semantic.Button.Add(gtx.Ops) semantic.DescriptionOp(fmt.Sprintf( "%s,版本 %s,状态 %s", item.Name, item.Version, statusLabel(item.Status), )).Add(gtx.Ops) background := shellColors.muted if controls.open.Hovered() || gtx.Focused(&controls.open) { background = color.NRGBA{R: 236, G: 253, B: 245, A: 255} } if shell.model.SelectedID() == item.ID { background = color.NRGBA{R: 220, G: 252, B: 231, A: 255} } return panel( gtx, background, unit.Dp(5), layout.UniformInset(unit.Dp(10)), func(gtx layout.Context) layout.Dimensions { return layout.Flex{Alignment: layout.Middle}.Layout( gtx, layout.Rigid(func(gtx layout.Context) layout.Dimensions { return shell.layoutAppIcon( gtx, theme, item.ID, item.Name, unit.Dp(40), unit.Dp(5), ) }), layout.Rigid(layout.Spacer{Width: unit.Dp(10)}.Layout), layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { return layout.Flex{Axis: layout.Vertical}.Layout( gtx, layout.Rigid(material.Body1(theme, item.Name).Layout), layout.Rigid(layout.Spacer{Height: unit.Dp(3)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { label := material.Caption( theme, fmt.Sprintf("%s · %s · %s", item.ID, item.Version, item.Category), ) label.Color = shellColors.secondary return label.Layout(gtx) }), ) }), layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { label := material.Body2(theme, statusLabel(item.Status)) label.Color = statusColor(item.Status) return label.Layout(gtx) }), ) }, ) }) }) } func (shell *AppShell) layoutAppIcon( gtx layout.Context, theme *material.Theme, appID string, name string, iconSize unit.Dp, radius unit.Dp, ) layout.Dimensions { size := gtx.Dp(iconSize) gtx.Constraints.Min = image.Pt(size, size) gtx.Constraints.Max = gtx.Constraints.Min if icon, exists := shell.icons[appID]; exists { return panel( gtx, shellColors.surface, radius, layout.UniformInset(unit.Dp(2)), func(gtx layout.Context) layout.Dimensions { return widget.Image{ Src: icon, Fit: widget.Contain, Position: layout.Center, }.Layout(gtx) }, ) } letter := "S" for _, character := range name { letter = string(character) break } return panel( gtx, shellColors.primary, radius, layout.UniformInset(unit.Dp(0)), func(gtx layout.Context) layout.Dimensions { return layout.Center.Layout(gtx, func(gtx layout.Context) layout.Dimensions { label := material.Body1(theme, letter) label.Color = shellColors.onPrimary return label.Layout(gtx) }) }, ) } func (shell *AppShell) layoutEmptyState( gtx layout.Context, theme *material.Theme, ) layout.Dimensions { return layout.Center.Layout(gtx, func(gtx layout.Context) layout.Dimensions { title := "没有匹配的软件" body := "清除搜索词、分类或视图筛选后重试。" showReset := shell.model.TotalCount() > 0 if shell.model.TotalCount() == 0 { title = "软件目录尚未加载" body = "联网刷新或读取已验证缓存后会显示软件。" switch shell.catalogState { case catalogStateLoading: title = "正在加载软件目录" body = "正在等待已验证 Catalog 快照。" case catalogStateUnconfigured: title = "Catalog 来源尚未配置" body = "此构建未装配可信发布配置,因此未显示任何软件。" case catalogStateLoadFailed: title = "Catalog 加载失败" body = "未收到可验证的 Catalog;已显示的目录不会被清除。" case catalogStateReady: title = "Catalog 暂无可显示软件" body = "已验证 Catalog 没有适用于当前目标的软件。" } } return layout.Flex{Axis: layout.Vertical, Alignment: layout.Middle}.Layout( gtx, layout.Rigid(material.H6(theme, title).Layout), layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { label := material.Caption(theme, body) label.Color = shellColors.secondary return label.Layout(gtx) }), layout.Rigid(func(gtx layout.Context) layout.Dimensions { if !showReset { return layout.Dimensions{} } return layout.Inset{Top: unit.Dp(12)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions { return shell.layoutFilterButton( gtx, theme, &shell.resetFilters, "显示全部软件", true, ) }) }), ) }) }