1use std::rc::Rc;
2
3use gpui::{
4 App, DefiniteLength, Entity, IntoElement, RenderOnce, SharedString, StyleRefinement, Styled,
5 Window, prelude::FluentBuilder as _, relative,
6};
7
8use super::{EditorState, Input};
9use crate::native_menu::NativeMenu;
10use crate::{ActiveTheme as _, RoleOverride, StyledExt as _};
11
12const EDITOR_LINE_HEIGHT: f32 = 1.5;
15
16#[derive(IntoElement)]
18pub struct Editor {
19 state: Entity<EditorState>,
20 style: StyleRefinement,
21 height: Option<DefiniteLength>,
22 appearance: bool,
23 bordered: bool,
24 disabled: bool,
25 readonly: bool,
26 tab_index: isize,
27 role: RoleOverride,
28 aria_label: Option<SharedString>,
29
30 context_menu_builder: Option<Rc<dyn Fn(NativeMenu, &mut Window, &mut App) -> NativeMenu>>,
34}
35
36impl Editor {
37 pub fn new(state: &Entity<EditorState>) -> Self {
38 Self {
39 state: state.clone(),
40 style: StyleRefinement::default(),
41 height: None,
42 appearance: true,
43 bordered: true,
44 disabled: false,
45 readonly: false,
46 tab_index: 0,
47 role: RoleOverride::default(),
48 aria_label: None,
49 context_menu_builder: None,
50 }
51 }
52
53 pub fn h(mut self, height: impl Into<DefiniteLength>) -> Self {
54 self.height = Some(height.into());
55 self
56 }
57
58 pub fn appearance(mut self, appearance: bool) -> Self {
59 self.appearance = appearance;
60 self
61 }
62
63 pub fn bordered(mut self, bordered: bool) -> Self {
64 self.bordered = bordered;
65 self
66 }
67
68 pub fn disabled(mut self, disabled: bool) -> Self {
69 self.disabled = disabled;
70 self
71 }
72
73 pub fn readonly(mut self, readonly: bool) -> Self {
79 self.readonly = readonly;
80 self
81 }
82
83 pub fn tab_index(mut self, index: isize) -> Self {
84 self.tab_index = index;
85 self
86 }
87
88 pub fn role(mut self, role: impl Into<RoleOverride>) -> Self {
89 self.role = role.into();
90 self
91 }
92
93 pub fn aria_label(mut self, label: impl Into<SharedString>) -> Self {
94 self.aria_label = Some(label.into());
95 self
96 }
97
98 pub fn context_menu(
103 mut self,
104 f: impl Fn(NativeMenu, &mut Window, &mut App) -> NativeMenu + 'static,
105 ) -> Self {
106 self.context_menu_builder = Some(Rc::new(f));
107 self
108 }
109}
110
111impl Styled for Editor {
112 fn style(&mut self) -> &mut StyleRefinement {
113 &mut self.style
114 }
115}
116
117impl RenderOnce for Editor {
118 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
119 Input::from_state(self.state.clone())
120 .font_family(cx.theme().mono_font_family.clone())
125 .text_size(cx.theme().mono_font_size)
126 .line_height(relative(EDITOR_LINE_HEIGHT))
127 .appearance(self.appearance)
128 .bordered(self.bordered)
129 .focus_bordered(false)
130 .disabled(self.disabled)
131 .readonly(self.readonly)
132 .tab_index(self.tab_index)
133 .role(self.role)
134 .when_some(self.height, |this, height| this.h(height))
135 .when_some(self.aria_label, |this, label| this.aria_label(label))
136 .when_some(self.context_menu_builder, |this, build| {
137 this.context_menu(move |menu, window, cx| build(menu, window, cx))
138 })
139 .refine_style(&self.style)
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146 use crate::input::EditorState;
147 use gpui::{
148 AppContext as _, Context, ParentElement as _, Pixels, Render, TestAppContext,
149 VisualTestContext, div, px,
150 };
151
152 struct Harness {
153 state: Entity<EditorState>,
154 text_size: Option<Pixels>,
156 }
157
158 impl Render for Harness {
159 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
160 div().size_full().child(
161 Editor::new(&self.state)
162 .when_some(self.text_size, |this, size| this.text_size(size)),
163 )
164 }
165 }
166
167 fn line_height(cx: &mut TestAppContext, text_size: Option<Pixels>) -> Pixels {
169 cx.update(crate::init);
170 let mut state = None;
171 let (_, cx) = cx.add_window_view(|window, cx| {
172 let editor = cx.new(|cx| EditorState::new(window, cx).default_value("fn main() {}"));
173 state = Some(editor.clone());
174 Harness {
175 state: editor,
176 text_size,
177 }
178 });
179 let state = state.unwrap();
180 VisualTestContext::update(cx, |window, cx| window.draw(cx).clear(cx));
181
182 cx.read(|cx| {
183 state
184 .read(cx)
185 .line_height()
186 .expect("the editor must lay out")
187 })
188 }
189
190 #[gpui::test]
191 fn the_rows_follow_the_font_size(cx: &mut TestAppContext) {
192 assert_eq!(line_height(cx, None), px(20.));
194 assert_eq!(line_height(cx, Some(px(24.))), px(36.));
196 assert_eq!(line_height(cx, Some(px(40.))), px(60.));
197 }
198}