Skip to content
Merged
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
19 changes: 17 additions & 2 deletions src/apps/desktop/src/computer_use/macos_capture.m
Original file line number Diff line number Diff line change
Expand Up @@ -463,14 +463,29 @@ - (void)drawRect:(NSRect)rect {
[arrow lineToPoint:NSMakePoint(15, 8)];
[arrow curveToPoint:NSMakePoint(8, 8) controlPoint1:NSMakePoint(12, 7) controlPoint2:NSMakePoint(10, 6)];
[arrow closePath];
// Shrink around the hotspot, leaving room for the fixed 3pt white outline.
NSAffineTransform *size = [NSAffineTransform transform];
[size translateXBy:8 yBy:8];
[size scaleBy:0.7];
[size translateXBy:-8 yBy:-8];
[arrow transformUsingAffineTransform:size];
arrow.lineWidth = 3;
arrow.lineJoinStyle = NSLineJoinStyleRound;
[NSGraphicsContext saveGraphicsState];
NSShadow *shadow = [[NSShadow alloc] init];
shadow.shadowColor = [NSColor colorWithWhite:0 alpha:0.45];
shadow.shadowBlurRadius = 3;
shadow.shadowOffset = NSMakeSize(0, -2);
// This view is flipped, so positive Y places the shadow below the cursor.
shadow.shadowOffset = NSMakeSize(0, 2);
[shadow set];
[[NSColor colorWithSRGBRed:166.0/255 green:166.0/255 blue:166.0/255 alpha:1] setFill];
// Cast one shadow from the complete silhouette, including its white edge.
CGContextRef context = NSGraphicsContext.currentContext.CGContext;
CGContextBeginTransparencyLayer(context, NULL);
[[NSColor colorWithSRGBRed:64.0/255 green:64.0/255 blue:64.0/255 alpha:1] setFill];
[arrow fill];
[NSColor.whiteColor setStroke];
[arrow stroke];
CGContextEndTransparencyLayer(context);
[NSGraphicsContext restoreGraphicsState];
}
@end
Expand Down
39 changes: 32 additions & 7 deletions src/apps/desktop/src/computer_use/windows_pointer_feedback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,8 @@ unsafe extern "system" {

// Rasterize the shared 40x40 cubic contour at four samples per axis. Alpha is
// premultiplied for AC_SRC_ALPHA: black shadow pixels retain alpha, unlike a
// color-key surface. The blur is independent of the solid, unstroked arrow.
// color-key surface. The contour shrinks around its hotspot before adding a
// fixed 3px white stroke; the complete silhouette casts the shadow.
fn pointer_pixels(clicked: bool) -> Vec<u32> {
type P = (f64, f64);
fn cubic(points: &mut Vec<P>, a: P, b: P, c: P, d: P) {
Expand Down Expand Up @@ -172,29 +173,49 @@ fn pointer_pixels(clicked: bool) -> Vec<u32> {
(10.0, 6.0),
(8.0, 8.0),
);
for point in &mut outline {
point.0 = 8.0 + (point.0 - 8.0) * 0.7;
point.1 = 8.0 + (point.1 - 8.0) * 0.7;
}
let mut mask = vec![0.0; 1600];
let mut stroke_mask = vec![0.0; 1600];
for y in 0..40 {
for x in 0..40 {
let mut coverage = 0;
let mut stroke_coverage = 0;
for sy in 0..4 {
for sx in 0..4 {
let px = x as f64 + (f64::from(sx) + 0.5) / 4.0;
let py = y as f64 + (f64::from(sy) + 0.5) / 4.0;
let mut inside = false;
let mut on_stroke = false;
for edge in outline.windows(2) {
let (a, b) = (edge[0], edge[1]);
let (dx, dy) = (b.0 - a.0, b.1 - a.1);
let length_squared = dx * dx + dy * dy;
let t = if length_squared > 0.0 {
(((px - a.0) * dx + (py - a.1) * dy) / length_squared).clamp(0.0, 1.0)
} else {
0.0
};
on_stroke |= (px - a.0 - t * dx).powi(2) + (py - a.1 - t * dy).powi(2)
<= 1.5_f64.powi(2);
if (a.1 > py) != (b.1 > py)
&& px < (b.0 - a.0) * (py - a.1) / (b.1 - a.1) + a.0
{
inside = !inside;
}
}
if inside {
if inside || on_stroke {
coverage += 1;
}
if on_stroke {
stroke_coverage += 1;
}
}
}
mask[y * 40 + x] = f64::from(coverage) / 16.0;
stroke_mask[y * 40 + x] = f64::from(stroke_coverage) / 16.0;
}
}
let weights: Vec<f64> = (-5..=5)
Expand All @@ -216,7 +237,9 @@ fn pointer_pixels(clicked: bool) -> Vec<u32> {
}
}
}
let fill = mask[(y * 40 + x) as usize];
let silhouette = mask[(y * 40 + x) as usize];
let stroke = stroke_mask[(y * 40 + x) as usize];
let fill = silhouette - stroke;
let radius =
((f64::from(x) + 0.5 - 8.0).powi(2) + (f64::from(y) + 0.5 - 8.0).powi(2)).sqrt();
let ring = if clicked {
Expand All @@ -225,8 +248,9 @@ fn pointer_pixels(clicked: bool) -> Vec<u32> {
0.0
};
let behind = ring + shadow * 0.45 * (1.0 - ring);
let alpha = fill + behind * (1.0 - fill);
let channel = (166.0 * fill + 166.0 * ring * (1.0 - fill)).round() as u32;
let alpha = silhouette + behind * (1.0 - silhouette);
let channel =
(64.0 * fill + 255.0 * stroke + 166.0 * ring * (1.0 - silhouette)).round() as u32;
pixels[(y * 40 + x) as usize] =
((alpha * 255.0).round() as u32) << 24 | channel << 16 | channel << 8 | channel;
}
Expand Down Expand Up @@ -398,10 +422,11 @@ pub(super) fn show_pointer(target: usize, x: i32, y: i32, clicked: bool) {
mod tests {
use super::pointer_pixels;
#[test]
fn rounded_cursor_has_grey_fill_and_translucent_black_shadow() {
fn rounded_cursor_has_dark_fill_white_edge_and_translucent_black_shadow() {
let pixels = pointer_pixels(false);
assert_eq!(pixels.len(), 1600);
assert_eq!(pixels[16 * 40 + 16], 0xffa6a6a6);
assert_eq!(pixels[16 * 40 + 16], 0xff404040);
assert!(pixels.contains(&0xffffffff));
assert!(pixels
.iter()
.any(|p| p & 0xffffff == 0 && p >> 24 > 0 && p >> 24 < 115));
Expand Down
5 changes: 4 additions & 1 deletion src/web-ui/src/app/components/ComputerUseControlCard.scss
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@
inline-size: 100%;
block-size: 100%;
// Fixed neutral feedback over captured content, independent of accent color.
fill: color-mix(in srgb, var(--openbitfun-color-content-on-dark) 65%, var(--openbitfun-color-content-on-light));
fill: color-mix(in srgb, var(--openbitfun-color-content-on-dark) 25%, var(--openbitfun-color-content-on-light));
stroke: var(--openbitfun-color-content-on-dark);
stroke-width: 3px;
stroke-linejoin: round;
filter: drop-shadow(0 2px 3px color-mix(in srgb, var(--openbitfun-color-content-on-light) 45%, transparent));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,6 @@ export function PreviewCursor({ x, y, immediate = false }: { x: number; y: numbe
useEffect(() => { update.current?.(); }, [x, y, immediate]);
return <span ref={element} className="computer-control-card__pointer computer-control-card__pointer--physical"
data-openbitfun-component="computer-control" data-openbitfun-part="pointer" aria-hidden="true">
<svg viewBox="0 0 40 40" focusable="false"><path d="M8 8 C6 10 7 12 8 15 L14 31 C15.5 35 19 35 20.5 31.5 L23 26 C23.6 24.5 24.5 23.6 26 23 L31.5 20.5 C35 19 35 15.5 31 14 L15 8 C12 7 10 6 8 8 Z" /></svg>
<svg viewBox="0 0 40 40" focusable="false"><path transform="translate(8 8) scale(0.7) translate(-8 -8)" vectorEffect="non-scaling-stroke" d="M8 8 C6 10 7 12 8 15 L14 31 C15.5 35 19 35 20.5 31.5 L23 26 C23.6 24.5 24.5 23.6 26 23 L31.5 20.5 C35 19 35 15.5 31 14 L15 8 C12 7 10 6 8 8 Z" /></svg>
</span>;
}
Loading