diff --git a/packages/doenetml-prototype/src/renderers/doenet/boolean.tsx b/packages/doenetml-prototype/src/renderers/doenet/boolean.tsx index c891d4024..1ffa0400b 100644 --- a/packages/doenetml-prototype/src/renderers/doenet/boolean.tsx +++ b/packages/doenetml-prototype/src/renderers/doenet/boolean.tsx @@ -1,8 +1,8 @@ import React from "react"; import { BasicComponent } from "../types"; -import type { BooleanProps } from "@doenet/doenetml-worker-rust"; +import type { BooleanPropsInText } from "@doenet/doenetml-worker-rust"; -type BooleanData = { props: BooleanProps }; +type BooleanData = { props: BooleanPropsInText }; export const Boolean: BasicComponent = ({ node }) => { return {node.data.props.value.toString()}; diff --git a/packages/doenetml-prototype/src/renderers/doenet/division.tsx b/packages/doenetml-prototype/src/renderers/doenet/division.tsx index 91b9f2d80..309c9c6b2 100644 --- a/packages/doenetml-prototype/src/renderers/doenet/division.tsx +++ b/packages/doenetml-prototype/src/renderers/doenet/division.tsx @@ -1,11 +1,11 @@ import React from "react"; import { BasicComponentWithPassthroughChildren } from "../types"; import { Element } from "../element"; -import type { DivisionProps } from "@doenet/doenetml-worker-rust"; +import type { DivisionPropsInText } from "@doenet/doenetml-worker-rust"; import { generateHtmlId } from "../utils"; export const Division: BasicComponentWithPassthroughChildren<{ - props: DivisionProps; + props: DivisionPropsInText; }> = ({ children, node, visibilityRef, annotation, ancestors }) => { const htmlId = generateHtmlId(node, annotation, ancestors); const titleElmId = node.data.props.title; diff --git a/packages/doenetml-prototype/src/renderers/pretext-xml/line.tsx b/packages/doenetml-prototype/src/renderers/doenet/graph-line.tsx similarity index 88% rename from packages/doenetml-prototype/src/renderers/pretext-xml/line.tsx rename to packages/doenetml-prototype/src/renderers/doenet/graph-line.tsx index 481a6af4d..dd93ae94f 100644 --- a/packages/doenetml-prototype/src/renderers/pretext-xml/line.tsx +++ b/packages/doenetml-prototype/src/renderers/doenet/graph-line.tsx @@ -2,11 +2,16 @@ import React from "react"; import { BasicComponent } from "../types"; import { GraphContext, LAYER_OFFSETS } from "./graph"; import * as JSG from "jsxgraph"; -import { attachStandardGraphListeners } from "./jsxgraph/listeners"; +import { + attachStandardGraphListeners, + GraphListeners, + removeStandardGraphListeners, +} from "./jsxgraph/listeners"; export const LineInGraph: BasicComponent = ({ node }) => { const board = React.useContext(GraphContext); const lineRef = React.useRef(null); + const lineListenersAttached = React.useRef({}); React.useEffect(() => { if (!board) { @@ -36,15 +41,11 @@ export const LineInGraph: BasicComponent = ({ node }) => { return; } - attachStandardGraphListeners(line); + // TODO: actually create the line listeners and actions + lineListenersAttached.current = attachStandardGraphListeners(line, {}); return () => { - line.off("drag"); - line.off("down"); - line.off("hit"); - line.off("up"); - line.off("keyfocusout"); - line.off("keydown"); + removeStandardGraphListeners(line, lineListenersAttached.current); board.removeObject(line); }; }, [board, lineRef]); diff --git a/packages/doenetml-prototype/src/renderers/doenet/graph-point.tsx b/packages/doenetml-prototype/src/renderers/doenet/graph-point.tsx new file mode 100644 index 000000000..bdb30496e --- /dev/null +++ b/packages/doenetml-prototype/src/renderers/doenet/graph-point.tsx @@ -0,0 +1,172 @@ +import React from "react"; +import { BasicComponent } from "../types"; +import { GraphContext, LAYER_OFFSETS } from "./graph"; +import * as JSG from "jsxgraph"; +import { + attachStandardGraphListeners, + GraphListenerActions, + GraphListeners, + removeStandardGraphListeners, +} from "./jsxgraph/listeners"; +import { Action, PointPropsInGraph } from "@doenet/doenetml-worker-rust"; +import { useAppDispatch } from "../../state/hooks"; +import { coreActions } from "../../state/redux-slices/core"; +import { numberFromSerializedAst } from "../../utils/math/math-expression-utils"; + +type PointData = { props: PointPropsInGraph }; + +export const PointInGraph: BasicComponent = ({ node }) => { + const board = React.useContext(GraphContext); + const pointRef = React.useRef(null); + const pointListenersActions = React.useRef({}); + const pointListenersAttached = React.useRef({}); + const hadNonNumericCoords = React.useRef(false); + const id = node.data.id; + + const dispatch = useAppDispatch(); + + const x: number = numberFromSerializedAst(node.data.props.x.math_object); + const y: number = numberFromSerializedAst(node.data.props.y.math_object); + + React.useEffect(() => { + if (!board) { + pointRef.current = null; + return; + } + if (pointRef.current) { + return; + } + const point = createPoint(board, { + coords: [x, y], + labelForGraph: "test", + lineColor: "var(--mainPurple)", + hidden: false, + fixed: false, + draggable: true, + fixLocation: false, + layer: 0, + selectedStyle: { lineStyle: "solid", lineOpacity: 1, lineWidth: 2 }, + dashed: false, + }); + pointRef.current = point; + if (!point) { + return; + } + + // TODO: call actions when point moves + + pointListenersActions.current.drag = function (e, interactionState) { + let action: Action = { + component: "point", + actionName: "move", + componentIdx: id, + args: { x: pointRef.current!.X(), y: pointRef.current!.Y() }, + }; + dispatch(coreActions.dispatchAction(action)); + }; + + pointListenersAttached.current = attachStandardGraphListeners( + point, + pointListenersActions.current, + ); + + return () => { + removeStandardGraphListeners(point, pointListenersAttached.current); + board.removeObject(point); + }; + }, [board, pointRef]); + + if (!board || !pointRef.current) { + return null; + } + + // We have a pre-existing point. Update the rendered point so that it matches values from the props. + + if (pointRef.current.hasLabel) { + // the the point has a label, need to update it so that it moves if the point moves + pointRef.current.label!.needsUpdate = true; + pointRef.current.label!.update(); + } + + // move the point to the current location determined by the props + pointRef.current.coords.setCoordinates(JXG.COORDS_BY_USER, [1, x, y]); + + // update the point and the board so the point actually moves to the specified location + pointRef.current.needsUpdate = true; + // if the point previous had non-numeric coordinates, + // it appears that the point requires a fullUpdate to get it to reappear. + if (hadNonNumericCoords.current) { + //@ts-ignore + pointRef.current.fullUpdate(); + } else { + pointRef.current.update(); + } + + // record for next time whether or not we have non-numeric coordinates + hadNonNumericCoords.current = !Number.isFinite(x) || !Number.isFinite(y); + + board.updateRenderer(); + + return null; +}; + +function createPoint( + board: JSG.Board, + props: { + coords: [number, number]; + labelForGraph: string; + lineColor: string; + hidden: boolean; + fixed: boolean; + draggable: boolean; + fixLocation: boolean; + layer: number; + selectedStyle: { + lineStyle: string; + lineOpacity: number; + lineWidth: number; + }; + dashed: boolean; + }, +) { + const lineColor = props.lineColor; + + // Things to be passed to JSXGraph as attributes + const jsxPointAttributes: JSG.PointAttributes = { + name: props.labelForGraph, + visible: !props.hidden, + fixed: props.fixed, + layer: 10 * props.layer + LAYER_OFFSETS.line, + strokeColor: lineColor, + strokeOpacity: props.selectedStyle.lineOpacity, + highlightStrokeColor: lineColor, + highlightStrokeOpacity: props.selectedStyle.lineOpacity * 0.5, + strokeWidth: props.selectedStyle.lineWidth, + highlightStrokeWidth: props.selectedStyle.lineWidth, + dash: styleToDash(props.selectedStyle.lineStyle, props.dashed), + highlight: !props.fixLocation, + }; + + const point: JSG.Point = board.create( + "point", + props.coords, + jsxPointAttributes, + ); + + return point; +} + +/** + * Return the the dash length for a given style. + */ +function styleToDash(style: string, dash: boolean) { + if (style === "dashed" || dash) { + return 2; + } else if (style === "solid") { + return 0; + } else if (style === "dotted") { + return 1; + } else { + return 0; + } +} diff --git a/packages/doenetml-prototype/src/renderers/doenet/graph.tsx b/packages/doenetml-prototype/src/renderers/doenet/graph.tsx index cdba0f581..bbf42788b 100644 --- a/packages/doenetml-prototype/src/renderers/doenet/graph.tsx +++ b/packages/doenetml-prototype/src/renderers/doenet/graph.tsx @@ -15,7 +15,7 @@ import { BasicComponent } from "../types"; import "./graph.css"; import { Toolbar, ToolbarItem } from "@ariakit/react"; import { Element } from "../element"; -import { Action, GraphProps } from "@doenet/doenetml-worker-rust"; +import { Action, GraphPropsInText } from "@doenet/doenetml-worker-rust"; import { useAppDispatch } from "../../state/hooks"; import { coreActions } from "../../state/redux-slices/core"; import { arrayEq } from "../../utils/array"; @@ -40,7 +40,7 @@ export const LAYER_OFFSETS = { text: 6, }; -type GraphData = { props: GraphProps }; +type GraphData = { props: GraphPropsInText }; type BoundingBox = [x1: number, y1: number, x2: number, y2: number]; export const Graph: BasicComponent = ({ node }) => { diff --git a/packages/doenetml-prototype/src/renderers/doenet/li.tsx b/packages/doenetml-prototype/src/renderers/doenet/li.tsx index ef7a946f1..d06a43e0d 100644 --- a/packages/doenetml-prototype/src/renderers/doenet/li.tsx +++ b/packages/doenetml-prototype/src/renderers/doenet/li.tsx @@ -1,11 +1,11 @@ import React from "react"; import { BasicComponentWithPassthroughChildren } from "../types"; -import type { LiProps } from "@doenet/doenetml-worker-rust"; +import type { LiPropsInText } from "@doenet/doenetml-worker-rust"; import "./li.css"; import { generateHtmlId } from "../utils"; export const Li: BasicComponentWithPassthroughChildren<{ - props: LiProps; + props: LiPropsInText; }> = ({ children, node, annotation, ancestors }) => { const htmlId = generateHtmlId(node, annotation, ancestors); const label = node.data.props.label; diff --git a/packages/doenetml-prototype/src/renderers/doenet/math.tsx b/packages/doenetml-prototype/src/renderers/doenet/math.tsx index ff0bc79b9..4f122ede4 100644 --- a/packages/doenetml-prototype/src/renderers/doenet/math.tsx +++ b/packages/doenetml-prototype/src/renderers/doenet/math.tsx @@ -3,8 +3,9 @@ import { MathJax } from "better-react-mathjax"; import { BasicComponent } from "../types"; import { useAppSelector } from "../../state/hooks"; import { renderingOnServerSelector } from "../../state/redux-slices/global"; +import { MathPropsInText } from "@doenet/doenetml-worker-rust"; -type MathData = { props: { latex: string } }; +type MathData = { props: MathPropsInText }; export const Math: BasicComponent = ({ node }) => { const onServer = useAppSelector(renderingOnServerSelector); diff --git a/packages/doenetml-prototype/src/renderers/doenet/number.tsx b/packages/doenetml-prototype/src/renderers/doenet/number.tsx index aef5bd45b..d4d8dca30 100644 --- a/packages/doenetml-prototype/src/renderers/doenet/number.tsx +++ b/packages/doenetml-prototype/src/renderers/doenet/number.tsx @@ -1,8 +1,8 @@ import React from "react"; import { BasicComponent } from "../types"; -import type { NumberProps } from "@doenet/doenetml-worker-rust"; +import type { NumberPropsInText } from "@doenet/doenetml-worker-rust"; -type NumberData = { props: NumberProps }; +type NumberData = { props: NumberPropsInText }; export const Number: BasicComponent = ({ node }) => { return {node.data.props.text}; diff --git a/packages/doenetml-prototype/src/renderers/doenet/ol.tsx b/packages/doenetml-prototype/src/renderers/doenet/ol.tsx index 0d57d9cf0..6b8ac08ee 100644 --- a/packages/doenetml-prototype/src/renderers/doenet/ol.tsx +++ b/packages/doenetml-prototype/src/renderers/doenet/ol.tsx @@ -1,9 +1,9 @@ import React from "react"; import { BasicComponentWithPassthroughChildren } from "../types"; -import type { OlProps } from "@doenet/doenetml-worker-rust"; +import type { OlPropsInText } from "@doenet/doenetml-worker-rust"; export const Ol: BasicComponentWithPassthroughChildren<{ - props: OlProps; + props: OlPropsInText; }> = ({ children, node }) => { return
    {children}
; }; diff --git a/packages/doenetml-prototype/src/renderers/doenet/p.tsx b/packages/doenetml-prototype/src/renderers/doenet/p.tsx index ef24d8864..841ea0bb6 100644 --- a/packages/doenetml-prototype/src/renderers/doenet/p.tsx +++ b/packages/doenetml-prototype/src/renderers/doenet/p.tsx @@ -1,9 +1,9 @@ import React from "react"; import { BasicComponentWithPassthroughChildren } from "../types"; -import type { PProps } from "@doenet/doenetml-worker-rust"; +import type { PPropsInText } from "@doenet/doenetml-worker-rust"; -export const P: BasicComponentWithPassthroughChildren<{ props: PProps }> = ({ - children, -}) => { +export const P: BasicComponentWithPassthroughChildren<{ + props: PPropsInText; +}> = ({ children }) => { return
{children}
; }; diff --git a/packages/doenetml-prototype/src/renderers/doenet/point.tsx b/packages/doenetml-prototype/src/renderers/doenet/point.tsx index 74a052716..bdb30496e 100644 --- a/packages/doenetml-prototype/src/renderers/doenet/point.tsx +++ b/packages/doenetml-prototype/src/renderers/doenet/point.tsx @@ -8,12 +8,12 @@ import { GraphListeners, removeStandardGraphListeners, } from "./jsxgraph/listeners"; -import { Action, PointProps } from "@doenet/doenetml-worker-rust"; +import { Action, PointPropsInGraph } from "@doenet/doenetml-worker-rust"; import { useAppDispatch } from "../../state/hooks"; import { coreActions } from "../../state/redux-slices/core"; import { numberFromSerializedAst } from "../../utils/math/math-expression-utils"; -type PointData = { props: PointProps }; +type PointData = { props: PointPropsInGraph }; export const PointInGraph: BasicComponent = ({ node }) => { const board = React.useContext(GraphContext); diff --git a/packages/doenetml-prototype/src/renderers/doenet/text-input.tsx b/packages/doenetml-prototype/src/renderers/doenet/text-input.tsx index 35adb53f2..5e8ab2a8e 100644 --- a/packages/doenetml-prototype/src/renderers/doenet/text-input.tsx +++ b/packages/doenetml-prototype/src/renderers/doenet/text-input.tsx @@ -1,12 +1,15 @@ import React from "react"; -import type { Action, TextInputProps } from "@doenet/doenetml-worker-rust"; +import type { + Action, + TextInputPropsInText, +} from "@doenet/doenetml-worker-rust"; import { BasicComponent } from "../types"; import { useAppDispatch, useAppSelector } from "../../state/hooks"; import { renderingOnServerSelector } from "../../state/redux-slices/global"; import "./text-input.css"; import { coreActions } from "../../state/redux-slices/core"; -type TextInputData = { props: TextInputProps }; +type TextInputData = { props: TextInputPropsInText }; export const TextInput: BasicComponent = ({ node }) => { const onServer = useAppSelector(renderingOnServerSelector); diff --git a/packages/doenetml-prototype/src/renderers/doenet/text-point.tsx b/packages/doenetml-prototype/src/renderers/doenet/text-point.tsx new file mode 100644 index 000000000..208bd294c --- /dev/null +++ b/packages/doenetml-prototype/src/renderers/doenet/text-point.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import { MathJax } from "better-react-mathjax"; +import { BasicComponent } from "../types"; +import { useAppSelector } from "../../state/hooks"; +import { renderingOnServerSelector } from "../../state/redux-slices/global"; +import { PointPropsInText } from "@doenet/doenetml-worker-rust"; + +type PointData = { props: PointPropsInText }; + +export const PointInText: BasicComponent = ({ node }) => { + const onServer = useAppSelector(renderingOnServerSelector); + if (onServer) { + return ( + {node.data.props.coordsLatex} + ); + } + // better-react-mathjax cannot handle multiple children (it will not update when they change) + // so create a single string. + const latexString = `\\(${node.data.props.coordsLatex}\\)`; + return ( + + {latexString} + + ); +}; diff --git a/packages/doenetml-prototype/src/renderers/doenet/text.tsx b/packages/doenetml-prototype/src/renderers/doenet/text.tsx index b6d72e665..4cd4188b0 100644 --- a/packages/doenetml-prototype/src/renderers/doenet/text.tsx +++ b/packages/doenetml-prototype/src/renderers/doenet/text.tsx @@ -1,8 +1,8 @@ import React from "react"; import { BasicComponent } from "../types"; -import type { TextProps } from "@doenet/doenetml-worker-rust"; +import type { TextPropsInText } from "@doenet/doenetml-worker-rust"; -type TextData = { props: TextProps }; +type TextData = { props: TextPropsInText }; export const Text: BasicComponent = ({ node }) => { return {node.data.props.value}; diff --git a/packages/doenetml-prototype/src/renderers/doenet/ul.tsx b/packages/doenetml-prototype/src/renderers/doenet/ul.tsx index 894683d0b..2bedd5478 100644 --- a/packages/doenetml-prototype/src/renderers/doenet/ul.tsx +++ b/packages/doenetml-prototype/src/renderers/doenet/ul.tsx @@ -1,9 +1,9 @@ import React from "react"; import { BasicComponentWithPassthroughChildren } from "../types"; -import type { UlProps } from "@doenet/doenetml-worker-rust"; +import type { UlPropsInText } from "@doenet/doenetml-worker-rust"; export const Ul: BasicComponentWithPassthroughChildren<{ - props: UlProps; + props: UlPropsInText; }> = ({ children, node }) => { return
    {children}
; }; diff --git a/packages/doenetml-prototype/src/renderers/doenet/xref.tsx b/packages/doenetml-prototype/src/renderers/doenet/xref.tsx index 14624eb7f..436812f39 100644 --- a/packages/doenetml-prototype/src/renderers/doenet/xref.tsx +++ b/packages/doenetml-prototype/src/renderers/doenet/xref.tsx @@ -1,10 +1,10 @@ import React from "react"; import { BasicComponentWithPassthroughChildren } from "../types"; import { Element } from "../element"; -import type { XrefProps } from "@doenet/doenetml-worker-rust"; +import type { XrefPropsInText } from "@doenet/doenetml-worker-rust"; export const Xref: BasicComponentWithPassthroughChildren<{ - props: XrefProps; + props: XrefPropsInText; }> = ({ children, node }) => { const referentHtmlId = `doenet-id-${node.data.props.referent}`; const label = node.data.props.displayText; diff --git a/packages/doenetml-prototype/src/renderers/pretext-xml/boolean.tsx b/packages/doenetml-prototype/src/renderers/pretext-xml/boolean.tsx index fc0f99acd..054cfaf2d 100644 --- a/packages/doenetml-prototype/src/renderers/pretext-xml/boolean.tsx +++ b/packages/doenetml-prototype/src/renderers/pretext-xml/boolean.tsx @@ -1,8 +1,8 @@ import React from "react"; import { BasicComponent } from "../types"; -import type { BooleanProps } from "@doenet/doenetml-worker-rust"; +import type { BooleanPropsInText } from "@doenet/doenetml-worker-rust"; -type BooleanData = { props: BooleanProps }; +type BooleanData = { props: BooleanPropsInText }; export const Boolean: BasicComponent = ({ node }) => { return {node.data.props.value.toString()}; diff --git a/packages/doenetml-prototype/src/renderers/pretext-xml/division.tsx b/packages/doenetml-prototype/src/renderers/pretext-xml/division.tsx index 455591e76..23e6fcd38 100644 --- a/packages/doenetml-prototype/src/renderers/pretext-xml/division.tsx +++ b/packages/doenetml-prototype/src/renderers/pretext-xml/division.tsx @@ -1,11 +1,11 @@ import React from "react"; import { BasicComponentWithPassthroughChildren } from "../types"; import { Element } from "../element"; -import type { DivisionProps } from "@doenet/doenetml-worker-rust"; +import type { DivisionPropsInText } from "@doenet/doenetml-worker-rust"; import { generateHtmlId } from "../utils"; export const Division: BasicComponentWithPassthroughChildren<{ - props: DivisionProps; + props: DivisionPropsInText; }> = ({ children, node, annotation, ancestors }) => { const htmlId = generateHtmlId(node, annotation, ancestors); const titleElmId = node.data.props.title; diff --git a/packages/doenetml-prototype/src/renderers/pretext-xml/index.ts b/packages/doenetml-prototype/src/renderers/pretext-xml/index.ts index 4dca19d69..2b4ffdc5c 100644 --- a/packages/doenetml-prototype/src/renderers/pretext-xml/index.ts +++ b/packages/doenetml-prototype/src/renderers/pretext-xml/index.ts @@ -3,11 +3,10 @@ export * from "./m"; export * from "./math"; export * from "./graph"; export * from "./division"; +export * from "./text-point"; export * from "./problem"; export * from "./answer"; export * from "./text-input"; -export * from "./line"; -export * from "./point"; export * from "./text"; export * from "./boolean"; export * from "./title"; diff --git a/packages/doenetml-prototype/src/renderers/pretext-xml/li.tsx b/packages/doenetml-prototype/src/renderers/pretext-xml/li.tsx index 489455dca..4e532408e 100644 --- a/packages/doenetml-prototype/src/renderers/pretext-xml/li.tsx +++ b/packages/doenetml-prototype/src/renderers/pretext-xml/li.tsx @@ -1,10 +1,10 @@ import React from "react"; import { BasicComponentWithPassthroughChildren } from "../types"; -import type { LiProps } from "@doenet/doenetml-worker-rust"; +import type { LiPropsInText } from "@doenet/doenetml-worker-rust"; import { generateHtmlId } from "../utils"; export const Li: BasicComponentWithPassthroughChildren<{ - props: LiProps; + props: LiPropsInText; }> = ({ children, node, annotation, ancestors }) => { const htmlId = generateHtmlId(node, annotation, ancestors); const label = node.data.props.label; diff --git a/packages/doenetml-prototype/src/renderers/pretext-xml/math.tsx b/packages/doenetml-prototype/src/renderers/pretext-xml/math.tsx index fa4fecad8..50d481ef0 100644 --- a/packages/doenetml-prototype/src/renderers/pretext-xml/math.tsx +++ b/packages/doenetml-prototype/src/renderers/pretext-xml/math.tsx @@ -1,7 +1,8 @@ import React from "react"; import { BasicComponent } from "../types"; +import { MathPropsInText } from "@doenet/doenetml-worker-rust"; -type MathData = { props: { latex: string } }; +type MathData = { props: MathPropsInText }; export const Math: BasicComponent = ({ node }) => { // better-react-mathjax cannot handle multiple children (it will not update when they change) diff --git a/packages/doenetml-prototype/src/renderers/pretext-xml/ol.tsx b/packages/doenetml-prototype/src/renderers/pretext-xml/ol.tsx index 0d57d9cf0..6b8ac08ee 100644 --- a/packages/doenetml-prototype/src/renderers/pretext-xml/ol.tsx +++ b/packages/doenetml-prototype/src/renderers/pretext-xml/ol.tsx @@ -1,9 +1,9 @@ import React from "react"; import { BasicComponentWithPassthroughChildren } from "../types"; -import type { OlProps } from "@doenet/doenetml-worker-rust"; +import type { OlPropsInText } from "@doenet/doenetml-worker-rust"; export const Ol: BasicComponentWithPassthroughChildren<{ - props: OlProps; + props: OlPropsInText; }> = ({ children, node }) => { return
    {children}
; }; diff --git a/packages/doenetml-prototype/src/renderers/pretext-xml/p.tsx b/packages/doenetml-prototype/src/renderers/pretext-xml/p.tsx index 06f9f3b3c..8fae6461c 100644 --- a/packages/doenetml-prototype/src/renderers/pretext-xml/p.tsx +++ b/packages/doenetml-prototype/src/renderers/pretext-xml/p.tsx @@ -1,10 +1,9 @@ import React from "react"; import { BasicComponentWithPassthroughChildren } from "../types"; -import type { PProps } from "@doenet/doenetml-worker-rust"; +import type { PPropsInText } from "@doenet/doenetml-worker-rust"; -export const P: BasicComponentWithPassthroughChildren<{ props: PProps }> = ({ - children, - node, -}) => { +export const P: BasicComponentWithPassthroughChildren<{ + props: PPropsInText; +}> = ({ children, node }) => { return

{children}

; }; diff --git a/packages/doenetml-prototype/src/renderers/pretext-xml/point.tsx b/packages/doenetml-prototype/src/renderers/pretext-xml/point.tsx deleted file mode 100644 index b81cb30d8..000000000 --- a/packages/doenetml-prototype/src/renderers/pretext-xml/point.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import React from "react"; -import { BasicComponent } from "../types"; -import { GraphContext, LAYER_OFFSETS } from "./graph"; -import * as JSG from "jsxgraph"; - -export const PointInGraph: BasicComponent = ({ node }) => { - const board = React.useContext(GraphContext); - const pointRef = React.useRef(null); - - React.useEffect(() => { - if (!board) { - pointRef.current = null; - return; - } - if (pointRef.current) { - return; - } - const point = createPoint(board, { - coords: [1, 1], - labelForGraph: "test", - lineColor: "var(--mainPurple)", - hidden: false, - fixed: false, - draggable: true, - fixLocation: false, - layer: 0, - selectedStyle: { lineStyle: "solid", lineOpacity: 1, lineWidth: 2 }, - dashed: false, - }); - pointRef.current = point; - }, [board]); - - if (!board) { - return null; - } - - return null; -}; - -function createPoint( - board: JSG.Board, - props: { - coords: [number, number]; - labelForGraph: string; - lineColor: string; - hidden: boolean; - fixed: boolean; - draggable: boolean; - fixLocation: boolean; - layer: number; - selectedStyle: { - lineStyle: string; - lineOpacity: number; - lineWidth: number; - }; - dashed: boolean; - }, -) { - const lineColor = props.lineColor; - - // Things to be passed to JSXGraph as attributes - const jsxLineAttributes: JSG.PointAttributes = { - name: props.labelForGraph, - visible: !props.hidden, - fixed: props.fixed, - layer: 10 * props.layer + LAYER_OFFSETS.line, - strokeColor: lineColor, - strokeOpacity: props.selectedStyle.lineOpacity, - highlightStrokeColor: lineColor, - highlightStrokeOpacity: props.selectedStyle.lineOpacity * 0.5, - strokeWidth: props.selectedStyle.lineWidth, - highlightStrokeWidth: props.selectedStyle.lineWidth, - dash: styleToDash(props.selectedStyle.lineStyle, props.dashed), - highlight: !props.fixLocation, - }; - - const line: JSG.Point = board.create( - "point", - props.coords, - jsxLineAttributes, - ); - - return line; -} - -/** - * Return the the dash length for a given style. - */ -function styleToDash(style: string, dash: boolean) { - if (style === "dashed" || dash) { - return 2; - } else if (style === "solid") { - return 0; - } else if (style === "dotted") { - return 1; - } else { - return 0; - } -} diff --git a/packages/doenetml-prototype/src/renderers/pretext-xml/text-input.tsx b/packages/doenetml-prototype/src/renderers/pretext-xml/text-input.tsx index 2fdc5b53c..9caf047d9 100644 --- a/packages/doenetml-prototype/src/renderers/pretext-xml/text-input.tsx +++ b/packages/doenetml-prototype/src/renderers/pretext-xml/text-input.tsx @@ -1,8 +1,8 @@ import React from "react"; -import type { TextInputProps } from "@doenet/doenetml-worker-rust"; +import type { TextInputPropsInText } from "@doenet/doenetml-worker-rust"; import { BasicComponent } from "../types"; -type TextInputData = { props: TextInputProps }; +type TextInputData = { props: TextInputPropsInText }; export const TextInput: BasicComponent = ({ node }) => { const value = node.data.props.immediateValue; diff --git a/packages/doenetml-prototype/src/renderers/pretext-xml/text-point.tsx b/packages/doenetml-prototype/src/renderers/pretext-xml/text-point.tsx new file mode 100644 index 000000000..0366c74db --- /dev/null +++ b/packages/doenetml-prototype/src/renderers/pretext-xml/text-point.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import { BasicComponent } from "../types"; +import { PointPropsInText } from "@doenet/doenetml-worker-rust"; + +type PointData = { props: PointPropsInText }; + +export const PointInText: BasicComponent = ({ node }) => { + // better-react-mathjax cannot handle multiple children (it will not update when they change) + // so create a single string. + const latexString = node.data.props.coordsLatex; + return React.createElement("m", {}, latexString); +}; diff --git a/packages/doenetml-prototype/src/renderers/pretext-xml/text.tsx b/packages/doenetml-prototype/src/renderers/pretext-xml/text.tsx index 543de0f5a..b7336fb7f 100644 --- a/packages/doenetml-prototype/src/renderers/pretext-xml/text.tsx +++ b/packages/doenetml-prototype/src/renderers/pretext-xml/text.tsx @@ -1,8 +1,8 @@ import React from "react"; import { BasicComponent } from "../types"; -import type { TextProps } from "@doenet/doenetml-worker-rust"; +import type { TextPropsInText } from "@doenet/doenetml-worker-rust"; -type TextData = { props: TextProps }; +type TextData = { props: TextPropsInText }; export const Text: BasicComponent = ({ node }) => { return {node.data.props.value}; diff --git a/packages/doenetml-prototype/src/renderers/pretext-xml/ul.tsx b/packages/doenetml-prototype/src/renderers/pretext-xml/ul.tsx index 894683d0b..2bedd5478 100644 --- a/packages/doenetml-prototype/src/renderers/pretext-xml/ul.tsx +++ b/packages/doenetml-prototype/src/renderers/pretext-xml/ul.tsx @@ -1,9 +1,9 @@ import React from "react"; import { BasicComponentWithPassthroughChildren } from "../types"; -import type { UlProps } from "@doenet/doenetml-worker-rust"; +import type { UlPropsInText } from "@doenet/doenetml-worker-rust"; export const Ul: BasicComponentWithPassthroughChildren<{ - props: UlProps; + props: UlPropsInText; }> = ({ children, node }) => { return
    {children}
; }; diff --git a/packages/doenetml-prototype/src/renderers/pretext-xml/xref.tsx b/packages/doenetml-prototype/src/renderers/pretext-xml/xref.tsx index 5993c2441..b534d93e3 100644 --- a/packages/doenetml-prototype/src/renderers/pretext-xml/xref.tsx +++ b/packages/doenetml-prototype/src/renderers/pretext-xml/xref.tsx @@ -1,10 +1,10 @@ import React from "react"; import { BasicComponentWithPassthroughChildren } from "../types"; -import type { XrefProps } from "@doenet/doenetml-worker-rust"; +import type { XrefPropsInText } from "@doenet/doenetml-worker-rust"; import { normalizeAttrs } from "../../utils/pretext/normalize-attrs"; export const Xref: BasicComponentWithPassthroughChildren<{ - props: XrefProps; + props: XrefPropsInText; }> = ({ children, node }) => { const referentHtmlId = `doenet-id-${node.data.props.referent}`; diff --git a/packages/doenetml-prototype/src/renderers/renderers.ts b/packages/doenetml-prototype/src/renderers/renderers.ts index 3f2191c0d..66bf3eaca 100644 --- a/packages/doenetml-prototype/src/renderers/renderers.ts +++ b/packages/doenetml-prototype/src/renderers/renderers.ts @@ -23,6 +23,7 @@ import { ChoiceInput, } from "./doenet"; import * as PretextComponent from "./pretext-xml"; +import { PointInText } from "./doenet/text-point"; export type CommonProps = { monitorVisibility?: boolean; @@ -63,6 +64,7 @@ export const TEXT_MODE_COMPONENTS: RendererObject = { m: { component: M, passthroughChildren: true }, math: { component: Math }, graph: { component: Graph }, + point: { component: PointInText }, division: { component: Division, passthroughChildren: true, @@ -110,6 +112,7 @@ export const PRETEXT_TEXT_MODE_COMPONENTS: RendererObject = { m: { component: PretextComponent.M, passthroughChildren: true }, math: { component: PretextComponent.Math }, graph: { component: PretextComponent.Graph }, + point: { component: PretextComponent.PointInText }, division: { component: PretextComponent.Division, passthroughChildren: true, @@ -154,7 +157,4 @@ export const PRETEXT_TEXT_MODE_COMPONENTS: RendererObject = { }, }; -export const PRETEXT_GRAPH_MODE_COMPONENTS: RendererObject = { - line: { component: PretextComponent.LineInGraph }, - point: { component: PretextComponent.PointInGraph }, -}; +export const PRETEXT_GRAPH_MODE_COMPONENTS: RendererObject = {}; diff --git a/packages/doenetml-worker-rust/lib-doenetml-core/src/components/component.rs b/packages/doenetml-worker-rust/lib-doenetml-core/src/components/component.rs index 2568d7c5b..b7b2b7df9 100644 --- a/packages/doenetml-worker-rust/lib-doenetml-core/src/components/component.rs +++ b/packages/doenetml-worker-rust/lib-doenetml-core/src/components/component.rs @@ -6,6 +6,7 @@ use std::str::FromStr; use crate::core::props::{PropDefinition, PropDefinitionMeta, PropProfile}; use crate::dast::Position as DastPosition; +use crate::props::RenderContext; use super::_error::_Error; use super::_external::_External; @@ -76,7 +77,7 @@ impl ComponentProps for Component { PropDefinition { meta: PropDefinitionMeta { name: self.variant.get_prop_name(local_prop_idx), - for_render: self.variant.get_prop_is_for_render(local_prop_idx), + for_render: self.variant.get_prop_for_render_outputs(local_prop_idx), profile: self.variant.get_prop_profile(local_prop_idx), prop_pointer: PropPointer { component_idx, @@ -122,11 +123,21 @@ impl ComponentProps for Component { self.variant.get_default_prop_local_index() } - fn get_for_render_local_prop_indices(&self) -> impl Iterator { - (0..self.variant.get_num_props()).filter_map(|i| { - self.variant - .get_prop_is_for_render(LocalPropIdx::new(i)) - .then_some(LocalPropIdx::new(i)) + fn get_for_render_local_prop_indices( + &self, + render_context: RenderContext, + ) -> impl Iterator { + (0..self.variant.get_num_props()).filter_map(move |i| { + let for_render = self + .variant + .get_prop_for_render_outputs(LocalPropIdx::new(i)); + + let rendered_in_either = match render_context { + RenderContext::InGraph => for_render.in_graph, + RenderContext::InText => for_render.in_text, + }; + + rendered_in_either.then_some(LocalPropIdx::new(i)) }) } } diff --git a/packages/doenetml-worker-rust/lib-doenetml-core/src/components/doenet/point.rs b/packages/doenetml-worker-rust/lib-doenetml-core/src/components/doenet/point.rs index 766946289..1f10443b9 100644 --- a/packages/doenetml-worker-rust/lib-doenetml-core/src/components/doenet/point.rs +++ b/packages/doenetml-worker-rust/lib-doenetml-core/src/components/doenet/point.rs @@ -1,7 +1,7 @@ use std::rc::Rc; use crate::components::prelude::*; -use crate::general_prop::{BooleanProp, MathProp}; +use crate::general_prop::{BooleanProp, LatexProp, MathProp}; use crate::props::UpdaterObject; #[component(name = Point)] @@ -14,13 +14,19 @@ mod component { #[prop(value_type = PropValueType::Boolean, profile = PropProfile::Hidden)] Hidden, #[prop(value_type = PropValueType::Math, - profile = PropProfile::Math, - is_public, for_render)] + is_public, for_render(in_graph))] X, #[prop(value_type = PropValueType::Math, - profile = PropProfile::Math, - is_public, for_render)] + is_public, for_render(in_graph))] Y, + #[prop(value_type = PropValueType::Math, + profile = PropProfile::Math, + is_public)] + Coords, + #[prop(value_type = PropValueType::String, + profile = PropProfile::String, + for_render(in_text))] + CoordsLatex, } enum Attributes { @@ -52,6 +58,7 @@ mod component { pub use component::Point; pub use component::PointActions; pub use component::PointAttributes; +pub use component::PointMoveActionArgs; pub use component::PointProps; impl PropGetUpdater for PointProps { @@ -66,6 +73,14 @@ impl PropGetUpdater for PointProps { PointProps::Y => as_updater_object::<_, component::props::types::Y>( component::attrs::Y::get_prop_updater(), ), + PointProps::Coords => { + as_updater_object::<_, component::props::types::Coords>(custom_props::Coords::new()) + } + PointProps::CoordsLatex => { + as_updater_object::<_, component::props::types::CoordsLatex>(LatexProp::new( + PointProps::Coords.local_idx(), + )) + } } } } @@ -94,3 +109,90 @@ impl ComponentOnAction for Point { } } } + +mod custom_props { + use super::*; + + pub use coords::*; + + mod coords { + + use crate::state::types::math_expr::MathExpr; + + use super::*; + + #[derive(Debug, Default)] + pub struct Coords {} + + impl Coords { + pub fn new() -> Self { + Coords {} + } + } + + /// Structure to hold data generated from the data queries + #[derive(TryFromDataQueryResults, IntoDataQueryResults, Debug)] + #[data_query(query_trait = DataQueries)] + #[derive(TestDataQueryTypes)] + #[owning_component(Point)] + struct RequiredData { + x: PropView, + y: PropView, + } + + impl DataQueries for RequiredData { + fn x_query() -> DataQuery { + DataQuery::Prop { + source: PropSource::Me, + prop_specifier: PointProps::X.local_idx().into(), + } + } + fn y_query() -> DataQuery { + DataQuery::Prop { + source: PropSource::Me, + prop_specifier: PointProps::Y.local_idx().into(), + } + } + } + + impl PropUpdater for Coords { + type PropType = prop_type::Math; + + fn data_queries(&self) -> Vec { + RequiredData::to_data_queries() + } + fn calculate(&self, data: DataQueryResults) -> PropCalcResult { + let required_data = RequiredData::try_from_data_query_results(data).unwrap(); + let x = required_data.x.value; + let y = required_data.y.value; + + let coords = MathExpr::new_vector(&[(*x).clone(), (*y).clone()]); + + PropCalcResult::Calculated(coords.into()) + } + + fn invert( + &self, + data: DataQueryResults, + requested_value: Self::PropType, + _is_direct_change_from_action: bool, + ) -> Result { + let mut desired = RequiredData::try_new_desired(&data).unwrap(); + + match requested_value.to_vector_components() { + Err(_) => Err(InvertError::CouldNotUpdate), + Ok(components) => { + if components.len() < 2 { + return Err(InvertError::CouldNotUpdate); + } + let mut comp_iter = components.into_iter(); + desired.x.change_to(comp_iter.next().unwrap().into()); + desired.y.change_to(comp_iter.next().unwrap().into()); + + Ok(desired.into_data_query_results()) + } + } + } + } + } +} diff --git a/packages/doenetml-worker-rust/lib-doenetml-core/src/components/mod.rs b/packages/doenetml-worker-rust/lib-doenetml-core/src/components/mod.rs index 0853be79c..8433851ed 100644 --- a/packages/doenetml-worker-rust/lib-doenetml-core/src/components/mod.rs +++ b/packages/doenetml-worker-rust/lib-doenetml-core/src/components/mod.rs @@ -8,11 +8,11 @@ //! //! ## Features //! - [`component`] contains the [`Component`] struct, -//! which abstracts over all DoenetML components. It is what is used by [`Core`](crate::core) and -//! should not be used directly by component authors. +//! which abstracts over all DoenetML components. It is what is used by [`Core`](crate::core) and +//! should not be used directly by component authors. //! - [`special`] contains implementations of the special [`_Error`] and -//! [`_External`] components. These are treated differently from other -//! _DoenetML_ components during the render process. +//! [`_External`] components. These are treated differently from other +//! _DoenetML_ components during the render process. pub mod component; pub mod component_enum; diff --git a/packages/doenetml-worker-rust/lib-doenetml-core/src/components/special/_error.rs b/packages/doenetml-worker-rust/lib-doenetml-core/src/components/special/_error.rs index f143cd2e8..7d362b30c 100644 --- a/packages/doenetml-worker-rust/lib-doenetml-core/src/components/special/_error.rs +++ b/packages/doenetml-worker-rust/lib-doenetml-core/src/components/special/_error.rs @@ -1,7 +1,10 @@ //! An error component. The error component is processed like a regular component until rendering to `FlatDast`, //! whereupon it is converted into a DAST error. -use crate::{components::prelude::*, props::UpdaterObject}; +use crate::{ + components::prelude::*, + props::{ForRenderOutputs, RenderContext, UpdaterObject}, +}; #[derive(Debug, Default, Clone)] pub struct _Error { @@ -39,7 +42,10 @@ impl ComponentProps for _Error { fn get_default_prop_local_index(&self) -> Option { None } - fn get_for_render_local_prop_indices(&self) -> impl Iterator { + fn get_for_render_local_prop_indices( + &self, + _render_context: RenderContext, + ) -> impl Iterator { vec![].into_iter() } fn get_local_prop_index_from_name(&self, _name: &str) -> Option { @@ -60,7 +66,7 @@ impl ComponentVariantProps for _Error { fn get_num_props(&self) -> usize { 0 } - fn get_prop_is_for_render(&self, _local_prop_idx: LocalPropIdx) -> bool { + fn get_prop_for_render_outputs(&self, _local_prop_idx: LocalPropIdx) -> ForRenderOutputs { panic!("No props on _Error") } fn get_prop_name(&self, _local_prop_idx: LocalPropIdx) -> &'static str { diff --git a/packages/doenetml-worker-rust/lib-doenetml-core/src/components/special/_external.rs b/packages/doenetml-worker-rust/lib-doenetml-core/src/components/special/_external.rs index aa5b1238d..594eed7a7 100644 --- a/packages/doenetml-worker-rust/lib-doenetml-core/src/components/special/_external.rs +++ b/packages/doenetml-worker-rust/lib-doenetml-core/src/components/special/_external.rs @@ -2,7 +2,9 @@ //! as-is when converted into `FlatDast`. use crate::{ - components::prelude::*, general_prop::RenderedChildrenPassthroughProp, props::UpdaterObject, + components::prelude::*, + general_prop::RenderedChildrenPassthroughProp, + props::{ForRenderOutputs, RenderContext, UpdaterObject}, }; #[derive(Debug, Default, Clone)] @@ -15,7 +17,10 @@ impl _External { const PROPS: &'static [_ExternalProps] = &[_ExternalProps::RenderedChildren]; pub const PROP_NAMES: &'static [&'static str] = &["renderedChildren"]; const PROP_PROFILES: &'static [Option] = &[Some(PropProfile::RenderedChildren)]; - const PROP_FOR_RENDERS: &'static [bool] = &[false]; + const PROP_FOR_RENDERS: &'static [ForRenderOutputs] = &[ForRenderOutputs { + in_graph: false, + in_text: false, + }]; const PROP_IS_PUBLICS: &'static [bool] = &[false]; const PROP_VALUE_TYPES: &'static [PropValueType] = &[PropValueType::ContentRefs]; const DEFAULT_PROP: Option = None; @@ -54,7 +59,10 @@ impl ComponentProps for _External { fn get_default_prop_local_index(&self) -> Option { None } - fn get_for_render_local_prop_indices(&self) -> impl Iterator { + fn get_for_render_local_prop_indices( + &self, + _render_context: RenderContext, + ) -> impl Iterator { vec![].into_iter() } fn get_local_prop_index_from_name(&self, _name: &str) -> Option { @@ -95,7 +103,7 @@ impl ComponentVariantProps for _External { fn get_num_props(&self) -> usize { _External::PROP_NAMES.len() } - fn get_prop_is_for_render(&self, local_prop_idx: LocalPropIdx) -> bool { + fn get_prop_for_render_outputs(&self, local_prop_idx: LocalPropIdx) -> ForRenderOutputs { _External::PROP_FOR_RENDERS[local_prop_idx.as_usize()] } fn get_prop_name(&self, local_prop_idx: LocalPropIdx) -> &'static str { diff --git a/packages/doenetml-worker-rust/lib-doenetml-core/src/components/special/_ref.rs b/packages/doenetml-worker-rust/lib-doenetml-core/src/components/special/_ref.rs index c5e3ae7bb..1f59b7215 100644 --- a/packages/doenetml-worker-rust/lib-doenetml-core/src/components/special/_ref.rs +++ b/packages/doenetml-worker-rust/lib-doenetml-core/src/components/special/_ref.rs @@ -8,7 +8,9 @@ use std::rc::Rc; use crate::{ - components::prelude::*, general_prop::IndependentProp, props::UpdaterObject, + components::prelude::*, + general_prop::IndependentProp, + props::{ForRenderOutputs, RenderContext, UpdaterObject}, state::types::component_refs::ComponentRef, }; @@ -41,7 +43,10 @@ impl ComponentProps for _Ref { fn get_default_prop_local_index(&self) -> Option { None } - fn get_for_render_local_prop_indices(&self) -> impl Iterator { + fn get_for_render_local_prop_indices( + &self, + _render_context: RenderContext, + ) -> impl Iterator { vec![].into_iter() } fn get_local_prop_index_from_name(&self, _name: &str) -> Option { @@ -68,8 +73,8 @@ impl ComponentVariantProps for _Ref { fn get_num_props(&self) -> usize { 1 } - fn get_prop_is_for_render(&self, _local_prop_idx: LocalPropIdx) -> bool { - false + fn get_prop_for_render_outputs(&self, _local_prop_idx: LocalPropIdx) -> ForRenderOutputs { + ForRenderOutputs::default() } fn get_prop_name(&self, local_prop_idx: LocalPropIdx) -> &'static str { self.get_prop_names()[local_prop_idx.as_usize()] diff --git a/packages/doenetml-worker-rust/lib-doenetml-core/src/components/traits/component_props.rs b/packages/doenetml-worker-rust/lib-doenetml-core/src/components/traits/component_props.rs index 395e1c939..481ac7bb8 100644 --- a/packages/doenetml-worker-rust/lib-doenetml-core/src/components/traits/component_props.rs +++ b/packages/doenetml-worker-rust/lib-doenetml-core/src/components/traits/component_props.rs @@ -1,4 +1,4 @@ -use crate::{components::types::LocalPropIdx, core::props::PropDefinition}; +use crate::{components::types::LocalPropIdx, core::props::PropDefinition, props::RenderContext}; /// The main `Component` struct, which wraps all component variants, implements /// `ComponentProps`. This is used by `Core` to initialize props. @@ -26,6 +26,10 @@ pub trait ComponentProps { fn get_default_prop_local_index(&self) -> Option; /// Get the vector of the indices of all this component's props - /// that have been marked `for_render`. - fn get_for_render_local_prop_indices(&self) -> impl Iterator; + /// that have been marked `for_render`, + /// depending on the render context (either `in_graph` or `in_text`) + fn get_for_render_local_prop_indices( + &self, + render_context: RenderContext, + ) -> impl Iterator; } diff --git a/packages/doenetml-worker-rust/lib-doenetml-core/src/components/traits/component_variant_props.rs b/packages/doenetml-worker-rust/lib-doenetml-core/src/components/traits/component_variant_props.rs index efe0b0317..9522eea5d 100644 --- a/packages/doenetml-worker-rust/lib-doenetml-core/src/components/traits/component_variant_props.rs +++ b/packages/doenetml-worker-rust/lib-doenetml-core/src/components/traits/component_variant_props.rs @@ -3,7 +3,7 @@ use enum_dispatch::enum_dispatch; use crate::{ components::{types::LocalPropIdx, ComponentEnum}, core::props::{PropProfile, PropValueType}, - props::{PropUpdater, PropUpdaterUntyped, UpdaterObject}, + props::{ForRenderOutputs, PropUpdater, PropUpdaterUntyped, UpdaterObject}, }; // TODO: remove the default implementations for these methods. It should be a compiler @@ -32,9 +32,9 @@ pub trait ComponentVariantProps { fn get_num_props(&self) -> usize { unimplemented!() } - /// Get whether a specific prop is marked as `for_render`. I.e., it should - /// _always_ be sent to the UI. - fn get_prop_is_for_render(&self, local_prop_idx: LocalPropIdx) -> bool { + /// Get whether a specific prop is marked as `for_render` for one or more render outputs. I.e., it should + /// be sent to the UI if the component is in a graph or in text. + fn get_prop_for_render_outputs(&self, local_prop_idx: LocalPropIdx) -> ForRenderOutputs { unimplemented!() } /// Get the name of a prop. diff --git a/packages/doenetml-worker-rust/lib-doenetml-core/src/core/document_model/document_model.rs b/packages/doenetml-worker-rust/lib-doenetml-core/src/core/document_model/document_model.rs index d41f9d788..727dd151a 100644 --- a/packages/doenetml-worker-rust/lib-doenetml-core/src/core/document_model/document_model.rs +++ b/packages/doenetml-worker-rust/lib-doenetml-core/src/core/document_model/document_model.rs @@ -15,7 +15,8 @@ use crate::{ graph_node::{DependencyGraph, GraphNode}, props::{ cache::{PropCache, PropStatus, PropWithMeta}, - DataQuery, DataQueryResults, PropDefinition, PropProfile, StateCache, UpdaterObject, + DataQuery, DataQueryResults, PropDefinition, PropProfile, RenderContext, StateCache, + UpdaterObject, }, }; @@ -174,9 +175,26 @@ impl DocumentModel { ) -> impl Iterator { let local_prop_indices = { let document_structure = self.document_structure.borrow(); + + // Determine whether or not component is in a graph (i.e., has a graph ancestor). + // If so, we'll look for props that are rendered `in_graph` rather than `in_text`. + let in_graph = document_structure + .get_true_component_ancestors(component_idx) + .any(|ancestor_idx| { + document_structure + .get_component(ancestor_idx) + .get_component_type() + == "graph" + }); + + let render_context = match in_graph { + true => RenderContext::InGraph, + false => RenderContext::InText, + }; + let iterator = document_structure .get_component(component_idx) - .get_for_render_local_prop_indices(); + .get_for_render_local_prop_indices(render_context); // Note: collect into a vector so that stop borrowing from document_structure.components iterator.collect::>() }; diff --git a/packages/doenetml-worker-rust/lib-doenetml-core/src/core/document_model/prop_updates.rs b/packages/doenetml-worker-rust/lib-doenetml-core/src/core/document_model/prop_updates.rs index 2c809748c..22ff4e504 100644 --- a/packages/doenetml-worker-rust/lib-doenetml-core/src/core/document_model/prop_updates.rs +++ b/packages/doenetml-worker-rust/lib-doenetml-core/src/core/document_model/prop_updates.rs @@ -201,7 +201,7 @@ impl DocumentModel { // if prop is marked for render, add to components_with_changed_for_render_prop let prop_meta = &self.get_prop_definition(node).meta; - if prop_meta.for_render { + if prop_meta.for_render.in_graph || prop_meta.for_render.in_text { let component_idx = prop_meta.prop_pointer.component_idx; changed_components[component_idx.as_usize()] = true; } diff --git a/packages/doenetml-worker-rust/lib-doenetml-core/src/core/props/data_query/data_query.rs b/packages/doenetml-worker-rust/lib-doenetml-core/src/core/props/data_query/data_query.rs index 426fbc68b..e0e2203d7 100644 --- a/packages/doenetml-worker-rust/lib-doenetml-core/src/core/props/data_query/data_query.rs +++ b/packages/doenetml-worker-rust/lib-doenetml-core/src/core/props/data_query/data_query.rs @@ -116,9 +116,9 @@ pub enum DataQuery { /// `filter`. Results in a `prop_type::ComponentRefs` with the matching component refs. /// /// - `container`: the component whose children will be searched. For example, `PropComponent::Me` - /// to search your own children, or `PropComponent::Parent` to search your parent's children. + /// to search your own children, or `PropComponent::Parent` to search your parent's children. /// - `filter`: A composition of `ContentFilter`s. These can be combined with `Op::And`, `Op::Or`, - /// and `OpNot`. + /// and `OpNot`. /// /// ## Example /// ```rust diff --git a/packages/doenetml-worker-rust/lib-doenetml-core/src/core/props/prop.rs b/packages/doenetml-worker-rust/lib-doenetml-core/src/core/props/prop.rs index cd5ceda35..655455302 100644 --- a/packages/doenetml-worker-rust/lib-doenetml-core/src/core/props/prop.rs +++ b/packages/doenetml-worker-rust/lib-doenetml-core/src/core/props/prop.rs @@ -14,13 +14,27 @@ pub struct PropDefinitionMeta { /// The profile that this prop matches. pub profile: Option, /// Whether this prop is _always_ computed whenever this component is rendered. - pub for_render: bool, + pub for_render: ForRenderOutputs, pub public: bool, } /// Type of `PropUpdater` trait object. pub type UpdaterObject = Rc; +/// `ForRenderOutputs` specifies whether or not a prop is sent to the UI when the component is +/// being rendered in a graph or in text. +#[derive(Debug, Clone, Copy, Default)] +pub struct ForRenderOutputs { + pub in_graph: bool, + pub in_text: bool, +} + +#[derive(Debug, Clone, Copy)] +pub enum RenderContext { + InGraph, + InText, +} + /// A `PropDefinition` stores functions needed to compute a `PropValue` as required /// by a component. /// Its value is lazily computed and can depend on props coming from other components. diff --git a/packages/doenetml-worker-rust/lib-doenetml-core/src/core/props/prop_value.rs b/packages/doenetml-worker-rust/lib-doenetml-core/src/core/props/prop_value.rs index f77c8cb97..0da8a8212 100644 --- a/packages/doenetml-worker-rust/lib-doenetml-core/src/core/props/prop_value.rs +++ b/packages/doenetml-worker-rust/lib-doenetml-core/src/core/props/prop_value.rs @@ -18,6 +18,7 @@ use crate::utils::rc_serde; /// These values follow a naming convention: /// 1. They are all in the form `PropValue::VariantName(prop_type::VariantName)`. /// 2. There is a corresponding type alias `VariantName` in the `prop_type` module. +/// /// This naming convention is relied upon by macros that implement type checking. #[derive( Debug, diff --git a/packages/doenetml-worker-rust/lib-doenetml-core/src/core/state/types/math_expr.rs b/packages/doenetml-worker-rust/lib-doenetml-core/src/core/state/types/math_expr.rs index 807d3d3ce..8c78612d8 100644 --- a/packages/doenetml-worker-rust/lib-doenetml-core/src/core/state/types/math_expr.rs +++ b/packages/doenetml-worker-rust/lib-doenetml-core/src/core/state/types/math_expr.rs @@ -1,4 +1,5 @@ use anyhow::anyhow; +use itertools::Itertools; use serde::ser::SerializeStruct; use std::collections::HashMap; use strum_macros::Display; @@ -239,6 +240,61 @@ impl MathExpr { } } + /// Create a new math expression that is a vector with components + /// given by `components`. + pub fn new_vector(components: &[MathExpr]) -> Self { + let vector_object = JsMathExpr(format!( + "[\"vector\",{}]", + components.iter().map(|comp| &comp.math_object.0).join(",") + )); + + MathExpr { + math_object: vector_object, + } + } + + /// Calculate an array of math expressions corresponding to the vector components + /// of `self`. + /// + /// Return a Result with + /// - a `Vec` of the `MathExpr` corresponding to the vector components, if `self` is a vector + /// - an `Err` if `self` is not a vector + pub fn to_vector_components(&self) -> Result, anyhow::Error> { + let math_string = &self.math_object.0; + + let val: serde_json::Value = serde_json::from_str(math_string)?; + + let components = match val { + serde_json::Value::Array(c) => c, + _ => { + return Err(anyhow!("Math expression is not a vector")); + } + }; + + let operator = match &components[0] { + serde_json::Value::String(op) => op, + _ => { + return Err(anyhow!("Math expression is invalid")); + } + }; + + if operator != "vector" && operator != "tuple" { + return Err(anyhow!("Math expression is not a vector")); + } + + let math_strings = components[1..] + .iter() + .map(serde_json::to_string) + .collect::, _>>()? + .iter() + .map(|s| MathExpr { + math_object: JsMathExpr(s.to_string()), + }) + .collect::>(); + + Ok(math_strings) + } + /// Create a new mathematical expression formed by substituting variables with new expressions /// /// Parameters: diff --git a/packages/doenetml-worker-rust/lib-doenetml-core/src/lib.rs b/packages/doenetml-worker-rust/lib-doenetml-core/src/lib.rs index 987c2bfcf..d1163fc77 100644 --- a/packages/doenetml-worker-rust/lib-doenetml-core/src/lib.rs +++ b/packages/doenetml-worker-rust/lib-doenetml-core/src/lib.rs @@ -5,12 +5,12 @@ //! //! ## Features //! - [`Components`](components) are the building blocks of _DoenetML_. They are the smallest unit of a -//! _DoenetML_ document, and (most of the time) correspond to XML tags that the user has typed -//! in their _DoenetML_ source code. Examples include `

` and `` +//! _DoenetML_ document, and (most of the time) correspond to XML tags that the user has typed +//! in their _DoenetML_ source code. Examples include `

` and `` //! - [`Core`](core) computes properties and relationships between DoenetML components. It enables -//! interactivity via `Action`s, allowing components to respond and re-render based on user input. +//! interactivity via `Action`s, allowing components to respond and re-render based on user input. //! - [`Graph`](graph) provides an abstract implementation of a directed graph which is used -//! to track dependencies among _DoenetML_ components and their properties. +//! to track dependencies among _DoenetML_ components and their properties. #![allow(clippy::single_match)] diff --git a/packages/doenetml-worker-rust/lib-doenetml-core/tests/by_component/point.rs b/packages/doenetml-worker-rust/lib-doenetml-core/tests/by_component/point.rs index 4f0097d15..fbab83b94 100644 --- a/packages/doenetml-worker-rust/lib-doenetml-core/tests/by_component/point.rs +++ b/packages/doenetml-worker-rust/lib-doenetml-core/tests/by_component/point.rs @@ -1,87 +1,393 @@ use super::*; -use doenetml_core::{components::doenet::point::PointProps, state::types::math_expr::MathExpr}; +use doenetml_core::{ + components::{ + doenet::point::{PointActions, PointMoveActionArgs, PointProps}, + types::{Action, ActionBody}, + ActionsEnum, + }, + dast::{ForRenderPropValue, ForRenderPropValueOrContent, ForRenderProps}, + state::types::math_expr::{JsMathExpr, MathExpr}, +}; // Note: we can only test values points with numerical values, as otherwise it requires wasm to call out to math-expressions const X_LOCAL_IDX: LocalPropIdx = PointProps::X.local_idx(); const Y_LOCAL_IDX: LocalPropIdx = PointProps::Y.local_idx(); +const COORDS_LOCAL_IDX: LocalPropIdx = PointProps::Coords.local_idx(); #[test] fn point_is_2d_zero_by_default() { - let dast_root = dast_root_no_position(r#""#); + let dast_root = dast_root_no_position(r#""#); let mut core = TestCore::new(); core.init_from_dast_root(&dast_root); - // the point will be index 1, as the document tag will be index 0. - let point_idx = 1; + let point_idx = core.get_component_index_by_name("P"); let math_zero: MathExpr = 0.0.into(); + let math_zero_zero = MathExpr { + math_object: JsMathExpr("[\"vector\",0,0]".to_string()), + }; let x_prop = core.get_prop(point_idx, X_LOCAL_IDX); let y_prop = core.get_prop(point_idx, Y_LOCAL_IDX); + let coords_prop = core.get_prop(point_idx, COORDS_LOCAL_IDX); assert_eq!(x_prop.value, math_zero.clone().into()); assert_eq!(y_prop.value, math_zero.into()); + assert_eq!(coords_prop.value, math_zero_zero.into()); } #[test] fn point_that_specifies_just_x() { - let dast_root = dast_root_no_position(r#""#); + let dast_root = dast_root_no_position(r#""#); let mut core = TestCore::new(); core.init_from_dast_root(&dast_root); - // the point will be index 1, as the document tag will be index 0. - let point_idx = 1; + let point_idx = core.get_component_index_by_name("P"); let math_x: MathExpr = (-7.0).into(); let math_zero: MathExpr = 0.0.into(); + let math_coords = MathExpr { + math_object: JsMathExpr("[\"vector\",-7,0]".to_string()), + }; let x_prop = core.get_prop(point_idx, X_LOCAL_IDX); let y_prop = core.get_prop(point_idx, Y_LOCAL_IDX); + let coords_prop = core.get_prop(point_idx, COORDS_LOCAL_IDX); assert_eq!(x_prop.value, math_x.into()); assert_eq!(y_prop.value, math_zero.into()); + assert_eq!(coords_prop.value, math_coords.into()); } #[test] fn point_that_specifies_just_y() { - let dast_root = dast_root_no_position(r#""#); + let dast_root = dast_root_no_position(r#""#); let mut core = TestCore::new(); core.init_from_dast_root(&dast_root); - // the point will be index 1, as the document tag will be index 0. - let point_idx = 1; + let point_idx = core.get_component_index_by_name("P"); let math_zero: MathExpr = 0.0.into(); let math_y: MathExpr = 3.1.into(); + let math_coords = MathExpr { + math_object: JsMathExpr("[\"vector\",0,3.1]".to_string()), + }; let x_prop = core.get_prop(point_idx, X_LOCAL_IDX); let y_prop = core.get_prop(point_idx, Y_LOCAL_IDX); + let coords_prop = core.get_prop(point_idx, COORDS_LOCAL_IDX); assert_eq!(x_prop.value, math_zero.into()); assert_eq!(y_prop.value, math_y.into()); + assert_eq!(coords_prop.value, math_coords.into()); } #[test] fn point_that_specifies_x_and_y() { - let dast_root = dast_root_no_position(r#""#); + let dast_root = dast_root_no_position(r#""#); let mut core = TestCore::new(); core.init_from_dast_root(&dast_root); - // the point will be index 1, as the document tag will be index 0. - let point_idx = 1; + let point_idx = core.get_component_index_by_name("P"); let math_x: MathExpr = 8.9.into(); let math_y: MathExpr = 6.2.into(); + let math_coords = MathExpr { + math_object: JsMathExpr("[\"vector\",8.9,6.2]".to_string()), + }; let x_prop = core.get_prop(point_idx, X_LOCAL_IDX); let y_prop = core.get_prop(point_idx, Y_LOCAL_IDX); + let coords_prop = core.get_prop(point_idx, COORDS_LOCAL_IDX); assert_eq!(x_prop.value, math_x.into()); assert_eq!(y_prop.value, math_y.into()); + assert_eq!(coords_prop.value, math_coords.into()); +} + +#[test] +fn move_default_point() { + let dast_root = dast_root_no_position(r#""#); + + let mut core = TestCore::new(); + core.init_from_dast_root(&dast_root); + + let point_idx = core.get_component_index_by_name("P"); + + let move_action = Action { + component_idx: point_idx.into(), + action: ActionsEnum::Point(PointActions::Move(ActionBody { + args: PointMoveActionArgs { x: 1.0, y: 3.2 }, + })), + }; + + let _ = core.dispatch_action(move_action); + + let math_x: MathExpr = 1.0.into(); + let math_y: MathExpr = 3.2.into(); + let math_coords = MathExpr { + math_object: JsMathExpr("[\"vector\",1,3.2]".to_string()), + }; + + let x_prop = core.get_prop(point_idx, X_LOCAL_IDX); + let y_prop = core.get_prop(point_idx, Y_LOCAL_IDX); + let coords_prop = core.get_prop(point_idx, COORDS_LOCAL_IDX); + + assert_eq!(x_prop.value, math_x.clone().into()); + assert_eq!(y_prop.value, math_y.into()); + assert_eq!(coords_prop.value, math_coords.into()); +} + +#[test] +fn move_point_with_x() { + // Note: in order to be able to invert the x-coordinate of `point` in this test environment without wasm, + // the x-coordinate must reference a number. + // Otherwise, the x-coordinate will be based on a string, and its `invert()` will be depend on `.to_text()` of `MathExpr`, + // which requires a call to wasm to run. + let dast_root = + dast_root_no_position(r#"-5.2"#); + + let mut core = TestCore::new(); + core.init_from_dast_root(&dast_root); + + let point_idx = core.get_component_index_by_name("P"); + + let math_x: MathExpr = (-5.2).into(); + let math_y: MathExpr = 0.0.into(); + let math_coords = MathExpr { + math_object: JsMathExpr("[\"vector\",-5.2,0]".to_string()), + }; + + let x_prop = core.get_prop(point_idx, X_LOCAL_IDX); + let y_prop = core.get_prop(point_idx, Y_LOCAL_IDX); + let coords_prop = core.get_prop(point_idx, COORDS_LOCAL_IDX); + + assert_eq!(x_prop.value, math_x.clone().into()); + assert_eq!(y_prop.value, math_y.into()); + assert_eq!(coords_prop.value, math_coords.into()); + + let move_action = Action { + component_idx: point_idx.into(), + action: ActionsEnum::Point(PointActions::Move(ActionBody { + args: PointMoveActionArgs { x: 1.0, y: 3.2 }, + })), + }; + + let _ = core.dispatch_action(move_action); + + let math_x: MathExpr = 1.0.into(); + let math_y: MathExpr = 3.2.into(); + let math_coords = MathExpr { + math_object: JsMathExpr("[\"vector\",1,3.2]".to_string()), + }; + + let x_prop = core.get_prop(point_idx, X_LOCAL_IDX); + let y_prop = core.get_prop(point_idx, Y_LOCAL_IDX); + let coords_prop = core.get_prop(point_idx, COORDS_LOCAL_IDX); + + assert_eq!(x_prop.value, math_x.clone().into()); + assert_eq!(y_prop.value, math_y.into()); + assert_eq!(coords_prop.value, math_coords.into()); +} + +#[test] +fn move_point_with_x_and_y() { + // Note: in order to be able to invert the coordinates of `point` in this test environment without wasm, + // the coordinates must reference numbers. + // Otherwise, the coordinates will be based on strings, and their `invert()` will be depend on `.to_text()` of `MathExpr`, + // which requires a call to wasm to run. + let dast_root = dast_root_no_position( + r#"3.74.8"#, + ); + + let mut core = TestCore::new(); + core.init_from_dast_root(&dast_root); + + let point_idx = core.get_component_index_by_name("P"); + + let math_x: MathExpr = 3.7.into(); + let math_y: MathExpr = 4.8.into(); + let math_coords = MathExpr { + math_object: JsMathExpr("[\"vector\",3.7,4.8]".to_string()), + }; + + let x_prop = core.get_prop(point_idx, X_LOCAL_IDX); + let y_prop = core.get_prop(point_idx, Y_LOCAL_IDX); + let coords_prop = core.get_prop(point_idx, COORDS_LOCAL_IDX); + + assert_eq!(x_prop.value, math_x.clone().into()); + assert_eq!(y_prop.value, math_y.into()); + assert_eq!(coords_prop.value, math_coords.into()); + + let move_action = Action { + component_idx: point_idx.into(), + action: ActionsEnum::Point(PointActions::Move(ActionBody { + args: PointMoveActionArgs { x: 1.0, y: -3.2 }, + })), + }; + + let _ = core.dispatch_action(move_action); + + let math_x: MathExpr = 1.0.into(); + let math_y: MathExpr = (-3.2).into(); + let math_coords = MathExpr { + math_object: JsMathExpr("[\"vector\",1,-3.2]".to_string()), + }; + + let x_prop = core.get_prop(point_idx, X_LOCAL_IDX); + let y_prop = core.get_prop(point_idx, Y_LOCAL_IDX); + let coords_prop = core.get_prop(point_idx, COORDS_LOCAL_IDX); + + assert_eq!(x_prop.value, math_x.clone().into()); + assert_eq!(y_prop.value, math_y.into()); + assert_eq!(coords_prop.value, math_coords.into()); +} + +#[test] +fn dast_of_point_in_graph_returns_x_and_y() { + let dast_root = dast_root_no_position(r#""#); + + let mut core = TestCore::new(); + core.init_from_dast_root(&dast_root); + + let point_idx = core.get_component_index_by_name("P"); + + // check the flat dast + let flat_dast = core.to_flat_dast(); + + let point_renderer = &flat_dast.elements[point_idx]; + + let math_x: MathExpr = 8.9.into(); + let math_y: MathExpr = 6.2.into(); + assert_eq!( + point_renderer.data.props, + Some(ForRenderProps(vec![ + ForRenderPropValue { + name: "x", + value: ForRenderPropValueOrContent::PropValue(math_x.into()) + }, + ForRenderPropValue { + name: "y", + value: ForRenderPropValueOrContent::PropValue(math_y.into()) + } + ])) + ) +} + +#[test] +fn dast_of_point_outside_graph_returns_coords_latex() { + let dast_root = dast_root_no_position(r#""#); + + let mut core = TestCore::new(); + core.init_from_dast_root(&dast_root); + + let point_idx = core.get_component_index_by_name("P"); + + // check the flat dast + let flat_dast = core.to_flat_dast(); + + let point_renderer = &flat_dast.elements[point_idx]; + + // Note: since don't have access to wasm, the contents of the coordsLatex prop are not correct + let for_render_props_vec = &point_renderer.data.props.as_ref().unwrap().0; + assert_eq!(for_render_props_vec.len(), 1); + assert!(matches!( + for_render_props_vec[0], + ForRenderPropValue { + name: "coordsLatex", + value: ForRenderPropValueOrContent::PropValue(..) + } + )); +} + +#[test] +fn rendered_props_with_references() { + let dast_root = dast_root_no_position( + r#" + + + + + + +"#, + ); + + let mut core = TestCore::new(); + core.init_from_dast_root(&dast_root); + + let p_dast_idx = 2; + let q2_dast_idx = 3; + let p2_dast_idx = 4; + let q_dast_idx = 5; + + // check the flat dast + let flat_dast = core.to_flat_dast(); + + let p_renderer = &flat_dast.elements[p_dast_idx]; + let p2_renderer = &flat_dast.elements[p2_dast_idx]; + let q_renderer = &flat_dast.elements[q_dast_idx]; + let q2_renderer = &flat_dast.elements[q2_dast_idx]; + + // Since P and Q2 are in the graph, they have x and y rendered props + let p_x: MathExpr = 3.0.into(); + let p_y: MathExpr = 1.0.into(); + let q2_x: MathExpr = (-1.0).into(); + let q2_y: MathExpr = 7.0.into(); + assert_eq!( + p_renderer.data.props, + Some(ForRenderProps(vec![ + ForRenderPropValue { + name: "x", + value: ForRenderPropValueOrContent::PropValue(p_x.into()) + }, + ForRenderPropValue { + name: "y", + value: ForRenderPropValueOrContent::PropValue(p_y.into()) + } + ])) + ); + + assert_eq!( + q2_renderer.data.props, + Some(ForRenderProps(vec![ + ForRenderPropValue { + name: "x", + value: ForRenderPropValueOrContent::PropValue(q2_x.into()) + }, + ForRenderPropValue { + name: "y", + value: ForRenderPropValueOrContent::PropValue(q2_y.into()) + } + ])) + ); + + // Since P2 and Q are not in a graph, they have rendered coordsLatex prop + // Note: since don't have access to wasm, the contents of the coordsLatex prop are not correct + + let p2_for_render_props_vec = &p2_renderer.data.props.as_ref().unwrap().0; + let q_for_render_props_vec = &q_renderer.data.props.as_ref().unwrap().0; + + assert_eq!(p2_for_render_props_vec.len(), 1); + assert!(matches!( + p2_for_render_props_vec[0], + ForRenderPropValue { + name: "coordsLatex", + value: ForRenderPropValueOrContent::PropValue(..) + } + )); + assert_eq!(q_for_render_props_vec.len(), 1); + assert!(matches!( + q_for_render_props_vec[0], + ForRenderPropValue { + name: "coordsLatex", + value: ForRenderPropValueOrContent::PropValue(..) + } + )); } diff --git a/packages/doenetml-worker-rust/lib-doenetml-core/tests/test_utils/mod.rs b/packages/doenetml-worker-rust/lib-doenetml-core/tests/test_utils/mod.rs index 0d0c60dab..d007b7e0f 100644 --- a/packages/doenetml-worker-rust/lib-doenetml-core/tests/test_utils/mod.rs +++ b/packages/doenetml-worker-rust/lib-doenetml-core/tests/test_utils/mod.rs @@ -1,7 +1,7 @@ //! This file contains utilities for testing DoenetMLCore. -use doenetml_core::components::types::{ComponentIdx, LocalPropIdx, PropPointer}; +use doenetml_core::components::types::{Action, ComponentIdx, LocalPropIdx, PropPointer}; use doenetml_core::dast::ref_resolve::Resolver; -use doenetml_core::dast::{DastRoot, FlatDastRoot, PathPart}; +use doenetml_core::dast::{DastRoot, FlatDastElementUpdate, FlatDastRoot, PathPart}; use doenetml_core::props::cache::PropWithMeta; use doenetml_core::props::traits::IntoPropView; use doenetml_core::props::{PropValue, PropView}; @@ -9,6 +9,7 @@ use doenetml_core::Core; use serde_json; #[allow(unused)] pub use serde_json::{json, Value}; +use std::collections::HashMap; use std::fs::File; use std::io::Write; use std::path::PathBuf; @@ -252,4 +253,11 @@ impl TestCore { pub fn init_from_dast_root(&mut self, dast_root: &DastRoot) { self.resolver = Some(self.core.init_from_dast_root(dast_root)); } + + pub fn dispatch_action( + &mut self, + action: Action, + ) -> Result, String> { + self.core.dispatch_action(action) + } } diff --git a/packages/doenetml-worker-rust/lib-rust-macros/src/component_module/generate_component_module.test.rs b/packages/doenetml-worker-rust/lib-rust-macros/src/component_module/generate_component_module.test.rs index 98fa93336..6188af0c4 100644 --- a/packages/doenetml-worker-rust/lib-rust-macros/src/component_module/generate_component_module.test.rs +++ b/packages/doenetml-worker-rust/lib-rust-macros/src/component_module/generate_component_module.test.rs @@ -116,9 +116,14 @@ mod component { enum Props { #[prop( value_type = PropValueType::ContentRefs, - profile = PropProfile::RenderedChildren + profile = PropProfile::RenderedChildren, + for_render, )] RenderedChildren, + #[prop(value_type = PropValueType::Number, for_render(in_graph))] + InGraph, + #[prop(value_type = PropValueType::Number)] + None } //enum Attributes { // #[attribute(prop = BooleanProp)] diff --git a/packages/doenetml-worker-rust/lib-rust-macros/src/component_module/items/component.rs b/packages/doenetml-worker-rust/lib-rust-macros/src/component_module/items/component.rs index ca5adbb12..3264e9661 100644 --- a/packages/doenetml-worker-rust/lib-rust-macros/src/component_module/items/component.rs +++ b/packages/doenetml-worker-rust/lib-rust-macros/src/component_module/items/component.rs @@ -74,7 +74,18 @@ impl ComponentModule { } }) .collect::>(); - let prop_for_renders = self.props.get_prop_for_renders(); + let prop_for_renders = self + .props + .get_prop_for_renders() + .iter() + .map(|x| { + let in_graph = x.in_graph; + let in_text = x.in_text; + quote! { + crate::props::ForRenderOutputs { in_graph: #in_graph, in_text: #in_text} + } + }) + .collect::>(); let prop_is_publics = self.props.get_prop_is_publics(); let prop_value_types = self.props.get_prop_value_types(); let default_prop = match self.props.get_default_prop_local_index() { @@ -117,7 +128,7 @@ impl ComponentModule { const PROP_PROFILES: &'static [Option] = &[#(#prop_profiles),*]; - const PROP_FOR_RENDERS: &'static [bool] = &[#(#prop_for_renders),*]; + const PROP_FOR_RENDERS: &'static [crate::props::ForRenderOutputs] = &[#(#prop_for_renders),*]; const PROP_IS_PUBLICS: &'static [bool] = &[#(#prop_is_publics),*]; @@ -161,7 +172,7 @@ impl ComponentModule { fn get_num_props(&self) -> usize { Component::PROP_NAMES.len() } - fn get_prop_is_for_render(&self, local_prop_idx: LocalPropIdx) -> bool { + fn get_prop_for_render_outputs(&self, local_prop_idx: LocalPropIdx) -> crate::props::ForRenderOutputs { Component::PROP_FOR_RENDERS[local_prop_idx.as_usize()] } fn get_prop_name(&self, local_prop_idx: LocalPropIdx) -> &'static str { diff --git a/packages/doenetml-worker-rust/lib-rust-macros/src/component_module/items/props.rs b/packages/doenetml-worker-rust/lib-rust-macros/src/component_module/items/props.rs index 9e474b8d8..7a8bd4c04 100644 --- a/packages/doenetml-worker-rust/lib-rust-macros/src/component_module/items/props.rs +++ b/packages/doenetml-worker-rust/lib-rust-macros/src/component_module/items/props.rs @@ -1,7 +1,7 @@ //! Parse the `enum Props {...}` in a component module. use convert_case::{Case, Casing}; -use darling::{FromDeriveInput, FromVariant}; +use darling::{util::Override, FromDeriveInput, FromMeta, FromVariant}; use proc_macro2::{Ident, TokenStream}; use quote::quote; use syn::{parse_quote, Path, Variant}; @@ -27,13 +27,43 @@ pub struct PropsVariant { #[darling(default)] pub default: bool, #[darling(default)] - pub for_render: bool, + pub for_render: Option>, pub attrs: Vec, #[darling(default)] pub doc: Option, } +impl PropsVariant { + /// The `for_render()` method returns a cleaned-up `for_render` value from the result of darling's `Override` processing. + /// In the cleaned-up result, + /// - an absence of `for_render` leads to no render outputs, + /// - a simple `for_render` leads to all render outputs, and + /// - an explicit specification of the outputs, as in `for_render(in_graph)`, contains only the specified outputs. + fn for_render(&self) -> ForRenderOutputsPrelim { + match &self.for_render { + Some(Override::Explicit(value)) => value.clone(), + Some(Override::Inherit) => ForRenderOutputsPrelim { + in_text: true, + in_graph: true, + }, + None => ForRenderOutputsPrelim { + in_text: false, + in_graph: false, + }, + } + } +} + +/// The resulting render outputs calculated from the `for_render` attribute. +#[derive(Debug, Clone, FromMeta)] +pub struct ForRenderOutputsPrelim { + #[darling(default)] + pub in_text: bool, + #[darling(default)] + pub in_graph: bool, +} + /// The `enum Props {...}` in a component module. #[derive(Debug)] pub struct PropsEnum { @@ -104,8 +134,8 @@ impl PropsEnum { } /// The `for_render` property of all props defined on this component - pub fn get_prop_for_renders(&self) -> Vec { - self.get_variants().iter().map(|x| x.for_render).collect() + pub fn get_prop_for_renders(&self) -> Vec { + self.get_variants().iter().map(|x| x.for_render()).collect() } /// The `is_public` property of all props defined on this component @@ -169,12 +199,31 @@ impl PropsEnum { descriptions.push("- Private: this prop can only be used internally.".to_string()) } } - match variant.for_render { - true => descriptions.push( - "- ForRender: this prop is always rendered and available to the UI.".to_string(), - ), - false => descriptions.push("- NotForRender: this prop is not rendered.".to_string()), + + match (variant.for_render().in_graph, variant.for_render().in_text) { + (true, true) => { + descriptions.push( + "- ForRender: this prop is always rendered and available to the UI." + .to_string(), + ); + } + (true, false) => { + descriptions.push( + "- ForRender: this prop is rendered and available to the UI only in a graph." + .to_string(), + ); + } + (false, true) => { + descriptions.push( + "- ForRender: this prop is rendered and available to the UI only in text." + .to_string(), + ); + } + (false, false) => { + descriptions.push("- NotForRender: this prop is not rendered.".to_string()); + } } + match &variant.profile { Some(profile) => descriptions.push(format!( "- Profile: [`{}`]", @@ -297,16 +346,22 @@ impl PropsEnum { } } - /// Generate a typescript type for all the props marked as `for_render`. + /// Generate two typescript types for all the props marked as `for_render`. + /// - `[ComponentName]PropsInGraph`: the props that are marked `for_render` in a graph, + /// - `[ComponentName]PropsInText`: the props that are marked `for_render` in text, fn generate_for_render_props_typescript(&self, component_name: &str) -> TokenStream { - let type_name = format!("{}Props", component_name); + let type_name_in_graph = format!("{}PropsInGraph", component_name); + let type_name_in_text = format!("{}PropsInText", component_name); let for_render_props = self .get_prop_names() .into_iter() .zip(self.get_prop_for_renders()) - .zip(self.get_prop_value_types()) - .filter_map(|((prop_name, is_public), value_type)| { - if !is_public { + .zip(self.get_prop_value_types()); + + let for_render_props_in_graph = for_render_props + .clone() + .filter_map(|((prop_name, for_render), value_type)| { + if !for_render.in_graph { return None; } // The type should be specified as a `PropTypeValue::Foo`, where `Foo` is the prop value @@ -318,14 +373,40 @@ impl PropsEnum { }) .collect::>(); - let for_render_props_ts = for_render_props + let for_render_props_in_text = for_render_props + .filter_map(|((prop_name, for_render), value_type)| { + if !for_render.in_text { + return None; + } + // The type should be specified as a `PropTypeValue::Foo`, where `Foo` is the prop value + // discriminant. It should be safe (if component authors follow the convention) to use the + // `Foo` as a literal type name. (The corresponding type definition of `Foo` should be exported elsewhere.) + let value_type_name = value_type.segments.last().unwrap().ident.to_string(); + + Some((prop_name, value_type_name)) + }) + .collect::>(); + + let for_render_props_in_graph_ts = for_render_props_in_graph + .iter() + .map(|(prop_name, value_type_name)| format!("{}: {}", prop_name, value_type_name)) + .collect::>() + .join(", "); + let for_render_props_in_text_ts = for_render_props_in_text .iter() .map(|(prop_name, value_type_name)| format!("{}: {}", prop_name, value_type_name)) .collect::>() .join(", "); // This is the actual typescript that we want to end up in the generated file. - let type_string = format!("export type {} = {{ {} }};", type_name, for_render_props_ts); + let type_string_in_graph = format!( + "export type {} = {{ {} }};", + type_name_in_graph, for_render_props_in_graph_ts + ); + let type_string_in_text = format!( + "export type {} = {{ {} }};", + type_name_in_text, for_render_props_in_text_ts + ); quote! { // Generated typescript for all props marked as `for_render`. @@ -334,7 +415,11 @@ impl PropsEnum { pub use wasm_bindgen::prelude::*; #[wasm_bindgen(typescript_custom_section)] - const TS_APPEND_CONTENT: &'static str = #type_string; + const TS_APPEND_CONTENT_IN_GRAPH: &'static str = #type_string_in_graph; + + #[wasm_bindgen(typescript_custom_section)] + const TS_APPEND_CONTENT_IN_TEXT: &'static str = #type_string_in_text; + }; } } diff --git a/packages/doenetml-worker-rust/lib-rust-macros/src/lib.rs b/packages/doenetml-worker-rust/lib-rust-macros/src/lib.rs index a87237431..4b0bbc3e9 100644 --- a/packages/doenetml-worker-rust/lib-rust-macros/src/lib.rs +++ b/packages/doenetml-worker-rust/lib-rust-macros/src/lib.rs @@ -69,9 +69,9 @@ mod try_from_ref; /// It has the following options: /// - `name = ...` - Required; the name of your component in PascalCase supplied as an unquoted string. E.g. `name = MyComponent`. /// - `ref_transmutes_to = ...` - Optional; supplied as an unquoted string. If this component is used directly as a reference (i.e. using `$foo` -/// syntax), then instead of creating a component ``, create the component specified by `ref_transmutes_to`. -/// This is used, for example, in the `textInput` component where the code `$a` should render as -/// `` rather than ``. +/// syntax), then instead of creating a component ``, create the component specified by `ref_transmutes_to`. +/// This is used, for example, in the `textInput` component where the code `$a` should render as +/// `` rather than ``. /// /// ### `#[attribute(...)]` /// @@ -88,12 +88,12 @@ mod try_from_ref; /// /// It has the following options: /// - `prop = ...` - Required; the prop that will process this attribute's value. It can be a general prop (e.g. `BooleanProp` or -/// `StringProp`) or a custom prop defined elsewhere. +/// `StringProp`) or a custom prop defined elsewhere. /// - `default = ...` - Required; the default value of the attribute. For strings use `String::new()`, for other types, you can specify -/// their value literally. +/// their value literally. /// - `explicit_type = ...` - Optional; the type of the attribute. If not provided, the type will be inferred from the `prop` attribute. /// - `preserve_refs` - Optional; if set, the references in this attribute will not be expanded into components. Instead, they will become -/// an internal-use-only `_ref` component which preserves a pointer back to the referent component. +/// an internal-use-only `_ref` component which preserves a pointer back to the referent component. /// /// ### `#[prop(...)]` /// @@ -116,9 +116,11 @@ mod try_from_ref; /// - `value_type = ...` - Required; the type of the prop. It should be specified as one of the `PropValueType::...` variants. /// - `is_public` - Optional; if set, the prop will be accessible by a ref in the document. E.g. with `$foo.prop`. /// - `profile = ...` - Optional; the profile that the prop satisfies. It should be specified as one of the `PropProfile::...` variants. -/// If set, this prop will match [`DataQuery`]s for the specified profile. +/// If set, this prop will match [`DataQuery`]s for the specified profile. /// - `default` - Optional; if set, this prop will be the default prop for the component. Only **one** prop can be the default prop. -/// - `for_render` - Optional; if set, this prop will be sent to the renderer. That is, if set, this prop will be included in data sent to the UI. +/// - `for_render` or `for_render(...)` - Optional. If specify `for_render` without arguments, this prop will always be sent to the renderer, whether in a graph or in text. +/// If specify `for_render(in_graph)` or `for_render(in_text)`, the prop will be sent to the renderer only if the component is in a graph or in text. +/// If `for_render` is not given, this prop will be not included in data sent to the UI. /// #[proc_macro_attribute] pub fn component(attr: TokenStream, item: TokenStream) -> TokenStream { @@ -150,8 +152,8 @@ pub fn component(attr: TokenStream, item: TokenStream) -> TokenStream { /// ### Options /// - `query_trait = ...` - Required; the name of the trait that will be created. /// - `pass_data = ...` - Optional; the type of data that should be passed to each `*_query` functions. -/// If provided, the `*_query` functions will have the signature `*_query(&self, arg: )`, -/// and you must pass in the specified data when calling `to_data_queries(...)`. +/// If provided, the `*_query` functions will have the signature `*_query(&self, arg: )`, +/// and you must pass in the specified data when calling `to_data_queries(...)`. /// /// ## Example /// ```ignore