Compare commits

...
Author SHA1 Message Date
Stefan MaierandClaude Opus 5.5 827eb0fc1d The keyboard gets the menu pointer's clicks; its BACK and ENTER hints click
The menus' mouse handling took the pointer's clicks each frame before the
keyboard asked for them, so the keyboard never saw one: with the keyboard
up the pointer is now the keyboard's alone. Clicking the hints under it
(B = BACK, A = ENTER, in its picture) cancels or finishes as Done does.
The pointer counts as moved past 3 pixels, not a hand's tremor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 12:47:51 +02:00
Stefan MaierandClaude Opus 5.5 a482a5f5bb The on-screen keyboard follows the menu pointer in VR
The key under the controller's pointer is picked and a click types it.
In the menus the triggers no longer reach the game as the gamepad's (they
click the pointer; on the keyboard they also moved its cursor).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 12:42:57 +02:00
Stefan MaierandClaude Opus 5.5 81f321f959 Of overlapping screen keys, the one whose icon is nearest on the left
A key's label is a text box often far wider than its text, reaching over
the next key (Back pressed Delete). Of the keys under the pointer, the one
whose icon begins nearest on its left is taken: each key from its icon to
the next one's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 12:30:20 +02:00
Stefan MaierandClaude Opus 5.5 f97ff9219f Menu pointer: narrower keys, the click where the trigger began to move
The screen keys' margin in VR was wide enough to reach the next key (Back
pressed Delete): now 2 pixels to the sides and 4 above and below, with a
label at most 25 pixels off. A click is taken where the controller
pointed as its trigger began to move (past a tenth), not a fixed time
before, which could still be on the key it just left.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 12:27:28 +02:00
Stefan MaierandClaude Opus 5.5 85e79a067a Steadier menu pointer clicks in VR
A click is taken where the controller pointed a few frames before the
trigger went down (pulling it tips the controller), and in VR a screen
key's button takes a label up to 40 pixels off and a margin of 6 about it,
so the keys under the menus (A, B, Back) are hit as readily as items.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 12:23:28 +02:00
Stefan MaierandClaude Opus 5.5 effeda1b7c Point at the menus with a controller in VR
The desktop builds' mouse in the menus (halo_ui_pointer.h) now comes, on
Android in VR, from a controller: where its aim's line meets the menus'
screen fixed in the room is the pointer, the trigger clicks (the hand
pressed last points; a click ticks its rumble), and a ringed dot shows
where it points. The item under it takes the focus and a click presses A,
as with the mouse; the online games and host options panels take it too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 11:56:05 +02:00
7 changed files with 347 additions and 12 deletions

No files matched your search

