1use egui::Pos2;
2use egui_remixicon::icons;
3use serde::{Deserialize, Serialize};
4
5const DEFAULT_SPACE: f32 = 4.;
6#[derive(Clone, Serialize, Deserialize, Debug)]
7pub struct CommentMessage {
8 pub id: egui::Id,
9 pub user: String,
10 pub text: String,
11}
12#[derive(Clone, Serialize, Deserialize, Debug)]
13pub struct Comment {
14 pub id: egui::Id,
15 pub rect: egui::Rect,
16 pub color: egui::Color32,
17 pub offset: Pos2,
18 pub anchor: Pos2,
19 pub size: Pos2,
20 pub annotation_id: egui::Id,
21 pub name: String,
22 pub visible: bool,
23 pub message_id_source: u64,
24 pub message_chain: Vec<CommentMessage>,
25 pub new_text: String,
26 pub save_text: Option<String>,
27 pub change: bool,
28}
29
30impl Comment {
31 pub(crate) fn new(id: egui::Id, annotation_id: egui::Id) -> Self {
32 Comment {
33 id,
34 annotation_id,
35 rect: egui::Rect::ZERO,
36 color: egui::Color32::WHITE,
37 offset: Pos2::ZERO,
38 anchor: Pos2::ZERO,
39 size: Pos2 { x: 100., y: 50. },
40 message_chain: Vec::new(),
41 new_text: String::new(),
42 name: String::new(),
43 visible: false,
44 message_id_source: 0,
45 save_text: None,
46 change: false,
47 }
48 }
49}
50
51impl egui::Widget for &mut Comment {
52 fn ui(self, ui: &mut egui::Ui) -> egui::Response {
53 let mut layout_rect = self.rect;
57 layout_rect.set_height(2000.0);
58
59 let inner = ui.scope_builder(egui::UiBuilder::new().max_rect(layout_rect), |ui| {
61 let line_start = self.rect.left_top();
62
63 ui.painter().add(egui::Shape::dashed_line(
65 &[line_start, self.anchor],
66 egui::Stroke::new(1.0, egui::Color32::GRAY),
67 4.0,
68 2.0,
69 ));
70
71 let background_fill_shape = ui.painter().add(egui::Shape::Noop);
75 let background_stroke_shape = ui.painter().add(egui::Shape::Noop);
76
77 let mut state = egui::collapsing_header::CollapsingState::load_with_default_open(
80 ui.ctx(),
81 ui.make_persistent_id(self.id),
82 false,
83 );
84
85 let header = ui.horizontal(|ui| {
87 state.show_toggle_button(ui, |ui, _openness, response| {
89 let icon = icons::QUESTION_ANSWER_FILL;
90 ui.painter().text(
91 response.rect.center(),
92 egui::Align2::CENTER_CENTER,
93 icon,
94 egui::FontId::proportional(20.0),
95 ui.visuals().text_color(),
96 );
97 });
98
99 if state.is_open() {
101 ui.add_space(DEFAULT_SPACE);
102 ui.label(egui::RichText::new(&self.name).strong());
103 ui.add_space(DEFAULT_SPACE);
104 }
105 });
106
107 let mut text_response_rect = egui::Rect::ZERO;
108
109 state.show_body_unindented(ui, |ui| {
111 ui.vertical(|ui| {
112 ui.spacing_mut().item_spacing.y = DEFAULT_SPACE;
113
114 ui.painter().add(egui::Shape::line_segment(
116 [ui.cursor().left_top(), ui.cursor().right_top()],
117 egui::Stroke::new(0.5, egui::Color32::WHITE),
118 ));
119
120 for comment in &self.message_chain {
122 ui.label(egui::RichText::new(&comment.text).size(14.0));
123 ui.painter().add(egui::Shape::line_segment(
124 [ui.cursor().left_top(), ui.cursor().right_top()],
125 egui::Stroke::new(0.5, egui::Color32::WHITE),
126 ));
127 }
128
129 ui.add_space(DEFAULT_SPACE);
130
131 let text_response = ui.add(
133 egui::TextEdit::multiline(&mut self.new_text)
134 .desired_rows(1)
135 .desired_width(self.size.x)
136 .hint_text("Comment..."),
137 );
138
139 text_response_rect = text_response.rect;
141
142 if text_response.has_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
144 let clean_text = self.new_text.trim().to_string();
145 if !clean_text.is_empty() {
146 self.message_id_source += 1;
147 self.save_text = Some(clean_text);
148 self.change = true;
149 }
150 self.new_text = String::new(); }
152 });
153 });
154
155 let final_rect = ui.min_rect();
158 let content_height = final_rect.height();
159
160 if self.size.y < content_height {
162 self.size.y = content_height;
163 }
164
165 let background_rect = final_rect.expand(5.0);
166
167 ui.painter().set(
170 background_fill_shape,
171 egui::Shape::rect_filled(background_rect, 4.0, egui::Color32::BLACK),
172 );
173 ui.painter().set(
174 background_stroke_shape,
175 egui::Shape::rect_stroke(
176 background_rect,
177 4.0,
178 egui::Stroke::new(1.0, egui::Color32::WHITE),
179 egui::StrokeKind::Middle,
180 ),
181 );
182
183 let handle_rect = egui::Rect::from_min_max(
187 background_rect.max - egui::vec2(15.0, 15.0),
188 background_rect.max,
189 );
190
191 let se_res = ui.interact(handle_rect, self.id.with("se_res"), egui::Sense::drag());
193
194 if se_res.hovered() || se_res.dragged() {
195 ui.ctx().set_cursor_icon(egui::CursorIcon::ResizeNwSe);
196 }
197
198 ui.painter().text(
200 handle_rect.center(),
201 egui::Align2::CENTER_CENTER,
202 icons::DRAG_MOVE_2_FILL,
203 egui::FontId::proportional(10.0),
204 ui.visuals().text_color().linear_multiply(0.4),
205 );
206
207 let can_drag_body = !se_res.dragged()
210 && !text_response_rect.contains(ui.ctx().pointer_hover_pos().unwrap_or_default());
211
212 let body_res = ui.interact(
213 background_rect,
214 self.id.with("body_res"),
215 if can_drag_body {
216 egui::Sense::drag()
217 } else {
218 egui::Sense::hover()
219 },
220 );
221
222 if body_res.dragged() {
223 let delta = body_res.drag_delta();
224 self.offset.x += delta.x;
225 self.offset.y += delta.y;
226 self.change = true;
227 }
228
229 let header_res = ui.interact(
231 header.response.rect,
232 self.id.with("head_res"),
233 egui::Sense::click_and_drag(),
234 );
235
236 if header_res.clicked() {
237 state.toggle(ui);
238 state.store(ui.ctx());
239 } else if header_res.dragged() {
240 self.offset.x += header_res.drag_delta().x;
241 self.offset.y += header_res.drag_delta().y;
242 self.change = true;
243 }
244
245 se_res | body_res | header_res
247 });
248
249 inner.inner
250 }
251}