Skip to content

Commit

Permalink
GUI menus and basic layout.
Browse files Browse the repository at this point in the history
  • Loading branch information
Jhuighuy committed Feb 20, 2025
1 parent 0fcf00e commit a35c67e
Show file tree
Hide file tree
Showing 10 changed files with 537 additions and 8 deletions.
70 changes: 70 additions & 0 deletions source/titfront/src/components/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ *\
* Part of BlueTit Solver, licensed under Apache 2.0 with Commons Clause.
* Commercial use, including SaaS, requires a separate license, see /LICENSE.md
\* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */

import { FC } from "react";
import { AiOutlinePython as PythonIcon } from "react-icons/ai";
import {

Check warning on line 8 in source/titfront/src/components/App.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/App.tsx#L7-L8

Added lines #L7 - L8 were not covered by tests
FiActivity as ActivityIcon,
FiDatabase as DatabaseIcon,
FiSettings as SettingsIcon,
FiSliders as SlidersIcon,
FiTerminal as TerminalIcon,
} from "react-icons/fi";

import { Menu, MenuItem } from "~/components/Menu";
import { PythonShell } from "~/components/PythonShell";
import { Viewer } from "~/components/Viewer";

Check warning on line 18 in source/titfront/src/components/App.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/App.tsx#L16-L18

Added lines #L16 - L18 were not covered by tests

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

export const App: FC = () => {

Check warning on line 22 in source/titfront/src/components/App.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/App.tsx#L22

Added line #L22 was not covered by tests
const leftIconSize = 24;
const bottomIconSize = 16;
return (
<div className="h-screen w-screen flex flex-row select-none text-sm">
<Menu side="left">
<MenuItem
name="Configuration"
icon={<SlidersIcon size={leftIconSize} />}
group={0}
/>
<MenuItem
name="Storage"
icon={<DatabaseIcon size={leftIconSize} />}
group={0}
/>
<MenuItem
name="Activity"
icon={<ActivityIcon size={leftIconSize} />}
group={1}
/>
<MenuItem
name="Settings"
icon={<SettingsIcon size={leftIconSize} />}
group={1}

Check warning on line 46 in source/titfront/src/components/App.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/App.tsx#L26-L46

Added lines #L26 - L46 were not covered by tests
/>
</Menu>
<div className="flex-1 flex flex-col">
<Viewer />
<Menu side="bottom">
<MenuItem
name="Python shell"
icon={<PythonIcon size={bottomIconSize} />}
group={0}

Check warning on line 55 in source/titfront/src/components/App.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/App.tsx#L48-L55

Added lines #L48 - L55 were not covered by tests
>
<PythonShell />
</MenuItem>
<MenuItem
name="Console"
icon={<TerminalIcon size={bottomIconSize} />}
group={0}

Check warning on line 62 in source/titfront/src/components/App.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/App.tsx#L57-L62

Added lines #L57 - L62 were not covered by tests
/>
</Menu>
</div>
</div>

Check warning on line 66 in source/titfront/src/components/App.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/App.tsx#L64-L66

Added lines #L64 - L66 were not covered by tests
);
};

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
181 changes: 181 additions & 0 deletions source/titfront/src/components/Menu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ *\
* Part of BlueTit Solver, licensed under Apache 2.0 with Commons Clause.
* Commercial use, including SaaS, requires a separate license, see /LICENSE.md
\* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */

import {

Check warning on line 6 in source/titfront/src/components/Menu.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/Menu.tsx#L6

Added line #L6 was not covered by tests
FC,
Fragment,
ReactNode,
ReactElement,
createContext,
useContext,
} from "react";
import { IconBaseProps } from "react-icons";
import { FiMinimize as MinimizeIcon } from "react-icons/fi";

Check warning on line 15 in source/titfront/src/components/Menu.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/Menu.tsx#L15

Added line #L15 was not covered by tests

import { Resizable } from "~/components/Resizable";
import { useMenuStore } from "~/stores/LayoutStore";
import { Side, iota, cn } from "~/utils";

Check warning on line 19 in source/titfront/src/components/Menu.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/Menu.tsx#L17-L19

Added lines #L17 - L19 were not covered by tests

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

interface Action {
key: string;
icon: ReactElement<IconBaseProps>;
action: () => void;
}

export interface MenuProviderProps {
actions: Action[];
}

const MenuContext = createContext<MenuProviderProps | null>(null);

Check warning on line 33 in source/titfront/src/components/Menu.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/Menu.tsx#L33

Added line #L33 was not covered by tests

export function useMenu() {
return useContext(MenuContext)!;

Check warning on line 36 in source/titfront/src/components/Menu.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/Menu.tsx#L35-L36

Added lines #L35 - L36 were not covered by tests
}

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

export interface MenuItemProps {
name: string;
icon: ReactElement<IconBaseProps>;
group: number;
children?: ReactNode;
}

