Files
DevvNirvana--afterglow/tests/unit/components_pickers_test.cpp
Tushar Rohilla e5295f36aa Add Afterglow 1.0.6, a PS5 internet radio and music visualizer
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.
2026-10-16 00:59:10 -04:00

768 lines
29 KiB
C++

// ps5-homebrew-ui - Tests: the picker components.
// Copyright (C) 2026 BlackBearReloaded
// SPDX-License-Identifier: GPL-3.0-or-later
#include "component_fixture.hpp"
#include "ui/components/choice_group.hpp"
#include "ui/components/color_picker.hpp"
#include "ui/components/range_slider.hpp"
#include "ui/components/select.hpp"
#include "ui/components/tag_select.hpp"
#include "ui/components/wheel_picker.hpp"
#include <gtest/gtest.h>
#include <cmath>
#include <string>
#include <vector>
namespace
{
using hui::Action;
using hui::Direction;
using hui::InputFrame;
using hui::audio::Cue;
using hui::gfx::Color;
using hui::gfx::Rect;
using hui::ui::CheckGroup;
using hui::ui::ColorPicker;
using hui::ui::DatePicker;
using hui::ui::Event;
using hui::ui::RadioGroup;
using hui::ui::RangeSlider;
using hui::ui::Select;
using hui::ui::Slider;
using hui::ui::TagSelect;
using hui::ui::TimePicker;
using hui::ui::WheelPicker;
class ComponentsPickers : public hui::testing::ComponentFixture
{
protected:
// One input to a component, with the feedback cleared first.
template <typename Component> Event send(Component &component, const InputFrame &input)
{
feedback_.clear();
const Event event = component.handle(input, feedback_);
for (int i = 0; i < 20; ++i)
component.update(kFrame);
return event;
}
static InputFrame held(Direction direction)
{
InputFrame input = nav(direction);
input.nav_repeat = true;
return input;
}
static Select make_select()
{
Select select;
std::vector<hui::ui::SelectOption> options;
for (const char *name : {"North", "East", "South", "West", "Up", "Down", "In", "Out"})
{
hui::ui::SelectOption option;
option.label = name;
options.push_back(option);
}
options[3].disabled = true;
options[4].description = "Has a second line";
select.set_label("Direction");
select.set_options(options);
select.set_index(1);
select.set_bounds({100.0f, 100.0f, 420.0f, select.preferred_height()});
return select;
}
static TagSelect make_tags()
{
TagSelect tags;
std::vector<hui::ui::TagOption> options;
for (const char *name : {"Action", "Puzzle", "Racing", "Horror", "Sports", "Arcade",
"Rhythm", "Stealth", "Tactics", "Sandbox", "Platform", "Trivia"})
options.push_back({name});
tags.set_title("Genres");
tags.set_options(options);
tags.set_bounds({100.0f, 100.0f, 420.0f, 300.0f});
return tags;
}
};
TEST_F(ComponentsPickers, SelectOpensPicksAndCloses)
{
Select select = make_select();
EXPECT_EQ(send(select, nav(Direction::down)), Event::none); // the screen's to use
EXPECT_EQ(send(select, press(Action::confirm)), Event::activated);
EXPECT_TRUE(select.is_open());
EXPECT_TRUE(asked(Cue::modal_open));
EXPECT_EQ(select.focus(), 1); // the list starts on the current option
EXPECT_EQ(send(select, nav(Direction::down)), Event::moved);
EXPECT_EQ(send(select, press(Action::confirm)), Event::changed);
EXPECT_TRUE(asked(Cue::select));
EXPECT_FALSE(select.is_open());
EXPECT_EQ(select.index(), 2);
EXPECT_EQ(select.value(), "South");
// A disabled option refuses and the list stays; back changes nothing.
send(select, press(Action::confirm));
send(select, nav(Direction::down));
EXPECT_EQ(send(select, press(Action::confirm)), Event::refused);
EXPECT_TRUE(asked(Cue::error));
EXPECT_TRUE(select.is_open());
EXPECT_EQ(send(select, press(Action::back)), Event::cancelled);
EXPECT_TRUE(asked(Cue::modal_close));
EXPECT_FALSE(select.is_open());
EXPECT_EQ(select.index(), 2);
// Confirm on the current option closes without a change.
send(select, press(Action::confirm));
EXPECT_EQ(send(select, press(Action::confirm)), Event::cancelled);
EXPECT_EQ(select.index(), 2);
// step_closed: left and right pick without the list, past disabled options.
select.style.step_closed = true;
EXPECT_EQ(send(select, nav(Direction::right)), Event::changed);
EXPECT_EQ(select.index(), 4);
EXPECT_FALSE(select.is_open());
// The list opens below, flips above when there is more room there, and
// stays inside its limits.
select = make_select();
const Rect limits{0.0f, 0.0f, 1920.0f, 1080.0f};
select.set_limits(limits);
EXPECT_FALSE(select.opens_above());
Rect list = select.popover_rect();
EXPECT_GE(list.y, select.field_rect().y + select.field_rect().h);
select.set_bounds({100.0f, 940.0f, 420.0f, select.preferred_height()});
EXPECT_TRUE(select.opens_above());
list = select.popover_rect();
EXPECT_LE(list.y + list.h, select.field_rect().y);
EXPECT_GE(list.y, limits.y);
// Squeezed from both sides it shows fewer rows rather than leave the limits.
select.set_limits({0.0f, 700.0f, 1920.0f, 380.0f});
list = select.popover_rect();
EXPECT_GE(list.y, 700.0f);
EXPECT_LE(list.y + list.h, 1080.0f);
}
TEST_F(ComponentsPickers, CheckGroupTogglesAndSelectsAll)
{
CheckGroup group;
group.style.select_all = true;
group.set_items({{"One"}, {"Two"}, {"Three", "", true}, {"Four"}});
group.set_bounds({100.0f, 100.0f, 400.0f, 300.0f});
EXPECT_EQ(group.focus(), 0);
EXPECT_EQ(send(group, press(Action::confirm)), Event::changed);
EXPECT_TRUE(group.checked(0));
EXPECT_TRUE(asked(Cue::toggle));
EXPECT_GT(feedback_.cues.back().pitch, 1.0f); // on is the higher note
EXPECT_EQ(send(group, press(Action::confirm)), Event::changed);
EXPECT_FALSE(group.checked(0));
EXPECT_LT(feedback_.cues.back().pitch, 1.0f);
// The header: everything that is not disabled, then nothing.
EXPECT_EQ(send(group, nav(Direction::up)), Event::moved);
EXPECT_EQ(group.focus(), -1);
EXPECT_EQ(send(group, press(Action::confirm)), Event::changed);
EXPECT_EQ(group.changed_index(), -1);
EXPECT_EQ(group.checked_count(), 3);
EXPECT_FALSE(group.checked(2));
EXPECT_EQ(send(group, press(Action::confirm)), Event::changed);
EXPECT_EQ(group.checked_count(), 0);
// A disabled item takes the focus and refuses; the end refuses, quietly
// on a held direction, or hands the focus on through an exit.
send(group, nav(Direction::down));
send(group, nav(Direction::down));
send(group, nav(Direction::down));
EXPECT_EQ(group.focus(), 2);
EXPECT_EQ(send(group, press(Action::confirm)), Event::refused);
send(group, nav(Direction::down));
EXPECT_EQ(send(group, nav(Direction::down)), Event::refused);
EXPECT_TRUE(asked(Cue::error));
EXPECT_EQ(send(group, held(Direction::down)), Event::refused);
EXPECT_TRUE(feedback_.cues.empty());
group.style.exits.down = true;
EXPECT_EQ(send(group, nav(Direction::down)), Event::none);
EXPECT_EQ(group.exit(), Direction::down);
EXPECT_EQ(group.focus(), 3);
// A grid moves in two directions.
group.style.layout = hui::ui::ChoiceLayout::grid;
group.style.columns = 2;
group.set_focus(0);
EXPECT_EQ(send(group, nav(Direction::right)), Event::moved);
EXPECT_EQ(group.focus(), 1);
EXPECT_EQ(send(group, nav(Direction::down)), Event::moved);
EXPECT_EQ(group.focus(), 3);
}
TEST_F(ComponentsPickers, RadioGroupKeepsOneSelected)
{
RadioGroup group;
group.set_items({{"Solo"}, {"Co-op"}, {"Versus"}});
group.set_bounds({100.0f, 100.0f, 480.0f, 200.0f});
group.set_selected(0);
send(group, nav(Direction::down));
EXPECT_EQ(group.selected(), 0); // moving is not selecting
EXPECT_EQ(send(group, press(Action::confirm)), Event::changed);
EXPECT_EQ(group.selected(), 1);
EXPECT_EQ(send(group, press(Action::confirm)), Event::none);
EXPECT_EQ(group.selected(), 1);
group.style.allow_none = true;
EXPECT_EQ(send(group, press(Action::confirm)), Event::changed);
EXPECT_EQ(group.selected(), -1);
group.style.select_on_move = true;
EXPECT_EQ(send(group, nav(Direction::down)), Event::changed);
EXPECT_EQ(group.selected(), 2);
EXPECT_TRUE(asked(Cue::toggle));
EXPECT_FALSE(asked(Cue::focus));
// In a row, left and right move and up is an edge.
group.style.layout = hui::ui::ChoiceLayout::horizontal;
EXPECT_EQ(send(group, nav(Direction::left)), Event::changed);
EXPECT_EQ(group.selected(), 1);
EXPECT_EQ(send(group, nav(Direction::up)), Event::refused);
group.style.wrap = true;
send(group, nav(Direction::right));
EXPECT_EQ(send(group, nav(Direction::right)), Event::changed);
EXPECT_EQ(group.selected(), 0);
}
TEST_F(ComponentsPickers, TagSelectMovesByPositionAndHoldsItsColumn)
{
TagSelect tags = make_tags();
tags.layout(fonts_);
ASSERT_GE(tags.rows(), 3);
EXPECT_EQ(tags.row_of(0), 0);
EXPECT_EQ(send(tags, nav(Direction::right)), Event::moved);
EXPECT_EQ(tags.focus(), 1);
const float column = tags.chip_rect(1).cx();
EXPECT_EQ(send(tags, nav(Direction::down)), Event::moved);
const int below = tags.focus();
EXPECT_EQ(tags.row_of(below), 1);
// The chip under the starting point, or the nearest one.
const Rect under = tags.chip_rect(below);
for (int i = 0; i < static_cast<int>(tags.options().size()); ++i)
{
if (tags.row_of(i) != 1)
continue;
const Rect other = tags.chip_rect(i);
const auto distance = [column](const Rect &r)
{ return column < r.x ? r.x - column : (column > r.x + r.w ? column - r.x - r.w : 0.0f); };
EXPECT_LE(distance(under), distance(other));
}
// Down and back up returns to where it started, whatever the rows between.
send(tags, nav(Direction::down));
EXPECT_EQ(tags.row_of(tags.focus()), 2);
send(tags, nav(Direction::up));
EXPECT_EQ(tags.focus(), below);
send(tags, nav(Direction::up));
EXPECT_EQ(tags.focus(), 1);
EXPECT_EQ(send(tags, nav(Direction::up)), Event::refused);
tags.style.exits.up = true;
EXPECT_EQ(send(tags, nav(Direction::up)), Event::none);
EXPECT_EQ(tags.exit(), Direction::up);
// Without flow the end of a row is an edge; with it the next row follows.
int last = 0;
while (last + 1 < static_cast<int>(tags.options().size()) && tags.row_of(last + 1) == 0)
++last;
tags.set_focus(last);
EXPECT_EQ(send(tags, nav(Direction::right)), Event::refused);
tags.style.flow = true;
EXPECT_EQ(send(tags, nav(Direction::right)), Event::moved);
EXPECT_EQ(tags.focus(), last + 1);
}
TEST_F(ComponentsPickers, TagSelectLimitRefusesSoftly)
{
TagSelect tags = make_tags();
tags.style.max_selected = 2;
tags.layout(fonts_);
EXPECT_EQ(send(tags, press(Action::confirm)), Event::changed);
EXPECT_TRUE(asked(Cue::toggle));
send(tags, nav(Direction::right));
EXPECT_EQ(send(tags, press(Action::confirm)), Event::changed);
send(tags, nav(Direction::right));
EXPECT_EQ(send(tags, press(Action::confirm)), Event::refused);
EXPECT_TRUE(asked(Cue::error));
EXPECT_EQ(tags.selected_count(), 2);
EXPECT_EQ(tags.selection(), (std::vector<int>{0, 1}));
// Switching one off makes room again.
send(tags, nav(Direction::left));
EXPECT_EQ(send(tags, press(Action::confirm)), Event::changed);
EXPECT_LT(feedback_.cues.back().pitch, 1.0f);
EXPECT_EQ(tags.selected_count(), 1);
tags.style.single = true;
send(tags, nav(Direction::right));
EXPECT_EQ(send(tags, press(Action::confirm)), Event::changed);
EXPECT_EQ(tags.selection(), (std::vector<int>{2}));
}
TEST_F(ComponentsPickers, ColorPickerSwatches)
{
ColorPicker picker;
picker.set_bounds({100.0f, 100.0f, 560.0f, 220.0f});
picker.set_index(0);
EXPECT_EQ(picker.focus(), 0);
EXPECT_EQ(ColorPicker::hex_of(Color::rgb(0x3e9eff)), "#3E9EFF");
EXPECT_EQ(send(picker, nav(Direction::right)), Event::moved);
EXPECT_EQ(picker.index(), 0); // the ring moved, the colour did not
EXPECT_EQ(send(picker, press(Action::confirm)), Event::changed);
EXPECT_EQ(picker.index(), 1);
EXPECT_EQ(picker.hex(), ColorPicker::hex_of(picker.palette()[1]));
EXPECT_EQ(send(picker, press(Action::confirm)), Event::none);
EXPECT_EQ(send(picker, nav(Direction::down)), Event::moved);
EXPECT_EQ(picker.focus(), 1 + picker.style.columns);
EXPECT_EQ(send(picker, nav(Direction::left)), Event::moved);
EXPECT_EQ(send(picker, nav(Direction::left)), Event::refused);
picker.style.exits.left = true;
EXPECT_EQ(send(picker, nav(Direction::left)), Event::none);
EXPECT_EQ(picker.exit(), Direction::left);
picker.style.select_on_move = true;
EXPECT_EQ(send(picker, nav(Direction::right)), Event::changed);
EXPECT_EQ(picker.index(), picker.focus());
}
TEST_F(ComponentsPickers, ColorPickerHueSaturationValue)
{
float h = 0.0f, s = 0.0f, v = 0.0f;
ColorPicker::to_hsv(ColorPicker::from_hsv(200.0f, 0.4f, 0.8f), &h, &s, &v);
EXPECT_NEAR(h, 200.0f, 0.01f);
EXPECT_NEAR(s, 0.4f, 0.001f);
EXPECT_NEAR(v, 0.8f, 0.001f);
ColorPicker picker;
picker.style.kind = hui::ui::ColorPickerKind::hsv;
picker.set_bounds({100.0f, 100.0f, 560.0f, 220.0f});
picker.set_color(ColorPicker::from_hsv(200.0f, 0.5f, 0.5f));
EXPECT_EQ(picker.index(), -1); // not one of the presets
EXPECT_EQ(send(picker, nav(Direction::right)), Event::changed);
EXPECT_NEAR(picker.saturation(), 0.55f, 0.001f);
EXPECT_TRUE(asked(Cue::slider));
EXPECT_EQ(send(picker, nav(Direction::up)), Event::changed);
EXPECT_NEAR(picker.value(), 0.55f, 0.001f);
// Confirm switches to the hue strip, where the hue goes round.
EXPECT_EQ(send(picker, press(Action::confirm)), Event::moved);
EXPECT_EQ(picker.zone(), 1);
EXPECT_EQ(send(picker, nav(Direction::right)), Event::changed);
EXPECT_NEAR(picker.hue(), 210.0f, 0.01f);
picker.set_color(ColorPicker::from_hsv(355.0f, 0.5f, 0.5f));
EXPECT_EQ(send(picker, nav(Direction::right)), Event::changed);
EXPECT_NEAR(picker.hue(), 5.0f, 0.01f);
EXPECT_EQ(send(picker, nav(Direction::up)), Event::moved);
EXPECT_EQ(picker.zone(), 0);
// A limit of the field refuses.
picker.set_color(ColorPicker::from_hsv(120.0f, 1.0f, 1.0f));
EXPECT_EQ(send(picker, nav(Direction::right)), Event::refused);
EXPECT_EQ(send(picker, nav(Direction::up)), Event::refused);
// The stick moves the marker smoothly: the change is reported on the
// frame after, and the stick's own direction steps are ignored.
InputFrame tilt = idle();
tilt.stick_x = -1.0f;
tilt.nav = Direction::left;
tilt.nav_from_stick = true;
const float before = picker.saturation();
feedback_.clear();
EXPECT_EQ(picker.handle(tilt, feedback_), Event::none);
picker.update(0.1f);
EXPECT_LT(picker.saturation(), before);
EXPECT_GT(picker.saturation(), before - 0.2f);
EXPECT_EQ(picker.handle(idle(), feedback_), Event::changed);
const float after = picker.saturation();
picker.update(0.1f); // no stick this frame: nothing moves
picker.update(0.1f);
EXPECT_EQ(picker.saturation(), after);
}
TEST_F(ComponentsPickers, WheelPickerTurnsWrapsAndSpeedsUp)
{
WheelPicker wheel;
hui::ui::WheelColumn letters;
letters.values = {"A", "B", "C", "D", "E"};
hui::ui::WheelColumn numbers;
for (int i = 0; i < 40; ++i)
numbers.values.push_back(std::to_string(i));
wheel.set_columns({letters, numbers});
wheel.set_bounds({100.0f, 100.0f, 360.0f, wheel.preferred_height()});
EXPECT_EQ(send(wheel, nav(Direction::up)), Event::refused); // no wrap: the first is an end
EXPECT_EQ(send(wheel, nav(Direction::down)), Event::changed);
EXPECT_EQ(wheel.index(0), 1);
EXPECT_EQ(wheel.value(0), "B");
EXPECT_TRUE(asked(Cue::tick));
const float down_pitch = feedback_.cues.back().pitch;
EXPECT_EQ(send(wheel, nav(Direction::up)), Event::changed);
EXPECT_LT(feedback_.cues.back().pitch, down_pitch);
wheel.style.wrap = true;
EXPECT_EQ(send(wheel, nav(Direction::up)), Event::changed);
EXPECT_EQ(wheel.index(0), 4);
EXPECT_EQ(send(wheel, nav(Direction::down)), Event::changed);
EXPECT_EQ(wheel.index(0), 0);
// Columns: left and right, with edges that refuse or exit.
EXPECT_EQ(send(wheel, nav(Direction::left)), Event::refused);
EXPECT_EQ(send(wheel, nav(Direction::right)), Event::moved);
EXPECT_EQ(wheel.column(), 1);
wheel.style.exits.right = true;
EXPECT_EQ(send(wheel, nav(Direction::right)), Event::none);
EXPECT_EQ(wheel.exit(), Direction::right);
// A held direction takes longer strides after a moment.
wheel.style.wrap = false;
send(wheel, nav(Direction::down));
const int steps = wheel.style.fast_after + 4;
for (int i = 0; i < steps; ++i)
send(wheel, held(Direction::down));
EXPECT_EQ(wheel.changed_column(), 1);
EXPECT_GT(wheel.index(1), steps + 1);
// ... and lands on the end instead of stopping short of it.
for (int i = 0; i < 40; ++i)
send(wheel, held(Direction::down));
EXPECT_EQ(wheel.index(1), 39);
EXPECT_EQ(send(wheel, held(Direction::down)), Event::refused);
EXPECT_TRUE(feedback_.cues.empty());
}
TEST_F(ComponentsPickers, DatePickerKnowsTheMonths)
{
EXPECT_TRUE(DatePicker::is_leap_year(2024));
EXPECT_TRUE(DatePicker::is_leap_year(2000));
EXPECT_FALSE(DatePicker::is_leap_year(1900));
EXPECT_FALSE(DatePicker::is_leap_year(2023));
EXPECT_EQ(DatePicker::days_in_month(2024, 2), 29);
EXPECT_EQ(DatePicker::days_in_month(2023, 2), 28);
EXPECT_EQ(DatePicker::days_in_month(2023, 4), 30);
EXPECT_EQ(DatePicker::days_in_month(2023, 12), 31);
DatePicker date;
date.set_years(1990, 2030);
date.set_date(2024, 1, 31);
date.set_bounds({100.0f, 100.0f, 360.0f, date.preferred_height()});
EXPECT_EQ(date.text(), "31 Jan 2024");
EXPECT_EQ(date.wheel().column_at(0).values.size(), 31u);
// January 31 turned to February is the 29th in a leap year ...
EXPECT_EQ(send(date, nav(Direction::right)), Event::moved);
EXPECT_EQ(send(date, nav(Direction::down)), Event::changed);
EXPECT_EQ(date.month(), 2);
EXPECT_EQ(date.day(), 29);
EXPECT_EQ(date.wheel().column_at(0).values.size(), 29u);
// ... and the 28th when the year turns to an ordinary one.
send(date, nav(Direction::right));
EXPECT_EQ(send(date, nav(Direction::up)), Event::changed);
EXPECT_EQ(date.year(), 2023);
EXPECT_EQ(date.day(), 28);
EXPECT_EQ(date.text(), "28 Feb 2023");
// Another order keeps the date and the active part (the year).
date.style.order = hui::ui::DateOrder::year_month_day;
date.update(kFrame);
EXPECT_EQ(date.text(), "2023-02-28");
EXPECT_EQ(date.wheel().column(), 0);
EXPECT_EQ(send(date, nav(Direction::down)), Event::changed);
EXPECT_EQ(date.year(), 2024);
}
TEST_F(ComponentsPickers, TimePickerTwelveAndTwentyFourHours)
{
TimePicker time;
time.set_time(19, 30);
time.set_bounds({100.0f, 100.0f, 260.0f, time.preferred_height()});
EXPECT_EQ(time.text(), "19:30");
EXPECT_EQ(time.wheel().column_count(), 2);
EXPECT_EQ(send(time, nav(Direction::down)), Event::changed);
EXPECT_EQ(time.hour(), 20);
time.style.twelve_hour = true;
time.update(kFrame);
EXPECT_EQ(time.wheel().column_count(), 3);
EXPECT_EQ(time.text(), "8:30 PM");
send(time, nav(Direction::right));
send(time, nav(Direction::right));
EXPECT_EQ(send(time, nav(Direction::up)), Event::changed);
EXPECT_EQ(time.hour(), 8);
EXPECT_EQ(time.text(), "8:30 AM");
EXPECT_EQ(send(time, nav(Direction::up)), Event::refused); // two values never wrap
time.set_time(0, 5);
EXPECT_EQ(time.text(), "12:05 AM");
time.style.minute_step = 15;
time.update(kFrame);
EXPECT_EQ(time.wheel().column_at(1).values.size(), 4u);
send(time, nav(Direction::left));
EXPECT_EQ(time.wheel().column(), 1);
EXPECT_EQ(send(time, nav(Direction::down)), Event::changed);
EXPECT_EQ(time.minute(), 15);
}
TEST_F(ComponentsPickers, SliderStepsRefusesAndSpeedsUp)
{
Slider slider;
slider.set_range(0.0f, 100.0f, 10.0f);
slider.set_unit(" %");
slider.set_value(80.0f);
slider.set_bounds({100.0f, 100.0f, 480.0f, slider.preferred_height()});
EXPECT_EQ(slider.text(), "80 %");
EXPECT_EQ(send(slider, nav(Direction::right)), Event::changed);
EXPECT_FLOAT_EQ(slider.value(), 90.0f);
EXPECT_TRUE(asked(Cue::slider));
const float pitch = feedback_.cues.back().pitch;
EXPECT_EQ(send(slider, nav(Direction::right)), Event::changed);
EXPECT_GT(feedback_.cues.back().pitch, pitch); // the cue rises with the value
EXPECT_EQ(send(slider, nav(Direction::right)), Event::refused);
EXPECT_FLOAT_EQ(slider.value(), 100.0f);
EXPECT_EQ(send(slider, nav(Direction::up)), Event::none); // the screen's to use
slider.set_range(0.0f, 100.0f, 1.0f);
slider.set_value(100.0f);
send(slider, nav(Direction::left));
for (int i = 0; i < slider.style.fast_after + 2; ++i)
send(slider, held(Direction::left));
EXPECT_LT(slider.value(), 99.0f - static_cast<float>(slider.style.fast_after + 2));
slider.format = [](float value) { return value >= 50.0f ? "High" : "Low"; };
EXPECT_EQ(slider.text(), "High");
}
TEST_F(ComponentsPickers, RangeSliderThumbsDoNotCross)
{
RangeSlider range;
range.set_range(0.0f, 10.0f, 1.0f);
range.set_min_gap(2.0f);
range.set_values(9.0f, 2.0f); // given the wrong way round
EXPECT_FLOAT_EQ(range.low(), 2.0f);
EXPECT_FLOAT_EQ(range.high(), 9.0f);
range.set_values(3.0f, 6.0f);
range.set_bounds({100.0f, 100.0f, 480.0f, range.preferred_height()});
EXPECT_EQ(range.thumb(), 0);
EXPECT_EQ(range.text(), "3 - 6");
EXPECT_EQ(send(range, nav(Direction::right)), Event::changed);
EXPECT_FLOAT_EQ(range.low(), 4.0f);
EXPECT_EQ(send(range, nav(Direction::right)), Event::refused); // the gap holds it back
EXPECT_TRUE(asked(Cue::error));
EXPECT_FLOAT_EQ(range.low(), 4.0f);
EXPECT_EQ(send(range, press(Action::confirm)), Event::moved);
EXPECT_EQ(range.thumb(), 1);
EXPECT_EQ(send(range, nav(Direction::left)), Event::refused);
EXPECT_FLOAT_EQ(range.high(), 6.0f);
EXPECT_EQ(send(range, nav(Direction::right)), Event::changed);
EXPECT_FLOAT_EQ(range.high(), 7.0f);
EXPECT_EQ(send(range, nav(Direction::up)), Event::none);
range.style.vertical_switches = true;
EXPECT_EQ(send(range, nav(Direction::up)), Event::moved);
EXPECT_EQ(range.thumb(), 0);
}
TEST_F(ComponentsPickers, RestylingKeepsEveryValue)
{
Select select = make_select();
CheckGroup checks;
checks.set_items({{"One"}, {"Two"}});
checks.set_checked(1, true);
TagSelect tags = make_tags();
tags.set_selected(3, true);
tags.set_focus(5);
ColorPicker picker;
picker.set_index(4);
DatePicker date;
date.set_date(2012, 12, 21);
TimePicker time;
time.set_time(7, 45);
RangeSlider range;
range.set_values(20.0f, 60.0f);
for (const hui::ui::Theme &theme : hui::ui::themes())
{
select.style.theme = theme;
checks.style.theme = theme;
tags.style.theme = theme;
picker.style.theme = theme;
picker.style.kind = hui::ui::ColorPickerKind::hsv;
date.style.theme = theme;
time.style.theme = theme;
range.style.theme = theme;
select.update(kFrame);
checks.update(kFrame);
tags.update(kFrame);
picker.update(kFrame);
date.update(kFrame);
time.update(kFrame);
range.update(kFrame);
EXPECT_EQ(select.index(), 1) << theme.id;
EXPECT_TRUE(checks.checked(1)) << theme.id;
EXPECT_TRUE(tags.selected(3)) << theme.id;
EXPECT_EQ(tags.focus(), 5) << theme.id;
EXPECT_EQ(picker.index(), 4) << theme.id;
EXPECT_EQ(date.text(), "21 Dec 2012") << theme.id;
EXPECT_EQ(time.text(), "07:45") << theme.id;
EXPECT_FLOAT_EQ(range.low(), 20.0f) << theme.id;
}
}
TEST_F(ComponentsPickers, DrawsInEveryThemeAndVariant)
{
using hui::ui::HighlightKind;
Select select = make_select();
CheckGroup checks;
checks.set_title("Checks");
checks.set_items({{"One", "With a description"}, {"Two"}, {"Three", "", true}, {"Four"}});
checks.set_checked(0, true);
RadioGroup radios;
radios.set_items({{"Solo"}, {"Co-op"}, {"Versus"}});
radios.set_selected(1);
TagSelect tags = make_tags();
tags.set_selected(2, true);
ColorPicker picker;
picker.set_title("Colour");
WheelPicker wheel;
hui::ui::WheelColumn sizes;
sizes.values = {"XS", "S", "M", "L", "XL"};
wheel.set_columns({sizes, sizes});
DatePicker date;
date.set_title("Date");
TimePicker time;
Slider slider;
slider.set_label("Volume");
RangeSlider range;
range.set_label("Range");
select.set_bounds({100.0f, 100.0f, 420.0f, 90.0f});
checks.set_bounds({100.0f, 220.0f, 420.0f, 260.0f});
radios.set_bounds({100.0f, 500.0f, 420.0f, 160.0f});
tags.set_bounds({600.0f, 100.0f, 500.0f, 200.0f});
picker.set_bounds({600.0f, 320.0f, 560.0f, 220.0f});
wheel.set_bounds({600.0f, 560.0f, 240.0f, 220.0f});
date.set_bounds({1200.0f, 100.0f, 320.0f, 220.0f});
time.set_bounds({1200.0f, 340.0f, 240.0f, 220.0f});
slider.set_bounds({1200.0f, 580.0f, 420.0f, 120.0f});
range.set_bounds({1200.0f, 720.0f, 420.0f, 120.0f});
for (const hui::ui::Theme &theme : hui::ui::themes())
{
for (int variant = 0; variant < 3; ++variant)
{
const HighlightKind kind = variant == 0 ? HighlightKind::tint
: variant == 1 ? HighlightKind::fill
: HighlightKind::ring;
select.style.theme = theme;
select.style.label =
variant == 1 ? hui::ui::SelectLabel::inside : hui::ui::SelectLabel::above;
select.style.highlight.kind = kind;
select.style.on_page = variant == 2;
checks.style.theme = theme;
checks.style.highlight.kind = kind;
checks.style.select_all = variant != 1;
checks.style.layout =
variant == 2 ? hui::ui::ChoiceLayout::grid : hui::ui::ChoiceLayout::vertical;
checks.style.on_page = variant == 2;
radios.style.theme = theme;
radios.style.highlight.kind = kind;
radios.style.layout =
variant == 1 ? hui::ui::ChoiceLayout::horizontal : hui::ui::ChoiceLayout::vertical;
tags.style.theme = theme;
tags.style.shape = variant == 0 ? hui::ui::TagShape::theme
: variant == 1 ? hui::ui::TagShape::pill
: hui::ui::TagShape::square;
tags.style.check = variant != 2;
tags.style.align = variant == 1 ? hui::gfx::Align::center : hui::gfx::Align::left;
picker.style.theme = theme;
picker.style.kind =
variant == 1 ? hui::ui::ColorPickerKind::hsv : hui::ui::ColorPickerKind::swatches;
picker.style.preview = variant != 2;
wheel.style.theme = theme;
wheel.style.highlight.kind = kind;
wheel.style.boxed = variant != 2;
wheel.style.on_page = variant == 2;
wheel.style.wrap = variant == 1;
date.style.theme = theme;
date.style.order = variant == 1 ? hui::ui::DateOrder::month_day_year
: hui::ui::DateOrder::day_month_year;
date.style.months =
variant == 2 ? hui::ui::MonthStyle::full_name : hui::ui::MonthStyle::short_name;
time.style.theme = theme;
time.style.twelve_hour = variant == 1;
for (hui::ui::SliderStyle *style : {&slider.style, &range.style})
{
style->theme = theme;
style->bubble = variant == 0 ? hui::ui::BubbleMode::always
: variant == 1 ? hui::ui::BubbleMode::focused
: hui::ui::BubbleMode::never;
style->ticks = variant == 1 ? 6 : 0;
style->end_labels = variant == 2;
}
const bool focused = variant != 2;
select.set_active(focused);
checks.set_active(focused);
radios.set_active(focused);
tags.set_active(focused);
picker.set_active(focused);
picker.set_engaged(variant == 1);
wheel.set_active(focused);
wheel.set_engaged(variant == 0);
date.set_active(focused);
time.set_active(focused);
slider.set_active(focused);
range.set_active(focused);
if (variant == 1)
select.open(feedback_);
else
select.dismiss();
for (int frame = 0; frame < 4; ++frame)
{
select.update(kFrame);
checks.update(kFrame);
radios.update(kFrame);
tags.update(kFrame);
picker.update(kFrame);
wheel.update(kFrame);
date.update(kFrame);
time.update(kFrame);
slider.update(kFrame);
range.update(kFrame);
}
hui::ui::Canvas target = canvas();
select.draw(target);
checks.draw(target);
radios.draw(target);
tags.draw(target);
picker.draw(target);
wheel.draw(target);
date.draw(target);
time.draw(target);
slider.draw(target);
range.draw(target);
select.draw_popover(target);
expect_drawn(theme.id);
}
}
}
} // namespace