This commit is contained in:
poanse 2025-12-18 09:54:24 +03:00
parent c3588ee40b
commit ce3eafb8df
11 changed files with 21 additions and 689 deletions

View file

@ -1,12 +1,12 @@
import TaskmapPlugin from "../main";
import type { ProjectData } from "../ProjectData.svelte";
import { StatusCode, type TaskId } from "../types";
import TaskmapPlugin from "./main";
import type { ProjectData } from "./ProjectData.svelte.js";
import { StatusCode, type TaskId } from "./types";
import { Spring } from "svelte/motion";
import type { App } from "obsidian";
import type { TaskmapView } from "../TaskmapView";
import type { NodePositionsCalculator } from "../NodePositionsCalculator";
import type { TaskmapView } from "./TaskmapView";
import type { NodePositionsCalculator } from "./NodePositionsCalculator";
export class UIState {
export class Context {
app: App;
view: TaskmapView;
nodePositionsCalculator: NodePositionsCalculator;
@ -132,12 +132,8 @@ export class UIState {
}
public getCurrentTaskPosition(taskId: number) {
const pos = this.taskPositions.find((t) => t.taskId === taskId)!.tween!
return this.taskPositions.find((t) => t.taskId === taskId)!.tween!
.current;
if (taskId == 5) {
console.log(JSON.stringify(pos));
}
return pos;
}
public serializeForDebugging() {

View file

@ -1,4 +1,4 @@
import { IconCode, StatusCode } from "../types";
import { IconCode, StatusCode } from "./types";
export const LOGO_NAME = "TaskmapLogo";
export const LOGO_CONTENT = `

View file

@ -3,9 +3,9 @@
import Toolbar from "./Toolbar.svelte";
import Task from "./Task.svelte";
import Panzoom, {type PanzoomObject} from '@panzoom/panzoom'
import type {Context} from "../types";
import type {Context} from "../Context.svelte.js";
let {uiState, projectData}: Context = $props();
let {context}: {context: Context} = $props();
let viewportEl: HTMLDivElement | null = null;
let sceneEl: HTMLDivElement | null = null;
@ -48,7 +48,7 @@
(ev) => {
getPanzoom().zoomWithWheel(ev);
if (getPanzoom().getScale() > 1) {
uiState.incrementUpdateOnZoomCounter();
context.incrementUpdateOnZoomCounter();
}
}
);
@ -68,7 +68,7 @@
function handleKey(e: KeyboardEvent) {
console.log('handleKey ', e.key);
if (e.key === "Escape") {
uiState.setSelectedTaskId(-1);
context.setSelectedTaskId(-1);
e.stopPropagation();
}
}
@ -93,10 +93,10 @@
return
}
mouseDown = false;
console.log(`Window clicked + ${uiState.serializeForDebugging()}`);
console.log('selectedTaskId ' + uiState.selectedTaskId);
uiState.pressedButtonIndex = -1;
uiState.setSelectedTaskId(-1);
console.log(`Window clicked + ${context.serializeForDebugging()}`);
console.log('selectedTaskId ' + context.selectedTaskId);
context.pressedButtonIndex = -1;
context.setSelectedTaskId(-1);
sceneEl!.focus();
}
function getPanzoom() {
@ -115,7 +115,7 @@
style="width: 100%; height: 100vh; overflow: hidden; position: relative; background: #1C1C1C;"
>
<div
class="pixi-container"
class="task-container"
bind:this={sceneEl}
onpointerdown={() => mouseDown = true}
onclick={handleCanvasClick}
@ -124,13 +124,13 @@
style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center;"
role="presentation"
>
{#each projectData.tasks.filter(t => !t.deleted) as task}
<Task taskId={task.taskId} {uiState} {projectData} coords={uiState.getCurrentTaskPosition(task.taskId)} />
{#each context.projectData.tasks.filter(t => !t.deleted) as task}
<Task taskId={task.taskId} {context} coords={context.getCurrentTaskPosition(task.taskId)} />
{/each}
{#if uiState.selectedTaskId !== -1}
{#if context.selectedTaskId !== -1}
<Toolbar
uiState={uiState}
context={context}
/>
{/if}
</div>

View file

@ -1,135 +0,0 @@
import {
type Application,
Color,
Container,
Graphics,
SCALE_MODES,
type Size,
Sprite,
Texture,
} from "pixi.js";
import { match, P } from "ts-pattern";
export enum ButtonState {
INACTIVE = "INACTIVE",
INACTIVE_HOVERED = "INACTIVE_HOVERED",
ACTIVE = "ACTIVE",
ACTIVE_HOVERED = "ACTIVE_HOVERED",
// TODO: pressed
}
export type ColorStateMap = {
[key in ButtonState]: Color;
};
class ButtonOptions {
size: Size;
backgroundColors: ColorStateMap;
iconColors: ColorStateMap;
}
export class Button extends Container {
private app: Application;
private state: ButtonState;
private opts: ButtonOptions;
private iconSource: Promise<Graphics | Texture>;
private background: Sprite;
private icon: Sprite;
constructor(
app: Application,
initialState: ButtonState,
iconSource: Promise<Graphics | Texture>,
opts: ButtonOptions,
) {
super();
this.app = app;
this.state = initialState;
this.iconSource = iconSource;
this.opts = opts;
}
setup() {
this.eventMode = "static";
this.cursor = "pointer";
this.on("pointerover", this.hoverOn);
this.on("pointerout", this.hoverOff);
this.on("pointerdown", () => {
console.log("Clicked icon");
});
// background
// tooltip
// ...
this.update();
// callbacks
// ...
}
async update() {
// TODO: я идиот и менял не тот бэкграунд
// this.background.destroy();
const bgTexture = this.app.renderer.generateTexture({
target: new Graphics()
.roundRect(0, 0, this.opts.size.width, this.opts.size.height, 2)
.fill({ color: this.opts.backgroundColors[this.state] }),
// resolution: 4,
// antialias: true,
});
bgTexture.source.scaleMode = "linear";
this.background = new Sprite(bgTexture);
this.addChild(this.background);
const icon = await this.iconSource;
const iconTexture = match(icon)
.returnType<Texture>()
.with(P.instanceOf(Graphics), () =>
this.app.renderer.generateTexture({
target: icon as Graphics,
// .fill({
// color: this.opts.iconColors[this.state],
// })
// antialias: true,
resolution: 4,
}),
)
.with(P.instanceOf(Texture), () => icon as Texture)
.exhaustive();
iconTexture.source.scaleMode = "linear";
this.icon = new Sprite({
texture: iconTexture,
anchor: 0.5,
// roundPixels: true,
});
this.icon.scale = 0.6;
this.icon.position.set(
this.opts.size.width / 2,
this.opts.size.height / 2,
);
this.addChild(this.icon);
}
changeState(state: ButtonState) {
this.state = state;
this.icon.destroy();
this.update();
}
hoverOn() {
if (this.state == ButtonState.ACTIVE) {
this.changeState(ButtonState.ACTIVE_HOVERED);
} else if (this.state == ButtonState.INACTIVE) {
this.changeState(ButtonState.INACTIVE_HOVERED);
}
}
hoverOff() {
if (this.state == ButtonState.ACTIVE_HOVERED) {
this.changeState(ButtonState.ACTIVE);
} else if (this.state == ButtonState.INACTIVE_HOVERED) {
this.changeState(ButtonState.INACTIVE);
}
}
}

View file

@ -1,219 +0,0 @@
import { Application, Color, Container, Graphics, TextStyle } from "pixi.js";
import TextInput from "./text-input";
import { Button, ButtonState } from "./Button";
import { alphaBlend } from "@napolab/alpha-blend";
import { IconService } from "../pixi/IconService";
export interface TaskCardOptions {
label: string;
width: number;
height: number;
}
const COLOR_WHITE = "#ffffff"; // Double-click detection
const DOUBLE_CLICK_DELAY = 300; // milliseconds
let lastClickTime = 0;
export const TASK_BACKGROUND_DEFAULT_COLOR: Color = new Color("0x1E1E1E");
const TASK_BORDER_DEFAULT_COLOR = new Color("0x343434");
// const TASK_BACKGROUND_DEFAULT_COLOR: Color = new Color(COLOR_WHITE);
const ICON_BACKGROUND_COLOR = new Color("0x1E1E1E");
const HOVERED_ICON_BACKGROUND_COLOR = new Color("0x343434");
const TOOLBAR_BACKGROUND_DEFAULT_COLOR: string = alphaBlend(
ICON_BACKGROUND_COLOR.toRgbaString(),
new Color("#000000").setAlpha(0.5).toRgbaString(),
);
console.log("TASK BACKGROUND COLOR: ", TOOLBAR_BACKGROUND_DEFAULT_COLOR);
export const TASK_DEFAULT_BORDER = {
color: 0x7e7e7e,
width: 1,
};
export const TASK_FOCUSED_BORDER = {
color: 0x7e7e7e,
width: 2,
};
export const TEXT_COLOR = COLOR_WHITE;
export const TEXT_STYLE = new TextStyle({
fill: TEXT_COLOR,
fontSize: 10,
fontFamily: "Segoe UI",
lineHeight: 1.5,
});
export class TaskView extends Container {
private toolbar!: Container;
private card!: Graphics;
// private text!: Text;
private app!: Application;
private readonly opts: TaskCardOptions;
private inputEl: TextInput | null = null;
private mountEl: HTMLDivElement;
constructor(
app: Application,
mountEl: HTMLDivElement,
opts: TaskCardOptions,
) {
super();
this.app = app;
this.mountEl = mountEl;
this.opts = opts;
}
async draw() {
// this.card = new Graphics()
// .roundRect(0, yShift, CARD_W, CARD_H, 10)
// .fill({ color: TASK_BACKGROUND_DEFAULT_COLOR });
// .stroke(TASK_DEFAULT_BORDER)
// this.addChild(this.card);
// TODO: почему-то поле с редактированием текста рисуется на бэкграунде и его не видно
// TODO: после редктирования показываются оба варианта текста...
let input = new TextInput(this.mountEl, {
text: this.opts.label,
input: {
// multiline: true,
fontFamily: TEXT_STYLE.fontFamily,
fontSize: `${TEXT_STYLE.fontSize}pt`,
padding: `${TEXT_STYLE.padding}px`,
width: `${this.opts.width}px`,
height: `${this.opts.height}px`,
color: TEXT_STYLE.fill,
textAlign: "center",
// dom params
"border-radius": 0,
// "background-color": COLOR_WHITE,
// "font-size": `${TEXT_STYLE.fontSize}pt`,
border: "0px solid #ccc",
"box-shadow": "none",
// outline: "none",
},
box: {
default: {
fill: TOOLBAR_BACKGROUND_DEFAULT_COLOR,
rounded: 10,
stroke: TASK_DEFAULT_BORDER,
},
focused: {
fill: TOOLBAR_BACKGROUND_DEFAULT_COLOR,
rounded: 10,
stroke: TASK_FOCUSED_BORDER,
},
disabled: {
fill: TOOLBAR_BACKGROUND_DEFAULT_COLOR,
rounded: 0,
},
},
});
input.placeholder = this.opts.label;
// Background
// input.x = 100;
// input.y = yShift;
// input.eventMode = "static";
// input.on("blur", () => this.finishEditing());
// input.on("pointerdown", (e) => {
// const now = Date.now();
// if (now - lastClickTime < DOUBLE_CLICK_DELAY) {
// this.startEditing();
// }
// lastClickTime = now;
// });
this.addChild(input);
this.inputEl = input;
this.inputEl.render(this.app.renderer);
// this.text = new Text({
// text: opts.label,
// resolution: 4,
// style: new TextStyle({
// fill: COLOR_WHITE,
// fontSize: 10,
// fontFamily: "Segoe UI",
// lineHeight: 1.5,
// }),
// });
// this.text.anchor.set(0.5);
// this.text.position = {
// x: CARD_W / 2,
// y: 50 + CARD_H / 2,
// };
// this.text.eventMode = "static";
// this.text.on("pointerdown", (e) => {
// const now = Date.now();
// if (now - lastClickTime < DOUBLE_CLICK_DELAY) {
// this.startEditing();
// }
// lastClickTime = now;
// });
// this.addChild(this.text);
// const TB_W = 98;
// const TB_H = 22;
// const TB_PADDING = { x: 2, y: 2 };
// const TB_GAP = 1;
//
// this.toolbar = new Container();
// this.toolbar.position = { x: (CARD_W - TB_W) / 2, y: TB_H };
// this.toolbar.addChild(
// new Graphics()
// .roundRect(0, 0, TB_W, TB_H, 4)
// .fill({ color: TOOLBAR_BACKGROUND_DEFAULT_COLOR }),
// );
// this.addChild(this.toolbar);
// // =========================
// // 4) Create icons as vector shapes
// // =========================
//
// const icons = [
// IconService.makeTrashIcon(),
// IconService.makeKeyIcon(),
// IconService.makeLockIcon(),
// IconService.makeFocusIcon(),
// IconService.makeCircleIcon(),
// ];
//
// icons.forEach((icon, i) => {
// const button = new Button(this.app, ButtonState.ACTIVE, icon, {
// size: { width: 18, height: 18 },
// backgroundColors: {
// [ButtonState.ACTIVE]: new Color(
// TOOLBAR_BACKGROUND_DEFAULT_COLOR,
// ),
// [ButtonState.ACTIVE_HOVERED]: HOVERED_ICON_BACKGROUND_COLOR,
// [ButtonState.INACTIVE]: new Color(ICON_BACKGROUND_COLOR),
// [ButtonState.INACTIVE_HOVERED]:
// HOVERED_ICON_BACKGROUND_COLOR,
// },
// iconColors: {
// [ButtonState.ACTIVE]: TASK_BACKGROUND_DEFAULT_COLOR,
// [ButtonState.ACTIVE_HOVERED]: TASK_BACKGROUND_DEFAULT_COLOR,
// [ButtonState.INACTIVE]: TASK_BACKGROUND_DEFAULT_COLOR,
// [ButtonState.INACTIVE_HOVERED]:
// TASK_BACKGROUND_DEFAULT_COLOR,
// },
// });
// button.x = TB_PADDING.x + i * (18 + TB_GAP);
// button.y = TB_PADDING.y;
//
// button.setup();
// this.toolbar.addChild(button);
// });
}
// ==================================================
// LABEL EDIT MODE (HTML input overlay)
// ==================================================
// private startEditing() {
// if (this.inputEl?.visible) return;
// this.inputEl!.visible = true;
// this.inputEl?.focus();
// }
//
// private finishEditing() {
// if (!this.inputEl?.visible) return;
//
// this.text.text = this.inputEl.text;
// this.inputEl.visible = false;
// }
}

View file

@ -1,15 +0,0 @@
<script lang="ts">
import type {TaskmapView} from "../TaskmapView";
import Task from "../components/Task.svelte";
let {
taskmapView
}: {
taskmapView: TaskmapView;
} = $props();
</script>
<div>
<p>Hello world!</p>
<Task taskData={taskmapView.projectData.taskData} taskmapView={taskmapView} />
</div>

View file

@ -1,70 +0,0 @@
<script>
import { Layer } from 'svelte-canvas';
import { Spring } from 'svelte/motion';
let { x, y, color, onclick } = $props();
let dragging = $state(false);
let _x = $state();
let _y = $state();
const radius = new Spring(80, { stiffness: 0.15, damping: 0.2 });
const setup = ({ width, height }) => {
_x = new Spring(width * x, { stiffness: 0.15, damping: 0.2 });
_y = new Spring(height * y, { stiffness: 0.15, damping: 0.2 });
};
const render = ({ context }) => {
context.globalCompositeOperation = 'screen';
context.fillStyle = color;
context.lineWidth = 10;
context.beginPath();
context.arc($_x, $_y, $radius, 0, Math.PI * 2);
context.fill();
context.stroke();
};
const onEnter = () => {
document.body.style.cursor = 'pointer';
radius.set(90);
};
const onLeave = () => {
document.body.style.cursor = 'auto';
dragging = false;
radius.set(80);
};
const onDown = (e) => {
dragging = true;
radius.set(120);
onclick?.();
};
const onUp = () => {
dragging = false;
radius.set(80);
};
const onMove = ({ x, y }) => {
if (dragging) {
_x.set(x);
_y.set(y);
}
};
</script>
<Layer
{setup}
{render}
onmouseenter={onEnter}
onmouseleave={onLeave}
onmousedown={onDown}
onmousemove={onMove}
onmouseup={onUp}
ontouchstart={onDown}
ontouchmove={onMove}
ontouchend={onUp}
/>

View file

@ -1,22 +0,0 @@
<script>
import { Canvas } from 'svelte-canvas';
import Ball from './Ball.svelte';
let balls = [
{ color: 'tomato', x: 0.5, y: 0.333 },
{ color: 'goldenrod', x: 0.333, y: 0.625 },
{ color: 'mediumturquoise', x: 0.667, y: 0.625 },
];
const reorder = (color) => {
balls = balls
.filter((c) => c.color !== color)
.concat(balls.filter((c) => c.color === color));
};
</script>
<Canvas layerEvents style="touch-action: none">
{#each balls as { color, x, y } (color)}
<Ball {color} {x} {y} onclick={() => reorder(color)} />
{/each}
</Canvas>

View file

@ -1,203 +0,0 @@
<script lang="ts">
import {tick} from "svelte";
import {TEXT_COLOR, TEXT_STYLE} from "../TaskView";
import {match, P} from "ts-pattern";
let coords = $state({ x: 150, y: 150 });
let dragging = $state(false);
let taskClicked = $state(false);
let editing = $state(false);
const maxLength = 20;
const taskNameEditSize = 20;
const fontSize = 20;
const fontFamily = match(TEXT_STYLE.fontFamily)
.returnType<string>()
.with(P.string, (str) => str)
.with(P.array<string>(), (ar)=> ar[0])
.exhaustive()
;
let textInput = $state('Task 1');
let title = $state("Task 1");
let task = $state({
width: 240,
height: 80,
shiftX: 2,
shiftY: 50,
rx: 30,
strokeWidthSelected: 4,
strokeWidthDefault: 2,
});
let toolbar = $state({
width: 100,
height: 30,
shiftX: 0,
shiftY: 0,
});
toolbar.shiftX = (task.width - toolbar.width) / 2;
const onMouseMove = (e: MouseEvent) => {
if (dragging) {
coords = { x: e.layerX, y: e.layerY };
}
};
const onMouseUp = () => {
if (dragging) {
dragging = false;
} else if (editing) {
finishEditing(true)
} else if (taskClicked) {
taskClicked = false;
}
};
const onMousedown = () => {
dragging = true;
};
const onTaskClick = () => {
if (!taskClicked) {
taskClicked = true;
} else if (!editing) {
taskClicked = false;
}
};
const onTextClick = () => {
if (!taskClicked) {
taskClicked = true;
} else if (!editing) {
editing = true;
textInput = title;
tick().then(() => {
const el = document.getElementById("titleInput");
if (el) el.focus();
});
}
};
const taskStrokeWidth = () => {
return taskClicked ? task.strokeWidthSelected : task.strokeWidthDefault;
};
function finishEditing(success: boolean) {
if (success && textInput.trim() !== "") {
title = textInput.trim();
// taskmapView.debouncedSave();
}
textInput = title;
editing = false;
}
function handleKey(e: KeyboardEvent) {
if (e.key === "Enter") finishEditing(true);
if (e.key === "Escape") finishEditing(false);
}
</script>
<svg class="Canvas"
width="100%"
height="100%"
onmousemove={onMouseMove}
onmouseup={onMouseUp}
role="presentation"
>
<svg class="Task"
role="presentation"
width="{task.width + task.shiftX + 2 * task.strokeWidthSelected}px"
height="{task.height + task.shiftY + 2 *task.strokeWidthSelected}px"
x={coords.x}
y={coords.y}
>
<rect class="taskBg"
onmousedown={onMousedown}
onclick={onTaskClick}
width={task.width}
height={task.height}
x="{task.shiftX}px"
y="{task.shiftY}px"
rx={task.rx}
fill=#ff3e00
stroke-width="{taskStrokeWidth()}"
stroke="white"
role="presentation"
>
</rect>
<text
onmousedown={onMousedown}
onclick={onTextClick}
x={task.width / 2 + task.shiftX}
y={task.height / 2 + task.shiftY}
fill={TEXT_COLOR}
stroke={TEXT_COLOR}
font-weight="normal"
stroke-width="1px"
dominant-baseline="middle"
text-anchor="middle"
role="presentation"
font-size="{fontSize}px"
font-family="{fontFamily}"
>
{title}
</text>
{#if taskClicked}
<rect class="toolbarBg"
x={toolbar.shiftX}
y={toolbar.shiftY}
width={toolbar.width}
height={toolbar.height}
>
</rect>
{/if}
</svg>
</svg>
{#if editing}
<div
class="textEditDiv"
xmlns="http://www.w3.org/1999/xhtml"
style="
position: absolute;
top: {coords.y + task.shiftY + task.height /2 - fontSize/2 - 7}px;
left: {coords.x + task.shiftX + task.strokeWidthSelected-2}px;
"
>
<input
id="titleInput"
bind:value={textInput}
onkeydown={handleKey}
size={taskNameEditSize}
maxlength={maxLength}
style="
font-family: {fontFamily};
font-size: {fontSize}px;
color: {TEXT_COLOR};
background-color: #ff3e00;
border: 0px;
outline-width: 0px;
padding-left: 5px;
text-align: center;
/*dominant-baseline: middle;*/
/*text-anchor: middle;*/
"
/>
</div>
{/if}
<!--onblur={() => finishEditing(true)}-->
<style>
svg {
position: absolute;
left: 0;
/*right: 200px;*/
top: 0;
/*bottom: 100px;*/
}
.textEditDiv {
position: absolute;
top: 500px;
left: 500px;
}
</style>