Skip to content

Instantly share code, notes, and snippets.

@morlay
Created September 13, 2023 13:33
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save morlay/1c771d2d9c2460ad1589dc7ff7c4a55a to your computer and use it in GitHub Desktop.
Save morlay/1c771d2d9c2460ad1589dc7ff7c4a55a to your computer and use it in GitHub Desktop.

  • 跨平台 GUI 工具
  • 平台支持列表
    • Windows 7 +
    • Linux
      • Ubuntu 20+
    • macOS

路线选择

Hybrid App

WebView based

Web tech (需要各平台的 WebView 兼容) + js-bridge

Windows 上已强制要求 WebView 2,而 Windows 7 无法支持,划掉。

Chromium embedded

Web tech (内嵌浏览器,一致性会好很多) + js-bridge

随着 Chromium 对 Windows 7/8 支持的放弃,此路不通。

Native / Cross-platform graphics engine

  • QT 要钱,划掉
  • Skia-based
    • Flutter
      • dart 储备
      • 编译需要对应操作系统
    • Compose multiplatform
      • Java/Kotlin 储备
      • 编译需要对应操作系统
      • 运行时有以 jdk-runtime 依赖
  • GPU renderer
    • iced (rust)
      • wgpu for Vulkan/Metal/DX12skia for fallback
      • 无 rust 储备
    • ebitengine (go)
      • 2D 游戏引擎
      • Shader 支持完整
      • 文本输入支持欠缺
    • gioui (go)
      • 交互支持完整
      • 文本输入支持完整
      • 布局元素堆栈合理

结论

鉴于团队技术储备,和成本考量,将在 gioui 的基础上进一步改造

Gio Compose

Gio 原理

flowchart TD
	ui_state[UI State]
	renderer[GPU Renderer]
	gio[GIO Engine]
	event(Event)

	ui_state -->|layout| gio -->|render| renderer
	event -.-> |Pointer/Input| gio -.->|check then do| ui_state

immediate mode 处理非常暴力,但凡有触发(产生帧),可视区域内全量绘制。

因此,不同于传统模式(如 DOM):

  • UI 的事件绑定不再需要 addEventListener/removeEventListener
    • 判断事件的触发位置是否在落在某个元素上,在绘制前直接修改 UI 状态即可
  • 动画同理:
    • 通过改变 UI 状态值,来绘制对应的状态的图形集合

这,很函数式。也,太熟悉了。

UI = f(state)

Render Engine

抽象了一个 driver adaptor,不同的平台用不同的实际实现,然而有个绘制模型 piet-gpu

  • macOS/iOS - Metal
  • Windows - dx11 (Windows 7 就支持的)
  • Android - OpenGL ES3
  • Linux - Vulkan
    • 窗体支持需要 X11 / Wayland
      • 看 Linux 桌面环境,Wayland 正在逐步替代,应该是可以关掉 x11 支持的

Refs

Biz 与 UI 还是得分离

从 gio 官方给出的示例 ,Biz State 和 UI State 没有严格的区分。 DOM 刀耕火种过来,痛点经历过了,能避免还是得避免。

因此,引入 Virtual DOM 这层抽象还是有必要的。

Component(props/state) 
	-> VNodeTree 
		-> WidgetTree 
			-> Widget(UI State) 
				-> Renderer

在这里 Widget 是绘制的基本单元,除了基本样式外,也是事件响应的载体。

Virtual DOM 的 DSL/Component/Hooks/Context

由于 Golang 的语言特性是非常简陋,有些模式实现起来会相当繁琐。 所以排除掉其他(Vue 的 Proxy,SwiftUI 的 Decorator ),还是走 React Hooks 的路子会直接的多。

基本的 VNode 声明

H(Component, Modifier...).Children(VNode...)

或者简写为

Box(Modifier...).Children(VNode...)

组件 与 Widget 并非一一对应,因此 Fragment 同样支持

