components.rs

  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}