export const MenuItem: FC<MenuItemProps> = ({ name, children }) => {
const { actions } = useMenu();

Check warning on line 49 in source/titfront/src/components/Menu.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/Menu.tsx#L48-L49

Added lines #L48 - L49 were not covered by tests
return (
<div
className={cn(
"size-full flex flex-col",
"bg-gradient-to-bl from-gray-700 to-gray-800"
)}

Check warning on line 55 in source/titfront/src/components/Menu.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/Menu.tsx#L51-L55

Added lines #L51 - L55 were not covered by tests
>
{/* Title. */}
<div className="h-4 m-1 mt-2 flex flex-row items-center justify-between">
<span className="ml-2 font-medium truncate">{name.toUpperCase()}</span>
<div className="flex flex-row items-center">
{actions.map(({ key, icon, action }) => (
<button
key={key}
className={cn(
"w-6 h-6 mr-2 rounded flex items-center justify-center",
"hover:bg-gray-500"
)}
onClick={action}

Check warning on line 68 in source/titfront/src/components/Menu.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/Menu.tsx#L58-L68

Added lines #L58 - L68 were not covered by tests
>
{icon}
</button>
))}
</div>
</div>

Check warning on line 74 in source/titfront/src/components/Menu.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/Menu.tsx#L70-L74

Added lines #L70 - L74 were not covered by tests
{/* Contents. */}
<div
className={cn("flex-grow m-1 rounded-lg overflow-auto", "bg-gray-900")}

Check warning on line 77 in source/titfront/src/components/Menu.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/Menu.tsx#L76-L77

Added lines #L76 - L77 were not covered by tests
>
{children}
</div>
</div>

Check warning on line 81 in source/titfront/src/components/Menu.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/Menu.tsx#L79-L81

Added lines #L79 - L81 were not covered by tests
);
};

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

export interface MenuProps {
side: Side;
children: ReactElement<MenuItemProps>[];
}

