use crate::view::root::Cydonia;
use artifact::board::{self, key};
use bezel::{
gpui::{
self, AnyElement, App, Context, Entity, Focusable as _, KeyBinding, SharedString,
Subscription, Window, actions, div, prelude::*, px,
},
theme::{TextStyle, Theme, Typeset},
ui::{
input::{FieldEvent, TextField},
widgets::{ButtonStyle, Buttons, Scaffolding as _},
},
};
use std::collections::HashSet;
actions!(cydonia_create, [CommitBoard, DismissBoard]);
const CREATE_CONTEXT: &str = "CydoniaNewBoard";
const LABEL_WIDTH: f32 = 34.;
pub fn bindings() -> Vec<KeyBinding> {
vec![
KeyBinding::new("enter", CommitBoard, Some(CREATE_CONTEXT)),
KeyBinding::new("escape", DismissBoard, Some(CREATE_CONTEXT)),
]
}
pub(crate) struct Making {
pub project: usize,
pub name: Entity<TextField>,
pub key: Entity<TextField>,
pub derived: SharedString,
pub error: Option<SharedString>,
_watch: Subscription,
}
fn seed(content: SharedString, placeholder: &'static str, cx: &mut App) -> Entity<TextField> {
let field = cx.new(|cx| {
TextField::new(cx)
.with_key_context(CREATE_CONTEXT)
.with_placeholder(placeholder)
});
field.update(cx, |field, cx| field.set_content(content, cx));
field
}
impl Cydonia {
pub(crate) fn ask_new_board(
&mut self,
project: usize,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.commit(cx);
self.menu = None;
self.renaming = None;
self.info = None;
let derived = self.derived_key(project, "", cx);
let name = seed(SharedString::default(), "name this board…", cx);
let key = seed(derived.clone(), "KEY", cx);
let watch = cx.subscribe(&name, |this, field, event: &FieldEvent, cx| {
if *event == FieldEvent::Changed {
let typed = field.read(cx).content().clone();
this.follow_key(&typed, cx);
}
});
window.focus(&name.read(cx).focus_handle(cx), cx);
self.making = Some(Making {
project,
name,
key,
derived,
error: None,
_watch: watch,
});
cx.notify();
}
fn follow_key(&mut self, name: &str, cx: &mut Context<Self>) {
let Some((project, field)) = self.making.as_ref().and_then(|making| {
(making.key.read(cx).content() == &making.derived)
.then(|| (making.project, making.key.clone()))
}) else {
return;
};
let derived = self.derived_key(project, name, cx);
field.update(cx, |field, cx| field.set_content(derived.clone(), cx));
if let Some(making) = self.making.as_mut() {
making.derived = derived;
}
}
fn derived_key(&self, project: usize, name: &str, cx: &App) -> SharedString {
let taken: HashSet<String> = self
.workspace
.read(cx)
.projects
.get(project)
.map(|open| open.boards.iter().map(|board| board.key.clone()).collect())
.unwrap_or_default();
let name = match name.trim().is_empty() {
true => board::NAMED,
false => name.trim(),
};
key::derive(name, &taken).into()
}
pub(crate) fn make_board(&mut self, _: &CommitBoard, _: &mut Window, cx: &mut Context<Self>) {
let Some(making) = self.making.as_ref() else {
return;
};
let project = making.project;
let key = making.key.read(cx).content().trim().to_owned();
let name = match making.name.read(cx).content().trim() {
"" => board::NAMED.to_owned(),
typed => typed.to_owned(),
};
let made = self.workspace.update(cx, |workspace, cx| {
workspace.new_board(project, name, &key, cx)
});
match made {
Ok(ix) => {
self.making = None;
self.open_board(project, ix, cx);
}
Err(why) => {
if let Some(making) = self.making.as_mut() {
making.error = Some(why.into());
}
}
}
cx.notify();
}
pub(crate) fn dismiss_new_board(
&mut self,
_: &DismissBoard,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.making = None;
cx.notify();
}
pub(crate) fn new_board_dialog(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
let making = self.making.as_ref()?;
let theme = Theme::of(cx).clone();
Some(
div()
.id("new-board-scrim")
.absolute()
.inset_0()
.flex()
.items_center()
.justify_center()
.bg(theme.scrim())
.on_click(cx.listener(|this, _, window, cx| {
this.dismiss_new_board(&DismissBoard, window, cx)
}))
.child(
div()
.id("new-board-dialog")
.w(px(360.))
.flex()
.flex_col()
.gap(px(10.))
.p(px(20.))
.rounded(px(Theme::panel_radius()))
.border_1()
.border_color(theme.border)
.bg(theme.surface)
.on_click(|_, _, cx| cx.stop_propagation())
.child(theme.row_title("New board"))
.child(self.field_row("Name", making.name.clone(), cx))
.child(self.field_row("Key", making.key.clone(), cx))
.children(making.error.clone().map(|why| {
div()
.text_style(TextStyle::Caption)
.text_color(theme.danger)
.child(why)
}))
.child(
div()
.mt(px(4.))
.flex()
.flex_row()
.justify_end()
.gap(px(8.))
.child(
theme
.button("Cancel", ButtonStyle::Ghost, None)
.id("new-board-cancel")
.on_click(cx.listener(|this, _, window, cx| {
this.dismiss_new_board(&DismissBoard, window, cx)
})),
)
.child(
theme
.button("Create", ButtonStyle::Prominent, None)
.id("new-board-create")
.on_click(cx.listener(|this, _, window, cx| {
this.make_board(&CommitBoard, window, cx)
})),
),
),
)
.into_any_element(),
)
}
fn field_row(&self, label: &str, field: Entity<TextField>, cx: &Context<Self>) -> AnyElement {
let theme = Theme::of(cx).clone();
div()
.flex()
.flex_row()
.items_center()
.gap(px(8.))
.child(
div()
.flex_none()
.w(px(LABEL_WIDTH))
.text_style(TextStyle::Caption)
.text_color(theme.text_muted)
.child(label.to_owned()),
)
.child(div().flex_1().min_w_0().child(field))
.into_any_element()
}
}