Add Gio app shells and platform stubs (T-003)
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,48 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"gioui.org/app"
|
||||
"gioui.org/op"
|
||||
"gioui.org/unit"
|
||||
|
||||
"softbox.local/app-win7/platform/windows"
|
||||
softboxgio "softbox.local/app-win7/ui/gio"
|
||||
"softbox.local/core"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Println(core.ProductName)
|
||||
go func() {
|
||||
if err := run(); err != nil {
|
||||
log.Printf("%s Legacy 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+" Legacy"),
|
||||
app.Size(unit.Dp(1024), unit.Dp(680)),
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1,10 @@
|
||||
gioui.org v0.6.0 h1:ZSXO/AbpFZJ2L9NU69uFQfDI3BKIH+YEJElrn0B+aZI=
|
||||
gioui.org v0.6.0/go.mod h1:eUvGo6FAzA7jUqeSu5a+M1W03yc9r1nanIBS8A5+Nng=
|
||||
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 h1:AGDDxsJE1RpcXTAxPG2B4jrwVUJGFDjINIPi1jtO6pc=
|
||||
gioui.org/shader v1.0.8 h1:6ks0o/A+b0ne7RzEqRZK5f4Gboz2CfG+mVliciy6+qA=
|
||||
github.com/go-text/typesetting v0.1.1 h1:bGAesCuo85nXnEN5LmFMVGAGpGkCPtHrZLi//qD7EJo=
|
||||
golang.org/x/exp v0.0.0-20221012211006-4de253d81b95 h1:sBdrWpxhGDdTAYNqbgBLAR+ULAPPhfgncLr1X0lyWtg=
|
||||
golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91 h1:ryT6Nf0R83ZgD8WnFFdfI8wCeyqgdXWN4+CkFVNPAT0=
|
||||
golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI=
|
||||
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
|
||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
// Package windows contains Win7-compatible Windows platform adapters.
|
||||
// Package windows provides Win7-compatible platform adapters and non-Windows
|
||||
// stubs for package-level tests.
|
||||
package windows
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package windows
|
||||
|
||||
// Edition identifies the application build channel shown by the UI.
|
||||
type Edition string
|
||||
|
||||
const EditionLegacy Edition = "Legacy"
|
||||
|
||||
// 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 EditionLegacy
|
||||
}
|
||||
@@ -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() != EditionLegacy {
|
||||
t.Fatalf("Edition() = %q, want %q", platform.Edition(), EditionLegacy)
|
||||
}
|
||||
}
|
||||
@@ -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 EditionLegacy
|
||||
}
|
||||
@@ -1,2 +1,5 @@
|
||||
// Package gio contains the Win7-compatible 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
|
||||
|
||||
@@ -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 Legacy 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 Legacy 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, "Legacy 窗口已就绪,业务功能将在后续任务接入。")
|
||||
label.Color = shellColors.secondary
|
||||
return label.Layout(context)
|
||||
}),
|
||||
)
|
||||
})
|
||||
}),
|
||||
layout.Rigid(func(context layout.Context) layout.Dimensions {
|
||||
label := material.Body2(theme, "Legacy · Windows 7 SP1 x64")
|
||||
label.Color = shellColors.secondary
|
||||
return label.Layout(context)
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -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(1024, 680)
|
||||
context := layout.Context{
|
||||
Ops: &operations,
|
||||
Metric: unit.Metric{PxPerDp: 1, PxPerSp: 1},
|
||||
Constraints: layout.Exact(size),
|
||||
}
|
||||
|
||||
dimensions := NewAppShell("Legacy").Layout(context, NewTheme())
|
||||
if dimensions.Size != size {
|
||||
t.Fatalf("Layout() size = %v, want %v", dimensions.Size, size)
|
||||
}
|
||||
}
|
||||
+10
-9
@@ -14,14 +14,14 @@
|
||||
|
||||
- 日期:2026-07-16
|
||||
- 阶段:MVP 起步(Phase 0 工程骨架建设中)
|
||||
- 技术栈:根 Go workspace 纳入 core/app-modern/app-win7 三模块;`app-win7/go.work` 隔离 Go 1.20.14 构建;Gio UI 与平台 stub 待后续 Phase 0 任务接入
|
||||
- 生产代码:core 已有软件状态模型、迁移规则、application Event 合约与 Runtime 事件总线骨架;modern/win7 仍为无 UI 命令入口
|
||||
- 测试:core 已覆盖状态合法性/非法迁移、事件类型、发布/取消/关闭
|
||||
- 技术栈:根 Go workspace 纳入 core/app-modern/app-win7 三模块;`app-win7/go.work` 隔离 Go 1.20.14 构建;modern Gio v0.10.1 与 win7 Gio v0.6.0 已实际接入
|
||||
- 生产代码:core 已有状态模型与事件 runtime;modern/win7 均可打开最小 AppShell,并有平台接口、Windows 实现与非 Windows stub
|
||||
- 测试:core 覆盖状态/事件 runtime;两个 app 覆盖 AppShell 布局尺寸与平台 stub 契约
|
||||
- 数据:无;Catalog 清单与测试样例待建
|
||||
- 标准启动路径:`./init.sh` / `./init.ps1`(同步依赖、验证 core、打印 modern/win7 双目标构建命令)
|
||||
- 标准验证路径:`go -C core vet ./...` + `go -C core test -count=1 ./...`;双目标构建命令见下文
|
||||
- 版本管理:git 已初始化,main 分支,远端 origin 为 Gitea `opc/soft_quay`;harness 文档已提交
|
||||
- 当前 blocker:无;下一步按路线图落成并领取 T-003
|
||||
- 当前 blocker:无;下一步按路线图落成并领取 T-004
|
||||
|
||||
## 当前目录要点
|
||||
|
||||
@@ -29,10 +29,10 @@
|
||||
| --- | --- | --- |
|
||||
| `docs/` | 已有 | harness coding 文档集(本次初始化完成) |
|
||||
| `docs/tasks/` | 已有 | 任务目录;T-001 已完成,T-002 待按路线图落成 |
|
||||
| `scripts/` | 已有 | harness 治理脚本(validate_agent_context 等);构建脚本待建 |
|
||||
| `scripts/` | 已有 | harness 治理脚本 + `check_core_boundaries.py`;Phase 0 构建闸门待 T-004 |
|
||||
| `core/` | 已建 | Go 1.20 兼容共享模块;已有 domain 状态模型与 application 事件 runtime |
|
||||
| `app-modern/` | 已建 | Go 1.25.0 + Gio v0.10.1 模块;当前为无 UI 命令入口 |
|
||||
| `app-win7/` | 已建 | Go 1.20 + Gio v0.6.0 模块;当前为无 UI 命令入口 |
|
||||
| `app-modern/` | 已建 | Go 1.25.0 + Gio v0.10.1;可打开 Modern AppShell |
|
||||
| `app-win7/` | 已建 | Go 1.20 + Gio v0.6.0;可打开带 Legacy 标识的 AppShell |
|
||||
| `schemas/` | 待建 | 协议 JSON Schema(T-201) |
|
||||
| `testdata/` | 待建 | 假数据测试样例(T-101 起) |
|
||||
|
||||
@@ -40,9 +40,9 @@
|
||||
|
||||
任务状态以 `docs/tasks/` 各任务文件 frontmatter 的 `status` 为准。本节只写项目级摘要:
|
||||
|
||||
- 已完成:`T-001 初始化 monorepo 骨架`、`T-002 建立 domain 状态模型与事件总线`。
|
||||
- 已完成:`T-001 初始化 monorepo 骨架`、`T-002 建立 domain 状态模型与事件总线`、`T-003 Gio 空窗口 + 平台层 stub`。
|
||||
- 正在进行:无。
|
||||
- 下一个可领取任务:按路线图落成并领取 `T-003 Gio 空窗口 + 平台层 stub`。
|
||||
- 下一个可领取任务:按路线图落成并领取 `T-004 CI 双目标编译闸门`。
|
||||
|
||||
## 当前可运行内容
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
# 独立验证:
|
||||
go -C core vet ./...
|
||||
go -C core test -count=1 ./...
|
||||
python scripts/check_core_boundaries.py
|
||||
GOTOOLCHAIN=go1.25.0 GOOS=windows GOARCH=amd64 go -C app-modern build ./cmd/softbox
|
||||
GOTOOLCHAIN=go1.20.14 GOOS=windows GOARCH=amd64 go -C app-win7 build ./cmd/softbox
|
||||
```
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
id: T-003
|
||||
title: 建立 Gio 空窗口 AppShell 与平台层 stub
|
||||
phase: 0
|
||||
deps: [T-001]
|
||||
status: DONE
|
||||
created: 2026-07-16
|
||||
issue: null
|
||||
context_ref: 0e20dd76b215015030c184436fb216f34e9b27e2
|
||||
claim_branch: null
|
||||
work_branch: agent/codex/T-003
|
||||
write_paths:
|
||||
- docs/tasks/T-003.md
|
||||
- app-modern/cmd/softbox/
|
||||
- app-modern/ui/gio/
|
||||
- app-modern/platform/windows/
|
||||
- app-win7/cmd/softbox/
|
||||
- app-win7/ui/gio/
|
||||
- app-win7/platform/windows/
|
||||
- scripts/check_core_boundaries.py
|
||||
- go.work.sum
|
||||
- docs/current-state.md
|
||||
---
|
||||
|
||||
## 问题 / 背景
|
||||
|
||||
两个应用模块当前只有控制台占位入口,尚不能打开桌面窗口;平台层也只有空包。需要建立 modern 与 Win7 各自适配 Gio 版本的最小 AppShell,并为后续 Windows 能力注入提供可在非 Windows 环境编译的接口/stub。
|
||||
|
||||
## 方案
|
||||
|
||||
1. modern 使用 Gio v0.10.1 API,win7 使用 Gio v0.6.0 API,各自建立窗口事件循环和 `ui/gio.AppShell`。
|
||||
2. AppShell 只展示高对比标题、版本标识与占位说明,使用 8dp 间距节奏,不做业务 IO、动画或交互。
|
||||
3. 两个 `platform/windows` 包定义最小 `Platform` 接口,分别提供 Windows 实现与 `!windows` stub。
|
||||
4. 新增离线边界检查脚本,通过 `go list -json` 拒绝 core 直接 import Gio、Windows 专属包或 SQLite。
|
||||
5. 生成并提交两个 app 模块所需的依赖校验文件。
|
||||
|
||||
## 验收要点
|
||||
|
||||
- modern 与 win7 两个 Windows amd64 目标都能编译为 GUI EXE。
|
||||
- 两个窗口均有 AppShell 骨架;Win7 版明确显示 Legacy 标识。
|
||||
- Gio 代码只位于 `ui/gio` 与 `cmd` 装配层;Layout 不包含磁盘、网络或哈希操作。
|
||||
- `platform/windows` 在 Windows 有实现,在非 Windows 有 stub,包级测试可在当前环境运行。
|
||||
- `python scripts/check_core_boundaries.py` 与 `--self-test` 通过,并能检测禁止依赖前缀。
|
||||
- core vet/test 与治理检查继续通过。
|
||||
|
||||
## 边界(不改什么)
|
||||
|
||||
- 不实现软件列表、搜索、下载队列或其他业务页面。
|
||||
- 不让 modern 与 win7 共享 Gio 控件代码;只保持布局语义一致。
|
||||
- 不新增 Windows API 调用或第三方依赖。
|
||||
- 不引入装饰动画、图标或品牌资产。
|
||||
|
||||
## 协作约束
|
||||
|
||||
未启用 Gitea;本任务在 `agent/codex/T-003` 分支串行执行。UI 采用 `ui-ux-pro-max` 的工具型桌面应用原则,但以 Gio/Windows 任务边界和仓库架构为最高约束。
|
||||
|
||||
## 执行记录
|
||||
|
||||
- 2026-07-16:modern 使用 Gio v0.10.1、win7 使用 Gio v0.6.0 建立各自窗口事件循环与 `AppShell`;两个版本共享相同布局语义,Win7 标题和页脚明确显示 Legacy。
|
||||
- 2026-07-16:依据 `ui-ux-pro-max` 的工具型产品建议,采用高对比语义色、16sp 正文、8dp 间距节奏与 44dp 交互基线;本任务没有交互控件,因此不添加动画、图标或装饰效果。
|
||||
- 2026-07-16:两个 `platform/windows` 包建立 `Platform` 接口、Windows 实现与 `!windows` stub,并添加包级契约测试。
|
||||
- 2026-07-16:新增 `scripts/check_core_boundaries.py`,通过 `go list -json` 检查 core 的普通/测试 import,拒绝 Gio、Windows、SQLite 与 app 反向依赖;`--self-test` 验证前缀匹配器。
|
||||
- 2026-07-16:`go mod tidy` 会忽略 workspace 本地替换并尝试下载 `softbox.local/core`;为避免把 `replace ../core` 写入 go.mod,保留 go.work 作为唯一替换源,由实际 test/build 生成 `go.work.sum`。
|
||||
- 验证通过:modern Go 1.25.0 执行 `go vet ./...`、UI/platform 测试与 Windows amd64 构建。
|
||||
- 验证通过:win7 Go 1.20.14 执行 `go vet ./...`、UI/platform 测试与 Windows amd64 构建。
|
||||
- 验证通过:两个 EXE 在当前 Windows 环境启动后保持运行,随后由 smoke 脚本关闭。
|
||||
- 验证通过:`python scripts/check_core_boundaries.py --self-test`、`python scripts/check_core_boundaries.py`、core vet/test 与治理检查。
|
||||
@@ -1,12 +1,21 @@
|
||||
eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d/go.mod h1:OYVuxibdk9OSLX8vAqydtRPP87PyTFcT9uH3MlEGBQA=
|
||||
gioui.org v0.6.0/go.mod h1:eUvGo6FAzA7jUqeSu5a+M1W03yc9r1nanIBS8A5+Nng=
|
||||
gioui.org v0.10.1 h1:Dvp6iDk9RKuZk19jxhOmb4p673CLVvb656LyMxQ+uO0=
|
||||
gioui.org v0.10.1/go.mod h1:MZJZsdEPkTBzChdqeE8CiiQhreUQBj43qusDxQNDf7k=
|
||||
gioui.org/cpu v0.0.0-20210808092351-bfe733dd3334/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ=
|
||||
gioui.org/shader v1.0.8 h1:6ks0o/A+b0ne7RzEqRZK5f4Gboz2CfG+mVliciy6+qA=
|
||||
gioui.org/shader v1.0.8/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM=
|
||||
github.com/go-text/typesetting v0.3.4 h1:YYurUOtEb9kGSOz4uE3k4OpBGsp1dDL8+fjCeaFamAU=
|
||||
github.com/go-text/typesetting v0.3.4/go.mod h1:4qZCQphq4KSgGTAeI0uMEkVbROgfah8BuyF5LRYr7XY=
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
|
||||
golang.org/x/exp/shiny v0.0.0-20250408133849-7e4ce0ab07d0 h1:tMSqXTK+AQdW3LpCbfatHSRPHeW6+2WuxaVQuHftn80=
|
||||
golang.org/x/exp/shiny v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:ygj7T6vSGhhm/9yTpOQQNvuAUFziTH7RUiH74EoE2C8=
|
||||
golang.org/x/image v0.26.0 h1:4XjIFEZWQmCZi6Wv8BoxsDhRU3RVnLX04dToTDAEPlY=
|
||||
golang.org/x/image v0.26.0/go.mod h1:lcxbMFAovzpnJxzXS3nyL83K27tmqtKzIJpctK8YO5c=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Reject UI, Windows, SQLite, and app-layer imports from the core module."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
FORBIDDEN_IMPORT_PREFIXES = (
|
||||
"gioui.org",
|
||||
"golang.org/x/sys/windows",
|
||||
"github.com/mattn/go-sqlite3",
|
||||
"modernc.org/sqlite",
|
||||
"softbox.local/app-modern",
|
||||
"softbox.local/app-win7",
|
||||
)
|
||||
|
||||
|
||||
def is_forbidden(import_path):
|
||||
return any(
|
||||
import_path == prefix or import_path.startswith(prefix + "/")
|
||||
for prefix in FORBIDDEN_IMPORT_PREFIXES
|
||||
)
|
||||
|
||||
|
||||
def decode_json_stream(text):
|
||||
decoder = json.JSONDecoder()
|
||||
index = 0
|
||||
while index < len(text):
|
||||
while index < len(text) and text[index].isspace():
|
||||
index += 1
|
||||
if index >= len(text):
|
||||
return
|
||||
value, index = decoder.raw_decode(text, index)
|
||||
yield value
|
||||
|
||||
|
||||
def list_core_packages(repo_root):
|
||||
environment = os.environ.copy()
|
||||
environment["GOWORK"] = "off"
|
||||
result = subprocess.run(
|
||||
["go", "list", "-json", "./..."],
|
||||
cwd=str(repo_root / "core"),
|
||||
env=environment,
|
||||
check=False,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
universal_newlines=True,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
print(result.stderr, file=sys.stderr, end="")
|
||||
raise SystemExit(result.returncode)
|
||||
return list(decode_json_stream(result.stdout))
|
||||
|
||||
|
||||
def run_self_test():
|
||||
cases = (
|
||||
("gioui.org/layout", True),
|
||||
("golang.org/x/sys/windows/registry", True),
|
||||
("modernc.org/sqlite/lib", True),
|
||||
("softbox.local/app-modern/ui/gio", True),
|
||||
("context", False),
|
||||
("crypto/sha256", False),
|
||||
)
|
||||
failures = [
|
||||
import_path
|
||||
for import_path, expected in cases
|
||||
if is_forbidden(import_path) != expected
|
||||
]
|
||||
if failures:
|
||||
print(
|
||||
"ERROR: boundary matcher self-test failed for {}".format(
|
||||
", ".join(failures)
|
||||
),
|
||||
file=sys.stderr,
|
||||
)
|
||||
raise SystemExit(1)
|
||||
print("core boundary matcher self-test passed.")
|
||||
|
||||
|
||||
def main():
|
||||
if sys.argv[1:] == ["--self-test"]:
|
||||
run_self_test()
|
||||
return
|
||||
if len(sys.argv) != 1:
|
||||
print(
|
||||
"usage: python scripts/check_core_boundaries.py [--self-test]",
|
||||
file=sys.stderr,
|
||||
)
|
||||
raise SystemExit(2)
|
||||
|
||||
repo_root = pathlib.Path(__file__).resolve().parents[1]
|
||||
violations = []
|
||||
|
||||
for package in list_core_packages(repo_root):
|
||||
imports = set(package.get("Imports", []))
|
||||
imports.update(package.get("TestImports", []))
|
||||
imports.update(package.get("XTestImports", []))
|
||||
for import_path in sorted(imports):
|
||||
if is_forbidden(import_path):
|
||||
violations.append((package["ImportPath"], import_path))
|
||||
|
||||
if violations:
|
||||
for package, import_path in violations:
|
||||
print(
|
||||
"ERROR: {} imports forbidden dependency {}".format(
|
||||
package, import_path
|
||||
),
|
||||
file=sys.stderr,
|
||||
)
|
||||
raise SystemExit(1)
|
||||
|
||||
print(
|
||||
"core boundary check passed: {} forbidden prefixes absent.".format(
|
||||
len(FORBIDDEN_IMPORT_PREFIXES)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user