Add Gio app shells and platform stubs (T-003)

This commit is contained in:
ila
2026-07-16 15:43:03 +08:00
parent 0e20dd76b2
commit 6f920eb457
23 changed files with 668 additions and 15 deletions
+39 -2
View File
@@ -1,11 +1,48 @@
package main
import (
"fmt"
"log"
"os"
"gioui.org/app"
"gioui.org/op"
"gioui.org/unit"
"softbox.local/app-modern/platform/windows"
softboxgio "softbox.local/app-modern/ui/gio"
"softbox.local/core"
)
func main() {
fmt.Println(core.ProductName)
go func() {
if err := run(); err != nil {
log.Printf("%s stopped: %v", core.ProductName, err)
}
os.Exit(0)
}()
app.Main()
}
func run() error {
platform := windows.New()
window := new(app.Window)
window.Option(
app.Title(core.ProductName),
app.Size(unit.Dp(1080), unit.Dp(720)),
)
theme := softboxgio.NewTheme()
shell := softboxgio.NewAppShell(string(platform.Edition()))
var operations op.Ops
for {
switch event := window.Event().(type) {
case app.DestroyEvent:
return event.Err
case app.FrameEvent:
context := app.NewContext(&operations, event)
shell.Layout(context, theme)
event.Frame(context.Ops)
}
}
}
+2 -1
View File
@@ -1,2 +1,3 @@
// Package windows contains modern Windows platform adapters.
// Package windows provides modern Windows platform adapters and non-Windows
// stubs for package-level tests.
package windows
+17
View File
@@ -0,0 +1,17 @@
package windows
// Edition identifies the application build channel shown by the UI.
type Edition string
const EditionModern Edition = "Modern"
// Platform is the minimal boundary for target-specific capabilities.
type Platform interface {
OS() string
Edition() Edition
}
// New returns the platform implementation selected by build tags.
func New() Platform {
return newPlatform()
}
@@ -0,0 +1,19 @@
//go:build !windows
package windows
import "runtime"
type platformStub struct{}
func newPlatform() Platform {
return platformStub{}
}
func (platformStub) OS() string {
return runtime.GOOS
}
func (platformStub) Edition() Edition {
return EditionModern
}
@@ -0,0 +1,13 @@
package windows
import "testing"
func TestPlatformStubContract(t *testing.T) {
platform := New()
if platform.OS() == "" {
t.Fatal("OS() should not be empty")
}
if platform.Edition() != EditionModern {
t.Fatalf("Edition() = %q, want %q", platform.Edition(), EditionModern)
}
}
@@ -0,0 +1,17 @@
//go:build windows
package windows
type platform struct{}
func newPlatform() Platform {
return platform{}
}
func (platform) OS() string {
return "windows"
}
func (platform) Edition() Edition {
return EditionModern
}
+3
View File
@@ -1,2 +1,5 @@
// Package gio contains the modern Gio UI adapter.
//
// Layout code is rendering-only: it must not read files, access the network,
// calculate hashes, or directly mutate background application state.
package gio
+90
View File
@@ -0,0 +1,90 @@
package gio
import (
"image/color"
"gioui.org/layout"
"gioui.org/op/paint"
"gioui.org/unit"
"gioui.org/widget/material"
)
var shellColors = struct {
background color.NRGBA
foreground color.NRGBA
primary color.NRGBA
onPrimary color.NRGBA
secondary color.NRGBA
}{
background: color.NRGBA{R: 248, G: 250, B: 252, A: 255},
foreground: color.NRGBA{R: 15, G: 23, B: 42, A: 255},
primary: color.NRGBA{R: 13, G: 148, B: 136, A: 255},
onPrimary: color.NRGBA{R: 255, G: 255, B: 255, A: 255},
secondary: color.NRGBA{R: 71, G: 85, B: 105, A: 255},
}
// AppShell is the modern window frame. Feature views are added in later tasks.
type AppShell struct {
edition string
}
// NewAppShell creates an empty shell for the supplied build edition.
func NewAppShell(edition string) *AppShell {
return &AppShell{edition: edition}
}
// NewTheme creates the semantic palette shared by the modern shell.
func NewTheme() *material.Theme {
theme := material.NewTheme()
theme.Palette = material.Palette{
Bg: shellColors.background,
Fg: shellColors.foreground,
ContrastBg: shellColors.primary,
ContrastFg: shellColors.onPrimary,
}
theme.FingerSize = unit.Dp(44)
return theme
}
// Layout renders the shell without performing IO or submitting use cases.
func (shell *AppShell) Layout(context layout.Context, theme *material.Theme) layout.Dimensions {
paint.Fill(context.Ops, theme.Bg)
return layout.UniformInset(unit.Dp(32)).Layout(context, func(context layout.Context) layout.Dimensions {
return layout.Flex{Axis: layout.Vertical}.Layout(
context,
layout.Rigid(func(context layout.Context) layout.Dimensions {
return layout.Flex{Alignment: layout.Middle}.Layout(
context,
layout.Rigid(material.H5(theme, "SoftBox").Layout),
layout.Rigid(layout.Spacer{Width: unit.Dp(16)}.Layout),
layout.Rigid(func(context layout.Context) layout.Dimensions {
label := material.Body2(theme, shell.edition)
label.Color = shellColors.primary
return label.Layout(context)
}),
)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(24)}.Layout),
layout.Flexed(1, func(context layout.Context) layout.Dimensions {
return layout.Center.Layout(context, func(context layout.Context) layout.Dimensions {
return layout.Flex{Axis: layout.Vertical, Alignment: layout.Middle}.Layout(
context,
layout.Rigid(material.H6(theme, "软件目录准备中").Layout),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(func(context layout.Context) layout.Dimensions {
label := material.Body1(theme, "当前窗口仅验证 Gio AppShell 与双构建链路。")
label.Color = shellColors.secondary
return label.Layout(context)
}),
)
})
}),
layout.Rigid(func(context layout.Context) layout.Dimensions {
label := material.Body2(theme, "Modern · Windows 10/11 x64")
label.Color = shellColors.secondary
return label.Layout(context)
}),
)
})
}
+25
View File
@@ -0,0 +1,25 @@
package gio
import (
"image"
"testing"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/unit"
)
func TestAppShellFillsWindow(t *testing.T) {
var operations op.Ops
size := image.Pt(1080, 720)
context := layout.Context{
Ops: &operations,
Metric: unit.Metric{PxPerDp: 1, PxPerSp: 1},
Constraints: layout.Exact(size),
}
dimensions := NewAppShell("Modern").Layout(context, NewTheme())
if dimensions.Size != size {
t.Fatalf("Layout() size = %v, want %v", dimensions.Size, size)
}
}