A 5.9MB OCaml binary that renders SX pages directly using SDL2 + Cairo, bypassing HTML/CSS/JS entirely. Can fetch live pages from sx.rose-ash.com or render local .sx files. Architecture (1,387 lines of new code): sx_native_types.ml — render nodes, styles, layout boxes, color palette sx_native_style.ml — ~40 Tailwind classes → native style records sx_native_layout.ml — pure OCaml flexbox (measure + position) sx_native_render.ml — SX value tree → native render nodes sx_native_paint.ml — render nodes → Cairo draw commands sx_native_fetch.ml — HTTP fetch via curl with SX-Request headers sx_native_app.ml — SDL2 window, event loop, navigation, scrolling Usage: dune build # from hosts/native/ ./sx_native_app.exe /sx/ # browse sx.rose-ash.com home ./sx_native_app.exe /sx/(applications.(native-browser)) ./sx_native_app.exe demo/counter.sx # render local file Features: - Flexbox layout (row/column, gap, padding, alignment, grow) - Tailwind color palette (stone, violet, white) - Rounded corners, borders, shadows - Text rendering with font size/weight - Click navigation (links trigger refetch) - Scroll with mouse wheel - Window resize → re-layout - URL bar showing current path Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
157 lines
4.9 KiB
OCaml
157 lines
4.9 KiB
OCaml
(** Walk a positioned node tree and issue Cairo draw commands.
|
|
|
|
Handles backgrounds with rounded corners, borders, shadows,
|
|
and text rendering with proper font face/size/weight. *)
|
|
|
|
open Sx_native_types
|
|
open Sx_native_style
|
|
|
|
(* -- Rounded rectangle path -- *)
|
|
|
|
let rounded_rect (cr : Cairo.context) (x : float) (y : float) (w : float) (h : float) (r : float) =
|
|
let r = Float.min r (Float.min (w /. 2.) (h /. 2.)) in
|
|
if r <= 0. then
|
|
Cairo.rectangle cr x y ~w ~h
|
|
else begin
|
|
let pi = Float.pi in
|
|
Cairo.Path.sub cr;
|
|
Cairo.arc cr (x +. w -. r) (y +. r) ~r ~a1:(-.pi /. 2.) ~a2:0.;
|
|
Cairo.arc cr (x +. w -. r) (y +. h -. r) ~r ~a1:0. ~a2:(pi /. 2.);
|
|
Cairo.arc cr (x +. r) (y +. h -. r) ~r ~a1:(pi /. 2.) ~a2:pi;
|
|
Cairo.arc cr (x +. r) (y +. r) ~r ~a1:pi ~a2:(-.pi /. 2.);
|
|
Cairo.Path.close cr
|
|
end
|
|
|
|
(* -- Shadow painting -- *)
|
|
|
|
let paint_shadow (cr : Cairo.context) (b : box) (radius : float) (level : [`Sm | `Md]) =
|
|
let (offset, blur_passes, alpha) = match level with
|
|
| `Sm -> (1., 2, 0.04)
|
|
| `Md -> (2., 3, 0.05)
|
|
in
|
|
for i = 1 to blur_passes do
|
|
let spread = float_of_int i *. 2. in
|
|
Cairo.save cr;
|
|
Cairo.set_source_rgba cr 0. 0. 0. (alpha /. float_of_int i);
|
|
rounded_rect cr
|
|
(b.x -. spread)
|
|
(b.y +. offset -. spread +. float_of_int i)
|
|
(b.w +. spread *. 2.)
|
|
(b.h +. spread *. 2.)
|
|
(radius +. spread);
|
|
Cairo.fill cr;
|
|
Cairo.restore cr
|
|
done
|
|
|
|
(* -- Main paint function -- *)
|
|
|
|
(** Paint a positioned node tree to a Cairo context. *)
|
|
let rec paint (cr : Cairo.context) (node : node) : unit =
|
|
let s = node.style in
|
|
let b = node.box in
|
|
|
|
if s.display = `None then ()
|
|
else begin
|
|
(* Save state for potential clip *)
|
|
Cairo.save cr;
|
|
|
|
(* Shadow *)
|
|
(match s.shadow with
|
|
| `None -> ()
|
|
| `Sm -> paint_shadow cr b s.border_radius `Sm
|
|
| `Md -> paint_shadow cr b s.border_radius `Md);
|
|
|
|
(* Background *)
|
|
(match s.bg_color with
|
|
| Some c ->
|
|
Cairo.set_source_rgba cr c.r c.g c.b c.a;
|
|
rounded_rect cr b.x b.y b.w b.h s.border_radius;
|
|
Cairo.fill cr
|
|
| None -> ());
|
|
|
|
(* Border *)
|
|
if s.border_width > 0. then begin
|
|
let bc = match s.border_color with Some c -> c | None -> stone_800 in
|
|
Cairo.set_source_rgba cr bc.r bc.g bc.b bc.a;
|
|
Cairo.set_line_width cr s.border_width;
|
|
rounded_rect cr
|
|
(b.x +. s.border_width /. 2.)
|
|
(b.y +. s.border_width /. 2.)
|
|
(b.w -. s.border_width)
|
|
(b.h -. s.border_width)
|
|
(Float.max 0. (s.border_radius -. s.border_width /. 2.));
|
|
Cairo.stroke cr
|
|
end;
|
|
|
|
(* Clip for overflow *)
|
|
if s.overflow_hidden then begin
|
|
rounded_rect cr b.x b.y b.w b.h s.border_radius;
|
|
Cairo.clip cr
|
|
end;
|
|
|
|
(* Text *)
|
|
(match node.text with
|
|
| Some txt when String.length txt > 0 ->
|
|
let font_name = match s.font_family with `Sans -> "sans-serif" | `Mono -> "monospace" in
|
|
let weight = match s.font_weight with `Normal -> Cairo.Normal | `Bold -> Cairo.Bold in
|
|
let slant = match s.font_style with `Normal -> Cairo.Upright | `Italic -> Cairo.Italic in
|
|
Cairo.select_font_face cr ~slant ~weight font_name;
|
|
Cairo.set_font_size cr s.font_size;
|
|
let fe = Cairo.font_extents cr in
|
|
Cairo.set_source_rgba cr s.text_color.r s.text_color.g s.text_color.b s.text_color.a;
|
|
Cairo.move_to cr (b.x +. s.padding.left) (b.y +. s.padding.top +. fe.ascent);
|
|
Cairo.show_text cr txt
|
|
| _ -> ());
|
|
|
|
(* Children *)
|
|
List.iter (paint cr) node.children;
|
|
|
|
Cairo.restore cr
|
|
end
|
|
|
|
(** Paint a horizontal URL bar at the top of the window. *)
|
|
let paint_url_bar (cr : Cairo.context) (url : string) (width : float) : float =
|
|
let bar_height = 36. in
|
|
(* Bar background *)
|
|
Cairo.set_source_rgba cr stone_100.r stone_100.g stone_100.b 1.0;
|
|
Cairo.rectangle cr 0. 0. ~w:width ~h:bar_height;
|
|
Cairo.fill cr;
|
|
|
|
(* Bottom border *)
|
|
Cairo.set_source_rgba cr stone_200.r stone_200.g stone_200.b 1.0;
|
|
Cairo.set_line_width cr 1.;
|
|
Cairo.move_to cr 0. bar_height;
|
|
Cairo.line_to cr width bar_height;
|
|
Cairo.stroke cr;
|
|
|
|
(* URL text *)
|
|
Cairo.select_font_face cr ~slant:Cairo.Upright ~weight:Cairo.Normal "monospace";
|
|
Cairo.set_font_size cr 13.;
|
|
Cairo.set_source_rgba cr stone_600.r stone_600.g stone_600.b 1.0;
|
|
Cairo.move_to cr 12. 23.;
|
|
Cairo.show_text cr url;
|
|
|
|
bar_height
|
|
|
|
(** Paint the entire scene: clear, URL bar, then content. *)
|
|
let paint_scene (cr : Cairo.context) (root : node) (url : string) (width : float) (height : float) : unit =
|
|
(* Clear to white *)
|
|
Cairo.set_source_rgba cr 1. 1. 1. 1.;
|
|
Cairo.rectangle cr 0. 0. ~w:width ~h:height;
|
|
Cairo.fill cr;
|
|
|
|
(* URL bar *)
|
|
let bar_h = paint_url_bar cr url width in
|
|
|
|
(* Content area *)
|
|
Cairo.save cr;
|
|
Cairo.rectangle cr 0. bar_h ~w:width ~h:(height -. bar_h);
|
|
Cairo.clip cr;
|
|
|
|
(* Offset layout by bar height *)
|
|
root.box.y <- root.box.y +. bar_h;
|
|
paint cr root;
|
|
root.box.y <- root.box.y -. bar_h; (* restore for hit testing *)
|
|
|
|
Cairo.restore cr
|