From 07812bc47a85a97080ab96f7cfd481be40f34192 Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Thu, 12 Nov 2026 19:39:17 +0800 Subject: [PATCH] docs: explain layout example constraints and protocol calls --- examples/box-layout.endor | 10 ++++++++++ examples/flex-constraints.endor | 10 ++++++++++ examples/flex.endor | 10 ++++++++++ examples/layout-cache.endor | 10 ++++++++++ examples/layout-negotiation.endor | 10 ++++++++++ examples/source-layout.endor | 10 ++++++++++ examples/wrapped-text.endor | 11 +++++++++++ 7 files changed, 71 insertions(+) diff --git a/examples/box-layout.endor b/examples/box-layout.endor index c49dd59..4637ab5 100644 --- a/examples/box-layout.endor +++ b/examples/box-layout.endor @@ -3,45 +3,55 @@ // 内部布局基于容器自己的约束;Page 和相机只提供外部展示/投影上下文。 // 文字 boxWidth=fill 占父分配的可用宽度,marginBottom 在文字边框盒外参与排布。 shared = TextBlock("同一个对象可以有多个 Mount。每个位置分别测量、换行和对齐。", 2) +// fill 是共享对象的布局策略,每次出现仍从各自父约束得到实际宽度;不把首次测量宽度保存回 shared。 shared.boxWidth = "fill" shared.marginBottom = 8 // 左 Column 使用 fill、min/max 宽度限制和四边边框;非对称 paddingLeft 覆盖统一 padding。 left = Column(16, 12, TextBlock("Row / Column", 2), shared, Button("继续探索")) left.boxWidth = "fill" +// 给父 Row 的分配加上下限/上限,min/max 是 border-box 约束,不是原生窗口宽度。 left.minWidth = 180 left.maxWidth = 360 +// 四边分别设置厚度,测量/安排时边框与 padding 占内容外空间;颜色只影响绘制。 left.borderLeft = 2 left.borderTop = 2 left.borderRight = 2 left.borderBottom = 2 left.borderColor = "#CBD5E1" left.paddingLeft = 24 +// Column 的交叉轴为横轴,未自行覆盖对齐的孩子按 center 排;共享文字还有 fill 自身宽度策略。 left.crossAlign = "center" // 右 Column 也引用同一 shared,但最大宽只有 240,因此对应 Mount 的换行高度可以不同。 right = Column(16, 12, TextBlock("共享内容,独立排版", 2), shared) right.boxWidth = "fill" right.maxWidth = 240 +// 这里 end 仅选择右容器内的交叉轴位置,不改变 shared 业务对象在左容器的对齐。 right.crossAlign = "end" // Row 分配有限 760 主轴空间,扣 padding/gap 后在两侧 fill 子项之间分配并服从上限。 content = Row(24, 24, left, right) content.boxWidth = 760 +// Row 的交叉轴为纵轴,start 让左右两列按可用上侧对齐,列高仍来自各自文字换行测量。 content.crossAlign = "start" // 角标是 Overlay 的另一子项,alignSelf 与 margin 控制它在容器交叉轴上的位置。 badge = TextBlock("Endorphin · layout") badge.marginRight = 12 badge.marginBottom = 12 +// 子项显式对齐覆盖父级默认交叉轴对齐,margin 保留在边框盒外,不作为文字内容绘制。 badge.alignSelf = "end" // 固定 800x400 的 Overlay 不按线性顺序挤开兄弟,overflow=clip 同时约束绘制和命中。 surface = Overlay(0, 0, content, badge) surface.boxWidth = 800 surface.boxHeight = 400 +// Overlay 的主轴对齐参与各子项在同一区域的分配,不像 Column 那样把 content 和 badge 顺序堆叠。 surface.mainAlign = "end" +// overflow 策略由宿主投影成子裁剪,超出有限表面的文字/按钮不能仅凭原布局盒穿透命中。 surface.overflow = "clip" // Page 只提供展示入口,根容器自身的盒模型规则不依赖页面导航方式。 page = Page() page.title = "嵌套盒模型" +// 顶层脚本执行到这里请求展示完整对象组合;布局驱动随后按出现约束求解,再发布不可变绘制帧。 page.render(surface) diff --git a/examples/flex-constraints.endor b/examples/flex-constraints.endor index 669f7b2..5d33568 100644 --- a/examples/flex-constraints.endor +++ b/examples/flex-constraints.endor @@ -3,6 +3,7 @@ // 给 Row 一个有限 340x60 的期望尺寸,便于精确观察分配数值。 class Strip: Row { // 源测量返回固定尺寸;后续排列仍使用原生 Row 的 padding/gap 与 flex 规则。 + // source_layout 在该 Strip 出现的测量阶段调用,返回 MeasureResult 仍会按父传入 Constraints 钳制。 function measure(self: Strip, mount: Mount, constraints: Constraints): MeasureResult { return MeasureResult(340, 60) } @@ -12,15 +13,18 @@ class Actions { first: RectBlock second: RectBlock // 两项 basis=100,按1:3分增长;首项 max=105 触顶后,多余增长转给第二项。 + // 增长按钮的绑定回调,先复位首项下界/上界,再改 basis 与权重,最终布局在本次输入后的刷新中求解。 function grow(self: Actions): Unit { self.first.flexMin = 0 self.first.flexMax = 105 + // basis 提供参与分配的起始主轴尺寸,与 RectBlock 构造时自然宽度不同。 self.first.flexBasis = 100 self.second.flexBasis = 100 self.first.flex = 1 self.second.flex = 3 } // 两项 basis=200 造成缺额,以 flexShrink*basis 分摊;首项 min=150 后第二项继续收缩到90。 + // 收缩按钮回调复位增长上界并设置下界,两个 flex=0 不参与增长,但 flexShrink=1 仍允许按 basis 收缩。 function shrink(self: Actions): Unit { self.first.flexMax = 300 self.first.flexMin = 150 @@ -28,22 +32,28 @@ class Actions { self.second.flexBasis = 200 self.first.flex = 0 self.second.flex = 0 + // 权重是 flexShrink×basis,本例 basis 同为 200,首项触底后剩余缺额继续转到第二项。 self.first.flexShrink = 1 self.second.flexShrink = 1 } } // 自然宽20/40与basis可不同;fixed不设置权重,保持原始60宽。 first = RectBlock(20, 20, 4, "#A4C8F0") +// 两个可调对象分别用不同颜色展示,当前自然宽并不决定设置 flexBasis 后的起始分配。 second = RectBlock(40, 20, 4, "#A6D8B2") +// 未改 flex/basis/shrink 的固定项不承担本例缺额,保留 60 宽参与总宽计算。 fixed = RectBlock(60, 20, 4, "#E8C990") actions = Actions(first, second) grow = Button("增长:上限后重新分配") +// 注册零参源方法,由点击派发执行;下面显式 actions.grow() 才是在初次 render 前立即初始化。 grow.onClick(actions.grow) shrink = Button("收缩:最小尺寸后重新分配") shrink.onClick(actions.shrink) // 先初始化增长策略再挂载,首次发布即采用105/135/60,不需等待按钮点击。 actions.grow() +// 340 总宽减左右 padding 共 20、两个 gap 共 20 和固定项 60,留给两项的主轴总量是 240。 strip = Strip(10, 10, first, second, fixed) +// 输出提示中的 105/135 或 150/90 基于 Strip 获得足够父约束的尺寸;实际几何仍由宿主约束校验。 page = Page() page.title = "基础尺寸与边界" page.render(Column(20, 12, Row(0, 10, grow, shrink), strip, TextBlock("增长:105 / 135 / 60;收缩:150 / 90 / 60。"))) diff --git a/examples/flex.endor b/examples/flex.endor index cd475e9..887377f 100644 --- a/examples/flex.endor +++ b/examples/flex.endor @@ -2,25 +2,35 @@ // paragraph 只有一份业务内容,但在两个 Row 的 Mount 中各自按真实分配宽度测量/换行。 // 只持有首个 Row 的 filler,修改它的权重来间接改变同排文字分配。 class Actions { + // Actions 只持有首排填充块引用,不持有共享段落或第二排的固定矩形,按钮修改的对象范围因此明确。 filler: RectBlock // 将 filler 权重恢复为 1,与 paragraph=1 近似等分可用空间。 + // Button.onClick 接受绑定后的零参回调,事件系统负责调用;这里改权重,不直接给 Mount 写最终几何。 function balanced(self: Actions): Unit { self.filler.flex = 1 } // filler 权重改为 3,paragraph 在首排获得剩余空间的四分之一,换行高度随之增加。 + // 同一父 Row 内 paragraph=1、filler=3 按剩余空间分配,父容器与子上限仍可进一步限制实际宽度。 function narrow(self: Actions): Unit { self.filler.flex = 3 } } // 共享文字的 flex=1 是业务布局策略,具体几何与自然尺寸缓存仍属于各 Mount。 paragraph = TextBlock("GUI 和代码操控同一套 Aster 内存对象系统。对象只有一份,但每次挂载都有自己的约束和布局。这里用同一个 TextBlock 在两个容器中显示不同的换行。修改权重会重新测量文字高度,随后更新相邻布局。GUI 和代码操控同一套 Aster 内存对象系统。对象只有一份,但每次挂载都有自己的约束和布局。这里用同一个 TextBlock 在两个容器中显示不同的换行。修改权重会重新测量文字高度,随后更新相邻布局。") +// 原生 measure 取得段落自然/换行尺寸,原生 arrange 分配主轴空间,源布局驱动可为分配宽度再次测高。 paragraph.flex = 1 +// 自然尺寸 20×20 不等于 flex 后最终宽度;flex=1 让这个出现参与首排剩余空间分配。 filler = RectBlock(20, 20) filler.flex = 1 +// 绑定下面两个按钮的方法时捕获同一 Actions 实例,后续点击仍更新已挂载的 filler。 actions = Actions(filler) balanced = Button("等比例分配") +// 注册监听器而不立即执行 balanced,所以第一次布局沿用上面已经设好的初始权重。 balanced.onClick(actions.balanced) narrow = Button("文字只占四分之一") +// 另一个按钮只选择同一实例的方法,切换策略不重新构造两排容器或共享文字。 narrow.onClick(actions.narrow) // 首排两个可伸缩子项,次排只有文字 flex 与固定 300px 兄弟,因此首排按钮不会改次排宽度。 first = Row(10, 10, paragraph, filler) +// 第二排的段落仍是同一业务对象,兄弟是未设置 flex 的固定宽矩形,分配宽度属于第二个出现。 second = Row(10, 10, paragraph, RectBlock(300, 20)) +// 同一个 Page 的根中同时包含两个段落 Mount,修改文本会影响两处,修改首排 filler 权重只改变首排约束。 page = Page() page.title = "按权重分配与文字重排" page.render(Column(10, 10, Row(0, 10, balanced, narrow), first, second)) diff --git a/examples/layout-cache.endor b/examples/layout-cache.endor index 36f17b7..b02b022 100644 --- a/examples/layout-cache.endor +++ b/examples/layout-cache.endor @@ -4,6 +4,7 @@ class WidthModel { width: Number // 点击把模型宽度置为 260;源 measure 读取过该字段,缓存因此失效。 + // 按钮 click 的 Event 回调,只写 WidthModel.width;后续是否重测由已记录的真实读取失效决定。 function expand(self: WidthModel, event: Event): Unit { self.width = 260 } @@ -12,21 +13,26 @@ class WidthModel { class Tint { color: String // 只改颜色;正常情况下需重绘而不重新执行无关源测量。 + // 另一按钮的 Event 回调只写 Tint.color,不改 dimensions,避免用无关模型写入混淆测量依赖。 function blue(self: Tint, event: Event): Unit { self.color = "#526AC7" } } // 原生矩形的尺寸/颜色策略分别由源协议覆盖,业务模型通过字段持有。 class CachedRect: RectBlock { + // CachedRect 引用两个不同策略对象;measure 读尺寸对象,paint 读颜色对象,各阶段依赖分开记录。 dimensions: WidthModel tint: Tint // 读取 dimensions.width 返回固定高 60;实际读观察由运行时缓存记录,不手工声明依赖表。 + // 只读测量由 source_layout 调用,self 是共享矩形,mount/constraints 是当前出现输入,不写 geometry。 function measure(self: CachedRect, mount: Mount, constraints: Constraints): MeasureResult { return MeasureResult(self.dimensions.width, 60) } // 复用原生矩形命令,再改新命令的颜色;修改新结果被允许,但 paint 不能修改已有业务对象。 function paint(self: CachedRect, mount: Mount, geometry: Bounds): DisplayList { + // 源 paint 由 source_paint 调用,显式委托基类得到本轮新 DisplayList,不在这里发输入事件或改变 Page。 drawing = RectBlock.paint(self, mount, geometry) + // 修改的是新分配命令的颜色字段,再由宿主统一解码校验;self.tint 仍只读。 drawing.commands.at(0).color = self.tint.color return drawing } @@ -34,6 +40,7 @@ class CachedRect: RectBlock { // 宽度来自 ui.viewport 的当前逻辑页面区域,显示窗口变化也能成为测量依赖。 class ViewportBand: RectBlock { // 返回可用视口宽度,最终仍服从父容器约束;外层 padding 会减少分配宽度。 + // ui.viewport 取当前窗口逻辑页面范围,实际返回尺寸还会受父级更窄的 Constraints 约束。 function measure(self: ViewportBand, mount: Mount, constraints: Constraints): MeasureResult { return MeasureResult(ui.viewport().width, 24) } @@ -41,12 +48,15 @@ class ViewportBand: RectBlock { // 创建相互独立的宽度/颜色模型并绑定两个按钮,用相同对象观察缓存与可见结果。 dimensions = WidthModel(200) tint = Tint("#ED6A5A") +// 源字段 dimensions/tint 放在原生构造参数后,RectBlock 的默认尺寸不是这个覆盖 measure 的最终输出。 rect = CachedRect(80, 20, 0, "#ED6A5A", dimensions, tint) band = ViewportBand(80, 24, 0, "#237A45") blue = Button("只改绘制颜色") expand = Button("修改测量依赖") +// 保存两个监听连接,颜色按钮绑定 tint,宽度按钮绑定 dimensions,不把 UI 事件对象用作缓存 owner。 blueConnection = blue.on("click", tint.blue) expandConnection = expand.on("click", dimensions.expand) +// 根组合同时展示 viewport 依赖和业务尺寸依赖;按钮回调完成后宿主统一刷新,不在点击方法里手工重排。 root = Column(24, 16, TextBlock("改变颜色保留测量缓存;改变宽度重新测量"), band, rect, blue, expand) page = Page() page.render(root) diff --git a/examples/layout-negotiation.endor b/examples/layout-negotiation.endor index ac96927..72cd96b 100644 --- a/examples/layout-negotiation.endor +++ b/examples/layout-negotiation.endor @@ -4,26 +4,33 @@ class Policy { width: Number // 选择 280 逻辑像素宽度,段落变高,后继文字下移。 + // narrow/wide 按钮事件回调更新同一 policy.width,只有持有该策略的 ArrangedColumn 读取它。 function narrow(self: Policy, event: Event): Unit { self.width = 280 } // 选择 560 逻辑像素宽度,段落变矮;另一个容器宽度不跟着改变。 function wide(self: Policy, event: Event): Unit { self.width = 560 } } // 在原生 Column 排列基础上覆盖第一个稳定子槽的宽度。 class ArrangedColumn: Column { + // 布局共享策略模型,不保存段落的全局最终宽度;某个出现的约束/分配由 Plan 独立记录。 policy: Policy // 先取得包含全部子槽的 Arrangement,仅改第一子项分配;保留其他槽使排列验证完整。 function arrange(self: ArrangedColumn, mount: Mount, geometry: Bounds): Arrangement { + // source_layout 的安排阶段调用覆盖方法,基类利用本轮孩子测量值生成包含全部稳定 slot 的临时结果。 result = Column.arrange(self, mount, geometry) + // 只改返回的第一子槽 Bounds.width,宿主会验证槽完整性并据显式分配反推下一轮精确测量约束。 result.children.at(0).width = self.policy.width return result } } // 通过源 measure 改整个容器的期望宽度,以对比 arrange 分配路径。 class MeasuredColumn: Column { + // MeasuredColumn 自有策略字段,不是共享 paragraph 的 boxWidth,也不复制段落内容。 width: Number // 调用原生测量后修改新结果 width;这个新结果不属于已有业务对象,允许在只读计算中填写。 function measure(self: MeasuredColumn, mount: Mount, constraints: Constraints): MeasureResult { + // 测量阶段先委托基类求自然尺寸,再修改临时 MeasureResult;已有业务对象仍受只读限制。 result = Column.measure(self, mount, constraints) + // 宿主读取这个新结果字段后还会按 Constraints 钳制;源返回 width 不等于绕过上层几何预算。 result.width = self.width return result } @@ -32,13 +39,16 @@ class MeasuredColumn: Column { policy = Policy(280) paragraph = TextBlock("源码 arrange 改变分配宽度后,文字会按实际宽度重新测量。增加的高度继续影响容器和相邻对象,直到整棵布局稳定后才发布。这里与下面使用的是同一个 TextBlock;每个 Mount 都有自己的约束与几何。同一套内存对象不需要复制业务状态,也不通过丢弃传播来结束更新。") tail = TextBlock("这一行跟随上方段落。") +// 首个孩子是 paragraph,故 at(0) 修改的是它的分配;tail 的位置在重新测高与收敛后随段落高度更新。 arranged = ArrangedColumn(0, 12, paragraph, tail, policy) +// 另一出现仍用同一 paragraph,但父测量策略为 360,与上面的 policy.width 独立。 measured = MeasuredColumn(0, 12, paragraph, TextBlock("这一段的宽度来自源码 measure。"), 360) narrow = Button("arrange 为 280") wide = Button("arrange 为 560") // 保留事件订阅引用,两个按钮调用同一 Policy 的不同方法。 narrowConnection = narrow.on("click", policy.narrow) wideConnection = wide.on("click", policy.wide) +// 这两个父容器都在同一文档根,布局需要整树稳定再一起发布,不能先显示新宽度再补旧高度。 page = Page() page.title = "约束协商" page.render(Column(24, 16, Row(0, 12, narrow, wide), arranged, measured)) diff --git a/examples/source-layout.endor b/examples/source-layout.endor index c0880cd..1caeda3 100644 --- a/examples/source-layout.endor +++ b/examples/source-layout.endor @@ -2,9 +2,11 @@ // 运行时在只读求解中跟踪这些读取,事件修改模型后重新布局;布局方法不能直接改业务字段。 // 保存宽度与左偏移;两字段只在事件回调中修改。 class Metrics { + // width 提供 measure 输出,left 提供 arrange 的首子槽 x;模型状态不是 Mount 的派生几何副本。 width: Number left: Number // 同时改变测量和排列依赖,整帧需收敛后一起发布新尺寸/位置。 + // button.on 的绑定事件回调写入两个字段,方法返回后的刷新把受影响布局合并到同一帧。 function expand(self: Metrics, event: Event): Unit { self.width = 260 self.left = 68 @@ -15,7 +17,9 @@ class MeasuredRect: RectBlock { // 首次测量前结构 Mount 链接已建立,因此可从 parent.block 读取当前出现位置的父策略。 // 此阶段还不能查询最终 bounds,返回值使用 MeasureResult 名义类型。 function measure(self: MeasuredRect, mount: Mount, constraints: Constraints): MeasureResult { + // 从当前出现而非共享 rect 查父 Mount,同一 MeasuredRect 换父容器时可读取不同的 metrics。 parent = mount.parent() + // 父 Block 需要符合示例的 metrics 约定;MeasureResult 宽高随后仍由宿主按当前 Constraints 检查和钳制。 return MeasureResult(parent.block.metrics.width, 60) } } @@ -24,16 +28,22 @@ class OffsetColumn: Column { metrics: Metrics // Arrangement 里的 x 是当前排列使用的坐标;读取 metrics.left 成为排列依赖。 function arrange(self: OffsetColumn, mount: Mount, geometry: Bounds): Arrangement { + // 保留原生 Column 对全部孩子的槽分配,临时结果可修改,不在 arrange 中写 self.metrics。 positions = Column.arrange(self, mount, geometry) + // 这里直接覆盖排列结果坐标为 metrics.left,不是给 geometry.x 再加相对位移;其他槽保持原生结果。 positions.children.at(0).x = self.metrics.left return positions } } // 将原生参数与源字段传给具体子类,button 事件更新模型,Page.render 建立完整展示关系。 metrics = Metrics(200, 44) +// 原生矩形构造参数仍提供绘制载荷,实际测量被源覆盖为模型宽度和固定高 60。 rect = MeasuredRect(80, 20, 0, "#526AC7") button = Button("更新布局") +// 首个孩子是 rect,metrics 是 Column 子类新增字段,不是额外的视觉子项。 root = OffsetColumn(24, 16, rect, TextBlock("尺寸来自源码 measure,位置来自源码 arrange"), button, metrics) +// 保存监听连接使按钮操作同一个模型;布局方法由宿主协议调用,不由事件回调直接手工调用。 connection = button.on("click", metrics.expand) page = Page() +// 先建立出现树和临时 Mount 上下文,再运行 measure/arrange,源方法才能在求解阶段查看父关系。 page.render(root) diff --git a/examples/wrapped-text.endor b/examples/wrapped-text.endor index 3437f02..fbac9d0 100644 --- a/examples/wrapped-text.endor +++ b/examples/wrapped-text.endor @@ -3,16 +3,19 @@ // 没有独立宽度副本;约束与换行结果属于出现位置,文本内容属于共享对象。 class Paragraph: TextBlock { // 将当前 Mount 与 Constraints 交给原生文字测量,宽度变化触发重测高度。 + // 宿主测量驱动调用源覆盖,源方法再显式调用原生基类,使用当前出现的可用宽度与字体系统计算高度。 function measure(self: Paragraph, mount: Mount, constraints: Constraints): MeasureResult { return TextBlock.measure(self, mount, constraints) } // 使用本次 geometry 画文字,和原生测量采用相同换行区域。 + // 源绘制驱动在布局完成后传本轮 Bounds,原生文字命令携带对应换行边界,不重新猜另一套宽度。 function paint(self: Paragraph, mount: Mount, geometry: Bounds): DisplayList { return TextBlock.paint(self, mount, geometry) } } // 保存主窗口、共享文字和原全文,四个按钮分别改窗口尺寸或改文字内容。 class Actions { + // 只保存主窗口资源;narrow/wide 不改下面 other 的尺寸,两个窗口共享的是 paragraph 而非 viewport。 window: Window paragraph: TextBlock full: String @@ -21,33 +24,41 @@ class Actions { // 请求主窗口变宽,文字高度随之减小,不改另一窗口尺寸。 function wide(self: Actions, event: Event): Unit { self.window.resize(1200, 650) } // 用短文本替换同一对象内容,两个窗口的文字都重新测量。 + // 短文本提示只是示例字符串;真实 paragraph 仍同时出现在两个窗口,修改内容不会把两处 Mount 合成一个。 function short(self: Actions, event: Event): Unit { self.paragraph.setText("短文本:同一个对象、同一个 Mount。") } // 恢复保存的 full 字符串,不创建新的 Paragraph。 function restore(self: Actions, event: Event): Unit { self.paragraph.setText(self.full) } } full = "GUI 和代码操控同一套 Aster 内存对象。这个段落同时出现在两个窗口:对象内容相同,每个 Mount 根据自己的可用宽度换行。窗口变窄,段落高度增加,后面的对象向下移动;窗口变宽,段落高度减小,后面的对象向上移动。对象身份和已有 Mount 身份保持稳定。English words wrap at word boundaries; a_long_unbroken_identifier_also_wraps_when_it_cannot_fit_on_one_line. 中文、组合字符 é 与 emoji 👩‍💻 使用同一套字体整形。再次修改文字会重新计算高度,并更新两个窗口,不需要复制业务对象。" +// 只创建一次共享业务对象,full 字符串另由 Actions 保存供 restore 恢复,不重新 new 一个段落实例。 paragraph = Paragraph(full) +// 后继文字没有硬编码 y,由 Column 安排读取前一个段落本轮实际高度来定位。 tail = TextBlock("这一行的位置由前面段落的实际高度决定。") actions = Actions(ui.window(), paragraph, full) narrow = Button("变窄") wide = Button("变宽") short = Button("短文本") restore = Button("恢复段落") +// 四个连接各绑定同一 Actions 实例的不同事件方法;方法声明的 event 由点击派发提供。 narrowConnection = narrow.on("click", actions.narrow) wideConnection = wide.on("click", actions.wide) shortConnection = short.on("click", actions.short) restoreConnection = restore.on("click", actions.restore) +// 主页面先展示四按钮、共享段落和 tail,页面与窗口身份和 paragraph 业务身份分别保存。 page = Page() page.title = "约束下的文字换行" page.render(Column(24, 16, Row(0, 12, narrow, wide, short, restore), paragraph, tail)) // 新窗口有独立可用宽度,但 second.render 仍用同一 paragraph,所以两个 Mount 可有不同高度。 +// 创建同 VM 的另一个窗口资源,后面的 resize/title/move 是提交后由 GUI 执行的窗口命令。 other = Window() other.resize(720, 500) other.setTitle("同一个段落 · 另一组宽度") other.moveTo(1700, 80) +// 第二页面使用不同的 Column 宽度上下文,再引用同一个 paragraph,产生独立换行测量和 Mount。 second = Page() second.title = "共享 Paragraph" second.render(Column(24, 16, paragraph)) +// 将此页归到副窗口,不把主窗口 Page 或它的 Mount 直接复制过去。 second.open(other) // 恢复主页面活动状态并设置原生窗口尺寸/标题/桌面位置,focus 命令在成功输入后交宿主执行。 page.activate()