Files
endorphin-tessera-gui/src/bridge/ui_bridge/start_page.endor
T

184 lines
13 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// The page, cards and actions belong to Endor. The host only owns its sidebar.
// 默认页由真实 Endor 对象组成:宿主负责窗口和侧栏,卡片布局、绘制和按钮回调在源代码中定义。
// developer_pages::install_start_page 在每个 VM 安装此模块;OpenTool 的 New tab 动作调用下面的 newTab。
// 渲染器通过视觉协议调用 measure/paint,按钮事件通过 VM 分派到 StartActions 的绑定方法。
// Note 复用 Column 的内容测量,但固定卡片宽度;paint 只提供背景,孩子仍沿正常树绘制。
class Note: Column {
// 卡片构造的最后一个子类字段保存背景色;前面的变参仍按 Column 的孩子参数处理。
tint: String
// source_layout 调用这个覆盖方法;显式借用 Column 的测量结果,只替换卡片宽度,保留内容自然高度。
function measure(self: Note, mount: Mount, constraints: Constraints): MeasureResult {
// 沿用 Column 内容测量得到高度,但卡片宽固定为 224;不把背景圆角绘制误当布局尺寸。
measured = Column.measure(self, mount, constraints)
return MeasureResult(224, measured.height)
}
// source_paint 在已完成几何上调用;这里返回绘制命令,未直接向 OS 或像素缓冲写入。
function paint(self: Note, mount: Mount, geometry: Bounds): DisplayList {
// 只生成当前卡片背景,命令使用宿主给定 Bounds 的绝对布局位置,孩子仍由各自 paint 绘制。
fill = PaintCommand()
fill.kind = "roundedRect"
fill.x = geometry.x
fill.y = geometry.y
fill.width = geometry.width
fill.height = geometry.height
fill.radius = 14
fill.color = self.tint
// DisplayList 中的 commands 必须是受检查 ObjectList,且成员为 PaintCommand,不能返回普通字符串。
return DisplayList(ObjectList(fill))
}
}
// 继承 Button 的事件能力与内容布局,仅覆盖自身背景 paint。
class QuietButton: Button {
// 返回空绘制列表保留 Button 的交互能力,同时让这个入口只显示孩子文字。
function paint(self: QuietButton, mount: Mount, geometry: Bounds): DisplayList {
return DisplayList(ObjectList())
}
}
// 动作对象集中保存回调和 home 引用;按钮绑定方法会按托管引用关系保留动作对象。
class StartActions {
// 保存起始页面身份,子页面中的返回按钮激活同一 Page,而不是复制一份页面。
home: Page
// 窗口页面是普通有限 Column;可用区域来自宿主发布的逻辑视口。
function blank(self: StartActions, event: Event): Unit {
// “窗口页面”按钮的 click 回调创建独立 Page,标题和内容属于新页面,home 仍指向原画布。
page = Page()
// 标题供 project_tabs 投影到原生侧栏,不影响页面身份。
page.title = "窗口页面"
// 返回按钮保留原 home 的绑定方法,点击时激活已有页而非新建一个画布。
back = Button("返回画布")
// 绑定的是原 home 的 activate 方法;此回调恢复已有页身份,不重新构造主画布。
back.onClick(self.home.activate)
// render 设置内容并安排展示,Column 的 padding/gap 只影响这张页的有限布局。
page.render(Column(56, 20, TextBlock("恰好一个窗口", 3), TextBlock("页面跟随可用区域调整。内容和工具栏由 Endor 自己组织。"), back))
}
// 延伸方向属于容器与滚动组合:Column 安排内容,有限 ScrollBlock 控制可见范围。
function vertical(self: StartActions, event: Event): Unit {
// “纵向延伸”按钮调用;保持 Page 默认有限布局,滚动行为由后面的 ScrollBlock 承担。
page = Page()
// 标签标题和其他示例区分,Page 本身不切换到旧式无限页面模式。
page.title = "纵向页面"
// 外 Column 累加卡片高度,内 Column 组织说明文字;自然内容高度可超过可见视口。
content = Column(48, 28, Column(0, 12, TextBlock("向下,继续生长", 3), TextBlock("纵向滚动和 Column 排版可以独立组合。")))
// 每张 Note 的最后一参对应 tint 子类字段,前面的视觉值仍是 Column 的子项。
content.append(Note(24, 16, TextBlock("01 / 收集想法", 2), RectBlock(176, 130, 10, "#E2EADB"), TextBlock("从一个小小的念头开始。"), "#F0F3E9"))
content.append(Note(24, 16, TextBlock("02 / 连接片段", 2), RectBlock(176, 130, 10, "#E6DBEF"), TextBlock("内容保留在同一条连续的页面里。"), "#F4EEF7"))
content.append(Note(24, 16, TextBlock("03 / 慢慢展开", 2), RectBlock(176, 130, 10, "#EDDCC5"), TextBlock("追加一个子组件,内容自然延伸。"), "#FAF2E7"))
content.append(Note(24, 16, TextBlock("04 / 留一点空白", 2), TextBlock("不必把所有想法都放在第一屏。"), "#EDF1F6"))
// 固定初始视口内装完整内容;下一行限制滚动轴,flex 和页面 stretch 再让视口服从窗口分配。
view = ScrollBlock(1024, 800, content)
// 只消费纵向滚轮,横向内容不会因普通滚动意外平移。
view.axis = "vertical"
// flex 请求父布局分配剩余主轴空间,crossAlign 让有限视口横向填满页面。
view.flex = 1
page.crossAlign = "stretch"
// 最后提交有限视口作为 Page 内容,触发展示请求;真正的测量、绘制与原生发布由宿主刷新流程完成。
page.render(view)
}
// Row 内可继续放 Column 卡片;横向滚动不要求改变 Page 的身份或生命周期。
function horizontal(self: StartActions, event: Event): Unit {
// “横向延伸”按钮调用;横向排列和可见区域分别交给 Row 与 ScrollBlock。
page = Page()
// 仅设置标签内容,实际页面资源仍在当前 VM 的 Window 注册表中登记。
page.title = "横向页面"
// Row 沿横轴连接 Note;各 Note 保留自己的纵向 Column 测量/背景绘制覆盖。
content = Row(48, 32, Note(24, 16, TextBlock("从左,走向右", 2), RectBlock(176, 180, 10, "#DDE8F1"), TextBlock("Row 内仍然可以包含 Column。"), "#EDF3F8"))
// append 按顺序创建稳定子 slot,横向坐标由布局累加,不需要手工填写 x/y。
content.append(Note(24, 16, TextBlock("收集", 2), RectBlock(176, 210, 10, "#D7E5DA"), TextBlock("一张卡片,一个想法。"), "#EFF4EC"))
content.append(Note(24, 16, TextBlock("整理", 2), RectBlock(176, 170, 10, "#E5D9EB"), TextBlock("横向滚动,保留思考的顺序。"), "#F4EFF7"))
content.append(Note(24, 16, TextBlock("继续", 2), RectBlock(176, 230, 10, "#EFDFC7"), TextBlock("Shift + 滚轮浏览更多内容。"), "#FAF3E8"))
content.append(Note(24, 16, TextBlock("不止一屏", 2), TextBlock("右侧仍有空间。"), "#EEF1F5"))
// 这些尺寸是构造缺省值,flex/stretch 会让视口接受当前窗口的实际可用空间。
view = ScrollBlock(1024, 800, content)
// Row 自然宽度可超出视口,只有横向滚动;卡片内部仍是 Column 的纵向排版。
view.axis = "horizontal"
// 保留横向滚动内容的自然宽度,视口本身仍参加有限页面的弹性布局。
view.flex = 1
page.crossAlign = "stretch"
// 只安排展示,不在 click 回调里递归运行 measure/paint;worker 在回调结束后刷新。
page.render(view)
}
// 重新调用工厂得到独立页面与相机,已有画布继续保留自己的视角。
function canvas(self: StartActions, event: Event): Unit {
// 每次动作都调用工厂,得到新 Page、世界树和独立相机,不复用当前页的视角状态。
page = newTab()
}
// 两个文字绑定观察同一个状态对象,示范共享业务状态与多个展示位置的区别。
function counter(self: StartActions, event: Event): Unit {
// 共享的是同一个托管状态对象;textOf 两次读取该对象 value,increment 更新后两个展示一起刷新。
count = ui.state(0)
// 按钮保留 increment 的绑定 owner,同一状态可同时驱动两个 textOf 派生视觉对象。
add = Button("增加计数")
add.onClick(count.increment)
// 创建承载两个观察视图的新页面,状态对象不会因函数返回立即消失:绑定/回调仍持有它。
page = Page()
page.title = "共享对象"
page.render(Column(48, 24, TextBlock("一个对象,两个视图", 2), Row(0, 32, ui.textOf(count, "value", 3), ui.textOf(count, "value", 3)), add))
}
}
// 页面工厂:先建立业务对象/动作,再建立世界组成,最后用一个有限视口显示它。
function newTab(): Page {
// OpenTool 的 New tab 与 StartActions.canvas 都调用这个工厂;每次都创建不同页面身份。
page = Page()
// 后续标签投影读此字段,不使用标题作为查找或去重依据。
page.title = "自由画布"
// 先建立引用当前 Page 的动作对象,再绑定按钮;回调中的 home 指向这一轮工厂创建的页面。
actions = StartActions(page)
// 每个按钮只保存动作入口,点击前不提前创建示例 Page。
fixed = Button("窗口页面")
// 绑定 vertical 回调后才会创建纵向滚动内容。
vertical = Button("纵向延伸")
// 横向示例和纵向示例各有独立 Page/ScrollBlock,互不共享滚动偏移。
horizontal = Button("横向延伸")
// 新画布动作再调用工厂,使相机导航状态与此画布隔离。
canvas = Button("新的画布")
// QuietButton 只隐藏原生按钮背景,其 click 事件和孩子文字仍沿普通 Button 协议工作。
shared = QuietButton("试试:一个对象,两个视图 ↗")
// 订阅绑定方法而非提前调用;事件触发后在原 VM 中创建/展示相应示例页。
fixed.on("click", actions.blank)
vertical.on("click", actions.vertical)
horizontal.on("click", actions.horizontal)
canvas.on("click", actions.canvas)
shared.on("click", actions.counter)
// hero 内部用有限行列排版,只有整个 hero 在世界场景中的摆放采用显式 x/y。
hero = Column(0, 16, TextBlock("给想法,一点空间。", 3, "#303941"), TextBlock("页面可以是一扇窗口,也可以一直延伸。", 1, "#7D858C"), Spacer(8), Row(0, 10, fixed, vertical), Row(0, 10, horizontal, canvas), shared)
// x/y 属于 Scene2D 子组件的世界位置;卡片内部依旧由普通 Row/Column 排版。
hero.x = 316
hero.y = 254
// Scene2D 只规定孩子的世界位置;加入后仍可追加普通卡片,位置不受窗口宽高限制。
world = Scene2D(hero)
// 左侧卡片由 Note 测高并绘制 tint 背景,文字和矩形作为正常孩子分别绘制。
left = Note(24, 14, TextBlock("随手记下", 2), RectBlock(176, 100, 10, "#D5E4DA"), TextBlock("想法不必排成一条直线。"), "#EDF3E9")
left.x = 36
left.y = 92
// 将卡片以稳定 slot 加入场景;其 x/y 在前面单独设置,不依赖追加次序推算位置。
world.append(left)
// 顶部卡片只有文本,Note 仍复用内容测量,不需要为没有矩形的情况增加特殊宿主代码。
top = Note(24, 14, TextBlock("四种页面,同一个世界"), TextBlock("窗口 · 纵向 · 横向 · 自由画布", 1, "#7D858C"), "#EFF1F7")
top.x = 466
top.y = 58
world.append(top)
// 右侧卡片展示“组件布局”和“相机视角”分开:移动相机不改写这个对象的世界坐标。
right = Note(24, 14, RectBlock(176, 138, 12, "#DDDAEA"), TextBlock("把片段放在一起", 2), TextBlock("布局属于组件,视角属于摄像机。"), "#F1EDF5")
right.x = 826
right.y = 190
world.append(right)
// 底部说明中的换行属于源文本内容,不能把它误当实际控制输入或页面模式配置。
bottom = Note(24, 14, TextBlock("不止眼前这一屏", 2), TextBlock("触控板或滚轮平移\n空格拖动 / 鼠标中键拖动\n双指捏合 / ⌘ Ctrl + 滚轮缩放"), "#F7F0E5")
bottom.x = 104
bottom.y = 524
world.append(bottom)
// 场景边缘再放一张普通 Note,展示超出初始屏幕范围的内容仍可通过相机漫游到达。
end = Note(24, 14, TextBlock("继续向外探索", 2), RectBlock(176, 100, 10, "#CEDFE7"), TextBlock("画布上的对象仍是普通 Endor 组件。"), "#EAF1F5")
end.x = 716
end.y = 614
world.append(end)
// 视口参加有限布局并占用剩余空间;移动它的 Camera2D 不改写卡片世界位置。
view = CanvasViewport(1024, 800, world)
// 有限视口申请剩余空间,页面交叉轴拉伸,不把 Scene2D 改造成窗口尺寸的有限容器。
view.flex = 1
page.crossAlign = "stretch"
page.render(view)
// render 已登记/安排展示,返回 Page 给 OpenTool/调用者;返回值不是 GUI 线程的原生窗口对象。
return page
}