From 2f4f611379d1dc611192380340b5e1f4dc935b67 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 24 May 2026 08:35:30 +0000 Subject: [PATCH] style: apply clang-format to all C++ sources Run clang-format -i against yip-app and vst-host using the existing .clang-format config (LLVM-based, C++20, 110-col, 4-space indent). All 17 files now pass --dry-run --Werror clean. https://claude.ai/code/session_015WKywfjPwRt1v9gGXqZN8o --- yip-app/App.xaml.cpp | 71 +- yip-app/App.xaml.h | 20 +- yip-app/AudioCoreInterop.cpp | 146 ++-- yip-app/AudioCoreInterop.h | 51 +- yip-app/IndicatorPersistence.cpp | 157 ++-- yip-app/IndicatorPersistence.h | 30 +- yip-app/IndicatorState.h | 58 +- yip-app/IndicatorWindow.xaml.cpp | 1286 +++++++++++++++--------------- yip-app/IndicatorWindow.xaml.h | 197 +++-- yip-app/MainWindow.xaml.cpp | 219 +++-- yip-app/MainWindow.xaml.h | 72 +- yip-app/Markers.cpp | 100 ++- yip-app/Markers.h | 24 +- yip-app/Settings.cpp | 191 +++-- yip-app/Settings.h | 48 +- yip-app/SettingsDialog.xaml.cpp | 237 +++--- yip-app/main.cpp | 29 +- 17 files changed, 1462 insertions(+), 1474 deletions(-) diff --git a/yip-app/App.xaml.cpp b/yip-app/App.xaml.cpp index 2ea7480..4cb02cf 100644 --- a/yip-app/App.xaml.cpp +++ b/yip-app/App.xaml.cpp @@ -11,51 +11,48 @@ #include namespace winrt { - using namespace winrt::Microsoft::UI::Xaml; - using namespace winrt::Microsoft::UI::Xaml::Controls; - using namespace winrt::Microsoft::UI::Composition::SystemBackdrops; -} +using namespace winrt::Microsoft::UI::Xaml; +using namespace winrt::Microsoft::UI::Xaml::Controls; +using namespace winrt::Microsoft::UI::Composition::SystemBackdrops; +} // namespace winrt -namespace winrt::yip::implementation +namespace winrt::yip::implementation { +App::App() { - App::App() - { - InitializeComponent(); + InitializeComponent(); - UnhandledException([](IInspectable const&, winrt::Microsoft::UI::Xaml::UnhandledExceptionEventArgs const& e) { + UnhandledException( + [](IInspectable const&, winrt::Microsoft::UI::Xaml::UnhandledExceptionEventArgs const& e) { const auto msg = e.Message(); ::OutputDebugStringW(L"Yip unhandled: "); ::OutputDebugStringW(msg.c_str()); ::OutputDebugStringW(L"\n"); }); - } +} - void App::OnLaunched(winrt::Microsoft::UI::Xaml::LaunchActivatedEventArgs const& /*args*/) - { - m_window = winrt::make(); - m_window.Title(L"Yip"); - - // Mica backdrop — Window owns the controller lifetime since 1.4. - if (winrt::Microsoft::UI::Composition::SystemBackdrops::MicaController::IsSupported()) - { - m_window.SystemBackdrop(winrt::Microsoft::UI::Xaml::Media::MicaBackdrop{}); - } - else - { - // Older hardware → acrylic fallback. - m_window.SystemBackdrop(winrt::Microsoft::UI::Xaml::Media::DesktopAcrylicBackdrop{}); - } - - m_window.Activate(); - - // Floating indicator pill — always-on-top tool window. Owns its own - // poll loop against the audio-core FFI; survives without MainWindow - // being focused. - // - // Per current design the pill is *invisible* in Idle + Armed states, - // so we do NOT call Activate() here — visibility is driven entirely - // by AppWindow.Show()/Hide() in TransitionTo. Window construction is - // enough to wire composition + the dispatcher queue. - m_indicator = winrt::make(); +void App::OnLaunched(winrt::Microsoft::UI::Xaml::LaunchActivatedEventArgs const& /*args*/) +{ + m_window = winrt::make(); + m_window.Title(L"Yip"); + + // Mica backdrop — Window owns the controller lifetime since 1.4. + if (winrt::Microsoft::UI::Composition::SystemBackdrops::MicaController::IsSupported()) { + m_window.SystemBackdrop(winrt::Microsoft::UI::Xaml::Media::MicaBackdrop{}); + } else { + // Older hardware → acrylic fallback. + m_window.SystemBackdrop(winrt::Microsoft::UI::Xaml::Media::DesktopAcrylicBackdrop{}); } + + m_window.Activate(); + + // Floating indicator pill — always-on-top tool window. Owns its own + // poll loop against the audio-core FFI; survives without MainWindow + // being focused. + // + // Per current design the pill is *invisible* in Idle + Armed states, + // so we do NOT call Activate() here — visibility is driven entirely + // by AppWindow.Show()/Hide() in TransitionTo. Window construction is + // enough to wire composition + the dispatcher queue. + m_indicator = winrt::make(); } +} // namespace winrt::yip::implementation diff --git a/yip-app/App.xaml.h b/yip-app/App.xaml.h index 741daf3..75c6827 100644 --- a/yip-app/App.xaml.h +++ b/yip-app/App.xaml.h @@ -2,16 +2,14 @@ #include "App.xaml.g.h" -namespace winrt::yip::implementation -{ - struct App : AppT - { - App(); +namespace winrt::yip::implementation { +struct App : AppT { + App(); - void OnLaunched(winrt::Microsoft::UI::Xaml::LaunchActivatedEventArgs const& args); + void OnLaunched(winrt::Microsoft::UI::Xaml::LaunchActivatedEventArgs const& args); - private: - winrt::Microsoft::UI::Xaml::Window m_window{ nullptr }; - winrt::Microsoft::UI::Xaml::Window m_indicator{ nullptr }; - }; -} +private: + winrt::Microsoft::UI::Xaml::Window m_window{nullptr}; + winrt::Microsoft::UI::Xaml::Window m_indicator{nullptr}; +}; +} // namespace winrt::yip::implementation diff --git a/yip-app/AudioCoreInterop.cpp b/yip-app/AudioCoreInterop.cpp index 28ad3ff..861f409 100644 --- a/yip-app/AudioCoreInterop.cpp +++ b/yip-app/AudioCoreInterop.cpp @@ -5,91 +5,103 @@ #include #include +using Microsoft::WRL::ClassicCom; using Microsoft::WRL::ComPtr; using Microsoft::WRL::RuntimeClass; using Microsoft::WRL::RuntimeClassFlags; -using Microsoft::WRL::ClassicCom; -namespace yip::interop +namespace yip::interop { +std::vector ListDevices() { - std::vector ListDevices() - { - std::vector out; - DeviceInfo* arr = nullptr; - size_t len = 0; - const auto status = rec_list_devices(&arr, &len); - if (status != REC_STATUS_OK || !arr) { - return out; - } - out.reserve(len); - for (size_t i = 0; i < len; ++i) { - Device d; - d.id = arr[i].id ? arr[i].id : ""; - d.name = arr[i].name ? arr[i].name : ""; - d.isCapture = arr[i].kind != 0; - d.isDefault = arr[i].is_default != 0; - out.push_back(std::move(d)); - } - rec_free_devices(arr, len); + std::vector out; + DeviceInfo* arr = nullptr; + size_t len = 0; + const auto status = rec_list_devices(&arr, &len); + if (status != REC_STATUS_OK || !arr) { return out; } + out.reserve(len); + for (size_t i = 0; i < len; ++i) { + Device d; + d.id = arr[i].id ? arr[i].id : ""; + d.name = arr[i].name ? arr[i].name : ""; + d.isCapture = arr[i].kind != 0; + d.isDefault = arr[i].is_default != 0; + out.push_back(std::move(d)); + } + rec_free_devices(arr, len); + return out; +} - // ---- DeviceWatcher implementation ---- +// ---- DeviceWatcher implementation ---- - namespace { +namespace { - class NotificationClient - : public RuntimeClass, IMMNotificationClient> - { - public: - explicit NotificationClient(std::function cb) : m_cb(std::move(cb)) {} +class NotificationClient : public RuntimeClass, IMMNotificationClient> { +public: + explicit NotificationClient(std::function cb) : m_cb(std::move(cb)) {} - IFACEMETHODIMP OnDeviceStateChanged(LPCWSTR, DWORD) override { fire(); return S_OK; } - IFACEMETHODIMP OnDeviceAdded(LPCWSTR) override { fire(); return S_OK; } - IFACEMETHODIMP OnDeviceRemoved(LPCWSTR) override { fire(); return S_OK; } - IFACEMETHODIMP OnDefaultDeviceChanged(EDataFlow, ERole, LPCWSTR) override { fire(); return S_OK; } - IFACEMETHODIMP OnPropertyValueChanged(LPCWSTR, const PROPERTYKEY) override { return S_OK; } + IFACEMETHODIMP OnDeviceStateChanged(LPCWSTR, DWORD) override + { + fire(); + return S_OK; + } + IFACEMETHODIMP OnDeviceAdded(LPCWSTR) override + { + fire(); + return S_OK; + } + IFACEMETHODIMP OnDeviceRemoved(LPCWSTR) override + { + fire(); + return S_OK; + } + IFACEMETHODIMP OnDefaultDeviceChanged(EDataFlow, ERole, LPCWSTR) override + { + fire(); + return S_OK; + } + IFACEMETHODIMP OnPropertyValueChanged(LPCWSTR, const PROPERTYKEY) override { return S_OK; } - private: - void fire() { - // Debounce: only forward if a callback isn't already in flight. - if (!m_inflight.exchange(true)) { - if (m_cb) m_cb(); - m_inflight.store(false); - } +private: + void fire() + { + // Debounce: only forward if a callback isn't already in flight. + if (!m_inflight.exchange(true)) { + if (m_cb) m_cb(); + m_inflight.store(false); } + } - std::function m_cb; - std::atomic m_inflight{ false }; - }; + std::function m_cb; + std::atomic m_inflight{false}; +}; - } // namespace +} // namespace - struct DeviceWatcher::Impl - { - ComPtr enumerator; - ComPtr client; - }; +struct DeviceWatcher::Impl { + ComPtr enumerator; + ComPtr client; +}; - DeviceWatcher::DeviceWatcher(std::function onChanged) - : m_impl(std::make_unique()) - { - HRESULT hr = ::CoInitializeEx(nullptr, COINIT_MULTITHREADED); - if (hr != S_OK && hr != S_FALSE && hr != RPC_E_CHANGED_MODE) { - return; - } - hr = ::CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL, - IID_PPV_ARGS(m_impl->enumerator.GetAddressOf())); - if (FAILED(hr)) return; - m_impl->client = Microsoft::WRL::Make(std::move(onChanged)); - if (!m_impl->client) return; - m_impl->enumerator->RegisterEndpointNotificationCallback(m_impl->client.Get()); +DeviceWatcher::DeviceWatcher(std::function onChanged) : m_impl(std::make_unique()) +{ + HRESULT hr = ::CoInitializeEx(nullptr, COINIT_MULTITHREADED); + if (hr != S_OK && hr != S_FALSE && hr != RPC_E_CHANGED_MODE) { + return; } + hr = ::CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL, + IID_PPV_ARGS(m_impl->enumerator.GetAddressOf())); + if (FAILED(hr)) return; + m_impl->client = Microsoft::WRL::Make(std::move(onChanged)); + if (!m_impl->client) return; + m_impl->enumerator->RegisterEndpointNotificationCallback(m_impl->client.Get()); +} - DeviceWatcher::~DeviceWatcher() - { - if (m_impl && m_impl->enumerator && m_impl->client) { - m_impl->enumerator->UnregisterEndpointNotificationCallback(m_impl->client.Get()); - } +DeviceWatcher::~DeviceWatcher() +{ + if (m_impl && m_impl->enumerator && m_impl->client) { + m_impl->enumerator->UnregisterEndpointNotificationCallback(m_impl->client.Get()); } } +} // namespace yip::interop diff --git a/yip-app/AudioCoreInterop.h b/yip-app/AudioCoreInterop.h index e5171ba..abecdbb 100644 --- a/yip-app/AudioCoreInterop.h +++ b/yip-app/AudioCoreInterop.h @@ -10,33 +10,30 @@ #include "audio_core.h" -namespace yip::interop -{ - struct Device - { - std::string id; // utf-8 - std::string name; // utf-8 - bool isCapture{ true }; - bool isDefault{ false }; - }; +namespace yip::interop { +struct Device { + std::string id; // utf-8 + std::string name; // utf-8 + bool isCapture{true}; + bool isDefault{false}; +}; - // Enumerate endpoints. On error returns an empty vector and silently - // swallows — callers can use `rec_last_error()` to read the message. - std::vector ListDevices(); +// Enumerate endpoints. On error returns an empty vector and silently +// swallows — callers can use `rec_last_error()` to read the message. +std::vector ListDevices(); - // RAII watcher: registers an `IMMNotificationClient` against the system - // device enumerator. `onChanged` is invoked from a WASAPI worker thread - // — callers must marshal to the UI dispatcher themselves. - class DeviceWatcher - { - public: - explicit DeviceWatcher(std::function onChanged); - ~DeviceWatcher(); - DeviceWatcher(const DeviceWatcher&) = delete; - DeviceWatcher& operator=(const DeviceWatcher&) = delete; +// RAII watcher: registers an `IMMNotificationClient` against the system +// device enumerator. `onChanged` is invoked from a WASAPI worker thread +// — callers must marshal to the UI dispatcher themselves. +class DeviceWatcher { +public: + explicit DeviceWatcher(std::function onChanged); + ~DeviceWatcher(); + DeviceWatcher(const DeviceWatcher&) = delete; + DeviceWatcher& operator=(const DeviceWatcher&) = delete; - private: - struct Impl; - std::unique_ptr m_impl; - }; -} +private: + struct Impl; + std::unique_ptr m_impl; +}; +} // namespace yip::interop diff --git a/yip-app/IndicatorPersistence.cpp b/yip-app/IndicatorPersistence.cpp index ce7a72b..137718b 100644 --- a/yip-app/IndicatorPersistence.cpp +++ b/yip-app/IndicatorPersistence.cpp @@ -6,101 +6,98 @@ #include #include -namespace fs = std::filesystem; +namespace fs = std::filesystem; namespace wdj = winrt::Windows::Data::Json; -namespace +namespace { +fs::path LocalAppDataRoot() { - fs::path LocalAppDataRoot() - { - wchar_t* base = nullptr; - if (SUCCEEDED(::SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, nullptr, &base)) && base) { - fs::path p(base); - ::CoTaskMemFree(base); - return p / L"Yip"; - } - return fs::current_path() / L"yip-data"; + wchar_t* base = nullptr; + if (SUCCEEDED(::SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, nullptr, &base)) && base) { + fs::path p(base); + ::CoTaskMemFree(base); + return p / L"Yip"; } + return fs::current_path() / L"yip-data"; +} - yip::DockEdge ParseEdge(uint32_t v) - { - switch (v) { - case 1: return yip::DockEdge::Top; - case 2: return yip::DockEdge::Bottom; - case 3: return yip::DockEdge::Left; - case 4: return yip::DockEdge::Right; - default: return yip::DockEdge::None; - } +yip::DockEdge ParseEdge(uint32_t v) +{ + switch (v) { + case 1: + return yip::DockEdge::Top; + case 2: + return yip::DockEdge::Bottom; + case 3: + return yip::DockEdge::Left; + case 4: + return yip::DockEdge::Right; + default: + return yip::DockEdge::None; } } +} // namespace -namespace yip +namespace yip { +fs::path IndicatorPersistence::FilePath() { - fs::path IndicatorPersistence::FilePath() - { - const auto root = LocalAppDataRoot(); - std::error_code ec; - fs::create_directories(root, ec); - return root / L"indicator.json"; - } + const auto root = LocalAppDataRoot(); + std::error_code ec; + fs::create_directories(root, ec); + return root / L"indicator.json"; +} - IndicatorPersistence IndicatorPersistence::Load() - { - IndicatorPersistence s; - std::ifstream in(FilePath(), std::ios::binary); - if (!in) return s; +IndicatorPersistence IndicatorPersistence::Load() +{ + IndicatorPersistence s; + std::ifstream in(FilePath(), std::ios::binary); + if (!in) return s; - std::stringstream buf; - buf << in.rdbuf(); - const auto u8 = buf.str(); - if (u8.empty()) return s; + std::stringstream buf; + buf << in.rdbuf(); + const auto u8 = buf.str(); + if (u8.empty()) return s; - const auto wide = winrt::to_hstring(u8); - wdj::JsonObject obj{ nullptr }; - if (!wdj::JsonObject::TryParse(wide, obj)) return s; + const auto wide = winrt::to_hstring(u8); + wdj::JsonObject obj{nullptr}; + if (!wdj::JsonObject::TryParse(wide, obj)) return s; - if (obj.HasKey(L"dock_edge")) { - s.dock_edge = ParseEdge(static_cast(obj.GetNamedNumber(L"dock_edge", 1.0))); - } - if (obj.HasKey(L"monitor_id")) { - s.monitor_id = static_cast(obj.GetNamedNumber(L"monitor_id", 0.0)); - } - if (obj.HasKey(L"edge_offset")) { - s.edge_offset = obj.GetNamedNumber(L"edge_offset", 0.5); - } - if (obj.HasKey(L"click_through")) { - s.click_through = obj.GetNamedBoolean(L"click_through", false); - } - if (obj.HasKey(L"last_expanded")) { - s.last_expanded = obj.GetNamedBoolean(L"last_expanded", false); - } - return s; + if (obj.HasKey(L"dock_edge")) { + s.dock_edge = ParseEdge(static_cast(obj.GetNamedNumber(L"dock_edge", 1.0))); } + if (obj.HasKey(L"monitor_id")) { + s.monitor_id = static_cast(obj.GetNamedNumber(L"monitor_id", 0.0)); + } + if (obj.HasKey(L"edge_offset")) { + s.edge_offset = obj.GetNamedNumber(L"edge_offset", 0.5); + } + if (obj.HasKey(L"click_through")) { + s.click_through = obj.GetNamedBoolean(L"click_through", false); + } + if (obj.HasKey(L"last_expanded")) { + s.last_expanded = obj.GetNamedBoolean(L"last_expanded", false); + } + return s; +} - bool IndicatorPersistence::Save() const - { - wdj::JsonObject obj; - obj.SetNamedValue(L"dock_edge", - wdj::JsonValue::CreateNumberValue(static_cast(dock_edge))); - obj.SetNamedValue(L"monitor_id", - wdj::JsonValue::CreateNumberValue(static_cast(monitor_id))); - obj.SetNamedValue(L"edge_offset", - wdj::JsonValue::CreateNumberValue(edge_offset)); - obj.SetNamedValue(L"click_through", - wdj::JsonValue::CreateBooleanValue(click_through)); - obj.SetNamedValue(L"last_expanded", - wdj::JsonValue::CreateBooleanValue(last_expanded)); +bool IndicatorPersistence::Save() const +{ + wdj::JsonObject obj; + obj.SetNamedValue(L"dock_edge", wdj::JsonValue::CreateNumberValue(static_cast(dock_edge))); + obj.SetNamedValue(L"monitor_id", wdj::JsonValue::CreateNumberValue(static_cast(monitor_id))); + obj.SetNamedValue(L"edge_offset", wdj::JsonValue::CreateNumberValue(edge_offset)); + obj.SetNamedValue(L"click_through", wdj::JsonValue::CreateBooleanValue(click_through)); + obj.SetNamedValue(L"last_expanded", wdj::JsonValue::CreateBooleanValue(last_expanded)); - const auto path = FilePath(); - const auto tmp = path.wstring() + L".tmp"; - const auto u8 = winrt::to_string(obj.Stringify()); - { - std::ofstream out(tmp, std::ios::binary | std::ios::trunc); - if (!out) return false; - out.write(u8.data(), static_cast(u8.size())); - if (!out) return false; - } - return ::MoveFileExW(tmp.c_str(), path.c_str(), - MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) != 0; + const auto path = FilePath(); + const auto tmp = path.wstring() + L".tmp"; + const auto u8 = winrt::to_string(obj.Stringify()); + { + std::ofstream out(tmp, std::ios::binary | std::ios::trunc); + if (!out) return false; + out.write(u8.data(), static_cast(u8.size())); + if (!out) return false; } + return ::MoveFileExW(tmp.c_str(), path.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) != 0; } +} // namespace yip diff --git a/yip-app/IndicatorPersistence.h b/yip-app/IndicatorPersistence.h index 0873d1f..5a9bdf7 100644 --- a/yip-app/IndicatorPersistence.h +++ b/yip-app/IndicatorPersistence.h @@ -10,20 +10,18 @@ #include #include -namespace yip -{ - struct IndicatorPersistence - { - DockEdge dock_edge{ DockEdge::Top }; - // Stable monitor key from Microsoft.UI.Windowing.DisplayId.Value. - uint64_t monitor_id{ 0 }; - // Position within the docked edge (0.0 = top/left, 1.0 = bottom/right). - double edge_offset{ 0.5 }; - bool click_through{ false }; - bool last_expanded{ false }; +namespace yip { +struct IndicatorPersistence { + DockEdge dock_edge{DockEdge::Top}; + // Stable monitor key from Microsoft.UI.Windowing.DisplayId.Value. + uint64_t monitor_id{0}; + // Position within the docked edge (0.0 = top/left, 1.0 = bottom/right). + double edge_offset{0.5}; + bool click_through{false}; + bool last_expanded{false}; - static std::filesystem::path FilePath(); - static IndicatorPersistence Load(); - bool Save() const; - }; -} + static std::filesystem::path FilePath(); + static IndicatorPersistence Load(); + bool Save() const; +}; +} // namespace yip diff --git a/yip-app/IndicatorState.h b/yip-app/IndicatorState.h index 5894fa1..82330bd 100644 --- a/yip-app/IndicatorState.h +++ b/yip-app/IndicatorState.h @@ -7,35 +7,37 @@ #include -namespace yip -{ - enum class IndicatorState : uint8_t - { - Idle = 0, // collapsed, dim 60% - Armed = 1, // hotkey ready, not yet recording (full opacity) - Recording = 2, // active capture: timer + GPU level meter - Saving = 3, // brief processing animation while writer flushes - Expanded = 4, // user-revealed controls (overlays any base state) - }; +namespace yip { +enum class IndicatorState : uint8_t { + Idle = 0, // collapsed, dim 60% + Armed = 1, // hotkey ready, not yet recording (full opacity) + Recording = 2, // active capture: timer + GPU level meter + Saving = 3, // brief processing animation while writer flushes + Expanded = 4, // user-revealed controls (overlays any base state) +}; - enum class DockEdge : uint8_t - { - None = 0, - Top = 1, - Bottom = 2, - Left = 3, - Right = 4, - }; +enum class DockEdge : uint8_t { + None = 0, + Top = 1, + Bottom = 2, + Left = 3, + Right = 4, +}; - inline const wchar_t* StateName(IndicatorState s) noexcept - { - switch (s) { - case IndicatorState::Idle: return L"idle"; - case IndicatorState::Armed: return L"armed"; - case IndicatorState::Recording: return L"recording"; - case IndicatorState::Saving: return L"saving"; - case IndicatorState::Expanded: return L"expanded"; - } - return L"?"; +inline const wchar_t* StateName(IndicatorState s) noexcept +{ + switch (s) { + case IndicatorState::Idle: + return L"idle"; + case IndicatorState::Armed: + return L"armed"; + case IndicatorState::Recording: + return L"recording"; + case IndicatorState::Saving: + return L"saving"; + case IndicatorState::Expanded: + return L"expanded"; } + return L"?"; } +} // namespace yip diff --git a/yip-app/IndicatorWindow.xaml.cpp b/yip-app/IndicatorWindow.xaml.cpp index 36c00c2..2e0a533 100644 --- a/yip-app/IndicatorWindow.xaml.cpp +++ b/yip-app/IndicatorWindow.xaml.cpp @@ -24,744 +24,746 @@ using namespace std::chrono_literals; -namespace mux = winrt::Microsoft::UI::Xaml; +namespace mux = winrt::Microsoft::UI::Xaml; namespace muxc = winrt::Microsoft::UI::Xaml::Controls; namespace muxh = winrt::Microsoft::UI::Xaml::Hosting; namespace muxi = winrt::Microsoft::UI::Xaml::Input; namespace mucomp = winrt::Microsoft::UI::Composition; -namespace muw = winrt::Microsoft::UI::Windowing; +namespace muw = winrt::Microsoft::UI::Windowing; namespace muxd = winrt::Microsoft::UI::Dispatching; -namespace +namespace { +// Token defaults — duplicated as fallback for code that runs before +// Application resources are queryable. App.xaml is the source of truth. +constexpr int kWindowW = 320; +constexpr int kWindowH = 56; +constexpr float kCorner = 22.0f; +constexpr int kFadeMs = 180; +constexpr int kResizeMs = 220; +constexpr int kMeterMs = 33; +constexpr int kSnapPx = 20; +constexpr int kAutoCollapseMs = 3000; +constexpr int kPollMs = 200; // SyncFromAudioCore cadence +constexpr int kBarCount = 4; +constexpr float kBarWidth = 3.0f; +constexpr float kBarGap = 4.0f; +constexpr float kBarMaxHeight = 18.0f; +constexpr float kSavingDebounceMs = 350.0f; + +struct StateGeom { + float w; + float h; + float opacity; +}; + +StateGeom GeometryFor(::yip::IndicatorState s) noexcept { - // Token defaults — duplicated as fallback for code that runs before - // Application resources are queryable. App.xaml is the source of truth. - constexpr int kWindowW = 320; - constexpr int kWindowH = 56; - constexpr float kCorner = 22.0f; - constexpr int kFadeMs = 180; - constexpr int kResizeMs = 220; - constexpr int kMeterMs = 33; - constexpr int kSnapPx = 20; - constexpr int kAutoCollapseMs = 3000; - constexpr int kPollMs = 200; // SyncFromAudioCore cadence - constexpr int kBarCount = 4; - constexpr float kBarWidth = 3.0f; - constexpr float kBarGap = 4.0f; - constexpr float kBarMaxHeight = 18.0f; - constexpr float kSavingDebounceMs = 350.0f; - - struct StateGeom { - float w; - float h; - float opacity; - }; - - StateGeom GeometryFor(::yip::IndicatorState s) noexcept - { - using S = ::yip::IndicatorState; - switch (s) { - case S::Idle: return { 92.0f, 28.0f, 0.60f }; - case S::Armed: return { 132.0f, 36.0f, 1.00f }; - case S::Recording: return { 188.0f, 44.0f, 1.00f }; - case S::Saving: return { 188.0f, 44.0f, 0.85f }; - case S::Expanded: return { 320.0f, 56.0f, 1.00f }; - } - return { 92.0f, 28.0f, 0.60f }; - } - - // CubicBezier(0.4, 0.0, 0.2, 1.0) — Fluent standard easing. - mucomp::CompositionEasingFunction StandardEase(mucomp::Compositor const& c) - { - winrt::Windows::Foundation::Numerics::float2 cp1{ 0.4f, 0.0f }; - winrt::Windows::Foundation::Numerics::float2 cp2{ 0.2f, 1.0f }; - return c.CreateCubicBezierEasingFunction(cp1, cp2); - } - - winrt::Windows::UI::Color FromHex(uint32_t argb) noexcept - { - return { - static_cast((argb >> 24) & 0xff), - static_cast((argb >> 16) & 0xff), - static_cast((argb >> 8) & 0xff), - static_cast(argb & 0xff), - }; + using S = ::yip::IndicatorState; + switch (s) { + case S::Idle: + return {92.0f, 28.0f, 0.60f}; + case S::Armed: + return {132.0f, 36.0f, 1.00f}; + case S::Recording: + return {188.0f, 44.0f, 1.00f}; + case S::Saving: + return {188.0f, 44.0f, 0.85f}; + case S::Expanded: + return {320.0f, 56.0f, 1.00f}; } + return {92.0f, 28.0f, 0.60f}; } -namespace winrt::yip::implementation +// CubicBezier(0.4, 0.0, 0.2, 1.0) — Fluent standard easing. +mucomp::CompositionEasingFunction StandardEase(mucomp::Compositor const& c) { - IndicatorWindow::IndicatorWindow() - { - InitializeComponent(); + winrt::Windows::Foundation::Numerics::float2 cp1{0.4f, 0.0f}; + winrt::Windows::Foundation::Numerics::float2 cp2{0.2f, 1.0f}; + return c.CreateCubicBezierEasingFunction(cp1, cp2); +} - m_persisted = ::yip::IndicatorPersistence::Load(); +winrt::Windows::UI::Color FromHex(uint32_t argb) noexcept +{ + return { + static_cast((argb >> 24) & 0xff), + static_cast((argb >> 16) & 0xff), + static_cast((argb >> 8) & 0xff), + static_cast(argb & 0xff), + }; +} +} // namespace - // Grab the HWND. Required for tool-window style + click-through flip. - if (auto native = try_as<::IWindowNative>()) { - native->get_WindowHandle(&m_hwnd); - } +namespace winrt::yip::implementation { +IndicatorWindow::IndicatorWindow() +{ + InitializeComponent(); - ApplyToolWindowStyle(); - ApplyAlwaysOnTop(); - - // Round HWND silhouette to the maximal pill outline. The Composition - // clip below animates the *visible* shape within this silhouette - // without further region updates. - if (m_hwnd) { - HRGN rgn = ::CreateRoundRectRgn( - 0, 0, kWindowW + 1, kWindowH + 1, - static_cast(kCorner * 2), static_cast(kCorner * 2)); - ::SetWindowRgn(m_hwnd, rgn, TRUE); // Windows takes ownership of rgn. - } + m_persisted = ::yip::IndicatorPersistence::Load(); - BuildCompositionLayer(); - ApplyClickThrough(m_persisted.click_through); - RestoreFromPersistence(); + // Grab the HWND. Required for tool-window style + click-through flip. + if (auto native = try_as<::IWindowNative>()) { + native->get_WindowHandle(&m_hwnd); + } - // Idle + Armed are *invisible* by design (per user). Pill only - // materialises on Recording / Saving / Expanded. - // - Idle → AppWindow.Hide() - // - Armed → AppWindow.Hide() (M7 hotkey arming may revisit) - // - Recording / Saving / Expanded → AppWindow.Show() - TransitionTo(::yip::IndicatorState::Idle, /*animate*/ false); - HideWindow(); + ApplyToolWindowStyle(); + ApplyAlwaysOnTop(); - // Periodic sync to audio-core (5Hz — low power). - auto dq = muxd::DispatcherQueue::GetForCurrentThread(); - m_pollTimer = dq.CreateTimer(); - m_pollTimer.Interval(std::chrono::milliseconds(kPollMs)); - m_pollTimer.IsRepeating(true); - m_pollTimer.Tick([weak = get_weak()](auto&&, auto&&) { - if (auto self = weak.get()) self->SyncFromAudioCore(); - }); - m_pollTimer.Start(); - - // Meter polling timer — created stopped, started only while Recording. - m_meterTimer = dq.CreateTimer(); - m_meterTimer.Interval(std::chrono::milliseconds(kMeterMs)); - m_meterTimer.IsRepeating(true); - m_meterTimer.Tick([weak = get_weak()](auto&&, auto&&) { - if (auto self = weak.get()) self->UpdateMeterBars(::rec_peak_level()); - }); + // Round HWND silhouette to the maximal pill outline. The Composition + // clip below animates the *visible* shape within this silhouette + // without further region updates. + if (m_hwnd) { + HRGN rgn = ::CreateRoundRectRgn(0, 0, kWindowW + 1, kWindowH + 1, static_cast(kCorner * 2), + static_cast(kCorner * 2)); + ::SetWindowRgn(m_hwnd, rgn, TRUE); // Windows takes ownership of rgn. + } - m_collapseTimer = dq.CreateTimer(); - m_collapseTimer.Interval(std::chrono::milliseconds(kAutoCollapseMs)); - m_collapseTimer.IsRepeating(false); - m_collapseTimer.Tick([weak = get_weak()](auto&&, auto&&) { - if (auto self = weak.get()) { - if (self->m_state == ::yip::IndicatorState::Expanded) { - self->TransitionTo(self->m_baseState, true); - } + BuildCompositionLayer(); + ApplyClickThrough(m_persisted.click_through); + RestoreFromPersistence(); + + // Idle + Armed are *invisible* by design (per user). Pill only + // materialises on Recording / Saving / Expanded. + // - Idle → AppWindow.Hide() + // - Armed → AppWindow.Hide() (M7 hotkey arming may revisit) + // - Recording / Saving / Expanded → AppWindow.Show() + TransitionTo(::yip::IndicatorState::Idle, /*animate*/ false); + HideWindow(); + + // Periodic sync to audio-core (5Hz — low power). + auto dq = muxd::DispatcherQueue::GetForCurrentThread(); + m_pollTimer = dq.CreateTimer(); + m_pollTimer.Interval(std::chrono::milliseconds(kPollMs)); + m_pollTimer.IsRepeating(true); + m_pollTimer.Tick([weak = get_weak()](auto&&, auto&&) { + if (auto self = weak.get()) self->SyncFromAudioCore(); + }); + m_pollTimer.Start(); + + // Meter polling timer — created stopped, started only while Recording. + m_meterTimer = dq.CreateTimer(); + m_meterTimer.Interval(std::chrono::milliseconds(kMeterMs)); + m_meterTimer.IsRepeating(true); + m_meterTimer.Tick([weak = get_weak()](auto&&, auto&&) { + if (auto self = weak.get()) self->UpdateMeterBars(::rec_peak_level()); + }); + + m_collapseTimer = dq.CreateTimer(); + m_collapseTimer.Interval(std::chrono::milliseconds(kAutoCollapseMs)); + m_collapseTimer.IsRepeating(false); + m_collapseTimer.Tick([weak = get_weak()](auto&&, auto&&) { + if (auto self = weak.get()) { + if (self->m_state == ::yip::IndicatorState::Expanded) { + self->TransitionTo(self->m_baseState, true); } - }); - } + } + }); +} - IndicatorWindow::~IndicatorWindow() - { - if (m_pollTimer) m_pollTimer.Stop(); - if (m_meterTimer) m_meterTimer.Stop(); - if (m_collapseTimer) m_collapseTimer.Stop(); - m_persisted.last_expanded = (m_state == ::yip::IndicatorState::Expanded); - (void)m_persisted.Save(); - } +IndicatorWindow::~IndicatorWindow() +{ + if (m_pollTimer) m_pollTimer.Stop(); + if (m_meterTimer) m_meterTimer.Stop(); + if (m_collapseTimer) m_collapseTimer.Stop(); + m_persisted.last_expanded = (m_state == ::yip::IndicatorState::Expanded); + (void)m_persisted.Save(); +} - // ============================================================ HWND style +// ============================================================ HWND style - void IndicatorWindow::ApplyToolWindowStyle() - { - if (!m_hwnd) return; +void IndicatorWindow::ApplyToolWindowStyle() +{ + if (!m_hwnd) return; - LONG_PTR ex = ::GetWindowLongPtrW(m_hwnd, GWL_EXSTYLE); - ex |= WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE | WS_EX_LAYERED; - ex &= ~WS_EX_APPWINDOW; - ::SetWindowLongPtrW(m_hwnd, GWL_EXSTYLE, ex); + LONG_PTR ex = ::GetWindowLongPtrW(m_hwnd, GWL_EXSTYLE); + ex |= WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE | WS_EX_LAYERED; + ex &= ~WS_EX_APPWINDOW; + ::SetWindowLongPtrW(m_hwnd, GWL_EXSTYLE, ex); - // WS_EX_LAYERED with no LWA_COLORKEY/LWA_ALPHA = fully opaque inside - // window region, fully clipped outside it. - ::SetLayeredWindowAttributes(m_hwnd, 0, 255, LWA_ALPHA); - } + // WS_EX_LAYERED with no LWA_COLORKEY/LWA_ALPHA = fully opaque inside + // window region, fully clipped outside it. + ::SetLayeredWindowAttributes(m_hwnd, 0, 255, LWA_ALPHA); +} - void IndicatorWindow::ApplyAlwaysOnTop() - { - if (!m_hwnd) return; - auto wid = winrt::Microsoft::UI::GetWindowIdFromWindow(m_hwnd); - auto appWindow = muw::AppWindow::GetFromWindowId(wid); - if (auto presenter = appWindow.Presenter().try_as()) { - presenter.SetBorderAndTitleBar(false, false); - presenter.IsResizable(false); - presenter.IsMaximizable(false); - presenter.IsMinimizable(false); - presenter.IsAlwaysOnTop(true); - } - appWindow.Resize({ kWindowW, kWindowH }); +void IndicatorWindow::ApplyAlwaysOnTop() +{ + if (!m_hwnd) return; + auto wid = winrt::Microsoft::UI::GetWindowIdFromWindow(m_hwnd); + auto appWindow = muw::AppWindow::GetFromWindowId(wid); + if (auto presenter = appWindow.Presenter().try_as()) { + presenter.SetBorderAndTitleBar(false, false); + presenter.IsResizable(false); + presenter.IsMaximizable(false); + presenter.IsMinimizable(false); + presenter.IsAlwaysOnTop(true); } + appWindow.Resize({kWindowW, kWindowH}); +} - void IndicatorWindow::ApplyClickThrough(bool enable) - { - if (!m_hwnd) return; - LONG_PTR ex = ::GetWindowLongPtrW(m_hwnd, GWL_EXSTYLE); - if (enable) ex |= WS_EX_TRANSPARENT; - else ex &= ~WS_EX_TRANSPARENT; - ::SetWindowLongPtrW(m_hwnd, GWL_EXSTYLE, ex); - m_persisted.click_through = enable; - } +void IndicatorWindow::ApplyClickThrough(bool enable) +{ + if (!m_hwnd) return; + LONG_PTR ex = ::GetWindowLongPtrW(m_hwnd, GWL_EXSTYLE); + if (enable) + ex |= WS_EX_TRANSPARENT; + else + ex &= ~WS_EX_TRANSPARENT; + ::SetWindowLongPtrW(m_hwnd, GWL_EXSTYLE, ex); + m_persisted.click_through = enable; +} - // ============================================================ Composition - - void IndicatorWindow::BuildCompositionLayer() - { - auto pillVisual = muxh::ElementCompositionPreview::GetElementVisual(PillFrame()); - m_compositor = pillVisual.Compositor(); - m_ease = StandardEase(m_compositor); - - // Composition geometric clip on the Border's visual. Animating this - // geometry's Size + Offset is what conveys state changes — no XAML - // layout passes, no UI-thread wakes during the tween. - m_clipGeo = m_compositor.CreateRoundedRectangleGeometry(); - m_clipGeo.CornerRadius({ kCorner, kCorner }); - m_clipGeo.Size({ kWindowW, kWindowH }); - m_clipGeo.Offset({ 0.0f, 0.0f }); - auto clip = m_compositor.CreateGeometricClip(m_clipGeo); - pillVisual.Clip(clip); - - // Child visual tree for dot + meter bars. Parented to the meter host - // (anchored within the pill grid by XAML layout so it stays inside - // the visible clip). - auto meterContainer = m_compositor.CreateContainerVisual(); - meterContainer.Size({ kBarCount * (kBarWidth + kBarGap) - kBarGap, kBarMaxHeight + 4 }); - muxh::ElementCompositionPreview::SetElementChildVisual(MeterHost(), meterContainer); - - // 4 vertical bars, anchored center-Y, with idle scale ~ 0.06 (a thin - // resting glyph). Live updates drive Scale.Y via composition anims. - m_barIdleBrush = m_compositor.CreateColorBrush(FromHex(0xFF525866)); - m_barLiveBrush = m_compositor.CreateColorBrush(FromHex(0xFFE5484D)); - for (int i = 0; i < kBarCount; ++i) { - auto bar = m_compositor.CreateSpriteVisual(); - bar.Size({ kBarWidth, kBarMaxHeight }); - bar.AnchorPoint({ 0.5f, 0.5f }); - bar.Offset({ - static_cast(i) * (kBarWidth + kBarGap) + kBarWidth * 0.5f, - (kBarMaxHeight + 4) * 0.5f, - 0.0f, - }); - bar.Scale({ 1.0f, 0.06f, 1.0f }); - bar.Brush(m_barIdleBrush); - meterContainer.Children().InsertAtTop(bar); - m_barVisuals[static_cast(i)] = bar; - } +// ============================================================ Composition - // Recording dot — drawn as a 10×10 sprite visual on the DotHost element. - auto dotContainer = m_compositor.CreateContainerVisual(); - dotContainer.Size({ 12.0f, 12.0f }); - muxh::ElementCompositionPreview::SetElementChildVisual(DotHost(), dotContainer); - - m_dotNeutralBrush = m_compositor.CreateColorBrush(FromHex(0xFF525866)); - m_dotRecordBrush = m_compositor.CreateColorBrush(FromHex(0xFFE5484D)); - m_dotVisual = m_compositor.CreateSpriteVisual(); - m_dotVisual.Size({ 8.0f, 8.0f }); - m_dotVisual.AnchorPoint({ 0.5f, 0.5f }); - m_dotVisual.Offset({ 6.0f, 6.0f, 0.0f }); - m_dotVisual.Brush(m_dotNeutralBrush); - // Composition has no "Border" geometry on SpriteVisual; we fake the - // dot via a sized SpriteVisual with the matching corner clip below. - auto dotClipGeo = m_compositor.CreateRoundedRectangleGeometry(); - dotClipGeo.Size({ 8.0f, 8.0f }); - dotClipGeo.CornerRadius({ 4.0f, 4.0f }); - dotClipGeo.Offset({ 0.0f, 0.0f }); - m_dotVisual.Clip(m_compositor.CreateGeometricClip(dotClipGeo)); - dotContainer.Children().InsertAtTop(m_dotVisual); +void IndicatorWindow::BuildCompositionLayer() +{ + auto pillVisual = muxh::ElementCompositionPreview::GetElementVisual(PillFrame()); + m_compositor = pillVisual.Compositor(); + m_ease = StandardEase(m_compositor); + + // Composition geometric clip on the Border's visual. Animating this + // geometry's Size + Offset is what conveys state changes — no XAML + // layout passes, no UI-thread wakes during the tween. + m_clipGeo = m_compositor.CreateRoundedRectangleGeometry(); + m_clipGeo.CornerRadius({kCorner, kCorner}); + m_clipGeo.Size({kWindowW, kWindowH}); + m_clipGeo.Offset({0.0f, 0.0f}); + auto clip = m_compositor.CreateGeometricClip(m_clipGeo); + pillVisual.Clip(clip); + + // Child visual tree for dot + meter bars. Parented to the meter host + // (anchored within the pill grid by XAML layout so it stays inside + // the visible clip). + auto meterContainer = m_compositor.CreateContainerVisual(); + meterContainer.Size({kBarCount * (kBarWidth + kBarGap) - kBarGap, kBarMaxHeight + 4}); + muxh::ElementCompositionPreview::SetElementChildVisual(MeterHost(), meterContainer); + + // 4 vertical bars, anchored center-Y, with idle scale ~ 0.06 (a thin + // resting glyph). Live updates drive Scale.Y via composition anims. + m_barIdleBrush = m_compositor.CreateColorBrush(FromHex(0xFF525866)); + m_barLiveBrush = m_compositor.CreateColorBrush(FromHex(0xFFE5484D)); + for (int i = 0; i < kBarCount; ++i) { + auto bar = m_compositor.CreateSpriteVisual(); + bar.Size({kBarWidth, kBarMaxHeight}); + bar.AnchorPoint({0.5f, 0.5f}); + bar.Offset({ + static_cast(i) * (kBarWidth + kBarGap) + kBarWidth * 0.5f, + (kBarMaxHeight + 4) * 0.5f, + 0.0f, + }); + bar.Scale({1.0f, 0.06f, 1.0f}); + bar.Brush(m_barIdleBrush); + meterContainer.Children().InsertAtTop(bar); + m_barVisuals[static_cast(i)] = bar; } - void IndicatorWindow::UpdateMeterBars(float peak) - { - const float clamped = std::clamp(peak, 0.0f, 1.0f); - const auto dur = std::chrono::milliseconds(kMeterMs); - - for (int i = 0; i < kBarCount; ++i) { - // Mild attenuation per bar position for VU-meter character. - const float falloff = 1.0f - 0.12f * static_cast(i); - const float target = std::max(0.06f, clamped * falloff); + // Recording dot — drawn as a 10×10 sprite visual on the DotHost element. + auto dotContainer = m_compositor.CreateContainerVisual(); + dotContainer.Size({12.0f, 12.0f}); + muxh::ElementCompositionPreview::SetElementChildVisual(DotHost(), dotContainer); + + m_dotNeutralBrush = m_compositor.CreateColorBrush(FromHex(0xFF525866)); + m_dotRecordBrush = m_compositor.CreateColorBrush(FromHex(0xFFE5484D)); + m_dotVisual = m_compositor.CreateSpriteVisual(); + m_dotVisual.Size({8.0f, 8.0f}); + m_dotVisual.AnchorPoint({0.5f, 0.5f}); + m_dotVisual.Offset({6.0f, 6.0f, 0.0f}); + m_dotVisual.Brush(m_dotNeutralBrush); + // Composition has no "Border" geometry on SpriteVisual; we fake the + // dot via a sized SpriteVisual with the matching corner clip below. + auto dotClipGeo = m_compositor.CreateRoundedRectangleGeometry(); + dotClipGeo.Size({8.0f, 8.0f}); + dotClipGeo.CornerRadius({4.0f, 4.0f}); + dotClipGeo.Offset({0.0f, 0.0f}); + m_dotVisual.Clip(m_compositor.CreateGeometricClip(dotClipGeo)); + dotContainer.Children().InsertAtTop(m_dotVisual); +} - auto anim = m_compositor.CreateScalarKeyFrameAnimation(); - anim.InsertKeyFrame(1.0f, target, m_ease); - anim.Duration(dur); - m_barVisuals[static_cast(i)].StartAnimation(L"Scale.Y", anim); - } +void IndicatorWindow::UpdateMeterBars(float peak) +{ + const float clamped = std::clamp(peak, 0.0f, 1.0f); + const auto dur = std::chrono::milliseconds(kMeterMs); + + for (int i = 0; i < kBarCount; ++i) { + // Mild attenuation per bar position for VU-meter character. + const float falloff = 1.0f - 0.12f * static_cast(i); + const float target = std::max(0.06f, clamped * falloff); + + auto anim = m_compositor.CreateScalarKeyFrameAnimation(); + anim.InsertKeyFrame(1.0f, target, m_ease); + anim.Duration(dur); + m_barVisuals[static_cast(i)].StartAnimation(L"Scale.Y", anim); } +} - void IndicatorWindow::UpdateDotForState(::yip::IndicatorState s) - { - if (!m_dotVisual) return; - const bool live = (s == ::yip::IndicatorState::Recording); - m_dotVisual.Brush(live ? m_dotRecordBrush : m_dotNeutralBrush); - - // Pulse opacity gently during recording for "alive" feel. - if (live) { - auto pulse = m_compositor.CreateScalarKeyFrameAnimation(); - pulse.InsertKeyFrame(0.0f, 1.0f, m_ease); - pulse.InsertKeyFrame(0.5f, 0.55f, m_ease); - pulse.InsertKeyFrame(1.0f, 1.0f, m_ease); - pulse.Duration(std::chrono::milliseconds(1200)); - pulse.IterationBehavior(mucomp::AnimationIterationBehavior::Forever); - m_dotVisual.StartAnimation(L"Opacity", pulse); - } else { - m_dotVisual.StopAnimation(L"Opacity"); - m_dotVisual.Opacity(1.0f); - } +void IndicatorWindow::UpdateDotForState(::yip::IndicatorState s) +{ + if (!m_dotVisual) return; + const bool live = (s == ::yip::IndicatorState::Recording); + m_dotVisual.Brush(live ? m_dotRecordBrush : m_dotNeutralBrush); + + // Pulse opacity gently during recording for "alive" feel. + if (live) { + auto pulse = m_compositor.CreateScalarKeyFrameAnimation(); + pulse.InsertKeyFrame(0.0f, 1.0f, m_ease); + pulse.InsertKeyFrame(0.5f, 0.55f, m_ease); + pulse.InsertKeyFrame(1.0f, 1.0f, m_ease); + pulse.Duration(std::chrono::milliseconds(1200)); + pulse.IterationBehavior(mucomp::AnimationIterationBehavior::Forever); + m_dotVisual.StartAnimation(L"Opacity", pulse); + } else { + m_dotVisual.StopAnimation(L"Opacity"); + m_dotVisual.Opacity(1.0f); } +} - void IndicatorWindow::StopMeterAnimations() - { - for (auto& bar : m_barVisuals) { - if (!bar) continue; - bar.StopAnimation(L"Scale.Y"); - // Snap back to idle resting scale. - auto anim = m_compositor.CreateScalarKeyFrameAnimation(); - anim.InsertKeyFrame(1.0f, 0.06f, m_ease); - anim.Duration(std::chrono::milliseconds(kFadeMs)); - bar.StartAnimation(L"Scale.Y", anim); - } +void IndicatorWindow::StopMeterAnimations() +{ + for (auto& bar : m_barVisuals) { + if (!bar) continue; + bar.StopAnimation(L"Scale.Y"); + // Snap back to idle resting scale. + auto anim = m_compositor.CreateScalarKeyFrameAnimation(); + anim.InsertKeyFrame(1.0f, 0.06f, m_ease); + anim.Duration(std::chrono::milliseconds(kFadeMs)); + bar.StartAnimation(L"Scale.Y", anim); } +} - // =========================================================== State machine - - void IndicatorWindow::SyncFromAudioCore() - { - const bool recording = ::rec_is_recording() != 0; - const auto now = std::chrono::steady_clock::now(); - if (recording) m_lastRecordingTrueTs = now; - - // Expanded is a user-driven overlay. Honor the user's expansion - // until either (a) recording stops (drop into Saving so the pill - // can fade out cleanly) or (b) auto-collapse fires. - if (m_state == ::yip::IndicatorState::Expanded) { - if (!recording) { - TransitionTo(::yip::IndicatorState::Saving, true); - return; - } - if (!m_meterTimer.IsRunning()) m_meterTimer.Start(); +// =========================================================== State machine + +void IndicatorWindow::SyncFromAudioCore() +{ + const bool recording = ::rec_is_recording() != 0; + const auto now = std::chrono::steady_clock::now(); + if (recording) m_lastRecordingTrueTs = now; + + // Expanded is a user-driven overlay. Honor the user's expansion + // until either (a) recording stops (drop into Saving so the pill + // can fade out cleanly) or (b) auto-collapse fires. + if (m_state == ::yip::IndicatorState::Expanded) { + if (!recording) { + TransitionTo(::yip::IndicatorState::Saving, true); return; } + if (!m_meterTimer.IsRunning()) m_meterTimer.Start(); + return; + } - if (recording) { - if (m_state != ::yip::IndicatorState::Recording) { - TransitionTo(::yip::IndicatorState::Recording, true); - } - } else if (m_state == ::yip::IndicatorState::Recording) { - // Brief Saving frame before settling to Idle. - TransitionTo(::yip::IndicatorState::Saving, true); - } else if (m_state == ::yip::IndicatorState::Saving) { - const auto since = std::chrono::duration( - now - m_lastRecordingTrueTs).count(); - if (since >= kSavingDebounceMs) { - TransitionTo(::yip::IndicatorState::Idle, true); - } + if (recording) { + if (m_state != ::yip::IndicatorState::Recording) { + TransitionTo(::yip::IndicatorState::Recording, true); + } + } else if (m_state == ::yip::IndicatorState::Recording) { + // Brief Saving frame before settling to Idle. + TransitionTo(::yip::IndicatorState::Saving, true); + } else if (m_state == ::yip::IndicatorState::Saving) { + const auto since = std::chrono::duration(now - m_lastRecordingTrueTs).count(); + if (since >= kSavingDebounceMs) { + TransitionTo(::yip::IndicatorState::Idle, true); } } +} - void IndicatorWindow::TransitionTo(::yip::IndicatorState s, bool animate) - { - // Visibility gate first — runs even when s == m_state on the very - // first call from the constructor (m_state initialised to Idle). - SyncVisibilityForState(s); +void IndicatorWindow::TransitionTo(::yip::IndicatorState s, bool animate) +{ + // Visibility gate first — runs even when s == m_state on the very + // first call from the constructor (m_state initialised to Idle). + SyncVisibilityForState(s); - if (s == m_state) return; + if (s == m_state) return; - // Remember "base" so Expanded can return after auto-collapse. - if (s == ::yip::IndicatorState::Expanded) { - m_baseState = m_state; - } + // Remember "base" so Expanded can return after auto-collapse. + if (s == ::yip::IndicatorState::Expanded) { + m_baseState = m_state; + } - m_state = s; + m_state = s; - // XAML opacity transitions (composition-backed in WinUI 3). - const bool wantActions = - (s == ::yip::IndicatorState::Expanded); - const bool wantMeter = - (s == ::yip::IndicatorState::Recording || - s == ::yip::IndicatorState::Expanded || - s == ::yip::IndicatorState::Saving); + // XAML opacity transitions (composition-backed in WinUI 3). + const bool wantActions = (s == ::yip::IndicatorState::Expanded); + const bool wantMeter = (s == ::yip::IndicatorState::Recording || s == ::yip::IndicatorState::Expanded || + s == ::yip::IndicatorState::Saving); - ExpandedActions().Opacity(wantActions ? 1.0 : 0.0); - ExpandedActions().IsHitTestVisible(wantActions); + ExpandedActions().Opacity(wantActions ? 1.0 : 0.0); + ExpandedActions().IsHitTestVisible(wantActions); - MeterHost().Opacity(wantMeter ? 1.0 : 0.0); + MeterHost().Opacity(wantMeter ? 1.0 : 0.0); - AnimatePillToState(s); - UpdateDotForState(s); + AnimatePillToState(s); + UpdateDotForState(s); - // Meter timer runs only while a meter is visible AND audio is live. - const bool recording = ::rec_is_recording() != 0; - const bool meterShouldRun = wantMeter && recording; - if (meterShouldRun && !m_meterTimer.IsRunning()) m_meterTimer.Start(); - if (!meterShouldRun && m_meterTimer.IsRunning()) { m_meterTimer.Stop(); StopMeterAnimations(); } + // Meter timer runs only while a meter is visible AND audio is live. + const bool recording = ::rec_is_recording() != 0; + const bool meterShouldRun = wantMeter && recording; + if (meterShouldRun && !m_meterTimer.IsRunning()) m_meterTimer.Start(); + if (!meterShouldRun && m_meterTimer.IsRunning()) { + m_meterTimer.Stop(); + StopMeterAnimations(); + } - // Auto-collapse after 3s when expanded. - if (s == ::yip::IndicatorState::Expanded) ResetAutoCollapseTimer(); - else StopAutoCollapseTimer(); + // Auto-collapse after 3s when expanded. + if (s == ::yip::IndicatorState::Expanded) + ResetAutoCollapseTimer(); + else + StopAutoCollapseTimer(); - if (!animate) { - // Force-finalize via 0-duration animation so resting state lands. - } + if (!animate) { + // Force-finalize via 0-duration animation so resting state lands. } +} - void IndicatorWindow::AnimatePillToState(::yip::IndicatorState s) - { - if (!m_clipGeo) return; - const auto g = GeometryFor(s); - - // Center the clip rect inside the window. - const float offX = (static_cast(kWindowW) - g.w) * 0.5f; - const float offY = (static_cast(kWindowH) - g.h) * 0.5f; - - auto sizeAnim = m_compositor.CreateVector2KeyFrameAnimation(); - sizeAnim.InsertKeyFrame(1.0f, { g.w, g.h }, m_ease); - sizeAnim.Duration(std::chrono::milliseconds(kResizeMs)); - m_clipGeo.StartAnimation(L"Size", sizeAnim); - - auto offAnim = m_compositor.CreateVector2KeyFrameAnimation(); - offAnim.InsertKeyFrame(1.0f, { offX, offY }, m_ease); - offAnim.Duration(std::chrono::milliseconds(kResizeMs)); - m_clipGeo.StartAnimation(L"Offset", offAnim); - - // Pill opacity via the Border's Visual. - auto pillVisual = muxh::ElementCompositionPreview::GetElementVisual(PillFrame()); - auto fade = m_compositor.CreateScalarKeyFrameAnimation(); - fade.InsertKeyFrame(1.0f, g.opacity, m_ease); - fade.Duration(std::chrono::milliseconds(kFadeMs)); - pillVisual.StartAnimation(L"Opacity", fade); +void IndicatorWindow::AnimatePillToState(::yip::IndicatorState s) +{ + if (!m_clipGeo) return; + const auto g = GeometryFor(s); + + // Center the clip rect inside the window. + const float offX = (static_cast(kWindowW) - g.w) * 0.5f; + const float offY = (static_cast(kWindowH) - g.h) * 0.5f; + + auto sizeAnim = m_compositor.CreateVector2KeyFrameAnimation(); + sizeAnim.InsertKeyFrame(1.0f, {g.w, g.h}, m_ease); + sizeAnim.Duration(std::chrono::milliseconds(kResizeMs)); + m_clipGeo.StartAnimation(L"Size", sizeAnim); + + auto offAnim = m_compositor.CreateVector2KeyFrameAnimation(); + offAnim.InsertKeyFrame(1.0f, {offX, offY}, m_ease); + offAnim.Duration(std::chrono::milliseconds(kResizeMs)); + m_clipGeo.StartAnimation(L"Offset", offAnim); + + // Pill opacity via the Border's Visual. + auto pillVisual = muxh::ElementCompositionPreview::GetElementVisual(PillFrame()); + auto fade = m_compositor.CreateScalarKeyFrameAnimation(); + fade.InsertKeyFrame(1.0f, g.opacity, m_ease); + fade.Duration(std::chrono::milliseconds(kFadeMs)); + pillVisual.StartAnimation(L"Opacity", fade); +} + +// =========================================================== Pointer + drag + +void IndicatorWindow::OnPillPointerPressed(winrt::Windows::Foundation::IInspectable const& /*sender*/, + muxi::PointerRoutedEventArgs const& args) +{ + // Ctrl+click → flip click-through. Do not capture; let the press fall + // through as a "tap" for state expansion. + const auto mods = args.KeyModifiers(); + if ((mods & winrt::Windows::System::VirtualKeyModifiers::Control) == + winrt::Windows::System::VirtualKeyModifiers::Control) { + ApplyClickThrough(!m_persisted.click_through); + (void)m_persisted.Save(); + return; } - // =========================================================== Pointer + drag - - void IndicatorWindow::OnPillPointerPressed( - winrt::Windows::Foundation::IInspectable const& /*sender*/, - muxi::PointerRoutedEventArgs const& args) - { - // Ctrl+click → flip click-through. Do not capture; let the press fall - // through as a "tap" for state expansion. - const auto mods = args.KeyModifiers(); - if ((mods & winrt::Windows::System::VirtualKeyModifiers::Control) - == winrt::Windows::System::VirtualKeyModifiers::Control) { - ApplyClickThrough(!m_persisted.click_through); - (void)m_persisted.Save(); - return; - } + m_movedDuringPress = false; + m_dragging = false; - m_movedDuringPress = false; - m_dragging = false; + const auto pt = args.GetCurrentPoint(nullptr).Position(); + m_dragOrigin = pt; - const auto pt = args.GetCurrentPoint(nullptr).Position(); - m_dragOrigin = pt; + RECT rc{}; + ::GetWindowRect(m_hwnd, &rc); + m_windowOriginAtDragStart = {static_cast(rc.left), static_cast(rc.top)}; - RECT rc{}; - ::GetWindowRect(m_hwnd, &rc); - m_windowOriginAtDragStart = { static_cast(rc.left), static_cast(rc.top) }; + PillFrame().CapturePointer(args.Pointer()); + m_dragging = true; +} - PillFrame().CapturePointer(args.Pointer()); - m_dragging = true; - } +void IndicatorWindow::OnPillPointerMoved(winrt::Windows::Foundation::IInspectable const& /*sender*/, + muxi::PointerRoutedEventArgs const& args) +{ + if (!m_dragging || !m_hwnd) return; - void IndicatorWindow::OnPillPointerMoved( - winrt::Windows::Foundation::IInspectable const& /*sender*/, - muxi::PointerRoutedEventArgs const& args) - { - if (!m_dragging || !m_hwnd) return; + const auto pt = args.GetCurrentPoint(nullptr).Position(); + const float dx = pt.X - m_dragOrigin.X; + const float dy = pt.Y - m_dragOrigin.Y; + if (std::abs(dx) < 2.0f && std::abs(dy) < 2.0f) return; - const auto pt = args.GetCurrentPoint(nullptr).Position(); - const float dx = pt.X - m_dragOrigin.X; - const float dy = pt.Y - m_dragOrigin.Y; - if (std::abs(dx) < 2.0f && std::abs(dy) < 2.0f) return; + m_movedDuringPress = true; - m_movedDuringPress = true; + const int newX = static_cast(std::lround(m_windowOriginAtDragStart.X + dx)); + const int newY = static_cast(std::lround(m_windowOriginAtDragStart.Y + dy)); - const int newX = static_cast(std::lround(m_windowOriginAtDragStart.X + dx)); - const int newY = static_cast(std::lround(m_windowOriginAtDragStart.Y + dy)); + auto wid = winrt::Microsoft::UI::GetWindowIdFromWindow(m_hwnd); + auto appWindow = muw::AppWindow::GetFromWindowId(wid); + appWindow.Move({newX, newY}); +} - auto wid = winrt::Microsoft::UI::GetWindowIdFromWindow(m_hwnd); - auto appWindow = muw::AppWindow::GetFromWindowId(wid); - appWindow.Move({ newX, newY }); - } +void IndicatorWindow::OnPillPointerReleased(winrt::Windows::Foundation::IInspectable const& /*sender*/, + muxi::PointerRoutedEventArgs const& args) +{ + if (!m_dragging) return; + m_dragging = false; + PillFrame().ReleasePointerCapture(args.Pointer()); - void IndicatorWindow::OnPillPointerReleased( - winrt::Windows::Foundation::IInspectable const& /*sender*/, - muxi::PointerRoutedEventArgs const& args) - { - if (!m_dragging) return; - m_dragging = false; - PillFrame().ReleasePointerCapture(args.Pointer()); - - if (m_movedDuringPress) { - SnapToNearestEdgeIfClose(); - RememberPosition(); - } + if (m_movedDuringPress) { + SnapToNearestEdgeIfClose(); + RememberPosition(); } +} - void IndicatorWindow::OnPillPointerCaptureLost( - winrt::Windows::Foundation::IInspectable const& /*sender*/, - muxi::PointerRoutedEventArgs const& /*args*/) - { - m_dragging = false; - } +void IndicatorWindow::OnPillPointerCaptureLost(winrt::Windows::Foundation::IInspectable const& /*sender*/, + muxi::PointerRoutedEventArgs const& /*args*/) +{ + m_dragging = false; +} - void IndicatorWindow::OnPillTapped( - winrt::Windows::Foundation::IInspectable const& /*sender*/, - muxi::TappedRoutedEventArgs const& /*args*/) - { - if (m_movedDuringPress) return; // drag, not tap +void IndicatorWindow::OnPillTapped(winrt::Windows::Foundation::IInspectable const& /*sender*/, + muxi::TappedRoutedEventArgs const& /*args*/) +{ + if (m_movedDuringPress) return; // drag, not tap - if (m_state == ::yip::IndicatorState::Expanded) { - TransitionTo(m_baseState, true); - } else { - TransitionTo(::yip::IndicatorState::Expanded, true); - } + if (m_state == ::yip::IndicatorState::Expanded) { + TransitionTo(m_baseState, true); + } else { + TransitionTo(::yip::IndicatorState::Expanded, true); } +} - // =========================================================== Expanded actions - - void IndicatorWindow::OnStopClicked( - winrt::Windows::Foundation::IInspectable const& /*sender*/, - mux::RoutedEventArgs const& /*args*/) - { - (void)::rec_stop(); - // Don't transition here — SyncFromAudioCore will pull through Saving - // → Idle automatically on the next poll tick. - ResetAutoCollapseTimer(); - } +// =========================================================== Expanded actions - void IndicatorWindow::OnMarkClicked( - winrt::Windows::Foundation::IInspectable const& /*sender*/, - mux::RoutedEventArgs const& /*args*/) - { - if (!::rec_is_recording()) return; - const auto t_ms = ::rec_elapsed_ms(); - const char* path_utf8 = ::rec_current_path(); - if (!path_utf8) return; - - // utf8 → wide → path - const int n = ::MultiByteToWideChar(CP_UTF8, 0, path_utf8, -1, nullptr, 0); - std::wstring wide; - if (n > 0) { - wide.resize(static_cast(n) - 1); - ::MultiByteToWideChar(CP_UTF8, 0, path_utf8, -1, wide.data(), n); - } - std::filesystem::path p(wide); - ::yip::markers::Marker m{ t_ms, std::nullopt }; - (void)::yip::markers::Append(p, m); +void IndicatorWindow::OnStopClicked(winrt::Windows::Foundation::IInspectable const& /*sender*/, + mux::RoutedEventArgs const& /*args*/) +{ + (void)::rec_stop(); + // Don't transition here — SyncFromAudioCore will pull through Saving + // → Idle automatically on the next poll tick. + ResetAutoCollapseTimer(); +} - ResetAutoCollapseTimer(); +void IndicatorWindow::OnMarkClicked(winrt::Windows::Foundation::IInspectable const& /*sender*/, + mux::RoutedEventArgs const& /*args*/) +{ + if (!::rec_is_recording()) return; + const auto t_ms = ::rec_elapsed_ms(); + const char* path_utf8 = ::rec_current_path(); + if (!path_utf8) return; + + // utf8 → wide → path + const int n = ::MultiByteToWideChar(CP_UTF8, 0, path_utf8, -1, nullptr, 0); + std::wstring wide; + if (n > 0) { + wide.resize(static_cast(n) - 1); + ::MultiByteToWideChar(CP_UTF8, 0, path_utf8, -1, wide.data(), n); } + std::filesystem::path p(wide); + ::yip::markers::Marker m{t_ms, std::nullopt}; + (void)::yip::markers::Append(p, m); - void IndicatorWindow::OnOpenLastClicked( - winrt::Windows::Foundation::IInspectable const& /*sender*/, - mux::RoutedEventArgs const& /*args*/) - { - std::wstring target; - if (::rec_is_recording()) { - const char* p = ::rec_current_path(); - if (p) { - const int n = ::MultiByteToWideChar(CP_UTF8, 0, p, -1, nullptr, 0); - if (n > 0) { - target.resize(static_cast(n) - 1); - ::MultiByteToWideChar(CP_UTF8, 0, p, -1, target.data(), n); - } + ResetAutoCollapseTimer(); +} + +void IndicatorWindow::OnOpenLastClicked(winrt::Windows::Foundation::IInspectable const& /*sender*/, + mux::RoutedEventArgs const& /*args*/) +{ + std::wstring target; + if (::rec_is_recording()) { + const char* p = ::rec_current_path(); + if (p) { + const int n = ::MultiByteToWideChar(CP_UTF8, 0, p, -1, nullptr, 0); + if (n > 0) { + target.resize(static_cast(n) - 1); + ::MultiByteToWideChar(CP_UTF8, 0, p, -1, target.data(), n); } } - if (target.empty()) { - // Last-modified .wav in the output folder. - const auto root = ::yip::Settings::Load().output_folder; - std::error_code ec; - std::filesystem::file_time_type best{}; - std::filesystem::path bestPath; - for (auto const& e : std::filesystem::directory_iterator(root, ec)) { - if (ec) break; - if (!e.is_regular_file()) continue; - if (e.path().extension() != L".wav") continue; - const auto t = std::filesystem::last_write_time(e.path(), ec); - if (bestPath.empty() || t > best) { best = t; bestPath = e.path(); } + } + if (target.empty()) { + // Last-modified .wav in the output folder. + const auto root = ::yip::Settings::Load().output_folder; + std::error_code ec; + std::filesystem::file_time_type best{}; + std::filesystem::path bestPath; + for (auto const& e : std::filesystem::directory_iterator(root, ec)) { + if (ec) break; + if (!e.is_regular_file()) continue; + if (e.path().extension() != L".wav") continue; + const auto t = std::filesystem::last_write_time(e.path(), ec); + if (bestPath.empty() || t > best) { + best = t; + bestPath = e.path(); } - if (!bestPath.empty()) target = bestPath.wstring(); } - if (target.empty()) return; - - std::wstring args = L"/select,\"" + target + L"\""; - ::ShellExecuteW(nullptr, L"open", L"explorer.exe", args.c_str(), - nullptr, SW_SHOWNORMAL); - - ResetAutoCollapseTimer(); + if (!bestPath.empty()) target = bestPath.wstring(); } + if (target.empty()) return; - // =========================================================== Edge snap + persistence + std::wstring args = L"/select,\"" + target + L"\""; + ::ShellExecuteW(nullptr, L"open", L"explorer.exe", args.c_str(), nullptr, SW_SHOWNORMAL); - void IndicatorWindow::RestoreFromPersistence() - { - if (!m_hwnd) return; + ResetAutoCollapseTimer(); +} - auto wid = winrt::Microsoft::UI::GetWindowIdFromWindow(m_hwnd); - auto appWindow = muw::AppWindow::GetFromWindowId(wid); +// =========================================================== Edge snap + persistence - // Find target monitor. - auto displays = muw::DisplayArea::FindAll(); - muw::DisplayArea chosen{ nullptr }; - for (auto const& d : displays) { - if (d.DisplayId().Value == m_persisted.monitor_id) { chosen = d; break; } - } - if (!chosen) { - chosen = muw::DisplayArea::Primary(muw::DisplayAreaFallback::Primary); - } - if (!chosen) return; - - const auto work = chosen.WorkArea(); - - int x = work.X + (work.Width - kWindowW) / 2; - int y = work.Y + 20; - - const double t = std::clamp(m_persisted.edge_offset, 0.0, 1.0); - switch (m_persisted.dock_edge) { - case ::yip::DockEdge::Top: - x = work.X + static_cast(std::lround( - (work.Width - kWindowW) * t)); - y = work.Y + 12; - break; - case ::yip::DockEdge::Bottom: - x = work.X + static_cast(std::lround( - (work.Width - kWindowW) * t)); - y = work.Y + work.Height - kWindowH - 12; - break; - case ::yip::DockEdge::Left: - x = work.X + 12; - y = work.Y + static_cast(std::lround( - (work.Height - kWindowH) * t)); - break; - case ::yip::DockEdge::Right: - x = work.X + work.Width - kWindowW - 12; - y = work.Y + static_cast(std::lround( - (work.Height - kWindowH) * t)); - break; - case ::yip::DockEdge::None: - default: - break; +void IndicatorWindow::RestoreFromPersistence() +{ + if (!m_hwnd) return; + + auto wid = winrt::Microsoft::UI::GetWindowIdFromWindow(m_hwnd); + auto appWindow = muw::AppWindow::GetFromWindowId(wid); + + // Find target monitor. + auto displays = muw::DisplayArea::FindAll(); + muw::DisplayArea chosen{nullptr}; + for (auto const& d : displays) { + if (d.DisplayId().Value == m_persisted.monitor_id) { + chosen = d; + break; } - appWindow.Move({ x, y }); } - - void IndicatorWindow::SnapToNearestEdgeIfClose() - { - if (!m_hwnd) return; - RECT rc{}; - ::GetWindowRect(m_hwnd, &rc); - const int cx = (rc.left + rc.right) / 2; - const int cy = (rc.top + rc.bottom) / 2; - - auto wid = winrt::Microsoft::UI::GetWindowIdFromWindow(m_hwnd); - auto appWindow = muw::AppWindow::GetFromWindowId(wid); - - // Pick the DisplayArea containing the window center. - muw::DisplayArea host{ muw::DisplayArea::GetFromWindowId(wid, muw::DisplayAreaFallback::Primary) }; - if (!host) return; - const auto work = host.WorkArea(); - - const int distTop = std::abs(rc.top - work.Y); - const int distBottom = std::abs((work.Y + work.Height) - rc.bottom); - const int distLeft = std::abs(rc.left - work.X); - const int distRight = std::abs((work.X + work.Width) - rc.right); - const int minDist = std::min({ distTop, distBottom, distLeft, distRight }); - if (minDist > kSnapPx) return; - - int x = rc.left; - int y = rc.top; - if (minDist == distTop) y = work.Y + 12; - else if (minDist == distBottom) y = work.Y + work.Height - kWindowH - 12; - else if (minDist == distLeft) x = work.X + 12; - else if (minDist == distRight) x = work.X + work.Width - kWindowW - 12; - appWindow.Move({ x, y }); + if (!chosen) { + chosen = muw::DisplayArea::Primary(muw::DisplayAreaFallback::Primary); } - - void IndicatorWindow::RememberPosition() - { - if (!m_hwnd) return; - - auto wid = winrt::Microsoft::UI::GetWindowIdFromWindow(m_hwnd); - muw::DisplayArea host{ muw::DisplayArea::GetFromWindowId(wid, muw::DisplayAreaFallback::Primary) }; - if (!host) return; - - m_persisted.monitor_id = host.DisplayId().Value; - - RECT rc{}; - ::GetWindowRect(m_hwnd, &rc); - const auto work = host.WorkArea(); - - const int distTop = std::abs(rc.top - work.Y); - const int distBottom = std::abs((work.Y + work.Height) - rc.bottom); - const int distLeft = std::abs(rc.left - work.X); - const int distRight = std::abs((work.X + work.Width) - rc.right); - const int minDist = std::min({ distTop, distBottom, distLeft, distRight }); - - if (minDist > kSnapPx) { - m_persisted.dock_edge = ::yip::DockEdge::None; - } else if (minDist == distTop) { - m_persisted.dock_edge = ::yip::DockEdge::Top; - const int span = std::max(1, work.Width - kWindowW); - m_persisted.edge_offset = std::clamp(double(rc.left - work.X) / span, 0.0, 1.0); - } else if (minDist == distBottom) { - m_persisted.dock_edge = ::yip::DockEdge::Bottom; - const int span = std::max(1, work.Width - kWindowW); - m_persisted.edge_offset = std::clamp(double(rc.left - work.X) / span, 0.0, 1.0); - } else if (minDist == distLeft) { - m_persisted.dock_edge = ::yip::DockEdge::Left; - const int span = std::max(1, work.Height - kWindowH); - m_persisted.edge_offset = std::clamp(double(rc.top - work.Y) / span, 0.0, 1.0); - } else { - m_persisted.dock_edge = ::yip::DockEdge::Right; - const int span = std::max(1, work.Height - kWindowH); - m_persisted.edge_offset = std::clamp(double(rc.top - work.Y) / span, 0.0, 1.0); - } - (void)m_persisted.Save(); + if (!chosen) return; + + const auto work = chosen.WorkArea(); + + int x = work.X + (work.Width - kWindowW) / 2; + int y = work.Y + 20; + + const double t = std::clamp(m_persisted.edge_offset, 0.0, 1.0); + switch (m_persisted.dock_edge) { + case ::yip::DockEdge::Top: + x = work.X + static_cast(std::lround((work.Width - kWindowW) * t)); + y = work.Y + 12; + break; + case ::yip::DockEdge::Bottom: + x = work.X + static_cast(std::lround((work.Width - kWindowW) * t)); + y = work.Y + work.Height - kWindowH - 12; + break; + case ::yip::DockEdge::Left: + x = work.X + 12; + y = work.Y + static_cast(std::lround((work.Height - kWindowH) * t)); + break; + case ::yip::DockEdge::Right: + x = work.X + work.Width - kWindowW - 12; + y = work.Y + static_cast(std::lround((work.Height - kWindowH) * t)); + break; + case ::yip::DockEdge::None: + default: + break; } + appWindow.Move({x, y}); +} - // =========================================================== Visibility - - void IndicatorWindow::ShowWindow() - { - if (!m_hwnd) return; - auto wid = winrt::Microsoft::UI::GetWindowIdFromWindow(m_hwnd); - auto appWindow = muw::AppWindow::GetFromWindowId(wid); - if (appWindow) appWindow.Show(); - // Re-assert TOPMOST + NoActivate after show, in case the Win32 - // show path clobbered them. - ::SetWindowPos(m_hwnd, HWND_TOPMOST, 0, 0, 0, 0, - SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); - } +void IndicatorWindow::SnapToNearestEdgeIfClose() +{ + if (!m_hwnd) return; + RECT rc{}; + ::GetWindowRect(m_hwnd, &rc); + const int cx = (rc.left + rc.right) / 2; + const int cy = (rc.top + rc.bottom) / 2; + + auto wid = winrt::Microsoft::UI::GetWindowIdFromWindow(m_hwnd); + auto appWindow = muw::AppWindow::GetFromWindowId(wid); + + // Pick the DisplayArea containing the window center. + muw::DisplayArea host{muw::DisplayArea::GetFromWindowId(wid, muw::DisplayAreaFallback::Primary)}; + if (!host) return; + const auto work = host.WorkArea(); + + const int distTop = std::abs(rc.top - work.Y); + const int distBottom = std::abs((work.Y + work.Height) - rc.bottom); + const int distLeft = std::abs(rc.left - work.X); + const int distRight = std::abs((work.X + work.Width) - rc.right); + const int minDist = std::min({distTop, distBottom, distLeft, distRight}); + if (minDist > kSnapPx) return; + + int x = rc.left; + int y = rc.top; + if (minDist == distTop) + y = work.Y + 12; + else if (minDist == distBottom) + y = work.Y + work.Height - kWindowH - 12; + else if (minDist == distLeft) + x = work.X + 12; + else if (minDist == distRight) + x = work.X + work.Width - kWindowW - 12; + appWindow.Move({x, y}); +} - void IndicatorWindow::HideWindow() - { - if (!m_hwnd) return; - auto wid = winrt::Microsoft::UI::GetWindowIdFromWindow(m_hwnd); - auto appWindow = muw::AppWindow::GetFromWindowId(wid); - if (appWindow) appWindow.Hide(); +void IndicatorWindow::RememberPosition() +{ + if (!m_hwnd) return; + + auto wid = winrt::Microsoft::UI::GetWindowIdFromWindow(m_hwnd); + muw::DisplayArea host{muw::DisplayArea::GetFromWindowId(wid, muw::DisplayAreaFallback::Primary)}; + if (!host) return; + + m_persisted.monitor_id = host.DisplayId().Value; + + RECT rc{}; + ::GetWindowRect(m_hwnd, &rc); + const auto work = host.WorkArea(); + + const int distTop = std::abs(rc.top - work.Y); + const int distBottom = std::abs((work.Y + work.Height) - rc.bottom); + const int distLeft = std::abs(rc.left - work.X); + const int distRight = std::abs((work.X + work.Width) - rc.right); + const int minDist = std::min({distTop, distBottom, distLeft, distRight}); + + if (minDist > kSnapPx) { + m_persisted.dock_edge = ::yip::DockEdge::None; + } else if (minDist == distTop) { + m_persisted.dock_edge = ::yip::DockEdge::Top; + const int span = std::max(1, work.Width - kWindowW); + m_persisted.edge_offset = std::clamp(double(rc.left - work.X) / span, 0.0, 1.0); + } else if (minDist == distBottom) { + m_persisted.dock_edge = ::yip::DockEdge::Bottom; + const int span = std::max(1, work.Width - kWindowW); + m_persisted.edge_offset = std::clamp(double(rc.left - work.X) / span, 0.0, 1.0); + } else if (minDist == distLeft) { + m_persisted.dock_edge = ::yip::DockEdge::Left; + const int span = std::max(1, work.Height - kWindowH); + m_persisted.edge_offset = std::clamp(double(rc.top - work.Y) / span, 0.0, 1.0); + } else { + m_persisted.dock_edge = ::yip::DockEdge::Right; + const int span = std::max(1, work.Height - kWindowH); + m_persisted.edge_offset = std::clamp(double(rc.top - work.Y) / span, 0.0, 1.0); } + (void)m_persisted.Save(); +} - void IndicatorWindow::SyncVisibilityForState(::yip::IndicatorState s) - { - using S = ::yip::IndicatorState; - const bool visible = - (s == S::Recording || s == S::Saving || s == S::Expanded); - if (visible) ShowWindow(); - else HideWindow(); - } +// =========================================================== Visibility - // =========================================================== Auto-collapse +void IndicatorWindow::ShowWindow() +{ + if (!m_hwnd) return; + auto wid = winrt::Microsoft::UI::GetWindowIdFromWindow(m_hwnd); + auto appWindow = muw::AppWindow::GetFromWindowId(wid); + if (appWindow) appWindow.Show(); + // Re-assert TOPMOST + NoActivate after show, in case the Win32 + // show path clobbered them. + ::SetWindowPos(m_hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); +} - void IndicatorWindow::ResetAutoCollapseTimer() - { - if (!m_collapseTimer) return; - m_collapseTimer.Stop(); - m_collapseTimer.Start(); - } +void IndicatorWindow::HideWindow() +{ + if (!m_hwnd) return; + auto wid = winrt::Microsoft::UI::GetWindowIdFromWindow(m_hwnd); + auto appWindow = muw::AppWindow::GetFromWindowId(wid); + if (appWindow) appWindow.Hide(); +} - void IndicatorWindow::StopAutoCollapseTimer() - { - if (m_collapseTimer) m_collapseTimer.Stop(); - } +void IndicatorWindow::SyncVisibilityForState(::yip::IndicatorState s) +{ + using S = ::yip::IndicatorState; + const bool visible = (s == S::Recording || s == S::Saving || s == S::Expanded); + if (visible) + ShowWindow(); + else + HideWindow(); +} + +// =========================================================== Auto-collapse + +void IndicatorWindow::ResetAutoCollapseTimer() +{ + if (!m_collapseTimer) return; + m_collapseTimer.Stop(); + m_collapseTimer.Start(); +} + +void IndicatorWindow::StopAutoCollapseTimer() +{ + if (m_collapseTimer) m_collapseTimer.Stop(); } +} // namespace winrt::yip::implementation diff --git a/yip-app/IndicatorWindow.xaml.h b/yip-app/IndicatorWindow.xaml.h index 90f6e4c..43caff6 100644 --- a/yip-app/IndicatorWindow.xaml.h +++ b/yip-app/IndicatorWindow.xaml.h @@ -7,107 +7,96 @@ #include #include -namespace winrt::yip::implementation -{ - struct IndicatorWindow : IndicatorWindowT - { - IndicatorWindow(); - ~IndicatorWindow(); - - // Event handlers (declared in XAML) - void OnPillPointerPressed( - winrt::Windows::Foundation::IInspectable const& sender, - winrt::Microsoft::UI::Xaml::Input::PointerRoutedEventArgs const& args); - void OnPillPointerMoved( - winrt::Windows::Foundation::IInspectable const& sender, - winrt::Microsoft::UI::Xaml::Input::PointerRoutedEventArgs const& args); - void OnPillPointerReleased( - winrt::Windows::Foundation::IInspectable const& sender, - winrt::Microsoft::UI::Xaml::Input::PointerRoutedEventArgs const& args); - void OnPillPointerCaptureLost( - winrt::Windows::Foundation::IInspectable const& sender, - winrt::Microsoft::UI::Xaml::Input::PointerRoutedEventArgs const& args); - void OnPillTapped( - winrt::Windows::Foundation::IInspectable const& sender, - winrt::Microsoft::UI::Xaml::Input::TappedRoutedEventArgs const& args); - - void OnStopClicked( - winrt::Windows::Foundation::IInspectable const& sender, - winrt::Microsoft::UI::Xaml::RoutedEventArgs const& args); - void OnMarkClicked( - winrt::Windows::Foundation::IInspectable const& sender, - winrt::Microsoft::UI::Xaml::RoutedEventArgs const& args); - void OnOpenLastClicked( - winrt::Windows::Foundation::IInspectable const& sender, - winrt::Microsoft::UI::Xaml::RoutedEventArgs const& args); - - private: - // ----- HWND helpers ----- - HWND Hwnd() const noexcept { return m_hwnd; } - void ApplyToolWindowStyle(); - void ApplyAlwaysOnTop(); - void ApplyClickThrough(bool enable); - - // ----- Composition layer ----- - void BuildCompositionLayer(); - void UpdateMeterBars(float peak); - void UpdateDotForState(::yip::IndicatorState s); - void StopMeterAnimations(); - - // ----- State machine ----- - void SyncFromAudioCore(); // polled by m_pollTimer - void TransitionTo(::yip::IndicatorState s, bool animate = true); - void AnimatePillToState(::yip::IndicatorState s); - - // ----- Edge dock / monitor restore ----- - void RestoreFromPersistence(); - void SnapToNearestEdgeIfClose(); - void RememberPosition(); - - // ----- Visibility ----- - void ShowWindow(); - void HideWindow(); - void SyncVisibilityForState(::yip::IndicatorState s); - - // ----- Auto-collapse ----- - void ResetAutoCollapseTimer(); - void StopAutoCollapseTimer(); - - // Field state - HWND m_hwnd{ nullptr }; - ::yip::IndicatorState m_state{ ::yip::IndicatorState::Idle }; - ::yip::IndicatorState m_baseState{ ::yip::IndicatorState::Idle }; // state before expansion - ::yip::IndicatorPersistence m_persisted{}; - - // Composition - winrt::Microsoft::UI::Composition::Compositor m_compositor{ nullptr }; - winrt::Microsoft::UI::Composition::ContainerVisual m_pillRoot{ nullptr }; - winrt::Microsoft::UI::Composition::CompositionRoundedRectangleGeometry m_clipGeo{ nullptr }; - winrt::Microsoft::UI::Composition::SpriteVisual m_dotVisual{ nullptr }; - std::array m_barVisuals{}; - winrt::Microsoft::UI::Composition::CompositionColorBrush m_dotNeutralBrush{ nullptr }; - winrt::Microsoft::UI::Composition::CompositionColorBrush m_dotRecordBrush{ nullptr }; - winrt::Microsoft::UI::Composition::CompositionColorBrush m_barIdleBrush{ nullptr }; - winrt::Microsoft::UI::Composition::CompositionColorBrush m_barLiveBrush{ nullptr }; - winrt::Microsoft::UI::Composition::CompositionEasingFunction m_ease{ nullptr }; - - // Timers - winrt::Microsoft::UI::Dispatching::DispatcherQueueTimer m_pollTimer{ nullptr }; - winrt::Microsoft::UI::Dispatching::DispatcherQueueTimer m_meterTimer{ nullptr }; - winrt::Microsoft::UI::Dispatching::DispatcherQueueTimer m_collapseTimer{ nullptr }; - - // Drag state - bool m_dragging{ false }; - winrt::Windows::Foundation::Point m_dragOrigin{}; - winrt::Windows::Foundation::Point m_windowOriginAtDragStart{}; - bool m_movedDuringPress{ false }; - - // Saving-state debouncer - std::chrono::steady_clock::time_point m_lastRecordingTrueTs{}; - }; -} - -namespace winrt::yip::factory_implementation -{ - struct IndicatorWindow : IndicatorWindowT {}; -} +namespace winrt::yip::implementation { +struct IndicatorWindow : IndicatorWindowT { + IndicatorWindow(); + ~IndicatorWindow(); + + // Event handlers (declared in XAML) + void OnPillPointerPressed(winrt::Windows::Foundation::IInspectable const& sender, + winrt::Microsoft::UI::Xaml::Input::PointerRoutedEventArgs const& args); + void OnPillPointerMoved(winrt::Windows::Foundation::IInspectable const& sender, + winrt::Microsoft::UI::Xaml::Input::PointerRoutedEventArgs const& args); + void OnPillPointerReleased(winrt::Windows::Foundation::IInspectable const& sender, + winrt::Microsoft::UI::Xaml::Input::PointerRoutedEventArgs const& args); + void OnPillPointerCaptureLost(winrt::Windows::Foundation::IInspectable const& sender, + winrt::Microsoft::UI::Xaml::Input::PointerRoutedEventArgs const& args); + void OnPillTapped(winrt::Windows::Foundation::IInspectable const& sender, + winrt::Microsoft::UI::Xaml::Input::TappedRoutedEventArgs const& args); + + void OnStopClicked(winrt::Windows::Foundation::IInspectable const& sender, + winrt::Microsoft::UI::Xaml::RoutedEventArgs const& args); + void OnMarkClicked(winrt::Windows::Foundation::IInspectable const& sender, + winrt::Microsoft::UI::Xaml::RoutedEventArgs const& args); + void OnOpenLastClicked(winrt::Windows::Foundation::IInspectable const& sender, + winrt::Microsoft::UI::Xaml::RoutedEventArgs const& args); + +private: + // ----- HWND helpers ----- + HWND Hwnd() const noexcept { return m_hwnd; } + void ApplyToolWindowStyle(); + void ApplyAlwaysOnTop(); + void ApplyClickThrough(bool enable); + + // ----- Composition layer ----- + void BuildCompositionLayer(); + void UpdateMeterBars(float peak); + void UpdateDotForState(::yip::IndicatorState s); + void StopMeterAnimations(); + + // ----- State machine ----- + void SyncFromAudioCore(); // polled by m_pollTimer + void TransitionTo(::yip::IndicatorState s, bool animate = true); + void AnimatePillToState(::yip::IndicatorState s); + + // ----- Edge dock / monitor restore ----- + void RestoreFromPersistence(); + void SnapToNearestEdgeIfClose(); + void RememberPosition(); + + // ----- Visibility ----- + void ShowWindow(); + void HideWindow(); + void SyncVisibilityForState(::yip::IndicatorState s); + + // ----- Auto-collapse ----- + void ResetAutoCollapseTimer(); + void StopAutoCollapseTimer(); + + // Field state + HWND m_hwnd{nullptr}; + ::yip::IndicatorState m_state{::yip::IndicatorState::Idle}; + ::yip::IndicatorState m_baseState{::yip::IndicatorState::Idle}; // state before expansion + ::yip::IndicatorPersistence m_persisted{}; + + // Composition + winrt::Microsoft::UI::Composition::Compositor m_compositor{nullptr}; + winrt::Microsoft::UI::Composition::ContainerVisual m_pillRoot{nullptr}; + winrt::Microsoft::UI::Composition::CompositionRoundedRectangleGeometry m_clipGeo{nullptr}; + winrt::Microsoft::UI::Composition::SpriteVisual m_dotVisual{nullptr}; + std::array m_barVisuals{}; + winrt::Microsoft::UI::Composition::CompositionColorBrush m_dotNeutralBrush{nullptr}; + winrt::Microsoft::UI::Composition::CompositionColorBrush m_dotRecordBrush{nullptr}; + winrt::Microsoft::UI::Composition::CompositionColorBrush m_barIdleBrush{nullptr}; + winrt::Microsoft::UI::Composition::CompositionColorBrush m_barLiveBrush{nullptr}; + winrt::Microsoft::UI::Composition::CompositionEasingFunction m_ease{nullptr}; + + // Timers + winrt::Microsoft::UI::Dispatching::DispatcherQueueTimer m_pollTimer{nullptr}; + winrt::Microsoft::UI::Dispatching::DispatcherQueueTimer m_meterTimer{nullptr}; + winrt::Microsoft::UI::Dispatching::DispatcherQueueTimer m_collapseTimer{nullptr}; + + // Drag state + bool m_dragging{false}; + winrt::Windows::Foundation::Point m_dragOrigin{}; + winrt::Windows::Foundation::Point m_windowOriginAtDragStart{}; + bool m_movedDuringPress{false}; + + // Saving-state debouncer + std::chrono::steady_clock::time_point m_lastRecordingTrueTs{}; +}; +} // namespace winrt::yip::implementation + +namespace winrt::yip::factory_implementation { +struct IndicatorWindow : IndicatorWindowT {}; +} // namespace winrt::yip::factory_implementation diff --git a/yip-app/MainWindow.xaml.cpp b/yip-app/MainWindow.xaml.cpp index 022fb88..70af31b 100644 --- a/yip-app/MainWindow.xaml.cpp +++ b/yip-app/MainWindow.xaml.cpp @@ -15,138 +15,125 @@ using namespace std::chrono_literals; namespace winrt { - using namespace winrt::Microsoft::UI::Xaml; - using namespace winrt::Microsoft::UI::Xaml::Controls; - using namespace winrt::Microsoft::UI::Dispatching; - using namespace winrt::Windows::Foundation; +using namespace winrt::Microsoft::UI::Xaml; +using namespace winrt::Microsoft::UI::Xaml::Controls; +using namespace winrt::Microsoft::UI::Dispatching; +using namespace winrt::Windows::Foundation; +} // namespace winrt + +namespace winrt::yip::implementation { +MainWindow::MainWindow() +{ + InitializeComponent(); + m_viewModel = winrt::make(); + m_viewModel.RefreshDevices(); + m_viewModel.RefreshRecordings(); + + Activated({this, &MainWindow::OnActivated}); + + // Live device updates via IMMNotificationClient. Callback fires on a + // WASAPI worker thread → marshal to UI dispatcher before touching VM. + auto dispatcher = winrt::Microsoft::UI::Dispatching::DispatcherQueue::GetForCurrentThread(); + m_deviceWatcher = std::make_unique<::yip::interop::DeviceWatcher>([weak = get_weak(), dispatcher]() { + dispatcher.TryEnqueue([weak]() { + if (auto self = weak.get()) { + if (self->m_viewModel) self->m_viewModel.RefreshDevices(); + } + }); + }); + + StartPolling(); } -namespace winrt::yip::implementation +MainWindow::~MainWindow() { - MainWindow::MainWindow() - { - InitializeComponent(); - m_viewModel = winrt::make(); - m_viewModel.RefreshDevices(); - m_viewModel.RefreshRecordings(); - - Activated({ this, &MainWindow::OnActivated }); - - // Live device updates via IMMNotificationClient. Callback fires on a - // WASAPI worker thread → marshal to UI dispatcher before touching VM. - auto dispatcher = winrt::Microsoft::UI::Dispatching::DispatcherQueue::GetForCurrentThread(); - m_deviceWatcher = std::make_unique<::yip::interop::DeviceWatcher>( - [weak = get_weak(), dispatcher]() { - dispatcher.TryEnqueue([weak]() { - if (auto self = weak.get()) { - if (self->m_viewModel) self->m_viewModel.RefreshDevices(); - } - }); - }); - - StartPolling(); + StopPolling(); + m_deviceWatcher.reset(); + if (rec_is_recording()) { + (void)rec_stop(); } +} - MainWindow::~MainWindow() - { - StopPolling(); - m_deviceWatcher.reset(); - if (rec_is_recording()) { - (void)rec_stop(); - } - } +winrt::yip::viewmodels::MainViewModel MainWindow::ViewModel() +{ + return m_viewModel; +} - winrt::yip::viewmodels::MainViewModel MainWindow::ViewModel() - { - return m_viewModel; +void MainWindow::OnActivated(winrt::Windows::Foundation::IInspectable const& /*sender*/, + winrt::Microsoft::UI::Xaml::WindowActivatedEventArgs const& args) +{ + const bool now_focused = + args.WindowActivationState() != winrt::Microsoft::UI::Xaml::WindowActivationState::Deactivated; + if (now_focused == m_focused) return; + m_focused = now_focused; + // Throttle meter poll: 60 Hz focused → 10 Hz blurred (spec). + if (m_pollTimer) { + m_pollTimer.Interval(m_focused ? std::chrono::milliseconds(16) : std::chrono::milliseconds(100)); } +} - void MainWindow::OnActivated( - winrt::Windows::Foundation::IInspectable const& /*sender*/, - winrt::Microsoft::UI::Xaml::WindowActivatedEventArgs const& args) - { - const bool now_focused = - args.WindowActivationState() != winrt::Microsoft::UI::Xaml::WindowActivationState::Deactivated; - if (now_focused == m_focused) return; - m_focused = now_focused; - // Throttle meter poll: 60 Hz focused → 10 Hz blurred (spec). - if (m_pollTimer) { - m_pollTimer.Interval(m_focused ? std::chrono::milliseconds(16) - : std::chrono::milliseconds(100)); +void MainWindow::StartPolling() +{ + auto queue = winrt::Microsoft::UI::Dispatching::DispatcherQueue::GetForCurrentThread(); + m_pollTimer = queue.CreateTimer(); + m_pollTimer.Interval(std::chrono::milliseconds(16)); + m_pollTimer.IsRepeating(true); + m_pollTimer.Tick([weak = get_weak()](auto&&, auto&&) { + if (auto self = weak.get()) { + if (self->m_viewModel) { + self->m_viewModel.PollPeak(); + } } - } + }); + m_pollTimer.Start(); +} - void MainWindow::StartPolling() - { - auto queue = winrt::Microsoft::UI::Dispatching::DispatcherQueue::GetForCurrentThread(); - m_pollTimer = queue.CreateTimer(); - m_pollTimer.Interval(std::chrono::milliseconds(16)); - m_pollTimer.IsRepeating(true); - m_pollTimer.Tick([weak = get_weak()](auto&&, auto&&) { - if (auto self = weak.get()) { - if (self->m_viewModel) { - self->m_viewModel.PollPeak(); - } - } - }); - m_pollTimer.Start(); +void MainWindow::StopPolling() +{ + if (m_pollTimer) { + m_pollTimer.Stop(); + m_pollTimer = nullptr; } +} - void MainWindow::StopPolling() - { - if (m_pollTimer) { - m_pollTimer.Stop(); - m_pollTimer = nullptr; - } - } +void MainWindow::OnRecordToggle(winrt::Windows::Foundation::IInspectable const& /*sender*/, + winrt::Microsoft::UI::Xaml::RoutedEventArgs const& /*args*/) +{ + m_viewModel.ToggleRecording(); +} - void MainWindow::OnRecordToggle( - winrt::Windows::Foundation::IInspectable const& /*sender*/, - winrt::Microsoft::UI::Xaml::RoutedEventArgs const& /*args*/) - { - m_viewModel.ToggleRecording(); - } +winrt::fire_and_forget MainWindow::OnOpenSettings(winrt::Windows::Foundation::IInspectable const& /*sender*/, + winrt::Microsoft::UI::Xaml::RoutedEventArgs const& /*args*/) +{ + auto strong = get_strong(); - winrt::fire_and_forget MainWindow::OnOpenSettings( - winrt::Windows::Foundation::IInspectable const& /*sender*/, - winrt::Microsoft::UI::Xaml::RoutedEventArgs const& /*args*/) - { - auto strong = get_strong(); - - auto dialog = winrt::make(); - dialog.OutputFolder(strong->m_viewModel.OutputFolder()); - dialog.SampleRate(strong->m_viewModel.SampleRate()); - dialog.Channels(strong->m_viewModel.Channels()); - - // ContentDialog needs an XamlRoot in WinAppSDK. - dialog.XamlRoot(strong->Content().XamlRoot()); - - const auto result = co_await dialog.ShowAsync(); - if (result == winrt::Microsoft::UI::Xaml::Controls::ContentDialogResult::Primary) - { - strong->m_viewModel.ApplySettings( - dialog.OutputFolder(), - dialog.SampleRate(), - dialog.Channels()); - } - co_return; - } + auto dialog = winrt::make(); + dialog.OutputFolder(strong->m_viewModel.OutputFolder()); + dialog.SampleRate(strong->m_viewModel.SampleRate()); + dialog.Channels(strong->m_viewModel.Channels()); + + // ContentDialog needs an XamlRoot in WinAppSDK. + dialog.XamlRoot(strong->Content().XamlRoot()); - void MainWindow::OnRefreshList( - winrt::Windows::Foundation::IInspectable const& /*sender*/, - winrt::Microsoft::UI::Xaml::RoutedEventArgs const& /*args*/) - { - m_viewModel.RefreshDevices(); - m_viewModel.RefreshRecordings(); + const auto result = co_await dialog.ShowAsync(); + if (result == winrt::Microsoft::UI::Xaml::Controls::ContentDialogResult::Primary) { + strong->m_viewModel.ApplySettings(dialog.OutputFolder(), dialog.SampleRate(), dialog.Channels()); } + co_return; +} - void MainWindow::OnRecordingClicked( - winrt::Windows::Foundation::IInspectable const& /*sender*/, - winrt::Microsoft::UI::Xaml::Controls::ItemClickEventArgs const& args) - { - if (auto entry = args.ClickedItem().try_as()) - { - m_viewModel.RevealRecording(entry); - } +void MainWindow::OnRefreshList(winrt::Windows::Foundation::IInspectable const& /*sender*/, + winrt::Microsoft::UI::Xaml::RoutedEventArgs const& /*args*/) +{ + m_viewModel.RefreshDevices(); + m_viewModel.RefreshRecordings(); +} + +void MainWindow::OnRecordingClicked(winrt::Windows::Foundation::IInspectable const& /*sender*/, + winrt::Microsoft::UI::Xaml::Controls::ItemClickEventArgs const& args) +{ + if (auto entry = args.ClickedItem().try_as()) { + m_viewModel.RevealRecording(entry); } } +} // namespace winrt::yip::implementation diff --git a/yip-app/MainWindow.xaml.h b/yip-app/MainWindow.xaml.h index 26142e0..e477445 100644 --- a/yip-app/MainWindow.xaml.h +++ b/yip-app/MainWindow.xaml.h @@ -5,43 +5,39 @@ #include -namespace yip::interop { class DeviceWatcher; } - -namespace winrt::yip::implementation -{ - struct MainWindow : MainWindowT - { - MainWindow(); - ~MainWindow(); - - winrt::yip::viewmodels::MainViewModel ViewModel(); - - void OnRecordToggle(winrt::Windows::Foundation::IInspectable const& sender, - winrt::Microsoft::UI::Xaml::RoutedEventArgs const& args); - winrt::fire_and_forget OnOpenSettings( - winrt::Windows::Foundation::IInspectable const& sender, - winrt::Microsoft::UI::Xaml::RoutedEventArgs const& args); - void OnRefreshList(winrt::Windows::Foundation::IInspectable const& sender, - winrt::Microsoft::UI::Xaml::RoutedEventArgs const& args); - void OnRecordingClicked(winrt::Windows::Foundation::IInspectable const& sender, - winrt::Microsoft::UI::Xaml::Controls::ItemClickEventArgs const& args); - - private: - winrt::yip::viewmodels::MainViewModel m_viewModel{ nullptr }; - winrt::Microsoft::UI::Dispatching::DispatcherQueueTimer m_pollTimer{ nullptr }; - std::unique_ptr<::yip::interop::DeviceWatcher> m_deviceWatcher; - bool m_focused{ true }; - - void StartPolling(); - void StopPolling(); - void OnActivated(winrt::Windows::Foundation::IInspectable const& sender, - winrt::Microsoft::UI::Xaml::WindowActivatedEventArgs const& args); - }; +namespace yip::interop { +class DeviceWatcher; } -namespace winrt::yip::factory_implementation -{ - struct MainWindow : MainWindowT - { - }; -} +namespace winrt::yip::implementation { +struct MainWindow : MainWindowT { + MainWindow(); + ~MainWindow(); + + winrt::yip::viewmodels::MainViewModel ViewModel(); + + void OnRecordToggle(winrt::Windows::Foundation::IInspectable const& sender, + winrt::Microsoft::UI::Xaml::RoutedEventArgs const& args); + winrt::fire_and_forget OnOpenSettings(winrt::Windows::Foundation::IInspectable const& sender, + winrt::Microsoft::UI::Xaml::RoutedEventArgs const& args); + void OnRefreshList(winrt::Windows::Foundation::IInspectable const& sender, + winrt::Microsoft::UI::Xaml::RoutedEventArgs const& args); + void OnRecordingClicked(winrt::Windows::Foundation::IInspectable const& sender, + winrt::Microsoft::UI::Xaml::Controls::ItemClickEventArgs const& args); + +private: + winrt::yip::viewmodels::MainViewModel m_viewModel{nullptr}; + winrt::Microsoft::UI::Dispatching::DispatcherQueueTimer m_pollTimer{nullptr}; + std::unique_ptr<::yip::interop::DeviceWatcher> m_deviceWatcher; + bool m_focused{true}; + + void StartPolling(); + void StopPolling(); + void OnActivated(winrt::Windows::Foundation::IInspectable const& sender, + winrt::Microsoft::UI::Xaml::WindowActivatedEventArgs const& args); +}; +} // namespace winrt::yip::implementation + +namespace winrt::yip::factory_implementation { +struct MainWindow : MainWindowT {}; +} // namespace winrt::yip::factory_implementation diff --git a/yip-app/Markers.cpp b/yip-app/Markers.cpp index 527763e..da9e059 100644 --- a/yip-app/Markers.cpp +++ b/yip-app/Markers.cpp @@ -6,68 +6,66 @@ #include #include -namespace fs = std::filesystem; +namespace fs = std::filesystem; namespace wdj = winrt::Windows::Data::Json; -namespace yip::markers +namespace yip::markers { +fs::path SidecarFor(const fs::path& wav_path) { - fs::path SidecarFor(const fs::path& wav_path) - { - auto p = wav_path; - p += L".markers.json"; - // Convention: keep the .wav extension visible so collation is obvious: - // yip-20260521-103200.wav - // yip-20260521-103200.wav.markers.json - return p; - } + auto p = wav_path; + p += L".markers.json"; + // Convention: keep the .wav extension visible so collation is obvious: + // yip-20260521-103200.wav + // yip-20260521-103200.wav.markers.json + return p; +} - bool Append(const fs::path& wav_path, const Marker& m) - { - const auto path = SidecarFor(wav_path); +bool Append(const fs::path& wav_path, const Marker& m) +{ + const auto path = SidecarFor(wav_path); - wdj::JsonObject root; - wdj::JsonArray arr; + wdj::JsonObject root; + wdj::JsonArray arr; - std::ifstream in(path, std::ios::binary); - if (in) { - std::stringstream buf; - buf << in.rdbuf(); - const auto u8 = buf.str(); - if (!u8.empty()) { - const auto wide = winrt::to_hstring(u8); - wdj::JsonObject parsed{ nullptr }; - if (wdj::JsonObject::TryParse(wide, parsed)) { - root = parsed; - if (parsed.HasKey(L"markers")) { - arr = parsed.GetNamedArray(L"markers"); - } + std::ifstream in(path, std::ios::binary); + if (in) { + std::stringstream buf; + buf << in.rdbuf(); + const auto u8 = buf.str(); + if (!u8.empty()) { + const auto wide = winrt::to_hstring(u8); + wdj::JsonObject parsed{nullptr}; + if (wdj::JsonObject::TryParse(wide, parsed)) { + root = parsed; + if (parsed.HasKey(L"markers")) { + arr = parsed.GetNamedArray(L"markers"); } } } - if (!arr) arr = wdj::JsonArray{}; + } + if (!arr) arr = wdj::JsonArray{}; - wdj::JsonObject entry; - entry.SetNamedValue(L"t_ms", wdj::JsonValue::CreateNumberValue(static_cast(m.t_ms))); - if (m.label) { - entry.SetNamedValue(L"label", wdj::JsonValue::CreateStringValue(*m.label)); - } else { - entry.SetNamedValue(L"label", wdj::JsonValue::CreateNullValue()); - } - arr.Append(entry); - root.SetNamedValue(L"markers", arr); + wdj::JsonObject entry; + entry.SetNamedValue(L"t_ms", wdj::JsonValue::CreateNumberValue(static_cast(m.t_ms))); + if (m.label) { + entry.SetNamedValue(L"label", wdj::JsonValue::CreateStringValue(*m.label)); + } else { + entry.SetNamedValue(L"label", wdj::JsonValue::CreateNullValue()); + } + arr.Append(entry); + root.SetNamedValue(L"markers", arr); - std::error_code ec; - fs::create_directories(path.parent_path(), ec); + std::error_code ec; + fs::create_directories(path.parent_path(), ec); - const auto tmp = path.wstring() + L".tmp"; - const auto u8 = winrt::to_string(root.Stringify()); - { - std::ofstream out(tmp, std::ios::binary | std::ios::trunc); - if (!out) return false; - out.write(u8.data(), static_cast(u8.size())); - if (!out) return false; - } - return ::MoveFileExW(tmp.c_str(), path.c_str(), - MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) != 0; + const auto tmp = path.wstring() + L".tmp"; + const auto u8 = winrt::to_string(root.Stringify()); + { + std::ofstream out(tmp, std::ios::binary | std::ios::trunc); + if (!out) return false; + out.write(u8.data(), static_cast(u8.size())); + if (!out) return false; } + return ::MoveFileExW(tmp.c_str(), path.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) != 0; } +} // namespace yip::markers diff --git a/yip-app/Markers.h b/yip-app/Markers.h index c2f322d..fdb1fc5 100644 --- a/yip-app/Markers.h +++ b/yip-app/Markers.h @@ -10,18 +10,16 @@ #include #include -namespace yip::markers -{ - struct Marker - { - uint64_t t_ms{ 0 }; - std::optional label; - }; +namespace yip::markers { +struct Marker { + uint64_t t_ms{0}; + std::optional label; +}; - // Sidecar path for a WAV at `wav_path`. - std::filesystem::path SidecarFor(const std::filesystem::path& wav_path); +// Sidecar path for a WAV at `wav_path`. +std::filesystem::path SidecarFor(const std::filesystem::path& wav_path); - // Append a marker. Creates the sidecar if missing; preserves any existing - // markers + unknown top-level keys. Returns false on filesystem error. - bool Append(const std::filesystem::path& wav_path, const Marker& m); -} +// Append a marker. Creates the sidecar if missing; preserves any existing +// markers + unknown top-level keys. Returns false on filesystem error. +bool Append(const std::filesystem::path& wav_path, const Marker& m); +} // namespace yip::markers diff --git a/yip-app/Settings.cpp b/yip-app/Settings.cpp index 86d83b5..4609c5f 100644 --- a/yip-app/Settings.cpp +++ b/yip-app/Settings.cpp @@ -10,116 +10,109 @@ namespace fs = std::filesystem; namespace wdj = winrt::Windows::Data::Json; -namespace +namespace { +fs::path LocalAppData() { - fs::path LocalAppData() - { - wchar_t* base = nullptr; - if (SUCCEEDED(::SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, nullptr, &base)) && base) { - fs::path p(base); - ::CoTaskMemFree(base); - return p / L"Yip"; - } - return fs::current_path() / L"yip-data"; + wchar_t* base = nullptr; + if (SUCCEEDED(::SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, nullptr, &base)) && base) { + fs::path p(base); + ::CoTaskMemFree(base); + return p / L"Yip"; } + return fs::current_path() / L"yip-data"; +} - fs::path DefaultOutputFolder() - { - wchar_t* base = nullptr; - if (SUCCEEDED(::SHGetKnownFolderPath(FOLDERID_Music, 0, nullptr, &base)) && base) { - fs::path p(base); - ::CoTaskMemFree(base); - return p / L"Yip"; - } - return fs::current_path() / L"Recordings"; +fs::path DefaultOutputFolder() +{ + wchar_t* base = nullptr; + if (SUCCEEDED(::SHGetKnownFolderPath(FOLDERID_Music, 0, nullptr, &base)) && base) { + fs::path p(base); + ::CoTaskMemFree(base); + return p / L"Yip"; } + return fs::current_path() / L"Recordings"; } +} // namespace -namespace yip +namespace yip { +fs::path Settings::SettingsPath() { - fs::path Settings::SettingsPath() - { - const auto root = LocalAppData(); - std::error_code ec; - fs::create_directories(root, ec); - return root / L"settings.json"; - } + const auto root = LocalAppData(); + std::error_code ec; + fs::create_directories(root, ec); + return root / L"settings.json"; +} - Settings Settings::Defaults() - { - Settings s; - s.output_folder = DefaultOutputFolder(); - s.sample_rate = 48000; - s.channels = 2; - s.format = 0; - return s; - } +Settings Settings::Defaults() +{ + Settings s; + s.output_folder = DefaultOutputFolder(); + s.sample_rate = 48000; + s.channels = 2; + s.format = 0; + return s; +} - Settings Settings::Load() - { - const auto path = SettingsPath(); - std::ifstream in(path, std::ios::binary); - if (!in) return Defaults(); - - std::stringstream buf; - buf << in.rdbuf(); - const auto u8 = buf.str(); - if (u8.empty()) return Defaults(); - - const auto wide = winrt::to_hstring(u8); - - wdj::JsonObject obj{ nullptr }; - if (!wdj::JsonObject::TryParse(wide, obj)) return Defaults(); - - Settings s = Defaults(); - if (obj.HasKey(L"output_folder")) { - const auto folder = obj.GetNamedString(L"output_folder", L""); - if (!folder.empty()) s.output_folder = std::wstring{ folder }; - } - if (obj.HasKey(L"sample_rate")) { - s.sample_rate = static_cast(obj.GetNamedNumber(L"sample_rate", 48000.0)); - } - if (obj.HasKey(L"channels")) { - s.channels = static_cast(obj.GetNamedNumber(L"channels", 2.0)); - } - if (obj.HasKey(L"format")) { - s.format = static_cast(obj.GetNamedNumber(L"format", 0.0)); - } - return s; +Settings Settings::Load() +{ + const auto path = SettingsPath(); + std::ifstream in(path, std::ios::binary); + if (!in) return Defaults(); + + std::stringstream buf; + buf << in.rdbuf(); + const auto u8 = buf.str(); + if (u8.empty()) return Defaults(); + + const auto wide = winrt::to_hstring(u8); + + wdj::JsonObject obj{nullptr}; + if (!wdj::JsonObject::TryParse(wide, obj)) return Defaults(); + + Settings s = Defaults(); + if (obj.HasKey(L"output_folder")) { + const auto folder = obj.GetNamedString(L"output_folder", L""); + if (!folder.empty()) s.output_folder = std::wstring{folder}; + } + if (obj.HasKey(L"sample_rate")) { + s.sample_rate = static_cast(obj.GetNamedNumber(L"sample_rate", 48000.0)); + } + if (obj.HasKey(L"channels")) { + s.channels = static_cast(obj.GetNamedNumber(L"channels", 2.0)); + } + if (obj.HasKey(L"format")) { + s.format = static_cast(obj.GetNamedNumber(L"format", 0.0)); } + return s; +} + +bool Settings::Save() const +{ + wdj::JsonObject obj; + obj.SetNamedValue(L"output_folder", wdj::JsonValue::CreateStringValue(output_folder.wstring())); + obj.SetNamedValue(L"sample_rate", wdj::JsonValue::CreateNumberValue(static_cast(sample_rate))); + obj.SetNamedValue(L"channels", wdj::JsonValue::CreateNumberValue(static_cast(channels))); + obj.SetNamedValue(L"format", wdj::JsonValue::CreateNumberValue(static_cast(format))); + + const auto path = SettingsPath(); + const auto tmp = path; + std::error_code ec; + fs::create_directories(path.parent_path(), ec); + + const auto wide = obj.Stringify(); + const auto u8 = winrt::to_string(wide); + const auto tmpPath = path.wstring() + L".tmp"; - bool Settings::Save() const { - wdj::JsonObject obj; - obj.SetNamedValue(L"output_folder", - wdj::JsonValue::CreateStringValue(output_folder.wstring())); - obj.SetNamedValue(L"sample_rate", - wdj::JsonValue::CreateNumberValue(static_cast(sample_rate))); - obj.SetNamedValue(L"channels", - wdj::JsonValue::CreateNumberValue(static_cast(channels))); - obj.SetNamedValue(L"format", - wdj::JsonValue::CreateNumberValue(static_cast(format))); - - const auto path = SettingsPath(); - const auto tmp = path; - std::error_code ec; - fs::create_directories(path.parent_path(), ec); - - const auto wide = obj.Stringify(); - const auto u8 = winrt::to_string(wide); - const auto tmpPath = path.wstring() + L".tmp"; - - { - std::ofstream out(tmpPath, std::ios::binary | std::ios::trunc); - if (!out) return false; - out.write(u8.data(), static_cast(u8.size())); - if (!out) return false; - } - // ReplaceFile-style atomic rename. Falls back to remove+rename. - if (!::MoveFileExW(tmpPath.c_str(), path.c_str(), - MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) { - return false; - } - return true; + std::ofstream out(tmpPath, std::ios::binary | std::ios::trunc); + if (!out) return false; + out.write(u8.data(), static_cast(u8.size())); + if (!out) return false; + } + // ReplaceFile-style atomic rename. Falls back to remove+rename. + if (!::MoveFileExW(tmpPath.c_str(), path.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) { + return false; } + return true; } +} // namespace yip diff --git a/yip-app/Settings.h b/yip-app/Settings.h index 35d0c37..00ffaf4 100644 --- a/yip-app/Settings.h +++ b/yip-app/Settings.h @@ -7,28 +7,26 @@ #include #include -namespace yip -{ - struct Settings - { - std::filesystem::path output_folder; - uint32_t sample_rate{ 48000 }; - uint16_t channels{ 2 }; - - // 0 = PCM float32 (only format in v1; reserved for future expansion). - uint16_t format{ 0 }; - - // Returns the path to the settings file, creating parent dirs. - static std::filesystem::path SettingsPath(); - - // Default-construct a settings object pointing at the user's - // Music\Yip folder. - static Settings Defaults(); - - // Best-effort load. Falls back to Defaults() on any error. - static Settings Load(); - - // Atomic save (write tmp + rename). Returns false on failure. - bool Save() const; - }; -} +namespace yip { +struct Settings { + std::filesystem::path output_folder; + uint32_t sample_rate{48000}; + uint16_t channels{2}; + + // 0 = PCM float32 (only format in v1; reserved for future expansion). + uint16_t format{0}; + + // Returns the path to the settings file, creating parent dirs. + static std::filesystem::path SettingsPath(); + + // Default-construct a settings object pointing at the user's + // Music\Yip folder. + static Settings Defaults(); + + // Best-effort load. Falls back to Defaults() on any error. + static Settings Load(); + + // Atomic save (write tmp + rename). Returns false on failure. + bool Save() const; +}; +} // namespace yip diff --git a/yip-app/SettingsDialog.xaml.cpp b/yip-app/SettingsDialog.xaml.cpp index 0e4bbf3..fd393cb 100644 --- a/yip-app/SettingsDialog.xaml.cpp +++ b/yip-app/SettingsDialog.xaml.cpp @@ -15,24 +15,27 @@ using namespace winrt; using namespace winrt::Microsoft::UI::Xaml::Controls; -namespace +namespace { +HWND TryFindForegroundHwnd() { - HWND TryFindForegroundHwnd() - { - // ContentDialog runs against the active XamlRoot, but FolderPicker - // needs the *Win32 owner* HWND for COM init. The dialog's XamlRoot - // doesn't directly expose it — use the foreground top-level window - // belonging to this process as a pragmatic anchor. - HWND fg = ::GetForegroundWindow(); - DWORD pid = 0; - ::GetWindowThreadProcessId(fg, &pid); - if (pid == ::GetCurrentProcessId()) return fg; - - // Fall back to enumerating top-level windows of this process. - HWND result = nullptr; - struct Ctx { DWORD pid; HWND* out; }; - Ctx ctx{ ::GetCurrentProcessId(), &result }; - ::EnumWindows([](HWND hwnd, LPARAM lparam) -> BOOL { + // ContentDialog runs against the active XamlRoot, but FolderPicker + // needs the *Win32 owner* HWND for COM init. The dialog's XamlRoot + // doesn't directly expose it — use the foreground top-level window + // belonging to this process as a pragmatic anchor. + HWND fg = ::GetForegroundWindow(); + DWORD pid = 0; + ::GetWindowThreadProcessId(fg, &pid); + if (pid == ::GetCurrentProcessId()) return fg; + + // Fall back to enumerating top-level windows of this process. + HWND result = nullptr; + struct Ctx { + DWORD pid; + HWND* out; + }; + Ctx ctx{::GetCurrentProcessId(), &result}; + ::EnumWindows( + [](HWND hwnd, LPARAM lparam) -> BOOL { auto* c = reinterpret_cast(lparam); DWORD wpid = 0; ::GetWindowThreadProcessId(hwnd, &wpid); @@ -41,104 +44,134 @@ namespace return FALSE; } return TRUE; - }, reinterpret_cast(&ctx)); - return result; - } + }, + reinterpret_cast(&ctx)); + return result; } +} // namespace -namespace winrt::yip::implementation +namespace winrt::yip::implementation { +SettingsDialog::SettingsDialog() { - SettingsDialog::SettingsDialog() - { - InitializeComponent(); - ApplyToControls(); - } + InitializeComponent(); + ApplyToControls(); +} - winrt::hstring SettingsDialog::OutputFolder() const noexcept { ReadFromControls(); return m_outputFolder; } - void SettingsDialog::OutputFolder(winrt::hstring const& v) - { - m_outputFolder = v; - ApplyToControls(); - } +winrt::hstring SettingsDialog::OutputFolder() const noexcept +{ + ReadFromControls(); + return m_outputFolder; +} +void SettingsDialog::OutputFolder(winrt::hstring const& v) +{ + m_outputFolder = v; + ApplyToControls(); +} - uint32_t SettingsDialog::SampleRate() const noexcept { ReadFromControls(); return m_sampleRate; } - void SettingsDialog::SampleRate(uint32_t v) - { - m_sampleRate = v; - ApplyToControls(); - } +uint32_t SettingsDialog::SampleRate() const noexcept +{ + ReadFromControls(); + return m_sampleRate; +} +void SettingsDialog::SampleRate(uint32_t v) +{ + m_sampleRate = v; + ApplyToControls(); +} - uint16_t SettingsDialog::Channels() const noexcept { ReadFromControls(); return m_channels; } - void SettingsDialog::Channels(uint16_t v) - { - m_channels = v; - ApplyToControls(); - } +uint16_t SettingsDialog::Channels() const noexcept +{ + ReadFromControls(); + return m_channels; +} +void SettingsDialog::Channels(uint16_t v) +{ + m_channels = v; + ApplyToControls(); +} - void SettingsDialog::ApplyToControls() - { - if (FolderBox()) { - FolderBox().Text(m_outputFolder); - } - if (SampleRateCombo()) { - int idx = 1; // default 48000 - switch (m_sampleRate) { - case 44100: idx = 0; break; - case 48000: idx = 1; break; - case 88200: idx = 2; break; - case 96000: idx = 3; break; - default: break; - } - SampleRateCombo().SelectedIndex(idx); - } - if (ChannelsCombo()) { - ChannelsCombo().SelectedIndex(m_channels == 1 ? 0 : 1); +void SettingsDialog::ApplyToControls() +{ + if (FolderBox()) { + FolderBox().Text(m_outputFolder); + } + if (SampleRateCombo()) { + int idx = 1; // default 48000 + switch (m_sampleRate) { + case 44100: + idx = 0; + break; + case 48000: + idx = 1; + break; + case 88200: + idx = 2; + break; + case 96000: + idx = 3; + break; + default: + break; } + SampleRateCombo().SelectedIndex(idx); + } + if (ChannelsCombo()) { + ChannelsCombo().SelectedIndex(m_channels == 1 ? 0 : 1); } +} - void SettingsDialog::ReadFromControls() const - { - // SettingsDialog is the implementation type; cast away const for the - // mutable cached fields below. - auto* self = const_cast(this); - if (FolderBox()) { - self->m_outputFolder = FolderBox().Text(); - } - if (SampleRateCombo()) { - switch (SampleRateCombo().SelectedIndex()) { - case 0: self->m_sampleRate = 44100; break; - case 1: self->m_sampleRate = 48000; break; - case 2: self->m_sampleRate = 88200; break; - case 3: self->m_sampleRate = 96000; break; - default: break; - } - } - if (ChannelsCombo()) { - self->m_channels = (ChannelsCombo().SelectedIndex() == 0) ? 1 : 2; +void SettingsDialog::ReadFromControls() const +{ + // SettingsDialog is the implementation type; cast away const for the + // mutable cached fields below. + auto* self = const_cast(this); + if (FolderBox()) { + self->m_outputFolder = FolderBox().Text(); + } + if (SampleRateCombo()) { + switch (SampleRateCombo().SelectedIndex()) { + case 0: + self->m_sampleRate = 44100; + break; + case 1: + self->m_sampleRate = 48000; + break; + case 2: + self->m_sampleRate = 88200; + break; + case 3: + self->m_sampleRate = 96000; + break; + default: + break; } } + if (ChannelsCombo()) { + self->m_channels = (ChannelsCombo().SelectedIndex() == 0) ? 1 : 2; + } +} - winrt::fire_and_forget SettingsDialog::OnPickFolder( - winrt::Windows::Foundation::IInspectable const& /*sender*/, - winrt::Microsoft::UI::Xaml::RoutedEventArgs const& /*args*/) - { - auto strong = get_strong(); - - winrt::Windows::Storage::Pickers::FolderPicker picker; - picker.SuggestedStartLocation(winrt::Windows::Storage::Pickers::PickerLocationId::MusicLibrary); - picker.FileTypeFilter().Append(L"*"); - - // Unpackaged WinUI 3: pickers need an HWND init via IInitializeWithWindow. - HWND owner = TryFindForegroundHwnd(); - if (owner) { - auto init = picker.as<::IInitializeWithWindow>(); - init->Initialize(owner); - } +winrt::fire_and_forget SettingsDialog::OnPickFolder( + winrt::Windows::Foundation::IInspectable const& /*sender*/, + winrt::Microsoft::UI::Xaml::RoutedEventArgs const& /*args*/) +{ + auto strong = get_strong(); - auto folder = co_await picker.PickSingleFolderAsync(); - if (folder) { - strong->FolderBox().Text(folder.Path()); - } - co_return; + winrt::Windows::Storage::Pickers::FolderPicker picker; + picker.SuggestedStartLocation(winrt::Windows::Storage::Pickers::PickerLocationId::MusicLibrary); + picker.FileTypeFilter().Append(L"*"); + + // Unpackaged WinUI 3: pickers need an HWND init via IInitializeWithWindow. + HWND owner = TryFindForegroundHwnd(); + if (owner) { + auto init = picker.as<::IInitializeWithWindow>(); + init->Initialize(owner); + } + + auto folder = co_await picker.PickSingleFolderAsync(); + if (folder) { + strong->FolderBox().Text(folder.Path()); } + co_return; } +} // namespace winrt::yip::implementation diff --git a/yip-app/main.cpp b/yip-app/main.cpp index 950a8ba..5b6fb9d 100644 --- a/yip-app/main.cpp +++ b/yip-app/main.cpp @@ -5,25 +5,21 @@ #include "App.xaml.h" namespace winrt { - using namespace winrt::Microsoft::UI::Xaml; +using namespace winrt::Microsoft::UI::Xaml; } -int APIENTRY wWinMain( - _In_ HINSTANCE /*hInstance*/, - _In_opt_ HINSTANCE /*hPrevInstance*/, - _In_ LPWSTR /*lpCmdLine*/, - _In_ int /*nCmdShow*/) { +int APIENTRY wWinMain(_In_ HINSTANCE /*hInstance*/, _In_opt_ HINSTANCE /*hPrevInstance*/, + _In_ LPWSTR /*lpCmdLine*/, _In_ int /*nCmdShow*/) +{ // Locate the matching Windows App SDK runtime (1.6.x). Required for // unpackaged apps so framework DLLs resolve without an MSIX identity. constexpr PCWSTR kVersionTag = L""; constexpr PACKAGE_VERSION kMinVersion{}; - const HRESULT bootstrapHr = MddBootstrapInitialize2( - WINDOWSAPPSDK_RELEASE_MAJORMINOR, - kVersionTag, - kMinVersion, - MddBootstrapInitializeOptions_OnNoMatch_ShowUI | - MddBootstrapInitializeOptions_OnPackageIdentity_NOOP); + const HRESULT bootstrapHr = + MddBootstrapInitialize2(WINDOWSAPPSDK_RELEASE_MAJORMINOR, kVersionTag, kMinVersion, + MddBootstrapInitializeOptions_OnNoMatch_ShowUI | + MddBootstrapInitializeOptions_OnPackageIdentity_NOOP); if (FAILED(bootstrapHr)) { OutputDebugStringW(L"Yip: MddBootstrapInitialize2 failed.\n"); return bootstrapHr; @@ -31,18 +27,15 @@ int APIENTRY wWinMain( // Smoke-tests: confirm both native libs are linked and reachable. const int32_t audioProbe = rec_dummy(); - const int32_t vstProbe = yip_vst_dummy(); + const int32_t vstProbe = yip_vst_dummy(); wchar_t buf[128]; - swprintf_s(buf, L"Yip: audio_core dummy=%d, vst_host dummy=%d\n", - audioProbe, vstProbe); + swprintf_s(buf, L"Yip: audio_core dummy=%d, vst_host dummy=%d\n", audioProbe, vstProbe); OutputDebugStringW(buf); { winrt::init_apartment(winrt::apartment_type::single_threaded); winrt::Microsoft::UI::Xaml::Application::Start( - [](auto&&) { - winrt::make<::winrt::yip::implementation::App>(); - }); + [](auto&&) { winrt::make<::winrt::yip::implementation::App>(); }); } MddBootstrapShutdown();