Skip to content

Commit 39fab9a

Browse files
committed
Improve text annotation selection and hover UI
1 parent fd295d3 commit 39fab9a

File tree

1 file changed

+134
-67
lines changed

1 file changed

+134
-67
lines changed

apps/desktop/src/routes/screenshot-editor/AnnotationLayer.tsx

Lines changed: 134 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,15 @@ export function AnnotationLayer(props: {
527527
onMouseMove={handleMouseMove}
528528
onMouseUp={handleMouseUp}
529529
>
530+
<style>{`
531+
.text-hover-overlay {
532+
transition: fill 0.15s, stroke 0.15s;
533+
}
534+
.group:hover .text-hover-overlay {
535+
fill: rgba(59, 130, 246, 0.05);
536+
stroke: rgba(59, 130, 246, 0.4);
537+
}
538+
`}</style>
530539
<For each={annotations}>
531540
{(ann) => (
532541
<g
@@ -558,6 +567,7 @@ export function AnnotationLayer(props: {
558567
"line-height": "1",
559568
}}
560569
ref={(el) => {
570+
el.textContent = ann.text ?? "";
561571
setTimeout(() => {
562572
el.focus();
563573
const range = document.createRange();
@@ -568,11 +578,11 @@ export function AnnotationLayer(props: {
568578
});
569579
}}
570580
onInput={(e) => {
571-
const text = e.currentTarget.innerText;
581+
const text = e.currentTarget.textContent ?? "";
572582
setAnnotations((a) => a.id === ann.id, "text", text);
573583
}}
574584
onBlur={(e) => {
575-
const text = e.currentTarget.innerText;
585+
const text = e.currentTarget.textContent ?? "";
576586

577587
if (!text.trim()) {
578588
if (textSnapshot) projectHistory.push(textSnapshot);
@@ -598,16 +608,38 @@ export function AnnotationLayer(props: {
598608
e.currentTarget.blur();
599609
}
600610
}}
601-
>
602-
{ann.text}
603-
</div>
611+
/>
604612
</foreignObject>
605613
</Show>
606614

607615
<Show when={textEditingId() !== ann.id}>
608616
<RenderAnnotation annotation={ann} />
609617
</Show>
610618

619+
{/* Text hover overlay - only shown when not selected */}
620+
<Show
621+
when={
622+
ann.type === "text" &&
623+
selectedAnnotationId() !== ann.id &&
624+
!textEditingId() &&
625+
activeTool() === "select"
626+
}
627+
>
628+
<rect
629+
x={ann.x - handleSize() * 0.3}
630+
y={ann.y - handleSize() * 0.3}
631+
width={Math.abs(ann.width) + handleSize() * 0.6}
632+
height={Math.abs(ann.height) + handleSize() * 0.6}
633+
fill="transparent"
634+
stroke="transparent"
635+
stroke-width={2}
636+
rx={4}
637+
ry={4}
638+
class="text-hover-overlay"
639+
style={{ "pointer-events": "all" }}
640+
/>
641+
</Show>
642+
611643
<Show when={selectedAnnotationId() === ann.id && !textEditingId()}>
612644
<SelectionHandles
613645
annotation={ann}
@@ -733,86 +765,121 @@ function SelectionHandles(props: {
733765
}) {
734766
const half = createMemo(() => props.handleSize / 2);
735767

768+
const isText = () => props.annotation.type === "text";
769+
const isArrow = () => props.annotation.type === "arrow";
770+
771+
const padding = createMemo(() => (isText() ? props.handleSize * 0.3 : 0));
772+
773+
const selectionRect = createMemo(() => {
774+
const ann = props.annotation;
775+
const p = padding();
776+
return {
777+
x: Math.min(ann.x, ann.x + ann.width) - p,
778+
y: Math.min(ann.y, ann.y + ann.height) - p,
779+
width: Math.abs(ann.width) + p * 2,
780+
height: Math.abs(ann.height) + p * 2,
781+
};
782+
});
783+
784+
const cornerHandles = () => {
785+
if (isText()) {
786+
return [
787+
{ id: "nw", x: 0, y: 0 },
788+
{ id: "ne", x: 1, y: 0 },
789+
{ id: "sw", x: 0, y: 1 },
790+
{ id: "se", x: 1, y: 1 },
791+
];
792+
}
793+
return [
794+
{ id: "nw", x: 0, y: 0 },
795+
{ id: "n", x: 0.5, y: 0 },
796+
{ id: "ne", x: 1, y: 0 },
797+
{ id: "w", x: 0, y: 0.5 },
798+
{ id: "e", x: 1, y: 0.5 },
799+
{ id: "sw", x: 0, y: 1 },
800+
{ id: "s", x: 0.5, y: 1 },
801+
{ id: "se", x: 1, y: 1 },
802+
];
803+
};
804+
736805
return (
737806
<Show
738-
when={props.annotation.type === "arrow"}
807+
when={!isArrow()}
739808
fallback={
740809
<g>
741-
<For
742-
each={[
743-
{ id: "nw", x: 0, y: 0 },
744-
{ id: "n", x: 0.5, y: 0 },
745-
{ id: "ne", x: 1, y: 0 },
746-
{ id: "w", x: 0, y: 0.5 },
747-
{ id: "e", x: 1, y: 0.5 },
748-
{ id: "sw", x: 0, y: 1 },
749-
{ id: "s", x: 0.5, y: 1 },
750-
{ id: "se", x: 1, y: 1 },
751-
]}
752-
>
753-
{(handle) => (
754-
<Handle
755-
x={
756-
props.annotation.x +
757-
handle.x * props.annotation.width -
758-
half()
759-
}
760-
y={
761-
props.annotation.y +
762-
handle.y * props.annotation.height -
763-
half()
764-
}
765-
size={props.handleSize}
766-
cursor={`${handle.id}-resize`}
767-
onMouseDown={(e) =>
768-
props.onResizeStart(e, props.annotation.id, handle.id)
769-
}
770-
/>
771-
)}
772-
</For>
810+
<Handle
811+
cx={props.annotation.x}
812+
cy={props.annotation.y}
813+
r={half()}
814+
cursor="crosshair"
815+
isText={false}
816+
onMouseDown={(e) =>
817+
props.onResizeStart(e, props.annotation.id, "start")
818+
}
819+
/>
820+
<Handle
821+
cx={props.annotation.x + props.annotation.width}
822+
cy={props.annotation.y + props.annotation.height}
823+
r={half()}
824+
cursor="crosshair"
825+
isText={false}
826+
onMouseDown={(e) =>
827+
props.onResizeStart(e, props.annotation.id, "end")
828+
}
829+
/>
773830
</g>
774831
}
775832
>
776833
<g>
777-
<Handle
778-
x={props.annotation.x - half()}
779-
y={props.annotation.y - half()}
780-
size={props.handleSize}
781-
cursor="crosshair"
782-
onMouseDown={(e) =>
783-
props.onResizeStart(e, props.annotation.id, "start")
784-
}
785-
/>
786-
<Handle
787-
x={props.annotation.x + props.annotation.width - half()}
788-
y={props.annotation.y + props.annotation.height - half()}
789-
size={props.handleSize}
790-
cursor="crosshair"
791-
onMouseDown={(e) =>
792-
props.onResizeStart(e, props.annotation.id, "end")
793-
}
794-
/>
834+
<Show when={isText()}>
835+
<rect
836+
x={selectionRect().x}
837+
y={selectionRect().y}
838+
width={selectionRect().width}
839+
height={selectionRect().height}
840+
fill="rgba(59, 130, 246, 0.1)"
841+
stroke="#3b82f6"
842+
stroke-width={2}
843+
rx={4}
844+
ry={4}
845+
style={{ "pointer-events": "none" }}
846+
/>
847+
</Show>
848+
<For each={cornerHandles()}>
849+
{(handle) => (
850+
<Handle
851+
cx={selectionRect().x + handle.x * selectionRect().width}
852+
cy={selectionRect().y + handle.y * selectionRect().height}
853+
r={half()}
854+
cursor={`${handle.id}-resize`}
855+
isText={isText()}
856+
onMouseDown={(e) =>
857+
props.onResizeStart(e, props.annotation.id, handle.id)
858+
}
859+
/>
860+
)}
861+
</For>
795862
</g>
796863
</Show>
797864
);
798865
}
799866

800867
function Handle(props: {
801-
x: number;
802-
y: number;
803-
size: number;
868+
cx: number;
869+
cy: number;
870+
r: number;
804871
cursor: string;
872+
isText: boolean;
805873
onMouseDown: (e: MouseEvent) => void;
806874
}) {
807875
return (
808-
<rect
809-
x={props.x}
810-
y={props.y}
811-
width={props.size}
812-
height={props.size}
813-
fill="white"
814-
stroke="#00A0FF"
815-
stroke-width={1}
876+
<circle
877+
cx={props.cx}
878+
cy={props.cy}
879+
r={props.r}
880+
fill={props.isText ? "#3b82f6" : "white"}
881+
stroke={props.isText ? "white" : "#3b82f6"}
882+
stroke-width={props.isText ? 1.5 : 1}
816883
class="cursor-pointer"
817884
style={{
818885
"pointer-events": "all",

0 commit comments

Comments
 (0)