184 lines
13 KiB
Plaintext
184 lines
13 KiB
Plaintext
// 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
|
||
}
|