Fragment(VNode...)

另外由于 Golang 语法的限制, 对于大量非必填属性的配置,参考 Jetpack Compose 的 Modifier 的 风格, 通过不定参数,按需传入。

并且,也方便扩展 Modifier

Row(  
   modifier.FillMaxSize(), // 撑满父容器
   modifier.PaddingAll(20), // Padding 4 方向 20dp
   modifier.Align(alignment.Center), // 子元素居中对齐
)

Component

type Component interface {
   Build(b BuildContext) VNode
}
  
type Counter struct {  
}  
  
func (Counter) Build(b BuildContext) VNode {  
    state := UseState(b, 0)  
  
    return Row(  
       modifier.FillMaxSize(),  
       modifier.PaddingAll(20),  
       modifier.Align(alignment.Center),  
    ).Children(  
       Row(  
          modifier.DetectGesture(  
             gesture.OnTap(func() {  
                state.UpdateFunc(func(prev int) int {  
                   return prev + 1  
                })  
             }),  
          ),  
          modifier.Height(80),  
          modifier.FillMaxWidth(),  
          modifier.RoundedAll(10),  
          modifier.BackgroundColor(color.White),  
          modifier.Shadow(2),  
          modifier.Align(alignment.Center),  
       ).Children(  
          Text(fmt.Sprint(state.Value()), modifier.TextAlign(text.Middle)),  
       ),  
    )  
}

Hook APIs

不同于 React 的 Hooks,这里不得不将 BuildContext 作为第一个参数传入,由于 Go 的并行特性。虽然单一 VNode Tree 无,但多窗体的时候,就会出现。

另外,Go 的泛型目前不支持 Method。 因此,Hooks 的接口只能是 Use(BuildContext, ...) 而不是 BuildContext.Use(...)

UseState[T any](c BuildContext, defaultValue T) State[T]
state := UseState(buildContext, 1)

v := state.Value()

state.Update(2)
state.UpdateFunc(func (prev int) int {
	return prev + 1
})

// 考虑支持?
// <- state.Watch()
UseEffect(c BuildContext, setup func() func(), deps: []any)
UseEffect(buildContext, func() func() {
    // do

	return func() {
	  // cleanup
	} 
}, []any{})
UseRef[T any](c BuildContext, defaultValue T) Ref[T]
ref := UseRef[*Some](buildContext, nil)
UseMemo[T any](c BuildContext, create func() T, deps []any)
v := UseMemo(buildContext, func() int {
   return x
}, []any{})

Context

BuildContext 其实扩展自 context.Context,复用注入和使用逻辑即可。

实现

复用遗产 https://github.com/go-courier/gox

Basic Widgets

Box/Row/Column

基础布局元素

https://developer.android.com/jetpack/compose/layouts/basics?hl=zh-cn

同时作为容器, BackgroundColor / BorderStroke / Rounded / Shadow 这些基本绘制也是需要支持的。

Column/Row 也需要支持滚动

Text

文本渲染

TextInput TODO

文本输入

Image TODO

图片渲染

Overlays

想 Modal,Tooltip 等 Overlays,我们需要将这些元素渲染到 Widget Tree 的顶层,以避免受到父组件的影响。

和 React 一样,支持 Portal 即可。

如 Tooltip

<Window>
	<AppRoot><TooltipTrigger/></AppRoot>
	<PortalContainer><TooltipContent></PortalContainer>
</Window>

同时,对于 Tooltip 这种,TooltipContent 的位置需要相对于 TooltipTrigger。 获取 TooltipTrigger 和 TooltipContent 的 BoundingRect 就是非常必要的了。

进而,Widget 需要进一步升级,在 gio layout 完成后,需要存下 Widget 的 Top/Left/Width/Height 等结果,供计算。

Animations TODO

TODO

展望

Audio/Video Player

Misc

Repo: https://github.com/octohelm/gio-compose

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment