Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
04ddf2d
Initial mashup of mentioned feature. Still need to resolve some quirk…
DustyShoe Dec 24, 2025
c7a163f
Merge branch 'invoke-ai:main' into Feature/Add-Text-tool-to-canvas
DustyShoe Dec 24, 2025
c74fc01
Clean text tool integration
DustyShoe Dec 25, 2025
94e5bb4
Merge branch 'Feature/Add-Text-tool-to-canvas' of https://github.com/…
DustyShoe Dec 25, 2025
8ebcfc3
Merge branch 'invoke-ai:main' into Feature/Add-Text-tool-to-canvas
DustyShoe Dec 25, 2025
16bc7e1
Merge branch 'invoke-ai:main' into Feature/Add-Text-tool-to-canvas
DustyShoe Dec 30, 2025
ff28779
Fixed text tool opions bar jumping and added more fonts
DustyShoe Dec 30, 2025
920297e
Touch up for cursor styling
DustyShoe Dec 30, 2025
c14b527
Minor addition to doc file
DustyShoe Dec 30, 2025
8757edd
Appeasing frontend checks
DustyShoe Dec 30, 2025
c79ecd9
Prettier fix
DustyShoe Dec 30, 2025
37c8533
knip fixes
DustyShoe Dec 30, 2025
17e83a6
Added safe zones to font selection and color picker to be clickable w…
DustyShoe Dec 30, 2025
22f147a
Removed color probing on cursor and added dynamic font display for fa…
DustyShoe Dec 30, 2025
c92e6ed
Finally fixed the text shifting on commit
DustyShoe Dec 30, 2025
5742f33
Cursor now represent actual input field size. Tidy up options UI
DustyShoe Dec 30, 2025
db09e2d
Some strikethrough and underline line tweaks
DustyShoe Dec 30, 2025
aea4811
Replaced the focus retry loop with a callback‑ref based approach in i…
DustyShoe Dec 30, 2025
0920a19
Merge branch 'main' into Feature/Add-Text-tool-to-canvas
DustyShoe Dec 30, 2025
fd0e9d1
Added missing localistaion string
DustyShoe Dec 31, 2025
aee1f0d
Merge branch 'main' into Feature/Add-Text-tool-to-canvas
DustyShoe Jan 7, 2026
fd9fd00
Merge branch 'invoke-ai:main' into Feature/Add-Text-tool-to-canvas
DustyShoe Jan 17, 2026
6b87de4
Merge branch 'main' into Feature/Add-Text-tool-to-canvas
DustyShoe Jan 21, 2026
09040e4
Merge branch 'main' into Feature/Add-Text-tool-to-canvas
DustyShoe Jan 27, 2026
9cbfcd9
Merge branch 'main' into Feature/Add-Text-tool-to-canvas
lstein Jan 30, 2026
72f8613
Moved canvas-text-tool.md to docs/contributing/frontend
DustyShoe Jan 30, 2026
e2f2820
ui: Improve functionality of the text toolbar
blessedcoolant Jan 30, 2026
e8c4b10
Added autoselect text in font size on click allowing immediate imput
DustyShoe Jan 31, 2026
957d3ed
Improvement: Added uncommited layer state with CTRL-move and options …
DustyShoe Jan 31, 2026
982eb34
Added rotation handle to rotate uncommiitted text layer.
DustyShoe Feb 1, 2026
ae0478c
Fix: Redirect user facing labels to use localization file + Add tool …
DustyShoe Feb 1, 2026
f9c336f
Fixed box padding. Disable tool swich when text input is active, adde…
DustyShoe Feb 1, 2026
5f7b6bb
Updated Text tool description
DustyShoe Feb 1, 2026
5e57b61
Updated Text tool description
DustyShoe Feb 1, 2026
389a093
Merge branch 'Feature/Add-Text-tool-to-canvas' of https://github.com/…
DustyShoe Feb 1, 2026
ef69847
Typo
DustyShoe Feb 1, 2026
a52375c
Add draggable text-box border with improved cursor feedback and large…
DustyShoe Feb 2, 2026
9956786
Merge branch 'main' into Feature/Add-Text-tool-to-canvas
DustyShoe Feb 3, 2026
062414b
Lint
DustyShoe Feb 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions docs/contributing/frontend/canvas-text-tool.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Canvas Text Tool

