Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 3 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 7 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,13 @@ name = "niner-standalone"
path = "src/main.rs"

[dependencies]
# Plugin framework — pinned to a specific commit for reproducible builds
nih_plug = { git = "https://github.com/robbert-vdh/nih-plug", rev = "28b149ec4d62757d0b448809148a0c3ca6e09a95", features = ["assert_process_allocs", "standalone"] }
nih_plug_egui = { git = "https://github.com/robbert-vdh/nih-plug", rev = "28b149ec4d62757d0b448809148a0c3ca6e09a95" }
# Plugin framework — pinned to a specific commit for reproducible builds.
# Hornfisk/nih-plug tracks upstream robbert-vdh@28b149e plus a single-line
# change exposing `EguiState::set_requested_size` (private upstream) so the
# UI-scale control can drive a programmatic window resize in-DAW. See
# https://github.com/Hornfisk/nih-plug/tree/niner/pub-set-requested-size
nih_plug = { git = "https://github.com/Hornfisk/nih-plug", rev = "8b51e93a707cb6226721b56825047322e8a8e623", features = ["assert_process_allocs", "standalone"] }
nih_plug_egui = { git = "https://github.com/Hornfisk/nih-plug", rev = "8b51e93a707cb6226721b56825047322e8a8e623" }

# Real-time safety
rtrb = "0.3"
Expand Down
23 changes: 16 additions & 7 deletions src/params.rs
Original file line number Diff line number Diff line change
Expand Up @@ -370,18 +370,27 @@ pub fn collect_kick_params(p: &NinerParams) -> KickParams {
}
}

/// Base editor size in logical pixels at 1x. The UI-scale control multiplies
/// this (see `ui::editor`); the layout always draws into this fixed space and
/// egui's zoom factor renders it larger, so nothing reflows.
pub const BASE_WINDOW_SIZE: (u32, u32) = (680, 444);

