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
16 changes: 8 additions & 8 deletions crates/renderer/src/canvas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ use wasm_bindgen::JsCast;
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement};

use crate::layout::{
compute_layout, split_tokens_into_lines, Layout, TRAFFIC_LIGHT_OFFSET_X, TRAFFIC_LIGHT_PITCH,
TRAFFIC_LIGHT_RADIUS,
compute_layout, split_tokens_into_lines, Layout, CARD_SHADOW_BLUR, CARD_SHADOW_OFFSET_Y,
TRAFFIC_LIGHT_OFFSET_X, TRAFFIC_LIGHT_PITCH, TRAFFIC_LIGHT_RADIUS,
};

/// Conservative maximum canvas dimension in *device* pixels. Safari caps
Expand Down Expand Up @@ -258,8 +258,8 @@ pub fn draw_prepared(
)?;
if options.window_frame {
ctx.set_shadow_color("rgba(0, 0, 0, 0.35)");
ctx.set_shadow_blur(20.0);
ctx.set_shadow_offset_y(10.0);
ctx.set_shadow_blur(CARD_SHADOW_BLUR);
ctx.set_shadow_offset_y(CARD_SHADOW_OFFSET_Y);
}
ctx.set_fill_style_str(&palette.background.to_css());
ctx.fill();
Expand Down Expand Up @@ -520,9 +520,9 @@ pub fn draw_split_prepared(
ctx.set_fill_style_str("rgba(128, 128, 128, 0.3)");
ctx.fill_rect(
divider_x,
options.padding,
split.left.card_y,
1.0,
split.canvas_height - 2.0 * options.padding,
split.canvas_height - 2.0 * split.left.card_y,
);

// 4. Right panel (offset by left width + gap).
Expand Down Expand Up @@ -558,8 +558,8 @@ fn draw_panel(
)?;
if options.window_frame {
ctx.set_shadow_color("rgba(0, 0, 0, 0.35)");
ctx.set_shadow_blur(20.0);
ctx.set_shadow_offset_y(10.0);
ctx.set_shadow_blur(CARD_SHADOW_BLUR);
ctx.set_shadow_offset_y(CARD_SHADOW_OFFSET_Y);
}
ctx.set_fill_style_str(&palette.background.to_css());
ctx.fill();
Expand Down
90 changes: 83 additions & 7 deletions crates/renderer/src/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,21 @@ pub const TRAFFIC_LIGHT_PITCH: f64 = 22.0;
/// X offset of the first traffic-light dot center from the card edge.
pub const TRAFFIC_LIGHT_OFFSET_X: f64 = 22.0;

/// Canvas2D `shadowBlur` of the code-card drop shadow (logical px).
pub const CARD_SHADOW_BLUR: f64 = 20.0;

/// Canvas2D `shadowOffsetY` of the code-card drop shadow (logical px).
pub const CARD_SHADOW_OFFSET_Y: f64 = 10.0;

/// Conservative paint extent of the card shadow beyond the card edge.
///
/// `shadowBlur` spreads the shadow both ways around the shape, so the
/// painted overflow is at most `blur + |offset|` — glyph-style metrics (e.g.
/// `measureText`) never cover it. [`compute_layout`] clamps the canvas
/// padding to at least this when the window frame is on, so the shadow never
/// clips at the canvas edge.
pub const CARD_SHADOW_SAFE_MARGIN: f64 = CARD_SHADOW_BLUR + CARD_SHADOW_OFFSET_Y;

/// Gap between the line-number gutter and the code, in character cells.
pub const GUTTER_GAP_CELLS: f64 = 1.5;

Expand Down Expand Up @@ -130,21 +145,29 @@ pub fn compute_layout(
} else {
0.0
};
// The card drop shadow paints up to `CARD_SHADOW_SAFE_MARGIN` px beyond the
// card edge (painted overflow is invisible to text metrics), so clamp the
// padding to keep it inside the canvas.
let padding = if options.window_frame {
options.padding.max(CARD_SHADOW_SAFE_MARGIN)
} else {
options.padding
};

let card_width = gutter + max_line_width_px + 2.0 * INNER_PADDING;
let card_height = header_height + code_height + 2.0 * INNER_PADDING;

Layout {
canvas_width: options.padding + card_width + options.padding,
canvas_height: options.padding + card_height + options.padding,
card_x: options.padding,
card_y: options.padding,
canvas_width: padding + card_width + padding,
canvas_height: padding + card_height + padding,
card_x: padding,
card_y: padding,
card_width,
card_height,
header_height,
code_origin_x: options.padding + INNER_PADDING + gutter,
code_origin_y: options.padding + header_height + INNER_PADDING,
gutter_right_x: options.padding + INNER_PADDING + gutter - 0.5 * char_width,
code_origin_x: padding + INNER_PADDING + gutter,
code_origin_y: padding + header_height + INNER_PADDING,
gutter_right_x: padding + INNER_PADDING + gutter - 0.5 * char_width,
line_height_px,
line_count,
}
Expand Down Expand Up @@ -261,4 +284,57 @@ mod tests {
options.padding + INNER_PADDING + gutter - 0.5 * char_width
);
}

