1use button_component::Button;
2
3use component::AdaptComponent;
4use gpui::{
5 color::Color,
6 elements::{ContainerStyle, Flex, Label, ParentElement},
7 fonts::{self, TextStyle},
8 platform::WindowOptions,
9 AnyElement, App, Element, Entity, View, ViewContext,
10};
11use log::LevelFilter;
12use pathfinder_geometry::vector::vec2f;
13use simplelog::SimpleLogger;
14use theme::Toggleable;
15use toggleable_button::ToggleableButton;
16
17fn main() {
18 SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger");
19
20 App::new(()).unwrap().run(|cx| {
21 cx.platform().activate(true);
22 cx.add_window(WindowOptions::with_bounds(vec2f(300., 200.)), |_| {
23 TestView {
24 count: 0,
25 is_doubling: false,
26 }
27 });
28 });
29}
30
31pub struct TestView {
32 count: usize,
33 is_doubling: bool,
34}
35
36impl TestView {
37 fn increase_count(&mut self) {
38 if self.is_doubling {
39 self.count *= 2;
40 } else {
41 self.count += 1;
42 }
43 }
44}
45
46impl Entity for TestView {
47 type Event = ();
48}
49
50type ButtonStyle = ContainerStyle;
51
52impl View for TestView {
53 fn ui_name() -> &'static str {
54 "TestView"
55 }
56
57 fn render(&mut self, cx: &mut ViewContext<'_, '_, Self>) -> AnyElement<Self> {
58 fonts::with_font_cache(cx.font_cache.to_owned(), || {
59 Flex::column()
60 .with_child(Label::new(
61 format!("Count: {}", self.count),
62 TextStyle::for_color(Color::red()),
63 ))
64 .with_child(
65 Button::new(move |_, v: &mut Self, cx| {
66 v.increase_count();
67 cx.notify();
68 })
69 .with_text(
70 "Hello from a counting BUTTON",
71 TextStyle::for_color(Color::blue()),
72 )
73 .with_style(ButtonStyle::fill(Color::yellow()))
74 .into_element(),
75 )
76 .with_child(
77 ToggleableButton::new(self.is_doubling, move |_, v: &mut Self, cx| {
78 v.is_doubling = !v.is_doubling;
79 cx.notify();
80 })
81 .with_text("Double the count?", TextStyle::for_color(Color::black()))
82 .with_style(Toggleable {
83 inactive: ButtonStyle::fill(Color::red()),
84 active: ButtonStyle::fill(Color::green()),
85 })
86 .into_element(),
87 )
88 .expanded()
89 .contained()
90 .with_background_color(Color::white())
91 .into_any()
92 })
93 }
94}
95
96mod theme {
97 pub struct Toggleable<T> {
98 pub inactive: T,
99 pub active: T,
100 }
101
102 impl<T> Toggleable<T> {
103 pub fn style_for(&self, active: bool) -> &T {
104 if active {
105 &self.active
106 } else {
107 &self.inactive
108 }
109 }
110 }
111}
112
113// Component creation:
114mod toggleable_button {
115 use gpui::{
116 elements::{ContainerStyle, LabelStyle},
117 scene::MouseClick,
118 EventContext, View,
119 };
120
121 use crate::{button_component::Button, component::Component, theme::Toggleable};
122
123 pub struct ToggleableButton<V: View> {
124 active: bool,
125 style: Option<Toggleable<ContainerStyle>>,
126 button: Button<V>,
127 }
128
129 impl<V: View> ToggleableButton<V> {
130 pub fn new<F>(active: bool, on_click: F) -> Self
131 where
132 F: Fn(MouseClick, &mut V, &mut EventContext<V>) + 'static,
133 {
134 Self {
135 active,
136 button: Button::new(on_click),
137 style: None,
138 }
139 }
140
141 pub fn with_text(self, text: &str, style: impl Into<LabelStyle>) -> ToggleableButton<V> {
142 ToggleableButton {
143 active: self.active,
144 style: self.style,
145 button: self.button.with_text(text, style),
146 }
147 }
148
149 pub fn with_style(self, style: Toggleable<ContainerStyle>) -> ToggleableButton<V> {
150 ToggleableButton {
151 active: self.active,
152 style: Some(style),
153 button: self.button,
154 }
155 }
156 }
157
158 impl<V: View> Component for ToggleableButton<V> {
159 type View = V;
160
161 fn render(
162 self,
163 v: &mut Self::View,
164 cx: &mut gpui::ViewContext<Self::View>,
165 ) -> gpui::AnyElement<V> {
166 let button = if let Some(style) = self.style {
167 self.button.with_style(*style.style_for(self.active))
168 } else {
169 self.button
170 };
171 button.render(v, cx)
172 }
173 }
174}
175
176mod button_component {
177
178 use gpui::{
179 elements::{ContainerStyle, Label, LabelStyle, MouseEventHandler},
180 platform::MouseButton,
181 scene::MouseClick,
182 AnyElement, Element, EventContext, TypeTag, View, ViewContext,
183 };
184
185 use crate::component::Component;
186
187 type ClickHandler<V> = Box<dyn Fn(MouseClick, &mut V, &mut EventContext<V>)>;
188
189 pub struct Button<V: View> {
190 click_handler: ClickHandler<V>,
191 tag: TypeTag,
192 contents: Option<AnyElement<V>>,
193 style: Option<ContainerStyle>,
194 }
195
196 impl<V: View> Button<V> {
197 pub fn new<F: Fn(MouseClick, &mut V, &mut EventContext<V>) + 'static>(handler: F) -> Self {
198 Self {
199 click_handler: Box::new(handler),
200 tag: TypeTag::new::<F>(),
201 style: None,
202 contents: None,
203 }
204 }
205
206 pub fn with_text(mut self, text: &str, style: impl Into<LabelStyle>) -> Self {
207 self.contents = Some(Label::new(text.to_string(), style).into_any());
208 self
209 }
210
211 pub fn _with_contents<E: Element<V>>(mut self, contents: E) -> Self {
212 self.contents = Some(contents.into_any());
213 self
214 }
215
216 pub fn with_style(mut self, style: ContainerStyle) -> Self {
217 self.style = Some(style);
218 self
219 }
220 }
221
222 impl<V: View> Component for Button<V> {
223 type View = V;
224
225 fn render(self, _: &mut Self::View, cx: &mut ViewContext<V>) -> AnyElement<Self::View> {
226 let click_handler = self.click_handler;
227
228 let result = MouseEventHandler::new_dynamic(self.tag, 0, cx, |_, _| {
229 self.contents
230 .unwrap_or_else(|| gpui::elements::Empty::new().into_any())
231 })
232 .on_click(MouseButton::Left, move |click, v, cx| {
233 click_handler(click, v, cx);
234 })
235 .contained();
236
237 let result = if let Some(style) = self.style {
238 result.with_style(style)
239 } else {
240 result
241 };
242
243 result.into_any()
244 }
245 }
246}
247
248mod component {
249
250 use gpui::{AnyElement, Element, View, ViewContext};
251 use pathfinder_geometry::vector::Vector2F;
252
253 // Public API:
254 pub trait Component {
255 type View: View;
256
257 fn render(
258 self,
259 v: &mut Self::View,
260 cx: &mut ViewContext<Self::View>,
261 ) -> AnyElement<Self::View>;
262 }
263
264 pub struct ComponentAdapter<E> {
265 component: Option<E>,
266 }
267
268 impl<E> ComponentAdapter<E> {
269 pub fn new(e: E) -> Self {
270 Self { component: Some(e) }
271 }
272 }
273
274 pub trait AdaptComponent<C: Component>: Sized {
275 fn into_element(self) -> ComponentAdapter<Self> {
276 ComponentAdapter::new(self)
277 }
278 }
279
280 impl<C: Component> AdaptComponent<C> for C {}
281
282 impl<C: Component + 'static> Element<C::View> for ComponentAdapter<C> {
283 type LayoutState = AnyElement<C::View>;
284
285 type PaintState = ();
286
287 fn layout(
288 &mut self,
289 constraint: gpui::SizeConstraint,
290 view: &mut C::View,
291 cx: &mut gpui::LayoutContext<C::View>,
292 ) -> (Vector2F, Self::LayoutState) {
293 let component = self.component.take().unwrap();
294 let mut element = component.render(view, cx.view_context());
295 let constraint = element.layout(constraint, view, cx);
296 (constraint, element)
297 }
298
299 fn paint(
300 &mut self,
301 scene: &mut gpui::SceneBuilder,
302 bounds: gpui::geometry::rect::RectF,
303 visible_bounds: gpui::geometry::rect::RectF,
304 layout: &mut Self::LayoutState,
305 view: &mut C::View,
306 cx: &mut gpui::PaintContext<C::View>,
307 ) -> Self::PaintState {
308 layout.paint(scene, bounds.origin(), visible_bounds, view, cx)
309 }
310
311 fn rect_for_text_range(
312 &self,
313 _: std::ops::Range<usize>,
314 _: gpui::geometry::rect::RectF,
315 _: gpui::geometry::rect::RectF,
316 _: &Self::LayoutState,
317 _: &Self::PaintState,
318 _: &C::View,
319 _: &ViewContext<C::View>,
320 ) -> Option<gpui::geometry::rect::RectF> {
321 todo!()
322 }
323
324 fn debug(
325 &self,
326 _: gpui::geometry::rect::RectF,
327 _: &Self::LayoutState,
328 _: &Self::PaintState,
329 _: &C::View,
330 _: &ViewContext<C::View>,
331 ) -> serde_json::Value {
332 todo!()
333 }
334 }
335}