authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-09-25 22:22:07-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-09-27 04:23:49-07:00
log263f6a1b7979ef2640fe4d7cfba53c4b9974b442
tree4508408f2d24eb501cc69404a9350872277eac42
parente0b4334a77134228b5f773ce7cb9227fc2dd027d
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU

feat: the toolbar formats the page

Undo and redo, bold, italic, underline, strikethrough, sub- and superscript, font and size lists over their boxes, font colour and highlight grids, bullets, numbering, indent, outdent, alignment and the To Do and Question tags act on the page, and toggles light while the selection has them. Assisted-by: claude-opus-5.5

7 files changed, 301 insertions(+), 51 deletions(-)

crates/snowbound/assets/icons/align-center.svg created+3
...@@ -0,0 +1,3 @@
1<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
2 <path d="M2.5 3.5H13.5M4.5 6.5H11.5M2.5 9.5H13.5M4.5 12.5H11.5" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
3</svg>
crates/snowbound/assets/icons/align-right.svg created+3
...@@ -0,0 +1,3 @@
1<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
2 <path d="M2.5 3.5H13.5M6.5 6.5H13.5M2.5 9.5H13.5M6.5 12.5H13.5" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
3</svg>
crates/snowbound/assets/icons/superscript.svg created+4
...@@ -0,0 +1,4 @@
1<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
2 <path d="M2.5 4.5L9.5 12.5M9.5 4.5L2.5 12.5" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
3 <path d="M11 2.5C11.2 1.4 13.6 1.3 13.6 2.6C13.6 3.8 11 4.3 11 5.5H13.8" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"/>
4</svg>
crates/snowbound/src/art.rs+3
...@@ -6,6 +6,8 @@ macro_rules! art {...@@ -6,6 +6,8 @@ macro_rules! art {
6 };6 };
7}7}
88
9pub const ALIGN_CENTER: &[&str] = art!("icons/align-center");
10pub const ALIGN_RIGHT: &[&str] = art!("icons/align-right");
9pub const ALIGN_LEFT: &[&str] = art!("icons/align-left");11pub const ALIGN_LEFT: &[&str] = art!("icons/align-left");
10pub const ATTACHMENT: &[&str] = art!("icons/attachment");12pub const ATTACHMENT: &[&str] = art!("icons/attachment");
11pub const BOLD: &[&str] = art!("icons/bold");13pub const BOLD: &[&str] = art!("icons/bold");
...@@ -30,6 +32,7 @@ pub const SIDEBAR_COLLAPSE: &[&str] = art!("icons/sidebar-collapse");...@@ -30,6 +32,7 @@ pub const SIDEBAR_COLLAPSE: &[&str] = art!("icons/sidebar-collapse");
30pub const SIDEBAR_EXPAND: &[&str] = art!("icons/sidebar-expand");32pub const SIDEBAR_EXPAND: &[&str] = art!("icons/sidebar-expand");
31pub const STRIKETHROUGH: &[&str] = art!("icons/strikethrough");33pub const STRIKETHROUGH: &[&str] = art!("icons/strikethrough");
32pub const SUBSCRIPT: &[&str] = art!("icons/subscript");34pub const SUBSCRIPT: &[&str] = art!("icons/subscript");
35pub const SUPERSCRIPT: &[&str] = art!("icons/superscript");
33pub const TABLE: &[&str] = art!("icons/table");36pub const TABLE: &[&str] = art!("icons/table");
34pub const UNDERLINE: &[&str] = art!("icons/underline");37pub const UNDERLINE: &[&str] = art!("icons/underline");
35pub const UNDO: &[&str] = art!("icons/undo");38pub const UNDO: &[&str] = art!("icons/undo");
crates/snowbound/src/main.rs+278-48
...@@ -9,7 +9,10 @@ use canvas::{...@@ -9,7 +9,10 @@ use canvas::{
9 editor::{CanvasEditor, DEFAULT_OUTLINE_WIDTH, TextOutline},9 editor::{CanvasEditor, DEFAULT_OUTLINE_WIDTH, TextOutline},
10 layout::TextEngine,10 layout::TextEngine,
11};11};
12use canvas::{gpu::tag_sources, outline::TagIcon};12use canvas::{
13 gpu::{colorref, tag_sources},
14 outline::TagIcon,
15};
13use draw::Renderer;16use draw::Renderer;
14use onestore::ExGuid;17use onestore::ExGuid;
15use onestore::document::Format;18use onestore::document::Format;
...@@ -42,6 +45,22 @@ const FRAME: f32 = 6.0;...@@ -42,6 +45,22 @@ const FRAME: f32 = 6.0;
42/// macOS rounds windows' corners by 10 pt; the page's corners share their centres.45/// macOS rounds windows' corners by 10 pt; the page's corners share their centres.
43const ROUNDING: f32 = 10.0 - FRAME;46const ROUNDING: f32 = 10.0 - FRAME;
44const PAGE_LIST: f32 = 240.0;47const PAGE_LIST: f32 = 240.0;
48/// Font sizes the size box offers, OneNote's list in points.
49const SIZES: [f32; 17] = [
50 8.0, 9.0, 10.0, 10.5, 11.0, 12.0, 14.0, 16.0, 18.0, 20.0, 22.0, 24.0, 26.0, 28.0, 36.0, 48.0,
51 72.0,
52];
53/// OneNote's highlight colours, COLORREF.
54const HIGHLIGHTS: &[u32] = &[
55 0x00ffff, 0x00ff00, 0xffff00, 0xff00ff, 0xff0000, 0x0000ff, 0x800000, 0x808000, 0x008000,
56 0x800080, 0x000080, 0x008080, 0x808080, 0xc0c0c0, 0x000000,
57];
58/// Office's theme and standard font colours, COLORREF.
59const FONT_COLORS: [u32; 20] = [
60 0xffffff, 0x000000, 0xe1ecee, 0x7d491f, 0xbd814f, 0x4d50c0, 0x59bb9b, 0xa26480, 0xc6ac4b,
61 0x4696f7, 0x0000c0, 0x0000ff, 0x00c0ff, 0x00ffff, 0x50d092, 0x50b000, 0xf0b000, 0xc07000,
62 0x602000, 0xa03070,
63];
45/// Height of a page tab's row, whose tab leaves `ROW_GAP` below it so tabs stand apart64/// Height of a page tab's row, whose tab leaves `ROW_GAP` below it so tabs stand apart
46/// while the gaps still take the pointer.65/// while the gaps still take the pointer.
47const ROW: f32 = 29.0;66const ROW: f32 = 29.0;
...@@ -235,6 +254,8 @@ struct State {...@@ -235,6 +254,8 @@ struct State {
235 filter: String,254 filter: String,
236 /// Whether the page list is shown beside the page.255 /// Whether the page list is shown beside the page.
237 pages_open: bool,256 pages_open: bool,
257 /// Installed font families, for the font box; listing them is slow.
258 fonts: Vec<String>,
238 /// The application's icon at the display's scale, for the title bar.259 /// The application's icon at the display's scale, for the title bar.
239 app_icon: Option<draw::RasterImage>,260 app_icon: Option<draw::RasterImage>,
240 commands: Vec<Command>,261 commands: Vec<Command>,
...@@ -426,6 +447,14 @@ impl State {...@@ -426,6 +447,14 @@ impl State {
426 eprintln!("Using {family} for the interface");447 eprintln!("Using {family} for the interface");
427 }448 }
428 ui.set_focus(Some(page()));449 ui.set_focus(Some(page()));
450 let mut fonts: Vec<String> = engine
451 .fonts
452 .collection
453 .family_names()
454 .map(String::from)
455 .collect();
456 fonts.sort_unstable_by_key(|name| name.to_lowercase());
457 fonts.dedup();
429 let state = Self {458 let state = Self {
430 window,459 window,
431 redraw: Arc::new(Redraw(proxy.clone())).into(),460 redraw: Arc::new(Redraw(proxy.clone())).into(),
...@@ -447,6 +476,7 @@ impl State {...@@ -447,6 +476,7 @@ impl State {
447 session,476 session,
448 filter: String::new(),477 filter: String::new(),
449 pages_open: true,478 pages_open: true,
479 fonts,
450 app_icon: macos::app_icon((16.0 * dpr).round() as u32),480 app_icon: macos::app_icon((16.0 * dpr).round() as u32),
451 commands: Vec::new(),481 commands: Vec::new(),
452 changed: false,482 changed: false,
...@@ -813,8 +843,27 @@ impl State {...@@ -813,8 +843,27 @@ impl State {
813 /// Two rows of formatting, tag, insert and zoom buttons, grouped as OneNote's Home843 /// Two rows of formatting, tag, insert and zoom buttons, grouped as OneNote's Home
814 /// ribbon groups them.844 /// ribbon groups them.
815 fn toolbar(&mut self, theme: &Theme) -> Result<(), Box<dyn Error>> {845 fn toolbar(&mut self, theme: &Theme) -> Result<(), Box<dyn Error>> {
846 use canvas::editor::{Alignment, FormatState, Formatting, NoteTag, Toggle};
847 // A focused picture has no text to show a format for.
848 let state = self
849 .view
850 .editor
851 .format_state()
852 .unwrap_or_else(|_| FormatState {
853 toggles: Vec::new(),
854 font: None,
855 font_size: None,
856 alignment: None,
857 bullets: false,
858 numbering: false,
859 tags: Vec::new(),
860 });
861 let fonts = &self.fonts;
816 let ui = &mut self.ui;862 let ui = &mut self.ui;
817 let text = theme.text;863 let text = theme.text;
864 let on = |toggle| state.toggles.contains(&toggle);
865 let mut command = None;
866 let mut history = None;
818 ui.open(867 ui.open(
819 "toolbar",868 "toolbar",
820 Spec {869 Spec {
...@@ -826,65 +875,232 @@ impl State {...@@ -826,65 +875,232 @@ impl State {
826 },875 },
827 );876 );
828 group(ui, "history", |ui| {877 group(ui, "history", |ui| {
829 ui::shell::tool_button(ui, "undo", art::UNDO, text);878 if ui::shell::tool_button(ui, "undo", art::UNDO, text, false).clicked {
830 ui::shell::tool_button(ui, "redo", art::REDO, text);879 history = Some(true);
880 }
881 if ui::shell::tool_button(ui, "redo", art::REDO, text, false).clicked {
882 history = Some(false);
883 }
831 });884 });
832 divider(ui, "history", theme);885 divider(ui, "history", theme);
886 let popup = |name: &str| Id::ROOT.child(("popup", name));
887 let font = state.font.clone().unwrap_or_default();
888 let size = state
889 .font_size
890 .map_or(String::new(), |size| format!("{size}"));
833 group(ui, "text", |ui| {891 group(ui, "text", |ui| {
834 row(ui, 0, |ui| {892 row(ui, 0, |ui| {
835 ui::shell::combo(ui, "font", "Calibri", 120.0);893 let combo = ui.id("font");
836 ui::shell::combo(ui, "size", "11", 44.0);894 if ui::shell::combo(ui, "font", &font, 120.0).clicked {
837 ui::shell::split_button(ui, "bullets", art::BULLETS, None);895 ui.open_popup(popup("font"));
838 ui::shell::split_button(ui, "numbering", art::NUMBERING, None);896 }
839 ui::shell::tool_button(ui, "clear", art::CLEAR_FORMATTING, text);897 let items: Vec<_> = fonts
898 .iter()
899 .map(|name| ui::popup::Item {
900 text: name,
901 checked: *name == font,
902 ..Default::default()
903 })
904 .collect();
905 let anchor = ui::Anchor::Over(ui.rect(combo).unwrap_or_default());
906 if let Some(index) = ui::popup::menu(ui, popup("font"), anchor, &items, Some(&font))
907 {
908 command = Some(Formatting::Font(fonts[index].clone()));
909 }
910 let combo = ui.id("size");
911 if ui::shell::combo(ui, "size", &size, 44.0).clicked {
912 ui.open_popup(popup("size"));
913 }
914 let labels = SIZES.map(|size| format!("{size}"));
915 let items: Vec<_> = labels
916 .iter()
917 .map(|label| ui::popup::Item {
918 text: label,
919 checked: *label == size,
920 ..Default::default()
921 })
922 .collect();
923 let anchor = ui::Anchor::Over(ui.rect(combo).unwrap_or_default());
924 if let Some(index) = ui::popup::menu(ui, popup("size"), anchor, &items, Some(&size))
925 {
926 command = Some(Formatting::FontSize(SIZES[index]));
927 }
928 let bullets =
929 ui::shell::split_button(ui, "bullets", art::BULLETS, None, state.bullets);
930 let numbering =
931 ui::shell::split_button(ui, "numbering", art::NUMBERING, None, state.numbering);
932 if bullets.iter().any(|signal| signal.clicked) {
933 command = Some(Formatting::Bullets);
934 }
935 if numbering.iter().any(|signal| signal.clicked) {
936 command = Some(Formatting::Numbering);
937 }
938 if ui::shell::tool_button(ui, "clear", art::CLEAR_FORMATTING, text, false).clicked {
939 command = Some(Formatting::Clear);
940 }
840 });941 });
841 row(ui, 1, |ui| {942 row(ui, 1, |ui| {
842 for (part, icon) in [943 for (part, icon, toggle) in [
843 ("bold", art::BOLD),944 ("bold", art::BOLD, Toggle::Bold),
844 ("italic", art::ITALIC),945 ("italic", art::ITALIC, Toggle::Italic),
845 ("underline", art::UNDERLINE),946 ("underline", art::UNDERLINE, Toggle::Underline),
846 ("strikethrough", art::STRIKETHROUGH),947 ("strikethrough", art::STRIKETHROUGH, Toggle::Strikethrough),
847 ] {948 ] {
848 ui::shell::tool_button(ui, part, icon, text);949 if ui::shell::tool_button(ui, part, icon, text, on(toggle)).clicked {
950 command = Some(Formatting::Toggle(toggle));
951 }
849 }952 }
850 ui::shell::split_button(ui, "script", art::SUBSCRIPT, None);953 let script = ui.id("script");
851 ui::shell::split_button(954 let superscript = on(Toggle::Superscript);
852 ui,955 let [button, menu] = ui::shell::split_button(
853 "highlight",
854 art::HIGHLIGHTER,
855 Some(draw::srgb(0xff, 0xff, 0x00)),
856 );
857 ui::shell::split_button(
858 ui,956 ui,
859 "color",957 "script",
860 art::FONT_COLOR,958 if superscript {
861 Some(draw::srgb(0xe8, 0x3a, 0x30)),959 art::SUPERSCRIPT
960 } else {
961 art::SUBSCRIPT
962 },
963 None,
964 on(Toggle::Subscript) || superscript,
862 );965 );
863 ui::shell::tool_button(ui, "outdent", art::OUTDENT, text);966 if button.clicked {
864 ui::shell::tool_button(ui, "indent", art::INDENT, text);967 command = Some(Formatting::Toggle(if superscript {
865 ui::shell::split_button(ui, "align", art::ALIGN_LEFT, None);968 Toggle::Superscript
969 } else {
970 Toggle::Subscript
971 }));
972 }
973 if menu.clicked {
974 ui.open_popup(popup("script"));
975 }
976 let items = [
977 ui::popup::Item {
978 text: "Subscript",
979 icon: Some(art::SUBSCRIPT),
980 shortcut: "โŒ˜=",
981 checked: on(Toggle::Subscript),
982 ..Default::default()
983 },
984 ui::popup::Item {
985 text: "Superscript",
986 icon: Some(art::SUPERSCRIPT),
987 shortcut: "โ‡งโŒ˜=",
988 checked: superscript,
989 ..Default::default()
990 },
991 ];
992 let anchor = ui::Anchor::Below(ui.rect(script).unwrap_or_default());
993 if let Some(index) = ui::popup::menu(ui, popup("script"), anchor, &items, None) {
994 command = Some(Formatting::Toggle(
995 [Toggle::Subscript, Toggle::Superscript][index],
996 ));
997 }
998 for (part, icon, swatches, columns, none, default) in [
999 (
1000 "highlight",
1001 art::HIGHLIGHTER,
1002 HIGHLIGHTS,
1003 5,
1004 "No colour",
1005 0x00ffff,
1006 ),
1007 (
1008 "color",
1009 art::FONT_COLOR,
1010 &FONT_COLORS[..],
1011 10,
1012 "Automatic",
1013 0x3a3ae8,
1014 ),
1015 ] {
1016 let split = ui.id(part);
1017 let [button, menu] =
1018 ui::shell::split_button(ui, part, icon, Some(colorref(default)), false);
1019 let paint = |color| {
1020 if part == "highlight" {
1021 Formatting::Highlight(color)
1022 } else {
1023 Formatting::Color(color)
1024 }
1025 };
1026 if button.clicked {
1027 command = Some(paint(Some(default)));
1028 }
1029 if menu.clicked {
1030 ui.open_popup(popup(part));
1031 }
1032 let colors: Vec<_> = swatches.iter().map(|color| colorref(*color)).collect();
1033 let anchor = ui::Anchor::Below(ui.rect(split).unwrap_or_default());
1034 if let Some(chosen) =
1035 ui::popup::colors(ui, popup(part), anchor, none, &colors, columns)
1036 {
1037 command = Some(paint(chosen.and_then(|chosen| {
1038 swatches
1039 .iter()
1040 .zip(&colors)
1041 .find(|(_, color)| **color == chosen)
1042 .map(|(stored, _)| *stored)
1043 })));
1044 }
1045 }
1046 if ui::shell::tool_button(ui, "outdent", art::OUTDENT, text, false).clicked {
1047 command = Some(Formatting::Outdent);
1048 }
1049 if ui::shell::tool_button(ui, "indent", art::INDENT, text, false).clicked {
1050 command = Some(Formatting::Indent);
1051 }
1052 let align = ui.id("align");
1053 let alignments = [
1054 (Alignment::Left, "Align left", art::ALIGN_LEFT),
1055 (Alignment::Center, "Centre", art::ALIGN_CENTER),
1056 (Alignment::Right, "Align right", art::ALIGN_RIGHT),
1057 ];
1058 let current = alignments
1059 .iter()
1060 .find(|(alignment, ..)| Some(*alignment) == state.alignment)
1061 .unwrap_or(&alignments[0]);
1062 let [button, menu] = ui::shell::split_button(ui, "align", current.2, None, false);
1063 if button.clicked || menu.clicked {
1064 ui.open_popup(popup("align"));
1065 }
1066 let items = alignments.map(|(alignment, name, icon)| ui::popup::Item {
1067 text: name,
1068 icon: Some(icon),
1069 checked: Some(alignment) == state.alignment,
1070 ..Default::default()
1071 });
1072 let anchor = ui::Anchor::Below(ui.rect(align).unwrap_or_default());
1073 if let Some(index) = ui::popup::menu(ui, popup("align"), anchor, &items, None) {
1074 command = Some(Formatting::Align(alignments[index].0));
1075 }
866 });1076 });
867 });1077 });
868 divider(ui, "text", theme);1078 divider(ui, "text", theme);
869 let tags = [1079 let tags: [(&[&str], Option<NoteTag>); 9] = [
870 tag_sources(TagIcon::CheckBox { checked: false }),1080 (
871 art::TAG_STAR,1081 tag_sources(TagIcon::CheckBox { checked: false }),
872 tag_sources(TagIcon::Question),1082 Some(NoteTag::ToDo),
873 art::TAG_REMEMBER,1083 ),
874 art::TAG_DEFINITION,1084 (art::TAG_STAR, None),
875 art::TAG_HIGHLIGHT,1085 (tag_sources(TagIcon::Question), Some(NoteTag::Question)),
876 art::TAG_CONTACT,1086 (art::TAG_REMEMBER, None),
877 art::TAG_ADDRESS,1087 (art::TAG_DEFINITION, None),
878 art::TAG_PHONE,1088 (art::TAG_HIGHLIGHT, None),
1089 (art::TAG_CONTACT, None),
1090 (art::TAG_ADDRESS, None),
1091 (art::TAG_PHONE, None),
879 ];1092 ];
880 group(ui, "tags", |ui| {1093 group(ui, "tags", |ui| {
881 for (index, tags) in tags.chunks(5).enumerate() {1094 for (index, tags) in tags.chunks(5).enumerate() {
882 row(ui, index, |ui| {1095 row(ui, index, |ui| {
883 for (column, icon) in tags.iter().enumerate() {1096 for (column, (icon, tag)) in tags.iter().enumerate() {
884 ui::shell::tool_button(ui, column, icon, [1.0; 4]);1097 let lit = tag.is_some_and(|tag| state.tags.contains(&tag));
1098 if ui::shell::tool_button(ui, column, icon, [1.0; 4], lit).clicked {
1099 command = tag.map(Formatting::Tag);
1100 }
885 }1101 }
886 if index == 1 {1102 if index == 1 {
887 ui::shell::tool_button(ui, "more", ui::shell::CHEVRON, text);1103 ui::shell::tool_button(ui, "more", ui::shell::CHEVRON, text, false);
888 }1104 }
889 });1105 });
890 }1106 }
...@@ -898,7 +1114,7 @@ impl State {...@@ -898,7 +1114,7 @@ impl State {
898 ("file", art::ATTACHMENT),1114 ("file", art::ATTACHMENT),
899 ("link", art::LINK),1115 ("link", art::LINK),
900 ] {1116 ] {
901 ui::shell::tool_button(ui, part, icon, text);1117 ui::shell::tool_button(ui, part, icon, text, false);
902 }1118 }
903 });1119 });
904 row(ui, 1, |ui| {1120 row(ui, 1, |ui| {
...@@ -907,7 +1123,7 @@ impl State {...@@ -907,7 +1123,7 @@ impl State {
907 ("time", art::CLOCK),1123 ("time", art::CLOCK),
908 ("equation", art::EQUATION),1124 ("equation", art::EQUATION),
909 ] {1125 ] {
910 ui::shell::tool_button(ui, part, icon, text);1126 ui::shell::tool_button(ui, part, icon, text, false);
911 }1127 }
912 });1128 });
913 });1129 });
...@@ -923,7 +1139,7 @@ impl State {...@@ -923,7 +1139,7 @@ impl State {
923 let mut chosen = None;1139 let mut chosen = None;
924 group(ui, "zoom", |ui| {1140 group(ui, "zoom", |ui| {
925 row(ui, 0, |ui| {1141 row(ui, 0, |ui| {
926 if ui::shell::tool_button(ui, "out", art::ZOOM_OUT, text).clicked {1142 if ui::shell::tool_button(ui, "out", art::ZOOM_OUT, text, false).clicked {
927 chosen = Some(zoom / 1.1);1143 chosen = Some(zoom / 1.1);
928 }1144 }
929 let level = ui.leaf(1145 let level = ui.leaf(
...@@ -941,7 +1157,7 @@ impl State {...@@ -941,7 +1157,7 @@ impl State {
941 if level.clicked {1157 if level.clicked {
942 chosen = Some(1.0);1158 chosen = Some(1.0);
943 }1159 }
944 if ui::shell::tool_button(ui, "in", art::ZOOM_IN, text).clicked {1160 if ui::shell::tool_button(ui, "in", art::ZOOM_IN, text, false).clicked {
945 chosen = Some(zoom * 1.1);1161 chosen = Some(zoom * 1.1);
946 }1162 }
947 });1163 });
...@@ -951,6 +1167,19 @@ impl State {...@@ -951,6 +1167,19 @@ impl State {
951 let response = self.view.set_zoom(zoom)?;1167 let response = self.view.set_zoom(zoom)?;
952 self.respond(response);1168 self.respond(response);
953 }1169 }
1170 if let Some(command) = command {
1171 let response = self.view.format(command)?;
1172 self.respond(response);
1173 }
1174 if let Some(undo) = history {
1175 let view = &mut self.view;
1176 let done = if undo {
1177 view.editor.undo(&mut view.engine)?
1178 } else {
1179 view.editor.redo(&mut view.engine)?
1180 };
1181 self.changed |= done;
1182 }
954 Ok(())1183 Ok(())
955 }1184 }
9561185
...@@ -1001,7 +1230,8 @@ impl State {...@@ -1001,7 +1230,8 @@ impl State {
1001 },1230 },
1002 );1231 );
1003 if !self.filter.is_empty()1232 if !self.filter.is_empty()
1004 && ui::shell::tool_button(&mut self.ui, "clear", art::CLOSE, theme.text_dim).clicked1233 && ui::shell::tool_button(&mut self.ui, "clear", art::CLOSE, theme.text_dim, false)
1234 .clicked
1005 {1235 {
1006 self.filter.clear();1236 self.filter.clear();
1007 }1237 }
...@@ -1022,13 +1252,13 @@ impl State {...@@ -1022,13 +1252,13 @@ impl State {
1022 self.ui.set_focus(Some(page()));1252 self.ui.set_focus(Some(page()));
1023 }1253 }
1024 }1254 }
1025 ui::shell::tool_button(&mut self.ui, "new", art::NEW_PAGE, theme.text);1255 ui::shell::tool_button(&mut self.ui, "new", art::NEW_PAGE, theme.text, false);
1026 let toggle = if self.pages_open {1256 let toggle = if self.pages_open {
1027 art::SIDEBAR_COLLAPSE1257 art::SIDEBAR_COLLAPSE
1028 } else {1258 } else {
1029 art::SIDEBAR_EXPAND1259 art::SIDEBAR_EXPAND
1030 };1260 };
1031 if ui::shell::tool_button(&mut self.ui, "toggle", toggle, theme.text).clicked {1261 if ui::shell::tool_button(&mut self.ui, "toggle", toggle, theme.text, false).clicked {
1032 self.pages_open = !self.pages_open;1262 self.pages_open = !self.pages_open;
1033 }1263 }
1034 self.ui.close();1264 self.ui.close();
crates/ui/examples/popups.rs+1
...@@ -343,6 +343,7 @@ impl Scene {...@@ -343,6 +343,7 @@ impl Scene {
343 "color",343 "color",
344 art!("icons/font-color"),344 art!("icons/font-color"),
345 Some(draw::srgb(0xe8, 0x3a, 0x30)),345 Some(draw::srgb(0xe8, 0x3a, 0x30)),
346 false,
346 );347 );
347 ui.close();348 ui.close();
348 ui.close();349 ui.close();
crates/ui/src/shell.rs+9-3
...@@ -100,12 +100,14 @@ fn lean(height: f32) -> f32 {...@@ -100,12 +100,14 @@ fn lean(height: f32) -> f32 {
100 (height - 2.0) / 2.0100 (height - 2.0) / 2.0
101}101}
102102
103/// A square button showing `icon` tinted by `tint`; artwork in its own colours takes white.103/// A square button showing `icon` tinted by `tint`, lit while `on`; artwork in its own
104/// colours takes white.
104pub fn tool_button(105pub fn tool_button(
105 ui: &mut Ui,106 ui: &mut Ui,
106 part: impl Hash,107 part: impl Hash,
107 icon: &'static [&'static str],108 icon: &'static [&'static str],
108 tint: [f32; 4],109 tint: [f32; 4],
110 on: bool,
109) -> Signal {111) -> Signal {
110 let hover = ui.theme.hover();112 let hover = ui.theme.hover();
111 ui.leaf(113 ui.leaf(
...@@ -115,6 +117,7 @@ pub fn tool_button(...@@ -115,6 +117,7 @@ pub fn tool_button(
115 size: [px(TOOL), px(TOOL)],117 size: [px(TOOL), px(TOOL)],
116 icon: Some(icon),118 icon: Some(icon),
117 color: Some(tint),119 color: Some(tint),
120 fill: on.then_some(hover),
118 hover_fill: Some(hover),121 hover_fill: Some(hover),
119 radius: 4.0,122 radius: 4.0,
120 center: true,123 center: true,
...@@ -123,13 +126,15 @@ pub fn tool_button(...@@ -123,13 +126,15 @@ pub fn tool_button(
123 )126 )
124}127}
125128
126/// A button showing `icon` with a menu arrow beside it, and optionally the colour it129/// A button showing `icon`, lit while `on`, with a menu arrow beside it, and optionally
127/// applies as a bar under the icon. Returns the button's and the arrow's signals.130/// the colour it applies as a bar under the icon. Returns the button's and the arrow's
131/// signals.
128pub fn split_button(132pub fn split_button(
129 ui: &mut Ui,133 ui: &mut Ui,
130 part: impl Hash,134 part: impl Hash,
131 icon: &'static [&'static str],135 icon: &'static [&'static str],
132 bar: Option<[f32; 4]>,136 bar: Option<[f32; 4]>,
137 on: bool,
133) -> [Signal; 2] {138) -> [Signal; 2] {
134 let theme = ui.theme.clone();139 let theme = ui.theme.clone();
135 ui.open(140 ui.open(
...@@ -146,6 +151,7 @@ pub fn split_button(...@@ -146,6 +151,7 @@ pub fn split_button(
146 size: [px(TOOL), px(TOOL)],151 size: [px(TOOL), px(TOOL)],
147 icon: Some(icon),152 icon: Some(icon),
148 color: Some(theme.text),153 color: Some(theme.text),
154 fill: on.then(|| theme.hover()),
149 hover_fill: Some(theme.hover()),155 hover_fill: Some(theme.hover()),
150 radius: 4.0,156 radius: 4.0,
151 center: true,157 center: true,