Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,31 @@
This file serves as the evolving knowledge base for working with this codebase.
Update it whenever you learn something new about the project's patterns, conventions, or receive feedback that should guide future behavior.

## Project Orientation
- This workspace is a Cargo workspace rooted at [Cargo.toml](../Cargo.toml) with core crates under top-level folders like [scene](../scene), [renderer](../renderer), [desktop](../desktop), and [shell](../shell).
- The source-of-truth architecture overview for scene/object lifetime is in [scene/src/lib.rs](../scene/src/lib.rs). Read this before changing scene handle or change-propagation behavior.
- Use [README.md](../README.md) for example entry points and expected demo behavior instead of inferring from code paths.

## Workspace Boundaries
- Treat [examples/code/rust-analyzer](../examples/code/rust-analyzer) and [examples/markdown/inlyne](../examples/markdown/inlyne) as imported upstream projects; do not modify them unless explicitly asked.
- Prefer changes in first-party crates listed in [Cargo.toml](../Cargo.toml) workspace members.
- Keep changes scoped to the relevant crate. Avoid cross-crate refactors unless the task explicitly requires them.

## Build, Run, and Validation
- Prefer `cargo build` for broad compile validation.
- Demo runs from [README.md](../README.md):
- `cargo run --release --example code`
- `cargo run --release --example markdown`
- WASM example workflows live in [justfile](../justfile), including `trunk serve --example markdown --port 8888 --open` and release build targets.
- If a task is scoped to one crate, prefer crate-targeted validation before workspace-wide commands.

## Architecture Anchors
- Scene graph and handle model: [scene/src/lib.rs](../scene/src/lib.rs), [scene/src/handle.rs](../scene/src/handle.rs), [scene/src/change.rs](../scene/src/change.rs)
- Fluent scene ergonomics: [scene/src/ergonomics.rs](../scene/src/ergonomics.rs)
- Desktop orchestration and event routing: [desktop/src/lib.rs](../desktop/src/lib.rs)
- Platform split (native vs wasm): [shell/Cargo.toml](../shell/Cargo.toml), [animation/src/lib.rs](../animation/src/lib.rs)
- Prefer linking to these files in explanations instead of duplicating architectural prose.

## Code Style
- Prefer small, self-contained changes unless explicitly asked for broader refactors.
- Match the surrounding code style.
Expand Down
5 changes: 5 additions & 0 deletions animation/src/coordinator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@ impl AnimationCoordinator {
self.inner.lock().current_cycle().mode = CycleMode::ApplyAnimations;
}

/// `true` if there are active animations right now.
pub fn animations_active(&self) -> bool {
self.inner.lock().animating
}

