Update input
This commit is contained in:
parent
b503d5f7e3
commit
ae8c0ab657
11 changed files with 556 additions and 18 deletions
23
Cargo.lock
generated
23
Cargo.lock
generated
|
|
@ -598,6 +598,17 @@ version = "0.1.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6"
|
||||
|
||||
[[package]]
|
||||
name = "catppuccin"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa1798ea21d5f88f057b9a4d075bbf3e9cc4bba98da0d6197a2019f61f633bf4"
|
||||
dependencies = [
|
||||
"itertools 0.13.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cbc"
|
||||
version = "0.1.2"
|
||||
|
|
@ -1762,7 +1773,7 @@ dependencies = [
|
|||
"gpui_macros",
|
||||
"http 0.1.0",
|
||||
"image",
|
||||
"itertools",
|
||||
"itertools 0.11.0",
|
||||
"lazy_static",
|
||||
"linkme",
|
||||
"log",
|
||||
|
|
@ -2201,6 +2212,15 @@ dependencies = [
|
|||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.11"
|
||||
|
|
@ -4227,6 +4247,7 @@ dependencies = [
|
|||
name = "ui"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"catppuccin",
|
||||
"gpui",
|
||||
"log",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -4,5 +4,6 @@ version = "0.1.0"
|
|||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
catppuccin = "2.4.0"
|
||||
gpui.workspace = true
|
||||
log.wokrspace = true
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@ pub mod button;
|
|||
mod colors;
|
||||
pub mod cursor;
|
||||
pub mod disableable;
|
||||
pub mod input;
|
||||
pub mod label;
|
||||
pub mod story;
|
||||
pub mod text_field;
|
||||
pub mod theme;
|
||||
|
||||
pub use colors::*;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use gpui::{
|
|||
Styled as _, ViewContext, VisualContext, WindowContext,
|
||||
};
|
||||
|
||||
use crate::input::Input;
|
||||
use crate::text_field::TextField;
|
||||
|
||||
use super::story_case;
|
||||
|
||||
|
|
@ -24,19 +24,11 @@ impl Render for InputStory {
|
|||
.justify_start()
|
||||
.gap_3()
|
||||
.child({
|
||||
cx.new_view(|cx| {
|
||||
let input = Input::new("input1", cx);
|
||||
input.set_placeholder("Enter text here...", cx);
|
||||
input
|
||||
})
|
||||
TextField::new(cx, "Enter text here...", false)
|
||||
})
|
||||
.child({
|
||||
cx.new_view(|cx| {
|
||||
let input = Input::new("input1", cx);
|
||||
input.set_placeholder("Enter text here...", cx);
|
||||
input.set_text("Hello, world!", cx);
|
||||
let input = TextField::new(cx, "Enter text here...", false);
|
||||
input
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ pub struct Stories {
|
|||
impl Stories {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
active: StoryType::Button,
|
||||
active: StoryType::Input,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
5
crates/ui/src/text_field.rs
Normal file
5
crates/ui/src/text_field.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
pub mod blink_manager;
|
||||
pub mod cursor_layout;
|
||||
pub mod text_field;
|
||||
|
||||
pub use text_field::*;
|
||||
34
crates/ui/src/text_field/blink_manager.rs
Normal file
34
crates/ui/src/text_field/blink_manager.rs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use gpui::ModelContext;
|
||||
|
||||
pub struct BlinkManager {
|
||||
blink_interval: Duration,
|
||||
|
||||
blink_epoch: usize,
|
||||
blinking_paused: bool,
|
||||
visible: bool,
|
||||
enabled: bool,
|
||||
}
|
||||
|
||||
impl BlinkManager {
|
||||
pub fn new(blink_interval: Duration) -> Self {
|
||||
Self {
|
||||
blink_interval: Duration::from_millis(500),
|
||||
blink_epoch: 0,
|
||||
blinking_paused: false,
|
||||
visible: true,
|
||||
enabled: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show_cursor(&self, cx: &mut ModelContext<'_, Self>) -> bool {
|
||||
self.enabled && (!self.blinking_paused || self.visible)
|
||||
}
|
||||
|
||||
pub fn blink_cursor(&mut self, epoch: usize, cx: &mut ModelContext<Self>) {}
|
||||
|
||||
pub fn disable(&mut self, _cx: &mut ModelContext<Self>) {
|
||||
self.enabled = false;
|
||||
}
|
||||
}
|
||||
51
crates/ui/src/text_field/cursor_layout.rs
Normal file
51
crates/ui/src/text_field/cursor_layout.rs
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
use gpui::{outline, px, AppContext, Bounds, Hsla, Pixels, ShapedLine, Size, ViewContext};
|
||||
|
||||
pub struct CursorLayout {
|
||||
origin: gpui::Point<Pixels>,
|
||||
block_width: Pixels,
|
||||
line_height: Pixels,
|
||||
color: Hsla,
|
||||
block_text: Option<ShapedLine>,
|
||||
}
|
||||
|
||||
impl CursorLayout {
|
||||
pub fn new(
|
||||
origin: gpui::Point<Pixels>,
|
||||
block_width: Pixels,
|
||||
line_height: Pixels,
|
||||
color: Hsla,
|
||||
block_text: Option<ShapedLine>,
|
||||
) -> CursorLayout {
|
||||
CursorLayout {
|
||||
origin,
|
||||
block_width,
|
||||
line_height,
|
||||
color,
|
||||
block_text,
|
||||
}
|
||||
}
|
||||
|
||||
fn bounds(&self, origin: gpui::Point<Pixels>) -> Bounds<Pixels> {
|
||||
Bounds {
|
||||
origin: self.origin + origin,
|
||||
size: Size {
|
||||
width: px(2.0),
|
||||
height: self.line_height,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn paint(&mut self, origin: gpui::Point<Pixels>, cx: &mut ViewContext<Self>) {
|
||||
let bounds = self.bounds(origin);
|
||||
|
||||
let cursor = outline(bounds, self.color);
|
||||
|
||||
cx.paint_quad(cursor);
|
||||
|
||||
if let Some(block_text) = &self.block_text {
|
||||
block_text
|
||||
.paint(self.origin + origin, self.line_height, cx)
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
343
crates/ui/src/text_field/text_field.rs
Normal file
343
crates/ui/src/text_field/text_field.rs
Normal file
|
|
@ -0,0 +1,343 @@
|
|||
use std::{ops::Range, time::Duration};
|
||||
|
||||
use gpui::{
|
||||
div, Context, EventEmitter, FocusHandle, HighlightStyle, InteractiveElement, InteractiveText,
|
||||
IntoElement, KeyDownEvent, Model, ParentElement, Render, RenderOnce, Styled, StyledText,
|
||||
TextStyle, View, ViewContext, VisualContext, WindowContext,
|
||||
};
|
||||
|
||||
use crate::{theme::Theme, disableable::Disableable};
|
||||
|
||||
use super::{blink_manager::BlinkManager, cursor_layout::CursorLayout};
|
||||
|
||||
#[derive(IntoElement, Clone)]
|
||||
pub struct TextField {
|
||||
focus_handle: FocusHandle,
|
||||
disable: bool,
|
||||
pub view: View<TextView>,
|
||||
}
|
||||
|
||||
impl TextField {
|
||||
pub fn new(cx: &mut WindowContext, placeholder: &str, disable: bool) -> Self {
|
||||
let focus_handle = cx.focus_handle();
|
||||
let view = TextView::init(cx, &focus_handle, placeholder, disable);
|
||||
|
||||
Self {
|
||||
focus_handle,
|
||||
view,
|
||||
disable,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn focus(&self, cx: &mut WindowContext) {
|
||||
cx.focus(&self.focus_handle);
|
||||
}
|
||||
}
|
||||
|
||||
impl Disableable for TextField {
|
||||
fn disabled(mut self, disabled: bool) -> Self {
|
||||
self.disable = disabled;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for TextField {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
// cx.focus(&self.focus_handle);
|
||||
let theme = cx.global::<Theme>();
|
||||
|
||||
let clone = self.view.clone();
|
||||
|
||||
div()
|
||||
.border_color(
|
||||
self.focus_handle
|
||||
.is_focused(cx)
|
||||
.then(|| theme.blue)
|
||||
.unwrap_or(theme.crust),
|
||||
)
|
||||
.border_1()
|
||||
.track_focus(&self.focus_handle)
|
||||
.on_key_down(move |event, cx| {
|
||||
if self.disable {
|
||||
return;
|
||||
}
|
||||
|
||||
self.view.update(cx, |text_view, vc| {
|
||||
let prev = text_view.text.clone();
|
||||
vc.emit(TextEvent::KeyDown(event.clone()));
|
||||
let keystroke = &event.keystroke.key;
|
||||
let chars = text_view.text.chars().collect::<Vec<char>>();
|
||||
|
||||
let m = event.keystroke.modifiers.platform;
|
||||
|
||||
if m {
|
||||
match keystroke.as_str() {
|
||||
_ => {}
|
||||
}
|
||||
} else if !event
|
||||
.keystroke
|
||||
.ime_key
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
.is_empty()
|
||||
{
|
||||
let ime_key = &event.keystroke.ime_key.clone().unwrap_or_default();
|
||||
text_view.text.replace_range(
|
||||
text_view.char_range_to_text_range(&text_view.text),
|
||||
ime_key,
|
||||
);
|
||||
let i = text_view.selection.start + ime_key.chars().count();
|
||||
text_view.selection = i..i;
|
||||
} else {
|
||||
match keystroke.as_str() {
|
||||
"left" => {
|
||||
if text_view.selection.start > 0 {
|
||||
text_view.selection =
|
||||
text_view.selection.start - 1..text_view.selection.end;
|
||||
}
|
||||
}
|
||||
"right" => {
|
||||
if text_view.selection.end < text_view.text.len() {
|
||||
text_view.selection =
|
||||
text_view.selection.start + 1..text_view.selection.end + 1;
|
||||
} else {
|
||||
text_view.selection =
|
||||
text_view.selection.start + 1..text_view.selection.end;
|
||||
}
|
||||
}
|
||||
"backspace" => {
|
||||
if text_view.text.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
if text_view.selection.start == text_view.selection.end {
|
||||
let i = (text_view.selection.start - 1).min(chars.len());
|
||||
text_view.text = chars[0..i].iter().collect::<String>()
|
||||
+ &(chars[text_view.selection.end.min(chars.len())..]
|
||||
.iter()
|
||||
.collect::<String>());
|
||||
text_view.selection = i..i;
|
||||
}
|
||||
|
||||
text_view.text.replace_range(
|
||||
text_view.char_range_to_text_range(&text_view.text),
|
||||
"",
|
||||
);
|
||||
|
||||
text_view.selection.end = text_view.selection.start;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if prev != text_view.text {
|
||||
vc.emit(TextEvent::Input {
|
||||
text: text_view.text.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
vc.notify();
|
||||
})
|
||||
})
|
||||
.rounded_lg()
|
||||
.py_1p5()
|
||||
.px_3()
|
||||
.min_w_20()
|
||||
.bg(self.disable.then(|| theme.crust).unwrap_or(theme.base))
|
||||
.child(clone)
|
||||
}
|
||||
}
|
||||
|
||||
pub enum TextEvent {
|
||||
Input { text: String },
|
||||
Blur,
|
||||
KeyDown(KeyDownEvent),
|
||||
}
|
||||
|
||||
impl EventEmitter<TextEvent> for TextView {}
|
||||
|
||||
pub struct TextView {
|
||||
pub text: String,
|
||||
pub placeholder: String,
|
||||
pub word_click: (usize, u16),
|
||||
pub selection: Range<usize>,
|
||||
pub disable: bool,
|
||||
pub blink_manager: Model<BlinkManager>,
|
||||
pub cursor: CursorLayout,
|
||||
}
|
||||
|
||||
const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
|
||||
|
||||
impl TextView {
|
||||
pub fn init(
|
||||
cx: &mut WindowContext,
|
||||
focus_handle: &FocusHandle,
|
||||
placeholder: &str,
|
||||
disable: bool,
|
||||
) -> View<Self> {
|
||||
let blink_manager = cx.new_model(|cx| BlinkManager::new(CURSOR_BLINK_INTERVAL));
|
||||
|
||||
let cursor = CursorLayout::new(
|
||||
gpui::Point::new(gpui::px(0.0), gpui::px(0.0)),
|
||||
gpui::px(2.0),
|
||||
gpui::px(20.0),
|
||||
gpui::Hsla {
|
||||
h: 0.0,
|
||||
s: 0.0,
|
||||
l: 0.0,
|
||||
a: 1.0,
|
||||
},
|
||||
None,
|
||||
);
|
||||
|
||||
let m = Self {
|
||||
text: String::new(),
|
||||
placeholder: placeholder.to_string(),
|
||||
word_click: (0, 0),
|
||||
selection: 0..0,
|
||||
blink_manager,
|
||||
cursor,
|
||||
disable,
|
||||
};
|
||||
|
||||
let view = cx.new_view(|cx| {
|
||||
cx.on_blur(
|
||||
focus_handle,
|
||||
|editor: &mut TextView, cx: &mut ViewContext<'_, TextView>| {
|
||||
editor.blink_manager.update(cx, BlinkManager::disable);
|
||||
cx.emit(TextEvent::Blur);
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
|
||||
cx.on_focus(focus_handle, |view, cx| {
|
||||
view.select_all(cx);
|
||||
})
|
||||
.detach();
|
||||
m
|
||||
});
|
||||
|
||||
cx.subscribe(
|
||||
&view,
|
||||
move |subscriber, emitter: &TextEvent, cx| match emitter {
|
||||
TextEvent::Input { text: _ } => {
|
||||
subscriber.update(cx, |editor, cx| {
|
||||
editor.word_click = (0, 0);
|
||||
});
|
||||
}
|
||||
TextEvent::Blur => {
|
||||
subscriber.update(cx, |editor, cx| {
|
||||
editor.blink_manager.update(cx, BlinkManager::disable);
|
||||
editor.word_click = (0, 0);
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
|
||||
view
|
||||
}
|
||||
|
||||
pub fn select_all(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.selection = 0..self.text.len();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn word_ranges(&self) -> Vec<Range<usize>> {
|
||||
let mut words = Vec::new();
|
||||
let mut last_was_boundary = true;
|
||||
let mut word_start = 0;
|
||||
let s = self.text.clone();
|
||||
|
||||
for (i, c) in s.char_indices() {
|
||||
if c.is_alphanumeric() || c == '_' {
|
||||
if last_was_boundary {
|
||||
word_start = i;
|
||||
}
|
||||
last_was_boundary = false;
|
||||
} else {
|
||||
if !last_was_boundary {
|
||||
words.push(word_start..i);
|
||||
}
|
||||
last_was_boundary = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the last characters form a word and push it if so
|
||||
if !last_was_boundary {
|
||||
words.push(word_start..s.len());
|
||||
}
|
||||
|
||||
words
|
||||
}
|
||||
|
||||
pub fn char_range_to_text_range(&self, text: &str) -> Range<usize> {
|
||||
let start = text
|
||||
.chars()
|
||||
.take(self.selection.start)
|
||||
.collect::<String>()
|
||||
.len();
|
||||
let end = text
|
||||
.chars()
|
||||
.take(self.selection.end)
|
||||
.collect::<String>()
|
||||
.len();
|
||||
start..end
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for TextView {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let theme = cx.global::<Theme>();
|
||||
let mut text = self.text.clone();
|
||||
|
||||
let mut style = TextStyle::default();
|
||||
style.color = theme.text;
|
||||
style.font_family = theme.font_sans.clone();
|
||||
|
||||
let mut selection_style = HighlightStyle::default();
|
||||
let mut color = theme.lavender;
|
||||
color.fade_out(0.8);
|
||||
selection_style.background_color = Some(color);
|
||||
|
||||
let highlights = vec![(self.char_range_to_text_range(&text), selection_style)];
|
||||
|
||||
let styled_text: StyledText = if text.len() == 0 {
|
||||
text = self.placeholder.to_string();
|
||||
style.color = theme.subtext0;
|
||||
StyledText::new(text)
|
||||
} else {
|
||||
StyledText::new(text).with_highlights(&style, highlights)
|
||||
};
|
||||
|
||||
let view = cx.view().clone();
|
||||
|
||||
InteractiveText::new("text", styled_text).on_click(self.word_ranges(), move |ev, cx| {
|
||||
view.update(cx, |text_view, cx| {
|
||||
let (index, mut count) = text_view.word_click;
|
||||
if index == ev {
|
||||
count += 1;
|
||||
} else {
|
||||
count = 1;
|
||||
}
|
||||
match count {
|
||||
2 => {
|
||||
let word_ranges = text_view.word_ranges();
|
||||
text_view.selection = word_ranges.get(ev).unwrap().clone();
|
||||
}
|
||||
3 => {
|
||||
// Should select the line
|
||||
}
|
||||
4 => {
|
||||
count = 0;
|
||||
text_view.selection = 0..text_view.text.len();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
text_view.word_click = (ev, count);
|
||||
cx.notify();
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
90
crates/ui/src/theme.rs
Normal file
90
crates/ui/src/theme.rs
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use catppuccin::{Flavor, FlavorColors};
|
||||
use gpui::{AppContext, Global, Hsla, Rgba, SharedString};
|
||||
|
||||
fn color_to_hsla(color: catppuccin::Color) -> Hsla {
|
||||
Rgba {
|
||||
r: color.rgb.r as f32 / 255.0,
|
||||
g: color.rgb.g as f32 / 255.0,
|
||||
b: color.rgb.b as f32 / 255.0,
|
||||
a: 1.0,
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Theme {
|
||||
pub font_sans: SharedString,
|
||||
pub font_mono: SharedString,
|
||||
pub crust: Hsla,
|
||||
pub text: Hsla,
|
||||
pub base: Hsla,
|
||||
pub mantle: Hsla,
|
||||
pub green: Hsla,
|
||||
pub red: Hsla,
|
||||
pub blue: Hsla,
|
||||
pub text_disabled: Hsla,
|
||||
pub subtext1: Hsla,
|
||||
pub subtext0: Hsla,
|
||||
pub overlay2: Hsla,
|
||||
pub overlay1: Hsla,
|
||||
pub overlay0: Hsla,
|
||||
pub surface2: Hsla,
|
||||
pub surface1: Hsla,
|
||||
pub surface0: Hsla,
|
||||
pub lavender: Hsla,
|
||||
}
|
||||
|
||||
impl Global for Theme {}
|
||||
|
||||
impl From<FlavorColors> for Theme {
|
||||
fn from(colors: FlavorColors) -> Self {
|
||||
Theme {
|
||||
font_sans: "Inter".into(),
|
||||
font_mono: "JetBrains Mono".into(),
|
||||
crust: color_to_hsla(colors.crust),
|
||||
text: color_to_hsla(colors.text),
|
||||
base: color_to_hsla(colors.base),
|
||||
mantle: color_to_hsla(colors.mantle),
|
||||
green: color_to_hsla(colors.green),
|
||||
red: color_to_hsla(colors.red),
|
||||
blue: color_to_hsla(colors.blue),
|
||||
subtext0: color_to_hsla(colors.subtext0),
|
||||
subtext1: color_to_hsla(colors.subtext1),
|
||||
text_disabled: color_to_hsla(colors.subtext0),
|
||||
overlay0: color_to_hsla(colors.overlay0),
|
||||
overlay1: color_to_hsla(colors.overlay1),
|
||||
overlay2: color_to_hsla(colors.overlay2),
|
||||
surface0: color_to_hsla(colors.surface0),
|
||||
surface1: color_to_hsla(colors.surface1),
|
||||
surface2: color_to_hsla(colors.surface2),
|
||||
lavender: color_to_hsla(colors.lavender),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Theme {
|
||||
fn new() -> Self {
|
||||
Self::from(catppuccin::PALETTE.mocha.colors)
|
||||
}
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.set_global(Theme::new())
|
||||
}
|
||||
|
||||
pub fn change(flavour: Flavor, cx: &mut AppContext) {
|
||||
cx.set_global(Self::from(flavour.colors));
|
||||
cx.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ActiveTheme {
|
||||
fn theme(&self) -> &Theme;
|
||||
}
|
||||
|
||||
impl ActiveTheme for AppContext {
|
||||
fn theme(&self) -> &Theme {
|
||||
self.global::<Theme>()
|
||||
}
|
||||
}
|
||||
|
|
@ -2,9 +2,7 @@ use gpui::{prelude::FluentBuilder, *};
|
|||
|
||||
use std::sync::Arc;
|
||||
use ui::{
|
||||
button::{Button, ButtonStyle},
|
||||
disableable::Clickable as _,
|
||||
Color,
|
||||
button::{Button, ButtonStyle}, disableable::Clickable as _, theme::Theme, Color
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
|
||||
|
|
@ -67,7 +65,9 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
|
|||
cx.on_action({
|
||||
let app_state = app_state.clone();
|
||||
move |action: &Open, cx: &mut AppContext| {}
|
||||
})
|
||||
});
|
||||
|
||||
Theme::init(cx);
|
||||
}
|
||||
|
||||
pub fn open_new(
|
||||
|
|
|
|||
Loading…
Reference in a new issue