tauri-plugin-decor 1.0.1

Opinionated window decoration controls for Tauri apps.
Documentation
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>tauri-plugin-decor example</title>
    <style>
      :root {
        --tb-h: 40px;
      }

      html,
      body {
        height: 100%;
        margin: 0;
        font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
        background: #fff;
        color: #111;
      }

      body {
        padding-top: var(--tb-h);
      }

      header.app-titlebar {
        position: fixed;
        top: 0;
        left: 0;
        right: 0;
        height: var(--tb-h);
        background: #f3f4f6;
        border-bottom: 1px solid #e5e7eb;
        display: flex;
        align-items: center;
        padding: 0 86px;
        font-size: 13px;
        color: #374151;
        cursor: grab;
        user-select: none;
        -webkit-user-select: none;
        z-index: 50;
      }

      header.app-titlebar:active {
        cursor: grabbing;
      }

      main {
        padding: 24px;
        max-width: 760px;
        margin: 0 auto;
      }

      h1 {
        margin: 0 0 12px;
        font-size: 22px;
      }

      p,
      li {
        font-size: 15px;
        line-height: 1.5;
      }

      code {
        background: #f3f4f6;
        padding: 1px 6px;
        border-radius: 4px;
      }

      .panel {
        margin-top: 24px;
        padding: 16px;
        border: 1px solid #e5e7eb;
        border-radius: 8px;
        background: #fafafa;
      }

      .panel h2 {
        margin: 0 0 4px;
        font-size: 16px;
      }

      .panel .sub {
        margin: 0 0 12px;
        color: #6b7280;
        font-size: 13px;
      }

      .row {
        display: flex;
        align-items: center;
        gap: 12px;
        margin: 8px 0;
        font-size: 14px;
      }

      .row label {
        min-width: 140px;
      }

      input[type="range"] {
        flex: 1;
      }

      input[type="color"] {
        width: 56px;
        height: 28px;
        padding: 0;
        border: 1px solid #d1d5db;
        border-radius: 4px;
        background: transparent;
      }

      .val {
        min-width: 56px;
        text-align: right;
        font-variant-numeric: tabular-nums;
        color: #6b7280;
      }

      .preset-row {
        display: flex;
        gap: 8px;
        flex-wrap: wrap;
      }

      button.preset {
        padding: 8px 14px;
        border: 1px solid #d1d5db;
        border-radius: 6px;
        background: #fff;
        cursor: pointer;
        font-size: 13px;
        color: #111;
      }

      button.preset:hover {
        background: #f3f4f6;
      }

      button.preset:active {
        background: #e5e7eb;
      }

      .log {
        margin-top: 12px;
        padding: 10px 12px;
        background: #111;
        color: #d1d5db;
        border-radius: 6px;
        font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
        font-size: 12px;
        white-space: pre-wrap;
        min-height: 20px;
      }

      .row.disabled {
        opacity: 0.45;
      }

      .platform-tag {
        display: inline-block;
        font-size: 10px;
        padding: 1px 6px;
        margin-left: 6px;
        border-radius: 999px;
        background: #e5e7eb;
        color: #4b5563;
        text-transform: uppercase;
        letter-spacing: 0.5px;
        vertical-align: middle;
      }

      .platform-tag.mac { background: #fef3c7; color: #92400e; }
      .platform-tag.win { background: #dbeafe; color: #1e40af; }
      .platform-tag.lin { background: #d1fae5; color: #065f46; }
    </style>
  </head>
  <body>
    <header class="app-titlebar" data-tauri-drag-region>
    </header>
    <main>
      <h1>tauri-plugin-decor example</h1>
      <p>
        Drag the gray bar at the top to move the window. The plugin's overlay
        controls live there too. Below: the two supported ways to drive runtime
        updates.
      </p>

      <div class="panel">
        <h2>Way #1 — Frontend command</h2>
        <p class="sub">
          Sliders call <code>invoke("plugin:decor|update_style", { style })</code>.
          The command lands in <code>Decor::reconfigure</code> on the Rust
          side and the new values are pushed back via the internal
          <code>decor://style-changed</code> Tauri event.
        </p>

        <div class="row" data-platforms="mac,win,lin">
          <label for="height">
            Controls height
            <span class="platform-tag mac">mac</span>
            <span class="platform-tag win">win</span>
            <span class="platform-tag lin">lin</span>
          </label>
          <input id="height" type="range" min="20" max="64" step="1" value="24" />
          <span class="val" id="height-v">24</span>
        </div>

        <div class="row" data-platforms="mac,win,lin">
          <label for="scale">
            Controls scale
            <span class="platform-tag mac">mac</span>
            <span class="platform-tag win">win</span>
            <span class="platform-tag lin">lin</span>
          </label>
          <input id="scale" type="range" min="0.7" max="1.6" step="0.05" value="1.0" />
          <span class="val" id="scale-v">1.00</span>
        </div>

        <div class="row" data-platforms="mac">
          <label for="inset">
            Inset X
            <span class="platform-tag mac">mac</span>
          </label>
          <input id="inset" type="range" min="6" max="40" step="1" value="18" />
          <span class="val" id="inset-v">18</span>
        </div>

        <div class="row" data-platforms="mac">
          <label for="spacing">
            Spacing
            <span class="platform-tag mac">mac</span>
          </label>
          <input id="spacing" type="range" min="14" max="40" step="1" value="26" />
          <span class="val" id="spacing-v">26</span>
        </div>

        <div class="row" data-platforms="win,lin">
          <label for="btnw">
            Button width
            <span class="platform-tag win">win</span>
            <span class="platform-tag lin">lin</span>
          </label>
          <input id="btnw" type="range" min="28" max="80" step="1" value="46" />
          <span class="val" id="btnw-v">46</span>
        </div>

        <div class="row" data-platforms="win,lin">
          <label for="closeBg">
            Close hover
            <span class="platform-tag win">win</span>
            <span class="platform-tag lin">lin</span>
          </label>
          <input id="closeBg" type="color" value="#c42b1c" />
        </div>

        <div class="row" data-platforms="win,lin">
          <label for="hoverBg">
            Button hover
            <span class="platform-tag win">win</span>
            <span class="platform-tag lin">lin</span>
          </label>
          <input id="hoverBg" type="color" value="#000000" />
        </div>

        <div class="log" id="log">awaiting first update…</div>
      </div>

      <div class="panel">
        <h2>Way #2 — Rust-side preset</h2>
        <p class="sub">
          These buttons invoke a custom command in <code>main.rs</code> that
          calls <code>app.decor().reconfigure(DecorStyle { … })</code>.
          Same code path inside the plugin as Way #1 — just driven from Rust.
        </p>

        <div class="preset-row">
          <button class="preset" data-preset="compact">Compact</button>
          <button class="preset" data-preset="default">Default</button>
          <button class="preset" data-preset="comfy">Comfy</button>
        </div>
      </div>
    </main>

    <script>
      const { invoke } = window.__TAURI__.core;
      const logEl = document.getElementById("log");

      const platform = (() => {
        const ua = navigator.userAgent;
        if (/Mac OS X|macOS/i.test(ua)) return "mac";
        if (/Windows/i.test(ua)) return "win";
        if (/Linux/i.test(ua)) return "lin";
        return "unknown";
      })();
      document.querySelectorAll(".row[data-platforms]").forEach((row) => {
        const platforms = row.dataset.platforms.split(",");
        if (!platforms.includes(platform)) {
          row.classList.add("disabled");
          row.title = `no-op on ${platform}; supported on: ${platforms.join(", ")}`;
        }
      });

      const setVal = (id, v) => (document.getElementById(`${id}-v`).textContent = v);

      const sendStyle = (style) => {
        logEl.textContent = "" + JSON.stringify(style);
        invoke("plugin:decor|update_style", { style })
          .then(() => {
            logEl.textContent = "" + JSON.stringify(style);
          })
          .catch((err) => {
            logEl.textContent = "" + JSON.stringify(style) + "  " + String(err);
            console.error("[decor] update_style failed:", err);
          });
      };

      document.getElementById("height").addEventListener("input", (e) => {
        const v = Number(e.target.value);
        setVal("height", v);
        sendStyle({ controls_height: v });
      });

      document.getElementById("scale").addEventListener("input", (e) => {
        const v = Number(e.target.value);
        setVal("scale", v.toFixed(2));
        sendStyle({ controls_scale: v });
      });

      document.getElementById("inset").addEventListener("input", (e) => {
        const v = Number(e.target.value);
        setVal("inset", v);
        sendStyle({ controls_inset_x: v });
      });

      document.getElementById("spacing").addEventListener("input", (e) => {
        const v = Number(e.target.value);
        setVal("spacing", v);
        sendStyle({ controls_spacing: v });
      });

      document.getElementById("btnw").addEventListener("input", (e) => {
        const v = Number(e.target.value);
        setVal("btnw", v);
        sendStyle({ controls_button_width: v });
      });

      const hexToRgba = (hex, alpha = 1) => {
        const r = parseInt(hex.slice(1, 3), 16);
        const g = parseInt(hex.slice(3, 5), 16);
        const b = parseInt(hex.slice(5, 7), 16);
        return `rgba(${r},${g},${b},${alpha})`;
      };

      document.getElementById("closeBg").addEventListener("input", (e) => {
        sendStyle({ controls_close_hover_bg: hexToRgba(e.target.value, 1) });
      });

      document.getElementById("hoverBg").addEventListener("input", (e) => {
        sendStyle({ controls_button_hover_bg: hexToRgba(e.target.value, 0.25) });
      });

      const syncFormToPreset = (preset) => {
        const presets = {
          compact: { height: 20, scale: 1.0, btnw: 38 },
          default: { height: 24, scale: 1.0, btnw: 46 },
          comfy: { height: 32, scale: 1.0, btnw: 56 },
        };
        const p = presets[preset] || presets.default;
        const set = (id, v, fmt = (x) => x) => {
          document.getElementById(id).value = v;
          setVal(id, fmt(v));
        };
        set("height", p.height);
        set("scale", p.scale, (x) => x.toFixed(2));
        set("btnw", p.btnw);
      };

      document.querySelectorAll("button.preset").forEach((btn) => {
        btn.addEventListener("click", () => {
          const preset = btn.dataset.preset;
          logEl.textContent = "→ apply_rust_preset(" + preset + ")";
          invoke("apply_rust_preset", { preset })
            .then(() => {
              logEl.textContent = "✓ apply_rust_preset(" + preset + ")";
            })
            .catch((err) => {
              logEl.textContent = "✗ apply_rust_preset(" + preset + ")  " + String(err);
              console.error("[decor] apply_rust_preset failed:", err);
            });
          syncFormToPreset(preset);
        });
      });
    </script>
  </body>
</html>