use zng_app::{
static_id,
widget::{
node::{BoxedUiNode, UiNode, UiVec},
property,
},
};
use zng_ext_config::{
settings::{Category, CategoryId, Setting, SettingBuilder, SETTINGS},
ConfigKey,
};
use zng_ext_font::FontWeight;
use zng_ext_l10n::l10n;
use zng_var::{ContextInitHandle, ReadOnlyContextVar};
use zng_wgt::{node::with_context_var, prelude::*, Wgt, WidgetFn, EDITORS, ICONS};
use zng_wgt_container::Container;
use zng_wgt_filter::opacity;
use zng_wgt_markdown::Markdown;
use zng_wgt_rule_line::{hr::Hr, vr::Vr};
use zng_wgt_scroll::{Scroll, ScrollMode};
use zng_wgt_stack::{Stack, StackDirection};
use zng_wgt_style::Style;
use zng_wgt_text::Text;
use zng_wgt_text_input::TextInput;
use zng_wgt_toggle::{Selector, Toggle};
use zng_wgt_tooltip::{disabled_tooltip, tooltip, Tip};
use crate::SettingsEditor;
context_var! {
pub static CATEGORY_ITEM_FN_VAR: WidgetFn<CategoryItemArgs> = WidgetFn::new(default_category_item_fn);
pub static CATEGORIES_LIST_FN_VAR: WidgetFn<CategoriesListArgs> = WidgetFn::new(default_categories_list_fn);
pub static CATEGORY_HEADER_FN_VAR: WidgetFn<CategoryHeaderArgs> = WidgetFn::new(default_category_header_fn);
pub static SETTING_FN_VAR: WidgetFn<SettingArgs> = WidgetFn::new(default_setting_fn);
pub static SETTINGS_FN_VAR: WidgetFn<SettingsArgs> = WidgetFn::new(default_settings_fn);
pub static SETTINGS_SEARCH_FN_VAR: WidgetFn<SettingsSearchArgs> = WidgetFn::new(default_settings_search_fn);
pub static PANEL_FN_VAR: WidgetFn<PanelArgs> = WidgetFn::new(default_panel_fn);
}
#[property(CONTEXT, default(CATEGORY_ITEM_FN_VAR), widget_impl(SettingsEditor))]
pub fn category_item_fn(child: impl UiNode, wgt_fn: impl IntoVar<WidgetFn<CategoryItemArgs>>) -> impl UiNode {
with_context_var(child, CATEGORY_ITEM_FN_VAR, wgt_fn)
}
#[property(CONTEXT, default(CATEGORIES_LIST_FN_VAR), widget_impl(SettingsEditor))]
pub fn categories_list_fn(child: impl UiNode, wgt_fn: impl IntoVar<WidgetFn<CategoriesListArgs>>) -> impl UiNode {
with_context_var(child, CATEGORIES_LIST_FN_VAR, wgt_fn)
}
#[property(CONTEXT, default(CATEGORY_HEADER_FN_VAR), widget_impl(SettingsEditor))]
pub fn category_header_fn(child: impl UiNode, wgt_fn: impl IntoVar<WidgetFn<CategoryHeaderArgs>>) -> impl UiNode {
with_context_var(child, CATEGORY_HEADER_FN_VAR, wgt_fn)
}
#[property(CONTEXT, default(SETTING_FN_VAR), widget_impl(SettingsEditor))]
pub fn setting_fn(child: impl UiNode, wgt_fn: impl IntoVar<WidgetFn<SettingArgs>>) -> impl UiNode {
with_context_var(child, SETTING_FN_VAR, wgt_fn)
}
#[property(CONTEXT, default(SETTINGS_FN_VAR), widget_impl(SettingsEditor))]
pub fn settings_fn(child: impl UiNode, wgt_fn: impl IntoVar<WidgetFn<SettingsArgs>>) -> impl UiNode {
with_context_var(child, SETTINGS_FN_VAR, wgt_fn)
}
#[property(CONTEXT, default(SETTINGS_SEARCH_FN_VAR), widget_impl(SettingsEditor))]
pub fn settings_search_fn(child: impl UiNode, wgt_fn: impl IntoVar<WidgetFn<SettingsSearchArgs>>) -> impl UiNode {
with_context_var(child, SETTINGS_SEARCH_FN_VAR, wgt_fn)
}
#[property(CONTEXT, default(PANEL_FN_VAR), widget_impl(SettingsEditor))]
pub fn panel_fn(child: impl UiNode, wgt_fn: impl IntoVar<WidgetFn<PanelArgs>>) -> impl UiNode {
with_context_var(child, PANEL_FN_VAR, wgt_fn)
}
pub fn default_category_item_fn(args: CategoryItemArgs) -> impl UiNode {
Toggle! {
child = Text!(args.category.name().clone());
value::<CategoryId> = args.category.id().clone();
}
}
pub fn default_category_header_fn(args: CategoryHeaderArgs) -> impl UiNode {
Text! {
txt = args.category.name().clone();
font_size = 1.5.em();
zng_wgt::margin = (10, 10, 10, 28);
}
}
pub fn default_categories_list_fn(args: CategoriesListArgs) -> impl UiNode {
Container! {
child = categories_list(args.items.boxed());
child_end = Vr!(zng_wgt::margin = 0), 0;
}
}
fn categories_list(items: BoxedUiNodeList) -> impl UiNode {
let list = Stack! {
zng_wgt_toggle::selector = Selector::single(SETTINGS.editor_selected_category());
direction = StackDirection::top_to_bottom();
children = items;
zng_wgt_toggle::style_fn = Style! {
replace = true;
opacity = 70.pct();
zng_wgt_size_offset::height = 2.em();
zng_wgt_container::child_align = Align::START;
zng_wgt_input::cursor = zng_wgt_input::CursorIcon::Pointer;
when *#zng_wgt_input::is_cap_hovered {
zng_wgt_text::font_weight = FontWeight::MEDIUM;
}
when *#zng_wgt_toggle::is_checked {
zng_wgt_text::font_weight = FontWeight::BOLD;
opacity = 100.pct();
}
};
};
Scroll! {
mode = ScrollMode::VERTICAL;
child_align = Align::FILL_TOP;
padding = (10, 20);
child = list;
}
}
pub fn default_categories_list_mobile_fn(args: CategoriesListArgs) -> impl UiNode {
let items = ArcNodeList::new(args.items);
Toggle! {
zng_wgt::margin = 4;
style_fn = zng_wgt_toggle::ComboStyle!();
child = Text! {
txt = SETTINGS
.editor_state()
.flat_map(|e| e.as_ref().unwrap().selected_cat.name().clone());
font_weight = FontWeight::BOLD;
zng_wgt_container::padding = 5;
};
checked_popup = wgt_fn!(|_| zng_wgt_layer::popup::Popup! {
child = categories_list(items.take_on_init().boxed());
});
}
}
pub fn default_setting_fn(args: SettingArgs) -> impl UiNode {
let name = args.setting.name().clone();
let description = args.setting.description().clone();
let can_reset = args.setting.can_reset();
Container! {
setting = args.setting.clone();
zng_wgt_input::focus::focus_scope = true;
zng_wgt_input::focus::focus_scope_behavior = zng_ext_input::focus::FocusScopeOnFocus::FirstDescendant;
child_start = {
let s = args.setting;
Wgt! {
zng_wgt::align = Align::TOP;
zng_wgt::visibility = can_reset.map(|c| match c {
true => Visibility::Visible,
false => Visibility::Hidden,
});
zng_wgt_input::gesture::on_click = hn!(|_| {
s.reset();
});
zng_wgt_fill::background = ICONS.req_or(["settings-reset", "settings-backup-restore"], || Text!("R"));
zng_wgt_size_offset::size = 18;
tooltip = Tip!(Text!("reset"));
disabled_tooltip = Tip!(Text!("is default"));
zng_wgt_input::focus::tab_index = zng_ext_input::focus::TabIndex::SKIP;
opacity = 70.pct();
when *#zng_wgt_input::is_cap_hovered {
opacity = 100.pct();
}
when *#zng_wgt::is_disabled {
opacity = 30.pct();
}
}
}, 4;
child_top = Container! {
child_top = Text! {
txt = name;
font_weight = FontWeight::BOLD;
}, 4;
child = Markdown! {
txt = description;
opacity = 70.pct();
};
}, 5;
child = args.editor;
}
}
pub fn default_settings_fn(args: SettingsArgs) -> impl UiNode {
Container! {
child_top = args.header, 5;
child = Scroll! {
mode = ScrollMode::VERTICAL;
padding = (0, 20, 20, 10);
child_align = Align::FILL_TOP;
child = Stack! {
direction = StackDirection::top_to_bottom();
spacing = 10;
children = args.items;
};
};
}
}
pub fn default_settings_search_fn(_: SettingsSearchArgs) -> impl UiNode {
Container! {
child = TextInput! {
txt = SETTINGS.editor_search();
style_fn = zng_wgt_text_input::SearchStyle!();
zng_wgt_input::focus::focus_shortcut = [shortcut![CTRL+'F'], shortcut![Find]];
placeholder_txt = l10n!("search.placeholder", "search settings ({$shortcut})", shortcut = "Ctrl+F");
};
child_bottom = Hr!(zng_wgt::margin = (10, 10, 0, 10)), 0;
}
}
pub fn default_panel_fn(args: PanelArgs) -> impl UiNode {
Container! {
child_top = args.search, 0;
child = Container! {
child_start = args.categories, 0;
child = args.settings;
};
}
}
pub fn default_panel_mobile_fn(args: PanelArgs) -> impl UiNode {
Container! {
child_top = args.search, 0;
child = Container! {
child_top = args.categories, 0;
child = args.settings;
};
}
}
pub struct CategoryItemArgs {
pub index: usize,
pub category: Category,
}
pub struct CategoryHeaderArgs {
pub category: Category,
}
pub struct CategoriesListArgs {
pub items: UiVec,
}
pub struct SettingArgs {
pub index: usize,
pub setting: Setting,
pub editor: BoxedUiNode,
}
pub struct SettingsArgs {
pub header: BoxedUiNode,
pub items: UiVec,
}
pub struct SettingsSearchArgs {}
pub struct PanelArgs {
pub search: BoxedUiNode,
pub categories: BoxedUiNode,
pub settings: BoxedUiNode,
}
pub trait SettingBuilderEditorExt {
fn editor_fn(&mut self, editor: WidgetFn<Setting>) -> &mut Self;
}
pub trait SettingEditorExt {
fn editor_fn(&self) -> Option<WidgetFn<Setting>>;
fn editor(&self) -> BoxedUiNode;
}
pub trait WidgetInfoSettingExt {
fn setting_key(&self) -> Option<ConfigKey>;
}
static_id! {
static ref CUSTOM_EDITOR_ID: StateId<WidgetFn<Setting>>;
static ref SETTING_KEY_ID: StateId<ConfigKey>;
}
impl SettingBuilderEditorExt for SettingBuilder<'_> {
fn editor_fn(&mut self, editor: WidgetFn<Setting>) -> &mut Self {
self.set(*CUSTOM_EDITOR_ID, editor)
}
}
impl SettingEditorExt for Setting {
fn editor_fn(&self) -> Option<WidgetFn<Setting>> {
self.meta().get_clone(*CUSTOM_EDITOR_ID)
}
fn editor(&self) -> BoxedUiNode {
match self.editor_fn() {
Some(f) => f(self.clone()),
None => EDITOR_SETTING_VAR.with_context_var(ContextInitHandle::current(), Some(self.clone()), || {
EDITORS.get(self.value().clone_any())
}),
}
}
}
impl WidgetInfoSettingExt for WidgetInfo {
fn setting_key(&self) -> Option<ConfigKey> {
self.meta().get_clone(*SETTING_KEY_ID)
}
}
pub trait SettingsCtxExt {
fn editor_search(&self) -> ContextVar<Txt>;
fn editor_selected_category(&self) -> ContextVar<CategoryId>;
fn editor_state(&self) -> ReadOnlyContextVar<Option<SettingsEditorState>>;
fn editor_setting(&self) -> ReadOnlyContextVar<Option<Setting>>;
}
impl SettingsCtxExt for SETTINGS {
fn editor_search(&self) -> ContextVar<Txt> {
EDITOR_SEARCH_VAR
}
fn editor_selected_category(&self) -> ContextVar<CategoryId> {
EDITOR_SELECTED_CATEGORY_VAR
}
fn editor_state(&self) -> ReadOnlyContextVar<Option<SettingsEditorState>> {
EDITOR_STATE_VAR.read_only()
}
fn editor_setting(&self) -> ReadOnlyContextVar<Option<Setting>> {
EDITOR_SETTING_VAR.read_only()
}
}
context_var! {
pub(crate) static EDITOR_SEARCH_VAR: Txt = Txt::from_static("");
pub(crate) static EDITOR_SELECTED_CATEGORY_VAR: CategoryId = CategoryId(Txt::from_static(""));
pub(crate) static EDITOR_STATE_VAR: Option<SettingsEditorState> = None;
static EDITOR_SETTING_VAR: Option<Setting> = None;
}
#[property(CONTEXT)]
pub fn setting(child: impl UiNode, setting: impl IntoValue<Setting>) -> impl UiNode {
let setting = setting.into();
let child = match_node(child, |_, op| {
if let UiNodeOp::Info { info } = op {
info.set_meta(*SETTING_KEY_ID, EDITOR_SETTING_VAR.with(|s| s.as_ref().unwrap().key().clone()));
}
});
with_context_var(child, EDITOR_SETTING_VAR, Some(setting))
}
#[derive(PartialEq, Debug, Clone)]
pub struct SettingsEditorState {
pub clean_search: Txt,
pub categories: Vec<Category>,
pub selected_cat: Category,
pub selected_settings: Vec<Setting>,
pub top_match: ConfigKey,
}