diff --git a/apple/aesel/MacInfo.plist b/apple/aesel/MacInfo.plist
index d9851c0f7d..6ad6f9d3c5 100644
--- a/apple/aesel/MacInfo.plist
+++ b/apple/aesel/MacInfo.plist
@@ -8,7 +8,7 @@
CFBundleDisplayNameAesel
CFBundlePackageTypeAPPL
CFBundleShortVersionString0.7.5
- CFBundleVersion2
+ CFBundleVersion3
LSMinimumSystemVersion$(MACOSX_DEPLOYMENT_TARGET)
LSApplicationCategoryTypepublic.app-category.graphics-design
NSHighResolutionCapable
diff --git a/apple/aesel/Sources/AeselButtonStyle.swift b/apple/aesel/Sources/AeselButtonStyle.swift
index ca28902cc1..f7fa40395a 100644
--- a/apple/aesel/Sources/AeselButtonStyle.swift
+++ b/apple/aesel/Sources/AeselButtonStyle.swift
@@ -50,6 +50,23 @@ private struct AeselCursorRegion: NSViewRepresentable {
let enabled: Bool
final class CursorView: NSView {
var enabled = true
+ private var pointerTracking: NSTrackingArea?
+ override func updateTrackingAreas() {
+ super.updateTrackingAreas()
+ if let pointerTracking { removeTrackingArea(pointerTracking) }
+ let area = NSTrackingArea(rect: .zero,
+ options: [.cursorUpdate, .mouseEnteredAndExited, .activeAlways, .inVisibleRect],
+ owner: self, userInfo: nil)
+ addTrackingArea(area)
+ pointerTracking = area
+ }
+ override func cursorUpdate(with event: NSEvent) {
+ if enabled { NSCursor.pointingHand.set() }
+ }
+ override func mouseEntered(with event: NSEvent) {
+ if enabled { NSCursor.pointingHand.set() }
+ }
+ override func mouseExited(with event: NSEvent) { NSCursor.arrow.set() }
override func hitTest(_ point: NSPoint) -> NSView? { nil }
override func resetCursorRects() {
if enabled { addCursorRect(visibleRect, cursor: .pointingHand) }
diff --git a/apple/aesel/Sources/ContentView.swift b/apple/aesel/Sources/ContentView.swift
index f7e23f061e..1273039089 100644
--- a/apple/aesel/Sources/ContentView.swift
+++ b/apple/aesel/Sources/ContentView.swift
@@ -390,7 +390,7 @@ struct ContentView: View {
/// The piece version opens settings from the fixed title strip.
private var versionLabel: some View {
Button { openSettings() } label: {
- AeselTitle(text: "v\(session.currentRevision)", size: compact ? 11 : 13, horizontalInset: 0, hoverAnchor: .trailing,
+ AeselTitle(text: "v\(session.currentRevision)", size: compact ? 12 : 16, horizontalInset: 0, hoverAnchor: .trailing,
hoverSound: { AeselHoverSound.play(project: session.route, revision: session.currentRevision, control: "version") })
}
.buttonStyle(AeselButtonStyle())
diff --git a/easel/desktop/frame-capture.cjs b/easel/desktop/frame-capture.cjs
index 870e5b7e96..6e4f9e307c 100644
--- a/easel/desktop/frame-capture.cjs
+++ b/easel/desktop/frame-capture.cjs
@@ -24,6 +24,8 @@ function startFrameCapture({workspace,root,context,guest}){
let result;try{
if(!matchesPreview(view,current.channel))throw new Error('Preview URL does not match the current piece channel; capture unavailable, do not repeatedly retry');
const capture=await Promise.race([view.executeJavaScript((await script).CAPTURE_SCRIPT),new Promise((_,reject)=>setTimeout(()=>reject(new Error('Canvas capture timed out')),3000))]);
+ if(capture?.pending)continue;
+ if(capture?.error)throw new Error(capture.error);
if(context()?.revision!==current.revision||context()?.channel!==current.channel)throw new Error('Piece changed during capture');
result={...capture,id:request.id,channel:current.channel,revision:current.revision,capturedAt:new Date().toISOString()};
}catch(error){result={id:request.id,error:error.message};}
diff --git a/easel/src/frame-capture-script.mjs b/easel/src/frame-capture-script.mjs
index 17080fb3b0..81cfe89d84 100644
--- a/easel/src/frame-capture-script.mjs
+++ b/easel/src/frame-capture-script.mjs
@@ -1,13 +1,14 @@
// Reads AC's software canvas or preserved WebGL composite, never app/desktop chrome.
export const CAPTURE_SCRIPT = `(()=>{
+ try {
const root=document.getElementById('aesthetic-computer');
- if(!root)throw new Error('AC display is not ready');
+ if(!root)return {pending:true,url:location.href};
const visible=c=>c&&getComputedStyle(c).display!=='none';
if(visible(root.querySelector('canvas[data-type="webgpu"]')))throw new Error('WebGPU capture is not supported by this canvas reader');
const composite=root.querySelector('canvas[data-type="webgl-composite"]');
const gpu=visible(composite)?composite:null;
const canvas=gpu||[...root.children].find(c=>c.tagName==='CANVAS'&&!c.dataset.type);
- if(!canvas)throw new Error('AC software canvas is unavailable');
+ if(!canvas||!canvas.width||!canvas.height)return {pending:true,url:location.href};
const width=canvas.width,height=canvas.height;
if(!width||!height||width>2048||height>2048||width*height>1048576)throw new Error('Canvas exceeds capture limit (1 megapixel)');
let surface=canvas;
@@ -16,4 +17,5 @@ export const CAPTURE_SCRIPT = `(()=>{
const data=ctx.getImageData(0,0,width,height).data;
let raw='';for(let at=0;at{
+ let root=null;
+ const context={document:{getElementById:()=>root},location:{href:'https://aesthetic.computer/@jeffrey/test'},getComputedStyle:()=>({display:'block'}),btoa:s=>Buffer.from(s,'binary').toString('base64')};
+ assert.equal(vm.runInNewContext(CAPTURE_SCRIPT,context).pending,true);
+ root={children:[],querySelector:()=>null};
+ assert.equal(vm.runInNewContext(CAPTURE_SCRIPT,context).pending,true);
+ root.children.push({tagName:'CANVAS',dataset:{},width:1,height:1,getContext:()=>({getImageData:()=>({data:new Uint8Array([255,0,0,255])})}),toDataURL:()=> 'data:image/png;base64,test'});
+ const captured=vm.runInNewContext(CAPTURE_SCRIPT,context);
+ assert.equal(captured.width,1);
+ assert.equal(captured.source,'software-canvas');
+ assert.equal(captured.pending,undefined);
+});
+test('capture failures return to the requester without throwing into the preview page',()=>{
+ const result=vm.runInNewContext(CAPTURE_SCRIPT,{document:{getElementById(){throw new Error('Unreadable canvas')}},location:{href:'https://aesthetic.computer/@jeffrey/test'}});
+ assert.equal(result.error,'Unreadable canvas');
+});
diff --git a/slab/menubar-swift/Sources/SlabMenubar/PromptPreview.swift b/slab/menubar-swift/Sources/SlabMenubar/PromptPreview.swift
index 036e4b0033..a063aa923f 100644
--- a/slab/menubar-swift/Sources/SlabMenubar/PromptPreview.swift
+++ b/slab/menubar-swift/Sources/SlabMenubar/PromptPreview.swift
@@ -732,14 +732,15 @@ final class PromptPreview {
// Mirrors easel/src/frame-capture-script.mjs; no request-supplied JavaScript.
private static let frameCaptureExpression = #"""
(()=>{
+ try {
const root=document.getElementById('aesthetic-computer');
- if(!root)throw new Error('AC display is not ready');
+ if(!root)return {pending:true,url:location.href};
const visible=c=>c&&getComputedStyle(c).display!=='none';
if(visible(root.querySelector('canvas[data-type="webgpu"]')))throw new Error('WebGPU capture is not supported by this canvas reader');
const composite=root.querySelector('canvas[data-type="webgl-composite"]');
const gpu=visible(composite)?composite:null;
const canvas=gpu||[...root.children].find(c=>c.tagName==='CANVAS'&&!c.dataset.type);
- if(!canvas)throw new Error('AC software canvas is unavailable');
+ if(!canvas||!canvas.width||!canvas.height)return {pending:true,url:location.href};
const width=canvas.width,height=canvas.height;
if(!width||!height||width>2048||height>2048||width*height>1048576)throw new Error('Canvas exceeds capture limit (1 megapixel)');
let surface=canvas;
@@ -748,6 +749,7 @@ final class PromptPreview {
const data=ctx.getImageData(0,0,width,height).data;
let raw='';for(let at=0;at