package gio import ( "image" "gioui.org/layout" "gioui.org/op/paint" "gioui.org/unit" "gioui.org/widget" "gioui.org/widget/material" "softbox.local/core/application" ) // AppShell is the Legacy software catalog window. type AppShell struct { edition string model *application.CatalogListModel search widget.Editor appList layout.List categoryList layout.List viewAll widget.Clickable viewInstalled widget.Clickable viewUpdates widget.Clickable resetFilters widget.Clickable closeDetail widget.Clickable categoryControls map[string]*widget.Clickable rows map[string]*rowControls icons map[string]paint.ImageOp iconReferences map[string]string iconRequests map[string]application.IconEventIdentity iconApplied map[string]application.IconEventIdentity iconFailures map[string]iconFailureState catalogState catalogPresentationState lastRendered int detailRendered bool } // NewAppShell creates the catalog shell with an optional in-memory snapshot. func NewAppShell( edition string, items ...application.CatalogListItem, ) *AppShell { shell := &AppShell{ edition: edition, model: application.NewCatalogListModel(nil), appList: layout.List{Axis: layout.Vertical}, categoryList: layout.List{Axis: layout.Horizontal}, categoryControls: make(map[string]*widget.Clickable), rows: make(map[string]*rowControls), icons: make(map[string]paint.ImageOp), iconReferences: make(map[string]string), iconRequests: make(map[string]application.IconEventIdentity), iconApplied: make(map[string]application.IconEventIdentity), iconFailures: make(map[string]iconFailureState), } shell.search.SingleLine = true shell.SetItems(items) if len(items) == 0 { shell.catalogState = catalogStateLoading } return shell } // ApplyIcon stores a decoded image for future Layout calls. // It is UI-goroutine-only; background workers must publish application events. func (shell *AppShell) ApplyIcon(appID string, icon image.Image) { delete(shell.iconApplied, appID) delete(shell.iconFailures, appID) if icon == nil { delete(shell.icons, appID) return } shell.icons[appID] = paint.NewImageOp(icon) } // SetItems applies a prepared, IO-free catalog/status snapshot. func (shell *AppShell) SetItems(items []application.CatalogListItem) { shell.model.SetItems(items) shell.catalogState = catalogStateReady nextRows := make(map[string]*rowControls, len(items)) nextIcons := make(map[string]paint.ImageOp, len(items)) nextReferences := make(map[string]string, len(items)) nextRequests := make(map[string]application.IconEventIdentity, len(items)) nextApplied := make(map[string]application.IconEventIdentity, len(items)) nextFailures := make(map[string]iconFailureState, len(items)) for _, item := range items { controls := shell.rows[item.ID] if controls == nil { controls = new(rowControls) } nextRows[item.ID] = controls reference := canonicalIconReference(item.IconRef) nextReferences[item.ID] = reference if previous, exists := shell.iconReferences[item.ID]; exists && previous == reference { if icon, exists := shell.icons[item.ID]; exists { nextIcons[item.ID] = icon } if request, exists := shell.iconRequests[item.ID]; exists && request.Reference == reference { nextRequests[item.ID] = request } if applied, exists := shell.iconApplied[item.ID]; exists && applied.Reference == reference { nextApplied[item.ID] = applied } if failure, exists := shell.iconFailures[item.ID]; exists && failure.Identity.Reference == reference { nextFailures[item.ID] = failure } } } shell.rows = nextRows shell.icons = nextIcons shell.iconReferences = nextReferences shell.iconRequests = nextRequests shell.iconApplied = nextApplied shell.iconFailures = nextFailures nextCategories := make(map[string]*widget.Clickable) for _, category := range append([]string{""}, shell.model.Categories()...) { control := shell.categoryControls[category] if control == nil { control = new(widget.Clickable) } nextCategories[category] = control } shell.categoryControls = nextCategories } // Layout drains input first and performs no disk, network or hash IO. func (shell *AppShell) Layout(gtx layout.Context, theme *material.Theme) layout.Dimensions { shell.drainInput(gtx) shell.lastRendered = 0 shell.detailRendered = false paint.Fill(gtx.Ops, shellColors.background) return layout.UniformInset(unit.Dp(16)).Layout(gtx, func(gtx layout.Context) layout.Dimensions { return layout.Flex{Axis: layout.Vertical}.Layout( gtx, layout.Rigid(func(gtx layout.Context) layout.Dimensions { return shell.layoutHeader(gtx, theme) }), layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout), layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { return shell.layoutContent(gtx, theme) }), layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { label := material.Caption( theme, shell.catalogStatusText()+" · "+shell.edition+" · Legacy · Windows 7 SP1 x64", ) label.Color = shellColors.secondary return label.Layout(gtx) }), ) }) } func (shell *AppShell) drainInput(gtx layout.Context) { for { if _, ok := shell.search.Update(gtx); !ok { break } } shell.model.SetQuery(shell.search.Text()) for shell.viewAll.Clicked(gtx) { shell.model.SetView(application.CatalogViewAll) } for shell.viewInstalled.Clicked(gtx) { shell.model.SetView(application.CatalogViewInstalled) } for shell.viewUpdates.Clicked(gtx) { shell.model.SetView(application.CatalogViewUpdates) } for category, control := range shell.categoryControls { for control.Clicked(gtx) { shell.model.SetCategory(category) } } for appID, controls := range shell.rows { for controls.open.Clicked(gtx) { shell.model.Select(appID) } } for shell.resetFilters.Clicked(gtx) { shell.search.SetText("") shell.model.ResetFilters() } for shell.closeDetail.Clicked(gtx) { shell.model.Select("") } }