diff --git a/web/src/app/pattern-lab/panels/TextPanel.module.css b/web/src/app/pattern-lab/panels/TextPanel.module.css index 4ef8ac4b1..516f61ba6 100644 --- a/web/src/app/pattern-lab/panels/TextPanel.module.css +++ b/web/src/app/pattern-lab/panels/TextPanel.module.css @@ -72,6 +72,13 @@ height: 32px; } +.field input[type="range"] { + width: 100%; + min-height: 32px; + margin: 0; + accent-color: #171512; +} + .fontFile, .actions { display: flex; @@ -136,6 +143,7 @@ @media (hover: none) { .field select, .field input[type="number"], + .field input[type="range"], .fontFile button, .actions button { min-height: 44px; diff --git a/web/src/app/pattern-lab/panels/TextPanel.test.tsx b/web/src/app/pattern-lab/panels/TextPanel.test.tsx index 6292bec09..0cc2b3cf8 100644 --- a/web/src/app/pattern-lab/panels/TextPanel.test.tsx +++ b/web/src/app/pattern-lab/panels/TextPanel.test.tsx @@ -22,6 +22,7 @@ vi.mock("@/lib/lab/textRaster", () => ({ })); import TextPanel from "./TextPanel"; +import KnobsPanel from "./KnobsPanel"; import PreviewPanel from "./PreviewPanel"; import RampPanel from "./RampPanel"; import { useLabStore } from "@/lib/lab/store"; @@ -160,10 +161,49 @@ describe("an effect on the text", () => { fireEvent.change(screen.getByRole("combobox", { name: "Speed knob" }), { target: { value: "0" } }); expect(text().spec.effectKnob).toBe(0); expect(screen.getByText(`${store().knobLabels[0]} sets how fast each LED dims`, { exact: false })).toHaveTextContent( - "It is all the way down, so nothing moves yet: turn it up in Knobs.", + "It is all the way down, so nothing moves yet: turn it up here or in Knobs.", ); }); + it("turns the speed knob beside its picker, the same knob Knobs turns", () => { + store().addTextLayer(); + store().setTextSpec(text().id, { effect: "scroll" }); + useLabStore.setState({ knobLabels: ["Hue", "Rate", "Mode", "Shift"] }); + const [low, high] = store().ranges[1]; + const textPanel = within(render().container); + const knobsPanel = within(render().container); + const here = textPanel.getByRole("slider", { name: "Rate" }); + const there = knobsPanel.getByRole("slider", { name: "Rate value" }); + + fireEvent.change(here, { target: { value: String(high) } }); + expect(store().knobs[1]).toBe(high); + expect(there).toHaveValue(String(high)); + expect(textPanel.getByText(/Rate sets how fast/)).toHaveTextContent("It is at 100% now."); + + fireEvent.change(there, { target: { value: String(low) } }); + expect(here).toHaveValue(String(low)); + expect(textPanel.getByText(/Rate sets how fast/)).toHaveTextContent("turn it up here or in Knobs."); + }); + + it("moves the slider to the knob a new pick names, and has none without an effect", () => { + store().addTextLayer(); + store().setTextSpec(text().id, { effect: "twinkle" }); + useLabStore.setState({ knobLabels: ["Hue", "Rate", "Mode", "Shift"] }); + const { ranges } = store(); + useLabStore.setState({ knobs: [ranges[0][1], ranges[1][0], ranges[2][0], ranges[3][0]] }); + render(); + expect(screen.getByRole("slider", { name: "Rate" })).toHaveValue(String(ranges[1][0])); + fireEvent.change(screen.getByRole("combobox", { name: "Speed knob" }), { target: { value: "0" } }); + expect(screen.queryByRole("slider", { name: "Rate" })).toBeNull(); + const hue = screen.getByRole("slider", { name: "Hue" }); + expect(hue).toHaveValue(String(ranges[0][1])); + fireEvent.change(hue, { target: { value: String(ranges[0][0]) } }); + expect(store().knobs).toEqual([ranges[0][0], ranges[1][0], ranges[2][0], ranges[3][0]]); + + fireEvent.change(screen.getByRole("combobox", { name: "Effect" }), { target: { value: "none" } }); + expect(screen.queryByRole("slider")).toBeNull(); + }); + it("names the knobs as the Knobs panel does", () => { store().addTextLayer(); store().setTextSpec(text().id, { effect: "scroll" }); diff --git a/web/src/app/pattern-lab/panels/TextPanel.tsx b/web/src/app/pattern-lab/panels/TextPanel.tsx index eca9e6b02..7f88a69b1 100644 --- a/web/src/app/pattern-lab/panels/TextPanel.tsx +++ b/web/src/app/pattern-lab/panels/TextPanel.tsx @@ -12,6 +12,9 @@ // // An effect moves the letters every frame at a speed one knob sets // (lib/lab/textEffects.ts), and the export and the firmware header carry it. +// That knob turns here too, beside its picker: the same store value the Knobs +// panel turns, so on a phone, where the two are tabs of one group, setting a +// speed needs no trip to Knobs and back. import { useEffect, useRef, useState, type ChangeEvent } from "react"; import { useResettingState } from "@/lib/hooks/useResettingState"; @@ -170,10 +173,11 @@ export default function TextPanel() { function TextEditor({ layer }: { layer: TextLayer }) { const setTextSpec = useLabStore((state) => state.setTextSpec); + const setKnob = useLabStore((state) => state.setKnob); const knobLabels = useLabStore((state) => state.knobLabels); - const speed = useLabStore((state) => - Math.round(knobShare(state.knobs[layer.spec.effectKnob], state.ranges[layer.spec.effectKnob]) * 100), - ); + const knobValue = useLabStore((state) => state.knobs[layer.spec.effectKnob] ?? 0); + const knobRange = useLabStore((state) => state.ranges[layer.spec.effectKnob]); + const speed = Math.round(knobShare(knobValue, knobRange) * 100); const options = useFontOptions(); const fileRef = useRef(null); const [remember, setRemember] = useState(false); @@ -201,6 +205,7 @@ function TextEditor({ layer }: { layer: TextLayer }) { options: options.filter((option) => option.group === group), })); const remembered = isFileFont(spec.font) && isFontRemembered(spec.font); + const knobName = knobLabels[spec.effectKnob] || `Knob ${spec.effectKnob + 1}`; const onFile = async (event: ChangeEvent) => { const file = event.target.files?.[0]; @@ -398,12 +403,25 @@ function TextEditor({ layer }: { layer: TextLayer }) { )} + {spec.effect !== "none" && knobRange && ( + + )} {spec.effect !== "none" && (

- {`${knobLabels[spec.effectKnob] || `Knob ${spec.effectKnob + 1}`} ${EFFECT_SPEED[spec.effect]}. `} + {`${knobName} ${EFFECT_SPEED[spec.effect]}. `} {speed === 0 - ? "It is all the way down, so nothing moves yet: turn it up in Knobs." + ? "It is all the way down, so nothing moves yet: turn it up here or in Knobs." : `It is at ${speed}% now. All the way down stands the effect still.`}

)}