feat: build functional Endor start page and balance compact tabs
This commit is contained in:
@@ -273,7 +273,15 @@ create additional managed Pages; Cmd/Ctrl+W closes the active Page. New tabs exp
|
||||
from left to right while existing tabs interpolate to their new bounds when space
|
||||
becomes limited. Hover and selection use the same capsule geometry, and animated
|
||||
pointer hits use the rendered bounds. Source and page identity remain in the VM;
|
||||
chrome animation retains only projected scalar IDs and rectangles.
|
||||
chrome animation retains only projected scalar IDs and rectangles. Tabs are capped at
|
||||
208 logical pixels wide, with a 40-pixel strip and equal 4-pixel vertical insets.
|
||||
The leading page symbol and close control have matching slots and edge spacing.
|
||||
|
||||
The default page is implemented in `src/ui_bridge/start_page.endor`, compiled once
|
||||
per VM into a private namespace. Its buttons create a Page, open a working shared
|
||||
counter, or show composed Blocks. `ui.newTab()` constructs the same ordinary Page
|
||||
used by native + and the keyboard shortcut. Layout, callbacks, state and bindings
|
||||
are Endor objects; the host does not paint an introduction over the guest page.
|
||||
|
||||
Console, Sources, Network and Settings are independent managed Pages with separate
|
||||
view modules and window-local input/focus/scroll/selection state. They are not
|
||||
|
||||
@@ -4,9 +4,9 @@ This audit follows the full conversation, including later corrections. Implement
|
||||
|
||||
| Requirement | Implementation | Evidence / limits |
|
||||
| --- | --- | --- |
|
||||
| One top row with native macOS controls | Transparent/full-size native title bar; reserved control space; 44 px tab strip | Actual Retina macOS windows viewed; native close/minimize/fullscreen controls retained |
|
||||
| One top row with native macOS controls | Transparent/full-size native title bar; reserved control space; 40 px tab strip | Actual Retina macOS windows viewed; native close/minimize/fullscreen controls retained |
|
||||
| Continuous left/top chrome | One neutral background; 56 px left rail; inset content panel | Actual main and detached windows viewed; no Tools label or rail launchers |
|
||||
| Larger symmetric round tabs, softer active state | Common capsule bounds, measured glyph centering, no hard active stroke | Native settled UI viewed; hover and selected use the same bounds |
|
||||
| Compact symmetric round tabs, softer active state | 208 px maximum width; equal 4 px vertical insets; matching icon/close slots; no hard active stroke | Native settled UI viewed; hover and selected use the same bounds |
|
||||
| + and startup default page | One startup request guarded against duplicate resume; + release-inside; Cmd/Ctrl+T | Immediately after restart: pages().count = 1. Two native Cmd+T actions produced three tabs |
|
||||
| New-tab motion, filled and unfilled | 200 ms cubic ease; new width grows from zero; existing bounds and + interpolate; scalar-only animation data | Real native new/close/fill operations completed; settled compression/overflow viewed. No frame-by-frame recording or Chrome timing comparison |
|
||||
| Overflow and scrolling | Separate + reservation; arrows, edge dwell and hit bounds share tab viewport | Native overflow reached; + in overflow increased VM count 7 → 8. Dragging out of + kept count at 7 |
|
||||
@@ -15,7 +15,7 @@ This audit follows the full conversation, including later corrections. Implement
|
||||
| Four tools not default or permanently placed | No Tools menu, rail buttons or new-page tool directory; existing API/shortcut retained | Final default surfaces viewed without tool entries |
|
||||
| Source reading/selection/copy | Grapheme-boundary selection, no gutter in copy, select-all/copy, Shift navigation, drag edge scrolling | Native single-line drag/copy/paste verified earlier in this change. Final keyboard copy was re-run after the chrome-focus fix: drag-copy matched the complete selected line; four Shift+Right operations copied the expected four characters; select-all copied multiline source without a gutter. Drag edge behavior compiled; exhaustive combinations remain untested |
|
||||
| Clicking Console output must not jump to prompt/end | Input hit region restricted to actual prompt rows | Native reading/copy path verified earlier; no input hit over transcript-only area |
|
||||
| Default page reflects Endor | Objects, code and UI share identity; persistence/model connections explicitly described as work in progress | Referenced chats read: 01a16819-28c3-7ce0-9423-0ffaa19402ce and 01a1689b-77d8-75a2-ba9e-b2fa3dce654e; actual native start surface viewed |
|
||||
| Default page reflects Endor | Real Endor Page with creation, shared-object and composition actions; no persistence/model backend claims | Referenced chats read: 01a16819-28c3-7ce0-9423-0ffaa19402ce and 01a1689b-77d8-75a2-ba9e-b2fa3dce654e; actual native start surface viewed |
|
||||
| Sensible source organization | Runtime/API tests separated; four page view modules; shared painter/selection; native tool Page factory; motion module | All-target Clippy compiled production and test sources. Runtime host reduced from ~7000 to ~1380 lines |
|
||||
| Protect existing work | examples/README.md and examples/workspace.endor excluded; vendor/Aster untouched | git status checked before staging |
|
||||
| Verification restrictions | fmt, all-target Clippy -D warnings, release build and manual native/API checks | No cargo test, smoke script, Docker, dependency test install or CI polling |
|
||||
@@ -44,3 +44,38 @@ After the fix, direct system clipboard comparisons (without printing clipboard c
|
||||
## Local delivery state
|
||||
|
||||
Implementation commits: 3220544 (runtime test-source separation), 656522e (managed workspace/chrome implementation), d3165b1 (behavior and requirement audit), followed by the chrome-focus fix commit. A push to origin/main was attempted once and rejected by automatic approval review because explicit authorization for this default-branch remote mutation was required. No alternative push route was attempted. The remote remains unchanged pending the user's approval.
|
||||
|
||||
|
||||
## Functional start-page revision
|
||||
|
||||
The previous Rust-painted introduction was removed. `ui_bridge/start_page.endor`
|
||||
now defines a managed Page subclass with native layout alignment, real buttons,
|
||||
a shared-state counter and a composed-Blocks example. Its private module globals
|
||||
do not overwrite user REPL bindings. The same factory is available as `ui.newTab()`;
|
||||
legacy `ui.developerTools("New tab")` delegates to it. No developer-tool entry is
|
||||
shown on the start page.
|
||||
|
||||
Native startup checks caught unsupported comparison syntax and unimplemented
|
||||
arithmetic bytecode in initial layout drafts. Those drafts were replaced with
|
||||
existing Page/Row measurement and native `mainAlign`, without changing Aster.
|
||||
A subsequent layout check caught unbounded child-height centering; sizing now
|
||||
belongs to the root Page, which receives the actual content constraints.
|
||||
These failures were corrected locally, and are not counted as passing checks.
|
||||
|
||||
|
||||
Final native checks for this revision: all three start-page buttons opened their
|
||||
corresponding managed Pages; the blank-page return action activated its original
|
||||
home Page; one native counter click changed both views from 0 to 1. Overflow
|
||||
navigation was visible while these Pages were added. A direct `ui.newTab()` call
|
||||
increased the live Page count from 4 to 5, confirming one Page per factory call.
|
||||
The final window was also being operated by the user (reported by the UI tool),
|
||||
so the pre-existing four tabs are not treated as a clean-start count measurement.
|
||||
No additional restart was done after that interaction. The earlier isolated
|
||||
startup count of one remains evidence for the guarded startup path; the new
|
||||
factory's single-call count was checked separately.
|
||||
|
||||
Formatting, all-target offline Clippy with warnings denied, the release macOS
|
||||
bundle and diff whitespace checks passed. No test suite or smoke script was run.
|
||||
All earlier requirements were reviewed against the table above; isolated Chrome
|
||||
motion comparison and the explicitly listed cross-window/DPI verification gaps
|
||||
remain unverified, rather than being marked complete.
|
||||
|
||||
+2
-1
@@ -257,7 +257,8 @@ Property binding caches now consume synchronous precise ReadSet dependencies, re
|
||||
Window workspace modules:
|
||||
- `tabs.rs`: common tab/close/+ geometry, overflow and shared hover/active capsules.
|
||||
- `tabs/motion.rs`: 200 ms eased geometry shared by painting and pointer hits.
|
||||
- `system_ui/start_page.rs`: the Endor start page; future capabilities are marked as such.
|
||||
- `ui_bridge/start_page.endor`: the functional default Page, layout, callbacks and examples in Endor.
|
||||
- `ui_bridge/developer_pages.rs`: loads the private start-page module and delegates new-tab creation to its managed factory.
|
||||
- `ui_bridge/developer_pages.rs`: managed tool/start Pages, identity-preserving reopen and focus.
|
||||
- `developer_tools/{console,sources,network,settings}.rs`: separate page views.
|
||||
- `developer_tools/painter.rs`: reusable pane painting and response formatting.
|
||||
|
||||
+1
-6
@@ -88,12 +88,7 @@ pub fn build_scene(
|
||||
);
|
||||
developer_list.translate(crate::geometry::Point::new(left * scale, offset * scale));
|
||||
developer_layout.translate(left, offset);
|
||||
if tool == Some("New tab") {
|
||||
developer_layout = DeveloperToolsLayout::default();
|
||||
crate::system_ui::start_page::draw(&mut display_list, content, scale);
|
||||
} else {
|
||||
display_list.extend(developer_list);
|
||||
}
|
||||
display_list.extend(developer_list);
|
||||
|
||||
DisplayScene {
|
||||
display_list,
|
||||
|
||||
@@ -63,4 +63,4 @@ those concepts enter this library. The library currently renders synchronously
|
||||
with immediate state feedback; it is not an Endor component implementation, an
|
||||
OS accessibility bridge or a separately published Cargo crate.
|
||||
|
||||
`start_page.rs` paints the built-in Endor start surface. Native window chrome uses the same neutral palette, while each tool Page has its own interaction state.
|
||||
The built-in start page lives in `ui_bridge/start_page.endor` and uses the ordinary guest Page/Block renderer. System UI no longer paints a welcome overlay. Native window chrome uses the same neutral palette, while each tool Page has its own interaction state.
|
||||
|
||||
@@ -5,5 +5,3 @@ pub mod theme;
|
||||
mod widgets;
|
||||
pub use interaction::{Axis, Cursor, Interaction, Role, Target, VisualState, WidgetId};
|
||||
pub use widgets::{Button, Ui};
|
||||
|
||||
pub mod start_page;
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
//! The start page introduces Endor without advertising unimplemented controls.
|
||||
use super::theme;
|
||||
use crate::{
|
||||
display_list::{DisplayList, DrawCommand},
|
||||
geometry::{Point, Rect},
|
||||
text::{self, TextStyle},
|
||||
};
|
||||
pub fn draw(list: &mut DisplayList, content: Rect, scale: f32) {
|
||||
list.push(DrawCommand::FillRoundedRect {
|
||||
rect: content,
|
||||
radius: 12.0 * scale,
|
||||
color: theme::BG,
|
||||
});
|
||||
let width = content.width / scale;
|
||||
let left = content.x / scale + ((width - 620.0) / 2.0).max(32.0);
|
||||
let top = content.y / scale + (content.height / scale * 0.20).clamp(40.0, 150.0);
|
||||
list.push(DrawCommand::PushClip(content));
|
||||
for (dy, value, size, color) in [
|
||||
(0.0, "Endor", 3, theme::TEXT),
|
||||
(82.0, "对象、代码与界面,在同一个世界里。", 1, theme::TEXT),
|
||||
(118.0, "构建可交互的页面、工具与工作流。", 1, theme::MUTED),
|
||||
(210.0, "对象拥有身份", 1, theme::TEXT),
|
||||
(
|
||||
240.0,
|
||||
"代码与可视界面共享同一份对象,改变对象,也改变它的展示。",
|
||||
1,
|
||||
theme::MUTED,
|
||||
),
|
||||
(310.0, "从这里开始", 1, theme::TEXT),
|
||||
(
|
||||
340.0,
|
||||
"这是一个新的页面。你的程序可以在这里组织和呈现对象。",
|
||||
1,
|
||||
theme::MUTED,
|
||||
),
|
||||
(
|
||||
430.0,
|
||||
"正在构建:持久对象与多模态模型连接。",
|
||||
1,
|
||||
theme::MUTED,
|
||||
),
|
||||
] {
|
||||
list.push(DrawCommand::Text {
|
||||
origin: Point::new(left * scale, (top + dy) * scale),
|
||||
value: text::fit(value, (width - 64.0).min(620.0), TextStyle::ui(size)),
|
||||
style: TextStyle::ui(size).scaled(scale),
|
||||
color,
|
||||
});
|
||||
}
|
||||
list.push(DrawCommand::PopClip);
|
||||
}
|
||||
+39
-12
@@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize};
|
||||
mod motion;
|
||||
pub use motion::{Motion, Visual};
|
||||
|
||||
pub const HEIGHT: f32 = 44.0;
|
||||
pub const HEIGHT: f32 = 40.0;
|
||||
pub const INSET: f32 = 8.0;
|
||||
const NEW_TAB_SPACE: f32 = 44.0;
|
||||
pub const RAIL: f32 = 56.0;
|
||||
@@ -33,7 +33,7 @@ pub struct Tab {
|
||||
|
||||
pub fn height(tabs: &[Tab]) -> f32 {
|
||||
let _ = tabs;
|
||||
HEIGHT + INSET
|
||||
HEIGHT
|
||||
}
|
||||
|
||||
/// A drag viewport is presentation state only: it never activates a Page.
|
||||
@@ -97,7 +97,7 @@ fn items(start: Option<usize>, tabs: &[Tab], width: f32) -> Vec<Item<'_>> {
|
||||
let overflow = count < tabs.len();
|
||||
let margin = if overflow { 32.0 } else { 0.0 };
|
||||
let width = width - NEW_TAB_SPACE;
|
||||
let tab_width = ((width - LEADING - margin * 2.0) / count as f32).min(240.0);
|
||||
let tab_width = ((width - LEADING - margin * 2.0) / count as f32).min(208.0);
|
||||
let mut result = Vec::new();
|
||||
if overflow && start > 0 {
|
||||
result.push(Item {
|
||||
@@ -299,7 +299,7 @@ pub fn draw(
|
||||
origin: Point::new(
|
||||
(plus.x + (plus.width - crate::text::measure("+", TextStyle::ui(1)).width) / 2.0)
|
||||
* scale,
|
||||
((HEIGHT - 20.0) / 2.0) * scale,
|
||||
((HEIGHT - TextStyle::ui(1).line_height) / 2.0) * scale,
|
||||
),
|
||||
value: "+".into(),
|
||||
style: TextStyle::ui(1).scaled(scale),
|
||||
@@ -326,24 +326,51 @@ pub fn draw(
|
||||
},
|
||||
});
|
||||
}
|
||||
if item.navigation.is_none() && item.rect.width >= 66.0 {
|
||||
// Same 20px slot and 8px edge inset as the close button.
|
||||
let icon = "◇";
|
||||
list.push(DrawCommand::Text {
|
||||
origin: Point::new(
|
||||
(item.rect.x
|
||||
+ 8.0
|
||||
+ (20.0 - crate::text::measure(icon, TextStyle::ui(1)).width) / 2.0)
|
||||
* scale,
|
||||
((HEIGHT - TextStyle::ui(1).line_height) / 2.0) * scale,
|
||||
),
|
||||
value: icon.into(),
|
||||
style: TextStyle::ui(1).scaled(scale),
|
||||
color: crate::system_ui::theme::MUTED,
|
||||
});
|
||||
}
|
||||
let title = item.navigation.unwrap_or(&item.tab.title);
|
||||
let text_rect = Rect::new(
|
||||
item.rect.x + 14.0,
|
||||
4.0,
|
||||
item.rect.x
|
||||
+ if item.navigation.is_some() {
|
||||
10.0
|
||||
} else {
|
||||
34.0
|
||||
},
|
||||
(HEIGHT - TextStyle::ui(1).line_height) / 2.0,
|
||||
(item.rect.width
|
||||
- if item.navigation.is_some() {
|
||||
12.0
|
||||
} else {
|
||||
46.0
|
||||
66.0
|
||||
})
|
||||
.max(0.0),
|
||||
28.0,
|
||||
TextStyle::ui(1).line_height,
|
||||
);
|
||||
list.push(DrawCommand::PushClip(scaled(text_rect)));
|
||||
list.push(DrawCommand::Text {
|
||||
origin: Point::new(
|
||||
(item.rect.x + 14.0) * scale,
|
||||
((HEIGHT - 20.0) / 2.0) * scale,
|
||||
(item.rect.x
|
||||
+ if item.navigation.is_some() {
|
||||
10.0
|
||||
} else {
|
||||
34.0
|
||||
})
|
||||
* scale,
|
||||
((HEIGHT - TextStyle::ui(1).line_height) / 2.0) * scale,
|
||||
),
|
||||
value: title.into(),
|
||||
style: TextStyle::ui(1).scaled(scale),
|
||||
@@ -369,7 +396,7 @@ pub fn draw(
|
||||
- crate::text::measure("×", TextStyle::ui(1)).width)
|
||||
/ 2.0)
|
||||
* scale,
|
||||
((HEIGHT - 20.0) / 2.0) * scale,
|
||||
((HEIGHT - TextStyle::ui(1).line_height) / 2.0) * scale,
|
||||
),
|
||||
value: "×".into(),
|
||||
style: TextStyle::ui(1).scaled(scale),
|
||||
@@ -425,7 +452,7 @@ mod tests {
|
||||
&tabs,
|
||||
LEADING + NEW_TAB_SPACE + 1000.0,
|
||||
1,
|
||||
Point::new(LEADING + 220.0, 18.0)
|
||||
Point::new(LEADING + 190.0, 18.0)
|
||||
)
|
||||
.is_none()
|
||||
);
|
||||
|
||||
+3
-1
@@ -3407,7 +3407,9 @@ pub fn install(vm: &mut Vm, roots: &mut Vec<ObjectRef>) -> Result<UiBridge> {
|
||||
crate::protocols::install(vm, roots, desktop, ui)?;
|
||||
let tasks = crate::tasks::install(vm, roots, ui, desktop)?;
|
||||
developer_pages::install(vm, ui, &session)?;
|
||||
Ok(UiBridge { ui, session, tasks })
|
||||
let bridge = UiBridge { ui, session, tasks };
|
||||
developer_pages::install_start_page(vm, &bridge)?;
|
||||
Ok(bridge)
|
||||
}
|
||||
|
||||
fn attach_named_type(
|
||||
|
||||
@@ -4,6 +4,7 @@ use super::*;
|
||||
#[derive(Debug)]
|
||||
struct OpenTool {
|
||||
session: Shared,
|
||||
ui: ObjectRef,
|
||||
}
|
||||
impl NativeCallable for OpenTool {
|
||||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||||
@@ -16,6 +17,13 @@ impl NativeCallable for OpenTool {
|
||||
if !["Console", "Sources", "Network", "Settings", "New tab"].contains(&name.as_str()) {
|
||||
return Err(invalid("unknown developer tool"));
|
||||
}
|
||||
if name == "New tab" {
|
||||
let factory = object(Some(&cx.store().get_attr(self.ui, "newTab")?))?;
|
||||
return Ok(NativeResult::Invoke {
|
||||
function: factory,
|
||||
arguments: vec![],
|
||||
});
|
||||
}
|
||||
let nodes = self
|
||||
.session
|
||||
.lock()
|
||||
@@ -23,9 +31,6 @@ impl NativeCallable for OpenTool {
|
||||
.nodes
|
||||
.clone();
|
||||
for weak in nodes {
|
||||
if name == "New tab" {
|
||||
break;
|
||||
}
|
||||
if let Some(page) = cx.upgrade(weak)?
|
||||
&& cx.store().get_attr(page, "_tesseraTool").ok() == Some(Value::Text(name.clone()))
|
||||
{
|
||||
@@ -63,10 +68,54 @@ pub(super) fn install(vm: &mut Vm, ui: ObjectRef, session: &Shared) -> Result<()
|
||||
"developerTools",
|
||||
OpenTool {
|
||||
session: session.clone(),
|
||||
ui,
|
||||
},
|
||||
vec![],
|
||||
vec![ui],
|
||||
))?;
|
||||
vm.set_attr(ui, "developerTools", Value::Object(function))?;
|
||||
vm.release(function)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compile the bundled Endor page once per VM, in a private module namespace.
|
||||
/// Its functions/classes and callbacks remain ordinary managed VM objects.
|
||||
pub(super) fn install_start_page(vm: &mut Vm, bridge: &UiBridge) -> Result<()> {
|
||||
let source = frontend::EndorReader::from_text(
|
||||
"tessera:start-page.endor",
|
||||
include_str!("start_page.endor"),
|
||||
);
|
||||
let ast = frontend::parse_source_ast(&source, frontend::ParseMode::Module)?;
|
||||
let compiled = compiler::compile_ast(
|
||||
&ast,
|
||||
&source,
|
||||
&compiler::CompileOptions {
|
||||
globals: std::iter::once("ui")
|
||||
.chain(TYPE_NAMES.iter().copied())
|
||||
.map(str::to_owned)
|
||||
.collect(),
|
||||
},
|
||||
)
|
||||
.map_err(|error| source.annotate(error))?;
|
||||
let globals = vm.new_dict()?;
|
||||
let result = (|| -> Result<()> {
|
||||
bridge.install_globals(vm, globals)?;
|
||||
let code = compiled.load(vm)?;
|
||||
let value = vm.execute_with_limits(
|
||||
code,
|
||||
globals,
|
||||
endor_core::ExecutionLimits {
|
||||
max_frames: 128,
|
||||
max_instructions: 100_000,
|
||||
},
|
||||
);
|
||||
vm.release(code)?;
|
||||
vm.release_value(value?)?;
|
||||
let factory = vm
|
||||
.dict_get(globals, &Value::Text("newTab".into()))?
|
||||
.ok_or_else(|| anyhow::anyhow!("start page module requires newTab"))?;
|
||||
vm.set_attr(bridge.ui, "newTab", factory)?;
|
||||
Ok(())
|
||||
})();
|
||||
vm.release(globals)?;
|
||||
result
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
// Tessera's start page is an ordinary Endor Page, including its layout and actions.
|
||||
// This module has its own globals; creating tabs does not replace user bindings.
|
||||
class StartPage: Page {
|
||||
function measure(self: StartPage, mount: Mount, constraints: Constraints): MeasureResult {
|
||||
return MeasureResult(constraints.maxWidth, constraints.maxHeight)
|
||||
}
|
||||
}
|
||||
class StartRow: Row {
|
||||
function measure(self: StartRow, mount: Mount, constraints: Constraints): MeasureResult {
|
||||
measured = Row.measure(self, mount, constraints)
|
||||
return MeasureResult(constraints.maxWidth, measured.height)
|
||||
}
|
||||
}
|
||||
class StartActions {
|
||||
home: Page
|
||||
function blank(self: StartActions, event: Event): Unit {
|
||||
page = Page()
|
||||
page.title = "未命名页面"
|
||||
back = Button("返回开始页")
|
||||
back.onClick(self.home.activate)
|
||||
page.render(Column(32, 16, TextBlock("未命名页面", 2), TextBlock("一个新的 Page 对象,已加入当前窗口。"), TextBlock("用 Endor 的 page.render(...) 组织内容。"), back))
|
||||
}
|
||||
function counter(self: StartActions, event: Event): Unit {
|
||||
count = ui.state(0)
|
||||
add = Button("增加计数")
|
||||
add.onClick(count.increment)
|
||||
page = Page()
|
||||
page.title = "共享对象"
|
||||
page.render(Column(32, 16, TextBlock("一个对象,两个视图", 2), TextBlock("点击一次,两处展示同时更新。切换标签后,状态仍然保留。"), Row(0, 32, ui.textOf(count, "value", 3), ui.textOf(count, "value", 3)), add))
|
||||
}
|
||||
function compose(self: StartActions, event: Event): Unit {
|
||||
label = TextBlock("对象可以被多处引用")
|
||||
page = Page()
|
||||
page.title = "组合页面"
|
||||
page.render(Column(32, 20, TextBlock("把对象组合成界面", 2), TextBlock("Column 组织纵向内容,Row 组织横向内容。"), Row(0, 16, RectBlock(100, 64, 12, "#DCE8FF"), RectBlock(100, 64, 12, "#E3ECDC"), RectBlock(100, 64, 12, "#F3E4DB")), label, Row(0, 24, label, label)))
|
||||
}
|
||||
}
|
||||
function newTab(): Page {
|
||||
page = StartPage()
|
||||
page.title = "新标签页"
|
||||
actions = StartActions(page)
|
||||
blank = Button("+ 创建页面")
|
||||
counter = Button("↗ 试试共享对象")
|
||||
compose = Button("▦ 探索界面组合")
|
||||
blank.on("click", actions.blank)
|
||||
counter.on("click", actions.counter)
|
||||
compose.on("click", actions.compose)
|
||||
content = Column(0, 14,
|
||||
TextBlock("Endor", 3, "#292B2D"),
|
||||
TextBlock("从一个对象开始。", 2, "#484B50"),
|
||||
TextBlock("用代码组织对象,让同一份内容成为可交互的界面。", 1, "#7D8085"),
|
||||
Spacer(16), blank, counter, compose, Spacer(16),
|
||||
TextBlock("⌘ / Ctrl + T 新建标签页 · ⌘ / Ctrl + W 关闭", 1, "#8B8E93"))
|
||||
row = StartRow(24, 0, content)
|
||||
row.mainAlign = "center"
|
||||
page.mainAlign = "center"
|
||||
page.render(row)
|
||||
return page
|
||||
}
|
||||
Reference in New Issue
Block a user