/// Ends an update cycle. Returns true if animations are active. This resets the current time.
pub fn end_cycle(&self) -> bool {
let mut inner = self.inner.lock();
Expand Down
89 changes: 68 additions & 21 deletions applications/src/instance_context.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
//! The context for an instance.

use std::{mem, sync::Arc};
use std::mem;
use std::sync::Arc;

use anyhow::Result;
use massive_scene::ChangeCollector;
use derive_more::Deref;
use tokio::sync::mpsc::UnboundedReceiver;

use massive_animation::AnimationCoordinator;
use massive_renderer::FontManager;
use massive_renderer::{FontManager, RenderPacing};
use massive_scene::{HandleChangeReceiver, Location, Ref, SceneChange};
use massive_util::{CoalescingKey, CoalescingReceiver};

use crate::view_builder::ViewBuilder;
use crate::{
InstanceEnvironment, InstanceId, InstanceParameters, Scene, ViewEvent, ViewExtent, ViewId,
view::{ViewCommand, ViewCreationInfo},
view_builder::ViewBuilder,
InstanceChange, InstanceEnvironment, InstanceId, InstanceParameters, InstanceSubmission, Scene,
ViewEvent, ViewExtent, ViewId,
};

#[derive(Debug, Clone, PartialEq, Eq)]
Expand All @@ -22,14 +24,31 @@ pub enum CreationMode {
Restore,
}

// Need a newtype here for the orphan rule.
#[derive(Debug, Default, Deref)]
pub struct InstanceChangeCollector(massive_util::ChangeCollector<InstanceChange>);

impl HandleChangeReceiver for InstanceChangeCollector {
fn send(&self, change: SceneChange) {
self.0.collect(InstanceChange::Scene(change))
}
}

#[derive(Debug)]
pub struct InstanceContext {
id: InstanceId,
creation_mode: CreationMode,
environment: InstanceEnvironment,
view_parent: Ref<Location>,

/// The `AnimationCoordinator` is here to create new scenes. There is one per instance for now.
/// We currently use one Scene per Context, so that everything is ordered properly. This also
/// contains the AnimationCoordinator, which we need one only per instance anyway.
animation_coordinator: AnimationCoordinator,

/// The current changes of this instance. This includes all Scene changes interleaved with the
/// instance changes (in order).
changes: Arc<InstanceChangeCollector>,

events: CoalescingReceiver<InstanceEvent>,
}

Expand All @@ -38,17 +57,28 @@ impl InstanceContext {
id: InstanceId,
creation_mode: CreationMode,
environment: InstanceEnvironment,
view_parent: Ref<Location>,
events: UnboundedReceiver<InstanceEvent>,
) -> Self {
// ADR: Every instance gets its own animation coordinator and its timestamp is reset as soon
// the scene is rendered. This way, consistence can be preserved when animations are applied
// in several instances in parallel. Otherwise, timestamps from one instance could affect the
// other.
let animation_coordinator = AnimationCoordinator::new();

// ADR: Every instance gets its own change collector, because of ordering constraints
// between the commands sent to the desktop and the scene updates (they must be processed in
// order by the desktop, otherwise it could happen that Visual refer to Locations /
// Transforms that are not available anymore).
let changes = InstanceChangeCollector::default();

Self {
id,
creation_mode,
environment,
animation_coordinator: AnimationCoordinator::new(),
view_parent,
animation_coordinator,
changes: changes.into(),
events: events.into(),
}
}
Expand Down Expand Up @@ -76,8 +106,12 @@ impl InstanceContext {
&self.environment.font_manager
}

/// ADR: We share _one_ single scene in all views now, so that we can keep the updates that we
/// send to desktop coordinated. Also, changes can't be submitted independently, all updates
/// from all views need to be submitted at once.
pub fn new_scene(&self) -> Scene {
Scene::new(self.animation_coordinator.clone())
let scene = massive_scene::Scene::new(self.changes.clone());
Scene::from_parts(scene, self.animation_coordinator.clone())
}

pub async fn wait_for_event(&mut self) -> Result<InstanceEvent> {
Expand All @@ -93,12 +127,35 @@ impl InstanceContext {

pub fn view(&self, extent: impl Into<ViewExtent>) -> ViewBuilder {
ViewBuilder::new(
self.environment.command_sender.clone(),
self.id,
self.changes.clone(),
self.view_parent.clone(),
extent.into().into(),
self.new_scene(),
)
}

pub fn submit(&mut self) -> Result<()> {
// Robustness: To be really thread safe, we would need to collect the changes and end the
// cycle in one go.
let animations_active = self.animation_coordinator.end_cycle();

// Empty changes need to end in a submission (we might have done some before, without ending
// the animation cycle)

let pacing = if animations_active {
RenderPacing::Smooth
} else {
RenderPacing::Fast
};

let changes = self.changes.take_all();

let submission = InstanceSubmission::new(changes, pacing);
Ok(self
.environment
.submission_sender
.send((self.id, submission))?)
}
}

#[derive(Debug, Clone)]
Expand All @@ -109,16 +166,6 @@ pub enum InstanceEvent {
ApplyAnimations,
}

/// Commands emitted by an instance and handled by the desktop layer.
#[derive(Debug)]
pub enum InstanceCommand {
CreateView(ViewCreationInfo),
// Detail: We pass the change collector up to the desktop, so it can make all Handles are destroyed and
// pending changes are sent to the renderer.
DestroyView(ViewId, Arc<ChangeCollector>),
View(ViewId, ViewCommand),
}

impl CoalescingKey for InstanceEvent {
type Key = InstanceEventCoalescingKey;

Expand Down
34 changes: 29 additions & 5 deletions applications/src/instance_environment.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
use massive_renderer::FontManager;
use derive_more::Constructor;
use serde_json::{Map, Value};
use tokio::sync::mpsc::UnboundedSender;

use crate::{InstanceCommand, InstanceId};
use massive_renderer::{FontManager, RenderPacing};
use massive_scene::SceneChange;
use massive_util::ChangeSet;

use crate::{InstanceId, ViewChange, ViewCreationInfo, ViewId};

#[derive(Debug, Clone)]
pub struct InstanceEnvironment {
pub(crate) command_sender: UnboundedSender<(InstanceId, InstanceCommand)>,
pub(crate) submission_sender: UnboundedSender<(InstanceId, InstanceSubmission)>,
// Robustness: This might change on runtime.
pub(crate) primary_monitor_scale_factor: f64,
pub(crate) font_manager: FontManager,
Expand All @@ -17,12 +21,12 @@ pub type InstanceParameters = Map<String, Value>;

impl InstanceEnvironment {
pub fn new(
requests_tx: UnboundedSender<(InstanceId, InstanceCommand)>,
requests_tx: UnboundedSender<(InstanceId, InstanceSubmission)>,
primary_monitor_scale_factor: f64,
font_manager: FontManager,
) -> Self {
Self {
command_sender: requests_tx,
submission_sender: requests_tx,
primary_monitor_scale_factor,
font_manager,
parameters: Default::default(),
Expand All @@ -34,3 +38,23 @@ impl InstanceEnvironment {
self
}
}

#[derive(Debug, Constructor)]
pub struct InstanceSubmission {
changes: ChangeSet<InstanceChange>,
pacing: RenderPacing,
}

impl InstanceSubmission {
pub fn into_parts(self) -> (ChangeSet<InstanceChange>, RenderPacing) {
(self.changes, self.pacing)
}
}

#[derive(Debug)]
pub enum InstanceChange {
Scene(SceneChange),
CreateView(ViewCreationInfo),
View(ViewId, ViewChange),
DestroyView(ViewId),
}
38 changes: 30 additions & 8 deletions applications/src/scene.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,30 @@ pub struct Scene {

impl Scene {
pub fn new(animation_coordinator: AnimationCoordinator) -> Self {
Self::new_with_change_collector(
animation_coordinator,
Arc::new(ChangeCollector::default()),
)
}

pub fn new_with_change_collector(
animation_coordinator: AnimationCoordinator,
collector: Arc<ChangeCollector>,
) -> Self {
// Robustness: We shouldn't allow arbitrary free generation of scenes anymore.
let scene = massive_scene::Scene::new(collector);
Self {
inner: scene,
animation_coordinator,
}
}

pub(crate) fn from_parts(
scene: massive_scene::Scene,
animation_coordinator: AnimationCoordinator,
) -> Self {
Self {
inner: Default::default(),
inner: scene,
animation_coordinator,
}
}
Expand Down Expand Up @@ -53,10 +75,10 @@ impl Scene {
self.animation_coordinator.time_scale()
}

/// Accumulate external changes into this scene.
pub fn accumulate_changes(&self, changes: massive_scene::SceneChanges) {
self.inner.push_changes(changes);
}
// Accumulate external changes into this scene.
// pub fn accumulate_changes(&self, changes: massive_scene::SceneChangeSet) {
// self.inner.push_changes(changes);
// }

// Render all the current scene changes.
//
Expand All @@ -79,7 +101,7 @@ impl Scene {
RenderSubmission::new(self.take_changes(), pacing)
}

pub fn into_collector(self) -> Arc<ChangeCollector> {
self.inner.into_collector()
}
// pub fn into_collector(self) -> Arc<ChangeCollector> {
// self.inner.into_collector()
// }
}
Loading
Loading