#include "Gallery.hpp" #include "Theme.hpp" #include "Ui.hpp" #include "BoxLayout.hpp" #include "StackLayout.hpp" #include #include using namespace tp; using namespace tp::ui; using namespace tp::ui::gallery; namespace { constexpr halnf kPageSpacing = 12; std::string percent(halnf fraction) { return std::to_string((int) std::round(fraction * 100)) + "%"; } std::string colorText(const RGBA& color) { char buffer[48]; std::snprintf(buffer, sizeof(buffer), "rgb %.2f %.2f %.2f", color.r, color.g, color.b); return buffer; } } ControlsPage::ControlsPage(GalleryState& state) : mState(state), mSecondSeparator(Axis::Horizontal) { setDebugName("controls page"); ColumnLayout& layout = emplaceLayout(kPageSpacing); layout.setPadding(Insets::uniform(16)); layout.setCrossAlign(CrossAlign::Start); mButtonRow.emplaceLayout(8).setCrossAlign(CrossAlign::Center); clickMe.setOnActivate([this]() { mState.buttonClicks++; refreshClickCount(); }); clickMe.setTooltip("Counts how many times it was clicked"); accent.setStyle(Button::Style::Accent); disabled.setEnabled(false); mButtonRow.addChild(clickMe); mButtonRow.addChild(accent); mButtonRow.addChild(disabled); mButtonRow.addChild(clickCount); refreshClickCount(); notifications.setChecked(mState.notificationsEnabled); notifications.setOnChanged([this](bool checked) { mState.notificationsEnabled = checked; }); darkMode.setChecked(mState.darkMode); darkMode.setOnChanged([this](bool checked) { mState.darkMode = checked; }); mRadioRow.emplaceLayout(16); for (RadioButton* option : { &low, &medium, &high }) { quality.add(*option); mRadioRow.addChild(*option); } quality.selectIndex((size_t) mState.qualityIndex); quality.setOnChanged([this](size_t index) { mState.qualityIndex = (long) index; }); volume.setSizing(AxisSizing::fixed(260), AxisSizing::fit()); volume.setValueFormatter(percent); volume.setValue(mState.volume); volume.setOnChanged([this](halnf value) { mState.volume = value; progress.setFraction(value); }); brushSize.setSizing(AxisSizing::fixed(260), AxisSizing::fit()); brushSize.setScale(Slider::Scale::Logarithmic); brushSize.setValueFormatter([](halnf value) { return std::to_string((int) std::lround(value)) + " px"; }); progress.setSizing(AxisSizing::fixed(260), AxisSizing::fit()); progress.setFraction(mState.volume); blendMode.setTooltip("Layer blend mode"); accentColor.setTooltip("Accent color"); accentColor.setOnSelected([this](long index) { const RGBA choices[] = { accents::blue, accents::purple, accents::green, accents::orange, accents::pink }; if (index >= 0 && index < 5 && ui()) ui()->setTheme(Theme::standard().withAccent(choices[index])); }); addChild(mButtonRow); addChild(mFirstSeparator); addChild(notifications); addChild(darkMode); addChild(mRadioRow); addChild(mSecondSeparator); addChild(volume); addChild(brushSize); addChild(progress); addChild(blendMode); addChild(accentColor); } void ControlsPage::refreshClickCount() { clickCount.setText("clicked " + std::to_string(mState.buttonClicks) + " times"); } InputPage::InputPage(GalleryState& state) : mState(state) { setDebugName("input page"); ColumnLayout& layout = emplaceLayout(kPageSpacing); layout.setPadding(Insets::uniform(16)); layout.setCrossAlign(CrossAlign::Start); name.setText(mState.userName); name.setPlaceholder("Type a name"); name.setSizing(AxisSizing::fixed(260), AxisSizing::fit()); name.setOnEdited([this](const std::string& text) { mState.userName = text; refresh(); }); size.setSuffix(" px"); size.setValue(mState.brushSize); size.setSizing(AxisSizing::fixed(260), AxisSizing::fit()); size.setOnChanged([this](halnf value) { mState.brushSize = value; refresh(); }); dialog.setMessage("Widgets gallery: text, number fields and dialogs."); dialog.addButton("Close", {}, true); openDialog.setOnActivate([this]() { if (ui()) dialog.open(*ui()); }); askConfirm.setOnActivate([this]() { if (!ui()) return; confirm.ask(*ui(), "Clear history", "Are you sure you want to clear history?", "Clear history", [this]() { mState.confirmations++; refresh(); }); }); showNotice.setOnActivate([this]() { notice.show(*this, "Saved " + mState.userName + "'s settings"); }); mDialogRow.emplaceLayout(8).setCrossAlign(CrossAlign::Center); mDialogRow.addChild(openDialog); mDialogRow.addChild(askConfirm); mDialogRow.addChild(showNotice); mDialogRow.addChild(confirmations); addChild(nameLabel); addChild(name); addChild(sizeLabel); addChild(size); addChild(greeting); addChild(mDialogRow); refresh(); } void InputPage::refresh() { char size[32]; std::snprintf(size, sizeof(size), "%.0f", (double) mState.brushSize); greeting.setText( "Hello, " + (mState.userName.empty() ? std::string("stranger") : mState.userName) + " - brush " + size + " px" ); confirmations.setText("confirmed " + std::to_string(mState.confirmations) + " times"); } FilesPage::FilesPage(FileSystem& fileSystem) : browser(fileSystem), dialog(fileSystem) { setDebugName("files page"); ColumnLayout& layout = emplaceLayout(kPageSpacing); layout.setPadding(Insets::uniform(16)); hint.setTone(Label::Tone::Muted); status.setTone(Label::Tone::Muted); browser.setSizing(AxisSizing::fill(), AxisSizing::fill()); browser.setDirectory(fileSystem.home()); browser.onSelected = [this](const FileEntry& entry) { status.setText("Selected " + joinPath(browser.directory(), entry.name)); }; browser.onFileOpened = [this](const std::string& path) { report("Opened", path); }; actions.setSizing(AxisSizing::fill(), AxisSizing::fit()); actions.emplaceLayout(8); openFile.setOnActivate([this]() { if (!ui()) return; dialog.show(*ui(), FileDialog::Mode::Open, "Open file", browser.directory(), {}, [this](const std::string& path) { report("Opened", path); }); }); saveFile.setOnActivate([this]() { if (!ui()) return; dialog.show( *ui(), FileDialog::Mode::Save, "Save as", browser.directory(), "untitled.txt", [this](const std::string& path) { report("Would save to", path); } ); }); chooseFolder.setOnActivate([this]() { if (!ui()) return; dialog.show( *ui(), FileDialog::Mode::ChooseDirectory, "Choose folder", browser.directory(), {}, [this](const std::string& path) { report("Chose", path); browser.setDirectory(path); } ); }); actions.addChild(openFile); actions.addChild(saveFile); actions.addChild(chooseFolder); addChild(hint); addChild(browser); addChild(actions); addChild(status); } void FilesPage::report(const std::string& action, const std::string& path) { status.setText(action + " " + path); } MediaPage::MediaPage() : playlist( { { "Jumpsuit", "twenty one pilots", "Trench", 238.5f, true }, { "Mystery Lady", "Masego & Don Toliver", "Studying Abroad", 205, false }, { "No Idea", "Don Toliver", "Heaven or Hell", 154, true } } ) { setDebugName("media page"); ColumnLayout& layout = emplaceLayout(kPageSpacing); layout.setPadding(Insets::uniform(16)); hint.setTone(Label::Tone::Muted); status.setTone(Label::Tone::Muted); player.onPlayPause = [this]() { togglePlaying(); }; player.onPrevious = [this]() { load((current + playlist.size() - 1) % playlist.size()); }; player.onNext = [this]() { load((current + 1) % playlist.size()); }; player.seek.setOnSeek([this](halnf position) { status.setText("Seeked to " + formatDuration(position * playlist[current].durationSeconds)); }); player.volume.setOnChanged([this](halnf volume) { status.setText("Volume " + std::to_string((int) std::lround(volume * 100)) + "%"); }); addChild(hint); addChild(player); addChild(status); load(0); } void MediaPage::load(size_t index) { current = index; player.setTrack(&playlist[current]); player.setPosition(0); player.setBuffered(1); status.setText("Loaded \"" + playlist[current].title + "\""); } void MediaPage::togglePlaying() { player.setPlaying(!player.isPlaying()); if (player.isPlaying()) startTicking(); else stopTicking(); status.setText(player.isPlaying() ? "Playing" : "Paused"); } void MediaPage::onTick(halnf deltaMs) { const halnf duration = playlist[current].durationSeconds; if (!player.isPlaying() || duration <= 0 || player.seek.isScrubbing()) return; const halnf position = player.seek.position() + deltaMs / 1000.0f / duration; if (position < 1) { player.setPosition(position); return; } load((current + 1) % playlist.size()); player.setPlaying(true); } ReorderPage::ReorderPage() { setDebugName("reorder page"); ColumnLayout& layout = emplaceLayout(kPageSpacing); layout.setPadding(Insets::uniform(16)); layout.setCrossAlign(CrossAlign::Start); hint.setTone(Label::Tone::Muted); list.setSizing(AxisSizing::fixed(320), AxisSizing::fit()); for (const char* task : { "Sketch", "Line art", "Flat colors", "Shading", "Highlights", "Background" }) { tasks.push_back(std::make_unique(task)); tasks.back()->setSizing(AxisSizing::fill(), AxisSizing::fit()); list.addItem(*tasks.back()); } list.setOnMoved([this](size_t from, size_t to) { status.setText( "Moved \"" + static_cast(list.item(to)).text() + "\" from " + std::to_string(from + 1) + " to " + std::to_string(to + 1) ); }); status.setTone(Label::Tone::Muted); addChild(hint); addChild(list); addChild(status); refreshStatus(); } void ReorderPage::refreshStatus() { status.setText("Nothing moved yet"); } ListPage::ListPage() { setDebugName("list page"); emplaceLayout(); content.emplaceLayout(0).setPadding(Insets::uniform(8)); content.setSizing(AxisSizing::fill(), AxisSizing::fit()); const char* const groupNames[] = { "Brushes", "Layers", "Documents" }; const char* const subgroupNames[] = { "Recent", "Favorites", "Archived" }; for (const char* groupName : groupNames) { groups.push_back(std::make_unique(groupName)); Collapsible& group = *groups.back(); group.header().setSummary(std::to_string(std::size(subgroupNames)) + " folders"); for (const char* subgroupName : subgroupNames) { subgroups.push_back(std::make_unique(subgroupName)); Collapsible& subgroup = *subgroups.back(); constexpr int kItems = 5; const bool reorderable = std::string(subgroupName) == "Favorites"; subgroup.header().setSummary(std::to_string(kItems) + (reorderable ? " ยท drag to sort" : "")); ReorderableList* sortable = nullptr; if (reorderable) { favoriteLists.push_back(std::make_unique()); sortable = favoriteLists.back().get(); subgroup.body().addChild(*sortable); } for (int item = 0; item < kItems; item++) { groupRows.push_back( std::make_unique