## Overview

The canvas text workflow is split between a Konva module that owns tool state and a React overlay that handles text entry.

- `invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool/CanvasTextToolModule.ts`
- Owns the tool, cursor preview, and text session state (including the cursor "T" marker).
- Manages dynamic cursor contrast, starts sessions on pointer down, and commits sessions by rasterizing the active text block into a new raster layer.
- `invokeai/frontend/web/src/features/controlLayers/components/Text/CanvasTextOverlay.tsx`
- Renders the on-canvas editor as a `contentEditable` overlay positioned in canvas space.
- Syncs keyboard input, suppresses app hotkeys, and forwards commits/cancels to the Konva module.
- `invokeai/frontend/web/src/features/controlLayers/components/Text/TextToolOptions.tsx`
- Provides the font dropdown, size slider/input, formatting toggles, and alignment buttons that appear when the Text tool is active.

## Rasterization pipeline

`renderTextToCanvas()` (`invokeai/frontend/web/src/features/controlLayers/text/textRenderer.ts`) converts the editor contents into a transparent canvas. The Text tool module configures the renderer with the active font stack, weight, styling flags, alignment, and the active canvas color. The resulting canvas is encoded to a PNG data URL and stored in a new raster layer (`image` object) with a transparent background.

Layer placement preserves the original click location:

- The session stores the anchor coordinate (where the user clicked) and current alignment.
- `calculateLayerPosition()` calculates the top-left position for the raster layer after applying the configured padding and alignment offsets.
- New layers are inserted directly above the currently-selected raster layer (when present) and selected automatically.

## Font stacks

Font definitions live in `invokeai/frontend/web/src/features/controlLayers/text/textConstants.ts` as ten deterministic stacks (sans, serif, mono, rounded, script, humanist, slab serif, display, narrow, UI serif). Each stack lists system-safe fallbacks so the editor can choose the first available font per platform.

To add or adjust fonts:

1. Update `TEXT_FONT_STACKS` with the new `id`, `label`, and CSS `font-family` stack.
2. If you add a new stack, extend the `TEXT_FONT_IDS` tuple and update the `canvasTextSlice` schema default (`TEXT_DEFAULT_FONT_ID`).
3. Provide translation strings for any new labels in `public/locales/*`.
4. The editor and renderer will automatically pick up the new stack via `getFontStackById()`.
19 changes: 19 additions & 0 deletions docs/features/Text_tool.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Text Tool

## Font selection

The Text tool uses a set of predefined font stacks. When you choose a font, the app resolves the first available font on your system from that stack and uses it for both the editor overlay and the rasterized result. This provides consistent styling across platforms while still falling back to safe system fonts if a preferred font is missing.

## Size and spacing

- **Size** controls the font size in pixels.
- **Spacing** controls the line height multiplier (Dense, Normal, Spacious). This affects the distance between lines while editing the text.

## Uncommitted state

While text is uncommitted, it remains editable on-canvas. Access to other tools is blocked. Switching to other tabs (Generate, Upascaling, Workflows etc.) discards the text. The uncommitted box can be moved and rotated:

- **Move:** Hold Ctrl (Windows/Linux) or Command (macOS) and drag to move the text box.
- **Rotate:** Drag the rotation handle above the box. Hold **Shift** while rotating to snap to 15 degree increments.

