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.
527 lines
17 KiB
C++
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);
|
|
}
|