From 63dc37c6038af01d420f6830ba9f54904ae61d52 Mon Sep 17 00:00:00 2001 From: Jose Gregorio Rincon Albarracin Date: Sun, 25 Oct 2026 23:14:06 -0500 Subject: [PATCH] feat(ui): a fixed Labeling tab for every label control on the ribbon After Style, the Labeling tab has the selected feature layer's label groups in the Labeling pane's tab order (Labels, Label Class, Text Symbol, Label Placement, Label Range), or a hint without one, then Map Labeling (Lock, Pause, View Unplaced, Convert), which needs no layer. Style and View lose their label groups, the label commands' locations move to Labeling, and current_tab opens the new tab for the old name. Closes #897. --- crates/engine/src/cmd/labels.rs | 20 +- crates/engine/src/cmd/layer.rs | 6 +- crates/engine/src/tests.rs | 2 +- crates/map/src/symbology.rs | 2 +- crates/ui-egui/src/lib.rs | 4 +- crates/ui-egui/src/ribbon.rs | 226 ++++++++++++---------- crates/ui-egui/src/scriptcomplete.rs | 2 +- crates/ui-egui/tests/measurement_units.rs | 2 +- crates/ui-egui/tests/smoke.rs | 11 +- docs/comparison/geolibre.md | 4 +- docs/comparison/qgis.md | 4 +- docs/contributing.md | 11 +- docs/user-guide/cartography.md | 10 +- docs/user-guide/index.md | 7 +- 14 files changed, 175 insertions(+), 136 deletions(-) diff --git a/crates/engine/src/cmd/labels.rs b/crates/engine/src/cmd/labels.rs index dc7893c72..108292589 100644 --- a/crates/engine/src/cmd/labels.rs +++ b/crates/engine/src/cmd/labels.rs @@ -1,5 +1,5 @@ -//! View › Labeling (lock, pause, unplaced labels, convert to annotation), the Style tab's label -//! visibility range and placement, and a layer's label classes (#618). +//! The Labeling tab (#897): Map Labeling (lock, pause, unplaced labels, convert to annotation), a +//! layer's label visibility range and placement, and its label classes (#618). use rustgis_data::{Dataset, Field, FieldType, Geometry, GeometryType, Value as Attr}; use rustgis_map::symbology::{MAX_LABEL_CLASSES, label_class_name}; @@ -12,28 +12,28 @@ use crate::{CmdResult, CommandSpec as C, Session}; pub fn commands() -> Vec { vec![ - C::new("labels.lock", "Lock", "View › Labeling", |s, p| { + C::new("labels.lock", "Lock", "Labeling › Map Labeling", |s, p| { let d = &mut s.map_mut().label_display; d.locked = b(p, "on").unwrap_or(!d.locked); Ok(json!({"locked": d.locked})) }) .view() .params(r#"{"on"?: bool}"#), - C::new("labels.pause", "Pause", "View › Labeling", |s, p| { + C::new("labels.pause", "Pause", "Labeling › Map Labeling", |s, p| { let d = &mut s.map_mut().label_display; d.paused = b(p, "on").unwrap_or(!d.paused); Ok(json!({"paused": d.paused})) }) .view() .params(r#"{"on"?: bool}"#), - C::new("labels.viewUnplaced", "View Unplaced", "View › Labeling", |s, p| { + C::new("labels.viewUnplaced", "View Unplaced", "Labeling › Map Labeling", |s, p| { let d = &mut s.map_mut().label_display; d.show_unplaced = b(p, "on").unwrap_or(!d.show_unplaced); Ok(json!({"showUnplaced": d.show_unplaced})) }) .view() .params(r#"{"on"?: bool}"#), - C::new("labels.fonts", "Label Fonts", "Style › Text Symbol", |_, p| { + C::new("labels.fonts", "Label Fonts", "View › Windows › Labeling", |_, p| { // The families a font picker lists, those containing `search` (in any case) if given. // While the app is still indexing them (commands run on its UI thread), none yet, // and `indexing` says so: asking again later lists them. @@ -45,16 +45,16 @@ pub fn commands() -> Vec { }) .view() .params(r#"{"search"?: text in the family name} → {"fonts": [the installed font families, by name], "indexing"?: true (still being indexed: none listed yet)}"#), - C::new("labels.convert", "Convert To Annotation", "View › Labeling", convert) + C::new("labels.convert", "Convert To Annotation", "Labeling › Map Labeling", convert) .when(need_feature_layer) .params(r#"{"layer"?, "name"?: string}"#), - C::new("labels.scaleRange", "Label Visibility Range", "Style › Label Range", scale_range) + C::new("labels.scaleRange", "Label Visibility Range", "Labeling › Label Range", scale_range) .when(need_feature_layer) .params(r#"{"layer"?, "class"?: name (none: the Default class, by whatever name it has), "min"?: scale|null (out beyond), "max"?: scale|null (in beyond), "clear"?: bool (clears the scale and distance limits, as Clear Limits does)}"#), - C::new("labels.distanceRange", "Label Distance Range", "Style › Label Range", distance_range) + C::new("labels.distanceRange", "Label Distance Range", "Labeling › Label Range", distance_range) .when(need_feature_layer) .params(r#"{"layer"?, "class"?: name (none: the Default class, by whatever name it has), "closest"?: metres|null, "farthest"?: metres|null, "clear"?: bool}"#), - C::new("labels.placement", "Label Placement", "Style › Label Placement", |s, p| { + C::new("labels.placement", "Label Placement", "Labeling › Label Placement", |s, p| { let code = ps(p, "position").ok_or("position is required")?; let pos = LabelPlacement::from_code(code).ok_or_else(|| format!("unknown placement '{code}'"))?; with_class(s, p, |c| c.placement = pos) diff --git a/crates/engine/src/cmd/layer.rs b/crates/engine/src/cmd/layer.rs index 17cde498b..9b7f76ebc 100644 --- a/crates/engine/src/cmd/layer.rs +++ b/crates/engine/src/cmd/layer.rs @@ -280,13 +280,13 @@ pub fn commands() -> Vec { }).params(r#"{"layer"?, "on"?: bool (toggles when omitted)}"#) .view() .when(need_layer), - C::new("labels.enable", "Label", "Style › Labels", |s, p| { + C::new("labels.enable", "Label", "Labeling › Labels", |s, p| { let on = b(p, "on"); with(s, p, |l| l.labels.enabled = on.unwrap_or(!l.labels.enabled)) }) .when(need_feature_layer) .params(r#"{"layer"?, "on"?: bool}"#), - C::new("labels.field", "Label Field", "Style › Label Class", |s, p| { + C::new("labels.field", "Label Field", "Labeling › Label Class", |s, p| { let name = s_req(p, "field")?.to_string(); let id = s.layer_id(p)?; let fname = s.map().layer(id).and_then(|l| l.dataset.field_index(&name).and_then(|i| l.dataset.fields.get(i)).map(|f| f.name.clone())); @@ -298,7 +298,7 @@ pub fn commands() -> Vec { }) }).params(r#"{"layer"?, "class"?: name (none: the Default class, by whatever name it has), "field": name}"#) .when(need_feature_layer), - C::new("labels.style", "Text Symbol", "Style › Text Symbol", |s, p| { + C::new("labels.style", "Text Symbol", "Labeling › Text Symbol", |s, p| { let size = f(p, "size").map(|v| v.clamp(*LABEL_SIZE.start(), *LABEL_SIZE.end()) as f32); let color = s.get_color(p, "color")?; let (halo, bold, italic) = (b(p, "halo"), b(p, "bold"), b(p, "italic")); diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 2841166b5..36c66f52b 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -1993,7 +1993,7 @@ fn identify_reports_every_overlapping_feature() { #[test] fn label_size_stays_within_what_text_symbol_shows() { - // Style › Text Symbol offers the range `labels.style` accepts (#457): 72 pt is kept, and + // Labeling › Text Symbol offers the range `labels.style` accepts (#457): 72 pt is kept, and // anything outside is brought back to the nearest end. let mut s = world(); let size = |s: &Session| s.map().layers.iter().find(|l| l.name == "Countries").map(|l| f64::from(l.labels.default.size)); diff --git a/crates/map/src/symbology.rs b/crates/map/src/symbology.rs index 16b306847..ef64be780 100644 --- a/crates/map/src/symbology.rs +++ b/crates/map/src/symbology.rs @@ -1486,7 +1486,7 @@ fn jenks(sorted: &[f64], k: usize) -> Vec { breaks } -/// The label sizes `labels.style` accepts, in points (Style › Text Symbol). +/// The label sizes `labels.style` accepts, in points (Labeling › Text Symbol). pub const LABEL_SIZE: std::ops::RangeInclusive = 4.0..=72.0; /// The most label classes a layer keeps besides its Default class. diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 2f2f77c93..a4861d226 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -1,5 +1,5 @@ -//! L6: the RustGIS desktop front end, built around a ribbon (Home, Data, Style, Edit, Analyze, -//! View, Publish), the Layers and Browser panes, a Tools pane, the map view, the +//! L6: the RustGIS desktop front end, built around a ribbon (Home, Data, Style, Labeling, Edit, +//! Analyze, View, Publish), the Layers and Browser panes, a Tools pane, the map view, the //! attribute table and the status bar. //! //! The UI is thin: it reads [`Session`] state and runs commands by id ([`RustGisApp::run`]). diff --git a/crates/ui-egui/src/ribbon.rs b/crates/ui-egui/src/ribbon.rs index 64845a858..4db1578eb 100644 --- a/crates/ui-egui/src/ribbon.rs +++ b/crates/ui-egui/src/ribbon.rs @@ -16,7 +16,7 @@ use crate::widgets::{ }; use crate::{RightPane, RustGisApp, icons}; -pub const TABS: [&str; 10] = ["Home", "Data", "Style", "Edit", "Analyze", "View", "Publish", "Plugins", "Settings", "Help"]; +pub const TABS: [&str; 11] = ["Home", "Data", "Style", "Labeling", "Edit", "Analyze", "View", "Publish", "Plugins", "Settings", "Help"]; /// Tabs that always show: Settings holds Customize › Ribbon, the way to show hidden tabs again. pub const FIXED_TABS: [&str; 2] = ["Settings", "Help"]; @@ -27,7 +27,7 @@ pub fn current_tab(name: &str) -> &str { let tab = match name { "Map" => "Home", "Insert" | "Raster Data" => "Data", - "Feature Layer" | "Labeling" | "Raster Layer" => "Style", + "Feature Layer" | "Raster Layer" => "Style", "Analysis" | "Imagery" => "Analyze", "Share" | "Layout" => "Publish", _ => name, @@ -104,6 +104,7 @@ pub fn show(app: &mut RustGisApp, ui: &mut Ui) { t if t.starts_with("plugin:") => plugin_tab(app, ui, t.trim_start_matches("plugin:")), "Data" => data_tab(app, ui), "Style" => style_tab(app, ui), + "Labeling" => labeling_tab(app, ui), "Edit" => edit_tab(app, ui), "Analyze" => analyze_tab(app, ui), "View" => view_tab(app, ui), @@ -948,20 +949,10 @@ fn feature_cache_group(app: &mut RustGisApp, ui: &mut Ui) { }); } -fn labeling_group(app: &mut RustGisApp, ui: &mut Ui) { - group(ui, "Labeling", |ui| { - let on = app.session.active_layer.and_then(|id| app.session.map().layer(id)).is_some_and(|l| l.labels.enabled); - big( - ui, - app, - if on { "label" } else { "label_off" }, - "Label", - "Turn labels on or off for the selected layer.", - "labels.enable", - Value::Null, - on, - false, - ); +/// The label settings of the whole map (#897): they work without a layer, so the Labeling tab +/// shows them whatever is selected. +fn map_labeling_group(app: &mut RustGisApp, ui: &mut Ui) { + group(ui, "Map Labeling", |ui| { let d = app.session.map().label_display; col(ui, |ui| { small( @@ -2095,7 +2086,6 @@ fn view_tab(app: &mut RustGisApp, ui: &mut Ui) { app.session.table_layer.is_some(), ); }); - labeling_group(app, ui); group(ui, "Navigation", |ui| { big(ui, app, "zoom_world", "World", "Zoom to the whole world.", "map.zoomWorld", Value::Null, false, false); col(ui, |ui| { @@ -2297,21 +2287,31 @@ fn data_source_group(app: &mut RustGisApp, ui: &mut Ui) { fn style_tab(app: &mut RustGisApp, ui: &mut Ui) { match active_layer(app) { Some(l) if l.is_raster() => raster_style_groups(app, ui), - Some(l) if l.geometry_type() != GeometryType::None => { - feature_style_groups(app, ui); - label_groups(app, ui); - } - _ => group(ui, "Layer", |ui| { - let t = Tokens::get(ui.ctx()); - ui.add_sized( - vec2(220.0, CONTENT_H), - egui::Label::new(egui::RichText::new(tl!("Select a layer in Layers to style it.")).color(t.text_dim)).wrap(), - ); - }), + Some(l) if l.geometry_type() != GeometryType::None => feature_style_groups(app, ui), + _ => layer_hint(ui, "Select a layer in Layers to style it."), } styles_group(app, ui); } +/// The Labeling tab (#897): how the map is labelled. The selected feature layer's groups, in the +/// order of the Labeling pane's tabs, then the settings of the whole map, which need no layer. +fn labeling_tab(app: &mut RustGisApp, ui: &mut Ui) { + match active_layer(app) { + Some(l) if l.is_raster() => layer_hint(ui, "Labels are drawn for feature layers, not rasters."), + Some(l) if l.geometry_type() != GeometryType::None => label_groups(app, ui), + _ => layer_hint(ui, "Select a feature layer in Layers to label it."), + } + map_labeling_group(app, ui); +} + +/// What a layer-dependent tab shows in place of its layer's groups: why there are none. +fn layer_hint(ui: &mut Ui, text: &str) { + group(ui, "Layer", |ui| { + let t = Tokens::get(ui.ctx()); + ui.add_sized(vec2(220.0, CONTENT_H), egui::Label::new(egui::RichText::new(tl!(text)).color(t.text_dim)).wrap()); + }); +} + /// The Analyze tab: geoprocessing, workflows, imagery and notebooks. fn analyze_tab(app: &mut RustGisApp, ui: &mut Ui) { analysis_groups(app, ui); @@ -4301,69 +4301,6 @@ fn label_groups(app: &mut RustGisApp, ui: &mut Ui) { false, ); }); - group(ui, "Label Range", |ui| { - ui.vertical(|ui| { - ui.add_space(6.0); - for (label, cur, key) in [("Out Beyond", c.it.min_scale, "min"), ("In Beyond", c.it.max_scale, "max")] { - ui.horizontal(|ui| { - let input_label = ui.label(egui::RichText::new(tl!(label)).size(11.5)); - egui::ComboBox::from_id_salt(format!("label_scale_{key}")) - .selected_text(scale_label(cur)) - .width(118.0) - .show_ui(ui, |ui| { - let mut choices: Vec> = vec![None]; - choices.extend(SCALES.iter().map(|s| Some(*s))); - for s in choices { - if ui.selectable_label(s == cur, scale_label(s)).clicked() { - let _ = app.run("labels.scaleRange", c.params(json!({key: s}))); - } - } - }) - .response - .labelled_by(input_label.id); - }); - } - }); - col(ui, |ui| { - for (label, cur, key, desc) in [ - ("Closest", c.it.closest, "closest", "Hide labels when the viewer is closer than this."), - ("Farthest", c.it.farthest, "farthest", "Hide labels when the viewer is farther than this."), - ] { - small_menu(ui, app, "eye", label, desc, |ui, app| { - for d in [None, Some(1_000.0), Some(10_000.0), Some(100_000.0), Some(1_000_000.0), Some(10_000_000.0)] { - let text = d - .map(|d| crate::measure_units::distance(d, app.ui.measurement_imperial)) - .unwrap_or_else(|| tl!("").to_string()); - if ui.radio(cur == d, text).clicked() { - let _ = app.run("labels.distanceRange", c.params(json!({key: d}))); - ui.close(); - } - } - }); - } - small( - ui, - app, - "scale_label", - "Clear Limits", - "Draw labels at every scale and distance.", - "labels.scaleRange", - c.params(json!({"clear": true})), - false, - ); - }); - }); - group(ui, "Label Placement", |ui| { - let cur = c.it.placement; - big_menu(ui, app, "placement", "Placement", "Where labels sit relative to their features.", |ui, app| { - for pl in rustgis_engine::map::LabelPlacement::ALL { - if ui.radio(cur == pl, tl!(pl.label())).clicked() { - let _ = app.run("labels.placement", c.params(json!({"position": pl.code()}))); - ui.close(); - } - } - }); - }); group(ui, "Label Class", |ui| { ui.vertical(|ui| { ui.add_space(6.0); @@ -4417,6 +4354,69 @@ fn label_groups(app: &mut RustGisApp, ui: &mut Ui) { ); }); }); + group(ui, "Label Placement", |ui| { + let cur = c.it.placement; + big_menu(ui, app, "placement", "Placement", "Where labels sit relative to their features.", |ui, app| { + for pl in rustgis_engine::map::LabelPlacement::ALL { + if ui.radio(cur == pl, tl!(pl.label())).clicked() { + let _ = app.run("labels.placement", c.params(json!({"position": pl.code()}))); + ui.close(); + } + } + }); + }); + group(ui, "Label Range", |ui| { + ui.vertical(|ui| { + ui.add_space(6.0); + for (label, cur, key) in [("Out Beyond", c.it.min_scale, "min"), ("In Beyond", c.it.max_scale, "max")] { + ui.horizontal(|ui| { + let input_label = ui.label(egui::RichText::new(tl!(label)).size(11.5)); + egui::ComboBox::from_id_salt(format!("label_scale_{key}")) + .selected_text(scale_label(cur)) + .width(118.0) + .show_ui(ui, |ui| { + let mut choices: Vec> = vec![None]; + choices.extend(SCALES.iter().map(|s| Some(*s))); + for s in choices { + if ui.selectable_label(s == cur, scale_label(s)).clicked() { + let _ = app.run("labels.scaleRange", c.params(json!({key: s}))); + } + } + }) + .response + .labelled_by(input_label.id); + }); + } + }); + col(ui, |ui| { + for (label, cur, key, desc) in [ + ("Closest", c.it.closest, "closest", "Hide labels when the viewer is closer than this."), + ("Farthest", c.it.farthest, "farthest", "Hide labels when the viewer is farther than this."), + ] { + small_menu(ui, app, "eye", label, desc, |ui, app| { + for d in [None, Some(1_000.0), Some(10_000.0), Some(100_000.0), Some(1_000_000.0), Some(10_000_000.0)] { + let text = d + .map(|d| crate::measure_units::distance(d, app.ui.measurement_imperial)) + .unwrap_or_else(|| tl!("").to_string()); + if ui.radio(cur == d, text).clicked() { + let _ = app.run("labels.distanceRange", c.params(json!({key: d}))); + ui.close(); + } + } + }); + } + small( + ui, + app, + "scale_label", + "Clear Limits", + "Draw labels at every scale and distance.", + "labels.scaleRange", + c.params(json!({"clear": true})), + false, + ); + }); + }); } fn feature_data_groups(app: &mut RustGisApp, ui: &mut Ui) { @@ -4598,6 +4598,42 @@ mod tests { app } + /// One tab's groups on `app`, as the ribbon draws them. + fn tab_harness(app: RustGisApp, tab: fn(&mut RustGisApp, &mut Ui)) -> egui_kittest::Harness<'static, RustGisApp> { + let mut fonts = false; + let mut h = egui_kittest::Harness::builder().with_size(vec2(2400.0, 300.0)).build_ui_state( + move |ui, app: &mut RustGisApp| { + if !fonts { + crate::theme::install_fonts(ui.ctx(), &[], None); + fonts = true; + return; + } + ui.horizontal(|ui| tab(app, ui)); + }, + app, + ); + h.run(); + h + } + + /// The Labeling tab holds every label group (#897): a feature layer's, then the map's, which + /// show with no layer too; Style and View have none left. + #[test] + fn the_labeling_tab_holds_the_label_groups() { + let has = |h: &egui_kittest::Harness<'static, RustGisApp>, label: &str| { + h.query_by_role_and_label(egui::accesskit::Role::Button, label).is_some() + }; + let h = tab_harness(cities(), labeling_tab); + assert!(has(&h, "Closest") && has(&h, "View Unplaced"), "a feature layer's groups and the map's"); + let mut app = cities(); + app.session.active_layer = None; + let h = tab_harness(app, labeling_tab); + assert!(h.query_by_label("Select a feature layer in Layers to label it.").is_some()); + assert!(!has(&h, "Closest") && has(&h, "View Unplaced"), "the map's settings need no layer"); + assert!(!has(&tab_harness(cities(), style_tab), "Closest"), "Style keeps no label groups"); + assert!(!has(&tab_harness(cities(), view_tab), "View Unplaced"), "View keeps no label groups"); + } + /// Isolates the label controls; the interrupted-gesture test below checks full-frame wiring. fn label_harness() -> egui_kittest::Harness<'static, RustGisApp> { label_harness_with(cities(), false) @@ -4706,7 +4742,7 @@ mod tests { for hide_ribbon in [false, true] { let mut app = RustGisApp::with_sample(); app.session.run("layer.select", &json!({"layer": "Cities"})).unwrap(); - app.ui.tab = "Style".into(); + app.ui.tab = "Labeling".into(); let mut h = egui_kittest::Harness::builder().with_size(vec2(1200.0, 800.0)).with_max_steps(64).build_ui_state( |ui, app: &mut RustGisApp| { let ctx = ui.ctx().clone(); @@ -4716,16 +4752,12 @@ mod tests { app, ); h.run(); - for _ in 0..2 { - h.get_by_label("Scroll the ribbon right").click(); - h.run(); - } h.state_mut().ws.recording = Some(Vec::new()); let id = h.state().session.active_layer.unwrap(); let size = |app: &RustGisApp| app.session.map().layer(id).unwrap().labels.default.size; let before = size(h.state()); let undo = h.state().session.undo_depth(); - // Text Symbol follows the Drawing group's numeric controls in the Style ribbon. + // Text Symbol's size is the Labeling tab's numeric control, drawn after any other. let at = h.query_all_by_role(egui::accesskit::Role::SpinButton).last().unwrap().rect().center(); h.hover_at(at); h.drag_at(at); diff --git a/crates/ui-egui/src/scriptcomplete.rs b/crates/ui-egui/src/scriptcomplete.rs index fd3e20d17..339e197af 100644 --- a/crates/ui-egui/src/scriptcomplete.rs +++ b/crates/ui-egui/src/scriptcomplete.rs @@ -1953,7 +1953,7 @@ mod tests { assert!(h.keys.starts_with(" {layer?, class?, size?"), "{}", h.keys); assert_eq!(h.label, "Text Symbol"); assert!(h.params.iter().any(|p| p == r#""size"?: number"#), "{:?}", h.params); - assert_eq!(h.location, "Style › Text Symbol"); + assert_eq!(h.location, "Labeling › Text Symbol"); // Required keys go bare. assert!(super::help(&app.session, "labels.field").unwrap().keys.contains("field}")); // No parameters: the id alone. diff --git a/crates/ui-egui/tests/measurement_units.rs b/crates/ui-egui/tests/measurement_units.rs index e58939b63..933f20097 100644 --- a/crates/ui-egui/tests/measurement_units.rs +++ b/crates/ui-egui/tests/measurement_units.rs @@ -244,7 +244,7 @@ fn distance_controls_follow_display_units_and_keep_metre_command_values() { assert!(text.iter().any(|s| s == "3281 ft") && text.iter().any(|s| s == "62.1 mi"), "{text:?}"); h.state_mut().run("ui.pane.labeling", json!({})).unwrap(); - h.state_mut().run("ui.tab", json!({"tab": "Style"})).unwrap(); + h.state_mut().run("ui.tab", json!({"tab": "Labeling"})).unwrap(); for _ in 0..3 { h.step(); } diff --git a/crates/ui-egui/tests/smoke.rs b/crates/ui-egui/tests/smoke.rs index c4ab4fa43..2d65186d8 100644 --- a/crates/ui-egui/tests/smoke.rs +++ b/crates/ui-egui/tests/smoke.rs @@ -462,7 +462,7 @@ fn every_tab_pane_and_dialog_renders() { for _ in 0..4 { h.step(); } - for tab in ["Home", "Data", "Style", "Edit", "Analyze", "View", "Publish", "Plugins", "Settings", "Help"] { + for tab in ["Home", "Data", "Style", "Labeling", "Edit", "Analyze", "View", "Publish", "Plugins", "Settings", "Help"] { h.state_mut().ui.tab = tab.into(); h.run(); } @@ -1236,7 +1236,14 @@ fn show_me_points_at_where_a_steps_command_is() { #[test] fn old_and_unknown_tab_names_open_a_current_tab() { let mut h = harness(); - for (asked, opened) in [("Imagery", "Analyze"), ("Feature Layer", "Style"), ("Share", "Publish"), ("Map", "Home"), ("nope", "Home")] { + for (asked, opened) in [ + ("Imagery", "Analyze"), + ("Feature Layer", "Style"), + ("Labeling", "Labeling"), + ("Share", "Publish"), + ("Map", "Home"), + ("nope", "Home"), + ] { let _ = h.state_mut().run("ui.tab", json!({"tab": asked})); assert_eq!(h.state().ui.tab, opened, "{asked}"); h.step(); diff --git a/docs/comparison/geolibre.md b/docs/comparison/geolibre.md index 6c6830001..e0a8a4e67 100644 --- a/docs/comparison/geolibre.md +++ b/docs/comparison/geolibre.md @@ -107,8 +107,8 @@ compact, scriptable desktop GIS, or use both. subreports, and layouts with publication presets, shaped map frames, legends, colorbars, grids, tables, charts, extent indicators and map series, exported to PDF or PNG. GeoLibre's print layout and atlas cover the common cases, and it has no report builder. -- **A ribbon interface.** Tabs follow the order of the work (Home, Data, Style, Edit, Analyze, - View, Publish), with Command Search (++ctrl+k++) to find any command or tool by name. People +- **A ribbon interface.** Tabs follow the order of the work (Home, Data, Style, Labeling, Edit, + Analyze, View, Publish), with Command Search (++ctrl+k++) to find any command or tool by name. People coming from ArcGIS Pro often find it familiar. GeoLibre uses menus and panels, with a command palette and beginner, intermediate and advanced interface levels. - **Offline local workflows.** RustGIS's local-file readers, built-in tools and rendering work diff --git a/docs/comparison/qgis.md b/docs/comparison/qgis.md index 794e76e46..6b00601af 100644 --- a/docs/comparison/qgis.md +++ b/docs/comparison/qgis.md @@ -81,8 +81,8 @@ The same World Atlas data, styled the same way, in each app. FlatGeobuf, PMTiles and Zarr are read in place by RustGIS's own readers, over HTTP byte ranges and from `s3://` buckets, on the desktop and in the browser alike. QGIS reads all of these too, through GDAL on the desktop. -- **A ribbon interface.** Tabs follow the order of the work (Home, Data, Style, Edit, Analyze, - View, Publish), with Command Search (++ctrl+k++) to find any command or tool by name. People +- **A ribbon interface.** Tabs follow the order of the work (Home, Data, Style, Labeling, Edit, + Analyze, View, Publish), with Command Search (++ctrl+k++) to find any command or tool by name. People coming from ArcGIS Pro often find it familiar; people who know QGIS's toolbars may not want it, which is a matter of taste. - **A permissive licence.** RustGIS is Apache-2.0, so its crates can be embedded in proprietary diff --git a/docs/contributing.md b/docs/contributing.md index d236a2434..7f4b9a972 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -56,7 +56,7 @@ is space. Agree on that purpose before reorganising a tab. | Home | The basics of working with a map: starting a map or layout, adding data, moving around, and selecting and inspecting features. | | Data | Working with the content and structure of data: looking at records, changing fields and values, connecting tables, and bringing data in and out. Exports here produce data, such as features and rasters. | | Style | Deciding how a layer looks on the map: how its features are drawn and shown at different scales. | -| Labeling (planned) | How the map is labelled: what gets a label, with which text, where, and at which scales, plus the label settings for the whole map. | +| Labeling | How the map is labelled: what gets a label, with which text, where, and at which scales, plus the label settings for the whole map. | | Edit | Creating and changing features and graphics on the map, and managing those edits. | | Analyze | Turning existing data into new results, from single tools to repeatable workflows, scripts and automation. | | View | How the map and workspace are displayed: map appearance and view configuration, and which panes and windows are open. Basic navigation stays on Home. | @@ -65,11 +65,10 @@ is space. Agree on that purpose before reorganising a tab. | Settings | Preferences that apply to every project, such as language, appearance and start-up. | | Help | Learning RustGIS, finding commands and shortcuts, and getting information about the app. | -Until the Labeling tab lands, Style still holds the layer's label controls. Keep Data, Style and -Labeling fixed, rather than making the tab strip change with the selected layer. Their target -should be explicit: a layer chooser at the left of each layer-dependent tab (for example, -“Styling: Countries ▾”) is planned, with a useful hint when no suitable layer is selected. Where -a class chooser is needed, the layer comes first, then the class. +Keep Data, Style and Labeling fixed, rather than making the tab strip change with the selected +layer. Their target should be explicit: a layer chooser at the left of each layer-dependent tab +(for example, “Styling: Countries ▾”) is planned, with a useful hint when no suitable layer is +selected. Where a class chooser is needed, the layer comes first, then the class. If a control seems to fit two tabs, or none, clarify its task and destination before placing it. Home is not a catch-all for anything described as “frequently used”. Data exports data; diff --git a/docs/user-guide/cartography.md b/docs/user-guide/cartography.md index 8afa182ef..751ac64a5 100644 --- a/docs/user-guide/cartography.md +++ b/docs/user-guide/cartography.md @@ -278,11 +278,11 @@ clause, text symbol, placement and visibility limits. Under its header (the laye off, and the class chosen), a tab per topic shows one part at a time: **Content** (the label field, with a search in its list, above the SQL query), **Symbol**, **Position** and **Range**. Open it for a feature layer with Layers › right-click › **Labeling Properties…**, -**View › Windows › Labeling**, or **Style › Labels › Properties**. It runs the same commands as +**View › Windows › Labeling**, or **Labeling › Labels › Properties**. It runs the same commands as the ribbon, so each setting change undoes on its own (a dragged size or a colour pick is one step). Label classes are described below. -Turn labels on per layer (View › Labeling › Label, or the Style tab), then: +Turn labels on per layer (Labeling › Labels › **Label**), then: - **Label field and text**: the field the labels show (`labels.field={"layer": "Cities", "field": "name"}`), and the text symbol's size (4–72 points), @@ -315,8 +315,8 @@ Turn labels on per layer (View › Labeling › Label, or the Style tab), then: (`labels.placement={"layer": "Rivers", "position": "curved"}`). - **Lock** keeps the labelled features fixed while you pan and zoom; **Pause** stops drawing labels for faster navigation; **View Unplaced** draws the labels that didn't fit in red. They - apply to the whole map, so they are on the ribbon (View › Labeling), not in the pane. -- **Convert** (View › Labeling, or Layers › right-click › **Convert To Annotation**) turns a + apply to the whole map, so they are on the ribbon (Labeling › Map Labeling), not in the pane. +- **Convert** (Labeling › Map Labeling, or Layers › right-click › **Convert To Annotation**) turns a layer's labels into an annotation layer: one point per label, carrying its text in a `TEXT` field, which you can then edit, move or delete like any other features. - **Label classes** label different features differently, such as large cities in larger text. @@ -333,7 +333,7 @@ Turn labels on per layer (View › Labeling › Label, or the Style tab), then: with the chosen class's field, text symbol, placement and range, listed before Default; **Rename** and **Move Up** / **Move Down** (the arrows) for the chosen class; and, at its far end, **Remove** (Default can be renamed or hidden, not moved or removed). The ribbon's label groups edit the - same chosen class, and **Style › Label Class › Class** chooses it as the pane's list does. By + same chosen class, and **Labeling › Label Class › Class** chooses it as the pane's list does. By command, make them with `labels.class.add` (a new class copies Default's field, text symbol, placement and range, or those of the class named by `from`), then `labels.class.rename`, `labels.class.update` (`where` or `visible`), diff --git a/docs/user-guide/index.md b/docs/user-guide/index.md index e9407e912..6b5df20ea 100644 --- a/docs/user-guide/index.md +++ b/docs/user-guide/index.md @@ -110,16 +110,17 @@ ribbon, arrows at its ends scroll it. | --- | --- | | **Home** | Project (New Map, Toolbox, Import Project, New Layout, Browser, Add Data, Add Folder, Import, Templates), Navigate (Pan, Bookmarks, Full Extent, Zoom In/Out, Previous/Next, Go To XY, Planar/First-Person, Identify, Find Place, Directions), Add (Basemap, Add Data, [Web Data](web-data.md)), Selection (Select, Select By Attributes, Select By Location, Clear, Zoom To, Attributes), Inspect (Measure, Locate, Zoom Box, Infographics) | | **Data** | Layer Templates, Data Source (Repair Source); for a feature layer: Definition Query, Table, Selection, Data Design, Archiving, Relationship and Export; for a raster layer: Information and Export; then Offline and Feature Cache | -| **Style** | For the layer selected in Layers. A feature layer: Visibility Range, Effects, Drawing, Drawing Effects, Extrusion for polygons (with Faces and Light and Shadow once on), Layer, Labels, Label Range, Label Placement, Label Class and Text Symbol. A raster layer: Visibility Range, Effects, Compare, Raster Effects, Rendering, Enhancement and Layer. Both: Styles | +| **Style** | For the layer selected in Layers. A feature layer: Visibility Range, Effects, Drawing, Drawing Effects, Extrusion for polygons (with Faces and Light and Shadow once on) and Layer. A raster layer: Visibility Range, Effects, Compare, Raster Effects, Rendering, Enhancement and Layer. Both: Styles | +| **Labeling** | For the feature layer selected in Layers: Labels (Label, Properties), Label Class (Class, Field), Text Symbol (Size, Color, Bold, Halo), Label Placement and Label Range. Whatever is selected: Map Labeling (Lock, Pause, View Unplaced, Convert) | | **Edit** | Manage Edits (Edit, Save, Undo, Redo, Status, Settings, Templates, Topology), Snapping, Features (Create, Delete, Edit Vertices, Modify), Tools, Elevation, Corrections, Graphics, Graphics and Text, Clipboard | | **Analyze** | Geoprocessing (History, Tools, Model Builder, Script, SQL, Environments), Tasks (the Tasks pane; Task: Record Task, New Task From History, Import Task, run a task), AI (AI Assistant), Portal (Feature Analysis, Raster Analysis), Workflows, Raster (Raster Functions, Function Editor), Tools (the tool gallery), Selection, Notebook, the imagery groups (Ortho Mapping, Reality Mapping, Alignment, Image Analysis, Image Classification, Imagery Sharing, Mensuration, Imagery Tools) | -| **View** | Display (Graticule, Dark Mode, GPU Rendering), View (Globe, Projection, Global; Local on the desktop), Effects, Scene, Windows, Labeling, Navigation, Link, Workspace, Animation, Time (Time Slider), Profile (Profile; Time Series on the desktop) | +| **View** | Display (Graticule, Dark Mode, GPU Rendering), View (Globe, Projection, Global; Local on the desktop), Effects, Scene, Windows, Navigation, Link, Workspace, Animation, Time (Time Slider), Profile (Profile; Time Series on the desktop) | | **Publish** | While a layout is open, its groups first (Page, Map Frames, Map Surrounds, Shapes, Text, Export); then Report, Presentation, Output (Export Map, Export Features, Print Map, Capture To Clipboard, Report, Presentation, Export To 3D), Package, Share As, Layout, Save As, Thumbnail | | **Plugins** | Plugins (Plugin Manager, Install From File, Browse Plugins), plus a group for each enabled [plugin](../plugins.md) with commands or tools; a plugin with its own tab adds that tab instead | | **Settings** | Customize (Ribbon, Quick Access), Language (interface language and worldview), Start-up (Start With, Projection, Extent), Autosave on the desktop | | **Help** | Performance (Diagnostic Monitor), Learn (Learning Resources), Help (About, Check for Updates on the desktop, Command Search, Shortcuts) | -The tabs stay the same whatever is selected: Style and Data act on the layer selected in Layers. +The tabs stay the same whatever is selected: Style, Labeling and Data act on the layer selected in Layers. Menus with checkboxes stay open while you change options: stack or show web basemaps, combine Visual and Camera Effects, or customize Ribbon tabs and Quick Access buttons. Click outside