logicaffeine_web/ui/components/
status_bar.rs1use dioxus::prelude::*;
6
7use super::problems::{Problem, Severity};
8
9#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
11pub struct EditorStatus {
12 pub caret: Caret,
13 pub problems: ProblemCount,
14}
15
16#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
18pub struct ProblemCount {
19 pub errors: usize,
20 pub warnings: usize,
21}
22
23impl ProblemCount {
24 pub fn of(problems: &[Problem]) -> ProblemCount {
26 let count = |severity: Severity| problems.iter().filter(|p| p.severity == severity).count();
27 ProblemCount { errors: count(Severity::Error), warnings: count(Severity::Warning) }
28 }
29}
30
31#[derive(Clone, Copy, Debug, PartialEq, Eq)]
34pub struct Caret {
35 pub line: usize,
36 pub column: usize,
37 pub selected: usize,
38}
39
40impl Default for Caret {
41 fn default() -> Self {
42 Caret { line: 1, column: 1, selected: 0 }
43 }
44}
45
46impl Caret {
47 pub fn of(text: &str, start: usize, end: usize, backward: bool) -> Caret {
51 let at = if backward { start } else { end };
52 let mut caret = Caret::default();
53 let mut offset = 0;
54 for c in text.chars() {
55 if offset >= at.max(end) {
56 break;
57 }
58 if offset < at {
59 if c == '\n' {
60 caret.line += 1;
61 caret.column = 1;
62 } else {
63 caret.column += 1;
64 }
65 }
66 if offset >= start && offset < end {
67 caret.selected += 1;
68 }
69 offset += c.len_utf16();
70 }
71 caret
72 }
73
74 pub fn label(&self) -> String {
76 let place = format!("Ln {}, Col {}", self.line, self.column);
77 if self.selected == 0 {
78 place
79 } else {
80 format!("{place} ({} selected)", self.selected)
81 }
82 }
83}
84
85pub const STATUS_BAR_STYLE: &str = r#"
86.studio-statusbar {
87 flex: none;
88 box-sizing: border-box;
89 height: 25px;
90 display: flex;
91 align-items: center;
92 justify-content: space-between;
93 gap: 16px;
94 padding: 0 12px;
95 border-top: 2px solid var(--studio-resizer);
96 background: var(--studio-panel-bg);
97 color: var(--studio-text-secondary);
98 font: 12px/1 -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
99 white-space: nowrap;
100 overflow: hidden;
101 user-select: none;
102}
103.statusbar-side {
104 display: flex;
105 align-items: center;
106 gap: 16px;
107 min-width: 0;
108}
109.statusbar-item {
110 display: inline-flex;
111 align-items: center;
112 gap: 4px;
113}
114.statusbar-count {
115 display: inline-flex;
116 align-items: center;
117 gap: 3px;
118}
119.statusbar-count svg {
120 width: 13px;
121 height: 13px;
122 flex: none;
123}
124.statusbar-count.errors svg { color: #f14c4c; }
125.statusbar-count.warnings svg { color: #cca700; }
126"#;
127
128const ERROR_MARK: &str = r#"<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" aria-hidden="true"><circle cx="8" cy="8" r="6.3"/><path d="M5.6 5.6l4.8 4.8M10.4 5.6l-4.8 4.8"/></svg>"#;
130const WARNING_MARK: &str = r#"<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round" aria-hidden="true"><path d="M8 2.2l6.3 11.3H1.7z"/><path d="M8 6.4v3.4M8 11.6v.2"/></svg>"#;
132
133#[component]
137pub fn StatusBar(status: Signal<EditorStatus>, language: String, indent: usize, problems: bool) -> Element {
138 let status = status();
139 let ProblemCount { errors, warnings } = status.problems;
140 let position = status.caret.label();
141 rsx! {
142 style { "{STATUS_BAR_STYLE}" }
143 footer { class: "studio-statusbar", role: "status",
144 div { class: "statusbar-side",
145 if problems {
146 span {
147 class: "statusbar-item",
148 "data-status": "problems",
149 "data-errors": "{errors}",
150 "data-warnings": "{warnings}",
151 title: "{errors} errors, {warnings} warnings",
152 span { class: "statusbar-count errors", span { dangerous_inner_html: ERROR_MARK } "{errors}" }
153 span { class: "statusbar-count warnings", span { dangerous_inner_html: WARNING_MARK } "{warnings}" }
154 }
155 }
156 }
157 div { class: "statusbar-side",
158 span { class: "statusbar-item", "data-status": "position", "{position}" }
159 span { class: "statusbar-item", "data-status": "indent", "Spaces: {indent}" }
160 span { class: "statusbar-item", "data-status": "language", "{language}" }
161 }
162 }
163 }
164}
165
166#[cfg(test)]
167#[path = "../../../../../crates/logicaffeine_tests/unit/logicaffeine_web/ui/components/status_bar/tests.rs"]
168mod tests;