sqlight: sqlite playground
This commit is contained in:
68
src/app/button_set.rs
Normal file
68
src/app/button_set.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
use istyles::istyles;
|
||||
use leptos::{prelude::*, tachys::html};
|
||||
use web_sys::MouseEvent;
|
||||
|
||||
istyles!(styles, "assets/module.postcss/button_set.module.css.map");
|
||||
|
||||
#[component]
|
||||
pub fn ButtonSet(
|
||||
#[prop(default = String::new())] class_name: String,
|
||||
children: Children,
|
||||
) -> impl IntoView {
|
||||
let class = format!("{} {}", styles::set, class_name);
|
||||
|
||||
view! { <div class=class>{children()}</div> }
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Button<C>(
|
||||
#[prop(default = false)] is_primary: bool,
|
||||
#[prop(default = false)] is_small: bool,
|
||||
#[prop(optional)] icon_left: Option<AnyView>,
|
||||
#[prop(optional)] icon_right: Option<AnyView>,
|
||||
#[prop(optional)] node_ref: NodeRef<html::element::Button>,
|
||||
on_click: C,
|
||||
children: Children,
|
||||
) -> impl IntoView
|
||||
where
|
||||
C: FnMut(MouseEvent) + Send + 'static,
|
||||
{
|
||||
let class = format!(
|
||||
"{} {}",
|
||||
if is_primary {
|
||||
styles::primary
|
||||
} else {
|
||||
styles::secondary
|
||||
},
|
||||
if is_small { styles::small } else { "" }
|
||||
);
|
||||
|
||||
let icon_left = move || {
|
||||
if let Some(icon) = icon_left {
|
||||
view! { <span class=styles::iconLeft>{icon}</span> }.into_any()
|
||||
} else {
|
||||
().into_any()
|
||||
}
|
||||
};
|
||||
|
||||
let icon_right = move || {
|
||||
if let Some(icon) = icon_right {
|
||||
view! { <span class=styles::iconRight>{icon}</span> }.into_any()
|
||||
} else {
|
||||
().into_any()
|
||||
}
|
||||
};
|
||||
|
||||
view! {
|
||||
<button type="button" class=class on:click=on_click node_ref=node_ref>
|
||||
{icon_left()}
|
||||
{children()}
|
||||
{icon_right()}
|
||||
</button>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Rule() -> impl IntoView {
|
||||
view! { <span class=styles::rule /> }
|
||||
}
|
||||
50
src/app/config_element.rs
Normal file
50
src/app/config_element.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
use istyles::istyles;
|
||||
use leptos::prelude::*;
|
||||
use web_sys::Event;
|
||||
|
||||
use crate::app::menu_item::MenuItem;
|
||||
|
||||
istyles!(
|
||||
styles,
|
||||
"assets/module.postcss/config_element.module.css.map"
|
||||
);
|
||||
|
||||
#[component]
|
||||
pub fn Select<E>(
|
||||
on_change: E,
|
||||
name: String,
|
||||
#[prop(default = true)] is_default: bool,
|
||||
children: Children,
|
||||
) -> impl IntoView
|
||||
where
|
||||
E: FnMut(Event) + Send + 'static,
|
||||
{
|
||||
view! {
|
||||
<ConfigElement name=name is_default=is_default>
|
||||
<select class=styles::select on:change=on_change>
|
||||
{children()}
|
||||
</select>
|
||||
</ConfigElement>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn ConfigElement(
|
||||
name: String,
|
||||
#[prop(default = true)] is_default: bool,
|
||||
children: Children,
|
||||
) -> impl IntoView {
|
||||
let style = if is_default {
|
||||
styles::name
|
||||
} else {
|
||||
styles::notDefault
|
||||
};
|
||||
view! {
|
||||
<MenuItem>
|
||||
<div class=styles::container>
|
||||
<span class=style>{name}</span>
|
||||
<div class=styles::value>{children()}</div>
|
||||
</div>
|
||||
</MenuItem>
|
||||
}
|
||||
}
|
||||
118
src/app/config_menu.rs
Normal file
118
src/app/config_menu.rs
Normal file
@@ -0,0 +1,118 @@
|
||||
use leptos::prelude::*;
|
||||
use reactive_stores::Store;
|
||||
use wasm_bindgen::JsValue;
|
||||
use web_sys::{Event, HtmlSelectElement};
|
||||
|
||||
use crate::{
|
||||
SQLightError,
|
||||
app::{
|
||||
GlobalState, GlobalStateStoreFields, Orientation, Theme,
|
||||
config_element::Select as SelectConfig, menu_group::MenuGroup,
|
||||
},
|
||||
};
|
||||
|
||||
const ACE_KEYBOARDS: [&str; 5] = ["ace", "emacs", "sublime", "vim", "vscode"];
|
||||
const ACE_THEMES: [&str; 3] = ["github", "github_dark", "gruvbox"];
|
||||
|
||||
fn selecet_view(s: &str, selected: &str) -> AnyView {
|
||||
if s == selected {
|
||||
view! {
|
||||
<option selected value=s>
|
||||
{s}
|
||||
</option>
|
||||
}
|
||||
.into_any()
|
||||
} else {
|
||||
view! { <option value=s>{s}</option> }.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn ConfigMenu() -> impl IntoView {
|
||||
let state = expect_context::<Store<GlobalState>>();
|
||||
|
||||
let ace_keyboard_change = move |event: Event| {
|
||||
if let Some(target) = event.target() {
|
||||
let select = HtmlSelectElement::from(JsValue::from(target));
|
||||
state.editor_config().write().keyboard = select.value();
|
||||
if let Some(Err(err)) = state.editor().read().as_ref().map(|editor| {
|
||||
editor.set_keyboard_handler(&format!("ace/keyboard/{}", select.value()))
|
||||
}) {
|
||||
state
|
||||
.last_error()
|
||||
.set(Some(SQLightError::new_ace_editor(err)));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let ace_theme_change = move |event: Event| {
|
||||
if let Some(target) = event.target() {
|
||||
let select = HtmlSelectElement::from(JsValue::from(target));
|
||||
state.editor_config().write().theme = select.value();
|
||||
if let Some(Err(err)) = state
|
||||
.editor()
|
||||
.read()
|
||||
.as_ref()
|
||||
.map(|editor| editor.set_theme(&format!("ace/theme/{}", select.value())))
|
||||
{
|
||||
state
|
||||
.last_error()
|
||||
.set(Some(SQLightError::new_ace_editor(err)));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let theme_change = move |event: Event| {
|
||||
if let Some(target) = event.target() {
|
||||
let select = HtmlSelectElement::from(JsValue::from(target));
|
||||
*state.theme().write() = Theme::from_value(&select.value());
|
||||
}
|
||||
};
|
||||
|
||||
let orientation_change = move |event: Event| {
|
||||
if let Some(target) = event.target() {
|
||||
let select = HtmlSelectElement::from(JsValue::from(target));
|
||||
*state.orientation().write() = Orientation::from_value(&select.value());
|
||||
}
|
||||
};
|
||||
|
||||
view! {
|
||||
<MenuGroup title="Editor".into()>
|
||||
<SelectConfig name="Keybinding".into() on_change=ace_keyboard_change>
|
||||
{move || {
|
||||
ACE_KEYBOARDS
|
||||
.into_iter()
|
||||
.map(|s| selecet_view(s, &state.editor_config().read().keyboard))
|
||||
.collect_view()
|
||||
}}
|
||||
</SelectConfig>
|
||||
<SelectConfig name="Theme".into() on_change=ace_theme_change>
|
||||
{move || {
|
||||
ACE_THEMES
|
||||
.into_iter()
|
||||
.map(|s| selecet_view(s, &state.editor_config().read().theme))
|
||||
.collect_view()
|
||||
}}
|
||||
</SelectConfig>
|
||||
</MenuGroup>
|
||||
|
||||
<MenuGroup title="UI".into()>
|
||||
<SelectConfig name="Theme".into() on_change=theme_change>
|
||||
{move || {
|
||||
["System", "Light", "Dark"]
|
||||
.into_iter()
|
||||
.map(|s| selecet_view(s, &state.theme().read().to_value()))
|
||||
.collect_view()
|
||||
}}
|
||||
</SelectConfig>
|
||||
<SelectConfig name="Orientation".into() on_change=orientation_change>
|
||||
{move || {
|
||||
["Automatic", "Horizontal", "Vertical"]
|
||||
.into_iter()
|
||||
.map(|s| selecet_view(s, &state.orientation().read().to_value()))
|
||||
.collect_view()
|
||||
}}
|
||||
</SelectConfig>
|
||||
</MenuGroup>
|
||||
}
|
||||
}
|
||||
36
src/app/context_menu.rs
Normal file
36
src/app/context_menu.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
use leptos::prelude::*;
|
||||
use reactive_stores::Store;
|
||||
|
||||
use crate::app::{
|
||||
GlobalState, GlobalStateStoreFields, menu_group::MenuGroup, select_one::SelectOne,
|
||||
};
|
||||
|
||||
#[component]
|
||||
pub fn ContextMenu() -> impl IntoView {
|
||||
let state = expect_context::<Store<GlobalState>>();
|
||||
|
||||
view! {
|
||||
<MenuGroup title="Choose whether to keep the context".into()>
|
||||
<SelectOne
|
||||
name="Discard Context".into()
|
||||
current_value=move || { *state.keep_ctx().read() }
|
||||
this_value=false
|
||||
change_value=move || {
|
||||
*state.keep_ctx().write() = false;
|
||||
}
|
||||
>
|
||||
"Each execution is in a new DB."
|
||||
</SelectOne>
|
||||
<SelectOne
|
||||
name="Keep Context".into()
|
||||
current_value=move || { *state.keep_ctx().read() }
|
||||
this_value=true
|
||||
change_value=move || {
|
||||
*state.keep_ctx().write() = true;
|
||||
}
|
||||
>
|
||||
"Keep the results of each execution."
|
||||
</SelectOne>
|
||||
</MenuGroup>
|
||||
}
|
||||
}
|
||||
60
src/app/editor.rs
Normal file
60
src/app/editor.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
use aceditor::EditorOptionsBuilder;
|
||||
use istyles::istyles;
|
||||
use leptos::prelude::*;
|
||||
use reactive_stores::Store;
|
||||
use wasm_bindgen_futures::spawn_local;
|
||||
use web_sys::UrlSearchParams;
|
||||
|
||||
use crate::{
|
||||
SQLightError,
|
||||
app::{GlobalState, GlobalStateStoreFields, header::execute},
|
||||
};
|
||||
|
||||
istyles!(styles, "assets/module.postcss/editor.module.css.map");
|
||||
|
||||
#[component]
|
||||
pub fn Editor() -> impl IntoView {
|
||||
let state = expect_context::<Store<GlobalState>>();
|
||||
|
||||
let editor_ref = NodeRef::new();
|
||||
|
||||
editor_ref.on_load(move |_| {
|
||||
let shared_code = || {
|
||||
let search = window().location().search().ok()?;
|
||||
let params = UrlSearchParams::new_with_str(&search).ok()?;
|
||||
params.get("code")
|
||||
};
|
||||
let opt = EditorOptionsBuilder::default()
|
||||
.mode("ace/mode/sql")
|
||||
.theme(&format!(
|
||||
"ace/theme/{}",
|
||||
state.editor_config().read_untracked().theme
|
||||
))
|
||||
.keyboard(&format!(
|
||||
"ace/keyboard/{}",
|
||||
state.editor_config().read_untracked().keyboard
|
||||
))
|
||||
.value(&shared_code().unwrap_or_else(|| state.code().get_untracked()))
|
||||
.build();
|
||||
|
||||
match aceditor::Editor::open("ace_editor", Some(&opt)) {
|
||||
Ok(editor) => state.editor().set(Some(editor)),
|
||||
Err(err) => state
|
||||
.last_error()
|
||||
.set(Some(SQLightError::new_ace_editor(err))),
|
||||
}
|
||||
|
||||
spawn_local(async move {
|
||||
if let Err(err) = aceditor::Editor::define_vim_w(execute(state)).await {
|
||||
state
|
||||
.last_error()
|
||||
.set(Some(SQLightError::new_ace_editor(err)));
|
||||
}
|
||||
});
|
||||
});
|
||||
view! {
|
||||
<div class=styles::container>
|
||||
<div node_ref=editor_ref id="ace_editor" class=styles::ace></div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
187
src/app/header.rs
Normal file
187
src/app/header.rs
Normal file
@@ -0,0 +1,187 @@
|
||||
use istyles::istyles;
|
||||
use leptos::{prelude::*, tachys::html};
|
||||
use reactive_stores::Store;
|
||||
use web_sys::{Url, UrlSearchParams};
|
||||
|
||||
use crate::{
|
||||
PrepareOptions, WorkerRequest,
|
||||
app::{
|
||||
button_set::{Button, ButtonSet, Rule},
|
||||
config_menu::ConfigMenu,
|
||||
context_menu::ContextMenu,
|
||||
icon::{build_icon, config_icon, expandable_icon},
|
||||
output::change_focus,
|
||||
pop_button::PopButton,
|
||||
state::{Focus, GlobalState, GlobalStateStoreFields},
|
||||
vfs_menu::VfsMenu,
|
||||
},
|
||||
};
|
||||
|
||||
istyles!(styles, "assets/module.postcss/header.module.css.map");
|
||||
|
||||
#[component]
|
||||
pub fn Header() -> impl IntoView {
|
||||
let menu_container = NodeRef::new();
|
||||
|
||||
view! {
|
||||
<>
|
||||
<div id="header" class=styles::container>
|
||||
<div class=styles::left>
|
||||
<ButtonSet>
|
||||
<ExecuteButton />
|
||||
</ButtonSet>
|
||||
|
||||
<ButtonSet>
|
||||
<VfsMenuButton menu_container=menu_container />
|
||||
<Rule />
|
||||
<ContextMenuButton menu_container=menu_container />
|
||||
</ButtonSet>
|
||||
</div>
|
||||
<div class=styles::right>
|
||||
<ButtonSet>
|
||||
<ShareButton />
|
||||
</ButtonSet>
|
||||
<ButtonSet>
|
||||
<ConfigMenuButton menu_container=menu_container />
|
||||
</ButtonSet>
|
||||
</div>
|
||||
</div>
|
||||
<div node_ref=menu_container></div>
|
||||
</>
|
||||
}
|
||||
}
|
||||
|
||||
pub fn execute(state: Store<GlobalState>) -> Box<dyn Fn() + Send + 'static> {
|
||||
Box::new(move || {
|
||||
let Some(code) = state
|
||||
.editor()
|
||||
.read_untracked()
|
||||
.as_ref()
|
||||
.map(|editor| editor.get_value())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
state.code().set(code.clone());
|
||||
change_focus(state, Some(Focus::Execute));
|
||||
std::mem::take(&mut *state.output().write());
|
||||
if let Some(worker) = &*state.worker().read_untracked() {
|
||||
worker.send_task(WorkerRequest::Prepare(PrepareOptions {
|
||||
id: String::new(),
|
||||
sql: code,
|
||||
clear_on_prepare: !*state.keep_ctx().read_untracked(),
|
||||
}));
|
||||
worker.send_task(WorkerRequest::Continue(String::new()));
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn ExecuteButton() -> impl IntoView {
|
||||
let state = expect_context::<Store<GlobalState>>();
|
||||
|
||||
let on_click = execute(state);
|
||||
|
||||
view! {
|
||||
<Button is_primary=true icon_right=build_icon() on_click=move |_| on_click()>
|
||||
"Run"
|
||||
</Button>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn VfsMenuButton(menu_container: NodeRef<html::element::Div>) -> impl IntoView {
|
||||
let state = expect_context::<Store<GlobalState>>();
|
||||
|
||||
let button = move |toggle, node_ref| {
|
||||
view! {
|
||||
<Button icon_right=expandable_icon() on_click=toggle node_ref=node_ref>
|
||||
{move || state.vfs().read().value()}
|
||||
</Button>
|
||||
}
|
||||
.into_any()
|
||||
};
|
||||
|
||||
view! {
|
||||
<PopButton
|
||||
button=button
|
||||
menu=Box::new(|_close| { view! { <VfsMenu /> }.into_any() })
|
||||
menu_container=menu_container
|
||||
></PopButton>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn ContextMenuButton(menu_container: NodeRef<html::element::Div>) -> impl IntoView {
|
||||
let state = expect_context::<Store<GlobalState>>();
|
||||
|
||||
let button = move |toggle, node_ref| {
|
||||
view! {
|
||||
<Button icon_right=expandable_icon() on_click=toggle node_ref=node_ref>
|
||||
{move || if *state.keep_ctx().read() { "Keep Context" } else { "Discard Context" }}
|
||||
</Button>
|
||||
}
|
||||
.into_any()
|
||||
};
|
||||
|
||||
view! {
|
||||
<PopButton
|
||||
button=button
|
||||
menu=Box::new(|_close| { view! { <ContextMenu /> }.into_any() })
|
||||
menu_container=menu_container
|
||||
></PopButton>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn ConfigMenuButton(menu_container: NodeRef<html::element::Div>) -> impl IntoView {
|
||||
let button = |toggle, node_ref| {
|
||||
view! {
|
||||
<Button
|
||||
icon_left=config_icon()
|
||||
icon_right=expandable_icon()
|
||||
on_click=toggle
|
||||
node_ref=node_ref
|
||||
>
|
||||
"Config"
|
||||
</Button>
|
||||
}
|
||||
.into_any()
|
||||
};
|
||||
|
||||
view! {
|
||||
<PopButton
|
||||
button=button
|
||||
menu=Box::new(|_close| { view! { <ConfigMenu /> }.into_any() })
|
||||
menu_container=menu_container
|
||||
></PopButton>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn ShareButton() -> impl IntoView {
|
||||
let state = expect_context::<Store<GlobalState>>();
|
||||
|
||||
let click = move |_| {
|
||||
let Some(code) = state
|
||||
.editor()
|
||||
.read()
|
||||
.as_ref()
|
||||
.map(|editor| editor.get_value())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Ok(href) = window().location().href().and_then(|href| {
|
||||
let url = Url::new(&href)?;
|
||||
let params = UrlSearchParams::new()?;
|
||||
params.set("code", &code);
|
||||
url.set_search(¶ms.to_string().as_string().unwrap());
|
||||
Ok(url.href())
|
||||
}) {
|
||||
*state.share_href().write() = Some(href);
|
||||
change_focus(state, Some(Focus::Share));
|
||||
}
|
||||
};
|
||||
|
||||
view! { <Button on_click=click>"Share"</Button> }
|
||||
}
|
||||
81
src/app/icon.rs
Normal file
81
src/app/icon.rs
Normal file
@@ -0,0 +1,81 @@
|
||||
use istyles::istyles;
|
||||
use leptos::prelude::*;
|
||||
|
||||
istyles!(styles, "assets/module.postcss/icon.module.css.map");
|
||||
|
||||
pub fn build_icon() -> AnyView {
|
||||
view! {
|
||||
<svg
|
||||
class=styles::icon
|
||||
height="14"
|
||||
viewBox="8 4 10 16"
|
||||
width="12"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M8 5v14l11-7z" />
|
||||
</svg>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
|
||||
pub fn config_icon() -> AnyView {
|
||||
view! {
|
||||
<svg
|
||||
class=styles::icon
|
||||
height="15"
|
||||
viewBox="0 0 24 24"
|
||||
width="15"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M19.43 12.98c.04-.32.07-.64.07-.98s-.03-.66-.07-.98l2.11-1.65c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.3-.61-.22l-2.49 1c-.52-.4-1.08-.73-1.69-.98l-.38-2.65C14.46 2.18 14.25 2 14 2h-4c-.25 0-.46.18-.49.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1c-.23-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64l2.11 1.65c-.04.32-.07.65-.07.98s.03.66.07.98l-2.11 1.65c-.19.15-.24.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1c.52.4 1.08.73 1.69.98l.38 2.65c.03.24.24.42.49.42h4c.25 0 .46-.18.49-.42l.38-2.65c.61-.25 1.17-.59 1.69-.98l2.49 1c.23.09.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.65zM12 15.5c-1.93 0-3.5-1.57-3.5-3.5s1.57-3.5 3.5-3.5 3.5 1.57 3.5 3.5-1.57 3.5-3.5 3.5z" />
|
||||
</svg>
|
||||
}.into_any()
|
||||
}
|
||||
|
||||
pub fn expandable_icon() -> AnyView {
|
||||
view! {
|
||||
<svg
|
||||
class=styles::icon
|
||||
height="10"
|
||||
viewBox="6 8 12 8"
|
||||
width="10"
|
||||
opacity="0.5"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z" />
|
||||
</svg>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
|
||||
pub fn checkmark_icon() -> AnyView {
|
||||
view! {
|
||||
<svg
|
||||
class=styles::icon
|
||||
height="18"
|
||||
viewBox="2 2 22 22"
|
||||
width="18"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" />
|
||||
</svg>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
|
||||
pub fn clipboard_icon() -> AnyView {
|
||||
view! {
|
||||
<svg
|
||||
class=styles::icon
|
||||
height="18"
|
||||
width="18"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect x="7" y="15" width="7" height="2" />
|
||||
<rect x="7" y="11" width="10" height="2" />
|
||||
<rect x="7" y="7" width="10" height="2" />
|
||||
<path d="M19,3L19,3h-4.18C14.4,1.84,13.3,1,12,1c-1.3,0-2.4,0.84-2.82,2H5h0C4.86,3,4.73,3.01,4.6,3.04 C4.21,3.12,3.86,3.32,3.59,3.59c-0.18,0.18-0.33,0.4-0.43,0.64C3.06,4.46,3,4.72,3,5v14c0,0.27,0.06,0.54,0.16,0.78 c0.1,0.24,0.25,0.45,0.43,0.64c0.27,0.27,0.62,0.47,1.01,0.55C4.73,20.99,4.86,21,5,21h0h14h0c1.1,0,2-0.9,2-2V5 C21,3.9,20.1,3,19,3z M12,2.75c0.41,0,0.75,0.34,0.75,0.75c0,0.41-0.34,0.75-0.75,0.75c-0.41,0-0.75-0.34-0.75-0.75 C11.25,3.09,11.59,2.75,12,2.75z M19,19H5V5h14V19z" />
|
||||
</svg>
|
||||
}.into_any()
|
||||
}
|
||||
15
src/app/loader.rs
Normal file
15
src/app/loader.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
use istyles::istyles;
|
||||
use leptos::prelude::*;
|
||||
|
||||
istyles!(styles, "assets/module.postcss/loader.module.css.map");
|
||||
|
||||
#[component]
|
||||
pub fn Loader() -> impl IntoView {
|
||||
view! {
|
||||
<div>
|
||||
<span class=styles::dot>"⬤"</span>
|
||||
<span class=styles::dot>"⬤"</span>
|
||||
<span class=styles::dot>"⬤"</span>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
14
src/app/menu_group.rs
Normal file
14
src/app/menu_group.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
use istyles::istyles;
|
||||
use leptos::prelude::*;
|
||||
|
||||
istyles!(styles, "assets/module.postcss/menu_group.module.css.map");
|
||||
|
||||
#[component]
|
||||
pub fn MenuGroup(title: String, children: Children) -> impl IntoView {
|
||||
view! {
|
||||
<div class=styles::container>
|
||||
<h1 class=styles::title>{title}</h1>
|
||||
<div class=styles::content>{children()}</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
9
src/app/menu_item.rs
Normal file
9
src/app/menu_item.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
use istyles::istyles;
|
||||
use leptos::prelude::*;
|
||||
|
||||
istyles!(styles, "assets/module.postcss/menu_item.module.css.map");
|
||||
|
||||
#[component]
|
||||
pub fn MenuItem(children: Children) -> impl IntoView {
|
||||
view! { <div class=styles::container>{children()}</div> }
|
||||
}
|
||||
20
src/app/mod.rs
Normal file
20
src/app/mod.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
mod button_set;
|
||||
mod config_element;
|
||||
mod config_menu;
|
||||
mod context_menu;
|
||||
mod editor;
|
||||
mod header;
|
||||
mod icon;
|
||||
mod loader;
|
||||
mod menu_group;
|
||||
mod menu_item;
|
||||
mod output;
|
||||
mod playground;
|
||||
mod pop_button;
|
||||
mod select_one;
|
||||
mod selectable_menu_item;
|
||||
mod state;
|
||||
mod vfs_menu;
|
||||
|
||||
pub use playground::playground;
|
||||
pub use state::*;
|
||||
141
src/app/output.rs
Normal file
141
src/app/output.rs
Normal file
@@ -0,0 +1,141 @@
|
||||
mod execute;
|
||||
mod header;
|
||||
mod loader;
|
||||
mod section;
|
||||
mod share;
|
||||
mod simple_pane;
|
||||
mod status;
|
||||
|
||||
use execute::Execute;
|
||||
use istyles::istyles;
|
||||
use leptos::prelude::*;
|
||||
use reactive_stores::Store;
|
||||
use share::Share;
|
||||
use status::Status;
|
||||
|
||||
use crate::app::state::{Focus, GlobalState, GlobalStateStoreFields};
|
||||
|
||||
istyles!(styles, "assets/module.postcss/output.module.css.map");
|
||||
|
||||
fn close() -> AnyView {
|
||||
let state = expect_context::<Store<GlobalState>>();
|
||||
|
||||
if state.read().is_focus() {
|
||||
view! {
|
||||
<button class=styles::tabClose on:click=move |_| change_focus(state, None)>
|
||||
"Close"
|
||||
</button>
|
||||
}
|
||||
.into_any()
|
||||
} else {
|
||||
().into_any()
|
||||
}
|
||||
}
|
||||
|
||||
fn body() -> AnyView {
|
||||
let state = expect_context::<Store<GlobalState>>();
|
||||
|
||||
if state.read().is_focus() {
|
||||
view! {
|
||||
<>
|
||||
<div class=styles::body>
|
||||
<Show
|
||||
when=move || matches!(*state.focus().read(), Some(Focus::Execute))
|
||||
fallback=|| ()
|
||||
>
|
||||
<Execute />
|
||||
</Show>
|
||||
|
||||
<Show
|
||||
when=move || matches!(*state.focus().read(), Some(Focus::Share))
|
||||
fallback=|| ()
|
||||
>
|
||||
<Share />
|
||||
</Show>
|
||||
|
||||
<Show
|
||||
when=move || matches!(*state.focus().read(), Some(Focus::Status))
|
||||
fallback=|| ()
|
||||
>
|
||||
<Status />
|
||||
</Show>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
.into_any()
|
||||
} else {
|
||||
().into_any()
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Tab(kind: Focus, label: String) -> impl IntoView {
|
||||
let state = expect_context::<Store<GlobalState>>();
|
||||
|
||||
let class = move || {
|
||||
if matches!(*state.focus().read(), Some(focus) if focus == kind) {
|
||||
styles::tabSelected
|
||||
} else {
|
||||
styles::tab
|
||||
}
|
||||
};
|
||||
|
||||
view! {
|
||||
<Show when=move || state.opened_focus().read().contains(&kind) fallback=|| ()>
|
||||
<button class=class>{label.clone()}</button>
|
||||
</Show>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Output() -> impl IntoView {
|
||||
let state = expect_context::<Store<GlobalState>>();
|
||||
|
||||
view! {
|
||||
<Show when=move || *state.show_something().read() fallback=|| ()>
|
||||
<div class=styles::container>
|
||||
<div class=styles::tabs>
|
||||
<Tab
|
||||
kind=Focus::Execute
|
||||
label="Execution".into()
|
||||
on:click=move |_| change_focus(state, Some(Focus::Execute))
|
||||
/>
|
||||
|
||||
<Tab
|
||||
kind=Focus::Share
|
||||
label="Share".into()
|
||||
on:click=move |_| change_focus(state, Some(Focus::Share))
|
||||
/>
|
||||
|
||||
<Tab
|
||||
kind=Focus::Status
|
||||
label="Status".into()
|
||||
on:click=move |_| change_focus(state, Some(Focus::Status))
|
||||
/>
|
||||
{close}
|
||||
</div>
|
||||
{body}
|
||||
</div>
|
||||
</Show>
|
||||
}
|
||||
}
|
||||
|
||||
pub fn change_focus(state: Store<GlobalState>, focus: Option<Focus>) {
|
||||
if let Some(focus) = focus {
|
||||
state.opened_focus().write().insert(focus);
|
||||
state
|
||||
.is_focused()
|
||||
.maybe_update(|before| !std::mem::replace(before, true));
|
||||
} else {
|
||||
state
|
||||
.is_focused()
|
||||
.maybe_update(|before| std::mem::replace(before, false));
|
||||
}
|
||||
|
||||
state
|
||||
.focus()
|
||||
.maybe_update(|before| std::mem::replace(before, focus) != focus);
|
||||
state
|
||||
.show_something()
|
||||
.maybe_update(|before| !std::mem::replace(before, true));
|
||||
}
|
||||
110
src/app/output/execute.rs
Normal file
110
src/app/output/execute.rs
Normal file
@@ -0,0 +1,110 @@
|
||||
use istyles::istyles;
|
||||
use leptos::prelude::*;
|
||||
use reactive_stores::{Store, StoreFieldIterator};
|
||||
|
||||
use crate::app::{
|
||||
output::{header::Header, loader::Loader, section::Section, simple_pane::SimplePane},
|
||||
state::{GlobalState, GlobalStateStoreFields},
|
||||
};
|
||||
use crate::{SQLiteStatementResult, SQLiteStatementTable};
|
||||
|
||||
istyles!(
|
||||
styles,
|
||||
"assets/module.postcss/output/execute.module.css.map"
|
||||
);
|
||||
|
||||
fn get_output(table: &SQLiteStatementTable) -> Option<AnyView> {
|
||||
let Some(values) = &table.values else {
|
||||
return None;
|
||||
};
|
||||
Some(
|
||||
view! {
|
||||
<table class=styles::table>
|
||||
<tr>
|
||||
{values
|
||||
.columns
|
||||
.iter()
|
||||
.map(|s| {
|
||||
view! { <th class=styles::tdAndTh>{s.to_string()}</th> }
|
||||
})
|
||||
.collect_view()}
|
||||
</tr>
|
||||
{values
|
||||
.rows
|
||||
.iter()
|
||||
.map(|row| {
|
||||
view! {
|
||||
<tr>
|
||||
|
||||
{row
|
||||
.iter()
|
||||
.map(|s| {
|
||||
view! { <td class=styles::tdAndTh>{s.to_string()}</td> }
|
||||
})
|
||||
.collect_view()}
|
||||
</tr>
|
||||
}
|
||||
})
|
||||
.collect_view()}
|
||||
</table>
|
||||
}
|
||||
.into_any(),
|
||||
)
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Output() -> AnyView {
|
||||
let state = expect_context::<Store<GlobalState>>();
|
||||
|
||||
view! {
|
||||
<>
|
||||
<Show
|
||||
when=move || {
|
||||
state
|
||||
.output()
|
||||
.read()
|
||||
.last()
|
||||
.is_none_or(|r| !matches!(r, SQLiteStatementResult::Finish))
|
||||
}
|
||||
fallback=|| ()
|
||||
>
|
||||
<Loader />
|
||||
</Show>
|
||||
|
||||
<For
|
||||
each=move || state.output().iter_unkeyed().enumerate()
|
||||
key=|(idx, _)| *idx
|
||||
children=move |(idx, item)| {
|
||||
match &*item.read() {
|
||||
SQLiteStatementResult::Finish => {
|
||||
view! { <Header label="Finished".into() /> }.into_any()
|
||||
}
|
||||
SQLiteStatementResult::Step(table) => {
|
||||
let label = format!("Statement #{}", idx + 1);
|
||||
if let Some(output) = get_output(table) {
|
||||
view! {
|
||||
<Section label=label>
|
||||
<p>{output}</p>
|
||||
</Section>
|
||||
}
|
||||
.into_any()
|
||||
} else {
|
||||
().into_any()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Execute() -> impl IntoView {
|
||||
view! {
|
||||
<SimplePane>
|
||||
<Output />
|
||||
</SimplePane>
|
||||
}
|
||||
}
|
||||
9
src/app/output/header.rs
Normal file
9
src/app/output/header.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
use istyles::istyles;
|
||||
use leptos::prelude::*;
|
||||
|
||||
istyles!(styles, "assets/module.postcss/output/header.module.css.map");
|
||||
|
||||
#[component]
|
||||
pub fn Header(label: String) -> impl IntoView {
|
||||
view! { <span class=styles::container>{label}</span> }
|
||||
}
|
||||
13
src/app/output/loader.rs
Normal file
13
src/app/output/loader.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
use leptos::prelude::*;
|
||||
|
||||
use crate::app::{loader::Loader as GenericLoader, output::header::Header};
|
||||
|
||||
#[component]
|
||||
pub fn Loader() -> impl IntoView {
|
||||
view! {
|
||||
<div>
|
||||
<Header label="Progress".into() />
|
||||
<GenericLoader />
|
||||
</div>
|
||||
}
|
||||
}
|
||||
21
src/app/output/section.rs
Normal file
21
src/app/output/section.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use istyles::istyles;
|
||||
use leptos::prelude::*;
|
||||
|
||||
use crate::app::output::header::Header;
|
||||
|
||||
istyles!(
|
||||
styles,
|
||||
"assets/module.postcss/output/section.module.css.map"
|
||||
);
|
||||
|
||||
#[component]
|
||||
pub fn Section(label: String, children: Children) -> impl IntoView {
|
||||
view! {
|
||||
<div>
|
||||
<Header label=label />
|
||||
<pre>
|
||||
<code class=styles::code>{children()}</code>
|
||||
</pre>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
55
src/app/output/share.rs
Normal file
55
src/app/output/share.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use istyles::istyles;
|
||||
use leptos::prelude::*;
|
||||
use reactive_stores::Store;
|
||||
use wasm_bindgen_futures::{JsFuture, spawn_local};
|
||||
|
||||
use crate::app::{GlobalState, GlobalStateStoreFields, icon::clipboard_icon};
|
||||
|
||||
istyles!(styles, "assets/module.postcss/output/share.module.css.map");
|
||||
|
||||
#[component]
|
||||
fn Copied<H>(href: H, children: Children) -> impl IntoView
|
||||
where
|
||||
H: Fn() -> String + Send + Sync + 'static,
|
||||
{
|
||||
let (copied, set_copied) = signal(false);
|
||||
let href = Arc::new(href);
|
||||
let href1 = Arc::clone(&href);
|
||||
|
||||
let copy = move |_| {
|
||||
let href = Arc::clone(&href1);
|
||||
spawn_local(async move {
|
||||
set_copied.set(true);
|
||||
if let Err(err) =
|
||||
JsFuture::from(window().navigator().clipboard().write_text(&href())).await
|
||||
{
|
||||
log::error!("Failed to write href to clipboard: {err:?}");
|
||||
}
|
||||
set_timeout(move || set_copied.set(false), Duration::from_millis(1000));
|
||||
});
|
||||
};
|
||||
|
||||
view! {
|
||||
<p class=move || { if *copied.read() { styles::active } else { styles::container } }>
|
||||
<a href=move || href()>{children()}</a>
|
||||
<button class=styles::button on:click=copy>
|
||||
{clipboard_icon()}
|
||||
</button>
|
||||
<span class=styles::text>"Copied!"</span>
|
||||
</p>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Links() -> impl IntoView {
|
||||
let state = expect_context::<Store<GlobalState>>();
|
||||
let code_url = move || state.share_href().get_untracked().unwrap_or_default();
|
||||
view! { <Copied href=code_url>Embedded code in link</Copied> }
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Share() -> impl IntoView {
|
||||
view! { <Links /> }
|
||||
}
|
||||
6
src/app/output/simple_pane.rs
Normal file
6
src/app/output/simple_pane.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
use leptos::prelude::*;
|
||||
|
||||
#[component]
|
||||
pub fn SimplePane(children: Children) -> impl IntoView {
|
||||
view! { <div>{children()}</div> }
|
||||
}
|
||||
80
src/app/output/status.rs
Normal file
80
src/app/output/status.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
use std::ops::Deref;
|
||||
|
||||
use leptos::prelude::*;
|
||||
use reactive_stores::Store;
|
||||
|
||||
use crate::{
|
||||
SQLightError, SQLitendError, WorkerError,
|
||||
app::{
|
||||
GlobalState, GlobalStateStoreFields,
|
||||
output::{section::Section, simple_pane::SimplePane},
|
||||
},
|
||||
};
|
||||
|
||||
const OPFS_SAH_POOL_OPENED_DETAILS: &str = "Due to OPFS SyncAccessHandle restrictions, \
|
||||
the db can only have one web tab access.
|
||||
|
||||
Please close other tabs and refresh, or switch to Memory VFS.";
|
||||
|
||||
#[component]
|
||||
pub fn Status() -> impl IntoView {
|
||||
let state = expect_context::<Store<GlobalState>>();
|
||||
|
||||
let show = move || match &*state.last_error().read() {
|
||||
Some(error) => {
|
||||
let summary = format!("{}", error.deref());
|
||||
let details = match error.deref() {
|
||||
SQLightError::Worker(worker) => match worker {
|
||||
WorkerError::SQLite(sqlitend_error) => match sqlitend_error {
|
||||
SQLitendError::ToCStr
|
||||
| SQLitendError::GetColumnName(_)
|
||||
| SQLitendError::Utf8Text => {
|
||||
"This shouldn't happen, please create an issue on github."
|
||||
}
|
||||
SQLitendError::OpenDb(_) => {
|
||||
"If database disk image is malformed, please enable the discard context option and use it once."
|
||||
}
|
||||
SQLitendError::Prepare(_) => "Please check if the syntax is correct.",
|
||||
SQLitendError::Step(_) => {
|
||||
"If database disk image is malformed, please enable the discard context option and use it once."
|
||||
}
|
||||
SQLitendError::UnsupportColumnType(_) => {
|
||||
"An unsupported type was encountered, please create an issue on github."
|
||||
}
|
||||
},
|
||||
WorkerError::NotFound | WorkerError::OpfsSAHError => {
|
||||
"This shouldn't happen, please create an issue on github."
|
||||
}
|
||||
WorkerError::InvaildState => {
|
||||
"SQLite is in an abnormal state when executing SQLite."
|
||||
}
|
||||
WorkerError::OpfsSAHPoolOpened => OPFS_SAH_POOL_OPENED_DETAILS,
|
||||
},
|
||||
SQLightError::AceEditor(ace_editor) => match ace_editor {
|
||||
aceditor::EditorError::Serde(_)
|
||||
| aceditor::EditorError::SetTheme(_)
|
||||
| aceditor::EditorError::SetKeyboardHandler(_)
|
||||
| aceditor::EditorError::Open(_)
|
||||
| aceditor::EditorError::DefineEx(_) => {
|
||||
"This shouldn't happen, please create an issue on github."
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
view! {
|
||||
<details open>
|
||||
<summary>{summary}</summary>
|
||||
<p>{details}</p>
|
||||
</details>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
None => view! { "No Error" }.into_any(),
|
||||
};
|
||||
|
||||
view! {
|
||||
<SimplePane>
|
||||
<Section label="Last Error".into()>{show}</Section>
|
||||
</SimplePane>
|
||||
}
|
||||
}
|
||||
250
src/app/playground.rs
Normal file
250
src/app/playground.rs
Normal file
@@ -0,0 +1,250 @@
|
||||
use std::ops::Deref;
|
||||
|
||||
use istyles::istyles;
|
||||
use leptos::prelude::*;
|
||||
use leptos::tachys::html;
|
||||
use reactive_stores::Store;
|
||||
use split_grid::{Gutter, SplitOptions};
|
||||
use tokio::sync::mpsc::UnboundedReceiver;
|
||||
use wasm_bindgen::{JsCast, prelude::Closure};
|
||||
use wasm_bindgen_futures::spawn_local;
|
||||
use web_sys::wasm_bindgen::JsValue;
|
||||
|
||||
use crate::app::{
|
||||
Focus,
|
||||
editor::Editor,
|
||||
header::Header,
|
||||
output::{Output, change_focus},
|
||||
state::{GlobalState, GlobalStateStoreFields, Orientation, Theme, Vfs},
|
||||
};
|
||||
use crate::{WorkerHandle, WorkerResponse, handle_state};
|
||||
|
||||
istyles!(styles, "assets/module.postcss/playground.module.css.map");
|
||||
|
||||
pub fn playground(
|
||||
worker: (WorkerHandle, UnboundedReceiver<WorkerResponse>),
|
||||
) -> Box<dyn FnOnce() -> AnyView + 'static> {
|
||||
Box::new(move || {
|
||||
let (worker_handle, rx) = worker;
|
||||
let state = GlobalState::load().unwrap_or_default();
|
||||
provide_context(Store::new(state));
|
||||
|
||||
let state = expect_context::<Store<GlobalState>>();
|
||||
state.worker().set(Some(worker_handle));
|
||||
|
||||
handle_last_error(state);
|
||||
handle_system_theme(state);
|
||||
handle_automic_orientation(state);
|
||||
handle_connect_db(state);
|
||||
hanlde_save_state(state);
|
||||
|
||||
spawn_local(handle_state(state, rx));
|
||||
|
||||
view! {
|
||||
<div id="playground" class=styles::container>
|
||||
<Header />
|
||||
<ResizableArea />
|
||||
</div>
|
||||
}
|
||||
.into_any()
|
||||
})
|
||||
}
|
||||
|
||||
fn hanlde_save_state(state: Store<GlobalState>) {
|
||||
Effect::new(move || {
|
||||
state.vfs().track();
|
||||
state.editor_config().track();
|
||||
state.orientation().track();
|
||||
state.theme().track();
|
||||
state.keep_ctx().track();
|
||||
state.code().track();
|
||||
|
||||
state.read_untracked().save();
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_connect_db(state: Store<GlobalState>) {
|
||||
Effect::new(move || {
|
||||
if let Some(worker) = &*state.worker().read() {
|
||||
worker.send_task(crate::WorkerRequest::Open(crate::OpenOptions {
|
||||
filename: "test.db".into(),
|
||||
persist: *state.vfs().read() == Vfs::OPFS,
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_system_theme(state: Store<GlobalState>) {
|
||||
Effect::new(move || {
|
||||
let theme = match *state.theme().read() {
|
||||
Theme::System => {
|
||||
if let Ok(Some(query)) = window().match_media("(prefers-color-scheme: dark)") {
|
||||
if query.matches() { "dark" } else { "light" }
|
||||
} else {
|
||||
"light"
|
||||
}
|
||||
}
|
||||
Theme::SystemLight | Theme::Light => "light",
|
||||
Theme::SystemDark | Theme::Dark => "dark",
|
||||
};
|
||||
if let Some(element) = document().document_element() {
|
||||
element.set_attribute("data-theme", theme).unwrap()
|
||||
}
|
||||
});
|
||||
|
||||
if let Ok(Some(query)) = window().match_media("(prefers-color-scheme: dark)") {
|
||||
let f = move |query: web_sys::MediaQueryList| {
|
||||
if state.theme().get_untracked().is_system() {
|
||||
*state.theme().write() = if query.matches() {
|
||||
Theme::SystemDark
|
||||
} else {
|
||||
Theme::SystemLight
|
||||
};
|
||||
}
|
||||
};
|
||||
f(query.clone());
|
||||
let callback = Closure::<dyn Fn(web_sys::MediaQueryList)>::new(f);
|
||||
query
|
||||
.add_event_listener_with_callback("change", callback.as_ref().unchecked_ref())
|
||||
.unwrap();
|
||||
callback.forget();
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_last_error(state: Store<GlobalState>) {
|
||||
Effect::new(move || {
|
||||
if state.last_error().read().is_some() {
|
||||
change_focus(state, Some(Focus::Status));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_automic_orientation(state: Store<GlobalState>) {
|
||||
if let Ok(Some(query)) = window().match_media("(max-width: 1600px)") {
|
||||
let f = move |query: web_sys::MediaQueryList| {
|
||||
if state.orientation().get_untracked().is_auto() {
|
||||
*state.orientation().write() = if query.matches() {
|
||||
Orientation::AutoHorizontal
|
||||
} else {
|
||||
Orientation::AutoVertical
|
||||
};
|
||||
}
|
||||
};
|
||||
f(query.clone());
|
||||
let callback = Closure::<dyn Fn(web_sys::MediaQueryList)>::new(f);
|
||||
query
|
||||
.add_event_listener_with_callback("change", callback.as_ref().unchecked_ref())
|
||||
.unwrap();
|
||||
callback.forget();
|
||||
}
|
||||
}
|
||||
|
||||
fn gird_style() -> String {
|
||||
let state = expect_context::<Store<GlobalState>>();
|
||||
|
||||
let (focused_grid_style, unfocused_grid_style) = match *state.orientation().read() {
|
||||
Orientation::Horizontal | Orientation::AutoHorizontal => (
|
||||
styles::resizeableAreaRowOutputFocused.to_string(),
|
||||
styles::resizeableAreaRowOutputUnfocused.to_string(),
|
||||
),
|
||||
Orientation::Automatic | Orientation::Vertical | Orientation::AutoVertical => (
|
||||
styles::resizeableAreaColumnOutputFocused.to_string(),
|
||||
styles::resizeableAreaColumnOutputUnfocused.to_string(),
|
||||
),
|
||||
};
|
||||
|
||||
if state.read().is_focus() {
|
||||
focused_grid_style
|
||||
} else {
|
||||
unfocused_grid_style
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_outer_style() -> String {
|
||||
let state = expect_context::<Store<GlobalState>>();
|
||||
|
||||
match *state.orientation().read() {
|
||||
Orientation::Horizontal | Orientation::AutoHorizontal => {
|
||||
styles::splitRowsGutter.to_string()
|
||||
}
|
||||
Orientation::Automatic | Orientation::Vertical | Orientation::AutoVertical => {
|
||||
styles::splitColumnsGutter.to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_inner_style() -> String {
|
||||
let state = expect_context::<Store<GlobalState>>();
|
||||
|
||||
match *state.orientation().read() {
|
||||
Orientation::Horizontal | Orientation::AutoHorizontal => {
|
||||
styles::splitRowsGutterHandle.to_string()
|
||||
}
|
||||
Orientation::Automatic | Orientation::Vertical | Orientation::AutoVertical => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn ResizableArea() -> impl IntoView {
|
||||
let state = expect_context::<Store<GlobalState>>();
|
||||
|
||||
let node_ref = NodeRef::<html::element::Div>::new();
|
||||
let drag_handle = NodeRef::<html::element::Div>::new();
|
||||
|
||||
Effect::new(move || {
|
||||
state.orientation().track();
|
||||
state.is_focused().track();
|
||||
|
||||
if let Some(div) = &*node_ref.read() {
|
||||
let style = div.deref().style();
|
||||
let _ = style.remove_property("grid-template-columns");
|
||||
let _ = style.remove_property("grid-template-rows");
|
||||
}
|
||||
});
|
||||
|
||||
Effect::new(move || {
|
||||
state.show_something().track();
|
||||
|
||||
let element = if let Some(element) = &*drag_handle.read() {
|
||||
JsValue::from(element)
|
||||
} else {
|
||||
JsValue::null()
|
||||
};
|
||||
|
||||
let options = match *state.orientation().read() {
|
||||
Orientation::Horizontal | Orientation::AutoHorizontal => SplitOptions {
|
||||
min_size: 100,
|
||||
row_gutters: Some(vec![Gutter { track: 1, element }]),
|
||||
column_gutters: None,
|
||||
},
|
||||
Orientation::Automatic | Orientation::Vertical | Orientation::AutoVertical => {
|
||||
SplitOptions {
|
||||
min_size: 100,
|
||||
row_gutters: None,
|
||||
column_gutters: Some(vec![Gutter { track: 1, element }]),
|
||||
}
|
||||
}
|
||||
};
|
||||
let grid = split_grid::split(&options.into());
|
||||
on_cleanup(move || grid.destroy());
|
||||
});
|
||||
|
||||
view! {
|
||||
<div node_ref=node_ref class=gird_style>
|
||||
<div class=styles::editor>
|
||||
<Editor />
|
||||
</div>
|
||||
<Show when=move || state.read().is_focus() fallback=|| ()>
|
||||
<div node_ref=drag_handle class=handle_outer_style>
|
||||
<span class=handle_inner_style>"⣿"</span>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when=move || *state.show_something().read() fallback=|| ()>
|
||||
<div class=styles::output>
|
||||
<Output />
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
</div>
|
||||
}
|
||||
}
|
||||
223
src/app/pop_button.rs
Normal file
223
src/app/pop_button.rs
Normal file
@@ -0,0 +1,223 @@
|
||||
use std::{ops::Deref, sync::Arc};
|
||||
|
||||
use floating_ui::{
|
||||
ArrowPosition, ComputePosition, MiddlewareData, auto_update, compute_options, compute_position,
|
||||
};
|
||||
use istyles::istyles;
|
||||
use js_sys::Object;
|
||||
use leptos::{portal::Portal, prelude::*, tachys::html};
|
||||
use wasm_bindgen::{JsCast, JsValue, prelude::Closure};
|
||||
use wasm_bindgen_futures::spawn_local;
|
||||
use web_sys::{KeyboardEvent, MouseEvent};
|
||||
|
||||
use crate::FragileComfirmed;
|
||||
|
||||
istyles!(styles, "assets/module.postcss/pop_button.module.css.map");
|
||||
|
||||
#[component]
|
||||
pub fn PopButton<B, M>(
|
||||
button: B,
|
||||
menu: M,
|
||||
#[prop(optional)] menu_container: NodeRef<html::element::Div>,
|
||||
) -> impl IntoView
|
||||
where
|
||||
B: FnOnce(Box<dyn FnMut(MouseEvent) + Send>, NodeRef<html::element::Button>) -> AnyView,
|
||||
M: Fn(Box<dyn Fn()>) -> AnyView + Send + Sync + 'static,
|
||||
{
|
||||
let (is_open, set_open) = signal(false);
|
||||
let toggle = move || set_open.set(!is_open.get());
|
||||
let close = move || set_open.set(false);
|
||||
|
||||
let arrow_ref = NodeRef::<html::element::Div>::new();
|
||||
let reference_ref = NodeRef::<html::element::Button>::new();
|
||||
let floating_ref = NodeRef::<html::element::Div>::new();
|
||||
let menu_ref = NodeRef::<html::element::Div>::new();
|
||||
|
||||
Effect::new(move || {
|
||||
let key_listener = move |event: KeyboardEvent| {
|
||||
if !is_open.get_untracked() {
|
||||
return;
|
||||
}
|
||||
|
||||
if event.key() == "Escape" {
|
||||
set_open(false);
|
||||
}
|
||||
};
|
||||
|
||||
let callback = FragileComfirmed::new(Closure::<dyn Fn(KeyboardEvent)>::new(key_listener));
|
||||
|
||||
window()
|
||||
.add_event_listener_with_callback("keydown", callback.as_ref().unchecked_ref())
|
||||
.unwrap();
|
||||
|
||||
on_cleanup(move || {
|
||||
window()
|
||||
.remove_event_listener_with_callback("keydown", callback.as_ref().unchecked_ref())
|
||||
.unwrap();
|
||||
drop(callback)
|
||||
});
|
||||
});
|
||||
|
||||
Effect::new(move || {
|
||||
let listener = move |event: MouseEvent| {
|
||||
if !is_open.get_untracked() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(target) = event.target() {
|
||||
let node = target.dyn_into::<web_sys::Node>().ok();
|
||||
if !reference_ref.with_untracked(|reference| {
|
||||
reference
|
||||
.as_ref()
|
||||
.is_some_and(|reference| reference.deref().contains(node.as_ref()))
|
||||
}) && !floating_ref.with_untracked(|floating| {
|
||||
floating
|
||||
.as_ref()
|
||||
.is_some_and(|floating| floating.deref().contains(node.as_ref()))
|
||||
}) {
|
||||
set_open(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let callback = FragileComfirmed::new(Closure::<dyn Fn(MouseEvent)>::new(listener));
|
||||
|
||||
window()
|
||||
.add_event_listener_with_callback("click", callback.as_ref().unchecked_ref())
|
||||
.unwrap();
|
||||
|
||||
on_cleanup(move || {
|
||||
window()
|
||||
.remove_event_listener_with_callback("click", callback.as_ref().unchecked_ref())
|
||||
.unwrap();
|
||||
drop(callback)
|
||||
});
|
||||
});
|
||||
|
||||
Effect::new(move || {
|
||||
let callback = Closure::new(move || {
|
||||
let options = compute_options(10, &arrow_ref.get_untracked().into());
|
||||
|
||||
spawn_local(async move {
|
||||
let value = compute_position(
|
||||
reference_ref.get_untracked().into(),
|
||||
floating_ref.get_untracked().into(),
|
||||
options,
|
||||
)
|
||||
.await;
|
||||
|
||||
let ComputePosition {
|
||||
x,
|
||||
y,
|
||||
placement,
|
||||
strategy,
|
||||
middleware_data,
|
||||
} = serde_wasm_bindgen::from_value(value).unwrap();
|
||||
|
||||
if let Some(element) = floating_ref.get_untracked() {
|
||||
let style = element.deref().style();
|
||||
#[derive(serde::Serialize)]
|
||||
struct Style {
|
||||
position: String,
|
||||
left: String,
|
||||
top: String,
|
||||
}
|
||||
|
||||
let pos = serde_wasm_bindgen::to_value(&Style {
|
||||
position: strategy,
|
||||
left: format!("{x}px"),
|
||||
top: format!("{y}px"),
|
||||
})
|
||||
.unwrap();
|
||||
Object::assign(&style, &pos.into());
|
||||
}
|
||||
|
||||
if let Some(element) = arrow_ref.get_untracked() {
|
||||
let MiddlewareData {
|
||||
arrow: ArrowPosition { x },
|
||||
} = middleware_data;
|
||||
let style = element.deref().style();
|
||||
#[derive(serde::Serialize)]
|
||||
struct Style {
|
||||
left: String,
|
||||
}
|
||||
|
||||
let pos = serde_wasm_bindgen::to_value(&Style {
|
||||
left: format!("{x}px"),
|
||||
})
|
||||
.unwrap();
|
||||
Object::assign(&style, &pos.into());
|
||||
}
|
||||
|
||||
if let Some(menu_ref) = menu_ref.get_untracked() {
|
||||
let class = if placement == "top" {
|
||||
styles::contentTop
|
||||
} else if placement == "bottom" {
|
||||
styles::contentBottom
|
||||
} else {
|
||||
""
|
||||
};
|
||||
menu_ref.set_class_name(class);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if let (Some(reference), Some(floating)) = (&*reference_ref.read(), &*floating_ref.read()) {
|
||||
let func = auto_update(
|
||||
reference.into(),
|
||||
floating.into(),
|
||||
&callback,
|
||||
JsValue::default(),
|
||||
);
|
||||
let func = FragileComfirmed::new(func);
|
||||
let callback = FragileComfirmed::new(callback);
|
||||
|
||||
on_cleanup(move || {
|
||||
func.call0(&JsValue::null()).unwrap();
|
||||
drop(callback);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let menu = Arc::new(menu);
|
||||
let float = move || {
|
||||
let menu_clone = Arc::clone(&menu);
|
||||
view! {
|
||||
<Show when=is_open fallback=|| ()>
|
||||
<div
|
||||
class=styles::container
|
||||
node_ref=floating_ref
|
||||
style="position: absolute; width: max-content;"
|
||||
>
|
||||
<div
|
||||
class=styles::arrow
|
||||
node_ref=arrow_ref
|
||||
style="position: absolute; pointer-events: none; bottom: 100%; transform: rotate(180deg);"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 20 20">
|
||||
<path stroke="none" d="M0,0 H20 L10,10 Q10,10 10,10 Z"></path>
|
||||
<clipPath id=":rh:">
|
||||
<rect x="0" y="0" width="20" height="20"></rect>
|
||||
</clipPath>
|
||||
</svg>
|
||||
</div>
|
||||
<div node_ref=menu_ref>
|
||||
<div>{menu_clone(Box::new(close))}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
}
|
||||
};
|
||||
|
||||
let float = Arc::new(float);
|
||||
let total = move || {
|
||||
let float_clone = Arc::clone(&float);
|
||||
if let Some(container) = menu_container.get() {
|
||||
view! { <Portal mount=container.clone()>{float_clone()}</Portal> }.into_any()
|
||||
} else {
|
||||
float_clone().into_any()
|
||||
}
|
||||
};
|
||||
|
||||
view! { <>{button(Box::new(move |_| toggle()), reference_ref)} {total}</> }
|
||||
}
|
||||
27
src/app/select_one.rs
Normal file
27
src/app/select_one.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
use leptos::prelude::*;
|
||||
|
||||
use crate::app::selectable_menu_item::SelectableMenuItem;
|
||||
|
||||
#[component]
|
||||
pub fn SelectOne<T, C, CH>(
|
||||
name: String,
|
||||
current_value: C,
|
||||
this_value: T,
|
||||
mut change_value: CH,
|
||||
children: Children,
|
||||
) -> impl IntoView
|
||||
where
|
||||
C: Fn() -> T + Send + 'static,
|
||||
CH: FnMut() + Send + 'static,
|
||||
T: PartialEq + Eq + Send + 'static,
|
||||
{
|
||||
view! {
|
||||
<SelectableMenuItem
|
||||
name=name
|
||||
selected=move || { current_value() == this_value }
|
||||
on_click=move |_| change_value()
|
||||
>
|
||||
{children()}
|
||||
</SelectableMenuItem>
|
||||
}
|
||||
}
|
||||
37
src/app/selectable_menu_item.rs
Normal file
37
src/app/selectable_menu_item.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use istyles::istyles;
|
||||
use leptos::prelude::*;
|
||||
use web_sys::MouseEvent;
|
||||
|
||||
use crate::app::{icon::checkmark_icon, menu_item::MenuItem};
|
||||
|
||||
istyles!(
|
||||
styles,
|
||||
"assets/module.postcss/selectable_menu_item.module.css.map"
|
||||
);
|
||||
|
||||
#[component]
|
||||
pub fn SelectableMenuItem<S, C>(
|
||||
name: String,
|
||||
selected: S,
|
||||
on_click: C,
|
||||
children: Children,
|
||||
) -> impl IntoView
|
||||
where
|
||||
S: Fn() -> bool + Send + 'static,
|
||||
C: FnMut(MouseEvent) + Send + 'static,
|
||||
{
|
||||
view! {
|
||||
<MenuItem>
|
||||
<button
|
||||
class=move || { if selected() { styles::selected } else { styles::container } }
|
||||
on:click=on_click
|
||||
>
|
||||
<div class=styles::header>
|
||||
<span class=styles::checkmark>{checkmark_icon()}</span>
|
||||
<span class=styles::name>{name}</span>
|
||||
</div>
|
||||
<div class=styles::description>{children()}</div>
|
||||
</button>
|
||||
</MenuItem>
|
||||
}
|
||||
}
|
||||
192
src/app/state.rs
Normal file
192
src/app/state.rs
Normal file
@@ -0,0 +1,192 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use aceditor::Editor;
|
||||
use leptos::tachys::dom::window;
|
||||
use reactive_stores::Store;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{FragileComfirmed, SQLightError, SQLiteStatementResult, WorkerHandle};
|
||||
|
||||
#[derive(Store, Serialize, Deserialize)]
|
||||
pub struct GlobalState {
|
||||
vfs: Vfs,
|
||||
editor_config: EditorConfig,
|
||||
orientation: Orientation,
|
||||
theme: Theme,
|
||||
keep_ctx: bool,
|
||||
code: String,
|
||||
// runtime state below
|
||||
#[serde(skip)]
|
||||
worker: Option<WorkerHandle>,
|
||||
#[serde(skip)]
|
||||
pub editor: Option<Editor>,
|
||||
#[serde(skip)]
|
||||
focus: Option<Focus>,
|
||||
#[serde(skip)]
|
||||
is_focused: bool,
|
||||
#[serde(skip)]
|
||||
opened_focus: HashSet<Focus>,
|
||||
#[serde(skip)]
|
||||
share_href: Option<String>,
|
||||
#[serde(skip)]
|
||||
show_something: bool,
|
||||
#[serde(skip)]
|
||||
output: Vec<SQLiteStatementResult>,
|
||||
#[serde(skip)]
|
||||
last_error: Option<FragileComfirmed<SQLightError>>,
|
||||
}
|
||||
|
||||
impl Default for GlobalState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
editor_config: EditorConfig::default(),
|
||||
code: String::new(),
|
||||
focus: None,
|
||||
show_something: false,
|
||||
orientation: Orientation::Automatic,
|
||||
theme: Theme::System,
|
||||
output: Vec::new(),
|
||||
vfs: Vfs::Memory,
|
||||
keep_ctx: false,
|
||||
share_href: None,
|
||||
is_focused: false,
|
||||
opened_focus: HashSet::new(),
|
||||
worker: None,
|
||||
editor: None,
|
||||
last_error: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GlobalState {
|
||||
pub fn load() -> Option<Self> {
|
||||
let storage = window().local_storage().ok()??;
|
||||
let value = storage.get("config").ok()??;
|
||||
serde_json::from_str(&value).ok()
|
||||
}
|
||||
|
||||
pub fn save(&self) {
|
||||
if let Some(Err(e)) = window()
|
||||
.local_storage()
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|s| s.set_item("config", &serde_json::to_string(self).unwrap()))
|
||||
{
|
||||
log::error!("Faild to save config to localstorage: {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct EditorConfig {
|
||||
pub keyboard: String,
|
||||
pub theme: String,
|
||||
}
|
||||
|
||||
impl Default for EditorConfig {
|
||||
fn default() -> Self {
|
||||
EditorConfig {
|
||||
keyboard: "ace".into(),
|
||||
theme: "github".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GlobalState {
|
||||
pub fn is_focus(&self) -> bool {
|
||||
self.focus.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum Vfs {
|
||||
Memory,
|
||||
OPFS,
|
||||
}
|
||||
|
||||
impl Vfs {
|
||||
pub fn value(&self) -> String {
|
||||
match self {
|
||||
Vfs::Memory => "Memory".into(),
|
||||
Vfs::OPFS => "OPFS".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum Focus {
|
||||
Execute,
|
||||
Share,
|
||||
Status,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Serialize, Deserialize)]
|
||||
pub enum Theme {
|
||||
System,
|
||||
SystemLight,
|
||||
SystemDark,
|
||||
Light,
|
||||
Dark,
|
||||
}
|
||||
|
||||
impl Theme {
|
||||
pub fn is_system(&self) -> bool {
|
||||
matches!(self, Theme::System | Theme::SystemLight | Theme::SystemDark)
|
||||
}
|
||||
|
||||
pub fn from_value(s: &str) -> Self {
|
||||
match s {
|
||||
"System" => Self::System,
|
||||
"Light" => Self::Light,
|
||||
"Dark" => Self::Dark,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_value(&self) -> String {
|
||||
match self {
|
||||
Theme::System | Theme::SystemLight | Theme::SystemDark => "System",
|
||||
Theme::Light => "Light",
|
||||
Theme::Dark => "Dark",
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Serialize, Deserialize)]
|
||||
pub enum Orientation {
|
||||
Automatic,
|
||||
AutoHorizontal,
|
||||
AutoVertical,
|
||||
Horizontal,
|
||||
Vertical,
|
||||
}
|
||||
|
||||
impl Orientation {
|
||||
pub fn is_auto(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Orientation::Automatic | Orientation::AutoVertical | Orientation::AutoHorizontal
|
||||
)
|
||||
}
|
||||
|
||||
pub fn from_value(s: &str) -> Self {
|
||||
match s {
|
||||
"Automatic" => Self::Automatic,
|
||||
"Horizontal" => Self::Horizontal,
|
||||
"Vertical" => Self::Vertical,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_value(&self) -> String {
|
||||
match self {
|
||||
Orientation::Automatic | Orientation::AutoVertical | Orientation::AutoHorizontal => {
|
||||
"Automatic"
|
||||
}
|
||||
Orientation::Horizontal => "Horizontal",
|
||||
Orientation::Vertical => "Vertical",
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
36
src/app/vfs_menu.rs
Normal file
36
src/app/vfs_menu.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
use leptos::prelude::*;
|
||||
use reactive_stores::Store;
|
||||
|
||||
use crate::app::{
|
||||
GlobalState, GlobalStateStoreFields, Vfs, menu_group::MenuGroup, select_one::SelectOne,
|
||||
};
|
||||
|
||||
#[component]
|
||||
pub fn VfsMenu() -> impl IntoView {
|
||||
let state = expect_context::<Store<GlobalState>>();
|
||||
|
||||
view! {
|
||||
<MenuGroup title="Choose SQLite VFS".into()>
|
||||
<SelectOne
|
||||
name="Memory".into()
|
||||
current_value=move || { *state.vfs().read() }
|
||||
this_value=Vfs::Memory
|
||||
change_value=move || {
|
||||
*state.vfs().write() = Vfs::Memory;
|
||||
}
|
||||
>
|
||||
"Data will be lost after refreshing."
|
||||
</SelectOne>
|
||||
<SelectOne
|
||||
name="OPFS".into()
|
||||
current_value=move || { *state.vfs().read() }
|
||||
this_value=Vfs::OPFS
|
||||
change_value=move || {
|
||||
*state.vfs().write() = Vfs::OPFS;
|
||||
}
|
||||
>
|
||||
"Persistent Storage."
|
||||
</SelectOne>
|
||||
</MenuGroup>
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user