Skip to content

Commit c8cc513

Browse files
committed
merge UI
1 parent 28d3ad8 commit c8cc513

45 files changed

Lines changed: 1578 additions & 222 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/frontend/src/components/id_input.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ export type IdInputOptions = {
2727
labelToId?: (label: QualifiedLabel) => NameLookup | undefined;
2828
isInvalid?: boolean;
2929
completions?: Uuid[];
30+
/** Called when the displayed text changes. */
31+
onTextChange?: (text: string) => void;
3032
} & Omit<InlineInputOptions, "completions" | "status">;
3133

3234
/** Input a UUID by specifying its human-readable name.
@@ -47,6 +49,7 @@ export function IdInput(
4749
"idToLabel",
4850
"labelToId",
4951
"isInvalid",
52+
"onTextChange",
5053
]);
5154

5255
const idToLabel = (id: QualifiedName): QualifiedLabel | undefined => props.idToLabel?.(id);
@@ -72,6 +75,19 @@ export function IdInput(
7275

7376
createEffect(() => updateText(props.id));
7477

78+
// Re-check if we can match an id when the elaborated model changes
79+
createEffect(() => {
80+
const currentText = text();
81+
if (currentText !== "" && !isComplete()) {
82+
const lookup = textToId(currentText);
83+
if (lookup.tag !== "None") {
84+
props.setId(lookup.content);
85+
}
86+
}
87+
});
88+
89+
createEffect(() => props.onTextChange?.(text()));
90+
7591
const handleNewText = (text: string) => {
7692
const lookup = textToId(text);
7793
if (lookup.tag !== "None") {
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
.morphism-decl {
2+
display: flex;
3+
flex-direction: row;
4+
align-items: center;
5+
gap: 0.25ex;
6+
}
7+
8+
.morphism-decl-name-separator {
9+
color: var(--color-gray-800);
10+
padding-left: 0.75ex;
11+
padding-right: 1.25ex;
12+
}
13+
14+
.morphism-decl-arrow-replacement {
15+
color: var(--color-gray-800);
16+
padding-right: 0.5ex;
17+
}
18+
19+
.morphism-decl-cod-prefix {
20+
font-size: 0.9rem;
21+
margin-right: -0.5ex;
22+
display: flex;
23+
flex-direction: column;
24+
text-align: center;
25+
26+
.fraction-numerator {
27+
border-bottom: 1px solid black;
28+
}
29+
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import { createMemo, createSignal, useContext } from "solid-js";
2+
import invariant from "tiny-invariant";
3+
4+
import { NameInput } from "catcolab-ui-components";
5+
import type { Ob } from "catlog-wasm";
6+
import { LiveModelContext } from "./context";
7+
import { ContributionMonomialEditor } from "./contribution_monomial_editor";
8+
import type { MorphismEditorProps } from "./editors";
9+
import { unwrapApp, wrapApp } from "./ob_operations";
10+
import { obClasses } from "./object_cell_editor";
11+
import { ObInput } from "./object_input";
12+
13+
import "./contribution_cell_editor.css";
14+
15+
/** Editor for a contribution declaration cell in a model. */
16+
export default function ContributionCellEditor(props: MorphismEditorProps) {
17+
const liveModel = useContext(LiveModelContext);
18+
invariant(liveModel, "Live model should be provided as context");
19+
20+
const [activeInput, setActiveInput] = createSignal<MorphismCellInput>("name");
21+
22+
const morTypeMeta = () => props.theory.modelMorTypeMeta(props.morphism.morType);
23+
24+
const domType = createMemo(() => {
25+
const theory = props.theory.theory;
26+
const op = morTypeMeta()?.domain?.apply;
27+
if (op === undefined) {
28+
return theory.src(props.morphism.morType);
29+
} else {
30+
// Codomain type for operation should equal source type above.
31+
return theory.dom(op);
32+
}
33+
});
34+
35+
const codType = createMemo(() => {
36+
const theory = props.theory.theory;
37+
const op = morTypeMeta()?.codomain?.apply;
38+
if (op === undefined) {
39+
return theory.tgt(props.morphism.morType);
40+
} else {
41+
// Codomain type for operation should equal target type above.
42+
return theory.dom(op);
43+
}
44+
});
45+
46+
const domClasses = () => ["morphism-decl-dom", ...obClasses(props.theory, domType())];
47+
const codClasses = () => ["morphism-decl-cod", ...obClasses(props.theory, codType())];
48+
49+
const nameClasses = () => ["morphism-decl-name", ...(morTypeMeta()?.textClasses ?? [])];
50+
51+
const errors = () => {
52+
const validated = liveModel().validatedModel();
53+
if (validated?.tag !== "Invalid") {
54+
return [];
55+
}
56+
return validated.errors.filter((err) => err.content === props.morphism.id);
57+
};
58+
59+
const domApplyOp = () => morTypeMeta()?.domain?.apply;
60+
61+
const domOb = () => {
62+
const op = domApplyOp();
63+
return op ? unwrapApp(props.morphism.dom, op) : props.morphism.dom;
64+
};
65+
66+
const setDomOb = (ob: Ob | null) => {
67+
const op = domApplyOp();
68+
const wrapped = ob && op ? wrapApp(ob, op) : ob;
69+
props.modifyMorphism((mor) => {
70+
mor.dom = wrapped;
71+
});
72+
};
73+
74+
return (
75+
<div class="formal-judgment morphism-decl">
76+
<div class={nameClasses().join(" ")}>
77+
<NameInput
78+
placeholder={morTypeMeta()?.preferUnnamed ? undefined : "Unnamed"}
79+
name={props.morphism.name}
80+
setName={(name) => {
81+
props.modifyMorphism((mor) => {
82+
mor.name = name;
83+
});
84+
}}
85+
isActive={props.isActive && activeInput() === "name"}
86+
deleteBackward={props.actions.deleteBackward}
87+
deleteForward={props.actions.deleteForward}
88+
exitBackward={props.actions.activateAbove}
89+
exitForward={() => setActiveInput("cod")}
90+
exitUp={props.actions.activateAbove}
91+
exitDown={props.actions.activateBelow}
92+
exitLeft={() => setActiveInput("cod")}
93+
exitRight={() => setActiveInput("dom")}
94+
hasFocused={() => {
95+
setActiveInput("name");
96+
props.actions.hasFocused?.();
97+
}}
98+
/>
99+
</div>
100+
<div class="morphism-decl-name-separator">:</div>
101+
<div class="morphism-decl-cod-prefix">
102+
<div class="fraction-numerator">d</div>
103+
<div class="fraction-denominator">dt</div>
104+
</div>
105+
<div class={codClasses().join(" ")}>
106+
<ObInput
107+
placeholder="..."
108+
ob={props.morphism.cod}
109+
setOb={(ob) => {
110+
props.modifyMorphism((mor) => {
111+
mor.cod = ob;
112+
});
113+
}}
114+
obType={codType()}
115+
applyOp={morTypeMeta()?.codomain?.apply}
116+
isInvalid={errors().some((err) => err.tag === "Cod" || err.tag === "CodType")}
117+
isActive={props.isActive && activeInput() === "cod"}
118+
deleteForward={() => setActiveInput("name")}
119+
exitBackward={props.actions.activateAbove}
120+
exitForward={() => setActiveInput("dom")}
121+
exitLeft={() => setActiveInput("name")}
122+
hasFocused={() => {
123+
setActiveInput("cod");
124+
props.actions.hasFocused?.();
125+
}}
126+
/>
127+
</div>
128+
<div class="morphism-decl-arrow-replacement">+=</div>
129+
<div class={domClasses().join(" ")}>
130+
<ContributionMonomialEditor
131+
placeholder="..."
132+
ob={domOb()}
133+
setOb={setDomOb}
134+
obType={domType()}
135+
isInvalid={errors().some((err) => err.tag === "Dom" || err.tag === "DomType")}
136+
isActive={props.isActive && activeInput() === "dom"}
137+
deleteBackward={() => setActiveInput("name")}
138+
exitBackward={() => setActiveInput("name")}
139+
exitForward={props.actions.activateBelow}
140+
exitRight={props.actions.activateBelow}
141+
hasFocused={() => {
142+
setActiveInput("dom");
143+
props.actions.hasFocused?.();
144+
}}
145+
/>
146+
</div>
147+
</div>
148+
);
149+
}
150+
151+
type MorphismCellInput = "name" | "dom" | "cod";
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
.monomial {
2+
display: flex;
3+
flex-direction: row;
4+
align-items: center;
5+
}
6+
7+
.delimiter {
8+
color: var(--color-gray-800);
9+
transform: scale(1, 1.5);
10+
}
11+
12+
.separator {
13+
color: var(--color-gray-800);
14+
}
15+
16+
.collapsed {
17+
cursor: text;
18+
}
19+
20+
.exponent {
21+
font-size: 0.75em;
22+
line-height: 0.5ex;
23+
}
24+
25+
.emptyMonomial {
26+
color: var(--color-gray-500);
27+
}
28+
29+
.productSeparator {
30+
padding: 0.25ex;
31+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { deepEqual } from "fast-equals";
2+
import { Index, Show, useContext } from "solid-js";
3+
import type { JSX } from "solid-js";
4+
import invariant from "tiny-invariant";
5+
6+
import type { TextInputOptions } from "catcolab-ui-components";
7+
import type { Ob } from "catlog-wasm";
8+
import { LiveModelContext } from "./context";
9+
import { extractObList } from "./ob_operations";
10+
import type { ObInputProps } from "./object_input";
11+
import { ObListEditor } from "./object_list_editor";
12+
13+
import styles from "./contribution_monomial_editor.module.css";
14+
15+
type ContributionMonomialEditorProps = ObInputProps &
16+
TextInputOptions & {
17+
insertKey?: string;
18+
startDelimiter?: JSX.Element | string;
19+
endDelimiter?: JSX.Element | string;
20+
separator?: (index: number) => JSX.Element | string;
21+
};
22+
23+
/** A run-length encoded entry: the object and how many times it repeats. */
24+
type RunEntry = {
25+
ob: Ob | null;
26+
count: number;
27+
};
28+
29+
/** Count occurrences of each distinct object, preserving first-appearance order. */
30+
function countObjects(objects: Array<Ob | null>): RunEntry[] {
31+
const entries: RunEntry[] = [];
32+
for (const ob of objects) {
33+
const existing = entries.find((e) => deepEqual(e.ob, ob));
34+
if (existing) {
35+
existing.count++;
36+
} else {
37+
entries.push({ ob, count: 1 });
38+
}
39+
}
40+
return entries;
41+
}
42+
43+
/** Edits a list of objects, displaying repeated objects with superscript counts when not editing. */
44+
export function ContributionMonomialEditor(props: ContributionMonomialEditorProps) {
45+
const liveModel = useContext(LiveModelContext);
46+
invariant(liveModel, "Live model should be provided as context");
47+
48+
const obList = (): Array<Ob | null> => extractObList(props.ob);
49+
50+
const runs = () => countObjects(obList());
51+
52+
/** Resolve the label for an object, returning null if not available. */
53+
const obLabel = (ob: Ob | null): string | null => {
54+
if (!ob || ob.tag !== "Basic") {
55+
return null;
56+
}
57+
return liveModel().elaboratedModel()?.obGeneratorLabel(ob.content)?.join(".") ?? null;
58+
};
59+
60+
return (
61+
<Show
62+
when={props.isActive || obList().some((ob) => ob === null)}
63+
fallback={
64+
<div
65+
class={`${styles.monomial} ${styles.collapsed}`}
66+
onMouseDown={(evt) => {
67+
props.hasFocused?.();
68+
evt.preventDefault();
69+
}}
70+
>
71+
<Index each={runs()} fallback={<span class={styles.emptyMonomial}>...</span>}>
72+
{(run, index) => (
73+
<span>
74+
{obLabel(run().ob) ?? "..."}
75+
<Show when={run().count > 1}>
76+
<sup class={styles.exponent}>{run().count}</sup>
77+
</Show>
78+
<Show when={index < runs().length - 1}>
79+
<span class={styles.productSeparator}>&middot;</span>
80+
</Show>
81+
</span>
82+
)}
83+
</Index>
84+
</div>
85+
}
86+
>
87+
<div class={styles.monomial}>
88+
<ObListEditor
89+
ob={props.ob}
90+
setOb={props.setOb}
91+
obType={props.obType}
92+
placeholder={props.placeholder}
93+
isInvalid={props.isInvalid}
94+
isActive={props.isActive}
95+
deleteBackward={props.deleteBackward}
96+
deleteForward={props.deleteForward}
97+
exitBackward={props.exitBackward}
98+
exitForward={props.exitForward}
99+
exitLeft={props.exitLeft}
100+
exitRight={props.exitRight}
101+
hasFocused={props.hasFocused}
102+
insertKey={props.insertKey ?? ","}
103+
startDelimiter={<div class={styles.delimiter}>{"["}</div>}
104+
endDelimiter={<div class={styles.delimiter}>{"]"}</div>}
105+
separator={() => <div class={styles.separator}>{","}</div>}
106+
/>
107+
</div>
108+
</Show>
109+
);
110+
}

0 commit comments

Comments
 (0)