mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
Merge branch 'v5' of https://github.com/noctalia-dev/noctalia-shell into v5
This commit is contained in:
@@ -1091,7 +1091,8 @@
|
||||
},
|
||||
"community-palette": {
|
||||
"label": "Community Palette",
|
||||
"description": "Choose a palette from the community catalog"
|
||||
"description": "Choose a palette from the community catalog",
|
||||
"search-placeholder": "Search palettes..."
|
||||
},
|
||||
"ui-scale": {
|
||||
"label": "UI Scale",
|
||||
|
||||
@@ -1309,7 +1309,11 @@ namespace settings {
|
||||
pendingDeleteWidgetSettingPath.clear();
|
||||
requestRebuild();
|
||||
});
|
||||
auto* pickerPtr = picker.get();
|
||||
inspector->addChild(std::move(picker));
|
||||
if (ctx.focusArea) {
|
||||
ctx.focusArea(pickerPtr->filterInputArea());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
class Button;
|
||||
class Flex;
|
||||
class InputArea;
|
||||
class Node;
|
||||
|
||||
namespace settings {
|
||||
@@ -29,6 +30,7 @@ namespace settings {
|
||||
|
||||
std::function<void()> requestRebuild;
|
||||
std::function<void()> resetContentScroll;
|
||||
std::function<void(InputArea*)> focusArea;
|
||||
std::function<void(std::vector<std::string>, ConfigOverrideValue)> setOverride;
|
||||
std::function<void(std::vector<std::pair<std::vector<std::string>, ConfigOverrideValue>>)> setOverrides;
|
||||
std::function<void(std::vector<std::string>)> clearOverride;
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "ui/controls/glyph.h"
|
||||
#include "ui/controls/input.h"
|
||||
#include "ui/controls/label.h"
|
||||
#include "ui/controls/search_picker.h"
|
||||
#include "ui/controls/segmented.h"
|
||||
#include "ui/controls/select.h"
|
||||
#include "ui/controls/separator.h"
|
||||
@@ -58,6 +59,26 @@ namespace settings {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::string pathKey(const std::vector<std::string>& path) {
|
||||
std::string out;
|
||||
for (const auto& part : path) {
|
||||
if (!out.empty()) {
|
||||
out.push_back('.');
|
||||
}
|
||||
out += part;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string optionLabel(const std::vector<SelectOption>& options, std::string_view value) {
|
||||
for (const auto& opt : options) {
|
||||
if (opt.value == value) {
|
||||
return opt.label;
|
||||
}
|
||||
}
|
||||
return std::string(value);
|
||||
}
|
||||
|
||||
std::vector<std::string> optionLabels(const std::vector<SelectOption>& options) {
|
||||
std::vector<std::string> labels;
|
||||
labels.reserve(options.size());
|
||||
@@ -67,6 +88,19 @@ namespace settings {
|
||||
return labels;
|
||||
}
|
||||
|
||||
std::vector<SearchPickerOption> searchPickerOptions(const std::vector<SelectOption>& options) {
|
||||
std::vector<SearchPickerOption> out;
|
||||
out.reserve(options.size());
|
||||
for (const auto& opt : options) {
|
||||
out.push_back(SearchPickerOption{.value = opt.value,
|
||||
.label = opt.label,
|
||||
.description = opt.description,
|
||||
.category = opt.category,
|
||||
.enabled = true});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
bool isBlankInput(std::string_view text) { return StringUtils::trim(text).empty(); }
|
||||
|
||||
const std::string& localeDecimalSeparator() {
|
||||
@@ -634,6 +668,149 @@ namespace settings {
|
||||
return select;
|
||||
};
|
||||
|
||||
const auto makeSearchPickerButton = [&](const SettingEntry& entry,
|
||||
const SearchPickerSetting& setting) -> std::unique_ptr<Node> {
|
||||
auto button = std::make_unique<Button>();
|
||||
button->setVariant(ButtonVariant::Outline);
|
||||
button->setGlyph("search");
|
||||
button->setText(optionLabel(setting.options, setting.selectedValue));
|
||||
button->setContentAlign(ButtonContentAlign::Start);
|
||||
button->setFontSize(Style::fontSizeBody * scale);
|
||||
button->setGlyphSize(Style::fontSizeBody * scale);
|
||||
button->setMinWidth(190.0f * scale);
|
||||
button->setMinHeight(Style::controlHeight * scale);
|
||||
button->setPadding(Style::spaceSm * scale, Style::spaceMd * scale);
|
||||
button->setRadius(Style::radiusMd * scale);
|
||||
button->setOnClick([&openPath = ctx.openSearchPickerPath, requestContentRebuild = ctx.requestContentRebuild,
|
||||
path = entry.path]() {
|
||||
openPath = pathKey(path);
|
||||
requestContentRebuild();
|
||||
});
|
||||
return button;
|
||||
};
|
||||
|
||||
const auto makeSearchPickerBlock = [&](Flex& section, const SettingEntry& entry,
|
||||
const SearchPickerSetting& setting) {
|
||||
const bool overridden = (ctx.configService != nullptr && ctx.configService->hasOverride(entry.path));
|
||||
const std::string pickerPath = pathKey(entry.path);
|
||||
|
||||
auto block = std::make_unique<Flex>();
|
||||
block->setDirection(FlexDirection::Vertical);
|
||||
block->setAlign(FlexAlign::Stretch);
|
||||
block->setGap(Style::spaceXs * scale);
|
||||
|
||||
const auto makeBadge = [&](std::string_view label, const ColorSpec& fill, const ColorSpec& color) {
|
||||
auto badge = std::make_unique<Flex>();
|
||||
badge->setAlign(FlexAlign::Center);
|
||||
badge->setPadding(1.0f * scale, Style::spaceXs * scale);
|
||||
badge->setRadius(Style::radiusSm * scale);
|
||||
badge->setFill(fill);
|
||||
badge->addChild(makeLabel(label, Style::fontSizeCaption * scale, color, true));
|
||||
return badge;
|
||||
};
|
||||
|
||||
auto headerRow = std::make_unique<Flex>();
|
||||
headerRow->setDirection(FlexDirection::Horizontal);
|
||||
headerRow->setAlign(FlexAlign::Center);
|
||||
headerRow->setJustify(FlexJustify::SpaceBetween);
|
||||
headerRow->setGap(Style::spaceXs * scale);
|
||||
headerRow->setPadding(2.0f * scale, 0.0f);
|
||||
headerRow->setMinHeight(Style::controlHeight * scale);
|
||||
|
||||
auto copy = std::make_unique<Flex>();
|
||||
copy->setDirection(FlexDirection::Vertical);
|
||||
copy->setAlign(FlexAlign::Start);
|
||||
copy->setGap(Style::spaceXs * scale);
|
||||
copy->setFlexGrow(1.0f);
|
||||
|
||||
auto titleRow = std::make_unique<Flex>();
|
||||
titleRow->setDirection(FlexDirection::Horizontal);
|
||||
titleRow->setAlign(FlexAlign::Center);
|
||||
titleRow->setGap(Style::spaceSm * scale);
|
||||
titleRow->addChild(
|
||||
makeLabel(entry.title, Style::fontSizeBody * scale, colorSpecFromRole(ColorRole::OnSurface), true));
|
||||
|
||||
if (overridden) {
|
||||
titleRow->addChild(makeBadge(i18n::tr("settings.badges.override"), colorSpecFromRole(ColorRole::Primary, 0.15f),
|
||||
colorSpecFromRole(ColorRole::Primary)));
|
||||
}
|
||||
if (entry.advanced) {
|
||||
titleRow->addChild(makeBadge(i18n::tr("settings.badges.advanced"),
|
||||
colorSpecFromRole(ColorRole::OnSurfaceVariant, 0.12f),
|
||||
colorSpecFromRole(ColorRole::OnSurfaceVariant)));
|
||||
}
|
||||
copy->addChild(std::move(titleRow));
|
||||
|
||||
if (!entry.subtitle.empty()) {
|
||||
auto detail = makeLabel(entry.subtitle, Style::fontSizeCaption * scale,
|
||||
colorSpecFromRole(ColorRole::OnSurfaceVariant), false);
|
||||
detail->setMaxWidth(360.0f * scale);
|
||||
copy->addChild(std::move(detail));
|
||||
}
|
||||
|
||||
headerRow->addChild(std::move(copy));
|
||||
|
||||
auto actions = std::make_unique<Flex>();
|
||||
actions->setDirection(FlexDirection::Horizontal);
|
||||
actions->setAlign(FlexAlign::Center);
|
||||
actions->setGap(Style::spaceSm * scale);
|
||||
if (overridden) {
|
||||
actions->addChild(makeResetButton(entry.path));
|
||||
}
|
||||
|
||||
auto closeBtn = std::make_unique<Button>();
|
||||
closeBtn->setVariant(ButtonVariant::Ghost);
|
||||
closeBtn->setGlyph("close");
|
||||
closeBtn->setGlyphSize(Style::fontSizeCaption * scale);
|
||||
closeBtn->setMinWidth(Style::controlHeightSm * scale);
|
||||
closeBtn->setMinHeight(Style::controlHeightSm * scale);
|
||||
closeBtn->setPadding(Style::spaceXs * scale);
|
||||
closeBtn->setRadius(Style::radiusSm * scale);
|
||||
closeBtn->setOnClick([&openPath = ctx.openSearchPickerPath, requestContentRebuild = ctx.requestContentRebuild]() {
|
||||
openPath.clear();
|
||||
requestContentRebuild();
|
||||
});
|
||||
actions->addChild(std::move(closeBtn));
|
||||
headerRow->addChild(std::move(actions));
|
||||
block->addChild(std::move(headerRow));
|
||||
|
||||
auto picker = std::make_unique<SearchPicker>();
|
||||
if (!setting.placeholder.empty()) {
|
||||
picker->setPlaceholder(setting.placeholder);
|
||||
}
|
||||
if (!setting.emptyText.empty()) {
|
||||
picker->setEmptyText(setting.emptyText);
|
||||
}
|
||||
picker->setOptions(searchPickerOptions(setting.options));
|
||||
picker->setSelectedValue(setting.selectedValue);
|
||||
picker->setSize(0.0f, setting.preferredHeight * scale);
|
||||
picker->setFillWidth(true);
|
||||
auto* pickerPtr = picker.get();
|
||||
picker->setOnActivated([&openPath = ctx.openSearchPickerPath, requestContentRebuild = ctx.requestContentRebuild,
|
||||
setOverride = ctx.setOverride, path = entry.path,
|
||||
selectedValue = setting.selectedValue](const SearchPickerOption& option) {
|
||||
if (option.value.empty()) {
|
||||
return;
|
||||
}
|
||||
openPath.clear();
|
||||
if (option.value == selectedValue) {
|
||||
requestContentRebuild();
|
||||
return;
|
||||
}
|
||||
setOverride(path, option.value);
|
||||
});
|
||||
picker->setOnCancel([&openPath = ctx.openSearchPickerPath, requestContentRebuild = ctx.requestContentRebuild]() {
|
||||
openPath.clear();
|
||||
requestContentRebuild();
|
||||
});
|
||||
block->addChild(std::move(picker));
|
||||
|
||||
section.addChild(std::move(block));
|
||||
if (ctx.openSearchPickerPath == pickerPath && ctx.focusArea) {
|
||||
ctx.focusArea(pickerPtr->filterInputArea());
|
||||
}
|
||||
};
|
||||
|
||||
const auto makeMultiSelectBlock = [&](Flex& section, const SettingEntry& entry, const MultiSelectSetting& setting) {
|
||||
const bool overridden = (ctx.configService != nullptr && ctx.configService->hasOverride(entry.path));
|
||||
|
||||
@@ -962,6 +1139,8 @@ namespace settings {
|
||||
return makeOptionalNumber(control, entry.path);
|
||||
} else if constexpr (std::is_same_v<T, ColorSetting>) {
|
||||
return makeColor(control, entry.path);
|
||||
} else if constexpr (std::is_same_v<T, SearchPickerSetting>) {
|
||||
return nullptr;
|
||||
} else if constexpr (std::is_same_v<T, MultiSelectSetting>) {
|
||||
return nullptr;
|
||||
} else if constexpr (std::is_same_v<T, ListSetting>) {
|
||||
@@ -1003,6 +1182,7 @@ namespace settings {
|
||||
.creatingWidgetType = ctx.creatingWidgetType,
|
||||
.requestRebuild = ctx.requestRebuild,
|
||||
.resetContentScroll = ctx.resetContentScroll,
|
||||
.focusArea = ctx.focusArea,
|
||||
.setOverride = ctx.setOverride,
|
||||
.setOverrides = ctx.setOverrides,
|
||||
.clearOverride = ctx.clearOverride,
|
||||
@@ -1067,6 +1247,12 @@ namespace settings {
|
||||
} else if (!isBarWidgetListPath(entry.path)) {
|
||||
makeListBlock(*activeSection, entry, *list);
|
||||
}
|
||||
} else if (const auto* picker = std::get_if<SearchPickerSetting>(&entry.control)) {
|
||||
if (ctx.openSearchPickerPath == pathKey(entry.path)) {
|
||||
makeSearchPickerBlock(*activeSection, entry, *picker);
|
||||
} else {
|
||||
makeRow(*activeSection, entry, makeSearchPickerButton(entry, *picker));
|
||||
}
|
||||
} else if (const auto* multi = std::get_if<MultiSelectSetting>(&entry.control)) {
|
||||
makeMultiSelectBlock(*activeSection, entry, *multi);
|
||||
} else {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <vector>
|
||||
|
||||
class Flex;
|
||||
class InputArea;
|
||||
|
||||
namespace settings {
|
||||
|
||||
@@ -26,6 +27,7 @@ namespace settings {
|
||||
bool showOverriddenOnly = false;
|
||||
|
||||
std::string& openWidgetPickerPath;
|
||||
std::string& openSearchPickerPath;
|
||||
std::string& editingWidgetName;
|
||||
std::string& pendingDeleteWidgetName;
|
||||
std::string& pendingDeleteWidgetSettingPath;
|
||||
@@ -33,7 +35,9 @@ namespace settings {
|
||||
std::string& creatingWidgetType;
|
||||
|
||||
std::function<void()> requestRebuild;
|
||||
std::function<void()> requestContentRebuild;
|
||||
std::function<void()> resetContentScroll;
|
||||
std::function<void(InputArea*)> focusArea;
|
||||
std::function<void(std::vector<std::string>, ConfigOverrideValue)> setOverride;
|
||||
std::function<void(std::vector<std::pair<std::vector<std::string>, ConfigOverrideValue>>)> setOverrides;
|
||||
std::function<void(std::vector<std::string>)> clearOverride;
|
||||
|
||||
@@ -274,7 +274,13 @@ namespace settings {
|
||||
} else if (cfg.theme.source == ThemeSource::Community) {
|
||||
SettingControl communityPaletteControl = TextSetting{cfg.theme.communityPalette, "Oxocarbon"};
|
||||
if (!env.communityPalettes.empty()) {
|
||||
communityPaletteControl = SelectSetting{env.communityPalettes, cfg.theme.communityPalette};
|
||||
communityPaletteControl = SearchPickerSetting{
|
||||
.options = env.communityPalettes,
|
||||
.selectedValue = cfg.theme.communityPalette,
|
||||
.placeholder = tr("settings.schema.appearance.community-palette.search-placeholder"),
|
||||
.emptyText = tr("ui.controls.search-picker.empty"),
|
||||
.preferredHeight = 240.0f,
|
||||
};
|
||||
}
|
||||
entries.push_back(makeEntry("appearance", "theme", tr("settings.schema.appearance.community-palette.label"),
|
||||
tr("settings.schema.appearance.community-palette.description"),
|
||||
|
||||
@@ -19,6 +19,8 @@ namespace settings {
|
||||
struct SelectOption {
|
||||
std::string value;
|
||||
std::string label;
|
||||
std::string description = {};
|
||||
std::string category = {};
|
||||
};
|
||||
|
||||
struct SelectSetting {
|
||||
@@ -28,6 +30,14 @@ namespace settings {
|
||||
bool segmented = false; // render as Segmented pill group instead of dropdown Select
|
||||
};
|
||||
|
||||
struct SearchPickerSetting {
|
||||
std::vector<SelectOption> options;
|
||||
std::string selectedValue;
|
||||
std::string placeholder;
|
||||
std::string emptyText;
|
||||
float preferredHeight = 240.0f;
|
||||
};
|
||||
|
||||
struct SliderSetting {
|
||||
float value = 0.0f;
|
||||
float minValue = 0.0f;
|
||||
@@ -80,7 +90,7 @@ namespace settings {
|
||||
|
||||
using SettingControl =
|
||||
std::variant<ToggleSetting, SelectSetting, SliderSetting, TextSetting, OptionalNumberSetting, ListSetting,
|
||||
ColorSetting, MultiSelectSetting, ButtonSetting, ColorRolePickerSetting>;
|
||||
ColorSetting, MultiSelectSetting, ButtonSetting, ColorRolePickerSetting, SearchPickerSetting>;
|
||||
|
||||
struct SettingEntry {
|
||||
std::string section;
|
||||
|
||||
@@ -311,6 +311,7 @@ void SettingsWindow::clearStatusMessage() {
|
||||
|
||||
void SettingsWindow::clearTransientSettingsState() {
|
||||
m_openWidgetPickerPath.clear();
|
||||
m_openSearchPickerPath.clear();
|
||||
m_editingWidgetName.clear();
|
||||
m_renamingWidgetName.clear();
|
||||
m_pendingDeleteWidgetName.clear();
|
||||
@@ -835,6 +836,7 @@ void SettingsWindow::rebuildSettingsContent() {
|
||||
}
|
||||
|
||||
const auto requestRebuild = [this]() { requestSceneRebuild(); };
|
||||
const auto requestContent = [this]() { requestContentRebuild(); };
|
||||
const auto setOverride = [this](std::vector<std::string> path, ConfigOverrideValue value) {
|
||||
setSettingOverride(std::move(path), std::move(value));
|
||||
};
|
||||
@@ -895,13 +897,16 @@ void SettingsWindow::rebuildSettingsContent() {
|
||||
.showAdvanced = m_showAdvanced,
|
||||
.showOverriddenOnly = m_showOverriddenOnly,
|
||||
.openWidgetPickerPath = m_openWidgetPickerPath,
|
||||
.openSearchPickerPath = m_openSearchPickerPath,
|
||||
.editingWidgetName = m_editingWidgetName,
|
||||
.pendingDeleteWidgetName = m_pendingDeleteWidgetName,
|
||||
.pendingDeleteWidgetSettingPath = m_pendingDeleteWidgetSettingPath,
|
||||
.renamingWidgetName = m_renamingWidgetName,
|
||||
.creatingWidgetType = m_creatingWidgetType,
|
||||
.requestRebuild = requestRebuild,
|
||||
.requestContentRebuild = requestContent,
|
||||
.resetContentScroll = [this]() { m_contentScrollState.offset = 0.0f; },
|
||||
.focusArea = [this](InputArea* area) { m_inputDispatcher.setFocus(area); },
|
||||
.setOverride = setOverride,
|
||||
.setOverrides = setOverrides,
|
||||
.clearOverride = clearOverride,
|
||||
@@ -1110,6 +1115,7 @@ void SettingsWindow::buildScene(std::uint32_t width, std::uint32_t height) {
|
||||
const bool searchActiveChanged = wasSearchActive != !m_searchQuery.empty();
|
||||
const bool hadPendingReset = !m_pendingResetPageScope.empty();
|
||||
m_pendingResetPageScope.clear();
|
||||
m_openSearchPickerPath.clear();
|
||||
if (hadPendingReset || searchActiveChanged) {
|
||||
m_focusSearchOnRebuild = true;
|
||||
requestSceneRebuild();
|
||||
@@ -1398,7 +1404,6 @@ void SettingsWindow::onKeyboardEvent(const KeyboardEvent& event) {
|
||||
m_renamingMonitorOverrideMatch.clear();
|
||||
m_pendingDeleteMonitorOverrideBarName.clear();
|
||||
m_pendingDeleteMonitorOverrideMatch.clear();
|
||||
m_contentScrollState.offset = 0.0f;
|
||||
requestRebuild();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -99,6 +99,7 @@ private:
|
||||
bool m_focusSearchOnRebuild = false;
|
||||
std::string m_searchQuery;
|
||||
std::string m_openWidgetPickerPath;
|
||||
std::string m_openSearchPickerPath;
|
||||
std::string m_editingWidgetName;
|
||||
std::string m_pendingDeleteWidgetName;
|
||||
std::string m_pendingDeleteWidgetSettingPath;
|
||||
|
||||
@@ -8,9 +8,10 @@
|
||||
#include "ui/controls/scroll_view.h"
|
||||
#include "ui/palette.h"
|
||||
#include "ui/style.h"
|
||||
#include "util/fuzzy_match.h"
|
||||
#include "util/string_utils.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <linux/input-event-codes.h>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
@@ -21,13 +22,6 @@ namespace {
|
||||
constexpr float kDefaultWidth = 320.0f;
|
||||
constexpr float kDefaultHeight = 360.0f;
|
||||
|
||||
std::string lower(std::string_view value) {
|
||||
std::string out(value);
|
||||
std::transform(out.begin(), out.end(), out.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string detailText(const SearchPickerOption& option) {
|
||||
if (!option.description.empty() && !option.category.empty()) {
|
||||
return option.category + " - " + option.description;
|
||||
@@ -116,6 +110,10 @@ void SearchPicker::setOnActivated(std::function<void(const SearchPickerOption&)>
|
||||
|
||||
void SearchPicker::setOnCancel(std::function<void()> callback) { m_onCancel = std::move(callback); }
|
||||
|
||||
InputArea* SearchPicker::filterInputArea() const noexcept {
|
||||
return m_input != nullptr ? m_input->inputArea() : nullptr;
|
||||
}
|
||||
|
||||
void SearchPicker::doLayout(Renderer& renderer) {
|
||||
Flex::doLayout(renderer);
|
||||
for (const auto& row : m_rows) {
|
||||
@@ -134,13 +132,40 @@ LayoutSize SearchPicker::doMeasure(Renderer& renderer, const LayoutConstraints&
|
||||
void SearchPicker::doArrange(Renderer& renderer, const LayoutRect& rect) { arrangeByLayout(renderer, rect); }
|
||||
|
||||
void SearchPicker::applyFilter() {
|
||||
struct ScoredOption {
|
||||
std::size_t index = 0;
|
||||
double score = 0.0;
|
||||
};
|
||||
|
||||
m_visible.clear();
|
||||
std::vector<ScoredOption> scored;
|
||||
const std::string query = StringUtils::trim(m_filter);
|
||||
|
||||
for (std::size_t i = 0; i < m_options.size(); ++i) {
|
||||
if (matchesFilter(m_options[i])) {
|
||||
if (query.empty()) {
|
||||
m_visible.push_back(i);
|
||||
continue;
|
||||
}
|
||||
|
||||
const double score = matchScore(m_options[i], query);
|
||||
if (FuzzyMatch::isMatch(score)) {
|
||||
scored.push_back(ScoredOption{.index = i, .score = score});
|
||||
}
|
||||
}
|
||||
|
||||
if (!query.empty()) {
|
||||
std::stable_sort(scored.begin(), scored.end(),
|
||||
[](const ScoredOption& lhs, const ScoredOption& rhs) { return lhs.score > rhs.score; });
|
||||
m_visible.reserve(scored.size());
|
||||
for (const auto& item : scored) {
|
||||
m_visible.push_back(item.index);
|
||||
}
|
||||
}
|
||||
|
||||
m_highlightedVisibleIndex = 0;
|
||||
if (m_scroll != nullptr) {
|
||||
m_scroll->setScrollOffset(0.0f);
|
||||
}
|
||||
rebuildRows();
|
||||
}
|
||||
|
||||
@@ -177,10 +202,11 @@ void SearchPicker::rebuildRows() {
|
||||
auto row = std::make_unique<Flex>();
|
||||
row->setDirection(FlexDirection::Vertical);
|
||||
row->setAlign(FlexAlign::Stretch);
|
||||
row->setGap(1.0f);
|
||||
const std::string detail = detailText(option);
|
||||
row->setGap(detail.empty() ? 0.0f : 1.0f);
|
||||
row->setPadding(Style::spaceXs, Style::spaceSm);
|
||||
row->setRadius(Style::radiusSm);
|
||||
row->setMinHeight(Style::controlHeightLg);
|
||||
row->setMinHeight(detail.empty() ? Style::controlHeight : Style::controlHeightLg);
|
||||
row->setFillWidth(true);
|
||||
|
||||
auto title = std::make_unique<Label>();
|
||||
@@ -189,11 +215,14 @@ void SearchPicker::rebuildRows() {
|
||||
title->setStableBaseline(true);
|
||||
auto* titlePtr = static_cast<Label*>(row->addChild(std::move(title)));
|
||||
|
||||
auto detail = std::make_unique<Label>();
|
||||
detail->setText(detailText(option));
|
||||
detail->setFontSize(Style::fontSizeCaption);
|
||||
detail->setStableBaseline(true);
|
||||
auto* detailPtr = static_cast<Label*>(row->addChild(std::move(detail)));
|
||||
Label* detailPtr = nullptr;
|
||||
if (!detail.empty()) {
|
||||
auto detailLabel = std::make_unique<Label>();
|
||||
detailLabel->setText(detail);
|
||||
detailLabel->setFontSize(Style::fontSizeCaption);
|
||||
detailLabel->setStableBaseline(true);
|
||||
detailPtr = static_cast<Label*>(row->addChild(std::move(detailLabel)));
|
||||
}
|
||||
|
||||
auto area = std::make_unique<InputArea>();
|
||||
area->setCursorShape(WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_POINTER);
|
||||
@@ -226,6 +255,7 @@ void SearchPicker::setHighlightedVisibleIndex(std::size_t index) {
|
||||
}
|
||||
m_highlightedVisibleIndex = index;
|
||||
applyRowStates();
|
||||
ensureHighlightedVisible();
|
||||
}
|
||||
|
||||
void SearchPicker::moveHighlight(int delta) {
|
||||
@@ -247,10 +277,31 @@ void SearchPicker::activateHighlighted() {
|
||||
}
|
||||
}
|
||||
|
||||
void SearchPicker::ensureHighlightedVisible() {
|
||||
if (m_scroll == nullptr || m_highlightedVisibleIndex >= m_rows.size()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& row = m_rows[m_highlightedVisibleIndex];
|
||||
if (row.row == nullptr || row.row->height() <= 0.0f || m_scroll->height() <= 0.0f) {
|
||||
return;
|
||||
}
|
||||
|
||||
const float top = row.row->y();
|
||||
const float bottom = top + row.row->height();
|
||||
const float offset = m_scroll->scrollOffset();
|
||||
const float viewportHeight = m_scroll->height();
|
||||
if (top < offset) {
|
||||
m_scroll->setScrollOffset(top);
|
||||
} else if (bottom > offset + viewportHeight) {
|
||||
m_scroll->setScrollOffset(bottom - viewportHeight);
|
||||
}
|
||||
}
|
||||
|
||||
void SearchPicker::applyRowStates() {
|
||||
for (std::size_t i = 0; i < m_rows.size(); ++i) {
|
||||
auto& row = m_rows[i];
|
||||
if (row.row == nullptr || row.title == nullptr || row.detail == nullptr || i >= m_visible.size()) {
|
||||
if (row.row == nullptr || row.title == nullptr || i >= m_visible.size()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -264,8 +315,10 @@ void SearchPicker::applyRowStates() {
|
||||
row.title->setColor(highlighted ? colorSpecFromRole(ColorRole::OnPrimary)
|
||||
: (enabled ? colorSpecFromRole(ColorRole::OnSurface)
|
||||
: colorSpecFromRole(ColorRole::OnSurface, 0.55f)));
|
||||
row.detail->setColor(highlighted ? colorSpecFromRole(ColorRole::OnPrimary, 0.78f)
|
||||
: colorSpecFromRole(ColorRole::OnSurfaceVariant, enabled ? 1.0f : 0.55f));
|
||||
if (row.detail != nullptr) {
|
||||
row.detail->setColor(highlighted ? colorSpecFromRole(ColorRole::OnPrimary, 0.78f)
|
||||
: colorSpecFromRole(ColorRole::OnSurfaceVariant, enabled ? 1.0f : 0.55f));
|
||||
}
|
||||
if (row.area != nullptr) {
|
||||
row.area->setEnabled(enabled);
|
||||
}
|
||||
@@ -273,13 +326,7 @@ void SearchPicker::applyRowStates() {
|
||||
markPaintDirty();
|
||||
}
|
||||
|
||||
bool SearchPicker::matchesFilter(const SearchPickerOption& option) const {
|
||||
const std::string query = lower(m_filter);
|
||||
if (query.empty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const std::string haystack =
|
||||
lower(option.label + " " + option.value + " " + option.description + " " + option.category);
|
||||
return haystack.find(query) != std::string::npos;
|
||||
double SearchPicker::matchScore(const SearchPickerOption& option, std::string_view query) const {
|
||||
const std::string haystack = option.label + " " + option.value + " " + option.description + " " + option.category;
|
||||
return FuzzyMatch::score(query, haystack);
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ public:
|
||||
void setOnCancel(std::function<void()> callback);
|
||||
|
||||
[[nodiscard]] const std::string& filter() const noexcept { return m_filter; }
|
||||
[[nodiscard]] InputArea* filterInputArea() const noexcept;
|
||||
|
||||
private:
|
||||
struct RowView {
|
||||
@@ -51,8 +52,9 @@ private:
|
||||
void setHighlightedVisibleIndex(std::size_t index);
|
||||
void moveHighlight(int delta);
|
||||
void activateHighlighted();
|
||||
void ensureHighlightedVisible();
|
||||
void applyRowStates();
|
||||
[[nodiscard]] bool matchesFilter(const SearchPickerOption& option) const;
|
||||
[[nodiscard]] double matchScore(const SearchPickerOption& option, std::string_view query) const;
|
||||
|
||||
Input* m_input = nullptr;
|
||||
ScrollView* m_scroll = nullptr;
|
||||
|
||||
Reference in New Issue
Block a user