Skip to main content

egui_charts/ui/profile/
mod.rs

1//! Profile details and follow controls for a host's [`ProfileDirectory`].
2
3use crate::localization as l10n;
4use crate::services::{FollowStatus, ProfileDirectory, ProfileId};
5use crate::theme::ThemeExt;
6use crate::ui_kit::EmptyState;
7use crate::ui_kit::buttons::Button;
8use crate::ui_kit::status_message::{MessageKind, status_message};
9use egui::{RichText, Ui};
10
11/// A profile card that embeds in any layout and sends follow requests to the directory's
12/// [`Profiles`](crate::services::Profiles) service. Details and follow state stay host-owned.
13/// Nothing is cached in the card, so replacing the directory's service clears what it shows.
14///
15/// Handle [`NotificationsAction::OpenProfile`] by selecting its profile in the host's layout, then
16/// call [`show`](Self::show) for it. Keep polling the directory every frame, including while
17/// the card is hidden. Install an egui image loader to display host-provided avatar URIs.
18///
19/// ```
20/// use egui_charts::services::{ProfileDirectory, ProfileId};
21/// use egui_charts::ui::profile::ProfileCard;
22///
23/// let mut directory = ProfileDirectory::new(); // Attach the host with set_service.
24/// let selected = ProfileId("ada".into());
25/// # egui::__run_test_ui(|ui| {
26/// directory.poll(ui.ctx());
27/// ProfileCard::new(&selected).show(ui, &mut directory);
28/// # });
29/// ```
30///
31/// [`NotificationsAction::OpenProfile`]: crate::ui::widget_bar::panels::notifications::NotificationsAction::OpenProfile
32pub struct ProfileCard<'a> {
33    id: &'a ProfileId,
34}
35
36impl<'a> ProfileCard<'a> {
37    /// Shows the profile with this host identifier, or an unavailable state until it is pushed.
38    pub fn new(id: &'a ProfileId) -> Self {
39        Self { id }
40    }
41
42    /// Draws the latest details, follow control and any pending request or host failure.
43    /// A failed request keeps the reported follow state and may be retried or dismissed.
44    pub fn show(self, ui: &mut Ui, directory: &mut ProfileDirectory) {
45        let Some(profile) = directory.profile(self.id) else {
46            EmptyState::new("Profile unavailable").show(ui);
47            return;
48        };
49        let theme = ui.active_theme();
50        let operation = directory.operation(self.id);
51        let pending = operation.is_some_and(|op| op.status == FollowStatus::Pending);
52        let mut follow = None;
53        let mut dismiss = false;
54        ui.push_id(("profile", self.id), |ui| {
55            ui.vertical(|ui| {
56                ui.horizontal_wrapped(|ui| {
57                    if let Some(avatar) = &profile.avatar {
58                        ui.add(
59                            egui::Image::new(avatar)
60                                .fit_to_exact_size(egui::vec2(48.0, 48.0))
61                                .alt_text(&profile.name)
62                                .corner_radius(24),
63                        );
64                    }
65                    ui.label(
66                        RichText::new(&profile.name)
67                            .size(theme.text.heading)
68                            .strong(),
69                    );
70                });
71                if !profile.bio.is_empty() {
72                    ui.label(&profile.bio);
73                }
74                if let Some(followers) = profile.followers {
75                    ui.label(
76                        RichText::new(l10n::format_text(
77                            ui.ctx(),
78                            "Followers: {count}",
79                            &[("count", followers.to_string())],
80                        ))
81                        .color(theme.ui.text_secondary),
82                    );
83                }
84                ui.add_space(theme.spacing.sm);
85                ui.horizontal_wrapped(|ui| {
86                    let label = if pending {
87                        if operation.is_some_and(|op| op.follow) {
88                            "Following…"
89                        } else {
90                            "Unfollowing…"
91                        }
92                    } else if profile.followed {
93                        "Unfollow"
94                    } else {
95                        "Follow"
96                    };
97                    if Button::new(label)
98                        .primary()
99                        .enabled(!pending)
100                        .show(ui)
101                        .clicked()
102                    {
103                        follow = Some(!profile.followed);
104                    }
105                    if pending {
106                        ui.spinner();
107                    }
108                });
109                if let Some(operation) = operation
110                    && let FollowStatus::Failed(message) = &operation.status
111                {
112                    status_message(ui, MessageKind::Error, message);
113                    dismiss = Button::new("Dismiss").small().show(ui).clicked();
114                }
115            });
116        });
117        if let Some(follow) = follow {
118            directory.follow(self.id, follow);
119        } else if dismiss {
120            directory.dismiss(self.id);
121        }
122    }
123}
124
125#[cfg(test)]
126mod tests;