From e22d2ccee82b829875fe343cf1be48975174b054 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Thu, 15 Oct 2026 11:24:49 +0200 Subject: refactor(island): the controls panel goes Each of its seven tiles opened a drawer page that already existed, and it went unused. SUPER alone will open the drawer instead. Removed with it: the status pill's controls icon, Island.toggleControls, the island IPC target, Controls.js and its test, and the controls list in Prefs. Prefs now holds only the pinned IP; a controls key left in desktop.json is still kept on write, because writes merge into the parsed document. Co-Authored-By: Claude Opus 5.5 --- desktop/island/ControlTile.qml | 143 ------------------------------- desktop/island/Controls.js | 107 ----------------------- desktop/island/ControlsPanel.qml | 179 --------------------------------------- desktop/island/Island.qml | 13 +-- desktop/island/README.md | 32 ++----- desktop/island/StatusPill.qml | 39 +-------- desktop/island/test_controls.js | 98 --------------------- 7 files changed, 13 insertions(+), 598 deletions(-) delete mode 100644 desktop/island/ControlTile.qml delete mode 100644 desktop/island/Controls.js delete mode 100644 desktop/island/ControlsPanel.qml delete mode 100644 desktop/island/test_controls.js (limited to 'desktop/island') diff --git a/desktop/island/ControlTile.qml b/desktop/island/ControlTile.qml deleted file mode 100644 index 873a42e..0000000 --- a/desktop/island/ControlTile.qml +++ /dev/null @@ -1,143 +0,0 @@ -// Copyright (C) 2026 Danilo M. -// -// This program is free software; you can redistribute it and/or modify -// it under the terms of the GNU General Public License version 2 as -// published by the Free Software Foundation. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -import Quickshell -import Quickshell.Widgets -import Quickshell.Networking -import Quickshell.Bluetooth -import QtQuick -import ".." -import "Controls.js" as Controls -import "Logic.js" as Logic - -// One control: icon over label. Left-click toggles, right-click opens the -// drawer page that holds the rest, scroll changes volume on the two audio -// tiles. Toggle tiles fill when on; audio tiles fill to their volume. -Rectangle { - id: tile - - required property string tileId - // The sound module's Service. - required property QtObject sound - - signal openPage(string page) - - readonly property var info: Controls.tile(tileId) - readonly property bool isAudio: tileId === "output" || tileId === "mic" - readonly property var audio: tileId === "output" ? (sound.sink?.audio ?? null) - : tileId === "mic" ? (sound.source?.audio ?? null) - : null - readonly property var adapter: Bluetooth.defaultAdapter - // Networking starts empty for about two seconds (AGENTS.md). - readonly property bool wifiReady: - (Networking.devices?.values ?? []).some(d => d.type === DeviceType.Wifi) - - readonly property bool ready: isAudio ? audio !== null - : tileId === "bluetooth" ? adapter !== null - : tileId === "wifi" ? wifiReady - : true - - readonly property bool on: { - switch (tileId) { - case "wifi": return Networking.wifiEnabled; - case "bluetooth": return adapter?.enabled ?? false; - case "dnd": return Status.dnd; - // Lit while a game holds it too; a click then clears only the manual - // half (Status.qml), so the tile stays lit until the game ends. - case "presentation": return Status.presentation; - case "nolock": return !Status.nolock; - default: return audio ? !audio.muted : false; - } - } - - function toggle() { - switch (tileId) { - case "wifi": Networking.wifiEnabled = !Networking.wifiEnabled; break; - case "bluetooth": adapter.enabled = !adapter.enabled; break; - case "dnd": - case "presentation": - case "nolock": Status.toggleMode(tileId); break; - default: audio.muted = !audio.muted; - } - } - - radius: 12 - color: Qt.alpha(Theme.surface, 0.5) - border.width: 1 - border.color: Qt.alpha(Theme.text, 0.08) - opacity: ready ? 1 : 0.4 - clip: true - - // Toggle tiles: lit when on. - Rectangle { - anchors.fill: parent - radius: parent.radius - visible: !tile.isAudio && tile.on - color: Qt.alpha(Theme.accent, 0.35) - } - - // Audio tiles: a bar to the volume. Muted keeps the width in a neutral - // colour, so the level stays readable. Not the accent as the neutral: - // udt can snap it onto a palette colour (AGENTS.md). - Rectangle { - visible: tile.isAudio - anchors { left: parent.left; top: parent.top; bottom: parent.bottom } - width: parent.width * Controls.fill(tile.audio?.volume) - radius: parent.radius - color: tile.audio?.muted ? Qt.alpha(Theme.overlay, 0.6) : Qt.alpha(Theme.accent, 0.35) - } - - Column { - anchors.centerIn: parent - width: parent.width - 8 - spacing: 4 - - IconImage { - anchors.horizontalCenter: parent.horizontalCenter - implicitSize: 22 - source: Quickshell.iconPath(tile.tileId === "output" - ? Logic.volumeIcon(tile.audio?.volume ?? 0, tile.audio?.muted ?? false) - : Controls.icon(tile.tileId, tile.on), true) - } - - Text { - width: parent.width - horizontalAlignment: Text.AlignHCenter - elide: Text.ElideRight - text: tile.info?.label ?? tile.tileId - font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } - color: Theme.text - } - } - - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.LeftButton | Qt.RightButton - cursorShape: Qt.PointingHandCursor - onClicked: mouse => { - if (mouse.button === Qt.RightButton) tile.openPage(tile.info.page); - else if (tile.ready) tile.toggle(); - } - // One step per wheel notch (120), as the layout flag does. A - // MouseArea, not a WheelHandler: the handler never fired in the - // island (AGENTS.md). - property real acc: 0 - onWheel: wheel => { - wheel.accepted = true; - if (!tile.isAudio || !tile.ready) return; - acc += wheel.angleDelta.y; - if (Math.abs(acc) < 120) return; - const max = tile.tileId === "output" ? 1.5 : 1.0; - tile.audio.volume = Controls.step(tile.audio.volume, acc > 0 ? 0.05 : -0.05, max); - acc = 0; - } - } -} diff --git a/desktop/island/Controls.js b/desktop/island/Controls.js deleted file mode 100644 index 0ca2f6c..0000000 --- a/desktop/island/Controls.js +++ /dev/null @@ -1,107 +0,0 @@ -.pragma library - -// Copyright (C) 2026 Danilo M. -// -// This program is free software; you can redistribute it and/or modify -// it under the terms of the GNU General Public License version 2 as -// published by the Free Software Foundation. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// The controls panel's tiles and the pure operations on their order. The -// order itself lives in Prefs.qml; everything here takes a list and returns -// a new one, so it runs under node (test_controls.js). - -// Table order is the default order and the order of the hidden strip. -const TILES = { - wifi: { label: "Wi-Fi", page: "network" }, - bluetooth: { label: "Bluetooth", page: "bluetooth" }, - dnd: { label: "DND", page: "status" }, - presentation: { label: "Presentation", page: "status" }, - nolock: { label: "Auto-lock", page: "status" }, - output: { label: "Output", page: "sound" }, - mic: { label: "Mic", page: "sound" }, -}; - -function defaults() { - return Object.keys(TILES); -} - -function tile(id) { - return TILES[id] || null; -} - -// Icon name for a tile's state. `on` is the tile's lit state: for the mic -// that is "not muted". Output is "" because Logic.volumeIcon owns it. -function icon(id, on) { - switch (id) { - case "wifi": return on ? "network-wireless" : "network-wireless-disconnected"; - case "bluetooth": return "preferences-system-bluetooth"; - case "dnd": return on ? "notifications-disabled" : "notifications"; - case "presentation": return "video-display"; - case "nolock": return "system-lock-screen"; - case "mic": return on ? "audio-input-microphone" : "microphone-sensitivity-muted"; - default: return ""; - } -} - -// A stored list made safe: unknown ids (a tile removed from the code) and -// duplicates dropped. An empty list is a valid choice; a non-list is not. -function normalize(list) { - if (!Array.isArray(list)) return defaults(); - const out = []; - for (let i = 0; i < list.length; i++) { - const id = list[i]; - if (TILES[id] && out.indexOf(id) < 0) out.push(id); - } - return out; -} - -function hidden(list) { - return defaults().filter(id => list.indexOf(id) < 0); -} - -function move(list, from, to) { - const out = list.slice(); - const id = out.splice(from, 1)[0]; - out.splice(to, 0, id); - return out; -} - -function hide(list, id) { - return list.filter(x => x !== id); -} - -function show(list, id) { - if (!TILES[id] || list.indexOf(id) >= 0) return list.slice(); - return list.concat([id]); -} - -// Audio tile bar width as a fraction. Output goes to 1.5, the bar stops at -// 1. !(> 0) also catches a volume not populated yet. -function fill(volume) { - if (!(volume > 0)) return 0; - return Math.min(1, volume); -} - -// One scroll step, rounded to the percent so repeated steps do not drift. -// The cap only stops a step up: a level already above it (set elsewhere, -// e.g. a mic at 120% from pavucontrol) is left alone by up and stepped from -// by down, never snapped to the cap. -function step(volume, delta, max) { - const cur = volume > 0 ? volume : 0; - const v = Math.round((cur + delta) * 100) / 100; - if (delta < 0) return Math.max(0, v); - return Math.max(cur, Math.min(max, v)); -} - -// The slot a dragged tile lands in, from its centre in grid coordinates. -// Off the grid clamps to the nearest edge; past the last tile is the last. -function slotAt(x, y, pitch, cols, count) { - const col = Math.max(0, Math.min(cols - 1, Math.floor(x / pitch))); - const row = Math.max(0, Math.floor(y / pitch)); - return Math.max(0, Math.min(count - 1, row * cols + col)); -} diff --git a/desktop/island/ControlsPanel.qml b/desktop/island/ControlsPanel.qml deleted file mode 100644 index a7872d7..0000000 --- a/desktop/island/ControlsPanel.qml +++ /dev/null @@ -1,179 +0,0 @@ -// Copyright (C) 2026 Danilo M. -// -// This program is free software; you can redistribute it and/or modify -// it under the terms of the GNU General Public License version 2 as -// published by the Free Software Foundation. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -import Quickshell -import Quickshell.Widgets -import QtQuick -import ".." -import "Controls.js" as Controls - -// The grid of controls and its edit mode. In edit mode a tile is dragged to -// a new slot, a × hides it, and hidden tiles wait in a strip below with a +. -// Every change is written to Prefs at once. -Column { - id: panel - - required property QtObject sound - property bool editing: false - - signal openDrawer(string page) - - spacing: 12 - - Item { - width: panel.width - height: 20 - - Text { - anchors.verticalCenter: parent.verticalCenter - text: panel.editing ? "Edit controls" : "Controls" - font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2; bold: true } - color: Theme.text - } - - IconImage { - anchors { right: parent.right; verticalCenter: parent.verticalCenter } - implicitSize: 16 - opacity: panel.editing ? 1 : 0.6 - source: Quickshell.iconPath("document-edit", true) - - MouseArea { - anchors.fill: parent - anchors.margins: -4 - cursorShape: Qt.PointingHandCursor - onClicked: panel.editing = !panel.editing - } - } - } - - Item { - id: grid - - readonly property int cols: 4 - readonly property real gap: 8 - readonly property real cell: (width - gap * (cols - 1)) / cols - readonly property real pitch: cell + gap - readonly property int rows: Math.ceil(Prefs.controls.length / cols) - - width: panel.width - height: Math.max(0, rows * pitch - gap) - - Repeater { - model: Prefs.controls - - delegate: Item { - id: slot - required property string modelData - required property int index - - width: grid.cell - height: grid.cell - x: (index % grid.cols) * grid.pitch - y: Math.floor(index / grid.cols) * grid.pitch - z: mover.pressed ? 1 : 0 - - ControlTile { - width: parent.width - height: parent.height - x: mover.pressed ? mover.dx : 0 - y: mover.pressed ? mover.dy : 0 - tileId: slot.modelData - sound: panel.sound - onOpenPage: page => panel.openDrawer(page) - } - - // Edit mode only, and above the tile so it takes the press - // instead of the toggle. Offsets the tile while held; on - // release the tile lands in the slot under its centre. - MouseArea { - id: mover - anchors.fill: parent - enabled: panel.editing - preventStealing: true - // Both buttons and the wheel, so in edit mode nothing - // reaches the tile: no drawer page, no volume change. - acceptedButtons: Qt.LeftButton | Qt.RightButton - onWheel: wheel => wheel.accepted = true - cursorShape: pressed ? Qt.ClosedHandCursor : Qt.OpenHandCursor - property real sx: 0 - property real sy: 0 - property real dx: 0 - property real dy: 0 - onPressed: mouse => { sx = mouse.x; sy = mouse.y; dx = 0; dy = 0; } - onPositionChanged: mouse => { dx = mouse.x - sx; dy = mouse.y - sy; } - onReleased: { - const to = Controls.slotAt(slot.x + dx + width / 2, slot.y + dy + height / 2, - grid.pitch, grid.cols, Prefs.controls.length); - if (to !== slot.index) - Prefs.setControls(Controls.move(Prefs.controls, slot.index, to)); - } - } - - IconImage { - visible: panel.editing && !mover.pressed - anchors { top: parent.top; right: parent.right; margins: 3 } - implicitSize: 14 - source: Quickshell.iconPath("window-close", true) - - MouseArea { - anchors.fill: parent - anchors.margins: -4 - cursorShape: Qt.PointingHandCursor - onClicked: Prefs.setControls(Controls.hide(Prefs.controls, slot.modelData)) - } - } - } - } - } - - Flow { - width: panel.width - spacing: 6 - visible: panel.editing && Controls.hidden(Prefs.controls).length > 0 - - Repeater { - model: Controls.hidden(Prefs.controls) - - delegate: Rectangle { - id: chip - required property string modelData - width: chipRow.implicitWidth + 16 - height: 28 - radius: 14 - color: Qt.alpha(Theme.surface, 0.5) - - Row { - id: chipRow - anchors.centerIn: parent - spacing: 4 - - IconImage { - anchors.verticalCenter: parent.verticalCenter - implicitSize: 14 - source: Quickshell.iconPath("list-add", true) - } - Text { - anchors.verticalCenter: parent.verticalCenter - text: Controls.tile(chip.modelData).label - font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } - color: Theme.text - } - } - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: Prefs.setControls(Controls.show(Prefs.controls, chip.modelData)) - } - } - } - } -} diff --git a/desktop/island/Island.qml b/desktop/island/Island.qml index d0d0a49..1807c82 100644 --- a/desktop/island/Island.qml +++ b/desktop/island/Island.qml @@ -38,7 +38,7 @@ PanelWindow { WlrLayershell.namespace: "quickshell-island" WlrLayershell.keyboardFocus: panel !== "" ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None - // "", "sound", "controls", "perf" or "calendar". + // "", "sound", "perf" or "calendar". property string panel: "" // --- reveal and hide ------------------------------------------------- @@ -64,15 +64,6 @@ PanelWindow { else if (!hovered) hideTimer.restart(); } - // The keybind's way in: reveal, and open or close the controls panel. - // Closing goes through onPanelChanged, which starts the hide timer when - // the pointer is elsewhere. - function toggleControls() { - hideTimer.stop(); - shown = true; - panel = panel === "controls" ? "" : "controls"; - } - Timer { id: hideTimer interval: 400 @@ -221,8 +212,6 @@ PanelWindow { onMenuShown: win.menuHeld = true onOpenSound: win.panel = win.panel === "sound" ? "" : "sound" onOpenDrawer: { win.panel = ""; win.drawer.show("sound"); } - onOpenControls: win.toggleControls() onOpenPerf: win.panel = win.panel === "perf" ? "" : "perf" - onOpenPage: page => { win.panel = ""; win.drawer.show(page); } } } diff --git a/desktop/island/README.md b/desktop/island/README.md index c4cf35f..4ad1050 100644 --- a/desktop/island/README.md +++ b/desktop/island/README.md @@ -46,12 +46,10 @@ is open or not. Click opens the calendar panel. ## Status pill -Left to right: performance icon, controls icon, volume icon, tray, layout -flag. +Left to right: performance icon, volume icon, tray, layout flag. - **Performance icon** opens the performance panel, see below. A dot on its corner warns when something needs attention. -- **Controls icon** opens the controls panel, see below. - **Volume icon** shows the default sink (muted, low, medium, high). Click opens the sound panel. - **Tray** shows `SystemTray` items. Left click activates, right click opens @@ -62,27 +60,13 @@ flag. `activelayout` event. Scroll switches layout with `hyprctl switchxkblayout current next|prev`. -## Controls panel - -Opened by the status pill's controls icon, or by SUPER pressed and released -alone (`qs -p desktop ipc call island controls`, a Hyprland release bind; -any key or mouse bind pressed in between cancels it). Seven tiles: wifi, -bluetooth, DND, presentation, auto-lock, output, mic. - -- Click toggles. Right-click closes the panel and opens the tile's drawer - page. -- Output and mic fill to their volume, clamped at 100%; muted greys the bar - instead of emptying it. Scroll steps 5%, output capped at 150%. -- A tile whose backing object has not arrived (the first two seconds, no - adapter, no sink) is dimmed and ignores input. -- The pencil enters edit mode: drag a tile to another slot, × hides it, the - strip below lists hidden tiles with a + each. Closing the panel ends it. - -The visible tiles and their order live in -`$XDG_STATE_HOME/quickshell/desktop.json` (`../Prefs.qml`), written only when -they change and keeping keys it does not know. A tile added to the code -later starts hidden. Pure helpers in `Controls.js`, tested with -`node island/test_controls.js`. +## Controls panel (removed) + +There was one, opened by SUPER alone. Each of its seven tiles opened a drawer +page that already existed and it went unused, so it was removed and SUPER +alone opens the drawer instead. `Prefs.qml` keeps only the pinned IP; a +`controls` key left in `desktop.json` by the old panel is kept on write and +otherwise ignored. ## Performance panel diff --git a/desktop/island/StatusPill.qml b/desktop/island/StatusPill.qml index d62b87f..f064f98 100644 --- a/desktop/island/StatusPill.qml +++ b/desktop/island/StatusPill.qml @@ -17,8 +17,8 @@ import ".." import "perf" import "Logic.js" as Logic -// Performance icon, controls icon, volume icon, tray, layout flag. The -// performance, controls and volume icons open their panels in this capsule; +// Performance icon, volume icon, tray, layout flag. The performance and +// volume icons open their panels in this capsule; // the tray is a plain tray; the flag switches layout on scroll and ignores // clicks. Capsule { @@ -30,28 +30,21 @@ Capsule { property string panel: "" signal openSound() - signal openControls() signal openPerf() signal openDrawer() - // A drawer page, from a control tile's right-click. - signal openPage(string page) // Emitted just before a tray menu opens; the menu cannot report its close. signal menuShown() - expanded: panel === "sound" || panel === "controls" || panel === "perf" + expanded: panel === "sound" || panel === "perf" restWidth: restRow.implicitWidth + 24 panelWidth: panel === "perf" ? 600 : 320 - panelHeight: (panel === "controls" ? controlsPanel.implicitHeight - : panel === "perf" ? (perfPanel.item ? perfPanel.item.implicitHeight : 0) + panelHeight: (panel === "perf" ? (perfPanel.item ? perfPanel.item.implicitHeight : 0) : soundPanel.implicitHeight) + 28 // The detailed sampling (per-core, network, VRAM) runs only while the // panel shows it. Binding { target: Perf; property: "detailed"; value: pill.panel === "perf" } - // Edit mode never outlives its panel. - onPanelChanged: controlsPanel.editing = false - Row { id: restRow anchors.centerIn: parent @@ -90,18 +83,6 @@ Capsule { } } - IconImage { - anchors.verticalCenter: parent.verticalCenter - implicitSize: 18 - source: Quickshell.iconPath("preferences-system", true) - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: pill.openControls() - } - } - IconImage { anchors.verticalCenter: parent.verticalCenter implicitSize: 18 @@ -179,18 +160,6 @@ Capsule { onOpenDrawer: pill.openDrawer() } - ControlsPanel { - id: controlsPanel - x: 14 - y: 14 - width: pill.panelWidth - 28 - sound: pill.sound - opacity: pill.panel === "controls" ? 1 : 0 - visible: opacity > 0 - Behavior on opacity { NumberAnimation { duration: 160 } } - onOpenDrawer: page => pill.openPage(page) - } - // Loaded only while open: its models are fresh arrays every sample, and // a hidden instance would rebuild its rows every 2s for the whole session. Loader { diff --git a/desktop/island/test_controls.js b/desktop/island/test_controls.js deleted file mode 100644 index b2db873..0000000 --- a/desktop/island/test_controls.js +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright (C) 2026 Danilo M. -// -// This program is free software; you can redistribute it and/or modify -// it under the terms of the GNU General Public License version 2 as -// published by the Free Software Foundation. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// Run: node desktop/island/test_controls.js -// Controls.js is a QML library, so its .pragma line is stripped before node -// evaluates it, as test_logic.js does for Logic.js. - -const fs = require("fs"); -const assert = require("assert"); - -const src = fs.readFileSync(__dirname + "/Controls.js", "utf8") - .replace(/^\.pragma library$/m, ""); -const C = new Function(src + - "\nreturn { defaults, tile, icon, normalize, hidden, move, hide, show, fill, step, slotAt };")(); - -const ALL = ["wifi", "bluetooth", "dnd", "presentation", "nolock", "output", "mic"]; - -// Defaults: all seven, a fresh copy each time. -assert.deepStrictEqual(C.defaults(), ALL); -const d = C.defaults(); d.pop(); -assert.deepStrictEqual(C.defaults(), ALL); - -// Table: label and drawer page; unknown is null. -assert.strictEqual(C.tile("wifi").page, "network"); -assert.strictEqual(C.tile("nolock").label, "Auto-lock"); -assert.strictEqual(C.tile("mic").page, "sound"); -assert.strictEqual(C.tile("nope"), null); - -// Icons by state. Output is Logic.volumeIcon's job, so "" here. -assert.strictEqual(C.icon("wifi", true), "network-wireless"); -assert.strictEqual(C.icon("wifi", false), "network-wireless-disconnected"); -assert.strictEqual(C.icon("dnd", true), "notifications-disabled"); -assert.strictEqual(C.icon("dnd", false), "notifications"); -assert.strictEqual(C.icon("mic", true), "audio-input-microphone"); -assert.strictEqual(C.icon("mic", false), "microphone-sensitivity-muted"); -assert.strictEqual(C.icon("bluetooth", false), "preferences-system-bluetooth"); -assert.strictEqual(C.icon("output", true), ""); - -// normalize: drops unknown ids and duplicates, keeps order; an empty list -// is valid (everything hidden); a non-array is the default. -assert.deepStrictEqual(C.normalize(["mic", "gone", "wifi", "mic"]), ["mic", "wifi"]); -assert.deepStrictEqual(C.normalize([]), []); -assert.deepStrictEqual(C.normalize("wifi"), ALL); -assert.deepStrictEqual(C.normalize(undefined), ALL); - -// hidden: known ids not in the list, in table order. -assert.deepStrictEqual(C.hidden(["wifi", "mic"]), ["bluetooth", "dnd", "presentation", "nolock", "output"]); -assert.deepStrictEqual(C.hidden(ALL), []); - -// move: from index to index, input untouched. -const src3 = ["a", "b", "c"]; -assert.deepStrictEqual(C.move(src3, 0, 2), ["b", "c", "a"]); -assert.deepStrictEqual(C.move(src3, 2, 0), ["c", "a", "b"]); -assert.deepStrictEqual(C.move(src3, 1, 1), ["a", "b", "c"]); -assert.deepStrictEqual(src3, ["a", "b", "c"]); - -// hide / show. -assert.deepStrictEqual(C.hide(["wifi", "mic"], "wifi"), ["mic"]); -assert.deepStrictEqual(C.show(["mic"], "wifi"), ["mic", "wifi"]); -assert.deepStrictEqual(C.show(["mic"], "mic"), ["mic"]); -assert.deepStrictEqual(C.show(["mic"], "nope"), ["mic"]); - -// fill: volume clamped to 0..1; unpopulated reads as empty, not full. -assert.strictEqual(C.fill(0.4), 0.4); -assert.strictEqual(C.fill(1.5), 1); -assert.strictEqual(C.fill(-1), 0); -assert.strictEqual(C.fill(undefined), 0); -assert.strictEqual(C.fill(NaN), 0); - -// step: 5% steps, rounded to the percent, clamped to 0..max. -assert.strictEqual(C.step(0.5, 0.05, 1.5), 0.55); -assert.strictEqual(C.step(0.02, -0.05, 1.5), 0); -assert.strictEqual(C.step(1.48, 0.05, 1.5), 1.5); -assert.strictEqual(C.step(0.98, 0.05, 1.0), 1); -assert.strictEqual(C.step(0.1 + 0.2, 0.05, 1), 0.35); -assert.strictEqual(C.step(undefined, 0.05, 1), 0.05); -// Above the cap (set elsewhere): up leaves it, down steps from where it is. -assert.strictEqual(C.step(1.2, 0.05, 1.0), 1.2); -assert.strictEqual(C.step(1.2, -0.05, 1.0), 1.15); - -// slotAt: pitch 70, 4 columns, 7 tiles. -assert.strictEqual(C.slotAt(35, 35, 70, 4, 7), 0); -assert.strictEqual(C.slotAt(175, 35, 70, 4, 7), 2); -assert.strictEqual(C.slotAt(105, 105, 70, 4, 7), 5); -assert.strictEqual(C.slotAt(-50, -50, 70, 4, 7), 0); // off the top left -assert.strictEqual(C.slotAt(500, 35, 70, 4, 7), 3); // past the right edge -assert.strictEqual(C.slotAt(275, 105, 70, 4, 7), 6); // empty 8th slot -> last -assert.strictEqual(C.slotAt(35, 900, 70, 4, 7), 6); // below the grid -> last - -console.log("test_controls: ok"); -- cgit v1.2.3