mirror of
https://github.com/DevvNirvana/afterglow.git
synced 2026-10-23 21:00:38 +02:00
222 stations in 17 genres, 56 visualizer scenes, Ogg and Opus playback, station logos, crossfade, My Music from USB, a command box and live BPM sync.
748 lines
28 KiB
C++
748 lines
28 KiB
C++
// ps5-homebrew-ui - Tests: the actions group (buttons, hold, quick actions, banner, tour).
|
|
// Copyright (C) 2026 BlackBearReloaded
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
#include "component_fixture.hpp"
|
|
#include "concepts/components/page.hpp"
|
|
#include "ui/components/banner.hpp"
|
|
#include "ui/components/button.hpp"
|
|
#include "ui/components/button_group.hpp"
|
|
#include "ui/components/coach_mark.hpp"
|
|
#include "ui/components/hold_button.hpp"
|
|
#include "ui/components/quick_action.hpp"
|
|
|
|
#include <gtest/gtest.h>
|
|
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
namespace
|
|
{
|
|
|
|
using hui::Action;
|
|
using hui::Direction;
|
|
using hui::audio::Cue;
|
|
using hui::gfx::Rect;
|
|
using hui::ui::Banner;
|
|
using hui::ui::BannerLook;
|
|
using hui::ui::ButtonGroup;
|
|
using hui::ui::ButtonRole;
|
|
using hui::ui::ButtonSize;
|
|
using hui::ui::CoachMark;
|
|
using hui::ui::CoachStep;
|
|
using hui::ui::Event;
|
|
using hui::ui::GroupMode;
|
|
using hui::ui::HoldButton;
|
|
using hui::ui::HoldVariant;
|
|
using hui::ui::IconButton;
|
|
using hui::ui::PushButton;
|
|
using hui::ui::QuickAction;
|
|
using hui::ui::QuickActionBar;
|
|
using hui::ui::SplitButton;
|
|
using hui::ui::StatusKind;
|
|
using hui::ui::TooltipPlacement;
|
|
|
|
class ComponentsActions : public hui::testing::ComponentFixture
|
|
{
|
|
protected:
|
|
template <typename Component> void settle(Component &component, float seconds = 1.0f)
|
|
{
|
|
for (int i = 0, frames = static_cast<int>(seconds / kFrame); i < frames; ++i)
|
|
component.update(kFrame);
|
|
}
|
|
|
|
// One input, then time for the animation to finish.
|
|
template <typename Component> Event send(Component &component, const hui::InputFrame &input)
|
|
{
|
|
feedback_.clear();
|
|
const Event event = component.handle(input, feedback_);
|
|
settle(component);
|
|
return event;
|
|
}
|
|
|
|
// Confirm kept down for a while: handle() and update() every frame, as a
|
|
// screen does. Returns how often the button reported Event::activated.
|
|
int hold(HoldButton &button, float seconds, Action action = Action::confirm)
|
|
{
|
|
int activations = 0;
|
|
const int frames = static_cast<int>(seconds / kFrame);
|
|
for (int i = 0; i < frames; ++i)
|
|
{
|
|
hui::InputFrame input = idle();
|
|
input.held = hui::action_bit(action);
|
|
if (i == 0)
|
|
input.pressed = input.held;
|
|
if (button.handle(input, feedback_) == Event::activated)
|
|
++activations;
|
|
button.update(kFrame);
|
|
}
|
|
return activations;
|
|
}
|
|
|
|
static hui::InputFrame repeat(Direction direction)
|
|
{
|
|
hui::InputFrame input = nav(direction);
|
|
input.nav_repeat = true;
|
|
return input;
|
|
}
|
|
|
|
static std::vector<CoachStep> steps()
|
|
{
|
|
return {
|
|
{{200.0f, 300.0f, 300.0f, 64.0f}, "First", "The first thing.", TooltipPlacement::below},
|
|
{{1500.0f, 880.0f, 300.0f, 64.0f}, "Second", "In a corner.", TooltipPlacement::below},
|
|
{{900.0f, 500.0f, 200.0f, 64.0f}, "Third", "The last one.", TooltipPlacement::right},
|
|
};
|
|
}
|
|
};
|
|
|
|
TEST_F(ComponentsActions, PushButtonActivatesAndRefusesWhileDisabledOrLoading)
|
|
{
|
|
PushButton button;
|
|
button.label = "Save";
|
|
button.set_bounds({100.0f, 100.0f, 320.0f, 64.0f});
|
|
button.set_active(true);
|
|
EXPECT_EQ(send(button, press(Action::confirm)), Event::activated);
|
|
EXPECT_TRUE(asked(Cue::select));
|
|
EXPECT_GT(feedback_.rumble_strength, 0.0f);
|
|
EXPECT_EQ(send(button, nav(Direction::left)), Event::none); // moving is the screen's job
|
|
|
|
button.set_loading(true);
|
|
EXPECT_EQ(send(button, press(Action::confirm)), Event::refused);
|
|
EXPECT_TRUE(asked(Cue::error));
|
|
button.set_loading(false);
|
|
button.set_disabled(true);
|
|
EXPECT_EQ(send(button, press(Action::confirm)), Event::refused);
|
|
button.set_disabled(false);
|
|
EXPECT_EQ(send(button, press(Action::confirm)), Event::activated);
|
|
|
|
button.style.sounds.activate = Cue::count;
|
|
button.style.sounds.rumble = 0.0f;
|
|
EXPECT_EQ(send(button, press(Action::confirm)), Event::activated);
|
|
EXPECT_TRUE(feedback_.cues.empty());
|
|
EXPECT_EQ(feedback_.rumble_strength, 0.0f);
|
|
}
|
|
|
|
TEST_F(ComponentsActions, PushButtonSizesAndHugging)
|
|
{
|
|
PushButton button;
|
|
button.label = "Go";
|
|
button.set_bounds({100.0f, 100.0f, 600.0f, 100.0f});
|
|
EXPECT_FLOAT_EQ(button.rect(fonts_).w, 600.0f);
|
|
EXPECT_FLOAT_EQ(button.rect(fonts_).h, 64.0f);
|
|
EXPECT_FLOAT_EQ(button.rect(fonts_).y, 118.0f); // centred in its bounds
|
|
button.style.size = ButtonSize::small;
|
|
EXPECT_FLOAT_EQ(button.rect(fonts_).h, 48.0f);
|
|
button.style.size = ButtonSize::large;
|
|
EXPECT_FLOAT_EQ(button.rect(fonts_).h, 80.0f);
|
|
button.style.height = 70.0f;
|
|
EXPECT_FLOAT_EQ(button.rect(fonts_).h, 70.0f);
|
|
|
|
button.style.full_width = false;
|
|
const float bare = button.rect(fonts_).w;
|
|
EXPECT_LT(bare, 300.0f);
|
|
button.glyph = hui::ui::Button::triangle;
|
|
EXPECT_GT(button.rect(fonts_).w, bare); // the glyph takes room
|
|
button.style.align = hui::gfx::Align::right;
|
|
const Rect right = button.rect(fonts_);
|
|
EXPECT_NEAR(right.x + right.w, 700.0f, 0.01f);
|
|
button.style.min_width = 400.0f;
|
|
EXPECT_FLOAT_EQ(button.rect(fonts_).w, 400.0f);
|
|
}
|
|
|
|
TEST_F(ComponentsActions, IconButtonTogglesCountsAndRefuses)
|
|
{
|
|
IconButton button;
|
|
button.tip = "Mute";
|
|
button.set_bounds({100.0f, 100.0f, 80.0f, 64.0f});
|
|
EXPECT_FLOAT_EQ(button.rect().w, 64.0f); // the smaller side
|
|
button.set_active(true);
|
|
EXPECT_EQ(send(button, press(Action::confirm)), Event::activated);
|
|
EXPECT_TRUE(asked(Cue::select));
|
|
|
|
button.style.toggle = true;
|
|
EXPECT_EQ(send(button, press(Action::confirm)), Event::changed);
|
|
EXPECT_TRUE(button.on());
|
|
EXPECT_TRUE(asked(Cue::toggle));
|
|
EXPECT_EQ(send(button, press(Action::confirm)), Event::changed);
|
|
EXPECT_FALSE(button.on());
|
|
|
|
button.set_badge(7);
|
|
EXPECT_EQ(button.badge(), 7);
|
|
button.set_disabled(true);
|
|
EXPECT_EQ(send(button, press(Action::confirm)), Event::refused);
|
|
EXPECT_FALSE(button.on());
|
|
EXPECT_TRUE(asked(Cue::error));
|
|
}
|
|
|
|
TEST_F(ComponentsActions, ButtonGroupMovesRefusesWrapsAndExits)
|
|
{
|
|
ButtonGroup group;
|
|
group.set_items({{"Cut"}, {"Copy"}, {"Paste"}});
|
|
group.set_bounds({100.0f, 100.0f, 600.0f, 64.0f});
|
|
group.set_active(true);
|
|
EXPECT_FLOAT_EQ(group.item_rect(1).x, 100.0f + 192.0f + 12.0f); // shared width and a gap
|
|
EXPECT_EQ(send(group, nav(Direction::left)), Event::refused);
|
|
EXPECT_TRUE(asked(Cue::error));
|
|
EXPECT_EQ(send(group, nav(Direction::right)), Event::moved);
|
|
EXPECT_TRUE(asked(Cue::focus));
|
|
EXPECT_EQ(send(group, nav(Direction::right)), Event::moved);
|
|
EXPECT_EQ(group.focus(), 2);
|
|
EXPECT_EQ(send(group, repeat(Direction::right)), Event::refused);
|
|
EXPECT_TRUE(feedback_.cues.empty()); // a held direction stays quiet
|
|
EXPECT_EQ(send(group, nav(Direction::down)), Event::none);
|
|
EXPECT_EQ(group.exit(), Direction::none);
|
|
|
|
group.style.wrap = true;
|
|
EXPECT_EQ(send(group, nav(Direction::right)), Event::moved);
|
|
EXPECT_EQ(group.focus(), 0);
|
|
group.style.wrap = false;
|
|
group.style.exits.left = true;
|
|
group.style.exits.down = true;
|
|
EXPECT_EQ(send(group, nav(Direction::left)), Event::none);
|
|
EXPECT_EQ(group.exit(), Direction::left);
|
|
EXPECT_EQ(send(group, nav(Direction::down)), Event::none);
|
|
EXPECT_EQ(group.exit(), Direction::down);
|
|
|
|
group.style.layout = hui::ui::GroupLayout::column;
|
|
group.style.joined = true;
|
|
EXPECT_FLOAT_EQ(group.item_rect(1).y, group.item_rect(0).y + 64.0f); // joined: no gap
|
|
EXPECT_EQ(send(group, nav(Direction::down)), Event::moved);
|
|
EXPECT_EQ(group.focus(), 1);
|
|
}
|
|
|
|
TEST_F(ComponentsActions, ButtonGroupModes)
|
|
{
|
|
ButtonGroup group;
|
|
std::vector<hui::ui::GroupItem> items = {{"Grid"}, {"List"}, {"Shelf"}};
|
|
items[2].disabled = true;
|
|
group.set_items(items);
|
|
group.set_bounds({100.0f, 100.0f, 600.0f, 64.0f});
|
|
|
|
EXPECT_EQ(send(group, press(Action::confirm)), Event::activated);
|
|
EXPECT_TRUE(asked(Cue::select));
|
|
EXPECT_EQ(group.selected(), -1); // actions select nothing
|
|
|
|
group.style.mode = GroupMode::exclusive;
|
|
EXPECT_EQ(send(group, press(Action::confirm)), Event::changed);
|
|
EXPECT_TRUE(asked(Cue::toggle));
|
|
EXPECT_EQ(group.selected(), 0);
|
|
EXPECT_EQ(send(group, press(Action::confirm)), Event::none); // already the choice
|
|
send(group, nav(Direction::right));
|
|
EXPECT_EQ(send(group, press(Action::confirm)), Event::changed);
|
|
EXPECT_EQ(group.selected(), 1);
|
|
EXPECT_FALSE(group.is_selected(0));
|
|
group.style.allow_none = true;
|
|
EXPECT_EQ(send(group, press(Action::confirm)), Event::changed);
|
|
EXPECT_EQ(group.selected(), -1);
|
|
|
|
send(group, nav(Direction::right));
|
|
EXPECT_EQ(send(group, press(Action::confirm)), Event::refused); // disabled
|
|
EXPECT_FALSE(group.is_selected(2));
|
|
|
|
group.style.mode = GroupMode::multiple;
|
|
group.set_focus(0);
|
|
EXPECT_EQ(send(group, press(Action::confirm)), Event::changed);
|
|
send(group, nav(Direction::right));
|
|
EXPECT_EQ(send(group, press(Action::confirm)), Event::changed);
|
|
EXPECT_TRUE(group.is_selected(0));
|
|
EXPECT_TRUE(group.is_selected(1));
|
|
EXPECT_EQ(send(group, press(Action::confirm)), Event::changed);
|
|
EXPECT_FALSE(group.is_selected(1));
|
|
|
|
// Restyling keeps the focus and the selection.
|
|
for (const hui::ui::Theme &theme : hui::ui::themes())
|
|
{
|
|
group.style.theme = theme;
|
|
EXPECT_EQ(group.focus(), 1) << theme.id;
|
|
EXPECT_TRUE(group.is_selected(0)) << theme.id;
|
|
}
|
|
}
|
|
|
|
TEST_F(ComponentsActions, SplitButtonFiresItsMainActionAndItsAlternatives)
|
|
{
|
|
SplitButton split;
|
|
split.label = "Save";
|
|
std::vector<hui::ui::MenuItem> items(2);
|
|
items[0].label = "Save as copy";
|
|
items[1].label = "Save and quit";
|
|
split.set_alternatives(items);
|
|
split.set_bounds({100.0f, 100.0f, 360.0f, 64.0f});
|
|
split.set_active(true);
|
|
EXPECT_FLOAT_EQ(split.part_rect(1).w, 64.0f); // as wide as the button is tall
|
|
|
|
EXPECT_EQ(send(split, press(Action::confirm)), Event::activated);
|
|
EXPECT_EQ(split.choice(), -1);
|
|
EXPECT_EQ(send(split, nav(Direction::left)), Event::refused);
|
|
EXPECT_EQ(send(split, nav(Direction::right)), Event::moved);
|
|
EXPECT_EQ(split.part(), 1);
|
|
EXPECT_EQ(send(split, nav(Direction::right)), Event::refused);
|
|
|
|
EXPECT_EQ(send(split, press(Action::confirm)), Event::none);
|
|
EXPECT_TRUE(split.menu_open());
|
|
EXPECT_TRUE(asked(Cue::modal_open));
|
|
EXPECT_EQ(send(split, nav(Direction::down)), Event::moved);
|
|
EXPECT_EQ(send(split, press(Action::confirm)), Event::activated);
|
|
EXPECT_EQ(split.choice(), 1);
|
|
EXPECT_FALSE(split.menu_open());
|
|
|
|
send(split, press(Action::confirm));
|
|
EXPECT_TRUE(split.menu_open());
|
|
EXPECT_EQ(send(split, press(Action::back)), Event::cancelled);
|
|
EXPECT_FALSE(split.menu_open());
|
|
|
|
split.style.exits.right = true;
|
|
EXPECT_EQ(send(split, nav(Direction::right)), Event::none);
|
|
EXPECT_EQ(split.exit(), Direction::right);
|
|
|
|
split.style.swap_on_choose = true;
|
|
send(split, press(Action::confirm));
|
|
EXPECT_EQ(send(split, press(Action::confirm)), Event::activated);
|
|
EXPECT_EQ(split.label, "Save as copy"); // the alternative became the main action
|
|
EXPECT_EQ(split.menu().items()[0].label, "Save");
|
|
}
|
|
|
|
TEST_F(ComponentsActions, HoldButtonCompletesOnlyAfterAFullHold)
|
|
{
|
|
HoldButton button;
|
|
button.label = "Delete save";
|
|
button.set_bounds({100.0f, 100.0f, 400.0f, 64.0f});
|
|
button.set_active(true);
|
|
button.style.hold_seconds = 1.0f;
|
|
|
|
// Half a hold: it fills, then drains without firing.
|
|
feedback_.clear();
|
|
EXPECT_EQ(hold(button, 0.5f), 0);
|
|
EXPECT_NEAR(button.progress(), 0.5f, 0.05f);
|
|
EXPECT_TRUE(button.holding());
|
|
EXPECT_GT(feedback_.rumble_strength, button.style.rumble_from); // the ramp
|
|
EXPECT_TRUE(asked(Cue::slider)); // a quarter tick
|
|
EXPECT_EQ(button.handle(idle(), feedback_), Event::none);
|
|
EXPECT_FALSE(button.holding());
|
|
settle(button);
|
|
EXPECT_FLOAT_EQ(button.progress(), 0.0f);
|
|
EXPECT_FALSE(button.hinting()); // a long press that was let go is not a tap
|
|
|
|
// A full hold fires exactly once, however long it goes on.
|
|
feedback_.clear();
|
|
EXPECT_EQ(hold(button, 2.0f), 1);
|
|
EXPECT_TRUE(asked(Cue::launch));
|
|
EXPECT_GE(feedback_.rumble_strength, button.style.rumble_done - 0.01f);
|
|
settle(button);
|
|
EXPECT_FLOAT_EQ(button.progress(), 0.0f);
|
|
|
|
// Another action can be the one that is held.
|
|
button.style.action = Action::west;
|
|
EXPECT_EQ(hold(button, 1.5f, Action::confirm), 0);
|
|
EXPECT_EQ(hold(button, 1.5f, Action::west), 1);
|
|
}
|
|
|
|
TEST_F(ComponentsActions, HoldButtonTapShowsTheHintAndRefuses)
|
|
{
|
|
HoldButton button;
|
|
button.label = "Sign out";
|
|
button.hint = "Hold to sign out";
|
|
button.set_bounds({100.0f, 100.0f, 400.0f, 64.0f});
|
|
button.set_active(true);
|
|
|
|
feedback_.clear();
|
|
EXPECT_EQ(button.handle(press(Action::confirm), feedback_), Event::none);
|
|
button.update(kFrame);
|
|
EXPECT_EQ(button.handle(idle(), feedback_), Event::refused); // let go at once
|
|
EXPECT_TRUE(asked(Cue::error));
|
|
EXPECT_TRUE(button.hinting());
|
|
settle(button, button.style.hint_seconds + 0.5f);
|
|
EXPECT_FALSE(button.hinting());
|
|
EXPECT_FLOAT_EQ(button.progress(), 0.0f);
|
|
|
|
button.set_disabled(true);
|
|
EXPECT_EQ(button.handle(press(Action::confirm), feedback_), Event::refused);
|
|
EXPECT_FALSE(button.holding());
|
|
|
|
// Losing the focus mid-hold lets go: no handle(), no hold.
|
|
button.set_disabled(false);
|
|
hold(button, 0.4f);
|
|
EXPECT_GT(button.progress(), 0.2f);
|
|
settle(button);
|
|
EXPECT_FLOAT_EQ(button.progress(), 0.0f);
|
|
EXPECT_FALSE(button.holding());
|
|
}
|
|
|
|
TEST_F(ComponentsActions, QuickActionAnswersItsOwnButtonOnly)
|
|
{
|
|
QuickAction action;
|
|
action.label = "Search";
|
|
action.glyph = hui::ui::Button::triangle;
|
|
action.action = Action::north;
|
|
action.set_bounds({96.0f, 236.0f, 1728.0f, 716.0f});
|
|
settle(action);
|
|
|
|
EXPECT_EQ(send(action, press(Action::confirm)), Event::none);
|
|
EXPECT_EQ(send(action, nav(Direction::left)), Event::none);
|
|
EXPECT_EQ(send(action, press(Action::north)), Event::activated);
|
|
EXPECT_TRUE(asked(Cue::select));
|
|
|
|
// It sits in its corner, inside its bounds.
|
|
const Rect r = action.rect(fonts_);
|
|
EXPECT_NEAR(r.x + r.w, 96.0f + 1728.0f, 0.01f);
|
|
EXPECT_NEAR(r.y + r.h, 236.0f + 716.0f, 0.01f);
|
|
action.style.anchor = hui::ui::QuickAnchor::top_left;
|
|
action.style.margin = 10.0f;
|
|
EXPECT_NEAR(action.rect(fonts_).x, 106.0f, 0.01f);
|
|
EXPECT_NEAR(action.rect(fonts_).y, 246.0f, 0.01f);
|
|
|
|
// The label folds away after a while and comes back on a press.
|
|
const float open = action.width(fonts_);
|
|
action.style.collapse_after = 1.0f;
|
|
settle(action, 3.0f);
|
|
EXPECT_TRUE(action.collapsed());
|
|
EXPECT_LT(action.width(fonts_), open - 20.0f);
|
|
EXPECT_EQ(action.handle(press(Action::north), feedback_), Event::activated);
|
|
EXPECT_FALSE(action.collapsed());
|
|
action.style.collapse_after = 0.0f;
|
|
|
|
action.set_enabled(false);
|
|
EXPECT_EQ(send(action, press(Action::north)), Event::refused);
|
|
EXPECT_TRUE(asked(Cue::error));
|
|
action.set_enabled(true);
|
|
action.set_visible(false);
|
|
EXPECT_EQ(send(action, press(Action::north)), Event::none); // hidden: not listening
|
|
action.set_badge(5);
|
|
EXPECT_EQ(action.badge(), 5);
|
|
}
|
|
|
|
TEST_F(ComponentsActions, QuickActionBarKeepsItsActionsApart)
|
|
{
|
|
QuickActionBar bar;
|
|
bar.set_actions({{"Search", hui::ui::Button::triangle, Action::north},
|
|
{"Sort", hui::ui::Button::square, Action::west}});
|
|
bar.set_bounds({96.0f, 236.0f, 1728.0f, 716.0f});
|
|
settle(bar);
|
|
EXPECT_EQ(bar.count(), 2);
|
|
|
|
const Rect first = bar.rect(fonts_, 0);
|
|
const Rect second = bar.rect(fonts_, 1);
|
|
EXPECT_NEAR(second.x - (first.x + first.w), bar.style.spacing, 0.01f);
|
|
EXPECT_NEAR(second.x + second.w, 96.0f + 1728.0f, 0.01f); // the row ends in the corner
|
|
|
|
EXPECT_EQ(send(bar, press(Action::west)), Event::activated);
|
|
EXPECT_EQ(bar.fired(), 1);
|
|
EXPECT_EQ(send(bar, press(Action::north)), Event::activated);
|
|
EXPECT_EQ(bar.fired(), 0);
|
|
EXPECT_EQ(send(bar, press(Action::confirm)), Event::none);
|
|
bar.at(1).set_enabled(false);
|
|
EXPECT_EQ(send(bar, press(Action::west)), Event::refused);
|
|
EXPECT_EQ(bar.fired(), 1);
|
|
|
|
bar.style.layout = hui::ui::GroupLayout::column;
|
|
settle(bar);
|
|
const Rect top = bar.rect(fonts_, 0);
|
|
const Rect bottom = bar.rect(fonts_, 1);
|
|
EXPECT_NEAR(bottom.y - (top.y + top.h), bar.style.spacing, 0.01f);
|
|
EXPECT_NEAR(bottom.y + bottom.h, 236.0f + 716.0f, 0.01f);
|
|
}
|
|
|
|
TEST_F(ComponentsActions, BannerPushesContentAndIsAnswered)
|
|
{
|
|
Banner banner;
|
|
banner.style.kind = StatusKind::warning;
|
|
banner.title = "Storage almost full";
|
|
banner.body = "2.1 GB left.";
|
|
banner.set_actions({"Manage", "Later", "Dropped"});
|
|
EXPECT_EQ(banner.actions().size(), 2u); // at most two
|
|
banner.set_bounds({96.0f, 240.0f, 1728.0f, 0.0f});
|
|
banner.set_active(true);
|
|
EXPECT_FALSE(banner.visible());
|
|
EXPECT_FLOAT_EQ(banner.pushed(fonts_), 0.0f);
|
|
EXPECT_EQ(banner.handle(press(Action::confirm), feedback_), Event::none); // hidden
|
|
|
|
feedback_.clear();
|
|
banner.show(feedback_);
|
|
EXPECT_TRUE(asked(Cue::notify));
|
|
settle(banner);
|
|
EXPECT_GE(banner.height(fonts_), banner.style.min_height);
|
|
EXPECT_NEAR(banner.pushed(fonts_), banner.height(fonts_), 0.5f);
|
|
EXPECT_EQ(banner.controls(), 3); // two actions and the dismiss control
|
|
|
|
// The controls sit inside the panel, left to right.
|
|
const Rect panel = banner.rect(fonts_);
|
|
const Rect last = banner.control_rect(fonts_, 2);
|
|
EXPECT_GT(banner.control_rect(fonts_, 1).x, banner.control_rect(fonts_, 0).x);
|
|
EXPECT_GT(last.x, banner.control_rect(fonts_, 1).x);
|
|
EXPECT_LE(last.x + last.w, panel.x + panel.w);
|
|
|
|
EXPECT_EQ(send(banner, nav(Direction::left)), Event::refused);
|
|
EXPECT_EQ(send(banner, nav(Direction::right)), Event::moved);
|
|
EXPECT_EQ(send(banner, press(Action::confirm)), Event::activated);
|
|
EXPECT_EQ(banner.choice(), 1);
|
|
EXPECT_TRUE(banner.is_shown()); // an action does not close it by itself
|
|
EXPECT_EQ(send(banner, nav(Direction::right)), Event::moved);
|
|
EXPECT_EQ(send(banner, nav(Direction::right)), Event::refused);
|
|
banner.style.exits.down = true;
|
|
EXPECT_EQ(send(banner, nav(Direction::down)), Event::none);
|
|
EXPECT_EQ(banner.exit(), Direction::down);
|
|
|
|
EXPECT_EQ(send(banner, press(Action::confirm)), Event::cancelled); // the dismiss control
|
|
EXPECT_TRUE(asked(Cue::modal_close));
|
|
EXPECT_FALSE(banner.is_shown());
|
|
EXPECT_FLOAT_EQ(banner.pushed(fonts_), 0.0f);
|
|
|
|
banner.show(feedback_);
|
|
settle(banner);
|
|
EXPECT_EQ(banner.focus(), 0); // it opens on its first action again
|
|
EXPECT_EQ(send(banner, press(Action::back)), Event::cancelled);
|
|
banner.show(feedback_);
|
|
banner.style.dismissable = false;
|
|
EXPECT_EQ(banner.controls(), 2);
|
|
EXPECT_EQ(send(banner, press(Action::back)), Event::none);
|
|
EXPECT_TRUE(banner.is_shown());
|
|
}
|
|
|
|
TEST_F(ComponentsActions, CoachMarkStepsGlidesAndEnds)
|
|
{
|
|
CoachMark coach;
|
|
EXPECT_FALSE(coach.is_open());
|
|
coach.start({}, feedback_);
|
|
EXPECT_FALSE(coach.is_open()); // nothing to show
|
|
EXPECT_EQ(coach.handle(press(Action::confirm), feedback_), Event::none);
|
|
|
|
feedback_.clear();
|
|
coach.start(steps(), feedback_);
|
|
EXPECT_TRUE(coach.is_open());
|
|
EXPECT_TRUE(asked(Cue::modal_open));
|
|
EXPECT_EQ(coach.count(), 3);
|
|
settle(coach, 2.0f);
|
|
// The spotlight came to rest around the first target, with its padding.
|
|
EXPECT_NEAR(coach.spotlight().x, 200.0f - coach.style.spot_padding, 0.5f);
|
|
EXPECT_NEAR(coach.spotlight().w, 300.0f + 2.0f * coach.style.spot_padding, 0.5f);
|
|
|
|
EXPECT_EQ(send(coach, nav(Direction::left)), Event::refused); // a direction does not skip
|
|
EXPECT_EQ(send(coach, press(Action::confirm)), Event::changed);
|
|
EXPECT_TRUE(asked(Cue::tab));
|
|
EXPECT_EQ(coach.step(), 1);
|
|
// No room below a target in the bottom corner: the bubble flips and stays
|
|
// inside the bounds.
|
|
const hui::ui::Tooltip::Placed placed = coach.bubble(fonts_);
|
|
EXPECT_EQ(placed.placement, TooltipPlacement::above);
|
|
EXPECT_GE(placed.bubble.x, coach.style.screen_margin - 0.01f);
|
|
EXPECT_LE(placed.bubble.x + placed.bubble.w, 1920.0f - coach.style.screen_margin + 0.01f);
|
|
EXPECT_GE(placed.bubble.y, coach.style.screen_margin - 0.01f);
|
|
|
|
EXPECT_EQ(send(coach, press(Action::back)), Event::changed);
|
|
EXPECT_EQ(coach.step(), 0);
|
|
EXPECT_EQ(send(coach, nav(Direction::right)), Event::changed);
|
|
EXPECT_EQ(send(coach, nav(Direction::right)), Event::changed);
|
|
EXPECT_EQ(coach.step(), 2);
|
|
EXPECT_EQ(send(coach, nav(Direction::right)), Event::refused); // the end needs confirm
|
|
EXPECT_TRUE(coach.is_open());
|
|
coach.set_target(2, {100.0f, 100.0f, 80.0f, 80.0f});
|
|
settle(coach, 2.0f);
|
|
EXPECT_NEAR(coach.spotlight().x, 100.0f - coach.style.spot_padding, 0.5f);
|
|
EXPECT_EQ(send(coach, press(Action::confirm)), Event::activated);
|
|
EXPECT_FALSE(coach.is_open());
|
|
|
|
coach.start(steps(), feedback_);
|
|
feedback_.clear();
|
|
EXPECT_EQ(send(coach, press(Action::back)), Event::cancelled); // skipped on the first step
|
|
EXPECT_TRUE(asked(Cue::back));
|
|
EXPECT_FALSE(coach.is_open());
|
|
settle(coach, 2.0f);
|
|
EXPECT_FALSE(coach.visible());
|
|
}
|
|
|
|
TEST_F(ComponentsActions, DrawsInEveryThemeAndVariant)
|
|
{
|
|
PushButton push;
|
|
push.label = "Continue";
|
|
push.glyph = hui::ui::Button::cross;
|
|
push.icon = [](hui::ui::Canvas &canvas, const Rect &box, hui::gfx::Color ink, float)
|
|
{ canvas.list.circle(box.cx(), box.cy(), box.w * 0.4f, ink); };
|
|
push.set_bounds({100.0f, 100.0f, 320.0f, 80.0f});
|
|
push.set_active(true);
|
|
|
|
IconButton icon;
|
|
icon.tip = "Favourite";
|
|
icon.style.toggle = true;
|
|
icon.set_bounds({100.0f, 200.0f, 64.0f, 64.0f});
|
|
icon.set_badge(120);
|
|
icon.set_active(true);
|
|
|
|
ButtonGroup group;
|
|
group.set_items({{"Grid"}, {"List"}, {"Shelf"}});
|
|
group.set_bounds({100.0f, 300.0f, 540.0f, 64.0f});
|
|
group.style.mode = GroupMode::exclusive;
|
|
group.set_selected(1);
|
|
group.set_active(true);
|
|
|
|
SplitButton split;
|
|
split.label = "Save";
|
|
std::vector<hui::ui::MenuItem> items(2);
|
|
items[0].label = "Save as copy";
|
|
items[1].label = "Export";
|
|
split.set_alternatives(items);
|
|
split.set_bounds({100.0f, 400.0f, 360.0f, 64.0f});
|
|
split.set_active(true);
|
|
split.set_part(1);
|
|
split.handle(press(Action::confirm), feedback_); // the menu is open
|
|
|
|
HoldButton held;
|
|
held.label = "Delete save";
|
|
held.set_bounds({100.0f, 500.0f, 400.0f, 64.0f});
|
|
held.set_active(true);
|
|
hold(held, 0.6f);
|
|
|
|
QuickActionBar bar;
|
|
bar.set_actions({{"Search", hui::ui::Button::triangle, Action::north},
|
|
{"Sort", hui::ui::Button::l1, Action::west}});
|
|
bar.at(0).set_badge(3);
|
|
|
|
Banner banner;
|
|
banner.title = "Update ready";
|
|
banner.body = "Version 2.4 adds a photo mode. It installs the next time you close the game, "
|
|
"and the notes are in the Extras menu if you want to read them first.";
|
|
banner.set_actions({"Install", "Notes"});
|
|
banner.set_active(true);
|
|
banner.set_shown(true, true);
|
|
|
|
CoachMark coach;
|
|
coach.start(steps(), feedback_);
|
|
|
|
const ButtonRole roles[] = {ButtonRole::primary, ButtonRole::secondary, ButtonRole::ghost,
|
|
ButtonRole::danger};
|
|
const ButtonSize sizes[] = {ButtonSize::small, ButtonSize::medium, ButtonSize::large};
|
|
const HoldVariant holds[] = {HoldVariant::fill, HoldVariant::ring, HoldVariant::underline};
|
|
const BannerLook looks[] = {BannerLook::filled, BannerLook::tinted, BannerLook::outlined,
|
|
BannerLook::accent};
|
|
const StatusKind kinds[] = {StatusKind::info, StatusKind::success, StatusKind::warning,
|
|
StatusKind::danger};
|
|
for (const hui::ui::Theme &theme : hui::ui::themes())
|
|
{
|
|
for (int variant = 0; variant < 4; ++variant)
|
|
{
|
|
const bool odd = variant % 2 == 1;
|
|
push.style.theme = theme;
|
|
push.style.role = roles[variant];
|
|
push.style.size = sizes[variant % 3];
|
|
push.style.full_width = !odd;
|
|
push.style.glyph_tinted = !odd;
|
|
push.style.loading_hides_label = odd;
|
|
push.set_loading(variant >= 2);
|
|
push.set_disabled(variant == 3);
|
|
|
|
icon.style.theme = theme;
|
|
icon.style.role = roles[variant];
|
|
icon.style.shape =
|
|
odd ? hui::ui::IconButtonShape::round : hui::ui::IconButtonShape::square;
|
|
icon.set_on(odd);
|
|
|
|
group.style.theme = theme;
|
|
group.style.role = roles[variant];
|
|
group.style.selected_role = roles[(variant + 1) % 4];
|
|
group.style.joined = odd;
|
|
group.style.size = sizes[variant % 3];
|
|
group.style.layout =
|
|
variant == 3 ? hui::ui::GroupLayout::column : hui::ui::GroupLayout::row;
|
|
group.style.icon_width = odd ? 24.0f : 0.0f;
|
|
|
|
split.style.theme = theme;
|
|
split.style.role = roles[variant];
|
|
|
|
held.style.theme = theme;
|
|
held.style.role = roles[3 - variant];
|
|
held.style.variant = holds[variant % 3];
|
|
held.style.glyph = odd ? hui::ui::Button::none : hui::ui::Button::cross;
|
|
|
|
bar.style.theme = theme;
|
|
bar.style.role = roles[variant];
|
|
bar.style.pill = !odd;
|
|
bar.style.collapse_after = odd ? 0.1f : 0.0f;
|
|
bar.style.anchor =
|
|
odd ? hui::ui::QuickAnchor::top_center : hui::ui::QuickAnchor::bottom_right;
|
|
|
|
banner.style.theme = theme;
|
|
banner.style.look = looks[variant];
|
|
banner.style.kind = kinds[variant];
|
|
banner.style.dismissable = variant != 1;
|
|
|
|
coach.style.theme = theme;
|
|
coach.style.counter = !odd;
|
|
coach.style.hints = variant != 3;
|
|
|
|
// Everything animates a little between draws: mid-flight is drawn too.
|
|
push.update(kFrame);
|
|
icon.update(0.5f);
|
|
group.update(kFrame);
|
|
split.update(kFrame);
|
|
held.update(kFrame);
|
|
bar.update(0.3f);
|
|
banner.update(kFrame);
|
|
coach.update(kFrame);
|
|
|
|
hui::ui::Canvas target = canvas();
|
|
push.draw(target);
|
|
icon.draw(target);
|
|
icon.draw_tooltip(target);
|
|
group.draw(target);
|
|
split.draw(target);
|
|
split.draw_menu(target);
|
|
held.draw(target);
|
|
bar.draw(target);
|
|
banner.draw(target);
|
|
coach.draw(target);
|
|
expect_drawn(theme.id);
|
|
for (const hui::gfx::MeshVertex &vertex : list_.mesh_vertices())
|
|
ASSERT_TRUE(std::isfinite(vertex.x) && std::isfinite(vertex.y)) << theme.id;
|
|
}
|
|
}
|
|
// Restyling thirty times lost nothing.
|
|
EXPECT_EQ(group.selected(), 1);
|
|
EXPECT_TRUE(split.menu_open());
|
|
EXPECT_TRUE(banner.is_shown());
|
|
EXPECT_TRUE(coach.is_open());
|
|
EXPECT_EQ(bar.at(0).badge(), 3);
|
|
}
|
|
|
|
TEST_F(ComponentsActions, PageTourEndsWhereItBegan)
|
|
{
|
|
const std::unique_ptr<hui::concepts::gallery::Page> page =
|
|
hui::concepts::gallery::make_actions_page(context_);
|
|
EXPECT_STREQ(page->title(), "Actions");
|
|
const std::string first = page->variant();
|
|
const std::size_t board_hints = page->hints().size();
|
|
int pictures = 0;
|
|
for (const hui::app::TourStep &step : page->tour())
|
|
{
|
|
// The wait, with whatever the step keeps held, then its input.
|
|
hui::InputFrame waiting = idle();
|
|
waiting.held = step.hold;
|
|
for (int i = 0, frames = static_cast<int>(step.wait / kFrame); i < frames; ++i)
|
|
{
|
|
feedback_.clear();
|
|
page->update(waiting, kFrame, feedback_);
|
|
}
|
|
if (step.capture != nullptr)
|
|
{
|
|
++pictures;
|
|
EXPECT_EQ(std::string(step.capture).rfind("actions", 0), 0u) << step.capture;
|
|
}
|
|
hui::InputFrame input = idle();
|
|
input.pressed = step.press;
|
|
input.held = step.press | step.hold;
|
|
input.nav = step.nav;
|
|
feedback_.clear();
|
|
page->update(input, kFrame, feedback_);
|
|
hui::ui::Canvas target = canvas();
|
|
page->draw(target);
|
|
page->draw_modal(target);
|
|
expect_drawn("actions page");
|
|
}
|
|
EXPECT_GE(page->tour().size(), 10u);
|
|
EXPECT_LE(page->tour().size(), 25u);
|
|
EXPECT_GE(pictures, 3);
|
|
EXPECT_LE(pictures, 5);
|
|
EXPECT_EQ(first, page->variant()); // the first variant again
|
|
EXPECT_EQ(page->hints().size(), board_hints); // nothing modal left open
|
|
}
|
|
|
|
} // namespace
|