mirror of
https://github.com/baketnk/frame-yap.git
synced 2026-10-06 03:00:04 +02:00
Tighten review panel; add battery and dashboard indicators; Type sends Enter when idle
- Drop the fixed 'Type adds a space' hint; a Bindings failure note now replaces the page count instead. - Grow the transcript card to six lines, closing the empty band above the footer; move pagination down. - Header: right-aligned clock/date, L/HMD/R battery readouts from OpenVR battery properties (headset falls back to the Linux system battery); missing readings are hidden. - Status line chip shows Buttons ready/paused from IsDashboardVisible(). - Type (A or button) with nothing to review queues Enter alone; the button is labelled Enter in that state. Offline and native x86-64 builds pass; not yet deployed to Frame. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
59f4f9cff8
commit
4c13b045e2
9 files changed
+179
-35
No files matched your search
@@ -27,7 +27,7 @@ sh install.sh --mode binary --archive /path/to/frameyap-VERSION-linux-aarch64.ta
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Right X, hold / release | Record while held; release to transcribe. |
|
| Right X, hold / release | Record while held; release to transcribe. |
|
||||||
| Right B | Cancel/discard, or close the Quick phrases picker. |
|
| Right B | Cancel/discard, or close the Quick phrases picker. |
|
||||||
| Right A | **Type:** queue reviewed text, normally with a trailing space. |
|
| Right A | **Type:** queue reviewed text, normally with a trailing space. With nothing to review, a press queues Enter alone (so a quick double press types then submits). |
|
||||||
| Right Y | Open **Quick phrases**; press again to cycle the selection. |
|
| Right Y | Open **Quick phrases**; press again to cycle the selection. |
|
||||||
| Left grip, double-tap | **Type + Enter:** queue the selected phrase verbatim + Enter, pending review (normally + space) then Enter, or Enter alone if neither exists. |
|
| Left grip, double-tap | **Type + Enter:** queue the selected phrase verbatim + Enter, pending review (normally + space) then Enter, or Enter alone if neither exists. |
|
||||||
| Overlay Record / Stop | Click-to-start/stop alternative to the PTT binding. |
|
| Overlay Record / Stop | Click-to-start/stop alternative to the PTT binding. |
|
||||||
|
|||||||
+23
-7
@@ -54,19 +54,33 @@ GPU setup/submission errors stop startup or the run with an explicit error.
|
|||||||
This replaces the raw-upload rendering path; headset flicker acceptance still
|
This replaces the raw-upload rendering path; headset flicker acceptance still
|
||||||
requires an on-device comparison. Native installation and offscreen GPU checks are separate from headset acceptance.
|
requires an on-device comparison. Native installation and offscreen GPU checks are separate from headset acceptance.
|
||||||
|
|
||||||
The header shows local time and date instead of the former on-device/review
|
The header shows local time and date (right-aligned) instead of the former
|
||||||
and current-mount labels. Settings explains Hold Quit (hold 0.9 seconds then
|
on-device/review and current-mount labels. Between the title and the clock it
|
||||||
release) and Lasers anytime (system-wide lasers may affect games). The review
|
shows battery levels for the left controller, headset (HMD) and right controller.
|
||||||
tab describes Type and Type + Enter. It updates when the displayed minute or date changes,
|
Controllers and the headset use OpenVR's `Prop_DeviceBatteryPercentage_Float`
|
||||||
|
(with `Prop_DeviceIsCharging_Bool`) when the device reports
|
||||||
|
`Prop_DeviceProvidesBatteryStatus_Bool`; if the headset reports none, the local
|
||||||
|
Linux `/sys/class/power_supply` system battery is read instead (peripheral
|
||||||
|
`scope=Device` supplies are skipped). Levels refresh every five seconds; a device
|
||||||
|
without a reading is hidden, never shown as zero. 20% or less is drawn in the
|
||||||
|
warning color, charging in the accent color with `+`. The status line carries a
|
||||||
|
**Buttons ready / Buttons paused** chip from `IVROverlay::IsDashboardVisible()`:
|
||||||
|
while the SteamVR dashboard is open, controller bindings do not reach FrameYap,
|
||||||
|
though pointer clicks still work. The chip reflects dashboard visibility only,
|
||||||
|
not every system or game input override. `--check-controls` prints the same
|
||||||
|
battery readings. Which devices Frame actually reports still needs a headset check.
|
||||||
|
Settings explains Hold Quit (hold 0.9 seconds then
|
||||||
|
release) and Lasers anytime (system-wide lasers may affect games). The header
|
||||||
|
updates when the displayed minute, date, battery or dashboard state changes,
|
||||||
not every frame. Settings toggles 12/24-hour time and cycles date Off →
|
not every frame. Settings toggles 12/24-hour time and cycles date Off →
|
||||||
MM/DD/YYYY → DD/MM/YYYY → YYYY-MM-DD → Off. These only affect display;
|
MM/DD/YYYY → DD/MM/YYYY → YYYY-MM-DD → Off. These only affect display;
|
||||||
mount choices remain in Settings.
|
mount choices remain in Settings.
|
||||||
|
|
||||||
The complete transcript preview is paginated by glyph width and four-line
|
The complete transcript preview is paginated by glyph width and six-line
|
||||||
height; Previous and Next navigate it without changing the source transcript.
|
height; Previous and Next navigate it without changing the source transcript.
|
||||||
Status fits on the single status line; the old bottom detail label is gone.
|
Status fits on the single status line; the old bottom detail label is gone.
|
||||||
The footer remains available on all tabs: Record (labelled Stop while recording),
|
The footer remains available on all tabs: Record (labelled Stop while recording),
|
||||||
Cancel, Type, Type + Enter, Hold Quit. Hold Quit needs a 900 ms press and release on
|
Cancel, Type (labelled Enter when nothing is pending review), Type + Enter, Hold Quit. Hold Quit needs a 900 ms press and release on
|
||||||
that same button; its thin progress bar shows the hold. Record can retry after an
|
that same button; its thin progress bar shows the hold. Record can retry after an
|
||||||
error; it is disabled while warming/transcribing and until an existing review is
|
error; it is disabled while warming/transcribing and until an existing review is
|
||||||
typed or discarded. Cancel can stop worker startup. Type and Type + Enter are
|
typed or discarded. Cancel can stop worker startup. Type and Type + Enter are
|
||||||
@@ -76,7 +90,9 @@ tab changes, action-state changes and relocation clear pending presses. Type + E
|
|||||||
is *always* a separate deliberate action, not inferred from text. Type normally
|
is *always* a separate deliberate action, not inferred from text. Type normally
|
||||||
appends a trailing space (without doubling an existing one); a full 4096-byte
|
appends a trailing space (without doubling an existing one); a full 4096-byte
|
||||||
transcript without room for that suffix is queued unchanged, with no extra error
|
transcript without room for that suffix is queued unchanged, with no extra error
|
||||||
for the missing space. Type + Enter types any pending review and then queues
|
for the missing space. With no pending review, Type (button or A) queues Enter
|
||||||
|
alone, the same as Type + Enter; a quick second A after Type therefore submits.
|
||||||
|
Type + Enter types any pending review and then queues
|
||||||
Enter; with no pending text it queues Enter only. Y opens the Quick phrases
|
Enter; with no pending text it queues Enter only. Y opens the Quick phrases
|
||||||
list over the review area; each further Y press cycles its highlighted choice. Cancel closes the picker without discarding an
|
list over the review area; each further Y press cycles its highlighted choice. Cancel closes the picker without discarding an
|
||||||
existing review. Type + Enter sends the selected phrase *without* a trailing
|
existing review. Type + Enter sends the selected phrase *without* a trailing
|
||||||
|
|||||||
@@ -335,6 +335,10 @@ void Controller::action(UiAction action) {
|
|||||||
if (text.back() != ' ' && text.size() < 4096) text += ' ';
|
if (text.back() != ' ' && text.size() < 4096) text += ' ';
|
||||||
queue_paced(std::move(text), false, true, manual_focus());
|
queue_paced(std::move(text), false, true, manual_focus());
|
||||||
} else delivery_detail(deliver_insert(session_, delivery_), false);
|
} else delivery_detail(deliver_insert(session_, delivery_), false);
|
||||||
|
} else if (!quick_open_ && (session_.state() == State::Ready || session_.state() == State::Queued)) {
|
||||||
|
// A deliberate Type press with nothing to review is an explicit Enter.
|
||||||
|
if (paced_) queue_paced({}, true, false, manual_focus());
|
||||||
|
else delivery_detail(deliver_enter(session_, delivery_), true);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case UiAction::Enter:
|
case UiAction::Enter:
|
||||||
|
|||||||
+1
-1
@@ -37,7 +37,7 @@ void help() {
|
|||||||
"SteamVR Developer setting Enable global input from overlays.\n"
|
"SteamVR Developer setting Enable global input from overlays.\n"
|
||||||
"Right X: hold to speak, release to review (default Frame binding).\n"
|
"Right X: hold to speak, release to review (default Frame binding).\n"
|
||||||
"Grip gestures are remappable but may be unavailable in the dashboard.\n"
|
"Grip gestures are remappable but may be unavailable in the dashboard.\n"
|
||||||
"Right B: cancel; A: Type (text + space); Y: Quick phrases picker.\n"
|
"Right B: cancel; A: Type (text + space, or Enter alone when no review); Y: Quick phrases.\n"
|
||||||
"Left grip: double-tap to Type + Enter (Enter alone when no review).\n"
|
"Left grip: double-tap to Type + Enter (Enter alone when no review).\n"
|
||||||
"Review by default. Auto insert requires uninterrupted verified Xwayland focus.\n"
|
"Review by default. Auto insert requires uninterrupted verified Xwayland focus.\n"
|
||||||
"Type approves current focus; Enter is never automatic.\n"
|
"Type approves current focus; Enter is never automatic.\n"
|
||||||
|
|||||||
+33
-1
@@ -66,6 +66,10 @@ template<class Query> std::vector<std::string> vulkan_extensions(Query query) {
|
|||||||
for (std::string name; words >> name;) result.push_back(std::move(name));
|
for (std::string name; words >> name;) result.push_back(std::move(name));
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
std::string battery_text(const std::optional<BatteryLevel>& level) {
|
||||||
|
if (!level) return "n/a";
|
||||||
|
return std::to_string(level->percent) + "%" + (level->charging ? " charging" : "");
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
struct Overlay::Impl {
|
struct Overlay::Impl {
|
||||||
@@ -88,6 +92,8 @@ struct Overlay::Impl {
|
|||||||
PanelSurface surface;
|
PanelSurface surface;
|
||||||
Panel panel;
|
Panel panel;
|
||||||
std::vector<ModelAction> model_actions;
|
std::vector<ModelAction> model_actions;
|
||||||
|
StatusIndicators indicators;
|
||||||
|
std::chrono::steady_clock::time_point batteries_read{};
|
||||||
bool world_ready = false, placed = false, has_texture = false, shown = false;
|
bool world_ready = false, placed = false, has_texture = false, shown = false;
|
||||||
float published_alpha = -1.f;
|
float published_alpha = -1.f;
|
||||||
float size_scale = 1.f;
|
float size_scale = 1.f;
|
||||||
@@ -371,9 +377,33 @@ struct Overlay::Impl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
bool available(UiAction action) const { return surface.available(action); }
|
bool available(UiAction action) const { return surface.available(action); }
|
||||||
|
std::optional<BatteryLevel> device_battery(vr::TrackedDeviceIndex_t index) const {
|
||||||
|
if (index == vr::k_unTrackedDeviceIndexInvalid || !system->IsTrackedDeviceConnected(index)) return {};
|
||||||
|
vr::ETrackedPropertyError error = vr::TrackedProp_Success;
|
||||||
|
if (!system->GetBoolTrackedDeviceProperty(index, vr::Prop_DeviceProvidesBatteryStatus_Bool, &error) ||
|
||||||
|
error != vr::TrackedProp_Success) return {};
|
||||||
|
const float level = system->GetFloatTrackedDeviceProperty(index, vr::Prop_DeviceBatteryPercentage_Float, &error);
|
||||||
|
if (error != vr::TrackedProp_Success || !std::isfinite(level) || level < 0.f || level > 1.f) return {};
|
||||||
|
const bool charging = system->GetBoolTrackedDeviceProperty(index, vr::Prop_DeviceIsCharging_Bool, &error);
|
||||||
|
return BatteryLevel{int(std::lround(level * 100.f)), error == vr::TrackedProp_Success && charging};
|
||||||
|
}
|
||||||
|
void update_indicators() {
|
||||||
|
indicators.dashboard_open = overlay->IsDashboardVisible();
|
||||||
|
const auto now = std::chrono::steady_clock::now();
|
||||||
|
if (now - batteries_read >= std::chrono::seconds(5) || batteries_read == decltype(now){}) {
|
||||||
|
batteries_read = now;
|
||||||
|
indicators.left = device_battery(system->GetTrackedDeviceIndexForControllerRole(vr::TrackedControllerRole_LeftHand));
|
||||||
|
indicators.right = device_battery(system->GetTrackedDeviceIndexForControllerRole(vr::TrackedControllerRole_RightHand));
|
||||||
|
// A standalone headset may report its battery only through Linux.
|
||||||
|
indicators.head = device_battery(vr::k_unTrackedDeviceIndex_Hmd);
|
||||||
|
if (!indicators.head) indicators.head = system_battery();
|
||||||
|
}
|
||||||
|
surface.set_indicators(indicators);
|
||||||
|
}
|
||||||
void draw(const Panel& p) {
|
void draw(const Panel& p) {
|
||||||
panel = p;
|
panel = p;
|
||||||
surface.set_clock_time(std::time(nullptr));
|
surface.set_clock_time(std::time(nullptr));
|
||||||
|
update_indicators();
|
||||||
if (surface.render(p, PanelSurface::Clock::now(), shown)) {
|
if (surface.render(p, PanelSurface::Clock::now(), shown)) {
|
||||||
gpu_texture->upload(surface.pixels());
|
gpu_texture->upload(surface.pixels());
|
||||||
auto texture = gpu_texture->texture();
|
auto texture = gpu_texture->texture();
|
||||||
@@ -413,7 +443,9 @@ struct Overlay::Impl {
|
|||||||
"\nMode dashboard=" + (overlay->IsDashboardVisible() ? "Y" : "N") +
|
"\nMode dashboard=" + (overlay->IsDashboardVisible() ? "Y" : "N") +
|
||||||
" lasers-anytime=" + (laser_error == vr::VROverlayError_None ? (laser_flag ? "Y" : "N") : "n/a") +
|
" lasers-anytime=" + (laser_error == vr::VROverlayError_None ? (laser_flag ? "Y" : "N") : "n/a") +
|
||||||
" system-input-available=" + (system->IsInputAvailable() ? "Y" : "N") +
|
" system-input-available=" + (system->IsInputAvailable() ? "Y" : "N") +
|
||||||
" panel-shown=" + (shown ? "Y" : "N") + " focus-gate=" + (focus ? "Y" : "N");
|
" panel-shown=" + (shown ? "Y" : "N") + " focus-gate=" + (focus ? "Y" : "N") +
|
||||||
|
"\nBattery L=" + battery_text(indicators.left) + " HMD=" + battery_text(indicators.head) +
|
||||||
|
" R=" + battery_text(indicators.right);
|
||||||
}
|
}
|
||||||
// Bound actions are accepted only with a connected tracked source. A held input
|
// Bound actions are accepted only with a connected tracked source. A held input
|
||||||
// following loss of activity must return to neutral before generating an edge.
|
// following loss of activity must return to neutral before generating an edge.
|
||||||
|
|||||||
+71
-14
@@ -39,8 +39,8 @@ constexpr std::array<Button, 32> buttons{{
|
|||||||
{{226, 138, 180, 46}, Control::Settings, "Settings"},
|
{{226, 138, 180, 46}, Control::Settings, "Settings"},
|
||||||
{{420, 138, 180, 46}, Control::Bindings, "Bindings"},
|
{{420, 138, 180, 46}, Control::Bindings, "Bindings"},
|
||||||
{{620, 138, 348, 46}, Control::LockLayout, "Lock grab/scale"},
|
{{620, 138, 348, 46}, Control::LockLayout, "Lock grab/scale"},
|
||||||
{{32, 406, 154, 44}, Control::Prev, "Previous"},
|
{{32, 496, 154, 44}, Control::Prev, "Previous"},
|
||||||
{{838, 406, 130, 44}, Control::Next, "Next"},
|
{{838, 496, 130, 44}, Control::Next, "Next"},
|
||||||
{{32, 574, 176, 68}, Control::Record, "Record"},
|
{{32, 574, 176, 68}, Control::Record, "Record"},
|
||||||
{{222, 574, 176, 68}, Control::Cancel, "Cancel"},
|
{{222, 574, 176, 68}, Control::Cancel, "Cancel"},
|
||||||
{{412, 574, 176, 68}, Control::Insert, "Type"},
|
{{412, 574, 176, 68}, Control::Insert, "Type"},
|
||||||
@@ -127,6 +127,7 @@ struct PanelSurface::Impl {
|
|||||||
std::time_t clock_time = std::time(nullptr);
|
std::time_t clock_time = std::time(nullptr);
|
||||||
ClockLabel displayed_clock;
|
ClockLabel displayed_clock;
|
||||||
std::string placement_note, binding_note;
|
std::string placement_note, binding_note;
|
||||||
|
StatusIndicators indicators;
|
||||||
std::array<int, 2> pressed{{-1, -1}};
|
std::array<int, 2> pressed{{-1, -1}};
|
||||||
std::array<PanelSurface::Clock::time_point, 2> press_time{};
|
std::array<PanelSurface::Clock::time_point, 2> press_time{};
|
||||||
int hold_progress = 0;
|
int hold_progress = 0;
|
||||||
@@ -139,7 +140,7 @@ struct PanelSurface::Impl {
|
|||||||
bool install_confirm = false;
|
bool install_confirm = false;
|
||||||
std::optional<ModelOption> consent_snapshot;
|
std::optional<ModelOption> consent_snapshot;
|
||||||
std::vector<std::string> consent_lines;
|
std::vector<std::string> consent_lines;
|
||||||
static constexpr size_t lines_per_page = 4;
|
static constexpr size_t lines_per_page = 6;
|
||||||
static constexpr size_t consent_lines_per_page = 6;
|
static constexpr size_t consent_lines_per_page = 6;
|
||||||
size_t consent_pages() const {
|
size_t consent_pages() const {
|
||||||
return std::max(size_t(1), (consent_lines.size() + consent_lines_per_page - 1) / consent_lines_per_page);
|
return std::max(size_t(1), (consent_lines.size() + consent_lines_per_page - 1) / consent_lines_per_page);
|
||||||
@@ -221,6 +222,30 @@ struct PanelSurface::Impl {
|
|||||||
std::clamp(.5f - d, 0.f, 1.f));
|
std::clamp(.5f - d, 0.f, 1.f));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Compact "L [###] 82%" groups; missing devices are omitted, not zeroed.
|
||||||
|
void batteries(int x, int right) {
|
||||||
|
const std::array<std::pair<const char*, const std::optional<BatteryLevel>*>, 3> groups{{
|
||||||
|
{"L", &indicators.left}, {"HMD", &indicators.head}, {"R", &indicators.right}}};
|
||||||
|
for (const auto& [name, level] : groups) {
|
||||||
|
if (!*level) continue;
|
||||||
|
const auto& b = **level;
|
||||||
|
const std::string percent = std::to_string(b.percent) + "%";
|
||||||
|
const int width = measure(name, 18) + 6 + 30 + 6 + measure(percent, 20) + (b.charging ? 12 : 0);
|
||||||
|
if (x + width > right) return;
|
||||||
|
text(name, x, 56, 18, muted, right);
|
||||||
|
x += measure(name, 18) + 6;
|
||||||
|
const Color tone = b.charging ? cyan : b.percent <= 20 ? pink : ink;
|
||||||
|
rounded({x, 42, 26, 16}, 4, background, mix(background, tone, .8f));
|
||||||
|
rect({x + 26, 47, 3, 6}, mix(background, tone, .8f));
|
||||||
|
const int fill = std::max(b.percent > 0 ? 1 : 0, 20 * std::clamp(b.percent, 0, 100) / 100);
|
||||||
|
if (fill) rect({x + 3, 45, fill, 10}, tone);
|
||||||
|
x += 36;
|
||||||
|
text(percent, x, 57, 20, tone, right);
|
||||||
|
x += measure(percent, 20);
|
||||||
|
if (b.charging) { text("+", x + 1, 55, 18, cyan, right); x += 12; }
|
||||||
|
x += 18;
|
||||||
|
}
|
||||||
|
}
|
||||||
void prepare_gradient(Clock::time_point now) {
|
void prepare_gradient(Clock::time_point now) {
|
||||||
// A periodic cosine field has matching values AND velocity at both
|
// A periodic cosine field has matching values AND velocity at both
|
||||||
// spatial and temporal seams. One phase drives the body and handles.
|
// spatial and temporal seams. One phase drives the body and handles.
|
||||||
@@ -268,6 +293,12 @@ struct PanelSurface::Impl {
|
|||||||
if (FT_Load_Char(face, cp, FT_LOAD_DEFAULT)) return 0;
|
if (FT_Load_Char(face, cp, FT_LOAD_DEFAULT)) return 0;
|
||||||
return int(face->glyph->advance.x >> 6);
|
return int(face->glyph->advance.x >> 6);
|
||||||
}
|
}
|
||||||
|
int measure(std::string_view s, unsigned px) {
|
||||||
|
size(px);
|
||||||
|
int width = 0;
|
||||||
|
for (size_t i = 0; i < s.size();) width += advance(next_codepoint(s, i));
|
||||||
|
return width;
|
||||||
|
}
|
||||||
std::vector<std::string> wrap(std::string_view s, unsigned px, int width) {
|
std::vector<std::string> wrap(std::string_view s, unsigned px, int width) {
|
||||||
size(px);
|
size(px);
|
||||||
std::vector<std::string> result;
|
std::vector<std::string> result;
|
||||||
@@ -314,7 +345,8 @@ struct PanelSurface::Impl {
|
|||||||
size_t page_count() const { return std::max(size_t(1), (lines.size() + lines_per_page - 1) / lines_per_page); }
|
size_t page_count() const { return std::max(size_t(1), (lines.size() + lines_per_page - 1) / lines_per_page); }
|
||||||
bool available(UiAction a) const {
|
bool available(UiAction a) const {
|
||||||
if (a == UiAction::Record) return tab != Tab::Models && !panel.quick_open && (panel.recording || panel.record_available);
|
if (a == UiAction::Record) return tab != Tab::Models && !panel.quick_open && (panel.recording || panel.record_available);
|
||||||
if (a == UiAction::Insert) return tab != Tab::Models && !panel.quick_open && panel.enabled && !panel.recording && !panel.transcript.empty();
|
// With nothing to review, Type is an explicit Enter.
|
||||||
|
if (a == UiAction::Insert) return tab != Tab::Models && !panel.quick_open && panel.enabled && !panel.recording;
|
||||||
if (a == UiAction::Enter) return tab != Tab::Models && panel.enabled && !panel.recording;
|
if (a == UiAction::Enter) return tab != Tab::Models && panel.enabled && !panel.recording;
|
||||||
if (a == UiAction::QuickChat) return tab != Tab::Models && panel.enabled && !panel.recording && !panel.quick_inputs.empty();
|
if (a == UiAction::QuickChat) return tab != Tab::Models && panel.enabled && !panel.recording && !panel.quick_inputs.empty();
|
||||||
if (a == UiAction::Toggle) return false;
|
if (a == UiAction::Toggle) return false;
|
||||||
@@ -403,16 +435,37 @@ struct PanelSurface::Impl {
|
|||||||
paint_background();
|
paint_background();
|
||||||
frame();
|
frame();
|
||||||
text("FrameYap", 32, 61, 40, ink, 300);
|
text("FrameYap", 32, 61, 40, ink, 300);
|
||||||
text(displayed_clock.time, 475, 58, 28, ink, 735);
|
// Clock and date are right-aligned; batteries fill the space after the title.
|
||||||
if (!displayed_clock.date.empty()) text(displayed_clock.date, 756, 58, 24, cyan, 968);
|
int clock_right = 968;
|
||||||
|
if (!displayed_clock.date.empty()) {
|
||||||
|
clock_right -= measure(displayed_clock.date, 24);
|
||||||
|
text(displayed_clock.date, clock_right, 58, 24, cyan, 968);
|
||||||
|
clock_right -= 22;
|
||||||
|
}
|
||||||
|
const int clock_left = clock_right - measure(displayed_clock.time, 28);
|
||||||
|
text(displayed_clock.time, clock_left, 58, 28, ink, clock_right);
|
||||||
|
batteries(222, clock_left - 20);
|
||||||
rounded({32, 78, 936, 48}, 13, mix(background, card, .7f),
|
rounded({32, 78, 936, 48}, 13, mix(background, card, .7f),
|
||||||
panel.recording ? mix(card, pink, .36f) : mix(card, cyan, .22f),
|
panel.recording ? mix(card, pink, .36f) : mix(card, cyan, .22f),
|
||||||
panel.recording ? .16f : 0.f);
|
panel.recording ? .16f : 0.f);
|
||||||
rounded({46, 95, 13, 13}, 6, panel.recording ? pink : cyan,
|
rounded({46, 95, 13, 13}, 6, panel.recording ? pink : cyan,
|
||||||
panel.recording ? pink : cyan, panel.recording ? .40f : .20f);
|
panel.recording ? pink : cyan, panel.recording ? .40f : .20f);
|
||||||
auto status = wrap(panel.status, 27, 790);
|
int status_right = 968;
|
||||||
text(status.front(), 70, 109, 27, ink, 844);
|
if (indicators.dashboard_open) {
|
||||||
if (status.size() > 1) text("[...]", 864, 109, 23, muted, 968);
|
// The SteamVR dashboard owns controller buttons while it is open;
|
||||||
|
// the pointer still works. Mirrors what the wearer can use right now.
|
||||||
|
const bool paused = *indicators.dashboard_open;
|
||||||
|
const char* label = paused ? "Buttons paused" : "Buttons ready";
|
||||||
|
const Rect chip{958 - measure(label, 19) - 38, 87, measure(label, 19) + 38, 30};
|
||||||
|
rounded(chip, 12, mix(card, paused ? pink : cyan, .12f), paused ? pink : mix(card, cyan, .5f),
|
||||||
|
0.f, 1);
|
||||||
|
rounded({chip.x + 11, 97, 10, 10}, 5, paused ? pink : cyan, paused ? pink : cyan);
|
||||||
|
text(label, chip.x + 28, 108, 19, paused ? pink : ink, chip.x + chip.w - 4);
|
||||||
|
status_right = chip.x - 12;
|
||||||
|
}
|
||||||
|
auto status = wrap(panel.status, 27, status_right - 70 - 74);
|
||||||
|
text(status.front(), 70, 109, 27, ink, status_right - 70);
|
||||||
|
if (status.size() > 1) text("[...]", status_right - 66, 109, 23, muted, status_right);
|
||||||
if (tab == Tab::Review && panel.quick_open) {
|
if (tab == Tab::Review && panel.quick_open) {
|
||||||
rounded({32, 212, 936, 324}, 16, mix(card, muted, .08f), mix(card, cyan, .25f), .18f);
|
rounded({32, 212, 936, 324}, 16, mix(card, muted, .08f), mix(card, cyan, .25f), .18f);
|
||||||
text("QUICK PHRASES Y: NEXT TYPE + ENTER: CHOICE + ENTER CANCEL: CLOSE", 48, 243, 21, cyan, 952);
|
text("QUICK PHRASES Y: NEXT TYPE + ENTER: CHOICE + ENTER CANCEL: CLOSE", 48, 243, 21, cyan, 952);
|
||||||
@@ -424,13 +477,13 @@ struct PanelSurface::Impl {
|
|||||||
text(panel.quick_inputs[i], 66, row.y + 28, 26, selected ? ink : muted, 930);
|
text(panel.quick_inputs[i], 66, row.y + 28, 26, selected ? ink : muted, 930);
|
||||||
}
|
}
|
||||||
} else if (tab == Tab::Review) {
|
} else if (tab == Tab::Review) {
|
||||||
rounded({32, 212, 936, 178}, 16, mix(card, muted, .08f), mix(card, cyan, .25f), .18f);
|
rounded({32, 204, 936, 278}, 16, mix(card, muted, .08f), mix(card, cyan, .25f), .18f);
|
||||||
for (size_t i = 0; i < lines_per_page && page * lines_per_page + i < lines.size(); ++i)
|
for (size_t i = 0; i < lines_per_page && page * lines_per_page + i < lines.size(); ++i)
|
||||||
text(lines[page * lines_per_page + i], 48, 246 + int(i) * 40, 32,
|
text(lines[page * lines_per_page + i], 48, 240 + int(i) * 42, 32,
|
||||||
panel.transcript.empty() ? muted : ink, 952);
|
panel.transcript.empty() ? muted : ink, 952);
|
||||||
text("PAGE " + std::to_string(page + 1) + " / " + std::to_string(page_count()), 416, 436, 23, muted, 790);
|
// A Bindings failure is the only note here; it replaces the page count.
|
||||||
text(binding_note.empty() ? "Type adds a space; Type + Enter explicitly submits." : binding_note,
|
if (!binding_note.empty()) text(binding_note, 200, 526, 18, pink, 824);
|
||||||
32, 203, 20, muted, 968);
|
else text("PAGE " + std::to_string(page + 1) + " / " + std::to_string(page_count()), 416, 526, 23, muted, 790);
|
||||||
} else if (tab == Tab::Models) {
|
} else if (tab == Tab::Models) {
|
||||||
text("Models: select to restart. Install always needs explicit confirmation.", 32, 202, 19, muted, 968);
|
text("Models: select to restart. Install always needs explicit confirmation.", 32, 202, 19, muted, 968);
|
||||||
const auto it = std::find_if(panel.models.begin(), panel.models.end(), [&](const auto& m) { return m.id == panel.selected_backend; });
|
const auto it = std::find_if(panel.models.begin(), panel.models.end(), [&](const auto& m) { return m.id == panel.selected_backend; });
|
||||||
@@ -489,6 +542,7 @@ struct PanelSurface::Impl {
|
|||||||
(m.id == panel.selected_backend && panel.model_busy ? "checking" : m.state);
|
(m.id == panel.selected_backend && panel.model_busy ? "checking" : m.state);
|
||||||
}() : b.id == Control::ModelInstall && install_confirm ? "Confirm Install" :
|
}() : b.id == Control::ModelInstall && install_confirm ? "Confirm Install" :
|
||||||
b.id == Control::Record && panel.recording ? "Stop" :
|
b.id == Control::Record && panel.recording ? "Stop" :
|
||||||
|
b.id == Control::Insert && panel.transcript.empty() ? "Enter" :
|
||||||
b.id == Control::Clock24h ? (clock_24h ? "Clock: 24 hour" : "Clock: 12 hour") :
|
b.id == Control::Clock24h ? (clock_24h ? "Clock: 24 hour" : "Clock: 12 hour") :
|
||||||
b.id == Control::Date ? (date_format == DateFormat::Off ? "Date: Off" :
|
b.id == Control::Date ? (date_format == DateFormat::Off ? "Date: Off" :
|
||||||
date_format == DateFormat::MonthDayYear ? "Date: MM/DD/YYYY" :
|
date_format == DateFormat::MonthDayYear ? "Date: MM/DD/YYYY" :
|
||||||
@@ -629,6 +683,9 @@ SurfaceEvent PanelSurface::pointer_up(unsigned cursor, float x, float y, Clock::
|
|||||||
else if (c == Control::Next) { ++impl_->page; impl_->dirty = true; }
|
else if (c == Control::Next) { ++impl_->page; impl_->dirty = true; }
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
void PanelSurface::set_indicators(const StatusIndicators& indicators) {
|
||||||
|
if (impl_->indicators != indicators) { impl_->indicators = indicators; impl_->dirty = true; }
|
||||||
|
}
|
||||||
void PanelSurface::set_binding_note(std::string note) {
|
void PanelSurface::set_binding_note(std::string note) {
|
||||||
if (impl_->binding_note != note) { impl_->binding_note = std::move(note); impl_->dirty = true; }
|
if (impl_->binding_note != note) { impl_->binding_note = std::move(note); impl_->dirty = true; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "overlay.hpp"
|
#include "overlay.hpp"
|
||||||
|
#include "battery.hpp"
|
||||||
#include "config.hpp"
|
#include "config.hpp"
|
||||||
#include "panel_drag.hpp"
|
#include "panel_drag.hpp"
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
@@ -24,6 +25,12 @@ struct SurfaceEvent {
|
|||||||
bool open_bindings = false;
|
bool open_bindings = false;
|
||||||
std::optional<ModelAction> model_action;
|
std::optional<ModelAction> model_action;
|
||||||
};
|
};
|
||||||
|
// Header indicators. Absent values are hidden, never shown as zero or as a guess.
|
||||||
|
struct StatusIndicators {
|
||||||
|
std::optional<bool> dashboard_open; // SteamVR dashboard: controller bindings paused
|
||||||
|
std::optional<BatteryLevel> left, head, right;
|
||||||
|
bool operator==(const StatusIndicators&) const = default;
|
||||||
|
};
|
||||||
// One CPU RGBA canvas, independent of OpenVR. Settings replace the review area;
|
// One CPU RGBA canvas, independent of OpenVR. Settings replace the review area;
|
||||||
// status and safety controls remain on the same surface.
|
// status and safety controls remain on the same surface.
|
||||||
class PanelSurface {
|
class PanelSurface {
|
||||||
@@ -60,6 +67,7 @@ public:
|
|||||||
void set_date_format(DateFormat format);
|
void set_date_format(DateFormat format);
|
||||||
void set_clock_time(std::time_t now);
|
void set_clock_time(std::time_t now);
|
||||||
void set_binding_note(std::string note);
|
void set_binding_note(std::string note);
|
||||||
|
void set_indicators(const StatusIndicators& indicators);
|
||||||
bool available(UiAction action) const;
|
bool available(UiAction action) const;
|
||||||
// Exact text rows currently shown in the consent viewport (empty outside it).
|
// Exact text rows currently shown in the consent viewport (empty outside it).
|
||||||
// OpenVR has no screen-reader accessibility channel for this canvas.
|
// OpenVR has no screen-reader accessibility channel for this canvas.
|
||||||
|
|||||||
@@ -120,7 +120,8 @@ int main() {
|
|||||||
int submitted = f.worker.submissions;
|
int submitted = f.worker.submissions;
|
||||||
c.action(UiAction::EndRecord); // old PTT release cannot submit after restart
|
c.action(UiAction::EndRecord); // old PTT release cannot submit after restart
|
||||||
c.tick(); assert(c.state() == State::Ready && f.worker.submissions == submitted);
|
c.tick(); assert(c.state() == State::Ready && f.worker.submissions == submitted);
|
||||||
c.action(UiAction::Insert); assert(f.input.text == 0);
|
c.action(UiAction::Insert); // nothing to review: Type is an explicit Enter
|
||||||
|
assert(f.input.text == 0 && f.input.enter == 1);
|
||||||
release(c, f);
|
release(c, f);
|
||||||
f.focus.valid = false; result(c, f, "review to discard");
|
f.focus.valid = false; result(c, f, "review to discard");
|
||||||
assert(c.state() == State::Review && f.input.text == 0);
|
assert(c.state() == State::Review && f.input.text == 0);
|
||||||
@@ -130,7 +131,7 @@ int main() {
|
|||||||
c.action(UiAction::Cancel); // a stale Cancel cannot re-enable an unavailable model
|
c.action(UiAction::Cancel); // a stale Cancel cannot re-enable an unavailable model
|
||||||
c.tick(); c.action(UiAction::BeginRecord); c.action(UiAction::Enter);
|
c.tick(); c.action(UiAction::BeginRecord); c.action(UiAction::Enter);
|
||||||
assert(c.state() == State::Error && f.worker.starts == starts &&
|
assert(c.state() == State::Error && f.worker.starts == starts &&
|
||||||
f.input.enter == 0 && f.input.text == 0);
|
f.input.enter == 1 && f.input.text == 0);
|
||||||
c.shutdown();
|
c.shutdown();
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
@@ -167,7 +168,8 @@ int main() {
|
|||||||
assert(f.input.text == 0 && f.input.enter == 0);
|
assert(f.input.text == 0 && f.input.enter == 0);
|
||||||
c.action(UiAction::Insert);
|
c.action(UiAction::Insert);
|
||||||
assert(f.input.text == 1 && f.input.sent == "hello " && f.input.enter == 0);
|
assert(f.input.text == 1 && f.input.sent == "hello " && f.input.enter == 0);
|
||||||
c.action(UiAction::Insert); assert(f.input.text == 1); // at most once
|
c.action(UiAction::Insert); // at most once; a second press is only an Enter
|
||||||
|
assert(f.input.text == 1 && f.input.enter == 1);
|
||||||
release(c, f); result(c, f, std::string(4096, 'x'));
|
release(c, f); result(c, f, std::string(4096, 'x'));
|
||||||
c.action(UiAction::Insert);
|
c.action(UiAction::Insert);
|
||||||
assert(f.input.sent.size() == 4096 && f.input.sent.back() == 'x');
|
assert(f.input.sent.size() == 4096 && f.input.sent.back() == 'x');
|
||||||
@@ -301,10 +303,13 @@ int main() {
|
|||||||
std::string joined;
|
std::string joined;
|
||||||
for (int i = 0; i < 4; ++i) joined += f.input.events[i];
|
for (int i = 0; i < 4; ++i) joined += f.input.events[i];
|
||||||
assert(joined == std::string(81, 'x') + " " && f.input.events.back() == "<Enter>");
|
assert(joined == std::string(81, 'x') + " " && f.input.events.back() == "<Enter>");
|
||||||
|
// With nothing left to review, Type is an explicit Enter; it still obeys the gap.
|
||||||
c.action(UiAction::Insert); assert(f.input.events.size() == 5);
|
c.action(UiAction::Insert); assert(f.input.events.size() == 5);
|
||||||
// A separate Enter press is a new action but still obeys the gap.
|
c.action(UiAction::Enter); // ignored while the Type-Enter is pending
|
||||||
c.action(UiAction::Enter); c.tick(); assert(f.input.enter == 1);
|
c.tick(); assert(f.input.enter == 1);
|
||||||
now += std::chrono::milliseconds(150); c.tick(); assert(f.input.enter == 2);
|
now += std::chrono::milliseconds(150); c.tick(); assert(f.input.enter == 2);
|
||||||
|
c.action(UiAction::Enter); now += std::chrono::milliseconds(150); c.tick();
|
||||||
|
assert(f.input.enter == 3 && f.input.text == 4);
|
||||||
c.shutdown();
|
c.shutdown();
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
|
|||||||
+28
-6
@@ -57,6 +57,28 @@ int main(int argc, char** argv) {
|
|||||||
no_action(click(surface, 207, 575));
|
no_action(click(surface, 207, 575));
|
||||||
assert(click(surface, 100, 610).action == UiAction::BeginRecord);
|
assert(click(surface, 100, 610).action == UiAction::BeginRecord);
|
||||||
assert(!surface.render(p)); // a diagnostic click does not change the canvas
|
assert(!surface.render(p)); // a diagnostic click does not change the canvas
|
||||||
|
{
|
||||||
|
// With nothing to review, the enabled Type button is an explicit Enter.
|
||||||
|
PanelSurface idle(argv[1], Mount::World, {}, {.enabled = false});
|
||||||
|
Panel ready{"Ready", "", "", true, false};
|
||||||
|
idle.render(ready);
|
||||||
|
assert(idle.available(UiAction::Insert));
|
||||||
|
assert(click(idle, 480, 610).action == UiAction::Insert);
|
||||||
|
ready.recording = true; idle.render(ready);
|
||||||
|
assert(!idle.available(UiAction::Insert));
|
||||||
|
// Indicators repaint only on change; absent values stay hidden.
|
||||||
|
ready.recording = false; idle.render(ready);
|
||||||
|
const auto plain = idle.pixels();
|
||||||
|
StatusIndicators shown{true, BatteryLevel{82, false}, BatteryLevel{15, false}, BatteryLevel{100, true}};
|
||||||
|
idle.set_indicators(shown);
|
||||||
|
assert(idle.render(ready) && idle.pixels() != plain);
|
||||||
|
idle.set_indicators(shown); assert(!idle.render(ready));
|
||||||
|
if (argc >= 3) snapshot(idle, std::string(argv[2]) + "-indicators.ppm");
|
||||||
|
shown.dashboard_open = false; idle.set_indicators(shown);
|
||||||
|
assert(idle.render(ready));
|
||||||
|
if (argc >= 3) snapshot(idle, std::string(argv[2]) + "-indicators-closed.ppm");
|
||||||
|
idle.set_indicators({}); assert(idle.render(ready) && idle.pixels() == plain);
|
||||||
|
}
|
||||||
assert(click(surface, 280, 610).action == UiAction::Cancel);
|
assert(click(surface, 280, 610).action == UiAction::Cancel);
|
||||||
assert(!surface.render(p));
|
assert(!surface.render(p));
|
||||||
no_action(surface.pointer_up(0, 100, 610));
|
no_action(surface.pointer_up(0, 100, 610));
|
||||||
@@ -204,7 +226,7 @@ int main(int argc, char** argv) {
|
|||||||
p.quick_open = true;
|
p.quick_open = true;
|
||||||
assert(surface.render(p));
|
assert(surface.render(p));
|
||||||
const auto first_quick = surface.pixels();
|
const auto first_quick = surface.pixels();
|
||||||
no_action(click(surface, 900, 430)); // review pagination hidden behind picker
|
no_action(click(surface, 900, 518)); // review pagination hidden behind picker
|
||||||
p.quick_selected = 1;
|
p.quick_selected = 1;
|
||||||
assert(surface.render(p) && surface.pixels() != first_quick);
|
assert(surface.render(p) && surface.pixels() != first_quick);
|
||||||
assert(click(surface, 680, 610).action == UiAction::Enter);
|
assert(click(surface, 680, 610).action == UiAction::Enter);
|
||||||
@@ -351,21 +373,21 @@ int main(int argc, char** argv) {
|
|||||||
surface.render(p);
|
surface.render(p);
|
||||||
surface.reset_pointers(); surface.render(p);
|
surface.reset_pointers(); surface.render(p);
|
||||||
const auto first = surface.pixels();
|
const auto first = surface.pixels();
|
||||||
no_action(click(surface, 900, 430));
|
no_action(click(surface, 900, 518));
|
||||||
surface.reset_pointers(); assert(surface.render(p));
|
surface.reset_pointers(); assert(surface.render(p));
|
||||||
assert(surface.pixels() != first);
|
assert(surface.pixels() != first);
|
||||||
no_action(click(surface, 90, 430));
|
no_action(click(surface, 90, 518));
|
||||||
surface.reset_pointers(); surface.render(p);
|
surface.reset_pointers(); surface.render(p);
|
||||||
assert(surface.pixels() == first);
|
assert(surface.pixels() == first);
|
||||||
for (int i = 0; i < 200; ++i) { no_action(click(surface, 900, 430)); surface.render(p); }
|
for (int i = 0; i < 200; ++i) { no_action(click(surface, 900, 518)); surface.render(p); }
|
||||||
for (int i = 0; i < 200; ++i) { no_action(click(surface, 90, 430)); surface.render(p); }
|
for (int i = 0; i < 200; ++i) { no_action(click(surface, 90, 518)); surface.render(p); }
|
||||||
surface.reset_pointers(); surface.render(p);
|
surface.reset_pointers(); surface.render(p);
|
||||||
assert(surface.pixels() == first);
|
assert(surface.pixels() == first);
|
||||||
p.transcript = "New result";
|
p.transcript = "New result";
|
||||||
assert(surface.render(p));
|
assert(surface.render(p));
|
||||||
surface.reset_pointers(); surface.render(p);
|
surface.reset_pointers(); surface.render(p);
|
||||||
auto replaced = surface.pixels();
|
auto replaced = surface.pixels();
|
||||||
no_action(click(surface, 900, 430));
|
no_action(click(surface, 900, 518));
|
||||||
surface.reset_pointers(); surface.render(p);
|
surface.reset_pointers(); surface.render(p);
|
||||||
assert(surface.pixels() == replaced);
|
assert(surface.pixels() == replaced);
|
||||||
p.status = std::string(4096, 's'); p.detail = std::string(4096, 'd');
|
p.status = std::string(4096, 's'); p.detail = std::string(4096, 'd');
|
||||||
|
|||||||
Reference in new issue
Block a user