use crate::compat::HashMap;
use crate::core::{Color, HorizontalAlignment, Point, Rect};
use crate::event::{Event, EventHandler};
use crate::render::RenderContext;
use crate::signal::{GenericSignal, Signal1};
use crate::widget::capability::coercion::{
expect_bool, expect_f32, expect_f64, expect_string, expect_u32,
};
use crate::widget::capability::properties_trait::{base_property_get, base_property_set};
use crate::widget::capability::types::{CapabilityAccessError, CapabilityValue};
use crate::widget::capability::WidgetProperties;
use crate::widget::metrics::effective_font;
use crate::widget::{BaseWidget, Draw, Widget, WidgetKind};
use crate::{impl_widget_property_hooks, property_names_of};
fn companion_palette(style: &crate::style::WidgetStyle, role: &str) -> (Color, Color, Color) {
let theme = crate::style::resolved_theme_style_shared(role);
let ink = style
.text_color
.or_else(|| theme.as_ref().and_then(|t| t.text_color))
.unwrap_or(Color::rgb(45, 58, 74));
let border = style
.border_color
.or_else(|| theme.as_ref().and_then(|t| t.border_color))
.unwrap_or(Color::rgb(168, 184, 201));
let surface = style
.background_color
.or_else(|| theme.as_ref().and_then(|t| t.background_color))
.unwrap_or(Color::rgb(235, 243, 250));
(surface, border, ink)
}
pub const MAX_TILE_ZOOM: u8 = 22;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct TileKey {
pub zoom: u8,
pub x: u32,
pub y: u32,
}
impl TileKey {
pub fn new(zoom: u8, x: u32, y: u32) -> Self {
Self { zoom, x, y }
}
pub fn tiles_per_axis(zoom: u8) -> u32 {
1u32.checked_shl(u32::from(zoom)).unwrap_or(0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TileLoadError {
ZoomOutOfRange,
TileOutOfWorld,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TileRequest {
pub key: TileKey,
pub url: String,
pub cached: bool,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TileViewport {
pub zoom: u8,
pub x: f64,
pub y: f64,
pub width: u32,
pub height: u32,
}
impl TileViewport {
pub fn new(zoom: u8, x: f64, y: f64, width: u32, height: u32) -> Self {
Self { zoom, x, y, width, height }
}
}
struct TileEntry {
payload: Option<Vec<u8>>,
}
pub struct TileLoader {
base: BaseWidget,
url_template: String,
min_zoom: u8,
max_zoom: u8,
tile_size: u32,
cache_limit: usize,
cache: HashMap<TileKey, TileEntry>,
order: alloc::collections::VecDeque<TileKey>,
pub tile_loaded: Signal1<TileKey>,
pub tile_failed: Signal1<TileKey>,
}
impl TileLoader {
pub fn new(geometry: Rect) -> Self {
Self {
base: BaseWidget::new(WidgetKind::TileLoader, geometry, "TileLoader"),
url_template: "tiles/{z}/{x}/{y}.png".to_string(),
min_zoom: 0,
max_zoom: 19,
tile_size: 256,
cache_limit: 256,
cache: HashMap::new(),
order: alloc::collections::VecDeque::new(),
tile_loaded: Signal1::new(),
tile_failed: Signal1::new(),
}
}
pub fn url_template(&self) -> &str {
&self.url_template
}
pub fn set_url_template(&mut self, template: impl Into<String>) {
let template = template.into();
if template.trim().is_empty() {
return;
}
self.url_template = template;
self.base.request_redraw();
}
pub fn min_zoom(&self) -> u8 {
self.min_zoom
}
pub fn max_zoom(&self) -> u8 {
self.max_zoom
}
pub fn set_zoom_range(&mut self, min_zoom: u8, max_zoom: u8) {
let min = min_zoom.min(MAX_TILE_ZOOM);
let max = max_zoom.clamp(min, MAX_TILE_ZOOM);
if self.min_zoom == min && self.max_zoom == max {
return;
}
self.min_zoom = min;
self.max_zoom = max;
self.base.request_redraw();
}
pub fn tile_size(&self) -> u32 {
self.tile_size
}
pub fn set_tile_size(&mut self, tile_size: u32) {
let size = tile_size.clamp(16, 1024);
if self.tile_size == size {
return;
}
self.tile_size = size;
self.base.request_redraw();
}
pub fn cache_limit(&self) -> usize {
self.cache_limit
}
pub fn set_cache_limit(&mut self, cache_limit: usize) {
self.cache_limit = cache_limit.clamp(1, 65536);
self.evict_over_limit();
self.base.request_redraw();
}
pub fn tile_url(&self, key: TileKey) -> String {
self.url_template
.replace("{z}", &key.zoom.to_string())
.replace("{x}", &key.x.to_string())
.replace("{y}", &key.y.to_string())
}
pub fn load_tile(&mut self, zoom: u8, x: u32, y: u32) -> Result<TileRequest, TileLoadError> {
if zoom < self.min_zoom || zoom > self.max_zoom {
return Err(TileLoadError::ZoomOutOfRange);
}
let per_axis = TileKey::tiles_per_axis(zoom);
if per_axis == 0 || x >= per_axis || y >= per_axis {
return Err(TileLoadError::TileOutOfWorld);
}
let key = TileKey::new(zoom, x, y);
let cached = self.cache.contains_key(&key);
let url = self.tile_url(key);
if cached {
if let Some(position) = self.order.iter().position(|queued| *queued == key) {
self.order.remove(position);
self.order.push_back(key);
}
} else {
self.cache.insert(key, TileEntry { payload: None });
self.order.push_back(key);
self.evict_over_limit();
self.base.request_redraw();
}
Ok(TileRequest { key, url, cached })
}
pub fn prefetch(&mut self, viewport: TileViewport) -> Vec<TileRequest> {
if !viewport.x.is_finite() || !viewport.y.is_finite() {
return Vec::new();
}
if viewport.width == 0 || viewport.height == 0 {
return Vec::new();
}
let per_axis = TileKey::tiles_per_axis(viewport.zoom) as i64;
if per_axis <= 0 {
return Vec::new();
}
let tile = self.tile_size.max(1) as f64;
let first_col = viewport.x.floor();
let first_row = viewport.y.floor();
let cols = (viewport.width as f64 / tile).ceil().max(1.0) as i64 + 1;
let rows = (viewport.height as f64 / tile).ceil().max(1.0) as i64 + 1;
let limit = self.cache_limit.max(1);
let mut requests = Vec::new();
for row in 0..rows {
for col in 0..cols {
if requests.len() >= limit {
return requests;
}
let x = first_col as i64 + col;
let y = first_row as i64 + row;
if x < 0 || y < 0 || x >= per_axis || y >= per_axis {
continue;
}
if let Ok(request) = self.load_tile(viewport.zoom, x as u32, y as u32) {
requests.push(request);
}
}
}
requests
}
pub fn deliver_tile(&mut self, key: TileKey, payload: Vec<u8>) -> bool {
let Some(entry) = self.cache.get_mut(&key) else {
return false;
};
entry.payload = Some(payload);
self.tile_loaded.emit(key);
self.base.request_redraw();
true
}
pub fn reject_tile(&mut self, key: TileKey) -> bool {
if self.cache.remove(&key).is_none() {
return false;
}
self.order.retain(|queued| *queued != key);
self.tile_failed.emit(key);
self.base.request_redraw();
true
}
pub fn clear_cache(&mut self) {
self.cache.clear();
self.order.clear();
self.base.request_redraw();
}
pub fn cache_len(&self) -> usize {
self.cache.len()
}
pub fn is_cached(&self, key: TileKey) -> bool {
self.cache.get(&key).is_some_and(|entry| entry.payload.is_some())
}
pub fn tile_pixels(&self, key: TileKey) -> Option<&[u8]> {
self.cache.get(&key).and_then(|entry| entry.payload.as_deref())
}
fn cached_count(&self) -> usize {
self.cache.values().filter(|entry| entry.payload.is_some()).count()
}
fn evict_over_limit(&mut self) {
while self.cache.len() > self.cache_limit {
let Some(oldest) = self.order.pop_front() else {
break;
};
self.cache.remove(&oldest);
}
}
}
impl Widget for TileLoader {
fn base(&self) -> &BaseWidget {
&self.base
}
fn base_mut(&mut self) -> &mut BaseWidget {
&mut self.base
}
fn set_state_theme_hook(&mut self) {
crate::style::reapply_active_theme_state(self);
}
fn size_hint(&self) -> crate::core::Size {
crate::core::Size::new(160, 24)
}
impl_draw_bridge!();
impl_widget_property_hooks!();
fn event_signal_dyn(&self, name: &str) -> Option<crate::signal::EventSignalRef> {
use crate::signal::EventSignalRef;
match name {
"tile_loaded" => {
Some(EventSignalRef::mapped("tile_loaded", &self.tile_loaded, |key| {
tile_key_value(*key)
}))
}
"tile_failed" => {
Some(EventSignalRef::mapped("tile_failed", &self.tile_failed, |key| {
tile_key_value(*key)
}))
}
_ => None,
}
}
}
fn tile_key_value(key: TileKey) -> CapabilityValue {
CapabilityValue::Tuple(alloc::vec![
CapabilityValue::Int(i64::from(key.zoom)),
CapabilityValue::Int(i64::from(key.x)),
CapabilityValue::Int(i64::from(key.y)),
])
}
impl WidgetProperties for TileLoader {
fn get(&self, name: &str) -> Result<CapabilityValue, CapabilityAccessError> {
match name {
"url_template" => Ok(CapabilityValue::String(self.url_template().to_string())),
"min_zoom" => Ok(CapabilityValue::UInt(u64::from(self.min_zoom))),
"max_zoom" => Ok(CapabilityValue::UInt(u64::from(self.max_zoom))),
"tile_size" => Ok(CapabilityValue::UInt(u64::from(self.tile_size))),
"cache_limit" => Ok(CapabilityValue::UInt(self.cache_limit as u64)),
"cache_size" => Ok(CapabilityValue::UInt(self.cache_len() as u64)),
_ => base_property_get(self, name),
}
}
fn set(&mut self, name: &str, value: CapabilityValue) -> Result<(), CapabilityAccessError> {
match name {
"url_template" => {
self.set_url_template(expect_string(value)?);
Ok(())
}
"min_zoom" => {
let min = expect_u32(value)?.min(u32::from(MAX_TILE_ZOOM)) as u8;
self.set_zoom_range(min, self.max_zoom);
Ok(())
}
"max_zoom" => {
let max = expect_u32(value)?.min(u32::from(MAX_TILE_ZOOM)) as u8;
self.set_zoom_range(self.min_zoom, max);
Ok(())
}
"tile_size" => {
self.set_tile_size(expect_u32(value)?);
Ok(())
}
"cache_limit" => {
self.set_cache_limit(expect_u32(value)? as usize);
Ok(())
}
"cache_size" => Err(CapabilityAccessError::ReadOnlyProperty),
_ => base_property_set(self, name, value),
}
}
fn property_names(&self) -> &'static [&'static str] {
property_names_of![
"url_template",
"min_zoom",
"max_zoom",
"tile_size",
"cache_limit",
"cache_size",
BASE_PROPERTY_NAMES
]
}
fn command(&mut self, name: &str) -> Result<(), CapabilityAccessError> {
match name {
"clear_cache" => {
self.clear_cache();
Ok(())
}
_ => self.default_command(name),
}
}
}
impl EventHandler for TileLoader {
fn handle_event(&mut self, event: &Event) {
self.base.handle_event(event);
}
}
impl Draw for TileLoader {
fn draw(&mut self, context: &mut RenderContext) {
let rect = self.geometry();
let style = self.base.style();
let (surface, border, ink) = companion_palette(style, "tile_loader");
context.face_with_gradient(rect, surface, None, Default::default(), 0, Color::BLACK);
crate::widget::draw::stroke_surface_border(
context,
rect,
self.style().border_radius.unwrap_or(0),
border,
1,
);
let status = format!(
"Tiles {}/{} z{}-{}",
self.cached_count(),
self.cache.len(),
self.min_zoom,
self.max_zoom
);
context.draw_text_fitted_cow(
Rect::new(rect.x + 4, rect.y + 2, rect.width.saturating_sub(8), 14),
&status,
effective_font(style),
ink,
HorizontalAlignment::Left,
);
}
}
const ZOOM_CONTROL_STEP: f32 = 2.0;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ZoomButton {
Out,
Reset,
In,
}
pub struct ZoomControl {
base: BaseWidget,
zoom_level: f32,
min: f32,
max: f32,
pub zoom_requested: Signal1<f32>,
}
impl ZoomControl {
pub fn new(geometry: Rect) -> Self {
Self {
base: BaseWidget::new(WidgetKind::ZoomControl, geometry, "ZoomControl"),
zoom_level: 1.0,
min: 0.25,
max: 8.0,
zoom_requested: Signal1::new(),
}
}
pub fn zoom_level(&self) -> f32 {
self.zoom_level
}
pub fn min(&self) -> f32 {
self.min
}
pub fn max(&self) -> f32 {
self.max
}
pub fn set_zoom(&mut self, zoom_level: f32) -> bool {
if !zoom_level.is_finite() {
return false;
}
let level = zoom_level.clamp(self.min, self.max);
if (self.zoom_level - level).abs() < f32::EPSILON {
return false;
}
self.zoom_level = level;
self.zoom_requested.emit(level);
self.base.request_redraw();
true
}
pub fn zoom_in(&mut self) -> bool {
self.set_zoom(self.zoom_level * ZOOM_CONTROL_STEP)
}
pub fn zoom_out(&mut self) -> bool {
self.set_zoom(self.zoom_level / ZOOM_CONTROL_STEP)
}
pub fn reset(&mut self) -> bool {
self.set_zoom(1.0)
}
pub fn set_min(&mut self, min: f32) {
if !min.is_finite() {
return;
}
let min = min.min(self.max);
if (self.min - min).abs() < f32::EPSILON {
return;
}
self.min = min;
self.zoom_level = self.zoom_level.clamp(self.min, self.max);
self.base.request_redraw();
}
pub fn set_max(&mut self, max: f32) {
if !max.is_finite() {
return;
}
let max = max.max(self.min);
if (self.max - max).abs() < f32::EPSILON {
return;
}
self.max = max;
self.zoom_level = self.zoom_level.clamp(self.min, self.max);
self.base.request_redraw();
}
pub fn button_rect(&self, button: ZoomButton) -> Rect {
let rect = self.geometry();
const GAP: i32 = 2;
const COUNT: i32 = 3;
let available = (rect.width as i32 - GAP * (COUNT - 1)).max(1);
let width = (available / COUNT).max(1);
let index = match button {
ZoomButton::Out => 0,
ZoomButton::Reset => 1,
ZoomButton::In => 2,
};
Rect::new(rect.x + index * (width + GAP), rect.y, width as u32, rect.height)
}
pub fn hit_button(&self, point: Point) -> Option<ZoomButton> {
[ZoomButton::Out, ZoomButton::Reset, ZoomButton::In].into_iter().find(|button| {
let rect = self.button_rect(*button);
point.x >= rect.x
&& point.x < rect.x + rect.width as i32
&& point.y >= rect.y
&& point.y < rect.y + rect.height as i32
})
}
}
impl Widget for ZoomControl {
fn base(&self) -> &BaseWidget {
&self.base
}
fn base_mut(&mut self) -> &mut BaseWidget {
&mut self.base
}
fn set_state_theme_hook(&mut self) {
crate::style::reapply_active_theme_state(self);
}
fn size_hint(&self) -> crate::core::Size {
crate::core::Size::new(96, 28)
}
impl_draw_bridge!();
impl_widget_property_hooks!();
fn event_signal_dyn(&self, name: &str) -> Option<crate::signal::EventSignalRef> {
use crate::signal::EventSignalRef;
match name {
"zoom_requested" => {
Some(EventSignalRef::mapped("zoom_requested", &self.zoom_requested, |level| {
CapabilityValue::Float(f64::from(*level))
}))
}
_ => None,
}
}
}
impl WidgetProperties for ZoomControl {
fn get(&self, name: &str) -> Result<CapabilityValue, CapabilityAccessError> {
match name {
"zoom_level" => Ok(CapabilityValue::Float(f64::from(self.zoom_level))),
"min" => Ok(CapabilityValue::Float(f64::from(self.min))),
"max" => Ok(CapabilityValue::Float(f64::from(self.max))),
_ => base_property_get(self, name),
}
}
fn set(&mut self, name: &str, value: CapabilityValue) -> Result<(), CapabilityAccessError> {
match name {
"zoom_level" => {
self.set_zoom(expect_f32(value)?);
Ok(())
}
"min" => {
self.set_min(expect_f32(value)?);
Ok(())
}
"max" => {
self.set_max(expect_f32(value)?);
Ok(())
}
_ => base_property_set(self, name, value),
}
}
fn property_names(&self) -> &'static [&'static str] {
property_names_of!["zoom_level", "min", "max", BASE_PROPERTY_NAMES]
}
}
impl EventHandler for ZoomControl {
fn handle_event(&mut self, event: &Event) {
self.base.handle_event(event);
if !self.base.is_enabled() {
return;
}
match event {
Event::MousePress { pos, button: 1, .. } => match self.hit_button(*pos) {
Some(ZoomButton::Out) => {
self.zoom_out();
}
Some(ZoomButton::Reset) => {
self.reset();
}
Some(ZoomButton::In) => {
self.zoom_in();
}
None => { }
},
Event::Wheel { delta, .. } => {
if delta.y < 0 {
self.zoom_in();
} else if delta.y > 0 {
self.zoom_out();
}
}
_ => { }
}
}
}
impl Draw for ZoomControl {
fn draw(&mut self, context: &mut RenderContext) {
let rect = self.geometry();
let style = self.base.style();
let (surface, border, ink) = companion_palette(style, "zoom_control");
context.face_with_gradient(rect, surface, None, Default::default(), 0, Color::BLACK);
crate::widget::draw::stroke_surface_border(
context,
rect,
self.style().border_radius.unwrap_or(0),
border,
1,
);
for button in [ZoomButton::Out, ZoomButton::Reset, ZoomButton::In] {
let button_rect = self.button_rect(button);
context.face_with_gradient(
button_rect,
surface,
None,
Default::default(),
2,
Color::BLACK,
);
crate::widget::draw::stroke_surface_border(context, button_rect, 2, border, 1);
let label = match button {
ZoomButton::Out => "-",
ZoomButton::Reset => "1:1",
ZoomButton::In => "+",
};
let font = effective_font(style);
let metrics = context.measure_text(label, font);
context.draw_text(
Point::new(
button_rect.x + (button_rect.width as i32 - metrics.width as i32) / 2,
button_rect.y + (button_rect.height as i32 - metrics.height as i32) / 2,
),
label,
font,
ink,
HorizontalAlignment::Left,
);
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ScaleUnits {
#[default]
Metric,
Imperial,
}
impl ScaleUnits {
pub fn token(self) -> &'static str {
match self {
Self::Metric => "metric",
Self::Imperial => "imperial",
}
}
pub fn from_token(token: &str) -> Option<Self> {
match token {
"metric" => Some(Self::Metric),
"imperial" => Some(Self::Imperial),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ScaleBarStyle {
#[default]
Plain,
Boxed,
}
impl ScaleBarStyle {
pub fn token(self) -> &'static str {
match self {
Self::Plain => "plain",
Self::Boxed => "boxed",
}
}
pub fn from_token(token: &str) -> Option<Self> {
match token {
"plain" => Some(Self::Plain),
"boxed" => Some(Self::Boxed),
_ => None,
}
}
}
const SCALE_BAR_PADDING: f64 = 8.0;
pub struct ScaleBar {
base: BaseWidget,
meters_per_pixel: f64,
units: ScaleUnits,
style: ScaleBarStyle,
}
impl ScaleBar {
pub fn new(geometry: Rect) -> Self {
Self {
base: BaseWidget::new(WidgetKind::ScaleBar, geometry, "ScaleBar"),
meters_per_pixel: 1.0,
units: ScaleUnits::Metric,
style: ScaleBarStyle::Plain,
}
}
pub fn meters_per_pixel(&self) -> f64 {
self.meters_per_pixel
}
pub fn set_scale(&mut self, meters_per_pixel: f64) {
if !meters_per_pixel.is_finite() || meters_per_pixel <= 0.0 {
return;
}
if (self.meters_per_pixel - meters_per_pixel).abs() < f64::EPSILON {
return;
}
self.meters_per_pixel = meters_per_pixel;
self.base.request_redraw();
}
pub fn units(&self) -> ScaleUnits {
self.units
}
pub fn set_units(&mut self, units: ScaleUnits) {
if self.units == units {
return;
}
self.units = units;
self.base.request_redraw();
}
pub fn style(&self) -> ScaleBarStyle {
self.style
}
pub fn set_style(&mut self, style: ScaleBarStyle) {
if self.style == style {
return;
}
self.style = style;
self.base.request_redraw();
}
pub fn bar_distance(&self, max_px: f64) -> f64 {
if !max_px.is_finite() || max_px <= 0.0 {
return 0.0;
}
nice_floor(self.meters_per_pixel * max_px)
}
pub fn label(&self) -> String {
let available = (f64::from(self.geometry().width) - 2.0 * SCALE_BAR_PADDING).max(16.0);
format_scale_distance(self.units, self.bar_distance(available))
}
pub fn bar_pixels(&self) -> u32 {
let available = (f64::from(self.geometry().width) - 2.0 * SCALE_BAR_PADDING).max(16.0);
let distance = self.bar_distance(available);
if distance <= 0.0 {
return 0;
}
(distance / self.meters_per_pixel).round().clamp(0.0, available) as u32
}
}
fn nice_floor(value: f64) -> f64 {
if !value.is_finite() || value <= 0.0 {
return 0.0;
}
let magnitude = 10f64.powf(value.log10().floor());
let mantissa = value / magnitude;
let step = if mantissa >= 5.0 {
5.0
} else if mantissa >= 2.0 {
2.0
} else {
1.0
};
step * magnitude
}
fn format_scale_distance(units: ScaleUnits, meters: f64) -> String {
if !meters.is_finite() || meters <= 0.0 {
return String::new();
}
match units {
ScaleUnits::Metric => {
if meters >= 1000.0 {
format_scaled(meters / 1000.0, "km")
} else {
format_scaled(meters, "m")
}
}
ScaleUnits::Imperial => {
let feet = meters * 3.280_839_895;
if feet >= 5280.0 {
format_scaled(feet / 5280.0, "mi")
} else if feet >= 1.0 {
format_scaled(feet, "ft")
} else {
format_scaled(feet * 12.0, "in")
}
}
}
}
fn format_scaled(value: f64, unit: &str) -> String {
if (value - value.round()).abs() < 0.05 {
format!("{:.0} {unit}", value)
} else {
format!("{:.1} {unit}", value)
}
}
impl Widget for ScaleBar {
fn base(&self) -> &BaseWidget {
&self.base
}
fn base_mut(&mut self) -> &mut BaseWidget {
&mut self.base
}
fn set_state_theme_hook(&mut self) {
crate::style::reapply_active_theme_state(self);
}
fn size_hint(&self) -> crate::core::Size {
crate::core::Size::new(120, 28)
}
impl_draw_bridge!();
impl_widget_property_hooks!();
}
impl WidgetProperties for ScaleBar {
fn get(&self, name: &str) -> Result<CapabilityValue, CapabilityAccessError> {
match name {
"meters_per_pixel" => Ok(CapabilityValue::Float(self.meters_per_pixel)),
"units" => Ok(CapabilityValue::String(self.units.token().to_string())),
"style" => Ok(CapabilityValue::String(self.style.token().to_string())),
_ => base_property_get(self, name),
}
}
fn set(&mut self, name: &str, value: CapabilityValue) -> Result<(), CapabilityAccessError> {
match name {
"meters_per_pixel" => {
self.set_scale(expect_f64(value)?);
Ok(())
}
"units" => {
let token = expect_string(value)?;
let units =
ScaleUnits::from_token(&token).ok_or(CapabilityAccessError::TypeMismatch)?;
self.set_units(units);
Ok(())
}
"style" => {
let token = expect_string(value)?;
let style =
ScaleBarStyle::from_token(&token).ok_or(CapabilityAccessError::TypeMismatch)?;
self.set_style(style);
Ok(())
}
_ => base_property_set(self, name, value),
}
}
fn property_names(&self) -> &'static [&'static str] {
property_names_of!["meters_per_pixel", "units", "style", BASE_PROPERTY_NAMES]
}
}
impl EventHandler for ScaleBar {
fn handle_event(&mut self, event: &Event) {
self.base.handle_event(event);
}
}
impl Draw for ScaleBar {
fn draw(&mut self, context: &mut RenderContext) {
let rect = self.geometry();
let style = self.base.style();
let (surface, border, ink) = companion_palette(style, "scale_bar");
if self.style == ScaleBarStyle::Boxed {
context.face_with_gradient(rect, surface, None, Default::default(), 3, Color::BLACK);
crate::widget::draw::stroke_surface_border(
context, rect,
3, border, 1,
);
}
let width = self.bar_pixels();
if width == 0 {
return;
}
let left = rect.x + SCALE_BAR_PADDING as i32;
let bottom = rect.y + rect.height as i32 - 6;
let right = left + width as i32;
context.draw_line(Point::new(left, bottom), Point::new(right, bottom), ink);
for x in [left, right] {
context.draw_line(Point::new(x, bottom - 3), Point::new(x, bottom), ink);
}
let label = self.label();
if !label.is_empty() {
context.draw_text(
Point::new(left, bottom - 16),
&label,
effective_font(style),
ink,
HorizontalAlignment::Left,
);
}
}
}
pub struct Compass {
base: BaseWidget,
bearing_deg: f32,
clickable: bool,
pub reset_north_requested: GenericSignal,
}
impl Compass {
pub fn new(geometry: Rect) -> Self {
Self {
base: BaseWidget::new(WidgetKind::Compass, geometry, "Compass"),
bearing_deg: 0.0,
clickable: true,
reset_north_requested: GenericSignal::new(),
}
}
pub fn bearing_deg(&self) -> f32 {
self.bearing_deg
}
pub fn set_bearing(&mut self, bearing_deg: f32) {
if !bearing_deg.is_finite() {
return;
}
let normalized = bearing_deg.rem_euclid(360.0);
if (self.bearing_deg - normalized).abs() < f32::EPSILON {
return;
}
self.bearing_deg = normalized;
self.base.request_redraw();
}
pub fn clickable(&self) -> bool {
self.clickable
}
pub fn set_clickable(&mut self, clickable: bool) {
if self.clickable == clickable {
return;
}
self.clickable = clickable;
self.base.request_redraw();
}
pub fn hit_dial(&self, point: Point) -> bool {
let rect = self.geometry();
let center = Point::new(rect.x + rect.width as i32 / 2, rect.y + rect.height as i32 / 2);
let radius = i64::from(self.radius());
let dx = i64::from(point.x) - i64::from(center.x);
let dy = i64::from(point.y) - i64::from(center.y);
dx * dx + dy * dy <= radius * radius
}
fn radius(&self) -> u32 {
let rect = self.geometry();
(rect.width.min(rect.height) as i32 / 2 - 2).max(4) as u32
}
}
impl Widget for Compass {
fn base(&self) -> &BaseWidget {
&self.base
}
fn base_mut(&mut self) -> &mut BaseWidget {
&mut self.base
}
fn set_state_theme_hook(&mut self) {
crate::style::reapply_active_theme_state(self);
}
fn size_hint(&self) -> crate::core::Size {
crate::core::Size::new(48, 48)
}
impl_draw_bridge!();
impl_widget_property_hooks!();
fn event_signal_dyn(&self, name: &str) -> Option<crate::signal::EventSignalRef> {
use crate::signal::EventSignalRef;
match name {
"reset_north_requested" => {
Some(EventSignalRef::unit("reset_north_requested", &self.reset_north_requested))
}
_ => None,
}
}
}
impl WidgetProperties for Compass {
fn get(&self, name: &str) -> Result<CapabilityValue, CapabilityAccessError> {
match name {
"bearing_deg" => Ok(CapabilityValue::Float(f64::from(self.bearing_deg))),
"clickable" => Ok(CapabilityValue::Bool(self.clickable)),
_ => base_property_get(self, name),
}
}
fn set(&mut self, name: &str, value: CapabilityValue) -> Result<(), CapabilityAccessError> {
match name {
"bearing_deg" => {
self.set_bearing(expect_f32(value)?);
Ok(())
}
"clickable" => {
self.set_clickable(expect_bool(value)?);
Ok(())
}
_ => base_property_set(self, name, value),
}
}
fn property_names(&self) -> &'static [&'static str] {
property_names_of!["bearing_deg", "clickable", BASE_PROPERTY_NAMES]
}
}
impl EventHandler for Compass {
fn handle_event(&mut self, event: &Event) {
self.base.handle_event(event);
if !self.base.is_enabled() {
return;
}
if let Event::MousePress { pos, button: 1, .. } = event {
if self.clickable && self.hit_dial(*pos) {
self.reset_north_requested.emit();
self.base.request_redraw();
}
}
}
}
impl Draw for Compass {
fn draw(&mut self, context: &mut RenderContext) {
let rect = self.geometry();
let style = self.base.style();
let (surface, border, ink) = companion_palette(style, "compass");
let center = rect.center();
let radius = self.radius();
context.fill_circle(center, radius, surface);
context.draw_circle(center, radius, border);
context.draw_text(
Point::new(center.x - 3, rect.y + 2),
"N",
effective_font(style),
ink,
HorizontalAlignment::Left,
);
let angle = self.bearing_deg.to_radians();
let length = radius as f32 - 2.0;
let tip = Point::new(
center.x + (angle.sin() * length) as i32,
center.y - (angle.cos() * length) as i32,
);
context.draw_line(center, tip, ink);
let head = (radius as f32 * 0.35).max(4.0);
for offset in [0.45f32, -0.45] {
let wing = angle + offset;
context.draw_line(
tip,
Point::new(tip.x - (wing.sin() * head) as i32, tip.y + (wing.cos() * head) as i32),
ink,
);
}
}
}