From a9ffac76e5c9dd1f937a94aea97d3d182771c6e8 Mon Sep 17 00:00:00 2001 From: Sads Date: Sat, 13 Jun 2026 16:28:10 +0200 Subject: [PATCH] Grid/search polish, cloud nav, PS-button glyphs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Grid: hide the selection square while touch-scrolling, restore it on d-pad. Square (□) slides the top search bar in/out; the grid fills the vacated strip (escroll = scroll + bar_hidden). Smart search now requests withExif so results carry aspect ratios and use the justified layout instead of uniform columns. START no longer exits the running app (PS button only); dropped the HUD hints. Cloud page: a top-right O/Back button, and PlayStation face-button glyphs beside each control (X details, /\ upload all, [] auto-upload) drawn as anti-aliased outline masks on flat dark discs; the search clear is a bare red O ring. Added cov_tri/cov_sq/cov_cross/cov_circle glyphs with a shared GLYPH_W stroke, and an anti-aliased disc helper (disc_tex/draw_disc) for the buttons and toggle knob. goals.md: recorded the UI-polish round. --- goals.md | 10 ++ src/main.c | 271 ++++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 226 insertions(+), 55 deletions(-) diff --git a/goals.md b/goals.md index 4af2c5f..35ccddb 100644 --- a/goals.md +++ b/goals.md @@ -2,6 +2,16 @@ Task list for the current round of work. Checked items are done. +## 0. UI polish (current round) +- [x] Hide the white selection square while touch-scrolling; show it again + when the selection moves with the d-pad. +- [x] Square (□) hides/shows the top search bar with a sliding animation. + The hidden bar slides up and the grid shifts to fill the vacated strip + (escroll = scroll + bar_hidden); the scroll-follow math cancels out. +- [x] Search results must use the justified/dynamic grid layout: smart search + now requests withExif so results carry width/height (without it every + asset had no ratio and the grid collapsed to uniform 3 columns). + ## 1. Make asset/local storage scalable (remove 1000 / 500 caps) - [x] Replace fixed `MAX_ASSETS` (1000) server-asset arrays with dynamically grown (realloc, doubling) arrays. diff --git a/src/main.c b/src/main.c index b5bd37d..a524f06 100644 --- a/src/main.c +++ b/src/main.c @@ -2031,12 +2031,14 @@ static int in_tri(float px, float py, float ax, float ay, float bx, float by, return !(neg && pos); } +/* shared glyph line thickness (pixels), so every outline reads the same */ +#define GLYPH_W 3.0f + /* a thin ring outline with a straight, round-capped handle off the lower right */ static int cov_search(float px, float py, int s) { - float R = s * 0.30f, W = s * 0.11f; - if (W < 1.8f) W = 1.8f; - float ccx = s * 0.40f, ccy = s * 0.40f, hw = W * 0.5f, c45 = 0.70710678f; + float R = s * 0.30f, W = GLYPH_W, hw = W * 0.5f, c45 = 0.70710678f; + float ccx = s * 0.40f, ccy = s * 0.40f; float dr = sqrtf((px - ccx) * (px - ccx) + (py - ccy) * (py - ccy)); int ring = (dr <= R && dr >= R - W); float ax = ccx + c45 * (R - hw), ay = ccy + c45 * (R - hw); @@ -2094,12 +2096,57 @@ static vita2d_texture *bake_icon(int s, int (*inside)(float, float, int)) return t; } +/* PlayStation triangle button glyph: an up-pointing outline, fixed-width edges */ +static int cov_tri(float px, float py, int s) +{ + float ax = s * 0.5f, ay = s * 0.10f; /* lifted up a bit */ + float bx = s * 0.14f, by = s * 0.80f; + float cx = s * 0.86f, cy = s * 0.80f; + if (!in_tri(px, py, ax, ay, bx, by, cx, cy)) + return 0; + float d = fminf(seg_dist(px, py, ax, ay, bx, by), + fminf(seg_dist(px, py, bx, by, cx, cy), + seg_dist(px, py, cx, cy, ax, ay))); + return d <= GLYPH_W; /* band just inside the edges */ +} + +/* PlayStation square button glyph: an outline square, fixed-width border */ +static int cov_sq(float px, float py, int s) +{ + float lo = s * 0.18f, hi = s * 0.82f; + int outer = px >= lo && px <= hi && py >= lo && py <= hi; + int inner = px >= lo + GLYPH_W && px <= hi - GLYPH_W && + py >= lo + GLYPH_W && py <= hi - GLYPH_W; + return outer && !inner; +} + +/* PlayStation cross button glyph: a thick X — two rotated rectangles, so the + * arm ends are flat and perpendicular (sharp), not box-clipped or rounded. */ +static int cov_cross(float px, float py, int s) +{ + float hw = GLYPH_W * 0.5f; /* half stroke */ + float half = s * 0.30f; /* arm half-length */ + float dx = px - s * 0.5f, dy = py - s * 0.5f; + float a = (dx + dy) * 0.70710678f; /* along one diagonal */ + float b = (dx - dy) * 0.70710678f; /* along the other */ + return (fabsf(b) <= hw && fabsf(a) <= half) || + (fabsf(a) <= hw && fabsf(b) <= half); +} + +/* PlayStation circle button glyph: an outline ring, fixed-width stroke */ +static int cov_circle(float px, float py, int s) +{ + float cx = s * 0.5f, cy = s * 0.5f, R = s * 0.38f, W = GLYPH_W; + float d = sqrtf((px - cx) * (px - cx) + (py - cy) * (py - cy)); + return d <= R && d >= R - W; +} + /* cache one texture per glyph; all are drawn at a fixed size each frame */ static vita2d_texture *icon_tex(int which, int s) { - static vita2d_texture *cache[3]; - static int cs[3]; - if (which < 0 || which > 2) + static vita2d_texture *cache[7]; + static int cs[7]; + if (which < 0 || which > 6) return NULL; if (cache[which] && cs[which] == s) return cache[which]; @@ -2109,7 +2156,11 @@ static vita2d_texture *icon_tex(int which, int s) cache[which] = NULL; } int (*fn)(float, float, int) = which == 0 ? cov_search : - which == 1 ? cov_map : cov_cloud; + which == 1 ? cov_map : + which == 2 ? cov_cloud : + which == 3 ? cov_tri : + which == 4 ? cov_sq : + which == 5 ? cov_cross : cov_circle; cache[which] = bake_icon(s, fn); cs[which] = s; return cache[which]; @@ -2117,14 +2168,69 @@ static vita2d_texture *icon_tex(int which, int s) #define ICON_SEARCH 0 #define ICON_MAP 1 #define ICON_CLOUD 2 +#define ICON_TRI 3 +#define ICON_SQUARE 4 +#define ICON_CROSS 5 +#define ICON_CIRCLE 6 -/* small clear (x) chip used on the right of the bar while a search is on */ -static void draw_clear_x(float cx, float cy, float s, unsigned int col) +/* a DualShock-style face button: a dark disc (subtle lighter rim) centred at + * (cx,cy) with the coloured glyph on top. each glyph keeps a per-call size. */ +/* an anti-aliased filled disc baked into a white alpha mask (4x4 supersampled), + * drawn tinted; replaces aliased vita2d_draw_fill_circle for UI discs */ +static vita2d_texture *disc_tex(int d) { - for (float t = -1.0f; t <= 1.0f; t += 0.2f) { - vita2d_draw_fill_circle(cx + t * s, cy + t * s, 1.5f, col); - vita2d_draw_fill_circle(cx + t * s, cy - t * s, 1.5f, col); - } + enum { NDISC = 8 }; + static vita2d_texture *cache[NDISC]; + static int cd[NDISC], n; + for (int i = 0; i < n; i++) + if (cd[i] == d) + return cache[i]; + if (n >= NDISC) + return NULL; + vita2d_texture *t = vita2d_create_empty_texture_format(d, d, + SCE_GXM_TEXTURE_FORMAT_A8B8G8R8); + if (!t) + return NULL; + uint32_t *data = vita2d_texture_get_datap(t); + int stride = (int)(vita2d_texture_get_stride(t) / 4); + memset(data, 0, (size_t)stride * d * 4); + float c = d / 2.0f, r = d / 2.0f; + for (int y = 0; y < d; y++) + for (int x = 0; x < d; x++) { + int hits = 0; + for (int sy = 0; sy < 4; sy++) + for (int sx = 0; sx < 4; sx++) { + float dx = x + (sx + 0.5f) / 4.0f - c; + float dy = y + (sy + 0.5f) / 4.0f - c; + if (dx * dx + dy * dy <= r * r) + hits++; + } + data[y * stride + x] = RGBA8(255, 255, 255, hits * 255 / 16); + } + cache[n] = t; + cd[n] = d; + n++; + return t; +} + +/* a filled (AA) disc at (cx,cy), tinted */ +static void draw_disc(float cx, float cy, float d, uint32_t col) +{ + vita2d_texture *t = disc_tex((int)(d + 0.5f)); + if (t) + vita2d_draw_texture_tint(t, cx - (int)(d + 0.5f) / 2.0f, + cy - (int)(d + 0.5f) / 2.0f, col); + else + vita2d_draw_fill_circle(cx, cy, d / 2.0f, col); +} + +static void draw_ps_button(float cx, float cy, float d, int icon, + uint32_t tint, int gs) +{ + draw_disc(cx, cy, d, RGBA8(46, 48, 56, 255)); /* flat dark face */ + vita2d_texture *g = icon_tex(icon, gs); + if (g) + vita2d_draw_texture_tint(g, cx - gs / 2.0f, cy - gs / 2.0f, tint); } /* pixel geometry of the search pill + the two buttons on its right, shared @@ -2142,51 +2248,59 @@ static void draw_clear_x(float cx, float cy, float s, unsigned int col) #define BAR_CLEAR_CX (BAR_M + BAR_W - 22.0f) /* centre of the clear chip */ /* one future-feature button: rounded-square background + a centred AA glyph */ -static void draw_bar_button(float cx, int icon) +static void draw_bar_button(float cx, float cy, int icon) { vita2d_texture *bg = rounded_mask_tex((int)BTN_SZ, (int)BTN_SZ, 11.0f); if (bg) - vita2d_draw_texture_tint(bg, cx - BTN_SZ / 2, BTN_CY - BTN_SZ / 2, + vita2d_draw_texture_tint(bg, cx - BTN_SZ / 2, cy - BTN_SZ / 2, RGBA8(46, 46, 52, 255)); int isz = 26; vita2d_texture *g = icon_tex(icon, isz); if (g) - vita2d_draw_texture_tint(g, cx - isz / 2.0f, BTN_CY - isz / 2.0f, + vita2d_draw_texture_tint(g, cx - isz / 2.0f, cy - isz / 2.0f, RGBA8(190, 190, 196, 255)); } -static void draw_search_bar(void) +/* draw the bar shifted vertically by `yoff` (Square slides it out of view) */ +static void draw_search_bar(float yoff) { unsigned int field = RGBA8(38, 38, 42, 255); + float by = BAR_Y + yoff, cy = BTN_CY + yoff; /* opaque strip so grid items scroll cleanly underneath the bar */ - vita2d_draw_rectangle(0, 0, SCREEN_W, SEARCH_H, RGBA8(16, 16, 16, 255)); + vita2d_draw_rectangle(0, yoff, SCREEN_W, SEARCH_H, RGBA8(16, 16, 16, 255)); /* borderless field: an AA rounded-rect mask tinted to the field colour */ vita2d_texture *mask = rounded_mask_tex((int)BAR_W, (int)BAR_H, BAR_R); if (mask) - vita2d_draw_texture_tint(mask, BAR_M, BAR_Y, field); + vita2d_draw_texture_tint(mask, BAR_M, by, field); else - vita2d_draw_rectangle(BAR_M, BAR_Y, BAR_W, BAR_H, field); + vita2d_draw_rectangle(BAR_M, by, BAR_W, BAR_H, field); int isz = 24; vita2d_texture *icon = icon_tex(ICON_SEARCH, isz); if (icon) vita2d_draw_texture_tint(icon, BAR_M + 22 - isz / 2.0f, - BAR_Y + BAR_H / 2 - isz / 2.0f, + cy - isz / 2.0f, RGBA8(190, 190, 196, 255)); /* placeholder buttons (map, cloud) for future features */ - draw_bar_button(BTN1_CX, ICON_MAP); - draw_bar_button(BTN2_CX, ICON_CLOUD); + draw_bar_button(BTN1_CX, cy, ICON_MAP); + draw_bar_button(BTN2_CX, cy, ICON_CLOUD); - float tx = BAR_M + 44, ty = BAR_Y + BAR_H - 13; + float tx = BAR_M + 44, ty = by + BAR_H - 13; if (g_search_active && g_search_query[0]) { char q[64]; snprintf(q, sizeof(q), "%.40s", g_search_query); draw_text(tx, ty, RGBA8(235, 235, 240, 255), 0.95f, q); - vita2d_draw_fill_circle(BAR_CLEAR_CX, BAR_Y + BAR_H / 2, 11, - RGBA8(70, 70, 76, 255)); - draw_clear_x(BAR_CLEAR_CX, BAR_Y + BAR_H / 2, 4, - RGBA8(210, 210, 215, 255)); + /* press O (or tap here) to clear: just the red ring glyph, no + * button disc/border on the pill */ + { + int gs = 26; + vita2d_texture *og = icon_tex(ICON_CIRCLE, gs); + if (og) + vita2d_draw_texture_tint(og, + BAR_CLEAR_CX - gs / 2.0f, cy - gs / 2.0f, + RGBA8(235, 90, 85, 255)); + } } else { draw_text(tx, ty, RGBA8(140, 140, 148, 255), 0.95f, "Search your photos"); @@ -2317,7 +2431,9 @@ static int run_smart_search(const char *query) char url[600]; snprintf(url, sizeof(url), "%s/api/search/smart", g_server); char body[320]; - snprintf(body, sizeof(body), "{\"query\":\"%s\"}", esc); + /* withExif so results carry width/height — without it every asset has + * no aspect ratio and the justified grid collapses to uniform columns */ + snprintf(body, sizeof(body), "{\"query\":\"%s\",\"withExif\":true}", esc); membuf buf; long code; @@ -3825,6 +3941,9 @@ int main(void) int mode = MODE_GRID; int sel = 0; int cloud_scroll = 0; /* first visible row on the upload-details list */ + int show_sel = 1; /* selection square: shown for d-pad, hidden for touch */ + int bar_shown = 1; /* top search bar visible */ + float bar_hidden = 0.0f;/* animated pixels the bar is slid up (0..SEARCH_H) */ /* periodic check for photos added to the server while we run */ uint64_t last_poll = sceKernelGetProcessTimeWide(); int grid_last_sel = -1; /* sel at the previous frame */ @@ -3871,8 +3990,6 @@ int main(void) unsigned int pressed = pad.buttons & ~prev_buttons; prev_buttons = pad.buttons; - if (pad.buttons & SCE_CTRL_START) - break; frame++; /* auto-backup: while enabled, queue any newly-found local-only @@ -3918,6 +4035,18 @@ int main(void) (pressed & SCE_CTRL_CIRCLE) != 0; int do_open_cloud = 0; /* tap on the cloud button */ + /* SQUARE slides the top bar in/out; animate the offset */ + if (pressed & SCE_CTRL_SQUARE) + bar_shown = !bar_shown; + float bar_t = bar_shown ? 0.0f : (float)SEARCH_H; + bar_hidden += (bar_t - bar_hidden) * 0.3f; + if (fabsf(bar_t - bar_hidden) < 0.5f) + bar_hidden = bar_t; + /* the d-pad (and month jumps) bring the selection square + * back; a touch drag hides it again (set below) */ + if (nav & dirs) + show_sel = 1; + if (nav & SCE_CTRL_RIGHT) sel++; if (nav & SCE_CTRL_LEFT) @@ -4031,15 +4160,19 @@ int main(void) touch_start_y = ty; touch_start_scroll = scroll; touch_vel = 0; - /* a contact on the bar opens the - * keyboard instead of scrolling */ - touch_on_bar = (ty < (float)SEARCH_H); + /* a contact on the (visible) bar opens + * the keyboard instead of scrolling */ + touch_on_bar = + (ty < (float)SEARCH_H - bar_hidden); } else if (!touch_on_bar) { float dy = ty - touch_start_y; if (!touch_dragged && (dy > 14.0f || dy < -14.0f)) touch_dragged = 1; if (touch_dragged) { + /* touch-scroll hides the + * selection square */ + show_sel = 0; float ns = touch_start_scroll - dy; if (ns < 0) ns = 0; @@ -4082,7 +4215,7 @@ int main(void) * (videos show their poster + * play button in the detail) */ int i = item_at(touch_x, - touch_y + scroll); + touch_y + scroll + bar_hidden); if (i >= 0) { sel = i; mode = MODE_DETAIL; @@ -4191,6 +4324,10 @@ int main(void) if (target > max_scroll) target = max_scroll; scroll += (target - scroll) * 0.35f; + /* on-screen scroll: the hidden bar shifts the grid up so + * it fills the vacated strip (the reveal-header math in + * scroll-follow cancels out, so it still uses `scroll`) */ + float escroll = scroll + bar_hidden; /* visible index range (item y is non-decreasing with i), * computed in the same pass as thumb eviction below */ @@ -4200,7 +4337,7 @@ int main(void) * the worker is currently loading); evicted textures * are recycled through the pool, see tex_release */ for (int i = 0; i < g_disp_count; i++) { - float dy = g_item_y[i] - scroll; + float dy = g_item_y[i] - escroll; if (dy > -g_item_h[i] && dy < SCREEN_H) { if (first_vis < 0) first_vis = i; @@ -4266,7 +4403,7 @@ int main(void) /* month/year header bands */ for (int h = 0; h < g_sect_count; h++) { - float hy = g_sect[h].y - scroll; + float hy = g_sect[h].y - escroll; if (hy + HEADER_H < 0 || hy > SCREEN_H) continue; draw_text(10, hy + HEADER_H - 12, @@ -4279,7 +4416,7 @@ int main(void) for (int i = first_vis; i >= 0 && i <= last_vis; i++) { float x = g_item_x[i]; - float y = g_item_y[i] - scroll; + float y = g_item_y[i] - escroll; float bx = x + CELL_PAD, by = y + CELL_PAD; float bw = g_item_w[i] - 2 * CELL_PAD; float bh = g_item_h[i] - 2 * CELL_PAD; @@ -4303,7 +4440,7 @@ int main(void) 0.85f, "VIDEO"); } draw_status_badge(bx, by, i, frame); - if (i == sel) + if (i == sel && show_sel) draw_sel_outline(x + 2, y + 2, g_item_w[i] - 4, g_item_h[i] - 4); @@ -4314,20 +4451,21 @@ int main(void) RGBA8(160, 160, 165, 255), "No results"); - /* pinned Immich-style search bar, on top of the grid */ - draw_search_bar(); + /* pinned Immich-style search bar, slid up by bar_hidden + * (Square toggles it); covers items up to its lower edge */ + draw_search_bar(-bar_hidden); char hud[200]; if (g_search_active) snprintf(hud, sizeof(hud), - "%d / %d results /\\ edit O clear " - "X view START exit", + "%d / %d results /\\ edit O clear " + "X view [] bar", g_disp_count > 0 ? sel + 1 : 0, g_disp_count); else snprintf(hud, sizeof(hud), "%d / %d%s %.10s X view /\\ search " - "START exit", + "[] bar", sel + 1, g_disp_count, g_next_page > 0 ? "+" : "", g_disp_count > 0 ? disp_date(sel) : ""); @@ -4791,16 +4929,14 @@ pf_skip: * (also [] = toggle, X = details, /\ = upload) */ float tgw = 60, tgh = 30, tgx = 470, tgy = 350; float bth = 46, bty = 408; - float dbx = 60, dbw = 360; /* See details */ - float ubx = dbx + dbw + 20, ubw = 320; /* Upload all */ + float dbx = 56, dbw = 300; /* See details (X glyph left) */ + float ubx = 402, ubw = 300; /* Upload all (/\ glyph left) */ int go_details = (pressed & SCE_CTRL_CROSS) != 0; int do_upload = (pressed & SCE_CTRL_TRIANGLE) != 0; int do_toggle = (pressed & SCE_CTRL_SQUARE) != 0; - - if (pressed & SCE_CTRL_CIRCLE) { - mode = MODE_GRID; - continue; - } + int do_back = (pressed & SCE_CTRL_CIRCLE) != 0; + /* O / Back button in the top-right corner */ + float bkd = 36, bkcx = SCREEN_W - 30, bkcy = 36; /* touch: tap the button to open the details list */ { @@ -4824,6 +4960,9 @@ pf_skip: touch_active = 0; if (touch_dragged) { /* ignore drags */ + } else if (touch_y < bkcy + bkd && + touch_x > bkcx - bkd - 70) { + do_back = 1; /* top-right Back */ } else if (touch_y >= tgy - 6 && touch_y <= tgy + tgh + 6 && touch_x >= 60 && touch_x <= tgx + tgw) { @@ -4840,6 +4979,10 @@ pf_skip: } } + if (do_back) { + mode = MODE_GRID; + continue; + } if (do_toggle) { g_autobackup = !g_autobackup; save_autobackup(); @@ -4863,6 +5006,15 @@ pf_skip: draw_centered(40, RGBA8(255, 255, 255, 255), "Server & Backup"); + /* top-right O / Back button: "Back" label then the circle */ + { + int bw = text_width(0.95f, "Back"); + draw_text(bkcx - bkd / 2 - 8 - bw, bkcy + 6, + RGBA8(220, 220, 225, 255), 0.95f, "Back"); + draw_ps_button(bkcx, bkcy, bkd, ICON_CIRCLE, + RGBA8(235, 90, 85, 255), 26); + } + char line[300]; uint32_t c = RGBA8(220, 220, 220, 255); uint32_t dim = RGBA8(150, 150, 155, 255); @@ -4921,11 +5073,14 @@ pf_skip: vita2d_draw_rectangle(tgx, tgy, tgw, tgh, trk); float kx = g_autobackup ? tgx + tgw - tgh / 2.0f : tgx + tgh / 2.0f; - vita2d_draw_fill_circle(kx, tgy + tgh / 2.0f, tgh / 2.0f - 3.0f, - RGBA8(245, 245, 250, 255)); + draw_disc(kx, tgy + tgh / 2.0f, tgh - 6.0f, + RGBA8(245, 245, 250, 255)); draw_text(tgx + tgw + 14, tgy + 21, g_autobackup ? RGBA8(120, 200, 120, 255) : dim, 0.95f, g_autobackup ? "ON" : "OFF"); + /* PS square button cueing the [] shortcut, left of switch */ + draw_ps_button(tgx - 23, tgy + tgh / 2, 38, ICON_SQUARE, + RGBA8(232, 120, 175, 255), 31); /* See details + Upload all buttons */ vita2d_texture *db = rounded_mask_tex((int)dbw, (int)bth, 10.0f); @@ -4938,6 +5093,9 @@ pf_skip: snprintf(line, sizeof(line), "See details (%d remaining)", remain); draw_text(dbx + 18, bty + bth - 16, RGBA8(230, 230, 235, 255), 0.95f, line); + /* PS cross button cueing the X shortcut, left of details */ + draw_ps_button(dbx - 23, bty + bth / 2, 38, ICON_CROSS, + RGBA8(120, 150, 235, 255), 37); vita2d_texture *ub = rounded_mask_tex((int)ubw, (int)bth, 10.0f); /* the upload button reads as active (indigo) when there's @@ -4950,8 +5108,11 @@ pf_skip: vita2d_draw_rectangle(ubx, bty, ubw, bth, uc); draw_text(ubx + 18, bty + bth - 16, RGBA8(245, 245, 250, 255), 0.95f, "Upload all"); + /* PS triangle button cueing the /\ shortcut, left of upload */ + draw_ps_button(ubx - 23, bty + bth / 2, 38, ICON_TRI, + RGBA8(95, 205, 130, 255), 28); - draw_hud("[] auto-upload X details /\\ upload all O back START exit"); + draw_hud("X / tap details O back"); vita2d_end_drawing(); vita2d_swap_buffers(); } else if (mode == MODE_CLOUD_DETAILS) { @@ -5017,7 +5178,7 @@ pf_skip: RGBA8(120, 200, 120, 255), "Everything is backed up"); - draw_hud("Up/Down scroll O back START exit"); + draw_hud("Up/Down scroll O back"); vita2d_end_drawing(); vita2d_swap_buffers(); }