Skip to content
Draft
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
6 changes: 6 additions & 0 deletions WIDGET.md
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,12 @@ Landed in this repo:
`--density=N` builds; and the second runtime — `examples/note-widget`
over `demos/note` (markdown view/edit/menu, autosave, tested in
`test/note.test.ts`).
6. `examples/chorus`: the scene-only data point — a `WidgetGame` with no
guest and no `EmbeddedUi`, an airbrush-chrome diorama (procedural
geometry + painted ramps, everything unlit) you orbit by dragging
through the frame opening. It proves the 3D shell stands alone as an
ambient-art form and honors the governor: `--still` renders 2 frames
over a 3 s windowed run.

Still ahead: the golden-specs wiring (§8), density-2 screens for the 3D
form (§5 — the flat form ships them), the `widget` surface (§7),
Expand Down
15 changes: 15 additions & 0 deletions pocket3d/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pocket3d/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ members = [
"crates/pocket-ui-wgpu",
"crates/pocket-vrm",
"crates/pocket-widget",
"examples/chorus",
"examples/handheld",
"examples/note-widget",
"examples/uihost",
Expand Down
5 changes: 5 additions & 0 deletions pocket3d/crates/pocket-widget/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,8 @@ winit = { workspace = true }
glam = { workspace = true, features = ["std"] }
anyhow = { workspace = true }
log = { workspace = true }

# Widget windows set NSWindow.collectionBehavior (all Spaces, over
# fullscreen apps) — winit doesn't expose it. Same objc2 winit pins.
[target.'cfg(target_os = "macos")'.dependencies]
objc2 = "0.5"
2 changes: 1 addition & 1 deletion pocket3d/crates/pocket-widget/src/embed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
//! re-renders nothing, which is the heart of the shell's demand rendering.

use anyhow::Result;
use pocket3d::gpu::{Gpu, OFFSCREEN_FORMAT, OffscreenTarget};
use pocket_ui_wgpu::{UiRenderer, UiSurface};
use pocket3d::gpu::{Gpu, OFFSCREEN_FORMAT, OffscreenTarget};

pub struct EmbeddedUi {
surface: UiSurface,
Expand Down
8 changes: 3 additions & 5 deletions pocket3d/crates/pocket-widget/src/pick.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,16 +70,14 @@ mod tests {
fn t_is_world_scale_under_rotation_and_translation() {
// Box rotated 45° around Y and pushed to x=10: a ray down -X from
// (20, 0, 0) hits the corner-on silhouette at 10 - sqrt(2).
let m = Mat4::from_translation(Vec3::new(10.0, 0.0, 0.0))
* Mat4::from_rotation_y(FRAC_PI_4);
let m =
Mat4::from_translation(Vec3::new(10.0, 0.0, 0.0)) * Mat4::from_rotation_y(FRAC_PI_4);
let t = ray_obb(Vec3::new(20.0, 0.0, 0.0), Vec3::NEG_X, &m, BOX).unwrap();
assert!((t - (10.0 - 2.0f32.sqrt())).abs() < 1e-3, "t = {t}");
}

#[test]
fn parallel_ray_outside_slab_misses() {
assert!(
ray_obb(Vec3::new(0.0, 2.0, 5.0), Vec3::NEG_Z, &Mat4::IDENTITY, BOX).is_none()
);
assert!(ray_obb(Vec3::new(0.0, 2.0, 5.0), Vec3::NEG_Z, &Mat4::IDENTITY, BOX).is_none());
}
}
63 changes: 50 additions & 13 deletions pocket3d/crates/pocket-widget/src/shell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ pub struct WidgetConfig {
/// `ime_events` stream; the game reports its caret rect through
/// `ime_cursor_area` so candidate windows dock next to the text.
pub ime: bool,
/// Live on every Space, including over fullscreen apps (macOS: NSWindow
/// collectionBehavior CanJoinAllSpaces | FullScreenAuxiliary |
/// Stationary). Without this a widget launched from a fullscreen
/// terminal opens on the desktop Space and appears to not exist.
pub all_spaces: bool,
}

impl Default for WidgetConfig {
Expand All @@ -75,10 +80,42 @@ impl Default for WidgetConfig {
resizable: false,
min_size: (160, 120),
ime: false,
all_spaces: true,
}
}
}

/// Make the window a real desktop widget: visible on every Space, allowed
/// over fullscreen apps, unaffected by Spaces gestures. winit has no API
/// for `NSWindow.collectionBehavior`, so this reaches through the raw
/// handle. No-op off macOS.
#[cfg(target_os = "macos")]
fn apply_all_spaces(window: &Window) {
use objc2::msg_send;
use objc2::runtime::AnyObject;
use winit::raw_window_handle::{HasWindowHandle, RawWindowHandle};

let Ok(handle) = window.window_handle() else {
return;
};
let RawWindowHandle::AppKit(appkit) = handle.as_raw() else {
return;
};
// NSWindowCollectionBehavior: CanJoinAllSpaces | Stationary |
// IgnoresCycle | FullScreenAuxiliary.
const BEHAVIOR: usize = (1 << 0) | (1 << 4) | (1 << 6) | (1 << 8);
unsafe {
let ns_view = appkit.ns_view.as_ptr() as *mut AnyObject;
let ns_window: *mut AnyObject = msg_send![&*ns_view, window];
if !ns_window.is_null() {
let _: () = msg_send![&*ns_window, setCollectionBehavior: BEHAVIOR];
}
}
}

#[cfg(not(target_os = "macos"))]
fn apply_all_spaces(_window: &Window) {}

/// What the widget loop needs from a 3D product.
pub trait WidgetGame {
/// Called once after the GPU exists — build assets, boot guests.
Expand Down Expand Up @@ -132,12 +169,7 @@ pub trait FlatWidget {
fn take_dirty(&mut self) -> bool;
/// Draw into the swapchain view (submit your own encoder). Called only
/// on frames that render.
fn render(
&mut self,
gpu: &Gpu,
view: &wgpu::TextureView,
window_px: (u32, u32),
) -> Result<()>;
fn render(&mut self, gpu: &Gpu, view: &wgpu::TextureView, window_px: (u32, u32)) -> Result<()>;
/// Left-press policy: OS window drag (move) at this cursor position?
fn drag_at(&mut self, cursor: Vec2) -> bool {
let _ = cursor;
Expand All @@ -163,7 +195,13 @@ pub trait FlatWidget {

/// Run a 3D widget (scene + camera + demand rendering).
pub fn run(config: WidgetConfig, game: impl WidgetGame) -> Result<()> {
run_driver(config, SceneDriver { game, renderer: None })
run_driver(
config,
SceneDriver {
game,
renderer: None,
},
)
}

/// Run a 2D widget (the window is the surface).
Expand Down Expand Up @@ -379,6 +417,9 @@ impl<D: Driver> WidgetApp<D> {
if self.config.ime {
window.set_ime_allowed(true);
}
if self.config.all_spaces {
apply_all_spaces(&window);
}
let instance = Gpu::new_instance();
let surface = instance.create_surface(window.clone())?;
let gpu = Gpu::from_instance_for_surface(instance, &surface)?;
Expand Down Expand Up @@ -505,12 +546,8 @@ impl<D: Driver> WidgetApp<D> {
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let size = (state.surface_config.width, state.surface_config.height);
self.driver.render(
&state.gpu,
&view,
size,
state.start.elapsed().as_secs_f32(),
)?;
self.driver
.render(&state.gpu, &view, size, state.start.elapsed().as_secs_f32())?;
state.window.pre_present_notify();
frame.present();

Expand Down
17 changes: 17 additions & 0 deletions pocket3d/examples/chorus/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "chorus"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
description = "A dream-corridor diorama widget: airbrush chrome profiles receding to a starburst, drag to peer inside"

[dependencies]
pocket3d = { workspace = true }
pocket-widget = { workspace = true }
wgpu = { workspace = true }
winit = { workspace = true }
glam = { workspace = true, features = ["std"] }
anyhow = { workspace = true }
log = { workspace = true }
env_logger = { workspace = true }
Loading