[gnome-weather] ui: Make thermometer labels sticky to the scale



commit d438a23bf717a9296b6ad68dad327782d307dcbf
Author: Vitaly Dyachkov <obyknovenius me com>
Date:   Sat Jun 18 16:06:10 2022 +0200

    ui: Make thermometer labels sticky to the scale

 src/app/thermometer.js                     | 300 +++++++++++++++++++----------
 src/app/thermometer.ui                     |  27 ---
 src/org.gnome.Weather.src.gresource.xml.in |   1 -
 3 files changed, 195 insertions(+), 133 deletions(-)
---
diff --git a/src/app/thermometer.js b/src/app/thermometer.js
index 9605ed6..dca14af 100644
--- a/src/app/thermometer.js
+++ b/src/app/thermometer.js
@@ -1,6 +1,6 @@
 /* thermometer.js
  *
- * Copyright 2021 Vitaly Dyachkov <obyknovenius me com>
+ * Copyright 2021-2022 Vitaly Dyachkov <obyknovenius me com>
  * Copyright 2022 Evan Welsh <contact evanwelsh com>
  *
  * This program is free software: you can redistribute it and/or modify
@@ -22,143 +22,233 @@
 import GLib from 'gi://GLib';
 import GObject from 'gi://GObject';
 import Gtk from 'gi://Gtk';
+import Gdk from 'gi://Gdk';
 import Graphene from 'gi://Graphene';
 import Gsk from 'gi://Gsk';
 
 import * as Util from '../misc/util.js';
 
 export class TemperatureRange {
-  dailyLow;
-  dailyHigh;
-  weeklyLow;
-  weeklyHigh;
-
-  constructor({ dailyLow, dailyHigh, weeklyLow, weeklyHigh }) {
-    this.dailyLow = dailyLow;
-    this.dailyHigh = dailyHigh;
-    this.weeklyLow = weeklyLow;
-    this.weeklyHigh = weeklyHigh;
-  }
+
+    dailyLow;
+    dailyHigh;
+    weeklyLow;
+    weeklyHigh;
+
+    constructor({ dailyLow, dailyHigh, weeklyLow, weeklyHigh }) {
+        this.dailyLow = dailyLow;
+        this.dailyHigh = dailyHigh;
+        this.weeklyLow = weeklyLow;
+        this.weeklyHigh = weeklyHigh;
+    }
 }
 
-GObject.registerClass({
-  CssName: 'WeatherThermometerScale',
-  Properties: {
-    'range': GObject.ParamSpec.jsobject(
-      'range',
-      'range',
-      'The TemperatureRange instance representing this thermometer scale',
-      GObject.ParamFlags.READWRITE,
-    ),
-  },
+const ThermometerScale = GObject.registerClass({
+    CssName: 'WeatherThermometerScale',
+    Properties: {
+        'range': GObject.ParamSpec.jsobject(
+            'range',
+            'range',
+            'The TemperatureRange instance representing this thermometer scale',
+            GObject.ParamFlags.READWRITE,
+        ),
+    },
 }, class ThermometerScale extends Gtk.Widget {
 
-  constructor({ range = null, ...params }) {
-    super({
-      vexpand: true,
-      halign: Gtk.Align.FILL,
-      overflow: Gtk.Overflow.HIDDEN,
-      ...params
-    });
+    minHeight = 64;
+    radius = 12;
+
+    constructor({ range = null, ...params }) {
+        super(params);
 
-    this.range = range;
-  }
+        this.range = range;
+    }
 
-  vfunc_map() {
-    super.vfunc_map();
+    vfunc_map() {
+        super.vfunc_map();
 
-    this._rangeChangedId = this.connect('notify::range', () => {
-      this.queue_draw();
-    });
-  }
+        this._rangeChangedId = this.connect('notify::range', () => {
+            this.queue_draw();
+        });
+    }
 
-  vfunc_unmap() {
-    this.disconnect(this._rangeChangedId);
+    vfunc_unmap() {
+        this.disconnect(this._rangeChangedId);
 
-    super.vfunc_unmap();
-  }
+        super.vfunc_unmap();
+    }
 
-  vfunc_snapshot(snapshot) {
-    super.vfunc_snapshot(snapshot);
+    vfunc_snapshot(snapshot) {
+        super.vfunc_snapshot(snapshot);
 
-    if (!this.range) return;
+        if (!this.range)
+            return;
 
-    const { width, height } = this.get_allocation();
+        const { width, height } = this.get_allocation();
 
-    // Don't render when allocation is shorter than 64
-    if (height < 64) return;
+        // Don't render when allocation is shorter than the minimal height
+        if (height < this.minHeight)
+            return;
 
-    const { dailyHigh, dailyLow, weeklyHigh, weeklyLow } = this.range;
+        const { dailyHigh, dailyLow, weeklyHigh, weeklyLow } = this.range;
 
-    const scaleRadius = 12;
-    const scaleFactor = (height - scaleRadius * 2) / (weeklyHigh - weeklyLow);
+        const radius = this.radius;
+        const factor = (height - 2 * radius) / (weeklyHigh - weeklyLow);
 
-    const scaleWidth = 24;
-    const scaleHeight = scaleFactor * (dailyHigh - dailyLow);
+        const gradientWidth = 2 * radius;
+        const gradientHeight = factor * (dailyHigh - dailyLow);
 
-    const x = (width - scaleWidth) / 2;
-    const y = scaleRadius + scaleFactor * (weeklyHigh - dailyHigh);
+        const x = (width - gradientWidth) / 2;
+        const y = radius + factor * (weeklyHigh - dailyHigh);
 
-    const bounds = new Graphene.Rect();
-    bounds.init(x, y - scaleRadius, scaleWidth, scaleHeight + 2 * scaleRadius);
+        const bounds = new Graphene.Rect();
+        bounds.init(x, y - radius, gradientWidth, gradientHeight + 2 * radius);
 
-    const outline = new Gsk.RoundedRect();
-    outline.init_from_rect(bounds, scaleRadius);
+        const outline = new Gsk.RoundedRect();
+        outline.init_from_rect(bounds, radius);
 
-    snapshot.push_rounded_clip(outline);
+        snapshot.push_rounded_clip(outline);
 
-    const [, warmColor] = this.get_style_context().lookup_color('weather_thermometer_warm_color');
-    const [, coolColor] = this.get_style_context().lookup_color('weather_thermometer_cold_color');
+        const [, warmColor] = this.get_style_context()
+            .lookup_color('weather_thermometer_warm_color');
 
-    snapshot.append_linear_gradient(
-      bounds,
-      new Graphene.Point({ x: x + scaleWidth / 2, y: 0 }),
-      new Graphene.Point({ x: x + scaleWidth / 2, y: height }),
-      [
-        new Gsk.ColorStop({ offset: 0.0, color: warmColor }),
-        new Gsk.ColorStop({ offset: 1.0, color: coolColor })
-      ]
-    );
+        const [, coolColor] = this.get_style_context()
+            .lookup_color('weather_thermometer_cold_color');
 
-    snapshot.pop();
-  }
+        snapshot.append_linear_gradient(
+            bounds,
+            new Graphene.Point({ x: x + gradientWidth / 2, y: 0 }),
+            new Graphene.Point({ x: x + gradientWidth / 2, y: height }),
+            [
+                new Gsk.ColorStop({ offset: 0.0, color: warmColor }),
+                new Gsk.ColorStop({ offset: 1.0, color: coolColor })
+            ]
+        );
+
+        snapshot.pop();
+    }
 });
 
 export const Thermometer = GObject.registerClass({
-  CssName: 'WeatherThermometer',
-  Template: GLib.Uri.resolve_relative(import.meta.url, './thermometer.ui', 0),
-  InternalChildren: ['scale', 'highLabel', 'lowLabel'],
-  Properties: {
-    'range': GObject.ParamSpec.jsobject(
-      'range',
-      'range',
-      'The TemperatureRange instance representing this thermometer scale',
-      GObject.ParamFlags.READWRITE,
-    ),
-  },
+    CssName: 'WeatherThermometer',
+    Properties: {
+        'range': GObject.ParamSpec.jsobject(
+            'range',
+            'range',
+            'The TemperatureRange instance representing this thermometer scale',
+            GObject.ParamFlags.READWRITE,
+        ),
+    },
 }, class Thermometer extends Gtk.Widget {
-  constructor({ ...params }) {
-    super(params);
-
-    Object.assign(this.layoutManager, {
-      orientation: Gtk.Orientation.VERTICAL,
-      spacing: 20
-    });
-  }
-
-  vfunc_root() {
-    super.vfunc_root();
-
-    this.bind_property('range', this._scale, 'range', GObject.BindingFlags.DEFAULT);
-
-    this.bind_property_full('range', this._lowLabel, 'label', GObject.BindingFlags.DEFAULT, (_, range) => {
-      return [!!range, Util.formatTemperature(range?.dailyLow) ?? ''];
-    }, null);
 
-    this.bind_property_full('range', this._highLabel, 'label', GObject.BindingFlags.DEFAULT, (_, range) => {
-      return [!!range, Util.formatTemperature(range?.dailyHigh) ?? ''];
-    }, null);
-  }
+    #highLabel;
+    #lowLabel;
+    #scale;
+
+    spacing = 18;
+
+    constructor({ ...params }) {
+        super(params);
+
+        this.#highLabel = new Gtk.Label({
+            css_classes: ['high', 'body'],
+        });
+        this.#highLabel.set_parent(this);
+
+        this.#lowLabel = new Gtk.Label({
+            css_classes: ['low', 'body'],
+        });
+        this.#lowLabel.set_parent(this);
+
+        this.#scale = new ThermometerScale({});
+        this.#scale.set_parent(this);
+    }
+
+    vfunc_measure(orientation, for_size) {
+        const [highMin, highNat, highMinBaseline, highNatBaseline] =
+            this.#highLabel.measure(orientation, for_size);
+
+        const [lowMin, lowNat, lowMinBaseline, lowNatBaseline] =
+            this.#lowLabel.measure(orientation, for_size);
+
+        if (orientation === Gtk.Orientation.HORIZONTAL) {
+            return [
+                Math.max(highMin, lowMin),
+                Math.max(highNat, lowNat),
+                Math.max(highMinBaseline, lowMinBaseline),
+                Math.max(highNatBaseline, lowNatBaseline)
+            ];
+        } else {
+            const spacing = this.spacing;
+            return [
+                highMin + spacing + lowMin,
+                highNat + spacing + lowNat,
+                highMinBaseline + spacing + lowMinBaseline,
+                highNatBaseline + spacing + lowNatBaseline
+            ];
+        }
+    }
+
+    vfunc_size_allocate(width, height, baseline) {
+        const [highMin, highNat] = this.#highLabel.get_preferred_size();
+        const [lowMin, lowNat] = this.#lowLabel.get_preferred_size();
+
+        const spacing = this.spacing;
+
+        const scaleHeight = Math.max(
+            0,
+            height - (highNat.height + lowNat.height) - 2 * spacing);
+
+        const scaleRect = new Gdk.Rectangle({
+            height: scaleHeight,
+            width,
+            x: 0,
+            y:highNat.height + spacing,
+        });
+        this.#scale.size_allocate(scaleRect, -1);
+
+        let highY = 0;
+        let lowY = height - lowNat.height;
+
+        if (scaleHeight >= this.#scale.minHeight) {
+            const { dailyHigh, dailyLow, weeklyHigh, weeklyLow } = this.range;
+
+            const radius = this.#scale.radius;
+            const factor = (scaleHeight - 2 * radius) / (weeklyHigh - weeklyLow);
+
+            highY += (weeklyHigh - dailyHigh) * factor;
+            lowY -= (dailyLow - weeklyLow) * factor;
+        }
+
+        const highRect = new Gdk.Rectangle({
+            height: highNat.height,
+            width: highNat.width,
+            x: (width - highNat.width) / 2,
+            y: highY,
+        });
+        this.#highLabel.size_allocate(highRect, -1);
+
+        const lowRect = new Gdk.Rectangle({
+            height: lowNat.height,
+            width: lowNat.width,
+            x: (width - lowNat.width) / 2,
+            y: lowY,
+        });
+        this.#lowLabel.size_allocate(lowRect, -1);
+    }
+
+    vfunc_root() {
+        super.vfunc_root();
+
+        this.bind_property('range', this.#scale,'range', GObject.BindingFlags.DEFAULT);
+
+        this.bind_property_full('range', this.#lowLabel, 'label', GObject.BindingFlags.DEFAULT, (_, range) 
=> {
+            return [!!range, Util.formatTemperature(range?.dailyLow) ?? ''];
+        }, null);
+
+        this.bind_property_full('range', this.#highLabel, 'label', GObject.BindingFlags.DEFAULT, (_, range) 
=> {
+            return [!!range, Util.formatTemperature(range?.dailyHigh) ?? ''];
+        }, null);
+    }
 });
 
-Thermometer.set_layout_manager_type(Gtk.BoxLayout);
diff --git a/src/org.gnome.Weather.src.gresource.xml.in b/src/org.gnome.Weather.src.gresource.xml.in
index 1631802..2b2178e 100644
--- a/src/org.gnome.Weather.src.gresource.xml.in
+++ b/src/org.gnome.Weather.src.gresource.xml.in
@@ -8,7 +8,6 @@
     <file>app/locationRow.js</file>
     <file>app/locationRow.ui</file>
     <file>app/thermometer.js</file>
-    <file>app/thermometer.ui</file>
     <file>app/dailyForecast.js</file>
     <file>app/entry.js</file>
     <file>app/main.js</file>


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]