Download
147 lines 3.8 KiBJavaScriptModified 2026-08-01 raw
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
// Pop-down slider panel, used by the volume and brightness widgets.
// Popup mechanics (positioning, dismissal, X11 shape) live in popup.js;
// this module supplies the slider body and change-throttling.

import { createPopup } from "./popup.js";

const CSS = `
  .dpopup {
    width: 260px;
    padding: 16px 18px;
    display: flex;
    align-items: center;
    gap: 14px;
  }
  .dpopup input[type="range"] {
    flex: 1;
    -webkit-appearance: none;
    appearance: none;
    height: 6px;
    border-radius: 3px;
    background: rgba(255, 255, 255, 0.18);
    outline: none;
  }
  .dpopup input[type="range"]::-webkit-slider-thumb {
    -webkit-appearance: none;
    appearance: none;
    width: 20px;
    height: 20px;
    border-radius: 50%;
    background: var(--accent);
    border: none;
    cursor: pointer;
  }
  .dpopup .dpopup-value {
    font-family: var(--font-mono);
    font-size: 17px;
    width: 4ch;
    text-align: right;
    color: var(--bar-fg);
    font-variant-numeric: tabular-nums;
  }
  .dpopup .dpopup-lead {
    display: flex;
    align-items: center;
    justify-content: center;
    border: none;
    background: transparent;
    color: var(--bar-fg);
    cursor: pointer;
    padding: 4px;
    border-radius: 8px;
  }
  .dpopup .dpopup-lead:hover { background: var(--chip-bg-hover); }
  .dpopup .dpopup-lead svg {
    display: block;
    width: calc(var(--icon-size) * 0.95);
    height: calc(var(--icon-size) * 0.95);
  }
`;

const THROTTLE_MS = 80;

let stylesInjected = false;

// createLeading (optional): called on each open; returns an element placed
// before the slider (e.g. a mute toggle button).
export function createSliderPopup({ anchor, onChange, createLeading }) {
  if (!stylesInjected) {
    stylesInjected = true;
    const style = document.createElement("style");
    style.dataset.lib = "slider-popup";
    style.textContent = CSS;
    document.head.appendChild(style);
  }

  let sliderEl = null;
  let valueEl = null;
  let lastValue = 0;
  let dragging = false;

  // Throttle slider changes so dragging doesn't flood the backend.
  let throttleTimer = null;
  let queuedValue = null;
  function emit(value) {
    if (throttleTimer) {
      queuedValue = value;
      return;
    }
    onChange(value);
    throttleTimer = setTimeout(() => {
      throttleTimer = null;
      if (queuedValue !== null) {
        const v = queuedValue;
        queuedValue = null;
        emit(v);
      }
    }, THROTTLE_MS);
  }

  const popup = createPopup({
    anchor,
    className: "dpopup",
    leaveDelay: 700,
    holdOpen: () => dragging,
    render(el) {
      dragging = false;
      sliderEl = document.createElement("input");
      sliderEl.type = "range";
      sliderEl.min = "0";
      sliderEl.max = "100";
      sliderEl.step = "1";
      sliderEl.value = lastValue;
      valueEl = document.createElement("span");
      valueEl.className = "dpopup-value";
      valueEl.textContent = `${lastValue}%`;
      if (createLeading) {
        const lead = createLeading();
        lead.classList.add("dpopup-lead");
        el.append(lead);
      }
      el.append(sliderEl, valueEl);

      sliderEl.addEventListener("pointerdown", () => (dragging = true));
      sliderEl.addEventListener("pointerup", () => (dragging = false));
      sliderEl.addEventListener("input", () => {
        const value = Number(sliderEl.value);
        valueEl.textContent = `${value}%`;
        emit(value);
      });
    },
  });

  return {
    open: popup.open,
    close: popup.close,
    toggle: popup.toggle,
    // Keeps the slider in sync with external changes (media keys etc.)
    // without fighting an in-progress drag.
    setValue(value) {
      lastValue = value;
      if (popup.isOpen() && !dragging) {
        sliderEl.value = value;
        valueEl.textContent = `${value}%`;
      }
    },
  };
}