components.rs

  1use button_component::Button;
  2
  3use gpui::{
  4    color::Color,
  5    elements::{ContainerStyle, Flex, Label, ParentElement, StatefulComponent},
  6    fonts::{self, TextStyle},
  7    platform::WindowOptions,
  8    AnyElement, App, Element, Entity, View, ViewContext,
  9};
 10use log::LevelFilter;
 11use pathfinder_geometry::vector::vec2f;
 12use simplelog::SimpleLogger;
 13use theme::Toggleable;
 14use toggleable_button::ToggleableButton;
 15
 16// cargo run -p gpui --example components
 17
 18fn main() {
 19    SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger");
 20
 21    App::new(()).unwrap().run(|cx| {
 22        cx.platform().activate(true);
 23        cx.add_window(WindowOptions::with_bounds(vec2f(300., 200.)), |_| {
 24            TestView {
 25                count: 0,
 26                is_doubling: false,
 27            }
 28        });
 29    });
 30}
 31
 32pub struct TestView {
 33    count: usize,
 34    is_doubling: bool,
 35}
 36
 37impl TestView {
 38    fn increase_count(&mut self) {
 39        if self.is_doubling {
 40            self.count *= 2;
 41        } else {
 42            self.count += 1;
 43        }
 44    }
 45}
 46
 47impl Entity for TestView {
 48    type Event = ();
 49}
 50
 51type ButtonStyle = ContainerStyle;
 52
 53impl View for TestView {
 54    fn ui_name() -> &'static str {
 55        "TestView"
 56    }
 57
 58    fn render(&mut self, cx: &mut ViewContext<'_, '_, Self>) -> AnyElement<Self> {
 59        fonts::with_font_cache(cx.font_cache.to_owned(), || {
 60            Flex::column()
 61                .with_child(Label::new(
 62                    format!("Count: {}", self.count),
 63                    TextStyle::for_color(Color::red()),
 64                ))
 65                .with_child(
 66                    Button::new(move |_, v: &mut Self, cx| {
 67                        v.increase_count();
 68                        cx.notify();
 69                    })
 70                    .with_text(
 71                        "Hello from a counting BUTTON",
 72                        TextStyle::for_color(Color::blue()),
 73                    )
 74                    .with_style(ButtonStyle::fill(Color::yellow()))
 75                    .element(),
 76                )
 77                .with_child(
 78                    ToggleableButton::new(self.is_doubling, move |_, v: &mut Self, cx| {
 79                        v.is_doubling = !v.is_doubling;
 80                        cx.notify();
 81                    })
 82                    .with_text("Double the count?", TextStyle::for_color(Color::black()))
 83                    .with_style(Toggleable {
 84                        inactive: ButtonStyle::fill(Color::red()),
 85                        active: ButtonStyle::fill(Color::green()),
 86                    })
 87                    .element(),
 88                )
 89                .expanded()
 90                .contained()
 91                .with_background_color(Color::white())
 92                .into_any()
 93        })
 94    }
 95}
 96
 97mod theme {
 98    pub struct Toggleable<T> {
 99        pub inactive: T,
100        pub active: T,
101    }
102
103    impl<T> Toggleable<T> {
104        pub fn style_for(&self, active: bool) -> &T {
105            if active {
106                &self.active
107            } else {
108                &self.inactive
109            }
110        }
111    }
112}
113
114// Component creation:
115mod toggleable_button {
116    use gpui::{
117        elements::{ContainerStyle, LabelStyle, StatefulComponent},
118        scene::MouseClick,
119        EventContext, View,
120    };
121
122    use crate::{button_component::Button, theme::Toggleable};
123
124    pub struct ToggleableButton<V: View> {
125        active: bool,
126        style: Option<Toggleable<ContainerStyle>>,
127        button: Button<V>,
128    }
129
130    impl<V: View> ToggleableButton<V> {
131        pub fn new<F>(active: bool, on_click: F) -> Self
132        where
133            F: Fn(MouseClick, &mut V, &mut EventContext<V>) + 'static,
134        {
135            Self {
136                active,
137                button: Button::new(on_click),
138                style: None,
139            }
140        }
141
142        pub fn with_text(self, text: &str, style: impl Into<LabelStyle>) -> ToggleableButton<V> {
143            ToggleableButton {
144                active: self.active,
145                style: self.style,
146                button: self.button.with_text(text, style),
147            }
148        }
149
150        pub fn with_style(self, style: Toggleable<ContainerStyle>) -> ToggleableButton<V> {
151            ToggleableButton {
152                active: self.active,
153                style: Some(style),
154                button: self.button,
155            }
156        }
157    }
158
159    impl<V: View> StatefulComponent<V> for ToggleableButton<V> {
160        fn render(self, v: &mut V, cx: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> {
161            let button = if let Some(style) = self.style {
162                self.button.with_style(*style.style_for(self.active))
163            } else {
164                self.button
165            };
166            button.render(v, cx)
167        }
168    }
169}
170
171mod button_component {
172
173    use gpui::{
174        elements::{ContainerStyle, Label, LabelStyle, MouseEventHandler, StatefulComponent},
175        platform::MouseButton,
176        scene::MouseClick,
177        AnyElement, Element, EventContext, TypeTag, View, ViewContext,
178    };
179
180    type ClickHandler<V> = Box<dyn Fn(MouseClick, &mut V, &mut EventContext<V>)>;
181
182    pub struct Button<V: View> {
183        click_handler: ClickHandler<V>,
184        tag: TypeTag,
185        contents: Option<AnyElement<V>>,
186        style: Option<ContainerStyle>,
187    }
188
189    impl<V: View> Button<V> {
190        pub fn new<F: Fn(MouseClick, &mut V, &mut EventContext<V>) + 'static>(handler: F) -> Self {
191            Self {
192                click_handler: Box::new(handler),
193                tag: TypeTag::new::<F>(),
194                style: None,
195                contents: None,
196            }
197        }
198
199        pub fn with_text(mut self, text: &str, style: impl Into<LabelStyle>) -> Self {
200            self.contents = Some(Label::new(text.to_string(), style).into_any());
201            self
202        }
203
204        pub fn _with_contents<E: Element<V>>(mut self, contents: E) -> Self {
205            self.contents = Some(contents.into_any());
206            self
207        }
208
209        pub fn with_style(mut self, style: ContainerStyle) -> Self {
210            self.style = Some(style);
211            self
212        }
213    }
214
215    impl<V: View> StatefulComponent<V> for Button<V> {
216        fn render(self, _: &mut V, cx: &mut ViewContext<V>) -> AnyElement<V> {
217            let click_handler = self.click_handler;
218
219            let result = MouseEventHandler::new_dynamic(self.tag, 0, cx, |_, _| {
220                self.contents
221                    .unwrap_or_else(|| gpui::elements::Empty::new().into_any())
222            })
223            .on_click(MouseButton::Left, move |click, v, cx| {
224                click_handler(click, v, cx);
225            })
226            .contained();
227
228            let result = if let Some(style) = self.style {
229                result.with_style(style)
230            } else {
231                result
232            };
233
234            result.into_any()
235        }
236    }
237}