export const Menu: FC<MenuProps> = ({ side, children }) => {
const { size, setSize, activeItem, setActiveItem } = useMenuStore(side);

Check warning on line 93 in source/titfront/src/components/Menu.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/Menu.tsx#L92-L93

Added lines #L92 - L93 were not covered by tests

const horizontal = side === "left" || side === "right";
const flexDirection = {
left: "flex-row",
right: "flex-row-reverse",
top: "flex-col",
bottom: "flex-col-reverse",
}[side];
const actions: Action[] = [

Check warning on line 102 in source/titfront/src/components/Menu.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/Menu.tsx#L95-L102

Added lines #L95 - L102 were not covered by tests
{
key: "hide",
icon: <MinimizeIcon size={16} />,
action: () => setActiveItem(-1),

Check warning on line 106 in source/titfront/src/components/Menu.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/Menu.tsx#L104-L106

Added lines #L104 - L106 were not covered by tests
},
];

Check warning on line 108 in source/titfront/src/components/Menu.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/Menu.tsx#L108

Added line #L108 was not covered by tests

const maxGroup = children.reduce(
(max, child) => Math.max(max, child.props.group),

Check warning on line 111 in source/titfront/src/components/Menu.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/Menu.tsx#L110-L111

Added lines #L110 - L111 were not covered by tests
0
);

Check warning on line 113 in source/titfront/src/components/Menu.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/Menu.tsx#L113

Added line #L113 was not covered by tests

const barCn = horizontal
? cn(
"w-16 h-full pb-6",
"flex flex-col items-center justify-between",
"bg-gradient-to-bl from-gray-800 to-gray-950"
)
: cn(
"w-full h-8 pl-1 flex flex-row items-center",
"bg-gradient-to-bl from-gray-800 to-gray-950"
);
const baseItemCn = horizontal
? cn("size-11 mt-6 flex flex-col items-center justify-center rounded-lg")
: cn("w-30 h-6 pl-1 flex flex-row items-center rounded-sm");
const itemCn = cn(
baseItemCn,
"hover:bg-gradient-to-bl hover:from-gray-700 hover:to-gray-800",
"hover:text-gray-100 hover:shadow-xl"
);
const activeItemCn = cn(
baseItemCn,
"inset-shadow-sm inset-shadow-gray-700",
"bg-gradient-to-bl from-gray-700 to-indigo-900 text-gray-100"
);

Check warning on line 137 in source/titfront/src/components/Menu.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/Menu.tsx#L115-L137

Added lines #L115 - L137 were not covered by tests
return (
<div className={cn("flex text-gray-300", flexDirection)}>
<div className={barCn}>
{iota(maxGroup + 1).map((group) => (
<div
key={group}
className={cn(horizontal || "flex flex-row items-center")}

Check warning on line 144 in source/titfront/src/components/Menu.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/Menu.tsx#L139-L144

Added lines #L139 - L144 were not covered by tests
>
{children.map(

Check warning on line 146 in source/titfront/src/components/Menu.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/Menu.tsx#L146

Added line #L146 was not covered by tests
(item, index) =>
item.props.group == group && (
<Fragment key={index}>
<button
className={index === activeItem ? activeItemCn : itemCn}
onClick={() => setActiveItem(index)}

Check warning on line 152 in source/titfront/src/components/Menu.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/Menu.tsx#L148-L152

Added lines #L148 - L152 were not covered by tests
>
{item.props.icon}
{horizontal || (
<span className="ml-2 fond-semibold">
{item.props.name}
</span>

Check warning on line 158 in source/titfront/src/components/Menu.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/Menu.tsx#L154-L158

Added lines #L154 - L158 were not covered by tests
)}
</button>
{horizontal || (
<div className="w-[1px] h-6 mr-1 ml-1 bg-gray-500" />

Check warning on line 162 in source/titfront/src/components/Menu.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/Menu.tsx#L160-L162

Added lines #L160 - L162 were not covered by tests
)}
</Fragment>

Check warning on line 164 in source/titfront/src/components/Menu.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/Menu.tsx#L164

Added line #L164 was not covered by tests
)
)}
</div>
))}
</div>
{activeItem !== -1 && (
<Resizable side={side} size={size} setSize={setSize}>
<MenuContext.Provider value={{ actions }}>
{children[activeItem]}
</MenuContext.Provider>
</Resizable>
)}
</div>

Check warning on line 177 in source/titfront/src/components/Menu.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/Menu.tsx#L166-L177

Added lines #L166 - L177 were not covered by tests
);
};

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
2 changes: 1 addition & 1 deletion source/titfront/src/components/PythonShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ export const PythonShell: FC = () => {
data-testid="python-shell"
className={cn(
"size-full flex flex-col overflow-auto",
"font-mono select-text bg-gray-900"
"font-mono select-text"
)}
aria-label="Python Shell"
onClick={handleClick}
Expand Down
123 changes: 123 additions & 0 deletions source/titfront/src/components/Resizable.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ *\
* Part of BlueTit Solver, licensed under Apache 2.0 with Commons Clause.
* Commercial use, including SaaS, requires a separate license, see /LICENSE.md
\* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */

import "@testing-library/jest-dom/vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import { vi } from "vitest";

import { Resizable } from "~/components/Resizable";

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

describe("Resizable", () => {
it("renders children correctly", () => {
const { getByText } = render(
<Resizable side="left" size={200} setSize={() => {}}>
<div>Test</div>
</Resizable>
);
expect(getByText("Test")).toBeInTheDocument();
});

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

it("applies initial width correctly", () => {
render(
<Resizable side="left" size={200} setSize={() => {}}>
<div>Test</div>
</Resizable>
);
expect(screen.getByTestId("resizable-content")).toHaveStyle("width: 200px");
});

it("applies initial height correctly", () => {
render(
<Resizable side="bottom" size={200} setSize={() => {}}>
<div>Test</div>
</Resizable>
);
expect(screen.getByTestId("resizable-content")).toHaveStyle(
"height: 200px"
);
});

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

it("handles width changes with mouse correctly", () => {
const setSize = vi.fn();
render(
<Resizable side="left" size={200} setSize={setSize}>
<div>Test Content</div>
</Resizable>
);

const resizer = screen.getByTestId("resizable-resizer");

fireEvent.mouseDown(resizer, { clientX: 0 });
fireEvent.mouseMove(window, { clientX: 50 });
expect(document.body.style.cursor).toBe("ew-resize");
expect(setSize).toHaveBeenCalledWith(250);

fireEvent.mouseUp(window);
expect(document.body.style.cursor).toBe("");
});

it("handles height changes with mouse correctly", () => {
const setSize = vi.fn();
render(
<Resizable side="bottom" size={200} setSize={setSize}>
<div>Test Content</div>
</Resizable>
);
const resizer = screen.getByTestId("resizable-resizer");

fireEvent.mouseDown(resizer, { clientY: 0 });
fireEvent.mouseMove(window, { clientY: -50 });
expect(document.body.style.cursor).toBe("ns-resize");
expect(setSize).toHaveBeenCalledWith(250);

fireEvent.mouseUp(window);
expect(document.body.style.cursor).toBe("");
});

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

it("respects size constraints", () => {
const setSize = vi.fn();
render(
<Resizable
side="left"
size={200}
setSize={setSize}
minSize={100}
maxSize={300}
>
<div>Test Content</div>
</Resizable>
);
const resizer = screen.getByTestId("resizable-resizer");

fireEvent.mouseDown(resizer, { clientX: 0 });
fireEvent.mouseMove(window, { clientX: 50 });
expect(setSize).toHaveBeenCalledWith(250);

fireEvent.mouseMove(window, { clientX: 150 });
expect(setSize).toHaveBeenCalledWith(300);

fireEvent.mouseUp(window);

fireEvent.mouseDown(resizer, { clientX: 0 });
fireEvent.mouseMove(window, { clientX: -50 });
expect(setSize).toHaveBeenCalledWith(150);

fireEvent.mouseMove(window, { clientX: -150 });
expect(setSize).toHaveBeenCalledWith(100);

fireEvent.mouseUp(window);
});
});

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Loading

0 comments on commit a35c67e

Please sign in to comment.