+6
View File
@@ -140,6 +140,12 @@ out (halo_vr_scope_pass). */
int halo_vr_scope(short local_player_index, float magnification, float position[3], float forward[3], float up[3],
float *vertical_field_of_view);
int halo_vr_scope_pass(void);
/* The menus' pointer (d3d8_gl.c's halo_ui_pointer_update): where a
controller points on the menus' screen, in their 640x480, whether that
moved since asked, and the trigger's clicks since (nonzero while it points
at the screen); and where it points this frame, to draw it (render.c) */
int halo_vr_menu_pointer(int *x, int *y, int *click_x, int *click_y, int *moved, int *clicks);
int halo_vr_menu_pointer_at(int *x, int *y);
/* Players' names over their heads (hud.c, render.c): whether they are drawn
apart this frame; the slot a name is drawn into (left, top, right, bottom
of the game's 640x480); a name's place (world units, over the head),
+13 -3
View File
@@ -1365,11 +1365,21 @@ HRESULT WINAPI Direct3D_CreateDevice(UINT adapter, D3DDEVTYPE device_type, void
/* ---------- the menus' pointer */
#ifdef HALO_ANDROID
/* the controller pointing at the menus' screen in VR (halo_vr_menu_pointer) */
int halo_ui_pointer_update(int menus_active, struct halo_ui_pointer *pointer)
{
(void)menus_active;
(void)pointer;
return 0;
int x, y, click_x, click_y, moved, clicks;
if (!menus_active || !halo_vr_menu_pointer(&x, &y, &click_x, &click_y, &moved, &clicks))
return 0;
memset(pointer, 0, sizeof(*pointer));
pointer->x = (short)(x - (halo_screen_width() - 640) / 2);
pointer->y = (short)y;
pointer->click_x = (short)(click_x - (halo_screen_width() - 640) / 2);
pointer->click_y = (short)click_y;
pointer->moved = (unsigned char)(moved != 0);
pointer->left_clicks = (unsigned char)(clicks < 255 ? clicks : 255);
return 1;
}
#else
/* a point in the window, as SDL reports it, in the menus' coordinates: the
+133
View File
@@ -1969,6 +1969,133 @@ static int crosshair_pose(struct halo_xr_pose *pose, float *size)
return 1;
}
/* ---------- the menus' pointer
In the menus (the screen fixed in the room), a controller points at the
screen and its trigger clicks, as the desktop builds' mouse does
(halo_ui_pointer.h): where its aim's line meets the screen, in the menus'
640x480. The hand whose trigger was pressed last points; a click gives a
short tick of the hand's rumble. */
static struct
{
int valid;
float x, y;
float reported_x, reported_y;
int hand;
int trigger_down[HALO_XR_HAND_COUNT];
int clicks;
/* a click is taken where it pointed as the trigger began to move
(pulling it through tips the controller) */
int pulling[HALO_XR_HAND_COUNT];
float pull_x[HALO_XR_HAND_COUNT], pull_y[HALO_XR_HAND_COUNT];
float click_x, click_y;
} menu_pointer;
static void menu_pointer_update(void)
{
static const float forward[3] = { 0.0f, 0.0f, -1.0f };
const struct halo_xr_hand *hand;
float direction[3], t, hit_x, hit_y, width, height;
int index;
menu_pointer.valid = 0;
if (!vr.active || vr.last_overlay_mode != HALO_XR_OVERLAY_SCREEN || !vr.frame.focused)
{
menu_pointer.trigger_down[0] = menu_pointer.trigger_down[1] = 0;
menu_pointer.pulling[0] = menu_pointer.pulling[1] = 0;
return;
}
/* (the hand that clicked last points; a click counts as it goes down) */
for (index = 0; index < HALO_XR_HAND_COUNT; index++)
{
int down = vr.frame.hands[index].active && (vr.frame.hands[index].buttons & HALO_XR_BUTTON_TRIGGER);
if (down && !menu_pointer.trigger_down[index])
{
menu_pointer.hand = index;
menu_pointer.clicks++;
}
menu_pointer.trigger_down[index] = down;
}
hand = &vr.frame.hands[menu_pointer.hand];
if (!hand->active || !(hand->flags & HALO_XR_POSE_VALID))
{
hand = &vr.frame.hands[1 - menu_pointer.hand];
if (!hand->active || !(hand->flags & HALO_XR_POSE_VALID))
return;
}
quaternion_rotate(hand->aim.orientation, forward, direction);
if (direction[2] > -1e-3f)
return;
t = (-vr.screen_distance - hand->aim.position[2]) / direction[2];
if (t <= 0.0f)
return;
hit_x = hand->aim.position[0] + direction[0] * t;
hit_y = hand->aim.position[1] + direction[1] * t;
width = vr.screen_width;
height = vr.screen_width * 0.75f;
menu_pointer.x = (hit_x / width + 0.5f) * 640.0f;
menu_pointer.y = (0.5f - hit_y / height) * 480.0f;
menu_pointer.valid = menu_pointer.x >= 0.0f && menu_pointer.x < 640.0f && menu_pointer.y >= 0.0f &&
menu_pointer.y < 480.0f;
/* (the click where the trigger began to move, if it pointed then) */
{
int pulling = vr.frame.hands[menu_pointer.hand].trigger > 0.1f;
if (pulling && !menu_pointer.pulling[menu_pointer.hand] && menu_pointer.valid)
{
menu_pointer.pull_x[menu_pointer.hand] = menu_pointer.x;
menu_pointer.pull_y[menu_pointer.hand] = menu_pointer.y;
}
if (!pulling || !menu_pointer.pulling[menu_pointer.hand])
{
menu_pointer.click_x = menu_pointer.x;
menu_pointer.click_y = menu_pointer.y;
}
else
{
menu_pointer.click_x = menu_pointer.pull_x[menu_pointer.hand];
menu_pointer.click_y = menu_pointer.pull_y[menu_pointer.hand];
}
if (!pulling || menu_pointer.valid)
menu_pointer.pulling[menu_pointer.hand] = pulling;
}
}
int halo_vr_menu_pointer(int *x, int *y, int *click_x, int *click_y, int *moved, int *clicks)
{
menu_pointer_update();
*click_x = (int)menu_pointer.click_x;
*click_y = (int)menu_pointer.click_y;
*clicks = menu_pointer.clicks;
if (menu_pointer.clicks)
host_xr_haptic(menu_pointer.hand, 0.3f, 0.02f);
menu_pointer.clicks = 0;
if (!menu_pointer.valid)
return 0;
*x = (int)menu_pointer.x;
*y = (int)menu_pointer.y;
/* (moved past a hand's tremor: a few pixels) */
*moved = fabsf(menu_pointer.x - menu_pointer.reported_x) >= 3.0f ||
fabsf(menu_pointer.y - menu_pointer.reported_y) >= 3.0f;
if (*moved)
{
menu_pointer.reported_x = menu_pointer.x;
menu_pointer.reported_y = menu_pointer.y;
}
return 1;
}
int halo_vr_menu_pointer_at(int *x, int *y)
{
if (!menu_pointer.valid)
return 0;
*x = (int)menu_pointer.x;
*y = (int)menu_pointer.y;
return 1;
}
/* ---------- players' names
Names over the heads (hud.c) are drawn into slots of a region of the back
@@ -2273,6 +2400,12 @@ int halo_vr_scope(short local_player_index, float magnification, float position[
return 0;
}
int halo_vr_scope_pass(void) { return 0; }
int halo_vr_menu_pointer(int *x, int *y, int *click_x, int *click_y, int *moved, int *clicks)
{
(void)x; (void)y; (void)click_x; (void)click_y; (void)moved; *clicks = 0;
return 0;
}
int halo_vr_menu_pointer_at(int *x, int *y) { (void)x; (void)y; return 0; }
int halo_vr_names_apart(void) { return 0; }
void halo_vr_name_slot(int slot, short bounds[4]) { (void)slot; (void)bounds; }
int halo_vr_add_name(const float point[3]) { (void)point; return -1; }
+3 -2
View File
@@ -573,9 +573,10 @@ static void vr_gamepad_state(XINPUT_GAMEPAD *pad)
if (left->buttons & HALO_XR_BUTTON_MENU) pad->wButtons |= XINPUT_GAMEPAD_START;
if (left->buttons & HALO_XR_BUTTON_THUMBSTICK) pad->wButtons |= XINPUT_GAMEPAD_LEFT_THUMB;
if (right->buttons & HALO_XR_BUTTON_THUMBSTICK) pad->wButtons |= XINPUT_GAMEPAD_RIGHT_THUMB;
if ((BYTE)(right->trigger * 255.0f) > pad->bAnalogButtons[XINPUT_GAMEPAD_RIGHT_TRIGGER])
/* (in the menus the triggers click the pointer: halo_vr_menu_pointer) */
if (!menus && (BYTE)(right->trigger * 255.0f) > pad->bAnalogButtons[XINPUT_GAMEPAD_RIGHT_TRIGGER])
pad->bAnalogButtons[XINPUT_GAMEPAD_RIGHT_TRIGGER] = (BYTE)(right->trigger * 255.0f);
if ((BYTE)(left->trigger * 255.0f) > pad->bAnalogButtons[XINPUT_GAMEPAD_LEFT_TRIGGER])
if (!menus && (BYTE)(left->trigger * 255.0f) > pad->bAnalogButtons[XINPUT_GAMEPAD_LEFT_TRIGGER])
pad->bAnalogButtons[XINPUT_GAMEPAD_LEFT_TRIGGER] = (BYTE)(left->trigger * 255.0f);
if (menus)
{
+101 -7
View File
@@ -677,6 +677,9 @@ struct widget_instance;
#include "text/text_group.h"
#include "text/unicode.h"
#include "ui_widget.h"
#ifdef HALO_ANDROID
#include "halo_vr.h"
#endif
/* ---------- constants */
@@ -5269,6 +5272,8 @@ struct ui_mouse_target
rectangle2d bounds;
short kind;
short button_index;
/* (a button's: where its icon begins) */
short icon_x0;
};
static struct ui_mouse_target ui_mouse_targets[UI_MOUSE_MAXIMUM_TARGETS];
@@ -5403,6 +5408,7 @@ static void ui_mouse_note_target(
rectangle2d bounds = definition->bounds;
short button_index;
short kind;
short icon_x0;
if (!ui_mouse_noting_targets ||
ui_mouse_target_count >= UI_MOUSE_MAXIMUM_TARGETS ||
@@ -5415,13 +5421,22 @@ static void ui_mouse_note_target(
bounds.y0 += offset.y;
bounds.y1 += offset.y;
button_index = ui_mouse_key_button(widget);
icon_x0 = bounds.x0;
if (button_index != NONE)
{
/* the icon and its label: the text beginning just right of it */
struct widget_instance *sibling;
rectangle2d label_bounds;
short best_distance = 17;
#ifdef HALO_ANDROID
/* (VR: a controller points less steadily than a mouse: the label a
little further off is the button's too) */
short most_distance = halo_vr_active() ? 25 : 17;
#else
short most_distance = 17;
#endif
best_distance = most_distance;
kind = _ui_mouse_target_button;
for (sibling = parent ? parent->child : NULL; sibling; sibling = sibling->next)
{
@@ -5445,12 +5460,23 @@ static void ui_mouse_note_target(
label_bounds = sibling_bounds;
}
}
if (best_distance < 17)
if (best_distance < most_distance)
{
bounds.x1 = MAX(bounds.x1, label_bounds.x1);
bounds.y0 = MIN(bounds.y0, label_bounds.y0);
bounds.y1 = MAX(bounds.y1, label_bounds.y1);
}
#ifdef HALO_ANDROID
/* (and a margin about it: above and below, little to the sides,
where the next key is) */
if (halo_vr_active())
{
bounds.x0 -= 2;
bounds.x1 += 2;
bounds.y0 -= 4;
bounds.y1 += 4;
}
#endif
}
else if (parent && ui_mouse_list_shows_several(parent))
{
@@ -5482,6 +5508,7 @@ static void ui_mouse_note_target(
target->bounds = bounds;
target->kind = kind;
target->button_index = button_index;
target->icon_x0 = icon_x0;
return;
}
@@ -5492,16 +5519,33 @@ static struct ui_mouse_target *ui_mouse_target_at(
short y)
{
long index;
struct ui_mouse_target *button = NULL;
for (index = ui_mouse_target_count - 1; index >= 0; index--)
{
rectangle2d const *bounds = &ui_mouse_targets[index].bounds;
struct ui_mouse_target *target = &ui_mouse_targets[index];
rectangle2d const *bounds = &target->bounds;
if (x >= bounds->x0 && x < bounds->x1 && y >= bounds->y0 && y < bounds->y1)
return &ui_mouse_targets[index];
if (!(x >= bounds->x0 && x < bounds->x1 && y >= bounds->y0 && y < bounds->y1))
continue;
/* port: a key's label's text box can be far wider than its text, over
the next key: of keys under the point, the one whose icon is nearest
on its left (each key from its icon to the next one's) */
if (target->kind == _ui_mouse_target_button)
{
if (!button ||
(target->icon_x0 <= x + 2 && (button->icon_x0 > x + 2 || target->icon_x0 > button->icon_x0)))
{
button = target;
}
continue;
}
if (!button)
return target;
break;
}
return NULL;
return button;
}
static boolean ui_mouse_widget_has_focus(
@@ -5692,6 +5736,8 @@ enum
MAXIMUM_SHOWN_PUBLIC_GAMES = 32,
/* the online games panel's rows: "enter a code" and the games */
ONLINE_PANEL_ROWS = 11,
/* (the rows before the games: "enter a code") */
ONLINE_PANEL_FIXED_ROWS = 1,
};
static struct
@@ -5932,6 +5978,48 @@ static boolean ui_join_code_take_event(
return FALSE;
}
/* the pointer over the VR port's panels: the row under it chosen, and a
click on it as A (TRUE: a panel is open and took it) */
static boolean ui_join_code_take_pointer(
struct halo_ui_pointer const *pointer)
{
struct event_record press;
short row;
/* (a click where it was clicked, which may be a moment before) */
short x = pointer->left_clicks ? pointer->click_x : pointer->x;
short y = pointer->left_clicks ? pointer->click_y : pointer->y;
csmemset(&press, 0, sizeof(press));
press.type = _event_type_button;
press.data.button.index = _gamepad_analog_button_a;
press.data.button.value = 1;
if (ui_join_code_globals.panel_open)
{
if (x >= 48 && x < 592 && y >= 128 && y < 128 + ONLINE_PANEL_ROWS * 22)
{
row = (short)(ui_join_code_globals.scroll + (y - 128) / 22);
if (row < ui_join_code_globals.game_count + ONLINE_PANEL_FIXED_ROWS)
{
ui_join_code_globals.selection = row;
if (pointer->left_clicks)
ui_join_code_panel_event(&press);
}
}
return TRUE;
}
if (ui_join_code_globals.host_panel_open)
{
if (x >= 148 && x < 492 && y >= 187 && y < 187 + HOST_PANEL_ROWS * 30)
{
ui_join_code_globals.host_selection = (short)((y - 187) / 30);
if (pointer->left_clicks)
ui_join_code_host_panel_event(&press);
}
return TRUE;
}
return FALSE;
}
/* the keyboard closed: the code typed, looked up */
static void ui_join_code_update(
void)
@@ -6186,8 +6274,9 @@ static void ui_widgets_process_mouse(
struct ui_mouse_target *target;
short controller_index = 0;
if (!halo_ui_pointer_update(ui_mouse_menus_active(), &pointer) ||
virtual_keyboard_active())
/* (the keyboard takes the pointer itself: virtual_keyboard.c) */
if (virtual_keyboard_active() ||
!halo_ui_pointer_update(ui_mouse_menus_active(), &pointer))
{
ui_mouse_press_count = 0;
ui_mouse_hover_pending = FALSE;
@@ -6195,6 +6284,11 @@ static void ui_widgets_process_mouse(
}
else
{
#ifdef HALO_ANDROID
/* (the VR port's panels over the menus take the pointer themselves) */
if (ui_join_code_take_pointer(&pointer))
return;
#endif
if (pointer.moved)
{
ui_mouse_hover_pending = TRUE;
+69
View File
@@ -1166,6 +1166,25 @@ static boolean virtual_keyboard_select(
return TRUE;
}
#ifdef HALO_ANDROID
/* the key drawn at a point of the menus' 640x480, or NONE */
static short virtual_keyboard_key_at(
short x,
short y)
{
short key;
for (key = 0; key < NUMBER_OF_VIRTUAL_KEYS; key++)
{
rectangle2d const *bounds = &keyboard_rect[key];
if (x >= bounds->x0 && x < bounds->x1 && y >= bounds->y0 && y < bounds->y1)
return key;
}
return NONE;
}
#endif
static void virtual_keyboard_process_internal(
void)
{
@@ -1304,6 +1323,56 @@ static void virtual_keyboard_process_internal(
}
}
#ifdef HALO_ANDROID
/* the VR port's: the controller's pointer (halo_ui_pointer.h) picks the
key under it, and its click types it */
{
struct halo_ui_pointer pointer;
if (halo_ui_pointer_update(TRUE, &pointer))
{
short x = pointer.left_clicks ? pointer.click_x : pointer.x;
short y = pointer.left_clicks ? pointer.click_y : pointer.y;
short key = virtual_keyboard_key_at(x, y);
if (key != NONE && (pointer.moved || pointer.left_clicks))
{
short row, column;
boolean found = FALSE;
for (row = 0; row < VIRTUAL_KEYBOARD_ROW_COUNT && !found; row++)
{
for (column = 0; column < VIRTUAL_KEYBOARD_COLUMN_COUNT && !found; column++)
{
if (virtual_keyboard_layout_table[row][column] == key)
{
virtual_keyboard_globals.row = row;
virtual_keyboard_globals.column = column;
found = TRUE;
}
}
}
}
if (pointer.left_clicks && key != NONE)
action = _event_key_select;
/* (the keys' hints under the keyboard, in its picture: B = BACK
cancels, A = ENTER finishes, as Done does) */
if (pointer.left_clicks && key == NONE && y >= 412 && y < 448)
{
if (x >= 380 && x < 480)
{
action = _event_cancel;
}
else if (x >= 488 && x < 600)
{
virtual_keyboard_globals.row = 0;
virtual_keyboard_globals.column = 0;
action = _event_key_select;
}
}
}
}
#endif
if (action != NONE)
{
virtual_keyboard_globals.last_key = virtual_keyboard_layout_table[
+22
View File
@@ -874,6 +874,28 @@ static void render_vr_overlay(
interface_draw_screen();
halo_screen_ui_offset(TRUE);
render_ui_widgets(window->local_player_index, &window->rasterizer_camera.viewport_bounds);
if (menus_active)
{
/* the controller's pointer on the menus' screen: a dot, ringed */
int pointer_x, pointer_y;
if (halo_vr_menu_pointer_at(&pointer_x, &pointer_y))
{
rectangle2d dot;
pointer_x -= (int)(halo_screen_width() - 640) / 2;
dot.x0 = (short)(pointer_x - 5);
dot.x1 = (short)(pointer_x + 5);
dot.y0 = (short)(pointer_y - 5);
dot.y1 = (short)(pointer_y + 5);
draw_quad(&dot, 0xE0102030);
dot.x0 += 2;
dot.x1 -= 2;
dot.y0 += 2;
dot.y1 -= 2;
draw_quad(&dot, 0xFF8FD3FF);
}
}
halo_screen_ui_offset(FALSE);
rasterizer_window_end();