Modules/Widgets/examples/Gallery.cpp
Шурупов Илья Викторович 81949323d9 Rename the code name to Sketch2D
Stroke2D becomes Sketch2D across folders, files, namespaces, classes,
targets, scripts, the Android package (online.ilushabasic.sketch2d)
and the update URL. A stroke2d-sketches folder is adopted like the
older leo-sketches one.
2026-10-17 19:47:01 +03:00

527 lines
17 KiB
C++

#include "Gallery.hpp"
#include "Theme.hpp"
#include "Ui.hpp"
#include "BoxLayout.hpp"
#include "StackLayout.hpp"
#include <cmath>
#include <cstdio>
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<ColumnLayout>(kPageSpacing);
layout.setPadding(Insets::uniform(16));
layout.setCrossAlign(CrossAlign::Start);
mButtonRow.emplaceLayout<RowLayout>(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<RowLayout>(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<ColumnLayout>(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<RowLayout>(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<ColumnLayout>(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<RowLayout>(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<ColumnLayout>(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<ColumnLayout>(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<Checkbox>(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<Checkbox&>(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<StackLayout>();
content.emplaceLayout<ColumnLayout>(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<Collapsible>(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<Collapsible>(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<ReorderableList>());
sortable = favoriteLists.back().get();
subgroup.body().addChild(*sortable);
}
for (int item = 0; item < kItems; item++) {
groupRows.push_back(
std::make_unique<Button>(std::string(groupName) + " / " + subgroupName + " " + std::to_string(item + 1))
);
groupRows.back()->setStyle(Button::Style::Flat);
groupRows.back()->setSizing(AxisSizing::fill(), AxisSizing::fit());
if (sortable) sortable->addItem(*groupRows.back());
else subgroup.body().addChild(*groupRows.back());
}
group.body().addChild(subgroup);
}
list.addItem(group);
}
for (int index = 0; index < 60; index++) {
rows.push_back(std::make_unique<Button>("Row " + std::to_string(index)));
rows.back()->setStyle(Button::Style::Flat);
rows.back()->setSizing(AxisSizing::fill(), AxisSizing::fit());
allRows.body().addChild(*rows.back());
}
allRows.header().setSummary(std::to_string(rows.size()));
list.addItem(allRows);
const char* const steps[] = { "Sketch", "Line art", "Flat colors", "Shading", "Highlights" };
for (const char* step : steps) {
draggableItems.push_back(std::make_unique<Checkbox>(step));
draggableItems.back()->setSizing(AxisSizing::fill(), AxisSizing::fit());
draggable.addItem(*draggableItems.back());
staticItems.push_back(std::make_unique<Checkbox>(step));
staticItems.back()->setSizing(AxisSizing::fill(), AxisSizing::fit());
staticList.addChild(*staticItems.back());
}
draggableStatus.setTone(Label::Tone::Muted);
draggableStatus.setText("Drag the grip or the row to reorder");
draggable.setOnMoved([this](size_t from, size_t to) {
draggableStatus.setText(
"Moved \"" + static_cast<Checkbox&>(draggable.item(to)).text() + "\" from " + std::to_string(from + 1) + " to " +
std::to_string(to + 1)
);
});
draggableSection.header().setSummary("reorderable");
draggableSection.body().addChild(draggable);
draggableSection.body().addChild(draggableStatus);
list.addItem(draggableSection);
staticList.emplaceLayout<ColumnLayout>(2);
staticList.setSizing(AxisSizing::fill(), AxisSizing::fit());
staticSection.header().setSummary("fixed order");
staticSection.body().addChild(staticList);
list.addItem(staticSection);
content.addChild(list);
scroll.setContent(&content);
addChild(scroll);
}
CanvasPage::CanvasPage(GalleryState& state) :
mState(state) {
setDebugName("canvas page");
emplaceLayout<StackLayout>();
desk.emplaceLayout<FreeLayout>();
wheel.setColor(mState.brushColor);
wheel.setOnChanged([this](const RGBA& color) {
mState.brushColor = color;
refreshReadout();
});
colorPanel.setContent(&wheel);
colorPanel.setPanelSize({ 240, 260 });
colorPanel.requestPosition({ 24, 24 });
desk.addChild(colorPanel);
inspector.emplaceLayout<ColumnLayout>(8).setPadding(Insets::uniform(12));
inspector.addChild(inspectorTitle);
inspector.addChild(colorReadout);
refreshReadout();
split.setFirst(&desk);
split.setSecond(&inspector);
split.setRatio(0.7f);
addChild(split);
}
void CanvasPage::refreshReadout() { colorReadout.setText(colorText(mState.brushColor)); }
DockPage::DockPage() {
setDebugName("dock page");
emplaceLayout<StackLayout>().setPadding(Insets::uniform(Theme::standard().metrics.surfaceGap));
editor.emplaceLayout<StackLayout>().setPadding(Insets::uniform(16));
editor.addChild(editorText);
editor.setSizing(AxisSizing::fill(), AxisSizing::fill());
dock.setCenter(&editor);
project.setContent(&projectText);
structure.setContent(&structureText);
terminal.setContent(&terminalText);
properties.setContent(&propertiesText);
terminal.setDockedLength(160);
for (DockPanel* panel : { &project, &structure, &terminal, &properties })
dock.addPanel(*panel);
dock.openPanel(project);
dock.openPanel(terminal);
addChild(dock);
}
Gallery::Gallery() :
Panel(Surface::Window) {
setDebugName("gallery");
setSizing(AxisSizing::fill(), AxisSizing::fill());
emplaceLayout<ColumnLayout>();
buildMenus();
tabs.setSizing(AxisSizing::fill(), AxisSizing::fill());
controlsScroll.setContent(&controls);
tabs.addTab("Controls", controlsScroll);
tabs.addTab("List", listPage);
tabs.addTab("Canvas", canvasPage);
tabs.addTab("Docking", dockPage);
tabs.addTab("Input", inputPage);
tabs.addTab("Reorder", reorderPage);
tabs.addTab("Theme", themePage);
tabs.addTab("Media", mediaPage);
tabs.addTab("Files", filesPage);
statusBar.setTone(Label::Tone::Muted);
statusBar.setText("ready");
statusArea.setSizing(AxisSizing::fill(), AxisSizing::fit());
statusArea.emplaceLayout<RowLayout>().setPadding(Insets::symmetric(10, 3));
statusArea.addChild(statusBar);
addChild(menuBar);
addChild(tabs);
addChild(statusArea);
}
void Gallery::onEvent(Event& event) {
const auto* binding = shortcuts.find(event);
if (!binding) return;
run(binding->command);
event.accept();
}
void Gallery::run(Action action) {
switch (action) {
case Action::New: report("new"); break;
case Action::Open: report("open"); break;
case Action::Quit: report("quit"); break;
case Action::NextTab: tabs.setCurrentIndex((tabs.currentIndex() + 1) % tabs.tabCount()); break;
}
}
void Gallery::buildMenus() {
shortcuts.bind(ctrl(InputID::N), Action::New);
shortcuts.bind(ctrl(InputID::O), Action::Open);
shortcuts.bind(ctrl(InputID::Q), Action::Quit);
shortcuts.bind(ctrl(InputID::TAB), Action::NextTab, true);
fileMenu.addItem("New", [this]() { run(Action::New); }, shortcuts.textFor(Action::New));
fileMenu.addItem("Open...", [this]() { run(Action::Open); }, shortcuts.textFor(Action::Open));
fileMenu.addSubmenu("Open Recent", recentMenu);
fileMenu.addSeparator();
fileMenu.addItem("Quit", [this]() { run(Action::Quit); }, shortcuts.textFor(Action::Quit));
recentMenu.addItem("sketch.sketch2d", [this]() { report("open sketch.sketch2d"); });
recentMenu.addItem("portrait.sketch2d", [this]() { report("open portrait.sketch2d"); });
viewMenu.addToggle(
"Dark Mode", [this]() { return state.darkMode; }, [this]() { controls.darkMode.setChecked(!state.darkMode); }
);
MenuEntry lastAction = MenuEntry::item("Last action", {});
lastAction.dynamicText = [this]() {
return state.lastAction.empty() ? std::string("No action yet") : "Last action: " + state.lastAction;
};
lastAction.isEnabled = [this]() { return !state.lastAction.empty(); };
viewMenu.add(lastAction);
viewMenu.addSeparator();
viewMenu.addItem("Controls", [this]() { tabs.setCurrentIndex(0); });
viewMenu.addItem("List", [this]() { tabs.setCurrentIndex(1); });
viewMenu.addItem("Canvas", [this]() { tabs.setCurrentIndex(2); });
viewMenu.addItem("Input", [this]() { tabs.setCurrentIndex(4); });
viewMenu.addItem("Next Tab", [this]() { run(Action::NextTab); }, shortcuts.textFor(Action::NextTab));
menuBar.addMenu("File", fileMenu);
menuBar.addMenu("View", viewMenu);
}
void Gallery::report(const std::string& action) {
state.lastAction = action;
statusBar.setText(action);
}