Files
endorphin-tessera-gui/src/source_paint.rs
T
codex-bot 2b20976658
Verify Tessera GUI / verify (push) Failing after 7m43s
Honor replaced visual methods and correct event contracts
2026-10-22 19:08:28 +08:00

355 lines
12 KiB
Rust

//! Source paint dispatch produces checked immutable commands. One read-only VM
//! call sequences all overrides, sharing a single instruction/frame budget.
use crate::{
document::{Frame, RenderCommand},
layout::{self, Size},
mount::Mount,
object_list,
ui_bridge::{MAX_DIMENSION, UiNode, attach_desktop_type},
};
use anyhow::{Result, bail};
use endor_core::{
CodeLayout, ContextNativeFunction, EndorCode, EndorCodeObject, Instruction, NativeCallable,
NativeContext, NativeResult, ObjectRef, ObjectStore, Opcode, Value, Vm, VmError, WeakObjectRef,
};
use std::{
collections::{BTreeMap, BTreeSet, HashMap},
sync::{Arc, Mutex},
};
fn invalid(message: &'static str) -> VmError {
VmError::InvalidArgument(message)
}
fn object(value: Value) -> Result<ObjectRef, VmError> {
if let Value::Object(object) = value {
Ok(object)
} else {
Err(invalid("paint must return managed objects"))
}
}
fn text(store: &ObjectStore, object: ObjectRef, name: &str) -> Result<String, VmError> {
if let Value::Text(value) = store.get_attr(object, name)? {
Ok(value)
} else {
Err(invalid("paint field must be text"))
}
}
fn number(store: &ObjectStore, object: ObjectRef, name: &str) -> Result<f32, VmError> {
let Value::Number(value) = store.get_attr(object, name)? else {
return Err(invalid("paint field must be a Number"));
};
let value = value
.to_string()
.parse::<f32>()
.map_err(|_| invalid("invalid paint number"))?;
if !value.is_finite()
|| value.abs() > MAX_DIMENSION as f32
|| (!matches!(name, "x" | "y") && value < 0.0)
{
return Err(invalid("paint number exceeds bounds"));
}
Ok(value)
}
fn named_type(store: &ObjectStore, desktop: ObjectRef, name: &str) -> Result<ObjectRef, VmError> {
let registry = object(store.get_attr(desktop, "types")?)?;
object(
store
.dict_get(registry, &Value::Text(name.into()))?
.ok_or(invalid("missing paint type"))?,
)
}
fn decode(
store: &ObjectStore,
command: ObjectRef,
expected: ObjectRef,
) -> Result<RenderCommand, VmError> {
if !store.is_instance(command, expected)? {
return Err(invalid("paint commands must be PaintCommand instances"));
}
let x = number(store, command, "x")?;
let y = number(store, command, "y")?;
let color = text(store, command, "color")?;
let kind = text(store, command, "kind")?;
let result = match kind.as_str() {
"roundedRect" | "strokeRoundedRect" => {
let width = number(store, command, "width")?;
let height = number(store, command, "height")?;
let radius = number(store, command, "radius")?;
if x + width > MAX_DIMENSION as f32 || y + height > MAX_DIMENSION as f32 {
return Err(invalid("paint rectangle exceeds bounds"));
}
if kind == "roundedRect" {
RenderCommand::RoundedRect {
x,
y,
width,
height,
radius,
color,
}
} else {
RenderCommand::StrokeRoundedRect {
x,
y,
width,
height,
radius,
color,
stroke_width: number(store, command, "strokeWidth")?,
}
}
}
"text" => {
let size = number(store, command, "size")?;
if size.fract() != 0.0 || !(1.0..=8.0).contains(&size) {
return Err(invalid("paint text size must be an integer from 1 to 8"));
}
let wrap_bounds = match (
store.get_attr(command, "wrapWidth"),
store.get_attr(command, "wrapHeight"),
) {
(Err(VmError::AttributeNotFound(_)), Err(VmError::AttributeNotFound(_))) => None,
(Ok(_), Ok(_)) => Some([
number(store, command, "wrapWidth")?,
number(store, command, "wrapHeight")?,
]),
(Err(error), _) | (_, Err(error)) => return Err(error),
};
RenderCommand::Text {
x,
y,
value: text(store, command, "text")?,
size: size as u32,
color,
wrap_bounds,
}
}
_ => return Err(invalid("unsupported paint command kind")),
};
result
.validate()
.map_err(|_| invalid("invalid paint command geometry, text or color"))?;
Ok(result)
}
#[derive(Debug, Default)]
struct PaintOutput {
commands: BTreeMap<u64, Vec<RenderCommand>>,
count: usize,
}
type Output = Arc<Mutex<PaintOutput>>;
#[derive(Debug)]
struct Collect {
desktop: ObjectRef,
mount: u64,
output: Output,
}
impl NativeCallable for Collect {
fn arity(&self) -> Option<endor_core::CallArity> {
Some(endor_core::CallArity::exact(1))
}
fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
let [Value::Object(result)] = args else {
return Err(invalid("paint must return a DisplayList"));
};
if !cx.store().is_instance(
*result,
named_type(cx.store(), self.desktop, "DisplayList")?,
)? {
return Err(invalid("paint must return a DisplayList"));
}
let commands = object(cx.store().get_attr(*result, "commands")?)?;
let expected = named_type(cx.store(), self.desktop, "PaintCommand")?;
let commands = object_list::items(cx.store(), commands)?
.iter()
.map(|command| decode(cx.store(), *command, expected))
.collect::<Result<Vec<_>, _>>()?;
let mut output = self
.output
.lock()
.map_err(|_| invalid("paint output poisoned"))?;
if output.count + commands.len() > 10_000 {
return Err(invalid("paint command budget exceeded"));
}
output.count += commands.len();
output.commands.insert(self.mount, commands);
Ok(NativeResult::Return(Value::Nil))
}
}
#[derive(Debug)]
struct Prepare {
desktop: ObjectRef,
mounts: Vec<(u64, WeakObjectRef)>,
output: Output,
}
impl NativeCallable for Prepare {
fn arity(&self) -> Option<endor_core::CallArity> {
Some(endor_core::CallArity::exact(0))
}
fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result<NativeResult, VmError> {
let mut constants = Vec::new();
let mut instructions = Vec::new();
for (id, weak) in &self.mounts {
let mount = cx
.upgrade(*weak)?
.ok_or(invalid("paint Mount has expired"))?;
let data = cx.store().payload::<Mount>(mount, "Mount")?.clone();
if !data.mounted {
return Err(invalid("paint Mount is detached"));
}
let definition = cx
.store()
.type_of(data.block)?
.ok_or(invalid("untyped paint target"))?;
let function = cx
.store()
.lookup_method(definition, "paint")?
.ok_or(invalid("missing paint method"))?;
if crate::native_visual::is_intrinsic(cx.store(), function, "paint", self.desktop)? {
continue;
}
let bounds = cx.new_object()?;
attach_desktop_type(cx, bounds, "Bounds", self.desktop)?;
for (name, value) in [
("x", data.geometry.x),
("y", data.geometry.y),
("width", data.geometry.width),
("height", data.geometry.height),
] {
cx.set_attr(bounds, name, Value::Number((value as f64).into()))?;
}
let collector = cx.allocate(ContextNativeFunction::new(
"collect source paint",
Collect {
desktop: self.desktop,
mount: *id,
output: self.output.clone(),
},
vec![self.desktop],
))?;
let start = constants.len() as u32;
constants.extend([
Value::Object(collector),
Value::Object(function),
Value::Object(data.block),
Value::Object(mount),
Value::Object(bounds),
]);
instructions.extend([
Instruction::new(Opcode::LoadConst, start),
Instruction::simple(Opcode::PushNull),
Instruction::new(Opcode::LoadConst, start + 1),
Instruction::simple(Opcode::PushNull),
Instruction::new(Opcode::LoadConst, start + 2),
Instruction::new(Opcode::LoadConst, start + 3),
Instruction::new(Opcode::LoadConst, start + 4),
Instruction::new(Opcode::Call, 3),
Instruction::new(Opcode::Call, 1),
Instruction::simple(Opcode::PopTop),
]);
}
if instructions.is_empty() {
return Ok(NativeResult::Return(Value::Nil));
}
let index = constants.len() as u32;
constants.push(Value::Nil);
instructions.extend([
Instruction::new(Opcode::LoadConst, index),
Instruction::simple(Opcode::ReturnValue),
]);
let code = EndorCode::from_instructions(&instructions, constants, CodeLayout::default())?
.with_location("<source paint>", "<tessera>", 1);
let code = cx.allocate(EndorCodeObject::new(code))?;
let globals = cx.new_dict()?;
let driver = cx.new_function("source paint", code, globals)?;
Ok(NativeResult::Invoke {
function: driver,
arguments: vec![],
})
}
}
pub fn render(
vm: &mut Vm,
desktop: ObjectRef,
mounts: Vec<(u64, WeakObjectRef)>,
root: Option<&UiNode>,
frame: &mut Frame,
) -> Result<BTreeSet<ObjectRef>> {
let output = Arc::new(Mutex::new(PaintOutput::default()));
let prepare = vm.allocate(ContextNativeFunction::new(
"prepare source paint",
Prepare {
desktop,
mounts,
output: output.clone(),
},
vec![desktop],
))?;
let result = vm.call_read_only_with_limits(
prepare,
&[],
endor_core::ExecutionLimits {
max_frames: 128,
max_instructions: 1_000_000,
},
);
vm.release(prepare)?;
let (value, reads) = result.map_err(|error| crate::endor_host::runtime_error(vm, error))?;
vm.release_value(value)?;
let output = output
.lock()
.map_err(|_| anyhow::anyhow!("paint output poisoned"))?;
if output.commands.is_empty() {
return Ok(BTreeSet::new());
}
let mut nodes = HashMap::new();
let mut pending: Vec<_> = root.into_iter().collect();
while let Some(node) = pending.pop() {
nodes.insert(node.block_id.as_str(), node);
pending.extend(node.children().iter().map(|node| node.as_ref()));
}
let mut commands = Vec::new();
let mut clips = Vec::new();
for mount in &frame.mounts {
let before = commands.len();
if let Some(custom) = output.commands.get(&mount.id) {
commands.extend(custom.iter().cloned());
} else {
let node = nodes
.get(mount.block_id.as_str())
.ok_or_else(|| anyhow::anyhow!("paint projection has no node"))?;
if let Some(command) = layout::paint_native(
node,
mount.x,
mount.y,
Size {
width: mount.width,
height: mount.height,
},
) {
commands.push(command);
}
}
clips.extend(std::iter::repeat_n(mount.clip, commands.len() - before));
if commands.len() > 10_000 {
bail!("paint command budget exceeded");
}
}
frame.commands = commands;
frame.command_clips = clips;
let registry = object(vm.get_attr(desktop, "types")?)?;
Ok(reads
.into_iter()
.filter(|object| {
*object != desktop
&& *object != registry
&& vm
.object(*object)
.is_ok_and(|object| !object.payload().as_any().is::<Mount>())
})
.collect())
}