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"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6"
|
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]]
|
[[package]]
|
||||||
name = "cbc"
|
name = "cbc"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
|
|
@ -1762,7 +1773,7 @@ dependencies = [
|
||||||
"gpui_macros",
|
"gpui_macros",
|
||||||
"http 0.1.0",
|
"http 0.1.0",
|
||||||
"image",
|
"image",
|
||||||
"itertools",
|
"itertools 0.11.0",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"linkme",
|
"linkme",
|
||||||
"log",
|
"log",
|
||||||
|
|
@ -2201,6 +2212,15 @@ dependencies = [
|
||||||
"either",
|
"either",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itertools"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.11"
|
version = "1.0.11"
|
||||||
|
|
@ -4227,6 +4247,7 @@ dependencies = [
|
||||||
name = "ui"
|
name = "ui"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"catppuccin",
|
||||||
"gpui",
|
"gpui",
|
||||||
"log",
|
"log",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -4,5 +4,6 @@ version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
catppuccin = "2.4.0"
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
log.wokrspace = true
|
log.wokrspace = true
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,9 @@ pub mod button;
|
||||||
mod colors;
|
mod colors;
|
||||||
pub mod cursor;
|
pub mod cursor;
|
||||||
pub mod disableable;
|
pub mod disableable;
|
||||||
pub mod input;
|
|
||||||
pub mod label;
|
pub mod label;
|
||||||
pub mod story;
|
pub mod story;
|
||||||
|
pub mod text_field;
|
||||||
|
pub mod theme;
|
||||||
|
|
||||||
pub use colors::*;
|
pub use colors::*;
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ use gpui::{
|
||||||
Styled as _, ViewContext, VisualContext, WindowContext,
|
Styled as _, ViewContext, VisualContext, WindowContext,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::input::Input;
|
use crate::text_field::TextField;
|
||||||
|
|
||||||
use super::story_case;
|
use super::story_case;
|
||||||
|
|
||||||
|
|
@ -24,19 +24,11 @@ impl Render for InputStory {
|
||||||
.justify_start()
|
.justify_start()
|
||||||
.gap_3()
|
.gap_3()
|
||||||
.child({
|
.child({
|
||||||
cx.new_view(|cx| {
|
TextField::new(cx, "Enter text here...", false)
|
||||||
let input = Input::new("input1", cx);
|
|
||||||
input.set_placeholder("Enter text here...", cx);
|
|
||||||
input
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.child({
|
.child({
|
||||||
cx.new_view(|cx| {
|
let input = TextField::new(cx, "Enter text here...", false);
|
||||||
let input = Input::new("input1", cx);
|
|
||||||
input.set_placeholder("Enter text here...", cx);
|
|
||||||
input.set_text("Hello, world!", cx);
|
|
||||||
input
|
input
|
||||||
})
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ pub struct Stories {
|
||||||
impl Stories {
|
impl Stories {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
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 std::sync::Arc;
|
||||||
use ui::{
|
use ui::{
|
||||||
button::{Button, ButtonStyle},
|
button::{Button, ButtonStyle}, disableable::Clickable as _, theme::Theme, Color
|
||||||
disableable::Clickable as _,
|
|
||||||
Color,
|
|
||||||
};
|
};
|
||||||
use util::ResultExt as _;
|
use util::ResultExt as _;
|
||||||
|
|
||||||
|
|
@ -67,7 +65,9 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
|
||||||
cx.on_action({
|
cx.on_action({
|
||||||
let app_state = app_state.clone();
|
let app_state = app_state.clone();
|
||||||
move |action: &Open, cx: &mut AppContext| {}
|
move |action: &Open, cx: &mut AppContext| {}
|
||||||
})
|
});
|
||||||
|
|
||||||
|
Theme::init(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn open_new(
|
pub fn open_new(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue