feat: bootstrap namespace-driven Tessera renderer

This commit is contained in:
2026-10-04 19:32:04 +08:00
commit 2f4c8f0fe2
14 changed files with 4262 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
/target
.DS_Store
*.swp
*.swo
Generated
+2037
View File
File diff suppressed because it is too large Load Diff
+20
View File
@@ -0,0 +1,20 @@
[package]
name = "tessera"
version = "0.1.0"
edition = "2024"
description = "Cross-platform desktop rendering client"
license = "MIT OR Apache-2.0"
[dependencies]
anyhow = "1.0"
font8x8 = "0.3"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
softbuffer = "0.4.8"
tiny_http = "0.12"
winit = "0.30.13"
[profile.release]
lto = "thin"
codegen-units = 1
strip = true
+74
View File
@@ -0,0 +1,74 @@
# Tessera
Tessera is a clean desktop rendering host. It starts with an empty page. Coding agents submit source code to a namespace through a local CRUD API; Tessera parses that code, builds its architecture tree, compiles it to drawing commands, and renders the result.
## Start
```bash
cargo run --release
```
Tessera listens only on `127.0.0.1:4737`.
Start every agent integration by reading the live Markdown guide:
```bash
curl http://127.0.0.1:4737/agent
```
## Namespace code API
The initial active namespace is `app.main`. Tessera has no file concept.
```text
GET /v1/namespaces/app.main/code
PUT /v1/namespaces/app.main/code
DELETE /v1/namespaces/app.main/code
```
Submit code as plain text:
```bash
curl \
--request PUT \
--header 'Content-Type: text/plain; charset=utf-8' \
--data-binary @- \
http://127.0.0.1:4737/v1/namespaces/app.main/code <<'TESSERA'
namespace app.main
use tessera.ui::{Column, Text}
export scene Main {
render {
Column(gap: 12, padding: 24) {
Text("Hello", role: .title)
Text("Rendered by Tessera")
}
}
}
TESSERA
```
Updates are atomic. Invalid code returns diagnostics with HTTP 422 and preserves the last successfully rendered revision.
## Developer tools
The developer sidebar is closed by default.
- macOS: `Command-Option-I` or `F12`
- Windows/Linux: `Control-Shift-I` or `F12`
The upper section displays the compiler-produced architecture tree. Selecting a node shows the corresponding source range below it.
## Current syntax experiment
The first parser intentionally supports only enough syntax to test the architecture:
- `namespace`
- namespace-only `use`
- one `export scene`
- one `render` root
- `Column`, `Row`, `Spacer`, and `Text`
- `gap`, `padding`, and `.title`
There are no files, HTML, CSS, JavaScript, DOM, or resource-network concepts in the runtime.
+306
View File
@@ -0,0 +1,306 @@
use std::{
io::Read,
sync::{Arc, RwLock},
thread,
};
use anyhow::{Context as _, Result};
use tiny_http::{Header, Method, Request, Response, Server, StatusCode};
use winit::event_loop::EventLoopProxy;
use crate::{
app::AppEvent,
compiler,
document::{NamespaceState, RenderDocument},
};
pub const API_ADDRESS: &str = "127.0.0.1:4737";
const MAX_CODE_SIZE: u64 = 1_048_576;
pub fn start(
namespace: Arc<RwLock<NamespaceState>>,
proxy: EventLoopProxy<AppEvent>,
) -> Result<()> {
let server = Server::http(API_ADDRESS)
.map_err(|error| anyhow::anyhow!(error))
.with_context(|| format!("bind Tessera API to {API_ADDRESS}"))?;
thread::Builder::new()
.name("tessera-agent-api".to_owned())
.spawn(move || {
for request in server.incoming_requests() {
handle(request, &namespace, &proxy);
}
})
.context("start Tessera agent API thread")?;
Ok(())
}
fn handle(
mut request: Request,
namespace: &Arc<RwLock<NamespaceState>>,
proxy: &EventLoopProxy<AppEvent>,
) {
let method = request.method().clone();
let path = request
.url()
.split('?')
.next()
.unwrap_or(request.url())
.to_owned();
let response = if method == Method::Get && path == "/agent" {
match namespace.read() {
Ok(namespace) => markdown(StatusCode(200), agent_guide(&namespace)),
Err(_) => error_response(StatusCode(500), "namespace lock is poisoned"),
}
} else if let Some(addressed_namespace) = namespace_from_path(&path).map(str::to_owned) {
code_request(&mut request, method, &addressed_namespace, namespace, proxy)
} else if method == Method::Options {
text(StatusCode(204), "", "text/plain")
} else {
error_response(StatusCode(404), "route not found; start with GET /agent")
};
let _ = request.respond(response);
}
fn code_request(
request: &mut Request,
method: Method,
addressed_namespace: &str,
namespace: &Arc<RwLock<NamespaceState>>,
proxy: &EventLoopProxy<AppEvent>,
) -> Response<std::io::Cursor<Vec<u8>>> {
let Ok(current) = namespace.read() else {
return error_response(StatusCode(500), "namespace lock is poisoned");
};
if addressed_namespace != current.name {
return error_response(
StatusCode(404),
format!(
"namespace `{addressed_namespace}` is not active; current namespace is `{}`",
current.name
),
);
}
if method == Method::Get {
let mut response = text(
StatusCode(200),
&current.document.source,
"text/plain; charset=utf-8",
);
add_header(
&mut response,
"X-Tessera-Revision",
&current.revision.to_string(),
);
return response;
}
drop(current);
match method {
Method::Put => match read_code(request) {
Ok(code) => match compiler::compile(&code, addressed_namespace) {
Ok(document) => {
if let Err(error) = document.validate() {
return compile_error(error.to_string());
}
match namespace.write() {
Ok(mut current) => {
current.revision += 1;
current.document = document;
let revision = current.revision;
drop(current);
let _ = proxy.send_event(AppEvent::DocumentUpdated);
json(
StatusCode(200),
serde_json::json!({
"ok": true,
"namespace": addressed_namespace,
"revision": revision,
"diagnostics": []
}),
)
}
Err(_) => error_response(StatusCode(500), "namespace lock is poisoned"),
}
}
Err(error) => compile_error(error.to_string()),
},
Err(error) => error_response(StatusCode(400), error.to_string()),
},
Method::Delete => match namespace.write() {
Ok(mut current) => {
current.revision += 1;
current.document = RenderDocument::default();
let revision = current.revision;
drop(current);
let _ = proxy.send_event(AppEvent::DocumentUpdated);
json(
StatusCode(200),
serde_json::json!({
"ok": true,
"namespace": addressed_namespace,
"revision": revision
}),
)
}
Err(_) => error_response(StatusCode(500), "namespace lock is poisoned"),
},
Method::Options => text(StatusCode(204), "", "text/plain"),
_ => error_response(StatusCode(405), "code supports GET, PUT, and DELETE"),
}
}
fn namespace_from_path(path: &str) -> Option<&str> {
path.strip_prefix("/v1/namespaces/")?
.strip_suffix("/code")
.filter(|namespace| !namespace.is_empty() && !namespace.contains('/'))
}
fn read_code(request: &mut Request) -> Result<String> {
if request
.body_length()
.is_some_and(|length| length as u64 > MAX_CODE_SIZE)
{
anyhow::bail!("code exceeds the 1 MiB limit");
}
let mut code = String::new();
request
.as_reader()
.take(MAX_CODE_SIZE + 1)
.read_to_string(&mut code)
.context("read code")?;
if code.len() as u64 > MAX_CODE_SIZE {
anyhow::bail!("code exceeds the 1 MiB limit");
}
Ok(code)
}
fn agent_guide(namespace: &NamespaceState) -> String {
format!(
r#"# Tessera Agent API
Current namespace: `{name}`
Current revision: `{revision}`
Tessera stores code by namespace. It has no files. Submit code only; Tessera parses,
checks, compiles, and renders it.
## Code CRUD
- Read: `GET http://{address}/v1/namespaces/{name}/code`
- Create or update: `PUT http://{address}/v1/namespaces/{name}/code`
- Delete: `DELETE http://{address}/v1/namespaces/{name}/code`
Send PUT bodies as `text/plain; charset=utf-8`.
## Minimal syntax
```tessera
namespace {name}
use tessera.ui::{{Column, Row, Spacer, Text}}
export scene Main {{
render {{
Column(gap: 12, padding: 24) {{
Text("Hello", role: .title)
Text("Rendered by Tessera")
}}
}}
}}
```
Supported by this syntax experiment: one exported scene, one render root,
`Column`, `Row`, `Spacer`, `Text`, `gap`, `padding`, and `.title`.
Compilation errors reject the update and preserve the last rendered revision.
"#,
name = namespace.name,
revision = namespace.revision,
address = API_ADDRESS
)
}
fn compile_error(message: String) -> Response<std::io::Cursor<Vec<u8>>> {
json(
StatusCode(422),
serde_json::json!({
"ok": false,
"diagnostics": [{
"severity": "error",
"message": message
}]
}),
)
}
fn error_response(
status: StatusCode,
message: impl Into<String>,
) -> Response<std::io::Cursor<Vec<u8>>> {
json(status, serde_json::json!({ "error": message.into() }))
}
fn json(status: StatusCode, value: serde_json::Value) -> Response<std::io::Cursor<Vec<u8>>> {
text(
status,
&value.to_string(),
"application/json; charset=utf-8",
)
}
fn markdown(status: StatusCode, value: String) -> Response<std::io::Cursor<Vec<u8>>> {
text(status, &value, "text/markdown; charset=utf-8")
}
fn text(status: StatusCode, value: &str, content_type: &str) -> Response<std::io::Cursor<Vec<u8>>> {
let mut response = Response::from_string(value).with_status_code(status);
add_header(&mut response, "Content-Type", content_type);
add_header(
&mut response,
"Access-Control-Allow-Origin",
"http://127.0.0.1",
);
add_header(
&mut response,
"Access-Control-Allow-Methods",
"GET, PUT, DELETE, OPTIONS",
);
add_header(
&mut response,
"Access-Control-Allow-Headers",
"Content-Type",
);
response
}
fn add_header(response: &mut Response<std::io::Cursor<Vec<u8>>>, name: &str, value: &str) {
if let Ok(header) = Header::from_bytes(name, value) {
response.add_header(header);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extracts_namespace_only_from_code_routes() {
assert_eq!(
namespace_from_path("/v1/namespaces/app.main/code"),
Some("app.main")
);
assert_eq!(namespace_from_path("/v1/namespaces/app.main"), None);
assert_eq!(namespace_from_path("/v1/namespaces/a/b/code"), None);
}
#[test]
fn guide_contains_only_code_crud_routes() {
let guide = agent_guide(&NamespaceState::default());
assert!(guide.contains("Code CRUD"));
assert!(guide.contains("PUT http://127.0.0.1:4737"));
assert!(!guide.contains("display_list"));
}
}
+193
View File
@@ -0,0 +1,193 @@
use std::{
num::NonZeroU32,
sync::{Arc, RwLock},
};
use softbuffer::{Context, Surface};
use winit::{
application::ApplicationHandler,
dpi::{LogicalSize, PhysicalPosition},
event::{ElementState, MouseButton, WindowEvent},
event_loop::ActiveEventLoop,
keyboard::{Key, ModifiersState, NamedKey},
window::{Window, WindowAttributes, WindowId},
};
use crate::{
developer_tools::DeveloperToolsState,
document::NamespaceState,
geometry::Point,
scene::{self, SceneLayout},
software_renderer::SoftwareRenderer,
};
#[derive(Clone, Debug)]
pub enum AppEvent {
DocumentUpdated,
}
pub struct TesseraApp {
window: Option<Arc<Window>>,
context: Option<Context<Arc<Window>>>,
surface: Option<Surface<Arc<Window>, Arc<Window>>>,
namespace: Arc<RwLock<NamespaceState>>,
developer_tools: DeveloperToolsState,
layout: SceneLayout,
pointer: Point,
modifiers: ModifiersState,
}
impl TesseraApp {
pub fn new(namespace: Arc<RwLock<NamespaceState>>) -> Self {
Self {
window: None,
context: None,
surface: None,
namespace,
developer_tools: DeveloperToolsState::default(),
layout: SceneLayout::default(),
pointer: Point::default(),
modifiers: ModifiersState::default(),
}
}
fn request_redraw(&self) {
if let Some(window) = &self.window {
window.request_redraw();
}
}
fn toggle_developer_tools(&mut self) {
if let Ok(namespace) = self.namespace.read() {
self.developer_tools.toggle(&namespace.document);
}
self.request_redraw();
}
fn document_updated(&mut self) {
if let Ok(namespace) = self.namespace.read() {
self.developer_tools.document_updated(&namespace.document);
}
self.request_redraw();
}
fn handle_keyboard(&mut self, event: &winit::event::KeyEvent) {
if event.state != ElementState::Pressed || event.repeat {
return;
}
let is_f12 = matches!(event.logical_key, Key::Named(NamedKey::F12));
let is_i =
matches!(&event.logical_key, Key::Character(value) if value.eq_ignore_ascii_case("i"));
let mac_shortcut = self.modifiers.super_key() && self.modifiers.alt_key() && is_i;
let other_shortcut = self.modifiers.control_key() && self.modifiers.shift_key() && is_i;
if is_f12 || mac_shortcut || other_shortcut {
self.toggle_developer_tools();
}
}
fn handle_click(&mut self) {
let Some(node_id) = self.layout.developer_tools.node_at(self.pointer) else {
return;
};
self.developer_tools.selected_node_id = Some(node_id.to_owned());
self.request_redraw();
}
fn redraw(&mut self) {
let (Some(window), Some(surface)) = (self.window.as_ref(), self.surface.as_mut()) else {
return;
};
let size = window.inner_size();
let (Some(width), Some(height)) =
(NonZeroU32::new(size.width), NonZeroU32::new(size.height))
else {
return;
};
let Ok(namespace) = self.namespace.read() else {
return;
};
let scene = scene::build_scene(
size.width,
size.height,
window.scale_factor() as f32,
&namespace.document,
&self.developer_tools,
);
drop(namespace);
self.layout = scene.layout;
surface
.resize(width, height)
.expect("resize software surface");
let mut buffer = surface.buffer_mut().expect("acquire software buffer");
SoftwareRenderer::render(&scene.display_list, &mut buffer, size.width, size.height);
buffer.present().expect("present software-rendered frame");
}
}
impl ApplicationHandler<AppEvent> for TesseraApp {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
if self.window.is_some() {
return;
}
let attributes = WindowAttributes::default()
.with_title("Tessera")
.with_inner_size(LogicalSize::new(1_280.0, 800.0))
.with_min_inner_size(LogicalSize::new(720.0, 480.0));
let window = Arc::new(
event_loop
.create_window(attributes)
.expect("create Tessera window"),
);
let context = Context::new(window.clone()).expect("create software-rendering context");
let surface =
Surface::new(&context, window.clone()).expect("create software-rendering surface");
self.window = Some(window.clone());
self.context = Some(context);
self.surface = Some(surface);
window.request_redraw();
}
fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: AppEvent) {
match event {
AppEvent::DocumentUpdated => self.document_updated(),
}
}
fn window_event(
&mut self,
event_loop: &ActiveEventLoop,
window_id: WindowId,
event: WindowEvent,
) {
let Some(window) = self.window.as_ref() else {
return;
};
if window.id() != window_id {
return;
}
match event {
WindowEvent::CloseRequested => event_loop.exit(),
WindowEvent::Resized(_) | WindowEvent::ScaleFactorChanged { .. } => {
window.request_redraw()
}
WindowEvent::CursorMoved {
position: PhysicalPosition { x, y },
..
} => self.pointer = Point::new(x as f32, y as f32),
WindowEvent::MouseInput {
state: ElementState::Released,
button: MouseButton::Left,
..
} => self.handle_click(),
WindowEvent::ModifiersChanged(modifiers) => self.modifiers = modifiers.state(),
WindowEvent::KeyboardInput { event, .. } => self.handle_keyboard(&event),
WindowEvent::RedrawRequested => self.redraw(),
_ => {}
}
}
}
+625
View File
@@ -0,0 +1,625 @@
use anyhow::{Result, bail};
use crate::document::{CodeNode, Frame, RenderCommand, RenderDocument};
pub fn compile(source: &str, expected_namespace: &str) -> Result<RenderDocument> {
let tokens = Lexer::new(source).tokenize()?;
let syntax = Parser::new(tokens).parse()?;
if syntax.namespace != expected_namespace {
bail!(
"namespace mismatch: API addresses `{expected_namespace}`, code declares `{}`",
syntax.namespace
);
}
let mut commands = Vec::new();
layout_node(&syntax.root, 0.0, 0.0, &mut commands);
Ok(RenderDocument {
source: source.to_owned(),
root: Some(architecture(source, &syntax)),
frame: Frame {
background: "#F7F7F5".to_owned(),
commands,
},
})
}
#[derive(Clone, Debug)]
struct SyntaxDocument {
namespace: String,
scene_name: String,
scene_span: Span,
render_span: Span,
root: SyntaxNode,
}
#[derive(Clone, Debug)]
struct SyntaxNode {
name: String,
arguments: Vec<Argument>,
children: Vec<SyntaxNode>,
span: Span,
}
#[derive(Clone, Debug)]
enum Argument {
Positional(Value),
Named(String, Value),
}
#[derive(Clone, Debug)]
enum Value {
Text(String),
Number(f32),
Symbol(String),
}
#[derive(Clone, Copy, Debug)]
struct Span {
start: usize,
end: usize,
}
#[derive(Clone, Debug, PartialEq)]
enum TokenKind {
Identifier(String),
String(String),
Number(f32),
LeftBrace,
RightBrace,
LeftParen,
RightParen,
Colon,
Comma,
Dot,
End,
}
#[derive(Clone, Debug)]
struct Token {
kind: TokenKind,
span: Span,
}
struct Lexer<'a> {
source: &'a str,
offset: usize,
}
impl<'a> Lexer<'a> {
fn new(source: &'a str) -> Self {
Self { source, offset: 0 }
}
fn tokenize(mut self) -> Result<Vec<Token>> {
let mut tokens = Vec::new();
while self.offset < self.source.len() {
self.skip_whitespace_and_comments();
if self.offset >= self.source.len() {
break;
}
let start = self.offset;
let character = self.current_char().expect("offset is in bounds");
let kind = match character {
'{' => self.single(TokenKind::LeftBrace),
'}' => self.single(TokenKind::RightBrace),
'(' => self.single(TokenKind::LeftParen),
')' => self.single(TokenKind::RightParen),
':' => self.single(TokenKind::Colon),
',' => self.single(TokenKind::Comma),
'.' => self.single(TokenKind::Dot),
'"' => self.string()?,
value if value.is_ascii_digit() || value == '-' => self.number()?,
value if is_identifier_start(value) => self.identifier(),
_ => bail!("unexpected character `{character}` at byte {start}"),
};
tokens.push(Token {
kind,
span: Span {
start,
end: self.offset,
},
});
}
tokens.push(Token {
kind: TokenKind::End,
span: Span {
start: self.source.len(),
end: self.source.len(),
},
});
Ok(tokens)
}
fn current_char(&self) -> Option<char> {
self.source[self.offset..].chars().next()
}
fn advance(&mut self) -> Option<char> {
let character = self.current_char()?;
self.offset += character.len_utf8();
Some(character)
}
fn single(&mut self, kind: TokenKind) -> TokenKind {
self.advance();
kind
}
fn skip_whitespace_and_comments(&mut self) {
loop {
while self.current_char().is_some_and(char::is_whitespace) {
self.advance();
}
if self.source[self.offset..].starts_with("//") {
while self.current_char().is_some_and(|value| value != '\n') {
self.advance();
}
} else {
break;
}
}
}
fn identifier(&mut self) -> TokenKind {
let start = self.offset;
while self.current_char().is_some_and(is_identifier_continue) {
self.advance();
}
TokenKind::Identifier(self.source[start..self.offset].to_owned())
}
fn number(&mut self) -> Result<TokenKind> {
let start = self.offset;
if self.current_char() == Some('-') {
self.advance();
}
while self
.current_char()
.is_some_and(|value| value.is_ascii_digit())
{
self.advance();
}
if self.current_char() == Some('.') {
self.advance();
while self
.current_char()
.is_some_and(|value| value.is_ascii_digit())
{
self.advance();
}
}
Ok(TokenKind::Number(self.source[start..self.offset].parse()?))
}
fn string(&mut self) -> Result<TokenKind> {
self.advance();
let mut value = String::new();
while let Some(character) = self.advance() {
match character {
'"' => return Ok(TokenKind::String(value)),
'\\' => match self.advance() {
Some('n') => value.push('\n'),
Some('t') => value.push('\t'),
Some('"') => value.push('"'),
Some('\\') => value.push('\\'),
Some(other) => bail!("unsupported string escape `\\{other}`"),
None => bail!("unterminated string escape"),
},
other => value.push(other),
}
}
bail!("unterminated string literal")
}
}
fn is_identifier_start(value: char) -> bool {
value == '_' || value.is_alphabetic()
}
fn is_identifier_continue(value: char) -> bool {
value == '_' || value.is_alphanumeric()
}
struct Parser {
tokens: Vec<Token>,
current: usize,
}
impl Parser {
fn new(tokens: Vec<Token>) -> Self {
Self { tokens, current: 0 }
}
fn parse(&mut self) -> Result<SyntaxDocument> {
self.expect_word("namespace")?;
let namespace = self.parse_path()?;
while self.check_word("use") {
self.parse_use()?;
}
self.expect_word("export")?;
let scene_start = self.peek().span.start;
self.expect_word("scene")?;
let scene_name = self.identifier()?;
self.expect(TokenKind::LeftBrace)?;
let render_start = self.peek().span.start;
self.expect_word("render")?;
self.expect(TokenKind::LeftBrace)?;
let root = self.parse_node()?;
let render_end = self.expect(TokenKind::RightBrace)?.span.end;
let scene_end = self.expect(TokenKind::RightBrace)?.span.end;
self.expect(TokenKind::End)?;
Ok(SyntaxDocument {
namespace,
scene_name,
scene_span: Span {
start: scene_start,
end: scene_end,
},
render_span: Span {
start: render_start,
end: render_end,
},
root,
})
}
fn parse_path(&mut self) -> Result<String> {
let mut path = self.identifier()?;
while self.matches(&TokenKind::Dot) {
path.push('.');
path.push_str(&self.identifier()?);
}
Ok(path)
}
fn parse_use(&mut self) -> Result<()> {
self.expect_word("use")?;
self.identifier()?;
while self.matches(&TokenKind::Dot) {
self.identifier()?;
}
if self.matches(&TokenKind::Colon) {
self.expect(TokenKind::Colon)?;
self.expect(TokenKind::LeftBrace)?;
if !self.check(&TokenKind::RightBrace) {
loop {
self.identifier()?;
if !self.matches(&TokenKind::Comma) {
break;
}
}
}
self.expect(TokenKind::RightBrace)?;
}
Ok(())
}
fn parse_node(&mut self) -> Result<SyntaxNode> {
let start = self.peek().span.start;
let name = self.identifier()?;
let mut arguments = Vec::new();
if self.matches(&TokenKind::LeftParen) {
if !self.check(&TokenKind::RightParen) {
loop {
if let TokenKind::Identifier(name) = &self.peek().kind
&& self.peek_next().kind == TokenKind::Colon
{
let name = name.clone();
self.advance();
self.advance();
arguments.push(Argument::Named(name, self.value()?));
} else {
arguments.push(Argument::Positional(self.value()?));
}
if !self.matches(&TokenKind::Comma) {
break;
}
}
}
self.expect(TokenKind::RightParen)?;
}
let mut children = Vec::new();
if self.matches(&TokenKind::LeftBrace) {
while !self.check(&TokenKind::RightBrace) {
children.push(self.parse_node()?);
}
self.expect(TokenKind::RightBrace)?;
}
let end = self.previous().span.end;
Ok(SyntaxNode {
name,
arguments,
children,
span: Span { start, end },
})
}
fn value(&mut self) -> Result<Value> {
let token = self.advance().clone();
match token.kind {
TokenKind::String(value) => Ok(Value::Text(value)),
TokenKind::Number(value) => Ok(Value::Number(value)),
TokenKind::Identifier(value) => Ok(Value::Symbol(value)),
TokenKind::Dot => Ok(Value::Symbol(format!(".{}", self.identifier()?))),
_ => bail!("expected a value at byte {}", token.span.start),
}
}
fn identifier(&mut self) -> Result<String> {
let token = self.advance().clone();
match token.kind {
TokenKind::Identifier(value) => Ok(value),
_ => bail!("expected an identifier at byte {}", token.span.start),
}
}
fn expect_word(&mut self, expected: &str) -> Result<()> {
let actual = self.identifier()?;
if actual != expected {
bail!("expected `{expected}`, found `{actual}`");
}
Ok(())
}
fn expect(&mut self, expected: TokenKind) -> Result<Token> {
if self.check(&expected) {
return Ok(self.advance().clone());
}
bail!(
"expected {}, found {} at byte {}",
token_name(&expected),
token_name(&self.peek().kind),
self.peek().span.start
)
}
fn matches(&mut self, expected: &TokenKind) -> bool {
if self.check(expected) {
self.advance();
true
} else {
false
}
}
fn check(&self, expected: &TokenKind) -> bool {
std::mem::discriminant(&self.peek().kind) == std::mem::discriminant(expected)
}
fn check_word(&self, expected: &str) -> bool {
matches!(&self.peek().kind, TokenKind::Identifier(value) if value == expected)
}
fn peek(&self) -> &Token {
&self.tokens[self.current]
}
fn peek_next(&self) -> &Token {
&self.tokens[(self.current + 1).min(self.tokens.len() - 1)]
}
fn previous(&self) -> &Token {
&self.tokens[self.current.saturating_sub(1)]
}
fn advance(&mut self) -> &Token {
let index = self.current;
if self.current < self.tokens.len() - 1 {
self.current += 1;
}
&self.tokens[index]
}
}
fn token_name(token: &TokenKind) -> &'static str {
match token {
TokenKind::Identifier(_) => "identifier",
TokenKind::String(_) => "string",
TokenKind::Number(_) => "number",
TokenKind::LeftBrace => "`{`",
TokenKind::RightBrace => "`}`",
TokenKind::LeftParen => "`(`",
TokenKind::RightParen => "`)`",
TokenKind::Colon => "`:`",
TokenKind::Comma => "`,`",
TokenKind::Dot => "`.`",
TokenKind::End => "end of code",
}
}
fn architecture(source: &str, syntax: &SyntaxDocument) -> CodeNode {
CodeNode {
id: format!("{}::{}", syntax.namespace, syntax.scene_name),
name: syntax.scene_name.clone(),
kind: "scene".to_owned(),
start_line: line_number(source, syntax.scene_span.start),
end_line: line_number(source, syntax.scene_span.end),
children: vec![CodeNode {
id: format!("{}::{}::render", syntax.namespace, syntax.scene_name),
name: "render".to_owned(),
kind: "render".to_owned(),
start_line: line_number(source, syntax.render_span.start),
end_line: line_number(source, syntax.render_span.end),
children: vec![node_architecture(
source,
&syntax.namespace,
&syntax.scene_name,
&syntax.root,
"0",
)],
}],
}
}
fn node_architecture(
source: &str,
namespace: &str,
scene: &str,
node: &SyntaxNode,
path: &str,
) -> CodeNode {
CodeNode {
id: format!("{namespace}::{scene}::render::{path}"),
name: node.name.clone(),
kind: node.name.to_lowercase(),
start_line: line_number(source, node.span.start),
end_line: line_number(source, node.span.end),
children: node
.children
.iter()
.enumerate()
.map(|(index, child)| {
node_architecture(source, namespace, scene, child, &format!("{path}.{index}"))
})
.collect(),
}
}
fn line_number(source: &str, offset: usize) -> usize {
source[..offset.min(source.len())]
.bytes()
.filter(|byte| *byte == b'\n')
.count()
+ 1
}
#[derive(Clone, Copy, Default)]
struct Size {
width: f32,
height: f32,
}
fn layout_node(node: &SyntaxNode, x: f32, y: f32, commands: &mut Vec<RenderCommand>) -> Size {
match node.name.as_str() {
"Text" => layout_text(node, x, y, commands),
"Row" => layout_row(node, x, y, commands),
"Spacer" => Size {
width: number_argument(node, "size").unwrap_or(0.0),
height: number_argument(node, "size").unwrap_or(0.0),
},
_ => layout_column(node, x, y, commands),
}
}
fn layout_text(node: &SyntaxNode, x: f32, y: f32, commands: &mut Vec<RenderCommand>) -> Size {
let value = node.arguments.iter().find_map(|argument| match argument {
Argument::Positional(Value::Text(value)) => Some(value.clone()),
_ => None,
});
let value = value.unwrap_or_default();
let size = match named_argument(node, "role") {
Some(Value::Symbol(value)) if value == ".title" => 2,
_ => 1,
};
commands.push(RenderCommand::Text {
x,
y,
value: value.clone(),
size,
color: "#242422".to_owned(),
});
Size {
width: value.chars().count() as f32 * 8.0 * size as f32,
height: 8.0 * size as f32,
}
}
fn layout_column(node: &SyntaxNode, x: f32, y: f32, commands: &mut Vec<RenderCommand>) -> Size {
let padding = number_argument(node, "padding").unwrap_or(0.0);
let gap = number_argument(node, "gap").unwrap_or(0.0);
let mut cursor_y = y + padding;
let mut maximum_width: f32 = 0.0;
for (index, child) in node.children.iter().enumerate() {
if index > 0 {
cursor_y += gap;
}
let size = layout_node(child, x + padding, cursor_y, commands);
cursor_y += size.height;
maximum_width = maximum_width.max(size.width);
}
Size {
width: maximum_width + padding * 2.0,
height: cursor_y - y + padding,
}
}
fn layout_row(node: &SyntaxNode, x: f32, y: f32, commands: &mut Vec<RenderCommand>) -> Size {
let padding = number_argument(node, "padding").unwrap_or(0.0);
let gap = number_argument(node, "gap").unwrap_or(0.0);
let mut cursor_x = x + padding;
let mut maximum_height: f32 = 0.0;
for (index, child) in node.children.iter().enumerate() {
if index > 0 {
cursor_x += gap;
}
let size = layout_node(child, cursor_x, y + padding, commands);
cursor_x += size.width;
maximum_height = maximum_height.max(size.height);
}
Size {
width: cursor_x - x + padding,
height: maximum_height + padding * 2.0,
}
}
fn number_argument(node: &SyntaxNode, name: &str) -> Option<f32> {
match named_argument(node, name) {
Some(Value::Number(value)) => Some(*value),
_ => None,
}
}
fn named_argument<'a>(node: &'a SyntaxNode, name: &str) -> Option<&'a Value> {
node.arguments.iter().find_map(|argument| match argument {
Argument::Named(argument_name, value) if argument_name == name => Some(value),
_ => None,
})
}
#[cfg(test)]
mod tests {
use super::*;
const EXAMPLE: &str = r#"namespace app.main
use tessera.ui::{Column, Text}
export scene Main {
render {
Column(gap: 12, padding: 24) {
Text("Hello", role: .title)
Text("Rendered by Tessera")
}
}
}"#;
#[test]
fn compiles_baseline_render_syntax() {
let document = compile(EXAMPLE, "app.main").unwrap();
assert_eq!(document.frame.commands.len(), 2);
assert_eq!(document.root.as_ref().unwrap().name, "Main");
assert_eq!(
document.root.as_ref().unwrap().children[0].children[0]
.children
.len(),
2
);
}
#[test]
fn rejects_namespace_mismatch() {
let error = compile(EXAMPLE, "app.other").unwrap_err();
assert!(error.to_string().contains("namespace mismatch"));
}
#[test]
fn reports_incomplete_syntax() {
let error = compile("namespace app.main", "app.main").unwrap_err();
assert!(error.to_string().contains("expected"));
}
}
+298
View File
@@ -0,0 +1,298 @@
use crate::{
display_list::{Color, DisplayList, DrawCommand},
document::{CodeNode, RenderDocument},
geometry::{Point, Rect},
};
const PANEL: Color = Color::rgb(247, 247, 245);
const PANEL_ALT: Color = Color::rgb(238, 238, 235);
const BORDER: Color = Color::rgb(214, 214, 209);
const TEXT: Color = Color::rgb(36, 36, 34);
const MUTED: Color = Color::rgb(112, 112, 107);
const SELECTED: Color = Color::rgb(224, 229, 247);
#[derive(Debug, Default)]
pub struct DeveloperToolsState {
pub open: bool,
pub selected_node_id: Option<String>,
}
impl DeveloperToolsState {
pub fn toggle(&mut self, document: &RenderDocument) {
self.open = !self.open;
self.ensure_selection(document);
}
pub fn document_updated(&mut self, document: &RenderDocument) {
self.ensure_selection(document);
}
fn ensure_selection(&mut self, document: &RenderDocument) {
let selection_exists = self
.selected_node_id
.as_deref()
.is_some_and(|id| document.node(id).is_some());
if !selection_exists {
self.selected_node_id = document.root.as_ref().map(|node| node.id.clone());
}
}
}
#[derive(Clone, Debug, Default)]
pub struct DeveloperToolsLayout {
rows: Vec<TreeRowHit>,
}
impl DeveloperToolsLayout {
pub fn node_at(&self, point: Point) -> Option<&str> {
self.rows
.iter()
.find(|row| row.rect.contains(point))
.map(|row| row.node_id.as_str())
}
}
#[derive(Clone, Debug)]
struct TreeRowHit {
rect: Rect,
node_id: String,
}
pub fn build(
width: u32,
height: u32,
scale: f32,
document: &RenderDocument,
state: &DeveloperToolsState,
) -> (DisplayList, DeveloperToolsLayout) {
if !state.open {
return (DisplayList::default(), DeveloperToolsLayout::default());
}
let mut list = DisplayList::default();
let width = width as f32;
let height = height as f32;
let panel_width = (430.0 * scale).min(width * 0.52);
let panel_x = width - panel_width;
let header_height = 44.0 * scale;
let tree_height = ((height - header_height) * 0.42).max(150.0 * scale);
let code_y = header_height + tree_height;
fill(
&mut list,
Rect::new(panel_x, 0.0, panel_width, height),
0.0,
PANEL,
);
fill(
&mut list,
Rect::new(panel_x, 0.0, 1.0 * scale, height),
0.0,
BORDER,
);
fill(
&mut list,
Rect::new(panel_x, header_height - scale, panel_width, scale),
0.0,
BORDER,
);
fill(
&mut list,
Rect::new(panel_x, code_y, panel_width, height - code_y),
0.0,
PANEL_ALT,
);
fill(
&mut list,
Rect::new(panel_x, code_y, panel_width, scale),
0.0,
BORDER,
);
text(
&mut list,
panel_x + 14.0 * scale,
15.0 * scale,
"DEVELOPER",
scale,
TEXT,
);
text(
&mut list,
width - 116.0 * scale,
15.0 * scale,
"F12 TOGGLE",
scale,
MUTED,
);
text(
&mut list,
panel_x + 14.0 * scale,
header_height + 13.0 * scale,
"ARCHITECTURE",
scale,
MUTED,
);
let mut layout = DeveloperToolsLayout::default();
if let Some(root) = &document.root {
let mut flattened = Vec::new();
flatten(root, 0, &mut flattened);
let row_height = 24.0 * scale;
let first_y = header_height + 34.0 * scale;
let maximum_rows = ((tree_height - 40.0 * scale) / row_height).max(0.0) as usize;
for (index, (depth, node)) in flattened.into_iter().take(maximum_rows).enumerate() {
let y = first_y + index as f32 * row_height;
let row = Rect::new(
panel_x + 7.0 * scale,
y,
panel_width - 14.0 * scale,
row_height,
);
if state.selected_node_id.as_deref() == Some(node.id.as_str()) {
fill(&mut list, row, 5.0 * scale, SELECTED);
}
text(
&mut list,
row.x + (9.0 + depth as f32 * 14.0) * scale,
row.y + 8.0 * scale,
&fit(&format!("{} {}", node.kind, node.name), 44),
scale,
TEXT,
);
layout.rows.push(TreeRowHit {
rect: row,
node_id: node.id.clone(),
});
}
} else {
text(
&mut list,
panel_x + 14.0 * scale,
header_height + 48.0 * scale,
"NO RENDER TREE",
scale,
MUTED,
);
}
text(
&mut list,
panel_x + 14.0 * scale,
code_y + 16.0 * scale,
"SOURCE",
scale,
MUTED,
);
draw_source(
&mut list,
Rect::new(panel_x, code_y, panel_width, height - code_y),
scale,
document,
state.selected_node_id.as_deref(),
);
(list, layout)
}
fn draw_source(
list: &mut DisplayList,
area: Rect,
scale: f32,
document: &RenderDocument,
selected_id: Option<&str>,
) {
if document.source.is_empty() {
text(
list,
area.x + 14.0 * scale,
area.y + 50.0 * scale,
"NO SOURCE LOADED",
scale,
MUTED,
);
return;
}
let all_lines: Vec<_> = document.source.lines().collect();
let selected = selected_id.and_then(|id| document.node(id));
let start = selected
.map(|node| node.start_line.saturating_sub(1))
.unwrap_or(0)
.min(all_lines.len());
let declared_end = selected
.map(|node| node.end_line)
.unwrap_or(all_lines.len());
let end = if declared_end == 0 {
all_lines.len()
} else {
declared_end.min(all_lines.len())
};
let line_height = 18.0 * scale;
let first_y = area.y + 42.0 * scale;
let maximum_lines =
((area.y + area.height - first_y - 10.0 * scale) / line_height).max(0.0) as usize;
let max_chars = ((area.width / (8.0 * scale)) - 9.0).max(8.0) as usize;
for (offset, source_line) in all_lines[start..end].iter().take(maximum_lines).enumerate() {
let line_number = start + offset + 1;
let y = first_y + offset as f32 * line_height;
text(
list,
area.x + 12.0 * scale,
y,
&format!("{:>4}", line_number),
scale,
MUTED,
);
text(
list,
area.x + 55.0 * scale,
y,
&fit(&source_line.replace('\t', " "), max_chars),
scale,
TEXT,
);
}
}
fn flatten<'a>(node: &'a CodeNode, depth: usize, output: &mut Vec<(usize, &'a CodeNode)>) {
output.push((depth, node));
for child in &node.children {
flatten(child, depth + 1, output);
}
}
fn fit(value: &str, maximum: usize) -> String {
let characters: Vec<_> = value.chars().collect();
if characters.len() <= maximum {
return value.to_owned();
}
if maximum < 3 {
return characters.into_iter().take(maximum).collect();
}
characters
.into_iter()
.take(maximum - 3)
.chain(['.', '.', '.'])
.collect()
}
fn fill(list: &mut DisplayList, rect: Rect, radius: f32, color: Color) {
list.push(DrawCommand::FillRoundedRect {
rect,
radius,
color,
});
}
fn text(list: &mut DisplayList, x: f32, y: f32, value: &str, scale: f32, color: Color) {
list.push(DrawCommand::Text {
origin: Point::new(x, y),
value: value.to_owned(),
scale: scale.round().max(1.0) as u32,
color,
});
}
+70
View File
@@ -0,0 +1,70 @@
use crate::geometry::{Point, Rect};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Color {
pub red: u8,
pub green: u8,
pub blue: u8,
pub alpha: u8,
}
impl Color {
pub const fn rgb(red: u8, green: u8, blue: u8) -> Self {
Self {
red,
green,
blue,
alpha: 255,
}
}
pub const fn rgba(red: u8, green: u8, blue: u8, alpha: u8) -> Self {
Self {
red,
green,
blue,
alpha,
}
}
}
#[derive(Clone, Debug)]
pub enum DrawCommand {
Clear(Color),
FillRoundedRect {
rect: Rect,
radius: f32,
color: Color,
},
StrokeRoundedRect {
rect: Rect,
radius: f32,
width: f32,
color: Color,
},
Text {
origin: Point,
value: String,
scale: u32,
color: Color,
},
}
#[derive(Clone, Debug, Default)]
pub struct DisplayList {
commands: Vec<DrawCommand>,
}
impl DisplayList {
pub fn push(&mut self, command: DrawCommand) {
self.commands.push(command);
}
pub fn commands(&self) -> &[DrawCommand] {
&self.commands
}
pub fn extend(&mut self, other: DisplayList) {
self.commands.extend(other.commands);
}
}
+326
View File
@@ -0,0 +1,326 @@
use std::collections::HashSet;
use anyhow::{Result, bail};
use serde::{Deserialize, Serialize};
use crate::{
display_list::{Color, DisplayList, DrawCommand},
geometry::{Point, Rect},
};
pub const DEFAULT_NAMESPACE: &str = "app.main";
#[derive(Clone, Debug)]
pub struct NamespaceState {
pub name: String,
pub revision: u64,
pub document: RenderDocument,
}
impl Default for NamespaceState {
fn default() -> Self {
Self {
name: DEFAULT_NAMESPACE.to_owned(),
revision: 0,
document: RenderDocument::default(),
}
}
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct RenderDocument {
#[serde(default)]
pub source: String,
#[serde(default)]
pub root: Option<CodeNode>,
#[serde(default)]
pub frame: Frame,
}
impl RenderDocument {
pub fn validate(&self) -> Result<()> {
if self.source.len() > 1_048_576 {
bail!("source exceeds the 1 MiB limit");
}
if self.frame.commands.len() > 10_000 {
bail!("frame exceeds the 10,000 command limit");
}
parse_color(&self.frame.background)?;
for command in &self.frame.commands {
command.validate()?;
}
let mut ids = HashSet::new();
if let Some(root) = &self.root {
root.validate(0, &mut ids)?;
}
Ok(())
}
pub fn display_list(&self, scale: f32) -> Result<DisplayList> {
let mut list = DisplayList::default();
list.push(DrawCommand::Clear(parse_color(&self.frame.background)?));
for command in &self.frame.commands {
list.push(command.to_draw_command(scale)?);
}
Ok(list)
}
pub fn node(&self, id: &str) -> Option<&CodeNode> {
self.root.as_ref()?.find(id)
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CodeNode {
pub id: String,
pub name: String,
pub kind: String,
#[serde(default)]
pub start_line: usize,
#[serde(default)]
pub end_line: usize,
#[serde(default)]
pub children: Vec<CodeNode>,
}
impl CodeNode {
fn validate<'a>(&'a self, depth: usize, ids: &mut HashSet<&'a str>) -> Result<()> {
if depth > 64 {
bail!("architecture tree exceeds 64 levels");
}
if self.id.is_empty() {
bail!("architecture node id cannot be empty");
}
if !ids.insert(&self.id) {
bail!("duplicate architecture node id: {}", self.id);
}
if self.start_line > 0 && self.end_line > 0 && self.start_line > self.end_line {
bail!("invalid source range for node {}", self.id);
}
for child in &self.children {
child.validate(depth + 1, ids)?;
}
Ok(())
}
fn find(&self, id: &str) -> Option<&CodeNode> {
if self.id == id {
return Some(self);
}
self.children.iter().find_map(|child| child.find(id))
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Frame {
#[serde(default = "default_background")]
pub background: String,
#[serde(default)]
pub commands: Vec<RenderCommand>,
}
impl Default for Frame {
fn default() -> Self {
Self {
background: default_background(),
commands: Vec::new(),
}
}
}
fn default_background() -> String {
"#F7F7F5".to_owned()
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum RenderCommand {
RoundedRect {
x: f32,
y: f32,
width: f32,
height: f32,
#[serde(default)]
radius: f32,
color: String,
},
StrokeRoundedRect {
x: f32,
y: f32,
width: f32,
height: f32,
#[serde(default)]
radius: f32,
#[serde(default = "default_stroke_width")]
stroke_width: f32,
color: String,
},
Text {
x: f32,
y: f32,
value: String,
#[serde(default = "default_text_size")]
size: u32,
color: String,
},
}
impl RenderCommand {
fn validate(&self) -> Result<()> {
match self {
Self::RoundedRect {
x,
y,
width,
height,
radius,
color,
} => {
validate_rect(*x, *y, *width, *height, *radius)?;
parse_color(color)?;
}
Self::StrokeRoundedRect {
x,
y,
width,
height,
radius,
stroke_width,
color,
} => {
validate_rect(*x, *y, *width, *height, *radius)?;
if !stroke_width.is_finite() || *stroke_width <= 0.0 {
bail!("stroke_width must be a positive finite number");
}
parse_color(color)?;
}
Self::Text {
x,
y,
value,
size,
color,
} => {
if !x.is_finite() || !y.is_finite() {
bail!("text coordinates must be finite");
}
if value.len() > 16_384 {
bail!("text command exceeds 16 KiB");
}
if !(1..=8).contains(size) {
bail!("text size must be between 1 and 8");
}
parse_color(color)?;
}
}
Ok(())
}
fn to_draw_command(&self, scale: f32) -> Result<DrawCommand> {
Ok(match self {
Self::RoundedRect {
x,
y,
width,
height,
radius,
color,
} => DrawCommand::FillRoundedRect {
rect: scaled_rect(*x, *y, *width, *height, scale),
radius: radius * scale,
color: parse_color(color)?,
},
Self::StrokeRoundedRect {
x,
y,
width,
height,
radius,
stroke_width,
color,
} => DrawCommand::StrokeRoundedRect {
rect: scaled_rect(*x, *y, *width, *height, scale),
radius: radius * scale,
width: stroke_width * scale,
color: parse_color(color)?,
},
Self::Text {
x,
y,
value,
size,
color,
} => DrawCommand::Text {
origin: Point::new(x * scale, y * scale),
value: value.clone(),
scale: ((*size as f32 * scale).round() as u32).max(1),
color: parse_color(color)?,
},
})
}
}
fn default_stroke_width() -> f32 {
1.0
}
fn default_text_size() -> u32 {
1
}
fn validate_rect(x: f32, y: f32, width: f32, height: f32, radius: f32) -> Result<()> {
if !x.is_finite() || !y.is_finite() || !width.is_finite() || !height.is_finite() {
bail!("rectangle values must be finite");
}
if width < 0.0 || height < 0.0 || !radius.is_finite() || radius < 0.0 {
bail!("rectangle dimensions and radius cannot be negative");
}
Ok(())
}
fn scaled_rect(x: f32, y: f32, width: f32, height: f32, scale: f32) -> Rect {
Rect::new(x * scale, y * scale, width * scale, height * scale)
}
pub fn parse_color(value: &str) -> Result<Color> {
let hex = value
.strip_prefix('#')
.ok_or_else(|| anyhow::anyhow!("color must begin with #"))?;
if hex.len() != 6 && hex.len() != 8 {
bail!("color must use #RRGGBB or #RRGGBBAA");
}
let parse = |range| u8::from_str_radix(&hex[range], 16);
Ok(Color::rgba(
parse(0..2)?,
parse(2..4)?,
parse(4..6)?,
if hex.len() == 8 { parse(6..8)? } else { 255 },
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_document_is_valid_and_blank() {
let document = RenderDocument::default();
document.validate().unwrap();
assert!(document.root.is_none());
assert!(document.frame.commands.is_empty());
}
#[test]
fn parses_rgb_and_rgba_colors() {
assert_eq!(
parse_color("#102030").unwrap(),
Color::rgba(16, 32, 48, 255)
);
assert_eq!(
parse_color("#10203080").unwrap(),
Color::rgba(16, 32, 48, 128)
);
}
}
+50
View File
@@ -0,0 +1,50 @@
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Point {
pub x: f32,
pub y: f32,
}
impl Point {
pub const fn new(x: f32, y: f32) -> Self {
Self { x, y }
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Rect {
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
}
impl Rect {
pub const fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
Self {
x,
y,
width,
height,
}
}
pub fn contains(self, point: Point) -> bool {
point.x >= self.x
&& point.x <= self.x + self.width
&& point.y >= self.y
&& point.y <= self.y + self.height
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rectangle_contains_edges_but_not_outside_points() {
let rect = Rect::new(10.0, 20.0, 30.0, 40.0);
assert!(rect.contains(Point::new(10.0, 20.0)));
assert!(rect.contains(Point::new(40.0, 60.0)));
assert!(!rect.contains(Point::new(9.9, 20.0)));
}
}
+25
View File
@@ -0,0 +1,25 @@
mod api;
mod app;
mod compiler;
mod developer_tools;
mod display_list;
mod document;
mod geometry;
mod scene;
mod software_renderer;
use std::sync::{Arc, RwLock};
use anyhow::Result;
use app::{AppEvent, TesseraApp};
use document::NamespaceState;
use winit::event_loop::{ControlFlow, EventLoop};
fn main() -> Result<()> {
let event_loop = EventLoop::<AppEvent>::with_user_event().build()?;
event_loop.set_control_flow(ControlFlow::Wait);
let namespace = Arc::new(RwLock::new(NamespaceState::default()));
api::start(namespace.clone(), event_loop.create_proxy())?;
event_loop.run_app(&mut TesseraApp::new(namespace))?;
Ok(())
}
+39
View File
@@ -0,0 +1,39 @@
use crate::{
developer_tools::{self, DeveloperToolsLayout, DeveloperToolsState},
display_list::DisplayList,
document::RenderDocument,
};
#[derive(Clone, Debug, Default)]
pub struct SceneLayout {
pub developer_tools: DeveloperToolsLayout,
}
pub fn build_scene(
width: u32,
height: u32,
scale: f32,
document: &RenderDocument,
developer_tools_state: &DeveloperToolsState,
) -> DisplayScene {
let mut display_list = document.display_list(scale).unwrap_or_else(|_| {
RenderDocument::default()
.display_list(scale)
.expect("default document is valid")
});
let (developer_list, developer_layout) =
developer_tools::build(width, height, scale, document, developer_tools_state);
display_list.extend(developer_list);
DisplayScene {
display_list,
layout: SceneLayout {
developer_tools: developer_layout,
},
}
}
pub struct DisplayScene {
pub display_list: DisplayList,
pub layout: SceneLayout,
}
+195
View File
@@ -0,0 +1,195 @@
use font8x8::{BASIC_FONTS, UnicodeFonts};
use crate::{
display_list::{Color, DisplayList, DrawCommand},
geometry::{Point, Rect},
};
pub struct SoftwareRenderer;
impl SoftwareRenderer {
pub fn render(list: &DisplayList, target: &mut [u32], width: u32, height: u32) {
let mut canvas = Canvas {
pixels: target,
width,
height,
};
for command in list.commands() {
match command {
DrawCommand::Clear(color) => canvas.clear(*color),
DrawCommand::FillRoundedRect {
rect,
radius,
color,
} => canvas.fill_rounded_rect(*rect, *radius, *color),
DrawCommand::StrokeRoundedRect {
rect,
radius,
width,
color,
} => canvas.stroke_rounded_rect(*rect, *radius, *width, *color),
DrawCommand::Text {
origin,
value,
scale,
color,
} => canvas.draw_text(*origin, value, *scale, *color),
}
}
}
}
struct Canvas<'a> {
pixels: &'a mut [u32],
width: u32,
height: u32,
}
impl Canvas<'_> {
fn clear(&mut self, color: Color) {
self.pixels.fill(pack(color));
}
fn fill_rounded_rect(&mut self, rect: Rect, radius: f32, color: Color) {
let bounds = self.clamped_bounds(rect);
let radius = radius.max(0.0).min(rect.width / 2.0).min(rect.height / 2.0);
for y in bounds.1..bounds.3 {
for x in bounds.0..bounds.2 {
if rounded_rect_contains(rect, radius, x as f32 + 0.5, y as f32 + 0.5) {
self.blend_pixel(x, y, color);
}
}
}
}
fn stroke_rounded_rect(&mut self, rect: Rect, radius: f32, width: f32, color: Color) {
let bounds = self.clamped_bounds(rect);
let width = width.max(1.0).min(rect.width / 2.0).min(rect.height / 2.0);
let outer_radius = radius.max(0.0).min(rect.width / 2.0).min(rect.height / 2.0);
let inner = Rect::new(
rect.x + width,
rect.y + width,
(rect.width - width * 2.0).max(0.0),
(rect.height - width * 2.0).max(0.0),
);
let inner_radius = (outer_radius - width).max(0.0);
for y in bounds.1..bounds.3 {
for x in bounds.0..bounds.2 {
let px = x as f32 + 0.5;
let py = y as f32 + 0.5;
if rounded_rect_contains(rect, outer_radius, px, py)
&& !rounded_rect_contains(inner, inner_radius, px, py)
{
self.blend_pixel(x, y, color);
}
}
}
}
fn draw_text(&mut self, origin: Point, value: &str, scale: u32, color: Color) {
let scale = scale.max(1) as i32;
let mut cursor_x = origin.x.round() as i32;
let baseline_y = origin.y.round() as i32;
for character in value.chars() {
if let Some(glyph) = BASIC_FONTS.get(character) {
for (row, byte) in glyph.iter().enumerate() {
for column in 0..8 {
if byte & (1 << column) != 0 {
self.fill_pixels(
cursor_x + column * scale,
baseline_y + row as i32 * scale,
scale,
scale,
color,
);
}
}
}
}
cursor_x += 8 * scale;
}
}
fn clamped_bounds(&self, rect: Rect) -> (i32, i32, i32, i32) {
(
rect.x.max(0.0).floor() as i32,
rect.y.max(0.0).floor() as i32,
(rect.x + rect.width).min(self.width as f32).ceil() as i32,
(rect.y + rect.height).min(self.height as f32).ceil() as i32,
)
}
fn fill_pixels(&mut self, x: i32, y: i32, width: i32, height: i32, color: Color) {
for py in y.max(0)..(y + height).min(self.height as i32) {
for px in x.max(0)..(x + width).min(self.width as i32) {
self.blend_pixel(px, py, color);
}
}
}
fn blend_pixel(&mut self, x: i32, y: i32, source: Color) {
if x < 0 || y < 0 || x >= self.width as i32 || y >= self.height as i32 || source.alpha == 0
{
return;
}
let index = y as usize * self.width as usize + x as usize;
if source.alpha == 255 {
self.pixels[index] = pack(source);
return;
}
let destination = self.pixels[index];
let dst_red = (destination >> 16) & 0xff;
let dst_green = (destination >> 8) & 0xff;
let dst_blue = destination & 0xff;
let alpha = source.alpha as u32;
let inverse = 255 - alpha;
let red = (source.red as u32 * alpha + dst_red * inverse) / 255;
let green = (source.green as u32 * alpha + dst_green * inverse) / 255;
let blue = (source.blue as u32 * alpha + dst_blue * inverse) / 255;
self.pixels[index] = (red << 16) | (green << 8) | blue;
}
}
fn rounded_rect_contains(rect: Rect, radius: f32, x: f32, y: f32) -> bool {
if rect.width <= 0.0 || rect.height <= 0.0 {
return false;
}
if radius <= 0.0 {
return x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height;
}
let closest_x = x.clamp(rect.x + radius, rect.x + rect.width - radius);
let closest_y = y.clamp(rect.y + radius, rect.y + rect.height - radius);
let dx = x - closest_x;
let dy = y - closest_y;
dx * dx + dy * dy <= radius * radius
}
fn pack(color: Color) -> u32 {
(color.red as u32) << 16 | (color.green as u32) << 8 | color.blue as u32
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn renderer_changes_pixels_inside_a_rectangle() {
let mut list = DisplayList::default();
list.push(DrawCommand::Clear(Color::rgb(0, 0, 0)));
list.push(DrawCommand::FillRoundedRect {
rect: Rect::new(1.0, 1.0, 2.0, 2.0),
radius: 0.0,
color: Color::rgb(255, 0, 0),
});
let mut pixels = vec![0; 16];
SoftwareRenderer::render(&list, &mut pixels, 4, 4);
assert_eq!(pixels[5], 0x00ff0000);
assert_eq!(pixels[0], 0);
}
}