impl Default for NinerParams {
fn default() -> Self {
// EguiState size is the LOGICAL window size. Actual on-screen scaling
// is applied by baseview's WindowScalePolicy — for standalone the
// launcher passes `--dpi-scale N` (read from `ui_scale.txt`); DAW
// hosts call `Editor::set_scale_factor`. Multiplying the logical
// size by the scale here would double-scale (window grows, content
// doesn't). Mirrors the squelchbox approach.
// The window opens at the saved UI scale. Baseview's native scale is
// pinned to 1.0 (DAW hosts default to it; the standalone launcher
// passes `--dpi-scale 1.0`), so the editor drives all scaling itself
// via egui's zoom factor plus a matching window resize (see
// `ui::editor`). Opening pre-scaled here avoids a first-frame resize
// flash; the editor keeps the window in sync when the badge changes.
let ui_scale = crate::util::paths::load_ui_scale();
let (bw, bh) = BASE_WINDOW_SIZE;

Self {
editor_state: EguiState::from_size(680, 444),
editor_state: EguiState::from_size(
(bw as f32 * ui_scale).round() as u32,
(bh as f32 * ui_scale).round() as u32,
),

seq_steps: Arc::new(Mutex::new(DEFAULT_STEP_BITS)),
seq_accents: Arc::new(Mutex::new(DEFAULT_ACCENT_BITS)),
Expand Down
41 changes: 28 additions & 13 deletions src/ui/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -282,12 +282,32 @@ pub fn create(
crate::ui::layout_overrides::init(ctx);
},
move |ctx, setter, _state| {
// Scaling is handled outside this callback: baseview applies the
// window scale factor (standalone via `--dpi-scale`, DAW via
// `Editor::set_scale_factor`), and egui's `pixels_per_point`
// follows. We do NOT call `ctx.set_pixels_per_point()` here —
// that fights baseview and double-scales the layout.
let _ = &editor_state_clone;
// UI scale (1.0 / 1.5 / 2.0). We drive both egui's content zoom
// and a matching programmatic window resize from a single value,
// so the scale badge works identically in a DAW and standalone.
//
// Baseview's native scale is pinned to 1.0 (DAW hosts default to
// it; the standalone launcher passes `--dpi-scale 1.0`), so the
// effective pixels-per-point is exactly `scale`: the host window
// grows to BASE × scale physical pixels while the layout keeps
// drawing into a fixed BASE logical space. Uniform scale, no
// reflow — knob rows never move relative to each other.
//
// `set_zoom_factor` is cheap and idempotent per frame; the resize
// request only fires when the stored size actually differs, so a
// settled scale costs one comparison.
{
let scale = (*params.ui_scale.lock()).clamp(1.0, 2.0);
ctx.set_zoom_factor(scale);
let (bw, bh) = crate::params::BASE_WINDOW_SIZE;
let want = (
(bw as f32 * scale).round() as u32,
(bh as f32 * scale).round() as u32,
);
if editor_state_clone.size() != want {
editor_state_clone.set_requested_size(want);
}
}

// Invalidate texture caches when the egui::Context changes. The
// `Arc<Mutex<Option<TextureHandle>>>`s captured above outlive any
Expand Down Expand Up @@ -1059,10 +1079,7 @@ pub fn create(
let ui_resp = ui
.interact(hit, egui::Id::new("ui_scale_btn"), egui::Sense::click())
.on_hover_cursor(egui::CursorIcon::PointingHand)
.on_hover_text(
"UI scale — click to cycle (1x / 1.5x / 2x).\n\
Reopen the plugin to apply.",
);
.on_hover_text("UI scale — click to cycle (1x / 1.5x / 2x).");
let color = if ui_resp.hovered() {
theme::WHITE
} else {
Expand All @@ -1084,9 +1101,7 @@ pub fn create(
};
*lock = next;
crate::util::paths::save_ui_scale(next);
tracing::info!(
"[ui_scale] cycled → {next}x (saved; reopen plugin to apply)"
);
tracing::info!("[ui_scale] cycled → {next}x (applied live)");
}
}
}
Expand Down
18 changes: 8 additions & 10 deletions tools/niner-launch.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
# niner-launch — desktop-launcher shim for the standalone.
#
# What it does:
# 1. Reads the persisted UI scale from `$XDG_DATA_HOME/niner/ui_scale.txt`
# (the in-GUI scale badge writes it; the standalone only honours the
# `--dpi-scale` CLI flag) and forwards it.
# 1. Pins baseview's native scale to 1.0 (`--dpi-scale 1.0`). The plugin
# drives UI scaling itself (egui zoom factor + a matching window resize),
# reading the saved factor from `$XDG_DATA_HOME/niner/ui_scale.txt` at
# startup. The launcher must NOT also scale or the two would compound.
# 2. Picks an audio backend, sets up MIDI auto-routing, and execs
# niner-standalone. Default is `--backend alsa` because:
# * PipeWire's ALSA bridge auto-routes niner's audio to the default
Expand Down Expand Up @@ -45,9 +46,6 @@
set -u

BIN_DIR="$(cd "$(dirname "$0")" && pwd)"
SCALE_FILE="${XDG_DATA_HOME:-$HOME/.local/share}/niner/ui_scale.txt"
SCALE=$(tr -d ' \n' < "$SCALE_FILE" 2>/dev/null || true)
SCALE="${SCALE:-1.0}"

STANDALONE="$BIN_DIR/niner-standalone"
WRAPPER="$BIN_DIR/nih-standalone-wrapper"
Expand Down Expand Up @@ -106,9 +104,9 @@ if [ "${NINER_FORCE_BACKEND:-alsa}" != "jack" ] && command -v aconnect >/dev/nul
PERIOD="${NINER_PERIOD_SIZE:-${PW_QUANTUM:-512}}"
ALSA_ARGS=(--backend alsa --midi-input "$MIDI_INPUT" --sample-rate "$PW_RATE" --period-size "$PERIOD")
if [ -x "$WRAPPER" ]; then
exec "$WRAPPER" "$STANDALONE" --dpi-scale "$SCALE" "${ALSA_ARGS[@]}" "$@"
exec "$WRAPPER" "$STANDALONE" --dpi-scale 1.0 "${ALSA_ARGS[@]}" "$@"
fi
exec "$STANDALONE" --dpi-scale "$SCALE" "${ALSA_ARGS[@]}" "$@"
exec "$STANDALONE" --dpi-scale 1.0 "${ALSA_ARGS[@]}" "$@"
fi
# No BeatStep found — fall through to JACK below (works without MIDI).
fi
Expand Down Expand Up @@ -139,6 +137,6 @@ if command -v pw-link >/dev/null 2>&1; then
fi

if [ -x "$WRAPPER" ]; then
exec "$WRAPPER" "$STANDALONE" --dpi-scale "$SCALE" --backend jack "$@"
exec "$WRAPPER" "$STANDALONE" --dpi-scale 1.0 --backend jack "$@"
fi
exec "$STANDALONE" --dpi-scale "$SCALE" --backend jack "$@"
exec "$STANDALONE" --dpi-scale 1.0 --backend jack "$@"
Loading