From 8c4b3fd0820251ec142925f8a31e16d839b027bd Mon Sep 17 00:00:00 2001 From: disconcision Date: Wed, 1 Jan 2025 22:32:29 -0500 Subject: [PATCH 01/13] card projector init --- src/haz3lcore/tiles/Base.re | 1 + src/haz3lcore/zipper/Projector.re | 3 + .../zipper/action/ProjectorPerform.re | 6 +- src/haz3lcore/zipper/projectors/CardProj.re | 219 ++++++++++++++++++ src/haz3lweb/app/common/ProjectorView.re | 2 + src/haz3lweb/app/inspector/ProjectorPanel.re | 3 + src/haz3lweb/www/img/cards-pixel.png | Bin 0 -> 15856 bytes src/haz3lweb/www/img/cards.png | Bin 0 -> 36095 bytes src/haz3lweb/www/style/projectors/base.css | 38 +++ 9 files changed, 270 insertions(+), 2 deletions(-) create mode 100644 src/haz3lcore/zipper/projectors/CardProj.re create mode 100644 src/haz3lweb/www/img/cards-pixel.png create mode 100644 src/haz3lweb/www/img/cards.png diff --git a/src/haz3lcore/tiles/Base.re b/src/haz3lcore/tiles/Base.re index cfc79efa8f..8501ec9c6b 100644 --- a/src/haz3lcore/tiles/Base.re +++ b/src/haz3lcore/tiles/Base.re @@ -11,6 +11,7 @@ type kind = | Checkbox | Slider | SliderF + | Card | TextArea; [@deriving (show({with_path: false}), sexp, yojson)] diff --git a/src/haz3lcore/zipper/Projector.re b/src/haz3lcore/zipper/Projector.re index c80a10acc5..4dd1716292 100644 --- a/src/haz3lcore/zipper/Projector.re +++ b/src/haz3lcore/zipper/Projector.re @@ -13,6 +13,9 @@ let to_module = (kind: Base.kind): (module Cooked) => | SliderF => (module Cook(SliderFProj.M)) | Checkbox => (module Cook(CheckboxProj.M)) | TextArea => (module Cook(TextAreaProj.M)) + | Card => + print_endline("Baking Card projector"); + (module Cook(CardProj.M)); }; /* Currently projection is limited to convex pieces */ diff --git a/src/haz3lcore/zipper/action/ProjectorPerform.re b/src/haz3lcore/zipper/action/ProjectorPerform.re index 647802269a..acd8c38116 100644 --- a/src/haz3lcore/zipper/action/ProjectorPerform.re +++ b/src/haz3lcore/zipper/action/ProjectorPerform.re @@ -67,12 +67,14 @@ let go = : result(ZipperBase.t, Action.Failure.t) => { switch (a) { | SetIndicated(p) => + print_endline("SetIndicated: kind: " ++ (p |> Base.show_kind)); switch (Indicated.for_index(z)) { | None => Error(Cant_project) | Some((piece, d, rel)) => Ok(move_out_of_piece(d, rel, z) |> Update.add(p, Piece.id(piece))) - } + }; | ToggleIndicated(p) => + print_endline("ToggleIndicated: kind: " ++ (p |> Base.show_kind)); switch (Indicated.for_index(z)) { | None => Error(Cant_project) | Some((piece, d, rel)) => @@ -80,7 +82,7 @@ let go = move_out_of_piece(d, rel, z) |> Update.add_or_remove(p, Piece.id(piece)), ) - } + }; | Remove(id) => Ok(Update.remove(id, z)) | SetSyntax(id, syntax) => /* Note we update piece id to keep in sync with projector id; diff --git a/src/haz3lcore/zipper/projectors/CardProj.re b/src/haz3lcore/zipper/projectors/CardProj.re new file mode 100644 index 0000000000..671ccbb7b1 --- /dev/null +++ b/src/haz3lcore/zipper/projectors/CardProj.re @@ -0,0 +1,219 @@ +open Util; +open Virtual_dom.Vdom; +open ProjectorBase; + +[@deriving (show({with_path: false}), sexp, yojson)] +type suit = + | Unknown + | Hearts + | Diamonds + | Clubs + | Spades; + +[@deriving (show({with_path: false}), sexp, yojson)] +type rank = + | Unknown + | Ace + | Two + | Three + | Four + | Five + | Six + | Seven + | Eight + | Nine + | Ten + | Jack + | Queen + | King; + +[@deriving (show({with_path: false}), sexp, yojson)] +type card = (suit, rank); + +let string_to_suit = (str): suit => + switch (str |> Sexplib.Sexp.of_string |> suit_of_sexp) { + | exception _ => Unknown + | s => s + }; + +// Helper to convert string to rank +let string_to_rank = (str): rank => + switch (str |> Sexplib.Sexp.of_string |> rank_of_sexp) { + | exception _ => Unknown + | r => r + }; + +let piece_to_card = (piece: Piece.t): option(card) => { + // Helper to convert string to suit (used sexp_to_suit) + // Look for constructor application pattern in segment + print_endline("piece_to_card: " ++ (piece |> Piece.show)); + switch (piece) { + | Tile({ + label: ["(", ")"], + children: + [ + [ + Tile({label: suit_label, _}), + Tile({label: [","], _}), + Tile({label: rank_label, _}), + ], + ], + _, + }) + | Tile({ + label: ["(", ")"], + children: + [ + [ + Tile({label: suit_label, _}), + Tile({label: [","], _}), + Secondary(_), + Tile({label: rank_label, _}), + ], + ], + _, + }) => + let suit = + switch (suit_label) { + | [suit_name] => string_to_suit(suit_name) + | _ => Unknown + }; + let rank = + switch (rank_label) { + | [rank_name] => string_to_rank(rank_name) + | _ => Unknown + }; + Some((suit, rank)); + | _ => None + }; +}; + +let suit_to_string = suit => suit |> sexp_of_suit |> Sexplib.Sexp.to_string; + +let rank_to_string = rank => rank |> sexp_of_rank |> Sexplib.Sexp.to_string; + +let card_to_piece = ((suit, rank): card): Piece.t => { + // Create a tuple piece with the suit and rank + let mk_text = (str): Piece.t => + Tile({ + id: Id.mk(), + label: [str], + mold: Mold.mk_op(Sort.Exp, []), + shards: [0], + children: [], + }); + + let mk_tuple = (children): Piece.t => + Tile({ + id: Id.mk(), + label: ["(", ")"], + mold: Mold.mk_op(Sort.Exp, [Exp]), + shards: [0], + children: [children], + }); + + mk_tuple([ + mk_text(suit_to_string(suit)), + mk_text(","), + mk_text(rank_to_string(rank)), + ]); +}; + +let suit_to_int = (suit: suit): int => + switch (suit) { + | Hearts => 0 + | Clubs => 1 + | Diamonds => 2 + | Spades => 3 + | Unknown => 0 + }; + +let rank_to_int = (rank: rank): int => + switch (rank) { + | Two => 1 + | Three => 2 + | Four => 3 + | Five => 4 + | Six => 5 + | Seven => 6 + | Eight => 7 + | Nine => 8 + | Ten => 9 + | Jack => 10 + | Queen => 11 + | King => 12 + | Ace => 13 + | Unknown => 0 + }; + +/* card images are stored in a single pixel sheet. this + * returns two ints representing the pixel offset of cards + * declare constants for W and H of each card; the image + has four rows (hears, clubs, diamonds, spades) and 14 + columns (first is misc, then 2 thru 10, the J Q K A) */ +let card_to_offset = (card: card): (int, int) => { + let width = 35; + let height = 47; + let (suit, rank) = card; + let row = suit |> suit_to_int; + let col = rank |> rank_to_int; + print_endline( + "row/col: " ++ string_of_int(row) ++ "/" ++ string_of_int(col), + ); + (col * width, row * height); +}; + +let view_card = (card: card): Node.t => { + let (offset_x, offset_y) = card_to_offset(card); + Node.div( + ~attrs=[ + Attr.class_("card-sprite"), + Attr.style( + Css_gen.create( + ~field="background-position", + ~value= + string_of_int(- offset_x) + ++ "px " + ++ string_of_int(- offset_y) + ++ "px", + ), + ), + ], + [], + ); +}; + +let put = card_to_piece; + +let get_opt = piece_to_card; + +let get = (piece: Piece.t): card => + switch (get_opt(piece)) { + | None => failwith("ERROR: Card: not integer literal") + | Some(card) => card + }; + +module M: Projector = { + [@deriving (show({with_path: false}), sexp, yojson)] + type model = unit; + [@deriving (show({with_path: false}), sexp, yojson)] + type action = unit; + let init = (); + let can_project = p => get_opt(p) != None; + let can_focus = false; + let dynamics = false; + let placeholder = (_, _) => Inline(4); + let update = (model, _) => model; + let view = + ( + _, + ~info, + ~local as _, + ~parent as _: external_action => Ui_effect.t(unit), + ~utility as _, + ) => { + let (suit, rank) = get(info.syntax); + view_card((suit, rank)); + }; + let focus = _ => (); +}; diff --git a/src/haz3lweb/app/common/ProjectorView.re b/src/haz3lweb/app/common/ProjectorView.re index 599df09c09..584c8586db 100644 --- a/src/haz3lweb/app/common/ProjectorView.re +++ b/src/haz3lweb/app/common/ProjectorView.re @@ -19,6 +19,7 @@ let name = (p: Base.kind): string => | Slider => "slider" | SliderF => "sliderf" | TextArea => "text" + | Card => "card" }; /* This must be updated and kept 1-to-1 with the above @@ -33,6 +34,7 @@ let of_name = (p: string): Base.kind => | "slider" => Slider | "sliderf" => SliderF | "text" => TextArea + | "card" => Card | _ => failwith("Unknown projector kind") }; diff --git a/src/haz3lweb/app/inspector/ProjectorPanel.re b/src/haz3lweb/app/inspector/ProjectorPanel.re index 7ffb3d29d7..85cf7ec96a 100644 --- a/src/haz3lweb/app/inspector/ProjectorPanel.re +++ b/src/haz3lweb/app/inspector/ProjectorPanel.re @@ -24,6 +24,9 @@ let applicable_projectors: option(Info.t) => list(Base.kind) = | Pat(Float) => [SliderF] | Exp(String) | Pat(String) => [TextArea] + //TODP(andrew): more specific + | Exp(Parens) + | Pat(Parens) => [Card] | _ => [] } ) diff --git a/src/haz3lweb/www/img/cards-pixel.png b/src/haz3lweb/www/img/cards-pixel.png new file mode 100644 index 0000000000000000000000000000000000000000..bd933387f37bf050ca4522a869caf24e0e9c4c6c GIT binary patch literal 15856 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�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 Q!VOYa(Ry5}WFGkc0Qr&H82|tP literal 0 HcmV?d00001 diff --git a/src/haz3lweb/www/img/cards.png b/src/haz3lweb/www/img/cards.png new file mode 100644 index 0000000000000000000000000000000000000000..54f8bd59623afa4a46bf1e61135559a1b1828ee6 GIT binary patch literal 36095 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 svg, +.projector.card.indicated > svg { + display: none; +} + + From 58775703903470e4b0f5874fb4dacfad7355a190 Mon Sep 17 00:00:00 2001 From: disconcision Date: Wed, 1 Jan 2025 23:32:35 -0500 Subject: [PATCH 02/13] improved card style and cleanup --- src/haz3lcore/zipper/projectors/CardProj.re | 226 ++++++++++---------- src/haz3lweb/www/style/projectors/base.css | 40 +--- src/haz3lweb/www/style/projectors/cards.css | 48 +++++ 3 files changed, 157 insertions(+), 157 deletions(-) create mode 100644 src/haz3lweb/www/style/projectors/cards.css diff --git a/src/haz3lcore/zipper/projectors/CardProj.re b/src/haz3lcore/zipper/projectors/CardProj.re index 671ccbb7b1..be6a7c5494 100644 --- a/src/haz3lcore/zipper/projectors/CardProj.re +++ b/src/haz3lcore/zipper/projectors/CardProj.re @@ -4,7 +4,7 @@ open ProjectorBase; [@deriving (show({with_path: false}), sexp, yojson)] type suit = - | Unknown + | Unknown(Piece.t) | Hearts | Diamonds | Clubs @@ -12,7 +12,7 @@ type suit = [@deriving (show({with_path: false}), sexp, yojson)] type rank = - | Unknown + | Unknown(Piece.t) | Ace | Two | Three @@ -30,70 +30,32 @@ type rank = [@deriving (show({with_path: false}), sexp, yojson)] type card = (suit, rank); -let string_to_suit = (str): suit => - switch (str |> Sexplib.Sexp.of_string |> suit_of_sexp) { - | exception _ => Unknown - | s => s - }; - -// Helper to convert string to rank -let string_to_rank = (str): rank => - switch (str |> Sexplib.Sexp.of_string |> rank_of_sexp) { - | exception _ => Unknown - | r => r - }; - -let piece_to_card = (piece: Piece.t): option(card) => { - // Helper to convert string to suit (used sexp_to_suit) - // Look for constructor application pattern in segment - print_endline("piece_to_card: " ++ (piece |> Piece.show)); - switch (piece) { - | Tile({ - label: ["(", ")"], - children: - [ - [ - Tile({label: suit_label, _}), - Tile({label: [","], _}), - Tile({label: rank_label, _}), - ], - ], - _, - }) - | Tile({ - label: ["(", ")"], - children: - [ - [ - Tile({label: suit_label, _}), - Tile({label: [","], _}), - Secondary(_), - Tile({label: rank_label, _}), - ], - ], - _, - }) => - let suit = - switch (suit_label) { - | [suit_name] => string_to_suit(suit_name) - | _ => Unknown - }; - let rank = - switch (rank_label) { - | [rank_name] => string_to_rank(rank_name) - | _ => Unknown - }; - Some((suit, rank)); - | _ => None - }; -}; - -let suit_to_string = suit => suit |> sexp_of_suit |> Sexplib.Sexp.to_string; - -let rank_to_string = rank => rank |> sexp_of_rank |> Sexplib.Sexp.to_string; +[@deriving (show({with_path: false}), sexp, yojson)] +type syntax = + | Card(card) + | Hand(list(card)); + +module Syntax = { + let suit_of_piece = (p: Piece.t): suit => + switch (p) { + | Tile({label: [str], _}) => + switch (str |> Sexplib.Sexp.of_string |> suit_of_sexp) { + | exception _ => Unknown(p) + | s => s + } + | _ => Unknown(p) + }; + + let rank_of_piece = (p: Piece.t): rank => + switch (p) { + | Tile({label: [str], _}) => + switch (str |> Sexplib.Sexp.of_string |> rank_of_sexp) { + | exception _ => Unknown(p) + | r => r + } + | _ => Unknown(p) + }; -let card_to_piece = ((suit, rank): card): Piece.t => { - // Create a tuple piece with the suit and rank let mk_text = (str): Piece.t => Tile({ id: Id.mk(), @@ -103,6 +65,18 @@ let card_to_piece = ((suit, rank): card): Piece.t => { children: [], }); + let piece_of_suit = (suit: suit): Piece.t => + switch (suit) { + | Unknown(p) => p + | _ => suit |> sexp_of_suit |> Sexplib.Sexp.to_string |> mk_text + }; + + let piece_of_rank = (rank: rank) => + switch (rank) { + | Unknown(p) => p + | _ => rank |> sexp_of_rank |> Sexplib.Sexp.to_string |> mk_text + }; + let mk_tuple = (children): Piece.t => Tile({ id: Id.mk(), @@ -112,11 +86,43 @@ let card_to_piece = ((suit, rank): card): Piece.t => { children: [children], }); - mk_tuple([ - mk_text(suit_to_string(suit)), - mk_text(","), - mk_text(rank_to_string(rank)), - ]); + let piece_to_card = (piece: Piece.t): option(card) => { + //TODO: generalize this or use Term + switch (piece) { + | Tile({ + label: ["(", ")"], + children: [[left_child, Tile({label: [","], _}), right_child]], + _, + }) + | Tile({ + label: ["(", ")"], + children: + [ + [left_child, Tile({label: [","], _}), Secondary(_), right_child], + ], + _, + }) => + Some((suit_of_piece(left_child), rank_of_piece(right_child))) + | _ => None + }; + }; + + let card_to_piece = ((suit, rank): card): Piece.t => + mk_tuple([ + piece_of_suit(suit), + Piece.mk_tile(Form.get("comma_exp"), []), + piece_of_rank(rank), + ]); + + let put = card_to_piece; + + let get_opt = piece_to_card; + + let get = (piece: Piece.t): card => + switch (get_opt(piece)) { + | None => failwith("ERROR: Card: not integer literal") + | Some(card) => card + }; }; let suit_to_int = (suit: suit): int => @@ -125,7 +131,7 @@ let suit_to_int = (suit: suit): int => | Clubs => 1 | Diamonds => 2 | Spades => 3 - | Unknown => 0 + | Unknown(_) => 0 }; let rank_to_int = (rank: rank): int => @@ -143,55 +149,41 @@ let rank_to_int = (rank: rank): int => | Queen => 11 | King => 12 | Ace => 13 - | Unknown => 0 + | Unknown(_) => 0 }; -/* card images are stored in a single pixel sheet. this - * returns two ints representing the pixel offset of cards - * declare constants for W and H of each card; the image - has four rows (hears, clubs, diamonds, spades) and 14 - columns (first is misc, then 2 thru 10, the J Q K A) */ -let card_to_offset = (card: card): (int, int) => { - let width = 35; - let height = 47; - let (suit, rank) = card; - let row = suit |> suit_to_int; - let col = rank |> rank_to_int; - print_endline( - "row/col: " ++ string_of_int(row) ++ "/" ++ string_of_int(col), - ); - (col * width, row * height); -}; +module Card = { + /* Card images are stored in a spritesheet. The sheet image + * has four rows (hearts, clubs, diamonds, spades) and 14 + * columns (first is misc, then 2-10, then J Q K A) */ -let view_card = (card: card): Node.t => { - let (offset_x, offset_y) = card_to_offset(card); - Node.div( - ~attrs=[ - Attr.class_("card-sprite"), - Attr.style( - Css_gen.create( - ~field="background-position", - ~value= - string_of_int(- offset_x) - ++ "px " - ++ string_of_int(- offset_y) - ++ "px", - ), - ), - ], - [], - ); -}; + let width = 35; /* Width of each card in pixels */ + let height = 47; /* Height of each card in pixels */ -let put = card_to_piece; + let card_to_offset = ((suit, rank): card): (int, int) => ( + rank_to_int(rank) * width, + suit_to_int(suit) * height, + ); -let get_opt = piece_to_card; + let background_offset = (card: card): Css_gen.t => { + let (offset_x, offset_y) = card_to_offset(card); + Css_gen.create( + ~field="background-position", + ~value=Printf.sprintf("%dpx %dpx", - offset_x, - offset_y), + ); + }; -let get = (piece: Piece.t): card => - switch (get_opt(piece)) { - | None => failwith("ERROR: Card: not integer literal") - | Some(card) => card + let view = (info: info): Node.t => { + let card = Syntax.get(info.syntax); + Node.div( + ~attrs=[ + Attr.class_("card-sprite"), + Attr.style(background_offset(card)), + ], + [], + ); }; +}; module M: Projector = { [@deriving (show({with_path: false}), sexp, yojson)] @@ -199,7 +191,7 @@ module M: Projector = { [@deriving (show({with_path: false}), sexp, yojson)] type action = unit; let init = (); - let can_project = p => get_opt(p) != None; + let can_project = p => Syntax.get_opt(p) != None; let can_focus = false; let dynamics = false; let placeholder = (_, _) => Inline(4); @@ -211,9 +203,7 @@ module M: Projector = { ~local as _, ~parent as _: external_action => Ui_effect.t(unit), ~utility as _, - ) => { - let (suit, rank) = get(info.syntax); - view_card((suit, rank)); - }; + ) => + Card.view(info); let focus = _ => (); }; diff --git a/src/haz3lweb/www/style/projectors/base.css b/src/haz3lweb/www/style/projectors/base.css index 93540c21ae..6c87d49309 100644 --- a/src/haz3lweb/www/style/projectors/base.css +++ b/src/haz3lweb/www/style/projectors/base.css @@ -2,6 +2,7 @@ @import "panel.css"; @import "probe.css"; +@import "cards.css"; /* Turn off caret when a block projector is focused */ #caret:has(~ .projectors .projector.block *:focus) { @@ -182,42 +183,3 @@ color: var(--BLACK); background-color: var(--shard-selected); } - - -/* CARD SPRITE */ - -/* Turn off caret when a block projector is focused */ -#caret:has(~ .projectors .projector.card.indicated), -.indication:has(~ .projectors .projector.card.indicated) { - display: none; -} - -.projector.card .card-sprite { - cursor: pointer; - translate: 0px 13px; - width: 35px; - height: 47px; - background-image: url('../../img/cards-pixel.png'); - /* background-size: calc(2136px* 0.3) calc(752px* 0.3); */ - /* scale: 0.3; */ - filter: drop-shadow(-1px 0px 0px black) drop-shadow(0px -1px 0px black) drop-shadow(1px 0px 0px black) drop-shadow(0px 1px 0px black); -} - -.projector.card.indicated.Left .card-sprite { - filter: drop-shadow(-1.5px 0px 0px red) drop-shadow(1px 0px 0px black) drop-shadow(0px 1px 0px black) drop-shadow(0px -1px 0px black); -} - -.projector.card.indicated.Right .card-sprite { - filter: drop-shadow(1.5px 0px 0px red) drop-shadow(-1px 0px 0px black) drop-shadow(0px 1px 0px black) drop-shadow(0px -1px 0px black); -} - -.projector.card.selected { - filter: brightness(0.72) sepia(1) hue-rotate(14deg) saturate(1.7); -} - -.projector.card > svg, -.projector.card.indicated > svg { - display: none; -} - - diff --git a/src/haz3lweb/www/style/projectors/cards.css b/src/haz3lweb/www/style/projectors/cards.css new file mode 100644 index 0000000000..fc14a70c56 --- /dev/null +++ b/src/haz3lweb/www/style/projectors/cards.css @@ -0,0 +1,48 @@ +/* CARD SPRITE */ + +/* Turn off caret when a block projector is focused */ +#caret:has(~ .projectors .projector.card.indicated), +.indication:has(~ .projectors .projector.card.indicated) { + display: none; +} + +.projector.card .card-sprite { + cursor: pointer; + translate: 0px 13px; + width: 35px; + height: 47px; + image-rendering: pixelated; + background-image: url('../../img/cards-pixel.png'); + filter: drop-shadow(-1px 0px 0px black) drop-shadow(0px -1px 0px black) drop-shadow(1px 0px 0px black) drop-shadow(0px 1px 0px black); +} + +@keyframes blink-shadow { + 0% { filter: drop-shadow(-2px 0px 0px red) drop-shadow(1px 0px 0px black) drop-shadow(0px 1px 0px black) drop-shadow(0px -1px 0px black); } + 50% { filter: drop-shadow(-2px 0px 0px red) drop-shadow(1px 0px 0px black) drop-shadow(0px 1px 0px black) drop-shadow(0px -1px 0px black); } + 51% { filter: drop-shadow(-1px 0px 0px black) drop-shadow(1px 0px 0px black) drop-shadow(0px 1px 0px black) drop-shadow(0px -1px 0px black); } + 100% { filter: drop-shadow(-1px 0px 0px black) drop-shadow(1px 0px 0px black) drop-shadow(0px 1px 0px black) drop-shadow(0px -1px 0px black); } +} + +@keyframes blink-shadow-right { + 0% { filter: drop-shadow(2px 0px 0px red) drop-shadow(-1px 0px 0px black) drop-shadow(0px 1px 0px black) drop-shadow(0px -1px 0px black); } + 50% { filter: drop-shadow(2px 0px 0px red) drop-shadow(-1px 0px 0px black) drop-shadow(0px 1px 0px black) drop-shadow(0px -1px 0px black); } + 51% { filter: drop-shadow(1px 0px 0px black) drop-shadow(-1px 0px 0px black) drop-shadow(0px 1px 0px black) drop-shadow(0px -1px 0px black); } + 100% { filter: drop-shadow(1px 0px 0px black) drop-shadow(-1px 0px 0px black) drop-shadow(0px 1px 0px black) drop-shadow(0px -1px 0px black); } +} + +.projector.card.indicated.Left .card-sprite { + animation: blink-shadow 1s infinite; +} + +.projector.card.indicated.Right .card-sprite { + animation: blink-shadow-right 1s infinite; +} + +.projector.card.selected { + filter: brightness(0.72) sepia(1) hue-rotate(12deg) saturate(1.5); +} + +.projector.card > svg, +.projector.card.indicated > svg { + display: none; +} \ No newline at end of file From 0c2022bfd3541c1f6260f8e0230d84213382488e Mon Sep 17 00:00:00 2001 From: disconcision Date: Thu, 2 Jan 2025 00:28:17 -0500 Subject: [PATCH 03/13] card projector now supports hands --- src/haz3lcore/zipper/Projector.re | 4 +- .../zipper/action/ProjectorPerform.re | 3 +- src/haz3lcore/zipper/projectors/CardProj.re | 142 +++++++++++++----- src/haz3lweb/app/inspector/ProjectorPanel.re | 6 +- src/haz3lweb/www/style/projectors/cards.css | 22 ++- 5 files changed, 126 insertions(+), 51 deletions(-) diff --git a/src/haz3lcore/zipper/Projector.re b/src/haz3lcore/zipper/Projector.re index 4dd1716292..90b60d2066 100644 --- a/src/haz3lcore/zipper/Projector.re +++ b/src/haz3lcore/zipper/Projector.re @@ -13,9 +13,7 @@ let to_module = (kind: Base.kind): (module Cooked) => | SliderF => (module Cook(SliderFProj.M)) | Checkbox => (module Cook(CheckboxProj.M)) | TextArea => (module Cook(TextAreaProj.M)) - | Card => - print_endline("Baking Card projector"); - (module Cook(CardProj.M)); + | Card => (module Cook(CardProj.M)) }; /* Currently projection is limited to convex pieces */ diff --git a/src/haz3lcore/zipper/action/ProjectorPerform.re b/src/haz3lcore/zipper/action/ProjectorPerform.re index acd8c38116..943996b12a 100644 --- a/src/haz3lcore/zipper/action/ProjectorPerform.re +++ b/src/haz3lcore/zipper/action/ProjectorPerform.re @@ -67,12 +67,11 @@ let go = : result(ZipperBase.t, Action.Failure.t) => { switch (a) { | SetIndicated(p) => - print_endline("SetIndicated: kind: " ++ (p |> Base.show_kind)); switch (Indicated.for_index(z)) { | None => Error(Cant_project) | Some((piece, d, rel)) => Ok(move_out_of_piece(d, rel, z) |> Update.add(p, Piece.id(piece))) - }; + } | ToggleIndicated(p) => print_endline("ToggleIndicated: kind: " ++ (p |> Base.show_kind)); switch (Indicated.for_index(z)) { diff --git a/src/haz3lcore/zipper/projectors/CardProj.re b/src/haz3lcore/zipper/projectors/CardProj.re index be6a7c5494..08caa994c9 100644 --- a/src/haz3lcore/zipper/projectors/CardProj.re +++ b/src/haz3lcore/zipper/projectors/CardProj.re @@ -30,10 +30,13 @@ type rank = [@deriving (show({with_path: false}), sexp, yojson)] type card = (suit, rank); +[@deriving (show({with_path: false}), sexp, yojson)] +type hand = list(card); + [@deriving (show({with_path: false}), sexp, yojson)] type syntax = | Card(card) - | Hand(list(card)); + | Hand(hand); module Syntax = { let suit_of_piece = (p: Piece.t): suit => @@ -56,6 +59,46 @@ module Syntax = { | _ => Unknown(p) }; + let rm_secondary = (segment: Segment.t): Segment.t => + List.filter(p => !Piece.is_secondary(p), segment); + + let piece_to_card = (piece: Piece.t): option(card) => + switch (piece) { + | Tile({label: ["(", ")"], children: [segment], _}) => + switch (rm_secondary(segment)) { + | [left_child, Tile({label: [","], _}), right_child] => + Some((suit_of_piece(left_child), rank_of_piece(right_child))) + | _ => None + } + | _ => None + }; + + let piece_to_hand = (piece: Piece.t): option(hand) => { + switch (piece) { + | Tile({label: ["[", "]"], children: [segment], _}) => + segment |> rm_secondary |> List.filter_map(piece_to_card) |> Option.some + | _ => None + }; + }; + + let piece_to_syntax = (piece: Piece.t): option(syntax) => + switch (piece_to_hand(piece)) { + | Some(hand) => Some(Hand(hand)) + | None => + open OptUtil.Syntax; + let+ card = piece_to_card(piece); + Card(card); + }; + + let mk_tuple = (children): Piece.t => + Tile({ + id: Id.mk(), + label: ["(", ")"], + mold: Mold.mk_op(Sort.Exp, [Exp]), + shards: [0], + children: [children], + }); + let mk_text = (str): Piece.t => Tile({ id: Id.mk(), @@ -77,36 +120,6 @@ module Syntax = { | _ => rank |> sexp_of_rank |> Sexplib.Sexp.to_string |> mk_text }; - let mk_tuple = (children): Piece.t => - Tile({ - id: Id.mk(), - label: ["(", ")"], - mold: Mold.mk_op(Sort.Exp, [Exp]), - shards: [0], - children: [children], - }); - - let piece_to_card = (piece: Piece.t): option(card) => { - //TODO: generalize this or use Term - switch (piece) { - | Tile({ - label: ["(", ")"], - children: [[left_child, Tile({label: [","], _}), right_child]], - _, - }) - | Tile({ - label: ["(", ")"], - children: - [ - [left_child, Tile({label: [","], _}), Secondary(_), right_child], - ], - _, - }) => - Some((suit_of_piece(left_child), rank_of_piece(right_child))) - | _ => None - }; - }; - let card_to_piece = ((suit, rank): card): Piece.t => mk_tuple([ piece_of_suit(suit), @@ -114,14 +127,36 @@ module Syntax = { piece_of_rank(rank), ]); - let put = card_to_piece; + let hand_to_piece = (hand: hand): Piece.t => + mk_tuple(List.map(card_to_piece, hand)); + + let syntax_to_piece = (syntax: syntax): Piece.t => + switch (syntax) { + | Card(card) => card_to_piece(card) + | Hand(hand) => hand_to_piece(hand) + }; + + let put = syntax_to_piece; - let get_opt = piece_to_card; + let get_opt = piece_to_syntax; - let get = (piece: Piece.t): card => + let get = (piece: Piece.t): syntax => switch (get_opt(piece)) { - | None => failwith("ERROR: Card: not integer literal") - | Some(card) => card + | None => failwith("ERROR: Card: Not card or hand") + | Some(syntax) => syntax + }; + + let width_of_syntax = (syntax: syntax): int => + switch (syntax) { + | Card(_) => 1 + | Hand(hand) => List.length(hand) + }; + + let width_of_piece = (piece: Piece.t): int => + switch (piece_to_syntax(piece)) { + | None => 0 + | Some(Card(_)) => 4 + | Some(Hand(hand)) => 4 + List.length(hand) / 2 }; }; @@ -173,8 +208,7 @@ module Card = { ); }; - let view = (info: info): Node.t => { - let card = Syntax.get(info.syntax); + let view = (card: card): Node.t => Node.div( ~attrs=[ Attr.class_("card-sprite"), @@ -182,6 +216,28 @@ module Card = { ], [], ); +}; + +module Hand = { + // a card, but each subsequent card should be absoluted positioned 20px to the right of the last and higher in z-index: + let card_wrapper = (index: int, card: card): Node.t => + Node.div( + ~attrs=[ + Attr.class_("card-wrapper"), + Attr.create( + "style", + Printf.sprintf( + "position: absolute; left: %dpx; z-index: %d;", + index * 8, + 100 + index, + ), + ), + ], + [Card.view(card)], + ); + + let view = (hand: hand): Node.t => { + Node.div(~attrs=[Attr.class_("hand")], List.mapi(card_wrapper, hand)); }; }; @@ -194,7 +250,7 @@ module M: Projector = { let can_project = p => Syntax.get_opt(p) != None; let can_focus = false; let dynamics = false; - let placeholder = (_, _) => Inline(4); + let placeholder = (_, info) => Inline(Syntax.width_of_piece(info.syntax)); let update = (model, _) => model; let view = ( @@ -203,7 +259,11 @@ module M: Projector = { ~local as _, ~parent as _: external_action => Ui_effect.t(unit), ~utility as _, - ) => - Card.view(info); + ) => { + switch (Syntax.get(info.syntax)) { + | Card(card) => Card.view(card) + | Hand(hand) => Hand.view(hand) + }; + }; let focus = _ => (); }; diff --git a/src/haz3lweb/app/inspector/ProjectorPanel.re b/src/haz3lweb/app/inspector/ProjectorPanel.re index 85cf7ec96a..c75822840f 100644 --- a/src/haz3lweb/app/inspector/ProjectorPanel.re +++ b/src/haz3lweb/app/inspector/ProjectorPanel.re @@ -24,9 +24,9 @@ let applicable_projectors: option(Info.t) => list(Base.kind) = | Pat(Float) => [SliderF] | Exp(String) | Pat(String) => [TextArea] - //TODP(andrew): more specific - | Exp(Parens) - | Pat(Parens) => [Card] + //TODO(andrew): more specific + | Exp(Parens | ListLit) + | Pat(Parens | ListLit) => [Card] | _ => [] } ) diff --git a/src/haz3lweb/www/style/projectors/cards.css b/src/haz3lweb/www/style/projectors/cards.css index fc14a70c56..eabc9e8d11 100644 --- a/src/haz3lweb/www/style/projectors/cards.css +++ b/src/haz3lweb/www/style/projectors/cards.css @@ -30,7 +30,8 @@ 100% { filter: drop-shadow(1px 0px 0px black) drop-shadow(-1px 0px 0px black) drop-shadow(0px 1px 0px black) drop-shadow(0px -1px 0px black); } } -.projector.card.indicated.Left .card-sprite { +.projector.card.indicated.Left > .card-sprite, +.projector.card.indicated.Left .hand > *:first-child > .card-sprite { animation: blink-shadow 1s infinite; } @@ -45,4 +46,21 @@ .projector.card > svg, .projector.card.indicated > svg { display: none; -} \ No newline at end of file +} + +.projector.card .hand { + display: flex; + flex-direction: row; + gap: 2px; + width: 100%; + height: 100%; + translate: 0px -10px; +} + +.projector.card .hand .card-sprite { + transition: translate 0.1s ease-in-out; +} + +.projector.card .hand .card-sprite:hover { + translate: 0px 9px; +} From 52353c36c349a6e924fad39767a6feeaad818d20 Mon Sep 17 00:00:00 2001 From: disconcision Date: Fri, 3 Jan 2025 00:33:34 -0500 Subject: [PATCH 04/13] pattern cards. card chooser for exp and pat cards. card style improvements --- src/haz3lcore/zipper/projectors/CardProj.re | 259 ++++++++++++++++--- src/haz3lweb/www/img/cards-pixel-pattern.png | Bin 0 -> 20032 bytes src/haz3lweb/www/style/projectors/cards.css | 102 +++++++- 3 files changed, 305 insertions(+), 56 deletions(-) create mode 100644 src/haz3lweb/www/img/cards-pixel-pattern.png diff --git a/src/haz3lcore/zipper/projectors/CardProj.re b/src/haz3lcore/zipper/projectors/CardProj.re index 08caa994c9..03918f2ed9 100644 --- a/src/haz3lcore/zipper/projectors/CardProj.re +++ b/src/haz3lcore/zipper/projectors/CardProj.re @@ -2,6 +2,24 @@ open Util; open Virtual_dom.Vdom; open ProjectorBase; +[@deriving (show({with_path: false}), sexp, yojson)] +type mode = + | Show + | Choose + | Flipped; + +[@deriving (show({with_path: false}), sexp, yojson)] +type model = {mode}; +[@deriving (show({with_path: false}), sexp, yojson)] +type action = + | SetMode(mode); + +let model_of_sexp = (sexp: Sexplib.Sexp.t): model => + switch (model_of_sexp(sexp)) { + | exception _ => {mode: Show} + | m => m + }; + [@deriving (show({with_path: false}), sexp, yojson)] type suit = | Unknown(Piece.t) @@ -34,10 +52,31 @@ type card = (suit, rank); type hand = list(card); [@deriving (show({with_path: false}), sexp, yojson)] -type syntax = +type collection = | Card(card) | Hand(hand); +[@deriving (show({with_path: false}), sexp, yojson)] +type sort = + | Exp + | Pat; + +[@deriving (show({with_path: false}), sexp, yojson)] +type syntax = (sort, collection); + +let sort_of = (sort: Sort.t): sort => + switch (sort) { + | Sort.Exp => Exp + | Sort.Pat => Pat + | _ => failwith("ERROR: Card: Invalid sort") + }; + +let to_sort = (sort: sort): Sort.t => + switch (sort) { + | Exp => Sort.Exp + | Pat => Sort.Pat + }; + module Syntax = { let suit_of_piece = (p: Piece.t): suit => switch (p) { @@ -81,21 +120,23 @@ module Syntax = { }; }; - let piece_to_syntax = (piece: Piece.t): option(syntax) => + let piece_to_syntax = (piece: Piece.t): option(syntax) => { + let sort = piece |> Piece.sort |> fst |> sort_of; switch (piece_to_hand(piece)) { - | Some(hand) => Some(Hand(hand)) + | Some(hand) => Some((sort, Hand(hand))) | None => open OptUtil.Syntax; let+ card = piece_to_card(piece); - Card(card); + (sort, Card(card)); }; + }; - let mk_tuple = (children): Piece.t => + let mk_tuple = (sort: Sort.t, children): Piece.t => Tile({ id: Id.mk(), label: ["(", ")"], - mold: Mold.mk_op(Sort.Exp, [Exp]), - shards: [0], + mold: Mold.mk_op(sort, [sort]), + shards: [0, 1], children: [children], }); @@ -120,20 +161,38 @@ module Syntax = { | _ => rank |> sexp_of_rank |> Sexplib.Sexp.to_string |> mk_text }; - let card_to_piece = ((suit, rank): card): Piece.t => - mk_tuple([ - piece_of_suit(suit), - Piece.mk_tile(Form.get("comma_exp"), []), - piece_of_rank(rank), - ]); + let card_to_piece_exp = ((suit, rank): card): Piece.t => + mk_tuple( + Sort.Exp, + [ + piece_of_suit(suit), + Piece.mk_tile(Form.get("comma_exp"), []), + piece_of_rank(rank), + ], + ); + + let card_to_piece_pat = ((suit, rank): card): Piece.t => + mk_tuple( + Sort.Pat, + [ + piece_of_suit(suit), + Piece.mk_tile(Form.get("comma_pat"), []), + piece_of_rank(rank), + ], + ); + + let hand_to_piece_exp = (hand: hand): Piece.t => + mk_tuple(Sort.Exp, List.map(card_to_piece_exp, hand)); - let hand_to_piece = (hand: hand): Piece.t => - mk_tuple(List.map(card_to_piece, hand)); + let hand_to_piece_pat = (hand: hand): Piece.t => + mk_tuple(Sort.Pat, List.map(card_to_piece_pat, hand)); let syntax_to_piece = (syntax: syntax): Piece.t => switch (syntax) { - | Card(card) => card_to_piece(card) - | Hand(hand) => hand_to_piece(hand) + | (Exp, Card(card)) => card_to_piece_exp(card) + | (Pat, Card(card)) => card_to_piece_pat(card) + | (Exp, Hand(hand)) => hand_to_piece_exp(hand) + | (Pat, Hand(hand)) => hand_to_piece_pat(hand) }; let put = syntax_to_piece; @@ -148,15 +207,18 @@ module Syntax = { let width_of_syntax = (syntax: syntax): int => switch (syntax) { - | Card(_) => 1 - | Hand(hand) => List.length(hand) + | (_, Card(_)) => 1 + | (_, Hand(hand)) => List.length(hand) }; let width_of_piece = (piece: Piece.t): int => switch (piece_to_syntax(piece)) { | None => 0 - | Some(Card(_)) => 4 - | Some(Hand(hand)) => 4 + List.length(hand) / 2 + | Some((_, Card(_))) + | Some((_, Hand([_]))) => 4 + | Some((_, Hand(hand))) => + //TODO: Better formula / card dimensions / offset + 4 + List.length(hand) - (List.length(hand) + 66) / 24 }; }; @@ -166,7 +228,7 @@ let suit_to_int = (suit: suit): int => | Clubs => 1 | Diamonds => 2 | Spades => 3 - | Unknown(_) => 0 + | Unknown(_) => 4 }; let rank_to_int = (rank: rank): int => @@ -184,7 +246,7 @@ let rank_to_int = (rank: rank): int => | Queen => 11 | King => 12 | Ace => 13 - | Unknown(_) => 0 + | Unknown(_) => 14 }; module Card = { @@ -195,32 +257,109 @@ module Card = { let width = 35; /* Width of each card in pixels */ let height = 47; /* Height of each card in pixels */ - let card_to_offset = ((suit, rank): card): (int, int) => ( + let card_to_offset = (_sort: Sort.t, (suit, rank): card): (int, int) => ( rank_to_int(rank) * width, suit_to_int(suit) * height, ); - let background_offset = (card: card): Css_gen.t => { - let (offset_x, offset_y) = card_to_offset(card); + let background_offset = (sort: Sort.t, card: card): Css_gen.t => { + let (offset_x, offset_y) = card_to_offset(sort, card); Css_gen.create( ~field="background-position", ~value=Printf.sprintf("%dpx %dpx", - offset_x, - offset_y), ); }; - let view = (card: card): Node.t => + let view = (sort: Sort.t, card: card): Node.t => Node.div( ~attrs=[ - Attr.class_("card-sprite"), - Attr.style(background_offset(card)), + Attr.classes(["card-sprite", Sort.show(sort)]), + Attr.style(background_offset(sort, card)), ], [], ); }; +module Chooser = { + let col_width = 8; + let row_height = 14; + + let grid = (sort: sort): list(list(card)) => { + let maybe_rank = + switch (sort) { + | Exp => [] + | Pat => [Unknown(Syntax.mk_text("_"))] + }; + let maybe_suit: list(suit) = + switch (sort) { + | Exp => [] + | Pat => [Unknown(Syntax.mk_text("_"))] + }; + let suits: list(suit) = [Hearts, Spades, Diamonds, Clubs] @ maybe_suit; + let ranks: list(rank) = + [ + Two, + Three, + Four, + Five, + Six, + Seven, + Eight, + Nine, + Ten, + Jack, + Queen, + King, + Ace, + ] + @ maybe_rank; + List.map( + (suit: suit) => List.map((rank: rank) => (suit, rank), ranks), + suits, + ); + }; + + let card_wrapper = + (~indicated, parent, sort: Sort.t, col: int, row: int, card: card) + : Node.t => + Node.div( + ~attrs=[ + Attr.classes(["card-wrapper"] @ (indicated ? ["indicated"] : [])), + Attr.on_click(_ => + parent(SetSyntax(Syntax.put((sort_of(sort), Card(card))))) + ), + Attr.create( + "style", + Printf.sprintf( + "position: absolute; left: %dpx; top: %dpx; z-index: %d;", + col * col_width, + row * row_height, + 100 + row + col, + ), + ), + ], + [Card.view(sort, card)], + ); + + let view = (parent, sort: Sort.t, card: card): Node.t => + Node.div( + ~attrs=[Attr.classes(["chooser", Sort.show(sort)])], + List.mapi( + (r, row) => + List.mapi( + (col, c) => + card_wrapper(parent, ~indicated=c == card, sort, col, r, c), + row, + ), + grid(sort_of(sort)), + ) + |> List.concat, + ); +}; + module Hand = { // a card, but each subsequent card should be absoluted positioned 20px to the right of the last and higher in z-index: - let card_wrapper = (index: int, card: card): Node.t => + let card_wrapper = (sort: Sort.t, index: int, card: card): Node.t => Node.div( ~attrs=[ Attr.class_("card-wrapper"), @@ -233,36 +372,72 @@ module Hand = { ), ), ], - [Card.view(card)], + [Card.view(sort, card)], ); - let view = (hand: hand): Node.t => { - Node.div(~attrs=[Attr.class_("hand")], List.mapi(card_wrapper, hand)); + let view = (sort: Sort.t, hand: hand): Node.t => { + Node.div( + ~attrs=[Attr.classes(["hand", Sort.show(sort)])], + List.mapi(card_wrapper(sort), hand), + ); }; }; +[@deriving (show({with_path: false}), sexp, yojson)] +type m = model; +[@deriving (show({with_path: false}), sexp, yojson)] +type a = action; + module M: Projector = { [@deriving (show({with_path: false}), sexp, yojson)] - type model = unit; + type model = m; [@deriving (show({with_path: false}), sexp, yojson)] - type action = unit; - let init = (); + type action = a; + let init: model = {mode: Show}; let can_project = p => Syntax.get_opt(p) != None; let can_focus = false; let dynamics = false; let placeholder = (_, info) => Inline(Syntax.width_of_piece(info.syntax)); - let update = (model, _) => model; + let update = (_model, action) => + switch (action) { + | SetMode(mode) => {mode: mode} + }; let view = ( - _, + model, ~info, - ~local as _, - ~parent as _: external_action => Ui_effect.t(unit), + ~local, + ~parent: external_action => Ui_effect.t(unit), ~utility as _, ) => { switch (Syntax.get(info.syntax)) { - | Card(card) => Card.view(card) - | Hand(hand) => Hand.view(hand) + | (sort, Card(card)) => + Node.div( + ~attrs=[ + Attr.classes( + switch (model.mode) { + | Show => [] + | Choose => ["choose"] + | Flipped => ["flipped"] + }, + ), + Attr.on_click(_ => + switch (model.mode) { + | Show => local(SetMode(Choose)) + | Choose => local(SetMode(Show)) + | Flipped => local(SetMode(Show)) + } + ), + ], + [ + switch (model.mode) { + | Show => Card.view(to_sort(sort), card) + | Choose => Chooser.view(parent, to_sort(sort), card) + | Flipped => Node.div([Node.text("Flipped")]) + }, + ], + ) + | (sort, Hand(hand)) => Hand.view(to_sort(sort), hand) }; }; let focus = _ => (); diff --git a/src/haz3lweb/www/img/cards-pixel-pattern.png b/src/haz3lweb/www/img/cards-pixel-pattern.png new file mode 100644 index 0000000000000000000000000000000000000000..afc9219ad1b37a081073851edde58b8876c93995 GIT binary patch literal 20032 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 literal 0 HcmV?d00001 diff --git a/src/haz3lweb/www/style/projectors/cards.css b/src/haz3lweb/www/style/projectors/cards.css index eabc9e8d11..483624e37b 100644 --- a/src/haz3lweb/www/style/projectors/cards.css +++ b/src/haz3lweb/www/style/projectors/cards.css @@ -6,28 +6,81 @@ display: none; } +.projector.card > div:has(> .card-sprite) { + display: flex; + flex-direction: row; + width: 100%; + height: 100%; +} + .projector.card .card-sprite { + display: flex; + justify-content: flex-start; + align-items: flex-start; cursor: pointer; - translate: 0px 13px; width: 35px; height: 47px; image-rendering: pixelated; - background-image: url('../../img/cards-pixel.png'); - filter: drop-shadow(-1px 0px 0px black) drop-shadow(0px -1px 0px black) drop-shadow(1px 0px 0px black) drop-shadow(0px 1px 0px black); + background-image: url("../../img/cards-pixel-pattern.png"); + filter: drop-shadow(-1px 0px 0px black) drop-shadow(0px -1px 0px black) + drop-shadow(1px 0px 0px black) drop-shadow(0px 1px 0px black); +} + +@keyframes rock-back-and-forth { + 0% { + transform: rotate(0deg); + } + 50% { + transform: rotate(4deg); + } +} + +.projector.card .card-sprite:hover { + animation: rock-back-and-forth 0.25s infinite; +} + +.projector.card.Pat .card-sprite { + filter: drop-shadow(-1px 0px 0px var(--PAT)) + drop-shadow(0px -1px 0px var(--PAT)) drop-shadow(1px 0px 0px var(--PAT)) + drop-shadow(0px 1px 0px var(--PAT)); } @keyframes blink-shadow { - 0% { filter: drop-shadow(-2px 0px 0px red) drop-shadow(1px 0px 0px black) drop-shadow(0px 1px 0px black) drop-shadow(0px -1px 0px black); } - 50% { filter: drop-shadow(-2px 0px 0px red) drop-shadow(1px 0px 0px black) drop-shadow(0px 1px 0px black) drop-shadow(0px -1px 0px black); } - 51% { filter: drop-shadow(-1px 0px 0px black) drop-shadow(1px 0px 0px black) drop-shadow(0px 1px 0px black) drop-shadow(0px -1px 0px black); } - 100% { filter: drop-shadow(-1px 0px 0px black) drop-shadow(1px 0px 0px black) drop-shadow(0px 1px 0px black) drop-shadow(0px -1px 0px black); } + 0% { + filter: drop-shadow(-2px 0px 0px red) drop-shadow(1px 0px 0px black) + drop-shadow(0px 1px 0px black) drop-shadow(0px -1px 0px black); + } + 50% { + filter: drop-shadow(-2px 0px 0px red) drop-shadow(1px 0px 0px black) + drop-shadow(0px 1px 0px black) drop-shadow(0px -1px 0px black); + } + 51% { + filter: drop-shadow(-1px 0px 0px black) drop-shadow(1px 0px 0px black) + drop-shadow(0px 1px 0px black) drop-shadow(0px -1px 0px black); + } + 100% { + filter: drop-shadow(-1px 0px 0px black) drop-shadow(1px 0px 0px black) + drop-shadow(0px 1px 0px black) drop-shadow(0px -1px 0px black); + } } @keyframes blink-shadow-right { - 0% { filter: drop-shadow(2px 0px 0px red) drop-shadow(-1px 0px 0px black) drop-shadow(0px 1px 0px black) drop-shadow(0px -1px 0px black); } - 50% { filter: drop-shadow(2px 0px 0px red) drop-shadow(-1px 0px 0px black) drop-shadow(0px 1px 0px black) drop-shadow(0px -1px 0px black); } - 51% { filter: drop-shadow(1px 0px 0px black) drop-shadow(-1px 0px 0px black) drop-shadow(0px 1px 0px black) drop-shadow(0px -1px 0px black); } - 100% { filter: drop-shadow(1px 0px 0px black) drop-shadow(-1px 0px 0px black) drop-shadow(0px 1px 0px black) drop-shadow(0px -1px 0px black); } + 0% { + filter: drop-shadow(2px 0px 0px red) drop-shadow(-1px 0px 0px black) + drop-shadow(0px 1px 0px black) drop-shadow(0px -1px 0px black); + } + 50% { + filter: drop-shadow(2px 0px 0px red) drop-shadow(-1px 0px 0px black) + drop-shadow(0px 1px 0px black) drop-shadow(0px -1px 0px black); + } + 51% { + filter: drop-shadow(1px 0px 0px black) drop-shadow(-1px 0px 0px black) + drop-shadow(0px 1px 0px black) drop-shadow(0px -1px 0px black); + } + 100% { + filter: drop-shadow(1px 0px 0px black) drop-shadow(-1px 0px 0px black) + drop-shadow(0px 1px 0px black) drop-shadow(0px -1px 0px black); + } } .projector.card.indicated.Left > .card-sprite, @@ -54,13 +107,34 @@ gap: 2px; width: 100%; height: 100%; - translate: 0px -10px; } .projector.card .hand .card-sprite { - transition: translate 0.1s ease-in-out; + transition: all 0.1s ease-in-out; } .projector.card .hand .card-sprite:hover { - translate: 0px 9px; + translate: 0px -9px; + filter: drop-shadow(-1px 0px 0px black) drop-shadow(0px -1px 0px black) + drop-shadow(1px 0px 0px black) drop-shadow(0px 1px 0px black) drop-shadow(2px 2px 0px #6666); +} + +.projector.card .chooser { + display: grid; + grid-template-columns: repeat(13, 1fr); +} +.projector.card:has(.chooser) { + justify-content: flex-start; + align-items: flex-start; +} + +.projector.card .chooser .card-wrapper:hover { + animation: rock-back-and-forth 0.25s infinite; +} + +.projector.card .chooser .card-wrapper.indicated { + filter: invert(1); +} +.projector.card .chooser .card-wrapper:hover { + filter: invert(1); } From d49a7cc39c5a2c0a2d3479c4773a344ab9de3bd3 Mon Sep 17 00:00:00 2001 From: disconcision Date: Fri, 3 Jan 2025 01:34:53 -0500 Subject: [PATCH 05/13] card flipping --- src/haz3lcore/zipper/projectors/CardProj.re | 64 +++++++++++++++------ src/haz3lweb/www/style/projectors/cards.css | 40 ++++++++++++- 2 files changed, 84 insertions(+), 20 deletions(-) diff --git a/src/haz3lcore/zipper/projectors/CardProj.re b/src/haz3lcore/zipper/projectors/CardProj.re index 03918f2ed9..a31ef8498c 100644 --- a/src/haz3lcore/zipper/projectors/CardProj.re +++ b/src/haz3lcore/zipper/projectors/CardProj.re @@ -262,8 +262,14 @@ module Card = { suit_to_int(suit) * height, ); - let background_offset = (sort: Sort.t, card: card): Css_gen.t => { - let (offset_x, offset_y) = card_to_offset(sort, card); + let background_offset = (~flipped, sort: Sort.t, card: card): Css_gen.t => { + let (offset_x, offset_y) = + flipped + ? switch (sort_of(sort)) { + | Exp => (0, 0) + | Pat => (0, height) + } + : card_to_offset(sort, card); Css_gen.create( ~field="background-position", ~value=Printf.sprintf("%dpx %dpx", - offset_x, - offset_y), @@ -272,11 +278,23 @@ module Card = { let view = (sort: Sort.t, card: card): Node.t => Node.div( - ~attrs=[ - Attr.classes(["card-sprite", Sort.show(sort)]), - Attr.style(background_offset(sort, card)), + ~attrs=[Attr.classes(["card-scene", Sort.show(sort)])], + [ + Node.div( + ~attrs=[ + Attr.classes(["card-sprite", "front", Sort.show(sort)]), + Attr.style(background_offset(~flipped=false, sort, card)), + ], + [], + ), + Node.div( + ~attrs=[ + Attr.classes(["card-sprite", "back", Sort.show(sort)]), + Attr.style(background_offset(~flipped=true, sort, card)), + ], + [], + ), ], - [], ); }; @@ -415,17 +433,29 @@ module M: Projector = { Node.div( ~attrs=[ Attr.classes( - switch (model.mode) { - | Show => [] - | Choose => ["choose"] - | Flipped => ["flipped"] - }, + ["outer"] + @ ( + switch (model.mode) { + | Show => [] + | Choose => ["choose"] + | Flipped => ["flipped"] + } + ), ), - Attr.on_click(_ => - switch (model.mode) { - | Show => local(SetMode(Choose)) - | Choose => local(SetMode(Show)) - | Flipped => local(SetMode(Show)) + Attr.on_click(evt => + switch (JsUtil.is_double_click(evt)) { + | false => + switch (model.mode) { + | Show => local(SetMode(Flipped)) + | Flipped => local(SetMode(Choose)) + | Choose => local(SetMode(Show)) + } + | true => + switch (model.mode) { + | Show => local(SetMode(Choose)) + | Choose => local(SetMode(Show)) + | Flipped => local(SetMode(Flipped)) + } } ), ], @@ -433,7 +463,7 @@ module M: Projector = { switch (model.mode) { | Show => Card.view(to_sort(sort), card) | Choose => Chooser.view(parent, to_sort(sort), card) - | Flipped => Node.div([Node.text("Flipped")]) + | Flipped => Card.view(to_sort(sort), card) }, ], ) diff --git a/src/haz3lweb/www/style/projectors/cards.css b/src/haz3lweb/www/style/projectors/cards.css index 483624e37b..095190b5d2 100644 --- a/src/haz3lweb/www/style/projectors/cards.css +++ b/src/haz3lweb/www/style/projectors/cards.css @@ -13,7 +13,35 @@ height: 100%; } +.projector.card { + perspective: 300px; +} + +.outer { + width: 100%; + height: 100%; +} + +.projector.card .card-scene { + width: 100%; + height: 100%; + transition: transform 0.3s; + transform-style: preserve-3d; + position: relative; + cursor: pointer; +} + +.projector.card .flipped .card-scene { + transform: rotateY(180deg); +} + +.card-sprite.back { + transform: rotateY(180deg) rotateX(9deg); +} + .projector.card .card-sprite { + position: absolute; + backface-visibility: hidden; display: flex; justify-content: flex-start; align-items: flex-start; @@ -35,7 +63,7 @@ } } -.projector.card .card-sprite:hover { +.projector.card.indicated .hand .card-scene:hover { animation: rock-back-and-forth 0.25s infinite; } @@ -113,10 +141,11 @@ transition: all 0.1s ease-in-out; } -.projector.card .hand .card-sprite:hover { +.projector.card.indicated .hand .card-sprite:hover { translate: 0px -9px; filter: drop-shadow(-1px 0px 0px black) drop-shadow(0px -1px 0px black) - drop-shadow(1px 0px 0px black) drop-shadow(0px 1px 0px black) drop-shadow(2px 2px 0px #6666); + drop-shadow(1px 0px 0px black) drop-shadow(0px 1px 0px black) + drop-shadow(2px 2px 0px #6666); } .projector.card .chooser { @@ -128,6 +157,11 @@ align-items: flex-start; } +.projector.card .card-wrapper { + width: 100%; + height: 100%; +} + .projector.card .chooser .card-wrapper:hover { animation: rock-back-and-forth 0.25s infinite; } From 882403a35a2e0d08304eb137017fdc6f7b400f5c Mon Sep 17 00:00:00 2001 From: disconcision Date: Fri, 3 Jan 2025 23:13:18 -0500 Subject: [PATCH 06/13] new projector shape: tab --- src/haz3lcore/Measured.re | 90 +++++--- src/haz3lcore/tiles/Base.re | 10 + src/haz3lcore/zipper/Editor.re | 8 +- src/haz3lcore/zipper/Printer.re | 2 +- src/haz3lcore/zipper/Projector.re | 21 +- src/haz3lcore/zipper/ProjectorBase.re | 5 +- src/haz3lcore/zipper/projectors/CardProj.re | 195 +++++++++++++----- .../zipper/projectors/CheckboxProj.re | 2 +- src/haz3lcore/zipper/projectors/FoldProj.re | 2 +- src/haz3lcore/zipper/projectors/InfoProj.re | 2 +- src/haz3lcore/zipper/projectors/ProbeProj.re | 2 +- .../zipper/projectors/SliderFProj.re | 2 +- src/haz3lcore/zipper/projectors/SliderProj.re | 2 +- .../zipper/projectors/TextAreaProj.re | 2 +- src/haz3lweb/app/common/ProjectorView.re | 21 +- src/haz3lweb/app/editors/code/Code.re | 78 ++++--- src/haz3lweb/app/editors/code/CodeViewable.re | 18 +- .../app/editors/code/CodeWithStatics.re | 4 +- .../app/editors/decoration/BackpackView.re | 6 +- src/haz3lweb/app/editors/decoration/Deco.re | 10 +- src/haz3lweb/app/editors/result/EvalResult.re | 6 +- src/haz3lweb/app/explainthis/ExplainThis.re | 2 +- src/haz3lweb/www/style/projectors/base.css | 22 +- src/haz3lweb/www/style/projectors/cards.css | 107 ++++++---- 24 files changed, 398 insertions(+), 221 deletions(-) diff --git a/src/haz3lcore/Measured.re b/src/haz3lcore/Measured.re index a524971079..0c3c5a0266 100644 --- a/src/haz3lcore/Measured.re +++ b/src/haz3lcore/Measured.re @@ -56,7 +56,6 @@ type t = { secondary: Id.Map.t(measurement), projectors: Id.Map.t(measurement), rows: Rows.t, - linebreaks: Id.Map.t(rel_indent), }; let empty = { @@ -65,7 +64,6 @@ let empty = { secondary: Id.Map.empty, projectors: Id.Map.empty, rows: Rows.empty, - linebreaks: Id.Map.empty, }; let add_s = (id: Id.t, i: int, m, map) => { @@ -131,11 +129,6 @@ let rec add_n_rows = (origin: Point.t, row_indent, n: abs_indent, map: t): t => |> add_row(origin.row + n - 1, {indent: row_indent, max_col: origin.col}) }; -let add_lb = (id, indent, map) => { - ...map, - linebreaks: Id.Map.add(id, indent, map.linebreaks), -}; - let singleton_w = (w, m) => empty |> add_w(w, m); let singleton_g = (g, m) => empty |> add_g(g, m); let singleton_s = (id, shard, m) => empty |> add_s(id, shard, m); @@ -147,8 +140,6 @@ let find_shards = (~msg="", t: Tile.t, map) => | _ => failwith("find_shards: " ++ msg) }; -let find_opt_lb = (id, map) => Id.Map.find_opt(id, map.linebreaks); - let find_shards' = (id: Id.t, map) => switch (Id.Map.find_opt(id, map.tiles)) { | None => [] @@ -275,14 +266,18 @@ let is_indented_map = (seg: Segment.t) => { go(seg); }; -let last_of_token = (token: string, origin: Point.t): Point.t => - /* Supports multi-line tokens e.g. projector placeholders */ - Point.{ - col: origin.col + StringUtil.max_line_width(token), - row: origin.row + StringUtil.num_linebreaks(token), - }; +/* Tab projectors add linebreaks after the end of their line */ +let deferred_linebreaks: ref(list(int)) = ref([]); + +let consume_deferred_linebreaks = () => { + let max_deferred_linebreaks = List.fold_left(max, 0, deferred_linebreaks^); + deferred_linebreaks := []; + max_deferred_linebreaks; +}; -let of_segment = (seg: Segment.t, token_of_proj: Base.projector => string): t => { +let of_segment = + (seg: Segment.t, shape_of_proj: Base.projector => Base.shape): t => { + deferred_linebreaks := []; let is_indented = is_indented_map(seg); // recursive across seg's bidelimited containers @@ -316,11 +311,6 @@ let of_segment = (seg: Segment.t, token_of_proj: Base.projector => string): t => ); (origin, map); | [hd, ...tl] => - let extra_rows = (token, origin, map) => { - let row_indent = container_indent + contained_indent; - let num_extra_rows = StringUtil.num_linebreaks(token); - add_n_rows(origin, row_indent, num_extra_rows, map); - }; let (contained_indent, origin, map) = switch (hd) { | Secondary(w) when Secondary.is_linebreak(w) => @@ -331,16 +321,16 @@ let of_segment = (seg: Segment.t, token_of_proj: Base.projector => string): t => } else { contained_indent + (Id.Map.find(w.id, is_indented) ? 2 : 0); }; + let num_extra_rows = 1 + consume_deferred_linebreaks(); let last = - Point.{row: origin.row + 1, col: container_indent + indent}; + Point.{ + row: origin.row + num_extra_rows, + col: container_indent + indent, + }; let map = map |> add_w(w, {origin, last}) - |> add_row( - origin.row, - {indent: row_indent, max_col: origin.col}, - ) - |> add_lb(w.id, indent); + |> add_n_rows(origin, row_indent, num_extra_rows); (indent, last, map); | Secondary(w) => let wspace_length = @@ -353,12 +343,52 @@ let of_segment = (seg: Segment.t, token_of_proj: Base.projector => string): t => let map = map |> add_g(g, {origin, last}); (contained_indent, last, map); | Projector(p) => - let token = token_of_proj(p); - let last = last_of_token(token, origin); - let map = extra_rows(token, origin, map); + let extra_rows_of_shape = (shape: Base.shape, origin, map) => + switch (shape) { + | Inline(_) => map + | NewInline({row: height, _}) => + let num_lb = height - 1; + if (num_lb > 0) { + deferred_linebreaks := [num_lb, ...deferred_linebreaks^]; + }; + map; + | Block({row: height, _}) => + let num_lb = height - 1; + let row_indent = container_indent + contained_indent; + let num_extra_rows = + num_lb + num_lb == 0 ? 0 : consume_deferred_linebreaks(); + add_n_rows(origin, row_indent, num_extra_rows, map); + }; + let last_of_shape = (shape: Base.shape, origin: Point.t): Point.t => + switch (shape) { + | Inline(width) => {col: origin.col + width, row: origin.row} + | NewInline({col: width, _}) => { + col: origin.col + width, + row: origin.row, + } + | Block({col: width, row: height}) => { + col: origin.col + width, + row: origin.row + height - 1, + } + }; + let shape = shape_of_proj(p); + let last = last_of_shape(shape, origin); + let map = extra_rows_of_shape(shape, origin, map); let map = add_pr(p, {origin, last}, map); (contained_indent, last, map); | Tile(t) => + let extra_rows = (token, origin, map) => { + let row_indent = container_indent + contained_indent; + let num_lb = StringUtil.num_linebreaks(token); + let num_extra_rows = + StringUtil.num_linebreaks(token) + num_lb == 0 + ? 0 : consume_deferred_linebreaks(); + add_n_rows(origin, row_indent, num_extra_rows, map); + }; + let last_of_token = (token: string, origin: Point.t): Point.t => { + col: origin.col + StringUtil.max_line_width(token), + row: origin.row + StringUtil.num_linebreaks(token), + }; let add_shard = (origin, shard, map) => { let token = List.nth(t.label, shard); let map = extra_rows(token, origin, map); diff --git a/src/haz3lcore/tiles/Base.re b/src/haz3lcore/tiles/Base.re index 8501ec9c6b..abff103604 100644 --- a/src/haz3lcore/tiles/Base.re +++ b/src/haz3lcore/tiles/Base.re @@ -14,6 +14,16 @@ type kind = | Card | TextArea; +/* Projectors currently have two options for placeholder + * shapes: A inline display of a given length, or a block + * display with given length & height. Both of these can + * depend on the projector model and info package */ +[@deriving (show({with_path: false}), sexp, yojson)] +type shape = + | Inline(int) + | NewInline(Point.t) + | Block(Point.t); + [@deriving (show({with_path: false}), sexp, yojson)] type segment = list(piece) and piece = diff --git a/src/haz3lcore/zipper/Editor.re b/src/haz3lcore/zipper/Editor.re index 6ad2f796a7..8005869a2f 100644 --- a/src/haz3lcore/zipper/Editor.re +++ b/src/haz3lcore/zipper/Editor.re @@ -33,7 +33,7 @@ module CachedSyntax = { let yojson_of_t = _ => failwith("Editor.Meta.yojson_of_t"); let t_of_yojson = _ => failwith("Editor.Meta.t_of_yojson"); - let init = (~token_of_proj, z): t => { + let init = (~shape_of_proj, z): t => { let segment = Zipper.unselect_and_zip(z); let MakeTerm.{term, terms, projectors} = MakeTerm.go(segment); { @@ -42,7 +42,7 @@ module CachedSyntax = { term_ranges: TermRanges.mk(segment), tiles: TileMap.mk(segment), holes: Segment.holes(segment), - measured: Measured.of_segment(segment, token_of_proj), + measured: Measured.of_segment(segment, shape_of_proj), selection_ids: Selection.selection_ids(z.selection), term, terms, @@ -54,7 +54,7 @@ module CachedSyntax = { let calculate = (z: Zipper.t, info_map, dyn_map, old: t) => old.old - ? init(z, ~token_of_proj=Projector.token_of_proj(info_map, dyn_map)) + ? init(z, ~shape_of_proj=Projector.shape_of_proj(info_map, dyn_map)) : {...old, selection_ids: Selection.selection_ids(z.selection)}; }; @@ -97,7 +97,7 @@ module Model = { col_target: None, }, history: History.empty, - syntax: CachedSyntax.init(zipper, ~token_of_proj=_ => ""), + syntax: CachedSyntax.init(zipper, ~shape_of_proj=_ => Base.Inline(0)), }; type persistent = PersistentZipper.t; diff --git a/src/haz3lcore/zipper/Printer.re b/src/haz3lcore/zipper/Printer.re index 107e5c6a4f..48b50b52c2 100644 --- a/src/haz3lcore/zipper/Printer.re +++ b/src/haz3lcore/zipper/Printer.re @@ -58,7 +58,7 @@ let measured = z => z |> ZipperBase.remove_all_projectors |> Zipper.seg_without_buffer - |> Measured.of_segment(_, _ => ""); // No projectors + |> Measured.of_segment(_, _ => Base.Inline(0)); // No projectors let pretty_print = (~holes: option(string)=Some(""), z: Zipper.t): string => to_rows( diff --git a/src/haz3lcore/zipper/Projector.re b/src/haz3lcore/zipper/Projector.re index 90b60d2066..d3f27c96e8 100644 --- a/src/haz3lcore/zipper/Projector.re +++ b/src/haz3lcore/zipper/Projector.re @@ -44,14 +44,23 @@ let shape = (p: Base.projector, info: info): shape => { P.placeholder(p.model, info); }; -/* Returns a token consisting of whitespace (possibly including linebreaks) - * representing the space to leave for the projector in the underlying code view */ -let token_of_proj = - (statics: Statics.Map.t, dynamics: Dynamics.Map.t, p: Base.projector) => { +let shape_of_proj = + (statics: Statics.Map.t, dynamics: Dynamics.Map.t, p: Base.projector) + : shape => { let statics = Statics.Map.lookup(p.id, statics); let dynamics = Dynamics.Map.lookup(p.id, dynamics); - switch (shape(p, {id: p.id, syntax: p.syntax, statics, dynamics})) { + shape(p, {id: p.id, syntax: p.syntax, statics, dynamics}); +}; + +let token_of_shape = (shape: shape): string => + switch (shape) { | Inline(width) => String.make(width, ' ') + | NewInline({row: _, col: width}) => String.make(width, ' ') | Block({row, col}) => String.make(row - 1, '\n') ++ String.make(col, ' ') }; -}; + +/* Returns a token consisting of whitespace (possibly including linebreaks) + * representing the space to leave for the projector in the underlying code view */ +let _token_of_proj = + (statics: Statics.Map.t, dynamics: Dynamics.Map.t, p: Base.projector) => + token_of_shape(shape_of_proj(statics, dynamics, p)); diff --git a/src/haz3lcore/zipper/ProjectorBase.re b/src/haz3lcore/zipper/ProjectorBase.re index 4f80671e09..d79ad97066 100644 --- a/src/haz3lcore/zipper/ProjectorBase.re +++ b/src/haz3lcore/zipper/ProjectorBase.re @@ -1,5 +1,6 @@ open Util; open Virtual_dom.Vdom; +include Base; [@deriving (show({with_path: false}), sexp, yojson)] type t = Base.kind; @@ -9,9 +10,7 @@ type t = Base.kind; * display with given length & height. Both of these can * depend on the projector model and info package */ [@deriving (show({with_path: false}), sexp, yojson)] -type shape = - | Inline(int) - | Block(Point.t); +type shape = Base.shape; /* The type of syntax which a projector can replace. * Right now projectors can replace a single piece */ diff --git a/src/haz3lcore/zipper/projectors/CardProj.re b/src/haz3lcore/zipper/projectors/CardProj.re index a31ef8498c..99713564fe 100644 --- a/src/haz3lcore/zipper/projectors/CardProj.re +++ b/src/haz3lcore/zipper/projectors/CardProj.re @@ -101,16 +101,21 @@ module Syntax = { let rm_secondary = (segment: Segment.t): Segment.t => List.filter(p => !Piece.is_secondary(p), segment); - let piece_to_card = (piece: Piece.t): option(card) => - switch (piece) { - | Tile({label: ["(", ")"], children: [segment], _}) => - switch (rm_secondary(segment)) { - | [left_child, Tile({label: [","], _}), right_child] => - Some((suit_of_piece(left_child), rank_of_piece(right_child))) - | _ => None - } - | _ => None - }; + let piece_to_card = + Core.Memo.general(~cache_size_bound=1000, (piece: Piece.t) => + ( + switch (piece) { + | Tile({label: ["(", ")"], children: [segment], _}) => + switch (rm_secondary(segment)) { + | [left_child, Tile({label: [","], _}), right_child] => + Some((suit_of_piece(left_child), rank_of_piece(right_child))) + | _ => None + } + | _ => None + }: + option(card) + ) + ); let piece_to_hand = (piece: Piece.t): option(hand) => { switch (piece) { @@ -343,9 +348,15 @@ module Chooser = { Node.div( ~attrs=[ Attr.classes(["card-wrapper"] @ (indicated ? ["indicated"] : [])), - Attr.on_click(_ => - parent(SetSyntax(Syntax.put((sort_of(sort), Card(card))))) - ), + Attr.on_mousedown(_ => { + print_endline("setting syntax"); + //TODO: make this work for hands + Effect.Many([ + parent(SetSyntax(Syntax.put((sort_of(sort), Card(card))))), + // Effect.Prevent_default, + // Effect.Stop_propagation, + ]); + }), Attr.create( "style", Printf.sprintf( @@ -375,9 +386,115 @@ module Chooser = { ); }; +module Singleton = { + let view = + ( + mode, + parent, + local: action => Ui_effect.t(unit), + sort: Sort.t, + card: card, + ) + : Node.t => { + let on_mousedown = evt => + switch (JsUtil.is_double_click(evt)) { + | _ when JsUtil.shift_held(evt) => + switch (mode) { + | Choose + | Flipped => local(SetMode(Show)) + | Show => local(SetMode(Choose)) + } + | _ => + switch (mode) { + | Flipped + | Choose => local(SetMode(Show)) + | _ => local(SetMode(Flipped)) + } + }; + + Node.div( + ~attrs=[ + Attr.classes( + ["card-outer"] + @ ( + switch (mode) { + | Show => ["show"] + | Flipped => ["flipped"] + | Choose => ["choose"] + } + ), + ), + Attr.on_mousedown(on_mousedown), + ], + [ + switch (mode) { + | Show => Card.view(sort, card) + | Choose => Chooser.view(parent, sort, card) + | Flipped => Card.view(sort, card) + }, + ], + ); + }; +}; + +module CardInHand = { + let view = + ( + mode, + parent, + local: action => Ui_effect.t(unit), + sort: Sort.t, + card: card, + ) + : Node.t => { + let on_mousedown = evt => + switch (JsUtil.is_double_click(evt)) { + | _ when JsUtil.shift_held(evt) => + switch (mode) { + | Choose + | Flipped => local(SetMode(Show)) + | Show => local(SetMode(Choose)) + } + | _ => Effect.Ignore + }; + + Node.div( + ~attrs=[ + Attr.classes( + ["card-outer"] + @ ( + switch (mode) { + | Show => ["show"] + | Flipped => ["flipped"] + | Choose => ["choose"] + } + ), + ), + Attr.on_mousedown(on_mousedown), + ], + [ + switch (mode) { + | Show => Card.view(sort, card) + | Choose => Chooser.view(parent, sort, card) + | Flipped => Card.view(sort, card) + }, + ], + ); + }; +}; + module Hand = { // a card, but each subsequent card should be absoluted positioned 20px to the right of the last and higher in z-index: - let card_wrapper = (sort: Sort.t, index: int, card: card): Node.t => + let card_wrapper = + ( + mode, + parent: external_action => Ui_effect.t(unit), + local: action => Ui_effect.t(unit), + sort: Sort.t, + index: int, + card: card, + ) + : Node.t => Node.div( ~attrs=[ Attr.class_("card-wrapper"), @@ -390,13 +507,13 @@ module Hand = { ), ), ], - [Card.view(sort, card)], + [CardInHand.view(mode, parent, local, sort, card)], ); - let view = (sort: Sort.t, hand: hand): Node.t => { + let view = (mode, parent, local, sort: Sort.t, hand: hand): Node.t => { Node.div( ~attrs=[Attr.classes(["hand", Sort.show(sort)])], - List.mapi(card_wrapper(sort), hand), + List.mapi(card_wrapper(mode, parent, local, sort), hand), ); }; }; @@ -415,7 +532,8 @@ module M: Projector = { let can_project = p => Syntax.get_opt(p) != None; let can_focus = false; let dynamics = false; - let placeholder = (_, info) => Inline(Syntax.width_of_piece(info.syntax)); + let placeholder = (_, info) => + Base.NewInline({row: 2, col: Syntax.width_of_piece(info.syntax)}); let update = (_model, action) => switch (action) { | SetMode(mode) => {mode: mode} @@ -430,44 +548,9 @@ module M: Projector = { ) => { switch (Syntax.get(info.syntax)) { | (sort, Card(card)) => - Node.div( - ~attrs=[ - Attr.classes( - ["outer"] - @ ( - switch (model.mode) { - | Show => [] - | Choose => ["choose"] - | Flipped => ["flipped"] - } - ), - ), - Attr.on_click(evt => - switch (JsUtil.is_double_click(evt)) { - | false => - switch (model.mode) { - | Show => local(SetMode(Flipped)) - | Flipped => local(SetMode(Choose)) - | Choose => local(SetMode(Show)) - } - | true => - switch (model.mode) { - | Show => local(SetMode(Choose)) - | Choose => local(SetMode(Show)) - | Flipped => local(SetMode(Flipped)) - } - } - ), - ], - [ - switch (model.mode) { - | Show => Card.view(to_sort(sort), card) - | Choose => Chooser.view(parent, to_sort(sort), card) - | Flipped => Card.view(to_sort(sort), card) - }, - ], - ) - | (sort, Hand(hand)) => Hand.view(to_sort(sort), hand) + Singleton.view(model.mode, parent, local, to_sort(sort), card) + | (sort, Hand(hand)) => + Hand.view(model.mode, parent, local, to_sort(sort), hand) }; }; let focus = _ => (); diff --git a/src/haz3lcore/zipper/projectors/CheckboxProj.re b/src/haz3lcore/zipper/projectors/CheckboxProj.re index 936ab7f791..4e4cab8ede 100644 --- a/src/haz3lcore/zipper/projectors/CheckboxProj.re +++ b/src/haz3lcore/zipper/projectors/CheckboxProj.re @@ -53,7 +53,7 @@ module M: Projector = { let can_project = p => state_of(p) != None; let can_focus = false; let dynamics = false; - let placeholder = (_, _) => Inline(2); + let placeholder = (_, _) => Base.Inline(2); let update = (model, _) => model; let view = view; let focus = _ => (); diff --git a/src/haz3lcore/zipper/projectors/FoldProj.re b/src/haz3lcore/zipper/projectors/FoldProj.re index 15d9dd5d78..4d6c97f1e5 100644 --- a/src/haz3lcore/zipper/projectors/FoldProj.re +++ b/src/haz3lcore/zipper/projectors/FoldProj.re @@ -19,7 +19,7 @@ module M: Projector = { let can_focus = false; let dynamics = false; let placeholder = (m, _) => - Inline(m.text == "⋱" ? 2 : m.text |> String.length); + Base.Inline(m.text == "⋱" ? 2 : m.text |> String.length); let update = (m, _) => m; let view = (m: model, ~info as _, ~local as _, ~parent, ~utility as _) => div( diff --git a/src/haz3lcore/zipper/projectors/InfoProj.re b/src/haz3lcore/zipper/projectors/InfoProj.re index 9bd3e3e02d..9874aa9c30 100644 --- a/src/haz3lcore/zipper/projectors/InfoProj.re +++ b/src/haz3lcore/zipper/projectors/InfoProj.re @@ -69,7 +69,7 @@ module M: Projector = { display_ty(model, info) |> totalize_ty |> Typ.pretty_print; let placeholder = (model, info) => - Inline((display(model, info.statics) |> String.length) + 5); + Base.Inline((display(model, info.statics) |> String.length) + 5); let update = (model, a: action) => switch (a, model) { diff --git a/src/haz3lcore/zipper/projectors/ProbeProj.re b/src/haz3lcore/zipper/projectors/ProbeProj.re index 61d6ad3410..5a95e497ba 100644 --- a/src/haz3lcore/zipper/projectors/ProbeProj.re +++ b/src/haz3lcore/zipper/projectors/ProbeProj.re @@ -307,7 +307,7 @@ let syntax_str = (info: info) => { let syntax_view = (info: info) => info |> syntax_str |> text; let placeholder = (_m, info) => - Inline(3 + String.length(syntax_str(info))); + Base.Inline(3 + String.length(syntax_str(info))); // let icon = div(~attrs=[Attr.classes(["icon"])], [text("🔍")]); let icon = div(~attrs=[Attr.classes(["icon"])], []); diff --git a/src/haz3lcore/zipper/projectors/SliderFProj.re b/src/haz3lcore/zipper/projectors/SliderFProj.re index c14e8b3566..5b570f3d62 100644 --- a/src/haz3lcore/zipper/projectors/SliderFProj.re +++ b/src/haz3lcore/zipper/projectors/SliderFProj.re @@ -25,7 +25,7 @@ module M: Projector = { let can_project = p => get_opt(p) != None; let can_focus = false; let dynamics = false; - let placeholder = (_, _) => Inline(10); + let placeholder = (_, _) => Base.Inline(10); let update = (model, _) => model; let view = ( diff --git a/src/haz3lcore/zipper/projectors/SliderProj.re b/src/haz3lcore/zipper/projectors/SliderProj.re index 975cfc0b24..1a8cc44087 100644 --- a/src/haz3lcore/zipper/projectors/SliderProj.re +++ b/src/haz3lcore/zipper/projectors/SliderProj.re @@ -22,7 +22,7 @@ module M: Projector = { let can_project = p => get_opt(p) != None; let can_focus = false; let dynamics = false; - let placeholder = (_, _) => Inline(10); + let placeholder = (_, _) => Base.Inline(10); let update = (model, _) => model; let view = ( diff --git a/src/haz3lcore/zipper/projectors/TextAreaProj.re b/src/haz3lcore/zipper/projectors/TextAreaProj.re index e0c64951b9..e92a737191 100644 --- a/src/haz3lcore/zipper/projectors/TextAreaProj.re +++ b/src/haz3lcore/zipper/projectors/TextAreaProj.re @@ -101,7 +101,7 @@ module M: Projector = { let dynamics = false; let placeholder = (_, info) => { let str = Form.strip_quotes(get(info.syntax)); - Block({ + Base.Block({ row: StringUtil.num_lines(str), /* +2 for left and right padding */ col: 2 + StringUtil.max_line_width(str), diff --git a/src/haz3lweb/app/common/ProjectorView.re b/src/haz3lweb/app/common/ProjectorView.re index 584c8586db..235984b2f9 100644 --- a/src/haz3lweb/app/common/ProjectorView.re +++ b/src/haz3lweb/app/common/ProjectorView.re @@ -49,6 +49,7 @@ let backing_deco = ) => switch (shape) { | Inline(_) + | NewInline(_) | Block(_) => PieceDec.relative_shard({ font_metrics, @@ -59,16 +60,9 @@ let backing_deco = /* Adds attributes to a projector UI to support * custom styling when selected or indicated */ -let status = - (indicated: option(Direction.t), selected: bool, shape: shape, sort) => +let status = (indicated: option(Direction.t), selected: bool, sort) => [Sort.show(sort)] @ (selected ? ["selected"] : []) - @ ( - switch (shape) { - | Inline(_) => ["inline"] - | Block(_) => ["block"] - } - ) @ ( switch (indicated) { | Some(d) => ["indicated", Direction.show(d)] @@ -98,8 +92,7 @@ let view_wrapper = div( ~attrs=[ Attr.classes( - ["projector", name(p.kind)] - @ status(indication, selected, shape, sort), + ["projector", name(p.kind)] @ status(indication, selected, sort), ), Attr.on_mousedown(focus(info.id)), DecUtil.abs_style(measurement, ~font_metrics), @@ -152,7 +145,13 @@ let collate_utility = cached_syntax.measured, ), view: (sort, seg) => - CodeViewable.view_segment(~globals, ~sort, ~token_of_proj=_ => "", seg), + /* Assume this doesn't contain projectors */ + CodeViewable.view_segment( + ~globals, + ~sort, + ~shape_of_proj=_ => Base.Inline(0), + seg, + ), exp_to_seg: exp => exp |> DHExp.strip_casts diff --git a/src/haz3lweb/app/editors/code/Code.re b/src/haz3lweb/app/editors/code/Code.re index 00a8778199..bda52527ab 100644 --- a/src/haz3lweb/app/editors/code/Code.re +++ b/src/haz3lweb/app/editors/code/Code.re @@ -6,6 +6,15 @@ open Util.Web; /* Helpers for rendering code text with holes and syntax highlighting */ +/* Tab projectors add linebreaks after the end of their line */ +let deferred_linebreaks: ref(list(int)) = ref([]); + +let consume_deferred_linebreaks = () => { + let max_deferred_linebreaks = List.fold_left(max, 0, deferred_linebreaks^); + deferred_linebreaks := []; + max_deferred_linebreaks; +}; + let of_delim' = Core.Memo.general( ~cache_size_bound=10000, @@ -23,9 +32,11 @@ let of_delim' = //let label = is_in_buffer ? AssistantExpander.mark(label) : label; let token = List.nth(label, i); /* Add indent to multiline tokens: */ + let num_lb = StringUtil.num_linebreaks(token); let token = - StringUtil.num_linebreaks(token) == 0 + num_lb == 0 ? token : token ++ StringUtil.repeat(indent, Unicode.nbsp); + //TODO: deffered linebreaks [ span( ~attrs=[Attr.classes(["token", cls, plurality])], @@ -52,36 +63,53 @@ let space = " "; //Unicode.nbsp; let of_grout = [Node.text(space)]; let of_secondary = - Core.Memo.general( - ~cache_size_bound=10000, ((content, secondary_icons, indent)) => - if (String.equal(Secondary.get_string(content), Form.linebreak)) { - let str = secondary_icons ? ">" : ""; - [ - span_c("linebreak", [text(str)]), - Node.text("\n"), - Node.text(StringUtil.repeat(indent, space)), - ]; - } else if (String.equal(Secondary.get_string(content), Form.space)) { - let str = secondary_icons ? "·" : space; - [span_c("whitespace", [text(str)])]; - } else if (Secondary.content_is_comment(content)) { - [span_c("comment", [Node.text(Secondary.get_string(content))])]; - } else { - [span_c("secondary", [Node.text(Secondary.get_string(content))])]; - } - ); + //Core.Memo.general( ~cache_size_bound=10000, + ((content, secondary_icons, indent)) => + if (String.equal(Secondary.get_string(content), Form.linebreak)) { + let str = secondary_icons ? ">" : ""; + [span_c("linebreak", [text(str)])] + @ List.init(1 + consume_deferred_linebreaks(), _ => Node.text("\n")) + @ [Node.text(StringUtil.repeat(indent, space))]; + } else if (String.equal(Secondary.get_string(content), Form.space)) { + let str = secondary_icons ? "·" : space; + [span_c("whitespace", [text(str)])]; + } else if (Secondary.content_is_comment(content)) { + [span_c("comment", [Node.text(Secondary.get_string(content))])]; + } else { + [span_c("secondary", [Node.text(Secondary.get_string(content))])]; + }; +//); -let of_projector = (expected_sort, indent, token) => +let of_projector = (expected_sort, indent, shape: Base.shape) => { + let token = + switch (shape) { + | Inline(_) => Projector.token_of_shape(shape) + | NewInline({row: height, _}) => + let num_lb = height - 1; + if (num_lb > 0) { + deferred_linebreaks := [num_lb, ...deferred_linebreaks^]; + }; + Projector.token_of_shape(shape); + | Block({row: height, _}) => + let num_lb = height - 1; + num_lb == 0 + ? "" + : String.make(consume_deferred_linebreaks(), '\n') + ++ Projector.token_of_shape(shape); + }; of_delim'(([token], false, expected_sort, true, true, indent, 0)); +}; module Text = ( M: { let map: Measured.t; let settings: Settings.Model.t; - let token_of_proj: Base.projector => string; + let shape_of_proj: Base.projector => Base.shape; }, ) => { + deferred_linebreaks := []; + let m = p => Measured.find_p(~msg="Text", p, M.map); let rec of_segment = (buffer_ids, no_sorts, sort, seg: Segment.t): list(Node.t) => { @@ -113,7 +141,7 @@ module Text = of_projector( expected_sort, m(Projector(p)).origin.col, - M.token_of_proj(p), + M.shape_of_proj(p), ) }; } @@ -156,13 +184,13 @@ let rec holes = ); let simple_view = (~font_metrics, ~segment, ~settings: Settings.t): Node.t => { - let token_of_proj = _ => ""; /* Assume this doesn't contain projectors */ - let map = Measured.of_segment(segment, token_of_proj); + let shape_of_proj = _ => Base.Inline(0); /* Assume this doesn't contain projectors */ + let map = Measured.of_segment(segment, shape_of_proj); module Text = Text({ let map = map; let settings = settings; - let token_of_proj = token_of_proj; + let shape_of_proj = shape_of_proj; }); let holes = holes(~map, ~font_metrics, segment); div( diff --git a/src/haz3lweb/app/editors/code/CodeViewable.re b/src/haz3lweb/app/editors/code/CodeViewable.re index 675d13e0a5..2b81b056bf 100644 --- a/src/haz3lweb/app/editors/code/CodeViewable.re +++ b/src/haz3lweb/app/editors/code/CodeViewable.re @@ -12,14 +12,14 @@ let view = ~buffer_ids, ~segment, ~holes, - ~token_of_proj, + ~shape_of_proj, ) : Node.t => { module Text = Code.Text({ let map = measured; let settings = globals.settings; - let token_of_proj = token_of_proj; + let shape_of_proj = shape_of_proj; }); let code = Text.of_segment(buffer_ids, false, sort, segment); let holes = List.map(Code.of_hole(~measured, ~globals), holes); @@ -51,8 +51,8 @@ let view = // }; let view_segment = - (~globals: Globals.t, ~sort: Sort.t, ~token_of_proj, segment: Segment.t) => { - let measured = Measured.of_segment(segment, token_of_proj); + (~globals: Globals.t, ~sort: Sort.t, ~shape_of_proj, segment: Segment.t) => { + let measured = Measured.of_segment(segment, shape_of_proj); let buffer_ids = []; let holes = Segment.holes(segment); view( @@ -62,21 +62,21 @@ let view_segment = ~buffer_ids, ~holes, ~segment, - ~token_of_proj, + ~shape_of_proj, ); }; let view_exp = (~dynamics, ~globals: Globals.t, ~settings, ~info_map, exp: Exp.t) => { - let token_of_proj = Projector.token_of_proj(info_map, dynamics); + let shape_of_proj = Projector.shape_of_proj(info_map, dynamics); exp |> ExpToSegment.exp_to_segment(~settings) - |> view_segment(~token_of_proj, ~globals, ~sort=Exp); + |> view_segment(~shape_of_proj, ~globals, ~sort=Exp); }; let view_typ = (~globals: Globals.t, ~settings, ~info_map, typ: Typ.t) => { - let token_of_proj = Projector.token_of_proj(info_map, Dynamics.Map.empty); + let shape_of_proj = Projector.shape_of_proj(info_map, Dynamics.Map.empty); typ |> ExpToSegment.typ_to_segment(~settings) - |> view_segment(~token_of_proj, ~globals, ~sort=Typ); + |> view_segment(~shape_of_proj, ~globals, ~sort=Typ); }; diff --git a/src/haz3lweb/app/editors/code/CodeWithStatics.re b/src/haz3lweb/app/editors/code/CodeWithStatics.re index a6bb65a7ad..335a5a3271 100644 --- a/src/haz3lweb/app/editors/code/CodeWithStatics.re +++ b/src/haz3lweb/app/editors/code/CodeWithStatics.re @@ -98,7 +98,7 @@ module View = { }, _, }: Model.t = model; - let token_of_proj = Projector.token_of_proj(info_map, dynamics); + let shape_of_proj = Projector.shape_of_proj(info_map, dynamics); let code_text_view = CodeViewable.view( ~globals, @@ -107,7 +107,7 @@ module View = { ~buffer_ids=Selection.is_buffer(z.selection) ? selection_ids : [], ~segment, ~holes, - ~token_of_proj, + ~shape_of_proj, ); let statics_decos = { module Deco = diff --git a/src/haz3lweb/app/editors/decoration/BackpackView.re b/src/haz3lweb/app/editors/decoration/BackpackView.re index 5ab468e0bf..6431e29b98 100644 --- a/src/haz3lweb/app/editors/decoration/BackpackView.re +++ b/src/haz3lweb/app/editors/decoration/BackpackView.re @@ -3,16 +3,16 @@ open Node; open Haz3lcore; open Util; -let token_of_proj = _ => ""; /* Assume this doesn't contain projectors */ +let shape_of_proj = _ => Base.Inline(0); /* Assume this doesn't contain projectors */ -let measured_of = seg => Measured.of_segment(seg, token_of_proj); +let measured_of = seg => Measured.of_segment(seg, shape_of_proj); let text_view = (seg: Segment.t): list(Node.t) => { module Text = Code.Text({ let map = measured_of(seg); let settings = Settings.Model.init; - let token_of_proj = token_of_proj; + let shape_of_proj = shape_of_proj; }); Text.of_segment([], true, Any, seg); }; diff --git a/src/haz3lweb/app/editors/decoration/Deco.re b/src/haz3lweb/app/editors/decoration/Deco.re index 014d6d6f2f..d593129d54 100644 --- a/src/haz3lweb/app/editors/decoration/Deco.re +++ b/src/haz3lweb/app/editors/decoration/Deco.re @@ -122,7 +122,7 @@ module HighlightSegment = switch (Measured.find_pr_opt(p, M.measured)) { | None => failwith("Deco.of_projector: missing measurement") | Some(_m) => - let token = Projector.token_of_proj(M.info_map, M.dynamics, p); + let shape = Projector.shape_of_proj(M.info_map, M.dynamics, p); /* Handling this internal to ProjectorsView at the moment because the * commented-out strategy doesn't work well, since the inserted str8- * edged lines vertical edge placement doesn't account for whether @@ -133,7 +133,13 @@ module HighlightSegment = // m, // (Some(Convex), Some(Convex)), // ); - let num_lb = StringUtil.num_linebreaks(token); + let num_lb = + //TODO: Better NewInline handling + switch (shape) { + | Base.Inline(_) => 0 + | Base.NewInline({col: height, _}) => height - 1 + | Base.Block({col: height, _}) => height - 1 + }; if (num_lb == 0) { [ Some( diff --git a/src/haz3lweb/app/editors/result/EvalResult.re b/src/haz3lweb/app/editors/result/EvalResult.re index 840f2b0961..682bdc5862 100644 --- a/src/haz3lweb/app/editors/result/EvalResult.re +++ b/src/haz3lweb/app/editors/result/EvalResult.re @@ -572,8 +572,8 @@ module View = { text("Evaluation disabled, showing elaboration:"), switch (Model.get_elaboration(model)) { | Some(elab) => - let token_of_proj = - Projector.token_of_proj( + let shape_of_proj = + Projector.shape_of_proj( Statics.Map.empty, Model.dynamics(model), ); @@ -584,7 +584,7 @@ module View = { Settings.of_core(~inline=false, globals.settings.core), ) ) - |> CodeViewable.view_segment(~globals, ~sort=Exp, ~token_of_proj); + |> CodeViewable.view_segment(~globals, ~sort=Exp, ~shape_of_proj); | None => text("No elaboration found") }, ]; diff --git a/src/haz3lweb/app/explainthis/ExplainThis.re b/src/haz3lweb/app/explainthis/ExplainThis.re index 5e47c4da83..afb8242a7e 100644 --- a/src/haz3lweb/app/explainthis/ExplainThis.re +++ b/src/haz3lweb/app/explainthis/ExplainThis.re @@ -278,7 +278,7 @@ let expander_deco = CodeViewable.view_segment( ~globals, ~sort=Exp, - ~token_of_proj=_ => "", // Assume no projectors + ~shape_of_proj=_ => Base.Inline(0), // Assume no projectors segment, ); let classes = diff --git a/src/haz3lweb/www/style/projectors/base.css b/src/haz3lweb/www/style/projectors/base.css index 6c87d49309..4dbbd59f9c 100644 --- a/src/haz3lweb/www/style/projectors/base.css +++ b/src/haz3lweb/www/style/projectors/base.css @@ -4,8 +4,8 @@ @import "probe.css"; @import "cards.css"; -/* Turn off caret when a block projector is focused */ -#caret:has(~ .projectors .projector.block *:focus) { +/* Turn off caret when a projector is focused */ +#caret:has(~ .projectors .projector *:focus) { display: none; } @@ -32,11 +32,6 @@ fill: var(--shard_projector); } -.projector.block > svg { - stroke-width: 0.5px; - stroke: var(--BR2); -} - .projector.indicated > svg { fill: var(--shard-caret-exp); } @@ -45,14 +40,8 @@ vector-effect: non-scaling-stroke; } -.projector.block.selected > svg > path { - vector-effect: non-scaling-stroke; - stroke: var(--Y3); -} - .projector.selected > svg, -.projector.selected.indicated > svg, -.projector.block.selected.indicated > svg { +.projector.selected.indicated > svg { fill: var(--shard-selected); } @@ -119,6 +108,11 @@ cursor: default; } +.projector.text > svg { + stroke-width: 0.5px; + stroke: var(--BR2); +} + .projector.text .wrapper { height: 100%; width: 100%; diff --git a/src/haz3lweb/www/style/projectors/cards.css b/src/haz3lweb/www/style/projectors/cards.css index 095190b5d2..27fd5f45ed 100644 --- a/src/haz3lweb/www/style/projectors/cards.css +++ b/src/haz3lweb/www/style/projectors/cards.css @@ -1,4 +1,4 @@ -/* CARD SPRITE */ +/* CARD SPRITES */ /* Turn off caret when a block projector is focused */ #caret:has(~ .projectors .projector.card.indicated), @@ -6,37 +6,65 @@ display: none; } -.projector.card > div:has(> .card-sprite) { - display: flex; - flex-direction: row; - width: 100%; - height: 100%; +.projector.card { + perspective: 300px; } +.projector.card.selected { + filter: brightness(0.72) sepia(1) hue-rotate(12deg) saturate(1.5); +} .projector.card { - perspective: 300px; + z-index: var(--projector-z); /* hack? to get above selection shard */ +} + +.projector.card > svg, +.projector.card.indicated > svg { + display: none; } -.outer { +/* Singleton */ +.projector.card > .card-outer { width: 100%; height: 100%; + display: flex; + flex-direction: row; } .projector.card .card-scene { width: 100%; height: 100%; - transition: transform 0.3s; + transition: transform 0.25s; transform-style: preserve-3d; position: relative; cursor: pointer; } +@keyframes flip-card { + 0% { + /* transform: none; */ + transform: rotateY(180deg) translateX(6px); + } + 50% { + transform: rotateY(160deg); + } + 75% { + transform: rotateY(70deg); + } + 100% { + transform: none; + } +} + .projector.card .flipped .card-scene { - transform: rotateY(180deg); + /* transform: rotateY(180deg) translateX(6px); */ + /* play once: */ + transform: rotateY(180deg) translateX(6px); + /* animation: flip-card 0.35s; + animation-direction: alternate-reverse; */ } .card-sprite.back { - transform: rotateY(180deg) rotateX(9deg); + transform: rotateY(180deg); } .projector.card .card-sprite { @@ -50,23 +78,10 @@ height: 47px; image-rendering: pixelated; background-image: url("../../img/cards-pixel-pattern.png"); - filter: drop-shadow(-1px 0px 0px black) drop-shadow(0px -1px 0px black) + filter: drop-shadow(-0.5px 0px 0px black) drop-shadow(0px -0.5px 0px black) drop-shadow(1px 0px 0px black) drop-shadow(0px 1px 0px black); } -@keyframes rock-back-and-forth { - 0% { - transform: rotate(0deg); - } - 50% { - transform: rotate(4deg); - } -} - -.projector.card.indicated .hand .card-scene:hover { - animation: rock-back-and-forth 0.25s infinite; -} - .projector.card.Pat .card-sprite { filter: drop-shadow(-1px 0px 0px var(--PAT)) drop-shadow(0px -1px 0px var(--PAT)) drop-shadow(1px 0px 0px var(--PAT)) @@ -111,25 +126,18 @@ } } -.projector.card.indicated.Left > .card-sprite, -.projector.card.indicated.Left .hand > *:first-child > .card-sprite { - animation: blink-shadow 1s infinite; +.projector.card.indicated.Left > .card-outer > .card-scene > .card-sprite, +.projector.card.indicated.Left .hand > *:first-child .card-sprite { + animation: blink-shadow 1s infinite !important; } .projector.card.indicated.Right .card-sprite { - animation: blink-shadow-right 1s infinite; + animation: blink-shadow-right 1s infinite !important; } -.projector.card.selected { - filter: brightness(0.72) sepia(1) hue-rotate(12deg) saturate(1.5); -} - -.projector.card > svg, -.projector.card.indicated > svg { - display: none; -} +/* HAND */ -.projector.card .hand { +.card .hand { display: flex; flex-direction: row; gap: 2px; @@ -137,6 +145,19 @@ height: 100%; } +@keyframes rock-back-and-forth { + 0% { + transform: rotate(0deg); + } + 50% { + transform: rotate(4deg); + } +} + +.projector.card.indicated .hand .card-scene:hover { + animation: rock-back-and-forth 0.25s infinite; +} + .projector.card .hand .card-sprite { transition: all 0.1s ease-in-out; } @@ -144,10 +165,12 @@ .projector.card.indicated .hand .card-sprite:hover { translate: 0px -9px; filter: drop-shadow(-1px 0px 0px black) drop-shadow(0px -1px 0px black) - drop-shadow(1px 0px 0px black) drop-shadow(0px 1px 0px black) + drop-shadow(1.5px 0px 0px black) drop-shadow(0px 1.5px 0px black) drop-shadow(2px 2px 0px #6666); } +/* CHOOSER */ + .projector.card .chooser { display: grid; grid-template-columns: repeat(13, 1fr); @@ -157,11 +180,6 @@ align-items: flex-start; } -.projector.card .card-wrapper { - width: 100%; - height: 100%; -} - .projector.card .chooser .card-wrapper:hover { animation: rock-back-and-forth 0.25s infinite; } @@ -171,4 +189,5 @@ } .projector.card .chooser .card-wrapper:hover { filter: invert(1); + translate: 0px -5px; } From 63010cf9169baa21b362b13635374a10ea60779d Mon Sep 17 00:00:00 2001 From: disconcision Date: Sat, 4 Jan 2025 00:25:53 -0500 Subject: [PATCH 07/13] new projector shape: tab. these can be multiline, but unlike Block they defer their linebreaks to the next linebreak --- src/haz3lcore/Measured.re | 40 +++++------- src/haz3lcore/tiles/Base.re | 10 +-- src/haz3lcore/tiles/ProjectorShape.re | 27 ++++++++ src/haz3lcore/zipper/Editor.re | 3 +- src/haz3lcore/zipper/Printer.re | 2 +- src/haz3lcore/zipper/Projector.re | 9 +-- src/haz3lcore/zipper/projectors/CardProj.re | 9 ++- .../zipper/projectors/CheckboxProj.re | 2 +- src/haz3lcore/zipper/projectors/FoldProj.re | 2 +- src/haz3lcore/zipper/projectors/InfoProj.re | 4 +- src/haz3lcore/zipper/projectors/ProbeProj.re | 2 +- .../zipper/projectors/SliderFProj.re | 2 +- src/haz3lcore/zipper/projectors/SliderProj.re | 2 +- .../zipper/projectors/TextAreaProj.re | 8 +-- src/haz3lweb/app/common/ProjectorView.re | 26 +++----- src/haz3lweb/app/editors/code/Code.re | 24 +++---- .../app/editors/decoration/BackpackView.re | 2 +- src/haz3lweb/app/editors/decoration/Deco.re | 9 ++- src/haz3lweb/app/explainthis/ExplainThis.re | 2 +- src/haz3lweb/www/style/projectors/cards.css | 64 ++++++++----------- 20 files changed, 124 insertions(+), 125 deletions(-) create mode 100644 src/haz3lcore/tiles/ProjectorShape.re diff --git a/src/haz3lcore/Measured.re b/src/haz3lcore/Measured.re index 0c3c5a0266..8836f69fb9 100644 --- a/src/haz3lcore/Measured.re +++ b/src/haz3lcore/Measured.re @@ -344,33 +344,27 @@ let of_segment = (contained_indent, last, map); | Projector(p) => let extra_rows_of_shape = (shape: Base.shape, origin, map) => - switch (shape) { - | Inline(_) => map - | NewInline({row: height, _}) => - let num_lb = height - 1; - if (num_lb > 0) { - deferred_linebreaks := [num_lb, ...deferred_linebreaks^]; - }; + switch (shape.vertical) { + | Inline + | Tab(0) + | Block(0) => map + | Tab(num_lb) => + deferred_linebreaks := [num_lb, ...deferred_linebreaks^]; map; - | Block({row: height, _}) => - let num_lb = height - 1; + | Block(num_lb) => let row_indent = container_indent + contained_indent; - let num_extra_rows = - num_lb + num_lb == 0 ? 0 : consume_deferred_linebreaks(); + let num_extra_rows = num_lb + consume_deferred_linebreaks(); add_n_rows(origin, row_indent, num_extra_rows, map); }; - let last_of_shape = (shape: Base.shape, origin: Point.t): Point.t => - switch (shape) { - | Inline(width) => {col: origin.col + width, row: origin.row} - | NewInline({col: width, _}) => { - col: origin.col + width, - row: origin.row, - } - | Block({col: width, row: height}) => { - col: origin.col + width, - row: origin.row + height - 1, - } - }; + let last_of_shape = (shape: Base.shape, origin: Point.t): Point.t => { + col: origin.col + shape.horizontal, + row: + switch (shape.vertical) { + | Inline => origin.row + | Tab(_num_lb) => origin.row + | Block(num_lb) => origin.row + num_lb + }, + }; let shape = shape_of_proj(p); let last = last_of_shape(shape, origin); let map = extra_rows_of_shape(shape, origin, map); diff --git a/src/haz3lcore/tiles/Base.re b/src/haz3lcore/tiles/Base.re index abff103604..84fa430704 100644 --- a/src/haz3lcore/tiles/Base.re +++ b/src/haz3lcore/tiles/Base.re @@ -1,4 +1,5 @@ open Util; +open ProjectorShape; /* The different kinds of projector. New projectors * types need to be registered here in order to be @@ -14,15 +15,8 @@ type kind = | Card | TextArea; -/* Projectors currently have two options for placeholder - * shapes: A inline display of a given length, or a block - * display with given length & height. Both of these can - * depend on the projector model and info package */ [@deriving (show({with_path: false}), sexp, yojson)] -type shape = - | Inline(int) - | NewInline(Point.t) - | Block(Point.t); +type shape = ProjectorShape.t; [@deriving (show({with_path: false}), sexp, yojson)] type segment = list(piece) diff --git a/src/haz3lcore/tiles/ProjectorShape.re b/src/haz3lcore/tiles/ProjectorShape.re new file mode 100644 index 0000000000..4ee99bce13 --- /dev/null +++ b/src/haz3lcore/tiles/ProjectorShape.re @@ -0,0 +1,27 @@ +open Util; + +/* A projector shape determines the space left for + * that projector, and how text flows around a projector + * in a text editor. All projectors have a horizontal + * extend (in characters), and the vertical extent may + * be either 1 character (Inline), or it may insert + * an additional number of linebreaks, either immediately + * after the projector (Block style) or defer them to + * the next linebreak (Tab style). In the latter case, + * if there are multiple Tab projectors on a line, the + * total extra linebreaks inserted is the maxium required + * to accomodate them */ +[@deriving (show({with_path: false}), sexp, yojson)] +type vertical = + | Inline + | Tab(int) + | Block(int); + +[@deriving (show({with_path: false}), sexp, yojson)] +type t = { + horizontal: int, + vertical, +}; + +let inline = (width: int): t => {horizontal: width, vertical: Inline}; +let default: t = inline(0); diff --git a/src/haz3lcore/zipper/Editor.re b/src/haz3lcore/zipper/Editor.re index 8005869a2f..a8fd599097 100644 --- a/src/haz3lcore/zipper/Editor.re +++ b/src/haz3lcore/zipper/Editor.re @@ -97,7 +97,8 @@ module Model = { col_target: None, }, history: History.empty, - syntax: CachedSyntax.init(zipper, ~shape_of_proj=_ => Base.Inline(0)), + syntax: + CachedSyntax.init(zipper, ~shape_of_proj=_ => ProjectorShape.default), }; type persistent = PersistentZipper.t; diff --git a/src/haz3lcore/zipper/Printer.re b/src/haz3lcore/zipper/Printer.re index 48b50b52c2..3a48fff39c 100644 --- a/src/haz3lcore/zipper/Printer.re +++ b/src/haz3lcore/zipper/Printer.re @@ -58,7 +58,7 @@ let measured = z => z |> ZipperBase.remove_all_projectors |> Zipper.seg_without_buffer - |> Measured.of_segment(_, _ => Base.Inline(0)); // No projectors + |> Measured.of_segment(_, _ => ProjectorShape.default); // No projectors let pretty_print = (~holes: option(string)=Some(""), z: Zipper.t): string => to_rows( diff --git a/src/haz3lcore/zipper/Projector.re b/src/haz3lcore/zipper/Projector.re index d3f27c96e8..dc306f3b39 100644 --- a/src/haz3lcore/zipper/Projector.re +++ b/src/haz3lcore/zipper/Projector.re @@ -53,10 +53,11 @@ let shape_of_proj = }; let token_of_shape = (shape: shape): string => - switch (shape) { - | Inline(width) => String.make(width, ' ') - | NewInline({row: _, col: width}) => String.make(width, ' ') - | Block({row, col}) => String.make(row - 1, '\n') ++ String.make(col, ' ') + switch (shape.vertical) { + | Inline + | Tab(_) => String.make(shape.horizontal, ' ') + | Block(num_lb) => + String.make(num_lb, '\n') ++ String.make(shape.horizontal, ' ') }; /* Returns a token consisting of whitespace (possibly including linebreaks) diff --git a/src/haz3lcore/zipper/projectors/CardProj.re b/src/haz3lcore/zipper/projectors/CardProj.re index 99713564fe..99782e720e 100644 --- a/src/haz3lcore/zipper/projectors/CardProj.re +++ b/src/haz3lcore/zipper/projectors/CardProj.re @@ -415,7 +415,7 @@ module Singleton = { Node.div( ~attrs=[ Attr.classes( - ["card-outer"] + ["card-wrapper"] @ ( switch (mode) { | Show => ["show"] @@ -461,7 +461,7 @@ module CardInHand = { Node.div( ~attrs=[ Attr.classes( - ["card-outer"] + ["card-wrapper"] @ ( switch (mode) { | Show => ["show"] @@ -533,7 +533,10 @@ module M: Projector = { let can_focus = false; let dynamics = false; let placeholder = (_, info) => - Base.NewInline({row: 2, col: Syntax.width_of_piece(info.syntax)}); + ProjectorShape.{ + horizontal: Syntax.width_of_piece(info.syntax), + vertical: Tab(1), + }; let update = (_model, action) => switch (action) { | SetMode(mode) => {mode: mode} diff --git a/src/haz3lcore/zipper/projectors/CheckboxProj.re b/src/haz3lcore/zipper/projectors/CheckboxProj.re index 4e4cab8ede..3a0f39ab03 100644 --- a/src/haz3lcore/zipper/projectors/CheckboxProj.re +++ b/src/haz3lcore/zipper/projectors/CheckboxProj.re @@ -53,7 +53,7 @@ module M: Projector = { let can_project = p => state_of(p) != None; let can_focus = false; let dynamics = false; - let placeholder = (_, _) => Base.Inline(2); + let placeholder = (_, _) => ProjectorShape.inline(2); let update = (model, _) => model; let view = view; let focus = _ => (); diff --git a/src/haz3lcore/zipper/projectors/FoldProj.re b/src/haz3lcore/zipper/projectors/FoldProj.re index 4d6c97f1e5..5942d6653c 100644 --- a/src/haz3lcore/zipper/projectors/FoldProj.re +++ b/src/haz3lcore/zipper/projectors/FoldProj.re @@ -19,7 +19,7 @@ module M: Projector = { let can_focus = false; let dynamics = false; let placeholder = (m, _) => - Base.Inline(m.text == "⋱" ? 2 : m.text |> String.length); + ProjectorShape.inline(m.text == "⋱" ? 2 : m.text |> String.length); let update = (m, _) => m; let view = (m: model, ~info as _, ~local as _, ~parent, ~utility as _) => div( diff --git a/src/haz3lcore/zipper/projectors/InfoProj.re b/src/haz3lcore/zipper/projectors/InfoProj.re index 9874aa9c30..9d65060355 100644 --- a/src/haz3lcore/zipper/projectors/InfoProj.re +++ b/src/haz3lcore/zipper/projectors/InfoProj.re @@ -69,7 +69,9 @@ module M: Projector = { display_ty(model, info) |> totalize_ty |> Typ.pretty_print; let placeholder = (model, info) => - Base.Inline((display(model, info.statics) |> String.length) + 5); + ProjectorShape.inline( + (display(model, info.statics) |> String.length) + 5, + ); let update = (model, a: action) => switch (a, model) { diff --git a/src/haz3lcore/zipper/projectors/ProbeProj.re b/src/haz3lcore/zipper/projectors/ProbeProj.re index 5a95e497ba..9c3271a3d1 100644 --- a/src/haz3lcore/zipper/projectors/ProbeProj.re +++ b/src/haz3lcore/zipper/projectors/ProbeProj.re @@ -307,7 +307,7 @@ let syntax_str = (info: info) => { let syntax_view = (info: info) => info |> syntax_str |> text; let placeholder = (_m, info) => - Base.Inline(3 + String.length(syntax_str(info))); + ProjectorShape.inline(3 + String.length(syntax_str(info))); // let icon = div(~attrs=[Attr.classes(["icon"])], [text("🔍")]); let icon = div(~attrs=[Attr.classes(["icon"])], []); diff --git a/src/haz3lcore/zipper/projectors/SliderFProj.re b/src/haz3lcore/zipper/projectors/SliderFProj.re index 5b570f3d62..04a4ee3690 100644 --- a/src/haz3lcore/zipper/projectors/SliderFProj.re +++ b/src/haz3lcore/zipper/projectors/SliderFProj.re @@ -25,7 +25,7 @@ module M: Projector = { let can_project = p => get_opt(p) != None; let can_focus = false; let dynamics = false; - let placeholder = (_, _) => Base.Inline(10); + let placeholder = (_, _) => ProjectorShape.inline(10); let update = (model, _) => model; let view = ( diff --git a/src/haz3lcore/zipper/projectors/SliderProj.re b/src/haz3lcore/zipper/projectors/SliderProj.re index 1a8cc44087..f3681fd40f 100644 --- a/src/haz3lcore/zipper/projectors/SliderProj.re +++ b/src/haz3lcore/zipper/projectors/SliderProj.re @@ -22,7 +22,7 @@ module M: Projector = { let can_project = p => get_opt(p) != None; let can_focus = false; let dynamics = false; - let placeholder = (_, _) => Base.Inline(10); + let placeholder = (_, _) => ProjectorShape.inline(10); let update = (model, _) => model; let view = ( diff --git a/src/haz3lcore/zipper/projectors/TextAreaProj.re b/src/haz3lcore/zipper/projectors/TextAreaProj.re index e92a737191..55bb0caec1 100644 --- a/src/haz3lcore/zipper/projectors/TextAreaProj.re +++ b/src/haz3lcore/zipper/projectors/TextAreaProj.re @@ -101,11 +101,11 @@ module M: Projector = { let dynamics = false; let placeholder = (_, info) => { let str = Form.strip_quotes(get(info.syntax)); - Base.Block({ - row: StringUtil.num_lines(str), + ProjectorShape.{ + vertical: Block(StringUtil.num_linebreaks(str)), /* +2 for left and right padding */ - col: 2 + StringUtil.max_line_width(str), - }); + horizontal: 2 + StringUtil.max_line_width(str), + }; }; let update = (model, _) => model; let view = view; diff --git a/src/haz3lweb/app/common/ProjectorView.re b/src/haz3lweb/app/common/ProjectorView.re index 235984b2f9..619539c18a 100644 --- a/src/haz3lweb/app/common/ProjectorView.re +++ b/src/haz3lweb/app/common/ProjectorView.re @@ -42,21 +42,12 @@ let of_name = (p: string): Base.kind => * to token decorations. This can be made transparent * in the CSS if no backing is wanted */ let backing_deco = - ( - ~font_metrics: FontMetrics.t, - ~measurement: Measured.measurement, - ~shape: shape, - ) => - switch (shape) { - | Inline(_) - | NewInline(_) - | Block(_) => - PieceDec.relative_shard({ - font_metrics, - measurement, - tips: (Some(Convex), Some(Convex)), - }) - }; + (~font_metrics: FontMetrics.t, ~measurement: Measured.measurement) => + PieceDec.relative_shard({ + font_metrics, + measurement, + tips: (Some(Convex), Some(Convex)), + }); /* Adds attributes to a projector UI to support * custom styling when selected or indicated */ @@ -86,7 +77,6 @@ let view_wrapper = ) => { let sort = Option.map(Info.sort_of, info.statics) |> Option.value(~default=Sort.Exp); - let shape = Projector.shape(p, info); let focus = (id, _) => Effect.(Many([Stop_propagation, inject(Project(Focus(id, None)))])); div( @@ -97,7 +87,7 @@ let view_wrapper = Attr.on_mousedown(focus(info.id)), DecUtil.abs_style(measurement, ~font_metrics), ], - [view, backing_deco(~font_metrics, ~measurement, ~shape)], + [view, backing_deco(~font_metrics, ~measurement)], ); }; @@ -149,7 +139,7 @@ let collate_utility = CodeViewable.view_segment( ~globals, ~sort, - ~shape_of_proj=_ => Base.Inline(0), + ~shape_of_proj=_ => ProjectorShape.default, seg, ), exp_to_seg: exp => diff --git a/src/haz3lweb/app/editors/code/Code.re b/src/haz3lweb/app/editors/code/Code.re index bda52527ab..dce5f2b92e 100644 --- a/src/haz3lweb/app/editors/code/Code.re +++ b/src/haz3lweb/app/editors/code/Code.re @@ -82,20 +82,16 @@ let of_secondary = let of_projector = (expected_sort, indent, shape: Base.shape) => { let token = - switch (shape) { - | Inline(_) => Projector.token_of_shape(shape) - | NewInline({row: height, _}) => - let num_lb = height - 1; - if (num_lb > 0) { - deferred_linebreaks := [num_lb, ...deferred_linebreaks^]; - }; + switch (shape.vertical) { + | Inline + | Tab(0) + | Block(0) => Projector.token_of_shape(shape) + | Tab(num_lb) => + deferred_linebreaks := [num_lb, ...deferred_linebreaks^]; Projector.token_of_shape(shape); - | Block({row: height, _}) => - let num_lb = height - 1; - num_lb == 0 - ? "" - : String.make(consume_deferred_linebreaks(), '\n') - ++ Projector.token_of_shape(shape); + | Block(_) => + String.make(consume_deferred_linebreaks(), '\n') + ++ Projector.token_of_shape(shape) }; of_delim'(([token], false, expected_sort, true, true, indent, 0)); }; @@ -184,7 +180,7 @@ let rec holes = ); let simple_view = (~font_metrics, ~segment, ~settings: Settings.t): Node.t => { - let shape_of_proj = _ => Base.Inline(0); /* Assume this doesn't contain projectors */ + let shape_of_proj = _ => ProjectorShape.default; /* Assume this doesn't contain projectors */ let map = Measured.of_segment(segment, shape_of_proj); module Text = Text({ diff --git a/src/haz3lweb/app/editors/decoration/BackpackView.re b/src/haz3lweb/app/editors/decoration/BackpackView.re index 6431e29b98..f1379e40d3 100644 --- a/src/haz3lweb/app/editors/decoration/BackpackView.re +++ b/src/haz3lweb/app/editors/decoration/BackpackView.re @@ -3,7 +3,7 @@ open Node; open Haz3lcore; open Util; -let shape_of_proj = _ => Base.Inline(0); /* Assume this doesn't contain projectors */ +let shape_of_proj = _ => ProjectorShape.default; /* Assume this doesn't contain projectors */ let measured_of = seg => Measured.of_segment(seg, shape_of_proj); diff --git a/src/haz3lweb/app/editors/decoration/Deco.re b/src/haz3lweb/app/editors/decoration/Deco.re index d593129d54..06b07270f9 100644 --- a/src/haz3lweb/app/editors/decoration/Deco.re +++ b/src/haz3lweb/app/editors/decoration/Deco.re @@ -134,11 +134,10 @@ module HighlightSegment = // (Some(Convex), Some(Convex)), // ); let num_lb = - //TODO: Better NewInline handling - switch (shape) { - | Base.Inline(_) => 0 - | Base.NewInline({col: height, _}) => height - 1 - | Base.Block({col: height, _}) => height - 1 + switch (shape.vertical) { + | Inline => 0 + | Tab(num_lbs) => num_lbs + | Block(num_lbs) => num_lbs }; if (num_lb == 0) { [ diff --git a/src/haz3lweb/app/explainthis/ExplainThis.re b/src/haz3lweb/app/explainthis/ExplainThis.re index afb8242a7e..2595abb1d5 100644 --- a/src/haz3lweb/app/explainthis/ExplainThis.re +++ b/src/haz3lweb/app/explainthis/ExplainThis.re @@ -278,7 +278,7 @@ let expander_deco = CodeViewable.view_segment( ~globals, ~sort=Exp, - ~shape_of_proj=_ => Base.Inline(0), // Assume no projectors + ~shape_of_proj=_ => ProjectorShape.default, // Assume no projectors segment, ); let classes = diff --git a/src/haz3lweb/www/style/projectors/cards.css b/src/haz3lweb/www/style/projectors/cards.css index 27fd5f45ed..876c8cd19d 100644 --- a/src/haz3lweb/www/style/projectors/cards.css +++ b/src/haz3lweb/www/style/projectors/cards.css @@ -1,5 +1,10 @@ /* CARD SPRITES */ +:root { + --card-width: 35px; + --card-height: 47px; +} + /* Turn off caret when a block projector is focused */ #caret:has(~ .projectors .projector.card.indicated), .indication:has(~ .projectors .projector.card.indicated) { @@ -23,16 +28,17 @@ } /* Singleton */ -.projector.card > .card-outer { +.projector.card > .card-wrapper { width: 100%; height: 100%; display: flex; flex-direction: row; + justify-content: center; } .projector.card .card-scene { - width: 100%; - height: 100%; + width: var(--card-width); + height: var(--card-height); transition: transform 0.25s; transform-style: preserve-3d; position: relative; @@ -41,8 +47,7 @@ @keyframes flip-card { 0% { - /* transform: none; */ - transform: rotateY(180deg) translateX(6px); + transform: rotateY(180deg); } 50% { transform: rotateY(160deg); @@ -56,9 +61,7 @@ } .projector.card .flipped .card-scene { - /* transform: rotateY(180deg) translateX(6px); */ - /* play once: */ - transform: rotateY(180deg) translateX(6px); + transform: rotateY(180deg); /* animation: flip-card 0.35s; animation-direction: alternate-reverse; */ } @@ -74,8 +77,8 @@ justify-content: flex-start; align-items: flex-start; cursor: pointer; - width: 35px; - height: 47px; + width: var(--card-width); + height: var(--card-height); image-rendering: pixelated; background-image: url("../../img/cards-pixel-pattern.png"); filter: drop-shadow(-0.5px 0px 0px black) drop-shadow(0px -0.5px 0px black) @@ -89,49 +92,38 @@ } @keyframes blink-shadow { - 0% { - filter: drop-shadow(-2px 0px 0px red) drop-shadow(1px 0px 0px black) - drop-shadow(0px 1px 0px black) drop-shadow(0px -1px 0px black); - } + 0%, 50% { - filter: drop-shadow(-2px 0px 0px red) drop-shadow(1px 0px 0px black) - drop-shadow(0px 1px 0px black) drop-shadow(0px -1px 0px black); - } - 51% { - filter: drop-shadow(-1px 0px 0px black) drop-shadow(1px 0px 0px black) - drop-shadow(0px 1px 0px black) drop-shadow(0px -1px 0px black); + filter: drop-shadow(0px -0.5px 0px black) drop-shadow(1px 0px 0px black) + drop-shadow(0px 1px 0px black) drop-shadow(-2px 0px 0px red); } + 51%, 100% { - filter: drop-shadow(-1px 0px 0px black) drop-shadow(1px 0px 0px black) - drop-shadow(0px 1px 0px black) drop-shadow(0px -1px 0px black); + filter: drop-shadow(-0.5px 0px 0px black) drop-shadow(0px -0.5px 0px black) + drop-shadow(1px 0px 0px black) drop-shadow(0px 1px 0px black); } } @keyframes blink-shadow-right { - 0% { - filter: drop-shadow(2px 0px 0px red) drop-shadow(-1px 0px 0px black) - drop-shadow(0px 1px 0px black) drop-shadow(0px -1px 0px black); - } + 0%, 50% { - filter: drop-shadow(2px 0px 0px red) drop-shadow(-1px 0px 0px black) - drop-shadow(0px 1px 0px black) drop-shadow(0px -1px 0px black); - } - 51% { - filter: drop-shadow(1px 0px 0px black) drop-shadow(-1px 0px 0px black) - drop-shadow(0px 1px 0px black) drop-shadow(0px -1px 0px black); + filter: drop-shadow(-0.5px 0px 0px black) drop-shadow(0px -0.5px 0px black) + drop-shadow(0px 1px 0px black) drop-shadow(2px 0px 0px red); } + 51%, 100% { - filter: drop-shadow(1px 0px 0px black) drop-shadow(-1px 0px 0px black) - drop-shadow(0px 1px 0px black) drop-shadow(0px -1px 0px black); + filter: drop-shadow(-0.5px 0px 0px black) drop-shadow(0px -0.5px 0px black) + drop-shadow(1px 0px 0px black) drop-shadow(0px 1px 0px black); } } -.projector.card.indicated.Left > .card-outer > .card-scene > .card-sprite, +.projector.card.indicated.Left > .card-wrapper > .card-scene > .card-sprite, .projector.card.indicated.Left .hand > *:first-child .card-sprite { animation: blink-shadow 1s infinite !important; } -.projector.card.indicated.Right .card-sprite { +.projector.card.indicated.Right > .card-wrapper > .card-scene > .card-sprite, +.projector.card.indicated.Right .hand > *:last-child .card-sprite { animation: blink-shadow-right 1s infinite !important; } From beb0dc95f3c165580c294f1ad2ab676f36025dd5 Mon Sep 17 00:00:00 2001 From: disconcision Date: Sun, 5 Jan 2025 16:17:52 -0500 Subject: [PATCH 08/13] FLIP technique based element transition animations. Currently applied to the caret --- src/haz3lcore/Animate.re | 130 ++++++++++++++++++++ src/haz3lcore/tiles/Base.re | 1 - src/haz3lcore/zipper/action/Perform.re | 12 +- src/haz3lcore/zipper/projectors/CardProj.re | 31 ++++- src/haz3lweb/Main.re | 7 +- src/haz3lweb/www/style/projectors/base.css | 4 +- src/haz3lweb/www/style/projectors/cards.css | 4 +- src/util/JsUtil.re | 19 +-- 8 files changed, 188 insertions(+), 20 deletions(-) create mode 100644 src/haz3lcore/Animate.re diff --git a/src/haz3lcore/Animate.re b/src/haz3lcore/Animate.re new file mode 100644 index 0000000000..453db23fa0 --- /dev/null +++ b/src/haz3lcore/Animate.re @@ -0,0 +1,130 @@ +open Util; +open Js_of_ocaml; + +/* Position & dimensions for a DOM element */ +[@deriving (show({with_path: false}), sexp, yojson)] +type box = { + top: int, + left: int, + height: float, + width: float, +}; + +/* Options for CSS Animations API */ +type options = { + duration: int, + easing: string, +}; + +/* CSS property-value pairs as strings */ +type keyframe = (string, string); + +/* Specify a transition for an element */ +type transition = { + id: string, + options, + animation: option(box) => list(keyframe), +}; + +type transition_internal = { + id: string, + options, + animation: option(box) => list(keyframe), + box: option(box), +}; + +let tracked_elems: ref(list(transition_internal)) = ref([]); + +let animate = + (id: string, keyframes: list((string, string)), options: options): unit => { + let elem = JsUtil.get_elem_by_id(id); + + let keyframe_objs = + keyframes + |> List.map(((prop, value)) => + Js.Unsafe.obj([|(prop, Js.Unsafe.inject(Js.string(value)))|]) + ) + |> Array.of_list + |> Js.array; + + let options_obj = + [ + ("duration", Js.Unsafe.inject(options.duration)), + ("easing", Js.Unsafe.inject(Js.string(options.easing))), + ] + |> Array.of_list + |> Js.Unsafe.obj; + + Js.Unsafe.meth_call( + elem, + "animate", + [|Js.Unsafe.inject(keyframe_objs), Js.Unsafe.inject(options_obj)|], + ); +}; + +let box_of = (elem: Js.t(Dom_html.element)): box => { + let container_rect = elem##getBoundingClientRect; + { + top: int_of_float(container_rect##.top), + left: int_of_float(container_rect##.left), + height: Js.Optdef.get(container_rect##.height, _ => (-1.0)), + width: Js.Optdef.get(container_rect##.width, _ => (-1.0)), + }; +}; + +let get_box = (id: string): option(box) => + switch (JsUtil.get_elem_by_id_opt(id)) { + | Some(elem) => Some(box_of(elem)) + | None => None + }; + +let delta_box = (init: box, final: box): box => { + left: final.left - init.left, + top: final.top - init.top, + width: final.width -. init.width, + height: final.height -. init.height, +}; + +let delta_box_opt = (init: option(box), final: option(box)): option(box) => + switch (final, init) { + | (Some(final), Some(init)) => Some(delta_box(init, final)) + | _ => None + }; + +let go = (): unit => + if (tracked_elems^ != []) { + tracked_elems^ + |> List.iter(({id, box, options, animation}) => + animate(id, animation(delta_box_opt(get_box(id), box)), options) + ); + tracked_elems := []; + }; + +let setup = (transitions: list(transition)): unit => { + tracked_elems := + List.map( + ({id, options, animation}: transition) => + {id, box: get_box(id), options, animation}, + transitions, + ); +}; + +module Keyframes = { + let transform_translate = (top: int, left: int) => ( + "transform", + Printf.sprintf("translate(%dpx, %dpx)", left, top), + ); + let transform_scale_uniform = (scale: float) => ( + "transform", + Printf.sprintf("scale(%f, %f)", scale, scale), + ); + let translate = (delta: option(box)): list(keyframe) => + switch (delta) { + | None => + // Scale up newly inserted elements + [transform_scale_uniform(0.0), transform_scale_uniform(1.0)] + | Some({left, top, _}) => + // Translate elements that exist in both states + [transform_translate(top, left), transform_translate(0, 0)] + }; +}; diff --git a/src/haz3lcore/tiles/Base.re b/src/haz3lcore/tiles/Base.re index 84fa430704..d6e2bb037e 100644 --- a/src/haz3lcore/tiles/Base.re +++ b/src/haz3lcore/tiles/Base.re @@ -1,5 +1,4 @@ open Util; -open ProjectorShape; /* The different kinds of projector. New projectors * types need to be registered here in order to be diff --git a/src/haz3lcore/zipper/action/Perform.re b/src/haz3lcore/zipper/action/Perform.re index 89c9059ac1..c030bcade7 100644 --- a/src/haz3lcore/zipper/action/Perform.re +++ b/src/haz3lcore/zipper/action/Perform.re @@ -113,7 +113,17 @@ let go_z = z, ) | Move(d) => - Move.go(d, z) |> Result.of_option(~error=Action.Failure.Cant_move) + Animate.setup([ + { + id: "caret", + animation: Animate.Keyframes.translate, + options: { + duration: 125, + easing: "cubic-bezier(0.16, 1, 0.3, 1)", + }, + }, + ]); + Move.go(d, z) |> Result.of_option(~error=Action.Failure.Cant_move); | Jump(jump_target) => ( switch (jump_target) { diff --git a/src/haz3lcore/zipper/projectors/CardProj.re b/src/haz3lcore/zipper/projectors/CardProj.re index 99782e720e..6d2378377c 100644 --- a/src/haz3lcore/zipper/projectors/CardProj.re +++ b/src/haz3lcore/zipper/projectors/CardProj.re @@ -440,6 +440,7 @@ module Singleton = { module CardInHand = { let view = ( + _elem_ids, mode, parent, local: action => Ui_effect.t(unit), @@ -455,7 +456,10 @@ module CardInHand = { | Flipped => local(SetMode(Show)) | Show => local(SetMode(Choose)) } - | _ => Effect.Ignore + | _ => + // Animate.setup(elem_ids); + // local(SetMode(Flipped)); + Effect.Ignore }; Node.div( @@ -483,10 +487,21 @@ module CardInHand = { }; }; +let of_id = (id: Id.t) => + "id" ++ (id |> Id.to_string |> String.sub(_, 0, 8)); +// return a list of strings "card-index-" for cards in hand +let hand_elem_ids = (id, hand: hand): list(string) => + List.mapi( + (i, _) => of_id(id) ++ "card-index-" ++ string_of_int(i), + hand, + ); + module Hand = { // a card, but each subsequent card should be absoluted positioned 20px to the right of the last and higher in z-index: let card_wrapper = ( + id, + elem_ids, mode, parent: external_action => Ui_effect.t(unit), local: action => Ui_effect.t(unit), @@ -497,23 +512,27 @@ module Hand = { : Node.t => Node.div( ~attrs=[ + Attr.id(of_id(id) ++ "card-index-" ++ string_of_int(index)), Attr.class_("card-wrapper"), Attr.create( "style", Printf.sprintf( "position: absolute; left: %dpx; z-index: %d;", - index * 8, + mode == Flipped ? 0 : index * 8, 100 + index, ), ), ], - [CardInHand.view(mode, parent, local, sort, card)], + [CardInHand.view(elem_ids, mode, parent, local, sort, card)], ); - let view = (mode, parent, local, sort: Sort.t, hand: hand): Node.t => { + let view = (id, mode, parent, local, sort: Sort.t, hand: hand): Node.t => { Node.div( ~attrs=[Attr.classes(["hand", Sort.show(sort)])], - List.mapi(card_wrapper(mode, parent, local, sort), hand), + List.mapi( + card_wrapper(id, hand_elem_ids(id, hand), mode, parent, local, sort), + hand, + ), ); }; }; @@ -553,7 +572,7 @@ module M: Projector = { | (sort, Card(card)) => Singleton.view(model.mode, parent, local, to_sort(sort), card) | (sort, Hand(hand)) => - Hand.view(model.mode, parent, local, to_sort(sort), hand) + Hand.view(info.id, model.mode, parent, local, to_sort(sort), hand) }; }; let focus = _ => (); diff --git a/src/haz3lweb/Main.re b/src/haz3lweb/Main.re index 747cff3098..30af3af5b3 100644 --- a/src/haz3lweb/Main.re +++ b/src/haz3lweb/Main.re @@ -161,11 +161,14 @@ let start = { // Triggers after every update let after_display = { Bonsai.Effect.of_sync_fun( - () => + () => { if (scroll_to_caret.contents) { scroll_to_caret := false; JsUtil.scroll_cursor_into_view_if_needed(); - }, + }; + Haz3lcore.Animate.go(); + (); + }, (), ); }; diff --git a/src/haz3lweb/www/style/projectors/base.css b/src/haz3lweb/www/style/projectors/base.css index 4dbbd59f9c..a4426ffad3 100644 --- a/src/haz3lweb/www/style/projectors/base.css +++ b/src/haz3lweb/www/style/projectors/base.css @@ -5,8 +5,8 @@ @import "cards.css"; /* Turn off caret when a projector is focused */ -#caret:has(~ .projectors .projector *:focus) { - display: none; +#caret:has(~ .projectors .projector *:focus) .caret-path { + fill: #0000; } /* Default projector styles */ diff --git a/src/haz3lweb/www/style/projectors/cards.css b/src/haz3lweb/www/style/projectors/cards.css index 876c8cd19d..6840aa3ce5 100644 --- a/src/haz3lweb/www/style/projectors/cards.css +++ b/src/haz3lweb/www/style/projectors/cards.css @@ -6,7 +6,9 @@ } /* Turn off caret when a block projector is focused */ -#caret:has(~ .projectors .projector.card.indicated), +#caret:has(~ .projectors .projector.card.indicated) .caret-path { + fill: #0000; +} .indication:has(~ .projectors .projector.card.indicated) { display: none; } diff --git a/src/util/JsUtil.re b/src/util/JsUtil.re index e84c34b69e..fbf4e62a88 100644 --- a/src/util/JsUtil.re +++ b/src/util/JsUtil.re @@ -3,15 +3,15 @@ open Virtual_dom.Vdom; let get_elem_by_id = id => { let doc = Dom_html.document; - Js.Opt.get( - doc##getElementById(Js.string(id)), - () => { - print_endline(id); - assert(false); - }, - ); + Js.Opt.get(doc##getElementById(Js.string(id)), () => {assert(false)}); }; +let get_elem_by_id_opt = id => + switch (get_elem_by_id(id)) { + | exception _ => None + | e => Some(e) + }; + let get_elem_by_selector = selector => { let doc = Dom_html.document; Js.Opt.get( @@ -23,6 +23,11 @@ let get_elem_by_selector = selector => { ); }; +let request_frame = kont => { + let _ = Dom_html.window##requestAnimationFrame(Js.wrap_callback(kont)); + (); +}; + let get_child_with_class = (element: Js.t(Dom_html.element), className) => { let rec loop = (sibling: Js.t(Dom_html.element)) => if (Js.to_bool(sibling##.classList##contains(Js.string(className)))) { From 5f544ca42fdd24415e76ad6bb599d1e56cbd1f89 Mon Sep 17 00:00:00 2001 From: disconcision Date: Mon, 6 Jan 2025 23:45:15 -0500 Subject: [PATCH 09/13] animation cleanup --- src/haz3lcore/Animate.re | 130 ------------- src/haz3lcore/Animation.re | 198 ++++++++++++++++++++ src/haz3lcore/dynamics/PatternMatch.re | 10 +- src/haz3lcore/zipper/action/Perform.re | 11 +- src/haz3lcore/zipper/projectors/CardProj.re | 5 +- src/haz3lweb/Main.re | 3 +- src/haz3lweb/app/editors/code/Code.re | 8 + 7 files changed, 210 insertions(+), 155 deletions(-) delete mode 100644 src/haz3lcore/Animate.re create mode 100644 src/haz3lcore/Animation.re diff --git a/src/haz3lcore/Animate.re b/src/haz3lcore/Animate.re deleted file mode 100644 index 453db23fa0..0000000000 --- a/src/haz3lcore/Animate.re +++ /dev/null @@ -1,130 +0,0 @@ -open Util; -open Js_of_ocaml; - -/* Position & dimensions for a DOM element */ -[@deriving (show({with_path: false}), sexp, yojson)] -type box = { - top: int, - left: int, - height: float, - width: float, -}; - -/* Options for CSS Animations API */ -type options = { - duration: int, - easing: string, -}; - -/* CSS property-value pairs as strings */ -type keyframe = (string, string); - -/* Specify a transition for an element */ -type transition = { - id: string, - options, - animation: option(box) => list(keyframe), -}; - -type transition_internal = { - id: string, - options, - animation: option(box) => list(keyframe), - box: option(box), -}; - -let tracked_elems: ref(list(transition_internal)) = ref([]); - -let animate = - (id: string, keyframes: list((string, string)), options: options): unit => { - let elem = JsUtil.get_elem_by_id(id); - - let keyframe_objs = - keyframes - |> List.map(((prop, value)) => - Js.Unsafe.obj([|(prop, Js.Unsafe.inject(Js.string(value)))|]) - ) - |> Array.of_list - |> Js.array; - - let options_obj = - [ - ("duration", Js.Unsafe.inject(options.duration)), - ("easing", Js.Unsafe.inject(Js.string(options.easing))), - ] - |> Array.of_list - |> Js.Unsafe.obj; - - Js.Unsafe.meth_call( - elem, - "animate", - [|Js.Unsafe.inject(keyframe_objs), Js.Unsafe.inject(options_obj)|], - ); -}; - -let box_of = (elem: Js.t(Dom_html.element)): box => { - let container_rect = elem##getBoundingClientRect; - { - top: int_of_float(container_rect##.top), - left: int_of_float(container_rect##.left), - height: Js.Optdef.get(container_rect##.height, _ => (-1.0)), - width: Js.Optdef.get(container_rect##.width, _ => (-1.0)), - }; -}; - -let get_box = (id: string): option(box) => - switch (JsUtil.get_elem_by_id_opt(id)) { - | Some(elem) => Some(box_of(elem)) - | None => None - }; - -let delta_box = (init: box, final: box): box => { - left: final.left - init.left, - top: final.top - init.top, - width: final.width -. init.width, - height: final.height -. init.height, -}; - -let delta_box_opt = (init: option(box), final: option(box)): option(box) => - switch (final, init) { - | (Some(final), Some(init)) => Some(delta_box(init, final)) - | _ => None - }; - -let go = (): unit => - if (tracked_elems^ != []) { - tracked_elems^ - |> List.iter(({id, box, options, animation}) => - animate(id, animation(delta_box_opt(get_box(id), box)), options) - ); - tracked_elems := []; - }; - -let setup = (transitions: list(transition)): unit => { - tracked_elems := - List.map( - ({id, options, animation}: transition) => - {id, box: get_box(id), options, animation}, - transitions, - ); -}; - -module Keyframes = { - let transform_translate = (top: int, left: int) => ( - "transform", - Printf.sprintf("translate(%dpx, %dpx)", left, top), - ); - let transform_scale_uniform = (scale: float) => ( - "transform", - Printf.sprintf("scale(%f, %f)", scale, scale), - ); - let translate = (delta: option(box)): list(keyframe) => - switch (delta) { - | None => - // Scale up newly inserted elements - [transform_scale_uniform(0.0), transform_scale_uniform(1.0)] - | Some({left, top, _}) => - // Translate elements that exist in both states - [transform_translate(top, left), transform_translate(0, 0)] - }; -}; diff --git a/src/haz3lcore/Animation.re b/src/haz3lcore/Animation.re new file mode 100644 index 0000000000..db8f7265fc --- /dev/null +++ b/src/haz3lcore/Animation.re @@ -0,0 +1,198 @@ +open Util; +open Js_of_ocaml; + +/* This implements arbitrary gpu-accelerated css position + * and scale transition animations via the the FLIP technique + * (https://aerotwist.com/blog/flip-your-animations/). + * + * From the client perspective, it suffices to call the request + * method with a list of the DOM element ids to animate, as well + * as some animation settings (keyframes, duration, easing). + * + * Some common keyframes are provided in the module at the bottom */ + +/* This is an extremely partial implementation of the Web Animations + * API, which currently does not have Js_of_ocaml wrappers */ +module Js = { + /* CSS property-value pairs */ + type keyframe = (string, string); + + type options = { + duration: int, + easing: string, + }; + + /* Options for CSS Animations API animate method */ + type animation = { + options, + keyframes: list(keyframe), + }; + + /* Position & dimensions for a DOM element */ + type box = { + top: int, + left: int, + height: float, + width: float, + }; + + let box_of = (elem: Js.t(Dom_html.element)): box => { + let container_rect = elem##getBoundingClientRect; + { + top: int_of_float(container_rect##.top), + left: int_of_float(container_rect##.left), + height: Js.Optdef.get(container_rect##.height, _ => (-1.0)), + width: Js.Optdef.get(container_rect##.width, _ => (-1.0)), + }; + }; + + let get_elem_box = (id: string): option(box) => + Option.map(box_of, JsUtil.get_elem_by_id_opt(id)); + + let keyframes_unsafe = (keyframes: list(keyframe)): Js.t(Js.js_array('a)) => + keyframes + |> List.map(((prop: string, value: string)) => + Js.Unsafe.obj([|(prop, Js.Unsafe.inject(Js.string(value)))|]) + ) + |> Array.of_list + |> Js.array; + + let options_unsafe = ({duration, easing}: options): Js.t(Js.js_array('a)) => + [ + ("duration", Js.Unsafe.inject(duration)), + ("easing", Js.Unsafe.inject(Js.string(easing))), + ] + |> Array.of_list + |> Js.Unsafe.obj; + + let animate_unsafe = + ( + keyframes: list(keyframe), + options: options, + elem: Js.t(Dom_html.element), + ) => + Js.Unsafe.meth_call( + elem, + "animate", + [| + Js.Unsafe.inject(keyframes_unsafe(keyframes)), + Js.Unsafe.inject(options_unsafe(options)), + |], + ); + + let animate = ({options, keyframes}, elem: Js.t(Dom_html.element)) => + if (keyframes != []) { + switch (animate_unsafe(keyframes, options, elem)) { + | exception exn => + print_endline("Animation: " ++ Printexc.to_string(exn)) + | () => () + }; + }; +}; + +open Js; + +/* If an element is new, report its new metrics. + * Otherwise, report both new & old metrics */ +type change = + | New(box) + //| Removed(box) + | Existing(box, box); + +/* Specify a transition for an element */ +type transition = { + /* A unique id used as attribute for + * the relevant DOM element */ + id: string, + /* The animation function recieves the diffs + * for the element's position and scale across a + * change, which it may use to calculate the + * parameters for a resulting animation */ + animate: change => animation, +}; + +/* Internally, transitions must track the initial + * metrics for an element, gathered in the `Request ` phase */ +type transition_internal = { + id: string, + animate: change => animation, + box: option(box), +}; + +/* Elements and their corresponding animations are tracked + * here between when the action is used (`request`) and + * when the animation is executed (`go`) */ +let tracked_elems: ref(list(transition_internal)) = ref([]); + +let animate_elem = ({id, box, animate}, elem): unit => + switch (box, get_elem_box(id)) { + | (Some(init), Some(final)) => + Js.animate(animate(Existing(init, final)), elem) + | (None, Some(final)) => Js.animate(animate(New(final)), elem) + | (Some(_init), None) => + //TODO: Removed case (requires retaining old element somehow) + () + | (None, None) => () + }; + +/* Execute animations. This is called during the + * render phase, after recalc but before repaint */ +let go = (): unit => + if (tracked_elems^ != []) { + tracked_elems^ + |> List.iter(animation => + JsUtil.get_elem_by_id_opt(animation.id) + |> Option.iter(animate_elem(animation)) + ); + tracked_elems := []; + }; + +/* Request animations. Call this during the MVU update */ +let request = (transitions: list(transition)): unit => { + tracked_elems := + List.map( + ({id, animate}: transition) => {id, box: get_elem_box(id), animate}, + transitions, + ); +}; + +module Keyframes = { + let transform_translate = (top: int, left: int): keyframe => ( + "transform", + Printf.sprintf("translate(%dpx, %dpx)", left, top), + ); + + let translate = (init: box, final: box): list(keyframe) => { + [ + transform_translate(init.top - final.top, init.left - final.left), + transform_translate(0, 0), + ]; + }; + + let transform_scale_uniform = (scale: float): keyframe => ( + "transform", + Printf.sprintf("scale(%f, %f)", scale, scale), + ); + + let scale_from_zero: list(keyframe) = [ + transform_scale_uniform(0.0), + transform_scale_uniform(1.0), + ]; +}; + +module Actions = { + let move = id => { + id, + animate: change => { + options: { + duration: 125, + easing: "cubic-bezier(0.16, 1, 0.3, 1)", + }, + keyframes: + switch (change) { + | New(_) => Keyframes.scale_from_zero + | Existing(init, final) => Keyframes.translate(init, final) + }, + }, + }; +}; diff --git a/src/haz3lcore/dynamics/PatternMatch.re b/src/haz3lcore/dynamics/PatternMatch.re index 5894331656..060007295d 100644 --- a/src/haz3lcore/dynamics/PatternMatch.re +++ b/src/haz3lcore/dynamics/PatternMatch.re @@ -69,21 +69,13 @@ let rec matches = (dp: Pat.t, d: DHExp.t): match_result => let* d2 = Unboxing.unbox(SumWithArg(ctr), d); matches(p2, d2); | Ap(_, _) => IndetMatch // TODO: should this fail? - | Var(x) => - print_endline( - "id of pat var " - ++ x - ++ " being bound to d:" - ++ Id.to_string(IdTagged.rep_id(dp)), - ); - Matches(Environment.singleton((x, d))); + | Var(x) => Matches(Environment.singleton((x, d))) | Tuple(ps) => let* ds = Unboxing.unbox(Tuple(List.length(ps)), d); List.map2(matches, ps, ds) |> List.fold_left(combine_result, Matches(Environment.empty)); | Parens(p, Paren) => matches(p, d) | Parens(p, Probe(pr)) => - print_endline("Pattern Match: found pattern probe"); let inner_match = matches(p, d); capture_closure(pr, Term.Pat.rep_id(dp), d, inner_match); inner_match; diff --git a/src/haz3lcore/zipper/action/Perform.re b/src/haz3lcore/zipper/action/Perform.re index c030bcade7..f4f0c3d4ac 100644 --- a/src/haz3lcore/zipper/action/Perform.re +++ b/src/haz3lcore/zipper/action/Perform.re @@ -113,16 +113,7 @@ let go_z = z, ) | Move(d) => - Animate.setup([ - { - id: "caret", - animation: Animate.Keyframes.translate, - options: { - duration: 125, - easing: "cubic-bezier(0.16, 1, 0.3, 1)", - }, - }, - ]); + Animation.request([Animation.Actions.move("caret")]); Move.go(d, z) |> Result.of_option(~error=Action.Failure.Cant_move); | Jump(jump_target) => ( diff --git a/src/haz3lcore/zipper/projectors/CardProj.re b/src/haz3lcore/zipper/projectors/CardProj.re index 6d2378377c..3ae53a34e1 100644 --- a/src/haz3lcore/zipper/projectors/CardProj.re +++ b/src/haz3lcore/zipper/projectors/CardProj.re @@ -456,10 +456,7 @@ module CardInHand = { | Flipped => local(SetMode(Show)) | Show => local(SetMode(Choose)) } - | _ => - // Animate.setup(elem_ids); - // local(SetMode(Flipped)); - Effect.Ignore + | _ => Effect.Ignore }; Node.div( diff --git a/src/haz3lweb/Main.re b/src/haz3lweb/Main.re index 30af3af5b3..7889c8a823 100644 --- a/src/haz3lweb/Main.re +++ b/src/haz3lweb/Main.re @@ -166,8 +166,7 @@ let start = { scroll_to_caret := false; JsUtil.scroll_cursor_into_view_if_needed(); }; - Haz3lcore.Animate.go(); - (); + Haz3lcore.Animation.go(); }, (), ); diff --git a/src/haz3lweb/app/editors/code/Code.re b/src/haz3lweb/app/editors/code/Code.re index dce5f2b92e..5a86eea747 100644 --- a/src/haz3lweb/app/editors/code/Code.re +++ b/src/haz3lweb/app/editors/code/Code.re @@ -48,6 +48,12 @@ let of_delim' = let of_delim = (is_in_buffer, is_consistent, indent, t: Piece.tile, i: int) : list(Node.t) => + // [ + // div( + // ~attrs=[ + // Attr.create("style", "display: inline-block; position: relative;"), + // Attr.id("token-" ++ (t.id |> Id.to_string |> String.sub(_, 0, 8))), + // ], of_delim'(( t.label, is_in_buffer, @@ -57,6 +63,8 @@ let of_delim = indent, i, )); +// ), +// ]; let space = " "; //Unicode.nbsp; From 5e9efd7a7e35ed8e7eb90c1414d88352f8561c40 Mon Sep 17 00:00:00 2001 From: disconcision Date: Mon, 13 Jan 2025 20:11:01 -0500 Subject: [PATCH 10/13] cleanup --- src/haz3lcore/zipper/ProjectorBase.re | 1 - src/haz3lcore/zipper/action/ProjectorPerform.re | 3 +-- src/haz3lweb/app/editors/code/Code.re | 8 -------- src/haz3lweb/www/img/cards-pixel.png | Bin 15856 -> 0 bytes src/haz3lweb/www/img/cards.png | Bin 36095 -> 0 bytes src/util/JsUtil.re | 5 ----- 6 files changed, 1 insertion(+), 16 deletions(-) delete mode 100644 src/haz3lweb/www/img/cards-pixel.png delete mode 100644 src/haz3lweb/www/img/cards.png diff --git a/src/haz3lcore/zipper/ProjectorBase.re b/src/haz3lcore/zipper/ProjectorBase.re index 90e2b14279..a6619b613b 100644 --- a/src/haz3lcore/zipper/ProjectorBase.re +++ b/src/haz3lcore/zipper/ProjectorBase.re @@ -1,6 +1,5 @@ open Util; open Virtual_dom.Vdom; -include Base; [@deriving (show({with_path: false}), sexp, yojson)] type t = Base.kind; diff --git a/src/haz3lcore/zipper/action/ProjectorPerform.re b/src/haz3lcore/zipper/action/ProjectorPerform.re index 943996b12a..647802269a 100644 --- a/src/haz3lcore/zipper/action/ProjectorPerform.re +++ b/src/haz3lcore/zipper/action/ProjectorPerform.re @@ -73,7 +73,6 @@ let go = Ok(move_out_of_piece(d, rel, z) |> Update.add(p, Piece.id(piece))) } | ToggleIndicated(p) => - print_endline("ToggleIndicated: kind: " ++ (p |> Base.show_kind)); switch (Indicated.for_index(z)) { | None => Error(Cant_project) | Some((piece, d, rel)) => @@ -81,7 +80,7 @@ let go = move_out_of_piece(d, rel, z) |> Update.add_or_remove(p, Piece.id(piece)), ) - }; + } | Remove(id) => Ok(Update.remove(id, z)) | SetSyntax(id, syntax) => /* Note we update piece id to keep in sync with projector id; diff --git a/src/haz3lweb/app/editors/code/Code.re b/src/haz3lweb/app/editors/code/Code.re index bf96ded1bb..e7de89592f 100644 --- a/src/haz3lweb/app/editors/code/Code.re +++ b/src/haz3lweb/app/editors/code/Code.re @@ -48,12 +48,6 @@ let of_delim' = let of_delim = (is_in_buffer, is_consistent, indent, t: Piece.tile, i: int) : list(Node.t) => - // [ - // div( - // ~attrs=[ - // Attr.create("style", "display: inline-block; position: relative;"), - // Attr.id("token-" ++ (t.id |> Id.to_string |> String.sub(_, 0, 8))), - // ], of_delim'(( t.label, is_in_buffer, @@ -63,8 +57,6 @@ let of_delim = indent, i, )); -// ), -// ]; let space = " "; //Unicode.nbsp; diff --git a/src/haz3lweb/www/img/cards-pixel.png b/src/haz3lweb/www/img/cards-pixel.png deleted file mode 100644 index bd933387f37bf050ca4522a869caf24e0e9c4c6c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15856 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�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 Q!VOYa(Ry5}WFGkc0Qr&H82|tP diff --git a/src/haz3lweb/www/img/cards.png b/src/haz3lweb/www/img/cards.png deleted file mode 100644 index 54f8bd59623afa4a46bf1e61135559a1b1828ee6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36095 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 { ); }; -let request_frame = kont => { - let _ = Dom_html.window##requestAnimationFrame(Js.wrap_callback(kont)); - (); -}; - let get_child_with_class = (element: Js.t(Dom_html.element), className) => { let rec loop = (sibling: Js.t(Dom_html.element)) => if (Js.to_bool(sibling##.classList##contains(Js.string(className)))) { From f8a90cd82b4ca0fcafd18bc5497eb3e8c3414468 Mon Sep 17 00:00:00 2001 From: disconcision Date: Mon, 13 Jan 2025 20:24:08 -0500 Subject: [PATCH 11/13] reconcile animation library with cards-anim-plus --- src/haz3lcore/Animation.re | 74 +++++++++++++++++++++++++++----------- 1 file changed, 54 insertions(+), 20 deletions(-) diff --git a/src/haz3lcore/Animation.re b/src/haz3lcore/Animation.re index db8f7265fc..eddf2cad6b 100644 --- a/src/haz3lcore/Animation.re +++ b/src/haz3lcore/Animation.re @@ -30,8 +30,8 @@ module Js = { /* Position & dimensions for a DOM element */ type box = { - top: int, - left: int, + top: float, + left: float, height: float, width: float, }; @@ -39,15 +39,26 @@ module Js = { let box_of = (elem: Js.t(Dom_html.element)): box => { let container_rect = elem##getBoundingClientRect; { - top: int_of_float(container_rect##.top), - left: int_of_float(container_rect##.left), + top: container_rect##.top, + left: container_rect##.left, height: Js.Optdef.get(container_rect##.height, _ => (-1.0)), width: Js.Optdef.get(container_rect##.width, _ => (-1.0)), }; }; - let get_elem_box = (id: string): option(box) => - Option.map(box_of, JsUtil.get_elem_by_id_opt(id)); + let client_height = (): float => + Js.Optdef.get( + Js.Unsafe.get(Dom_html.document, "documentElement")##.clientHeight, _ => + 0.0 + ); + + let inner_height = (): float => + Js.Optdef.get(Js.Unsafe.get(Dom_html.window, "innerHeight"), _ => 0.0); + + let check_visible = (client_height, inner_height, box: box): bool => { + let viewHeight = max(client_height, inner_height); + !(box.top +. box.height < 0.0 || box.top -. viewHeight >= 0.0); + }; let keyframes_unsafe = (keyframes: list(keyframe)): Js.t(Js.js_array('a)) => keyframes @@ -124,8 +135,8 @@ type transition_internal = { * when the animation is executed (`go`) */ let tracked_elems: ref(list(transition_internal)) = ref([]); -let animate_elem = ({id, box, animate}, elem): unit => - switch (box, get_elem_box(id)) { +let animate_elem = (({box, animate, _}, elem, new_box)): unit => + switch (box, new_box) { | (Some(init), Some(final)) => Js.animate(animate(Existing(init, final)), elem) | (None, Some(final)) => Js.animate(animate(New(final)), elem) @@ -135,15 +146,28 @@ let animate_elem = ({id, box, animate}, elem): unit => | (None, None) => () }; +let filter_visible_elements = (tracked_elems: list(transition_internal)) => { + let client_height = client_height(); + let inner_height = inner_height(); + List.filter_map( + (tr: transition_internal) => { + switch (JsUtil.get_elem_by_id_opt(tr.id)) { + | None => None + | Some(elem) => + let new_box = box_of(elem); + check_visible(client_height, inner_height, new_box) + ? Some((tr, elem, Some(new_box))) : None; + } + }, + tracked_elems, + ); +}; + /* Execute animations. This is called during the * render phase, after recalc but before repaint */ let go = (): unit => if (tracked_elems^ != []) { - tracked_elems^ - |> List.iter(animation => - JsUtil.get_elem_by_id_opt(animation.id) - |> Option.iter(animate_elem(animation)) - ); + tracked_elems^ |> filter_visible_elements |> List.iter(animate_elem); tracked_elems := []; }; @@ -151,21 +175,27 @@ let go = (): unit => let request = (transitions: list(transition)): unit => { tracked_elems := List.map( - ({id, animate}: transition) => {id, box: get_elem_box(id), animate}, + ({id, animate}: transition) => + { + id, + box: Option.map(box_of, JsUtil.get_elem_by_id_opt(id)), + animate, + }, transitions, - ); + ) + @ tracked_elems^; }; module Keyframes = { - let transform_translate = (top: int, left: int): keyframe => ( + let transform_translate = (top: float, left: float): keyframe => ( "transform", - Printf.sprintf("translate(%dpx, %dpx)", left, top), + Printf.sprintf("translate(%fpx, %fpx)", left, top), ); let translate = (init: box, final: box): list(keyframe) => { [ - transform_translate(init.top - final.top, init.left - final.left), - transform_translate(0, 0), + transform_translate(init.top -. final.top, init.left -. final.left), + transform_translate(0., 0.), ]; }; @@ -180,13 +210,17 @@ module Keyframes = { ]; }; +let easeOutExpo = "cubic-bezier(0.16, 1, 0.3, 1)"; +let easeInOutBack = "cubic-bezier(0.68, -0.6, 0.32, 1.6)"; +let easeInOutExpo = "cubic-bezier(0.87, 0, 0.13, 1)"; + module Actions = { let move = id => { id, animate: change => { options: { duration: 125, - easing: "cubic-bezier(0.16, 1, 0.3, 1)", + easing: easeOutExpo, }, keyframes: switch (change) { From 4f47eb7a3b7f280d4b65d298c1bf3670ea60a28a Mon Sep 17 00:00:00 2001 From: disconcision Date: Fri, 17 Jan 2025 14:00:27 -0500 Subject: [PATCH 12/13] update card proj to new proj API --- src/haz3lcore/zipper/projectors/CardProj.re | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/haz3lcore/zipper/projectors/CardProj.re b/src/haz3lcore/zipper/projectors/CardProj.re index 3ae53a34e1..988c905cc2 100644 --- a/src/haz3lcore/zipper/projectors/CardProj.re +++ b/src/haz3lcore/zipper/projectors/CardProj.re @@ -553,14 +553,14 @@ module M: Projector = { horizontal: Syntax.width_of_piece(info.syntax), vertical: Tab(1), }; - let update = (_model, action) => + let update = (_model, _, action) => switch (action) { | SetMode(mode) => {mode: mode} }; let view = ( model, - ~info, + info, ~local, ~parent: external_action => Ui_effect.t(unit), ~utility as _, @@ -572,5 +572,8 @@ module M: Projector = { Hand.view(info.id, model.mode, parent, local, to_sort(sort), hand) }; }; + let offside_view = None; + let overlay_view = None; + let underlay_view = None; let focus = _ => (); }; From d668a4407df347fe62f7cf7d2a5fd4169ad9b255 Mon Sep 17 00:00:00 2001 From: disconcision Date: Fri, 17 Jan 2025 14:59:43 -0500 Subject: [PATCH 13/13] style fix --- src/haz3lweb/www/style/projectors/cards.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/haz3lweb/www/style/projectors/cards.css b/src/haz3lweb/www/style/projectors/cards.css index 6840aa3ce5..5623c09293 100644 --- a/src/haz3lweb/www/style/projectors/cards.css +++ b/src/haz3lweb/www/style/projectors/cards.css @@ -6,10 +6,10 @@ } /* Turn off caret when a block projector is focused */ -#caret:has(~ .projectors .projector.card.indicated) .caret-path { +.code-deco:has(~ .projectors .projector.card.indicated) #caret .caret-path { fill: #0000; } -.indication:has(~ .projectors .projector.card.indicated) { +.code-deco:has(~ .projectors .projector.card.indicated) .indication { display: none; }