The text is committed to a raster layer when you press **Enter**. Press **Esc** to discard the current text session.
24 changes: 23 additions & 1 deletion invokeai/frontend/web/public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2286,6 +2286,14 @@
"sendToGalleryDesc": "Pressing Invoke generates and saves a unique image to your gallery.",
"sendToCanvas": "Send To Canvas",
"newLayerFromImage": "New Layer from Image",
"text": {
"font": "Font",
"size": "Size",
"lineHeight": "Spacing",
"lineHeightDense": "Dense",
"lineHeightNormal": "Normal",
"lineHeightSpacious": "Spacious"
},
"newCanvasFromImage": "New Canvas from Image",
"newImg2ImgCanvasFromImage": "New Img2Img from Image",
"copyToClipboard": "Copy to Clipboard",
Expand Down Expand Up @@ -2446,7 +2454,20 @@
"bbox": "Bbox",
"move": "Move",
"view": "View",
"colorPicker": "Color Picker"
"colorPicker": "Color Picker",
"text": "Text"
},
"text": {
"font": "Font",
"size": "Size",
"bold": "Bold",
"italic": "Italic",
"underline": "Underline",
"strikethrough": "Strikethrough",
"alignLeft": "Align Left",
"alignCenter": "Align Center",
"alignRight": "Align Right",
"px": "px"
},
"filter": {
"filter": "Filter",
Expand Down Expand Up @@ -2652,6 +2673,7 @@
"HUD": {
"bbox": "Bbox",
"scaledBbox": "Scaled Bbox",
"textSessionActive": "Text input is active",
"entityStatus": {
"isFiltering": "{{title}} is filtering",
"isTransforming": "{{title}} is transforming",
Expand Down
3 changes: 3 additions & 0 deletions invokeai/frontend/web/src/app/store/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { changeBoardModalSliceConfig } from 'features/changeBoardModal/store/sli
import { canvasSettingsSliceConfig } from 'features/controlLayers/store/canvasSettingsSlice';
import { canvasSliceConfig } from 'features/controlLayers/store/canvasSlice';
import { canvasSessionSliceConfig } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { canvasTextSliceConfig } from 'features/controlLayers/store/canvasTextSlice';
import { lorasSliceConfig } from 'features/controlLayers/store/lorasSlice';
import { paramsSliceConfig } from 'features/controlLayers/store/paramsSlice';
import { refImagesSliceConfig } from 'features/controlLayers/store/refImagesSlice';
Expand Down Expand Up @@ -62,6 +63,7 @@ const log = logger('system');
const SLICE_CONFIGS = {
[canvasSessionSliceConfig.slice.reducerPath]: canvasSessionSliceConfig,
[canvasSettingsSliceConfig.slice.reducerPath]: canvasSettingsSliceConfig,
[canvasTextSliceConfig.slice.reducerPath]: canvasTextSliceConfig,
[canvasSliceConfig.slice.reducerPath]: canvasSliceConfig,
[changeBoardModalSliceConfig.slice.reducerPath]: changeBoardModalSliceConfig,
[dynamicPromptsSliceConfig.slice.reducerPath]: dynamicPromptsSliceConfig,
Expand All @@ -87,6 +89,7 @@ const ALL_REDUCERS = {
[api.reducerPath]: api.reducer,
[canvasSessionSliceConfig.slice.reducerPath]: canvasSessionSliceConfig.slice.reducer,
[canvasSettingsSliceConfig.slice.reducerPath]: canvasSettingsSliceConfig.slice.reducer,
[canvasTextSliceConfig.slice.reducerPath]: canvasTextSliceConfig.slice.reducer,
// Undoable!
[canvasSliceConfig.slice.reducerPath]: undoable(
canvasSliceConfig.slice.reducer,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Alert, AlertIcon, AlertTitle } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';

export const CanvasAlertsTextSessionActive = memo(() => {
const { t } = useTranslation();
const canvasManager = useCanvasManager();
const session = useStore(canvasManager.tool.tools.text.$session);

if (!session || session.status === 'committed') {
return null;
}

return (
<Alert status="warning" borderRadius="base" fontSize="sm" shadow="md" w="fit-content">
<AlertIcon />
<AlertTitle>{t('controlLayers.HUD.textSessionActive')}</AlertTitle>
</Alert>
);
});

CanvasAlertsTextSessionActive.displayName = 'CanvasAlertsTextSessionActive';
Loading