#[test]
fn frame_raises_padding_below_shadow_margin() {
let options = ExportOptions {
padding: 16.0,
window_frame: true,
line_numbers: false,
..Default::default()
};
let layout = compute_layout(&options, 1, 50.0, 8.0);
assert_eq!(layout.card_x, CARD_SHADOW_SAFE_MARGIN);
assert_eq!(layout.card_y, CARD_SHADOW_SAFE_MARGIN);
assert_eq!(
layout.canvas_width,
CARD_SHADOW_SAFE_MARGIN + layout.card_width + CARD_SHADOW_SAFE_MARGIN
);
assert_eq!(
layout.canvas_height,
CARD_SHADOW_SAFE_MARGIN + layout.card_height + CARD_SHADOW_SAFE_MARGIN
);
}

#[test]
fn frame_keeps_padding_at_or_above_shadow_margin() {
let at_margin = ExportOptions {
padding: CARD_SHADOW_SAFE_MARGIN,
window_frame: true,
..Default::default()
};
assert_eq!(
compute_layout(&at_margin, 1, 50.0, 8.0).card_x,
CARD_SHADOW_SAFE_MARGIN
);
let above_margin = ExportOptions {
padding: 48.0,
window_frame: true,
..Default::default()
};
assert_eq!(compute_layout(&above_margin, 1, 50.0, 8.0).card_x, 48.0);
}

#[test]
fn no_frame_leaves_padding_untouched() {
let options = ExportOptions {
padding: 16.0,
window_frame: false,
line_numbers: false,
..Default::default()
};
let layout = compute_layout(&options, 1, 50.0, 8.0);
assert_eq!(layout.card_x, 16.0);
assert_eq!(layout.card_y, 16.0);
}
}
24 changes: 14 additions & 10 deletions crates/renderer/src/svg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
use codeframe_models::{Background, ExportOptions, FontStyle, GradientDir, ThemePalette, Token};

use crate::layout::{
compute_layout, split_tokens_into_lines, Layout, TRAFFIC_LIGHT_OFFSET_X, TRAFFIC_LIGHT_PITCH,
TRAFFIC_LIGHT_RADIUS,
compute_layout, split_tokens_into_lines, Layout, CARD_SHADOW_BLUR, CARD_SHADOW_OFFSET_Y,
TRAFFIC_LIGHT_OFFSET_X, TRAFFIC_LIGHT_PITCH, TRAFFIC_LIGHT_RADIUS,
};

/// macOS traffic-light colors (close, minimize, zoom).
Expand Down Expand Up @@ -94,11 +94,13 @@ pub fn render_svg(
svg.push_str("<defs>");
// Drop-shadow filter for the card.
if options.window_frame {
svg.push_str(
svg.push_str(&format!(
r#"<filter id="shadow" x="-10%" y="-5%" width="130%" height="130%">
<feDropShadow dx="0" dy="10" stdDeviation="10" flood-color="rgba(0,0,0,0.35)"/>
<feDropShadow dx="0" dy="{dy}" stdDeviation="{sd}" flood-color="rgba(0,0,0,0.35)"/>
</filter>"#,
);
dy = CARD_SHADOW_OFFSET_Y,
sd = CARD_SHADOW_BLUR / 2.0,
));
}
// Background gradient defs.
match &options.background {
Expand Down Expand Up @@ -293,11 +295,13 @@ pub fn render_split_svg(
// Defs.
svg.push_str("<defs>");
if options.window_frame {
svg.push_str(
svg.push_str(&format!(
r#"<filter id="shadow" x="-10%" y="-5%" width="130%" height="130%">
<feDropShadow dx="0" dy="10" stdDeviation="10" flood-color="rgba(0,0,0,0.35)"/>
<feDropShadow dx="0" dy="{dy}" stdDeviation="{sd}" flood-color="rgba(0,0,0,0.35)"/>
</filter>"#,
);
dy = CARD_SHADOW_OFFSET_Y,
sd = CARD_SHADOW_BLUR / 2.0,
));
}
match &options.background {
Background::LinearGradient { colors, dir } if colors.len() >= 2 => {
Expand Down Expand Up @@ -377,8 +381,8 @@ pub fn render_split_svg(
let divider_x = layout_left.canvas_width + SPLIT_GAP / 2.0;
svg.push_str(&format!(
"<rect x=\"{divider_x}\" y=\"{y}\" width=\"1\" height=\"{h}\" fill=\"rgba(128,128,128,0.3)\"/>",
y = options.padding,
h = total_h - 2.0 * options.padding,
y = layout_left.card_y,
h = total_h - 2.0 * layout_left.card_y,
));

// Right panel.
Expand Down
Loading