Files
feresaria/shell/ferese-settings/src/calendar_controls.rs
T

284 lines
12 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use super::{App, Element, Length, Message, column, container, row, schema, text_input, visuals};
use super::{
Task,
store::{Snapshot, set},
};
use cosmic::widget::text_editor;
use ferese_config::tr;
#[derive(Default)]
pub(super) struct FunctionEditor {
pub expanded: bool,
initialized: bool,
content: text_editor::Content,
validating: bool,
error: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
use cosmic::Application;
#[test]
fn function_icon_preserves_unsaved_code_and_only_applies_matching_validation() {
let mut app = App::init(
cosmic::app::Core::default(),
(
"/unused/calendar.kdl".into(),
Snapshot::parse(String::new()),
None,
None,
),
)
.0;
app.saving = true;
let original = app.draft.source.clone();
let _ = app.calendar_function_input(FunctionInput::Toggle);
assert!(app.calendar_editor.expanded);
assert!(!app.calendar_editor.dirty(&app.draft));
let edited = "fn japanese_holidays(year) { #{} }";
app.calendar_editor.content = text_editor::Content::with_text(edited);
let _ = app.calendar_function_input(FunctionInput::Toggle);
assert!(!app.calendar_editor.expanded);
let _ = app.calendar_function_input(FunctionInput::Toggle);
assert_eq!(app.calendar_editor.content.text(), edited);
assert_eq!(app.draft.source, original);
let _ = app.calendar_function_input(FunctionInput::Validated("stale".into(), Ok(())));
assert!(app.pending.is_empty());
let _ = app.calendar_function_input(FunctionInput::Validated(
edited.into(),
Err("invalid syntax".into()),
));
assert!(app.pending.is_empty() && app.calendar_editor.error.is_some());
let _ = app.calendar_function_input(FunctionInput::Validated(edited.into(), Ok(())));
assert_eq!(app.pending.len(), 1);
assert_eq!(
source(&Snapshot::parse(app.draft.source.clone()).unwrap()),
edited
);
let _ = app.calendar_function_input(FunctionInput::Restore);
assert_eq!(
source(&app.draft),
edited,
"restoring the editor still requires Apply"
);
assert!(app.calendar_editor.dirty(&app.draft));
}
}
const FUNCTION_PATH: &str = "status.calendar.japanese_holiday_function";
fn source(snapshot: &Snapshot) -> String {
snapshot.string(
FUNCTION_PATH,
ferese_config::calendar::DEFAULT_JAPANESE_HOLIDAY_FUNCTION,
)
}
impl FunctionEditor {
pub fn dirty(&self, snapshot: &Snapshot) -> bool {
self.initialized && self.content.text() != source(snapshot)
}
pub fn refresh(&mut self, snapshot: &Snapshot) {
if self.initialized {
self.content = text_editor::Content::with_text(&source(snapshot));
}
self.validating = false;
self.error = None;
}
}
#[derive(Clone, Debug)]
pub(super) enum FunctionInput {
Toggle,
Edit(text_editor::Action),
Apply,
Restore,
Validated(String, Result<(), String>),
}
impl App {
pub(super) fn calendar_function_input(&mut self, input: FunctionInput) -> Task<Message> {
match input {
FunctionInput::Toggle => {
if !self.calendar_editor.initialized || !self.calendar_editor.dirty(&self.draft) {
self.calendar_editor.content =
text_editor::Content::with_text(&source(&self.draft));
self.calendar_editor.initialized = true;
}
self.calendar_editor.expanded = !self.calendar_editor.expanded;
}
FunctionInput::Edit(action) => {
self.calendar_editor.content.perform(action);
self.calendar_editor.error = None;
}
FunctionInput::Restore => {
self.calendar_editor.content = text_editor::Content::with_text(
ferese_config::calendar::DEFAULT_JAPANESE_HOLIDAY_FUNCTION,
);
self.calendar_editor.error = None;
}
FunctionInput::Apply if !self.saving && !self.calendar_editor.validating => {
let source = self.calendar_editor.content.text();
let edited = source.clone();
self.calendar_editor.validating = true;
return Task::perform(
async move {
tokio::task::spawn_blocking(move || {
ferese_config::calendar::validate_japanese_holiday_function(&source)
})
.await
.unwrap_or_else(|error| Err(error.to_string()))
},
move |result| {
cosmic::Action::App(Message::CalendarFunction(FunctionInput::Validated(
edited.clone(),
result,
)))
},
);
}
FunctionInput::Validated(source, result) => {
self.calendar_editor.validating = false;
if source != self.calendar_editor.content.text() {
return Task::none();
}
match result {
Ok(()) => return self.change(set(FUNCTION_PATH, source)),
Err(error) => {
self.calendar_editor.error = Some(ferese_config::locale::translate(&error))
}
}
}
_ => {}
}
Task::none()
}
fn calendar_function_view(&self) -> Element<'_, Message> {
let state = &self.calendar_editor;
let palette = self.native_palette;
let mut rows = column([]).spacing(10).push(
row([])
.align_y(super::Alignment::Center)
.push(
self.panel_heading(tr!("Japanese holidays"), 14.)
.width(Length::Fill),
)
.push(
container(self.settings_icon_button(
tr!("Edit holiday calculation function"),
"M8 6l-6 6 6 6 M16 6l6 6-6 6 M14 4l-4 16",
Some(Message::CalendarFunction(FunctionInput::Toggle)),
))
.id("calendar-function-toggle"),
),
);
if state.expanded {
rows=rows.push(self.note(tr!("Rhai function japanese_holidays(year) returns a YYYY-MM-DD to name map. Built-in rules cover 1955–2099; unpublished years are estimates.")))
.push(cosmic::widget::TextEditor::new(&state.content)
// The context-menu wrapper repeats its inner editor ID;
// bypass it so named-state recovery retains the editor tree.
.context_menu(false)
.id("calendar-function-editor").font(self.font).size(11).height(280).padding(12)
.style(move|_,status|text_editor::Style{
background:palette.card.into(),
border:cosmic::iced::Border{width:1.,radius:5.into(),color:if matches!(status,text_editor::Status::Focused{..}){palette.accent}else{palette.muted.scale_alpha(0.25)},..Default::default()},
placeholder:palette.muted,value:palette.text,selection:palette.accent.scale_alpha(0.3),
})
.on_action(|action|Message::CalendarFunction(FunctionInput::Edit(action))))
.push(row([]).spacing(10)
.push(self.settings_button(if state.validating {tr!("Checking function…")} else {tr!("Apply function")},"M5 12l4 4L19 6",(!state.validating&&!self.saving&&state.dirty(&self.draft)).then_some(Message::CalendarFunction(FunctionInput::Apply)),false))
.push(self.settings_button(tr!("Restore built-in function"),"M4 11a8 8 0 1 1 2 7 M4 4v7h7",Some(Message::CalendarFunction(FunctionInput::Restore)),false)));
if let Some(error) = &state.error {
rows = rows.push(self.note(&ferese_config::locale::format(
"Holiday function error: {error}",
&[("error", error.clone())],
)));
}
}
rows.into()
}
}
impl App {
pub(super) fn calendar_controls(&self) -> Element<'_, Message> {
let palette = self.native_palette;
let date = self
.inputs
.get("$holiday.date")
.cloned()
.unwrap_or_default();
let name = self
.inputs
.get("$holiday.name")
.cloned()
.unwrap_or_default();
let valid = ferese_config::calendar::Holiday {
date: date.trim().into(),
name: name.trim().into(),
}
.validate()
.is_ok();
let mut rows = column([]).spacing(8);
for index in 0..self.draft.records("status.calendar.custom_holidays") {
let prefix = format!("status.calendar.custom_holidays.{index}");
rows = rows.push(
container(
column([])
.spacing(4)
.push(self.field(schema::text(
&format!("{prefix}.date"),
"Date",
"YYYY-MM-DD for one date; MM-DD repeats every year.",
"",
)))
.push(self.field(schema::text(
&format!("{prefix}.name"),
"Holiday name",
"",
"",
)))
.push(self.settings_button(
tr!("Remove holiday"),
"M5 6h14 M9 6V3h6v3 M6 6l1 15h10l1-15",
(!self.saving).then_some(Message::Remove(
"status.calendar.custom_holidays".into(),
index,
)),
false,
)),
)
.padding(4)
.class(visuals::section(palette)),
);
}
let input = |placeholder, value, path: &'static str| {
text_input(placeholder, value)
.font(self.font)
.size(12)
.padding([7, 10])
.style(visuals::input_style(palette))
.on_input(move |value| Message::Draft(path.into(), value))
.on_submit(|_| Message::AddHoliday)
};
rows = rows.push(
row([])
.spacing(10)
.align_y(super::Alignment::Center)
.push(input(tr!("YYYY-MM-DD / MM-DD"), date, "$holiday.date").width(170))
.push(input(tr!("Holiday name"), name, "$holiday.name").width(Length::Fill))
.push(self.settings_button(
tr!("Add holiday"),
"M12 5v14 M5 12h14",
(!self.saving && valid).then_some(Message::AddHoliday),
false,
)),
);
column([]).spacing(10)
.push(self.calendar_function_view())
.push(self.panel_heading(tr!("Custom holidays"),14.))
.push(self.note(tr!("Japanese public holidays are included. Add personal dates below; MM-DD repeats annually.")))
.push(rows).into()
}
}