Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,9 @@ jobs:
- name: Debug overlays
if: runner.os == 'Linux'
run: LIBGL_ALWAYS_SOFTWARE=1 xvfb-run -a -s "-screen 0 1600x960x24" python3 tests/overlaycheck.py build/splashedit
- name: Menu check marks and dots
if: runner.os == 'Linux'
run: LIBGL_ALWAYS_SOFTWARE=1 xvfb-run -a -s "-screen 0 1600x960x24" python3 tests/menumarkcheck.py build/splashedit
- name: Boot logo off
if: runner.os == 'Linux'
run: LIBGL_ALWAYS_SOFTWARE=1 xvfb-run -a -s "-screen 0 1600x960x24" python3 tests/bootlogocheck.py build/splashedit build/emulator_tests
Expand Down
37 changes: 28 additions & 9 deletions src/mockup/main_screen.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2596,6 +2596,7 @@ struct MenuEntry {
enum Kind { Item, Separator, Heading } kind = Item;
std::string label, shortcut;
bool enabled = true, checked = false;
bool choice = false; // one of a group: the picked one shows a dot, an on/off item a check mark
std::function<void()> run;
};

Expand All @@ -2608,6 +2609,12 @@ static MenuEntry item(std::string label, std::string shortcut, bool enabled, std
e.run = std::move(run);
return e;
}
// One option of a group (tool, look, console count): the picked one shows a dot.
static MenuEntry choice(std::string label, std::string shortcut, std::function<void()> run, bool picked) {
MenuEntry e = item(std::move(label), std::move(shortcut), true, std::move(run), picked);
e.choice = true;
return e;
}
static MenuEntry separator() {
MenuEntry e;
e.kind = MenuEntry::Separator;
Expand Down Expand Up @@ -2742,19 +2749,19 @@ static std::vector<MenuEntry> menuEntries(int which, State& st, editor::Document
case 6: // the Players button's list
m.push_back(heading("Consoles Play boots"));
for (int n = 1; n <= editor::kMaxPlayers; ++n)
m.push_back(item(n == 1 ? std::string("1 Console") : n == 2 ? std::string("2 Linked Consoles") : std::to_string(n) + " Consoles",
"", true, [&st, n] { setPlayers(st, n); }, st.play.tools.players == n));
m.push_back(choice(n == 1 ? std::string("1 Console") : n == 2 ? std::string("2 Linked Consoles") : std::to_string(n) + " Consoles",
"", [&st, n] { setPlayers(st, n); }, st.play.tools.players == n));
m.push_back(separator());
m.push_back(item("Use the Relay for 2 Consoles", "", true, [&] { setRelay(st, !st.play.tools.relay); }, st.play.tools.relay));
break;
case 4: // View
m.push_back(item("Hand", "Q", true, [&] { st.tool = 0; }, st.tool == 0));
m.push_back(item("Move", "W", true, [&] { st.tool = 1; }, st.tool == 1));
m.push_back(item("Rotate", "E", true, [&] { st.tool = 2; }, st.tool == 2));
m.push_back(item("Scale", "R", true, [&] { st.tool = 3; }, st.tool == 3));
m.push_back(choice("Hand", "Q", [&] { st.tool = 0; }, st.tool == 0));
m.push_back(choice("Move", "W", [&] { st.tool = 1; }, st.tool == 1));
m.push_back(choice("Rotate", "E", [&] { st.tool = 2; }, st.tool == 2));
m.push_back(choice("Scale", "R", [&] { st.tool = 3; }, st.tool == 3));
m.push_back(separator());
m.push_back(item("PS1 Look", "", true, [&] { st.viewMode = 0; }, st.viewMode == 0));
m.push_back(item("Clean Look", "", true, [&] { st.viewMode = 1; }, st.viewMode == 1));
m.push_back(choice("PS1 Look", "", [&] { st.viewMode = 0; }, st.viewMode == 0));
m.push_back(choice("Clean Look", "", [&] { st.viewMode = 1; }, st.viewMode == 1));
m.push_back(separator());
m.push_back(item("Snap", "", true, [&] { st.snap = !st.snap; }, st.snap));
m.push_back(item("Orthographic", "", true, [&] { view.setOrtho(!view.ortho()); }, view.ortho()));
Expand Down Expand Up @@ -2852,7 +2859,19 @@ static std::function<void()> menuRows(const std::vector<MenuEntry>& entries, flo
if (!menu.empty()) nameRect(menu + "/" + e.label, ir);
const bool on = e.enabled && hit.hover > 0;
if (on) dl->AddRectFilled(ir.Min, ir.Max, lerpColor(rgb(0x2a2f39, 0), color::hover, hit.hover), radius::field);
if (e.checked) dl->AddCircleFilled(ImVec2(ir.Min.x + space::md, ir.GetCenter().y), 3.5f, color::accent, 12);
if (e.checked) {
// Scripted checks find the mark as <menu>/<label>/dot or /check.
const ImVec2 c(ir.Min.x + space::md, ir.GetCenter().y);
if (e.choice) {
dl->AddCircleFilled(c, 3.5f, color::accent, 12);
if (!menu.empty()) nameRect(menu + "/" + e.label + "/dot", ImRect(c - ImVec2(4, 4), c + ImVec2(4, 4)));
} else {
const ImVec2 cs = measure(f.medium, type::label, icon::check);
const ImVec2 at(c.x - cs.x * 0.5f, centerY(f.medium, type::label, ir.Min.y, ir.Max.y));
text(dl, at, f.medium, type::label, color::accent, icon::check);
if (!menu.empty()) nameRect(menu + "/" + e.label + "/check", ImRect(at, at + cs));
}
}
const ImU32 tc = !e.enabled ? color::textFaint : on && hit.hover > 0.5f ? color::text : color::textDim;
text(dl, ImVec2(ir.Min.x + 28, centerY(f.regular, type::body, ir.Min.y, ir.Max.y)), f.regular, type::body, tc, e.label.c_str());
if (!e.shortcut.empty()) {
Expand Down
1 change: 1 addition & 0 deletions src/ui/icons.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ inline constexpr const char* settings = "\xee\x85\x94"; // e154
inline constexpr const char* move = "\xee\x8b\xa5"; // e2e5
inline constexpr const char* rotate = "\xee\x8b\xaa"; // e2ea
inline constexpr const char* restart = "\xee\x85\x88"; // e148 rotate-ccw
inline constexpr const char* check = "\xee\x81\xac"; // e06c
inline constexpr const char* scale = "\xee\x8b\xab"; // e2eb
inline constexpr const char* warning = "\xee\x86\x93"; // e193
inline constexpr const char* monitor = "\xee\x84\x9d"; // e11d
Expand Down
129 changes: 129 additions & 0 deletions tests/menumarkcheck.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# Title-bar menu marks, driven through the GUI: an on/off item that is on
# shows a check mark, and the picked option of a group (tool, look, console
# count) shows a dot. The editor names each mark <menu>/<label>/check or
# <menu>/<label>/dot while the menu is open, and the screenshot is read
# inside each mark for the accent colour, so a named mark that drew nothing
# fails too.
# usage: python3 menumarkcheck.py <splashedit> (needs a display: xvfb-run -a)
import os, re, shutil, subprocess, sys, tempfile, zlib, struct

editor = os.path.abspath(sys.argv[1])
here = os.path.dirname(os.path.abspath(__file__))
example = os.path.join(here, '..', 'examples', 'courtyard')
fails = 0
work = tempfile.mkdtemp()
settings = os.path.join(work, 'prefs', 'play.cfg')
ENV = dict(os.environ, SPLASHEDIT_REDUX='/nonexistent/pcsx-redux')
ACCENT = (0x8b, 0x7b, 0xff)


def check(name, ok, detail=''):
global fails
print(('ok ' if ok else 'FAIL ') + name + ('' if ok else ': ' + str(detail)))
fails += not ok


def readpng(path):
"""RGB rows of an 8-bit RGB or RGBA PNG, without third-party modules."""
data = open(path, 'rb').read()
pos, idat, w = 8, b'', 0
while pos < len(data):
n, kind = struct.unpack('>I4s', data[pos:pos + 8])
body = data[pos + 8:pos + 8 + n]
if kind == b'IHDR':
w, h, depth, ctype = struct.unpack('>IIBB', body[:10])
bpp = 4 if ctype == 6 else 3
elif kind == b'IDAT':
idat += body
pos += 12 + n
raw = zlib.decompress(idat)
rows, prev, stride = [], bytearray(w * bpp), w * bpp
for y in range(h):
f = raw[y * (stride + 1)]
line = bytearray(raw[y * (stride + 1) + 1:(y + 1) * (stride + 1)])
for i in range(stride):
a = line[i - bpp] if i >= bpp else 0
b = prev[i]
c = prev[i - bpp] if i >= bpp else 0
if f == 1:
line[i] = (line[i] + a) & 255
elif f == 2:
line[i] = (line[i] + b) & 255
elif f == 3:
line[i] = (line[i] + (a + b) // 2) & 255
elif f == 4:
p = a + b - c
pa, pb, pc = abs(p - a), abs(p - b), abs(p - c)
line[i] = (line[i] + (a if pa <= pb and pa <= pc else b if pb <= pc else c)) & 255
rows.append([tuple(line[x * bpp:x * bpp + 3]) for x in range(w)])
prev = line
return rows


def accentPixels(rows, rect):
x0, y0, x1, y1 = rect
n = 0
for y in range(max(0, y0), min(len(rows), y1 + 1)):
for x in range(max(0, x0), min(len(rows[0]), x1 + 1)):
p = rows[y][x]
if all(abs(p[i] - ACCENT[i]) < 40 for i in range(3)):
n += 1
return n


def run(args):
"""Runs the editor on a fresh courtyard copy, then `args`, and takes a
screenshot with the last menu still open. Returns the named marks
{name: rect} and the screenshot's rows."""
project = os.path.join(tempfile.mkdtemp(), 'courtyard')
shutil.copytree(example, project)
shot = os.path.join(project, 'shot.png')
cmd = [editor, '--size', '1600x960', '--project', project, '--play-settings', settings, '--wait', '5'] + args + \
['--wait', '5', '--screenshot', shot]
r = subprocess.run(cmd, capture_output=True, text=True, env=ENV, timeout=180)
lines = (r.stdout + r.stderr).strip().splitlines()
missing = [l for l in lines if 'no such control drawn' in l]
check('every control the script names was drawn', not missing, missing)
marks = {}
for l in lines:
m = re.match(r'control (menu\..+/(?:check|dot)) at (-?\d+),(-?\d+) (-?\d+),(-?\d+)$', l)
if m:
marks[m.group(1)] = tuple(int(v) for v in m.groups()[1:])
rows = readpng(shot) if os.path.exists(shot) else []
return marks, rows


def drawn(marks, rows, name):
return name in marks and rows and accentPixels(rows, marks[name]) >= 6


TOOLS = ('Hand', 'Move', 'Rotate', 'Scale')

# View: one tool and one look carry a dot; Project and Snap (on by
# default) carry check marks.
marks, rows = run(['--click', '@menu.view', '--wait', '3'])
tools = [k for k in marks if k.split('/')[1] in TOOLS]
check('exactly one tool is marked, with a dot', len(tools) == 1 and tools[0].endswith('/dot') and drawn(marks, rows, tools[0]), marks)
check('PS1 Look, the picked look, shows a dot', drawn(marks, rows, 'menu.view/PS1 Look/dot'), marks)
check('no option of a group shows a check mark',
not any(k.endswith('/check') and k.split('/')[1] in TOOLS + ('PS1 Look', 'Clean Look') for k in marks), marks)
check('Snap on shows a check mark', drawn(marks, rows, 'menu.view/Snap/check'), marks)
check('Snap on shows no dot', 'menu.view/Snap/dot' not in marks, marks)

# Snap off: no mark at all; picking Rotate moves the dot to it.
marks, rows = run(['--click', '@menu.view', '--wait', '3', '--click', '@menu.view/Snap', '--wait', '3', '--click', '@menu.view',
'--wait', '3', '--click', '@menu.view/Rotate', '--wait', '3', '--click', '@menu.view', '--wait', '3'])
check('Snap off shows no mark', not any(k.startswith('menu.view/Snap/') for k in marks), marks)
check('Rotate, once picked, shows the only tool dot',
drawn(marks, rows, 'menu.view/Rotate/dot') and [k for k in marks if k.split('/')[1] in TOOLS] == ['menu.view/Rotate/dot'], marks)

# Players: the picked console count shows a dot. Build: Boot Logo in Test
# Runs, an on/off item, is a check mark when marked.
marks, rows = run(['--click', '@menu.build', '--wait', '3', '--click', '@menu.build/Players: 1 player...', '--wait', '3'])
check('1 Console, the picked count, shows a dot', drawn(marks, rows, 'menu.players/1 Console/dot'), marks)
marks, rows = run(['--click', '@menu.build', '--wait', '3'])
logo = [k for k in marks if k.startswith('menu.build/Boot Logo in Test Runs/')]
check('Boot Logo in Test Runs is a check mark', logo == ['menu.build/Boot Logo in Test Runs/check'] and drawn(marks, rows, logo[0]), logo)

print('%d failure%s' % (fails, '' if fails == 1 else 's'))
sys.exit(1 if fails else 0)
Loading