Initial commit
This commit is contained in:
772
src/graph_widget.rs
Normal file
772
src/graph_widget.rs
Normal file
@@ -0,0 +1,772 @@
|
||||
/*
|
||||
* Original code stolen from Mission Center. It's a really cool program, you should check it out!
|
||||
* https://gitlab.com/mission-center-devs/mission-center/-/blob/9cac2b5ce9cac10323475217512c244c5a8e8673/src/performance_page/widgets/graph_widget.rs
|
||||
* performance_page/widgets/graph_widget.rs
|
||||
|
||||
* Copyright 2024 Romeo Calota
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
use std::cell::Cell;
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use glib::{ParamSpec, Properties, Value};
|
||||
use gtk::{
|
||||
gdk,
|
||||
gdk::prelude::*,
|
||||
glib::{
|
||||
self,
|
||||
subclass::{prelude::*, Signal},
|
||||
},
|
||||
graphene,
|
||||
gsk::{self, FillRule, PathBuilder, Stroke},
|
||||
prelude::*,
|
||||
subclass::prelude::*,
|
||||
Snapshot,
|
||||
};
|
||||
|
||||
pub use imp::DataSetDescriptor;
|
||||
|
||||
const GRAPH_RADIUS: f32 = 7.;
|
||||
|
||||
/// Values are truncated to the minimum and maximum values
|
||||
const NO_SCALING: i32 = 0;
|
||||
/// The graph min and max values are adjusted to the incoming data
|
||||
const AUTO_SCALING: i32 = 1;
|
||||
/// The graph min and max values are adjusted to the next power of 2
|
||||
const AUTO_POW2_SCALING: i32 = 2;
|
||||
/// The graph min and max values are hardcoded to the range [0, 1], and all values are normalized to this range
|
||||
const NORMALIZED_SCALING: i32 = 3;
|
||||
|
||||
mod imp {
|
||||
use super::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DataSetDescriptor {
|
||||
pub dashed: bool,
|
||||
pub fill: bool,
|
||||
pub visible: bool,
|
||||
|
||||
pub data_set: Vec<f32>,
|
||||
pub max_all_time: f32,
|
||||
}
|
||||
|
||||
#[derive(Properties)]
|
||||
#[properties(wrapper_type = super::GraphWidget)]
|
||||
pub struct GraphWidget {
|
||||
#[property(get, set = Self::set_data_points)]
|
||||
data_points: Cell<u32>,
|
||||
#[property(get, set = Self::set_data_sets)]
|
||||
data_set_count: Cell<u32>,
|
||||
#[property(get, set = Self::set_value_range_min)]
|
||||
value_range_min: Cell<f32>,
|
||||
#[property(get, set = Self::set_value_range_max)]
|
||||
value_range_max: Cell<f32>,
|
||||
#[property(get, set = Self::set_scaling)]
|
||||
scaling: Cell<i32>,
|
||||
#[property(get, set)]
|
||||
only_scale_up: Cell<bool>,
|
||||
#[property(get, set)]
|
||||
grid_visible: Cell<bool>,
|
||||
#[property(get, set)]
|
||||
scroll: Cell<bool>,
|
||||
#[property(get, set = Self::set_smooth_graphs)]
|
||||
smooth_graphs: Cell<bool>,
|
||||
#[property(get, set)]
|
||||
base_color: Cell<gdk::RGBA>,
|
||||
#[property(get, set = Self::set_horizontal_line_count)]
|
||||
horizontal_line_count: Cell<u32>,
|
||||
#[property(get, set = Self::set_vertical_line_count)]
|
||||
vertical_line_count: Cell<u32>,
|
||||
|
||||
pub data_sets: Cell<Vec<DataSetDescriptor>>,
|
||||
|
||||
scroll_offset: Cell<f32>,
|
||||
prev_size: Cell<(i32, i32)>,
|
||||
}
|
||||
|
||||
impl Default for GraphWidget {
|
||||
fn default() -> Self {
|
||||
const DATA_SET_LEN_DEFAULT: usize = 60;
|
||||
println!("Graph Widget loaded");
|
||||
|
||||
let mut data_set = vec![0.0; DATA_SET_LEN_DEFAULT];
|
||||
data_set.reserve(1);
|
||||
|
||||
Self {
|
||||
data_points: Cell::new(DATA_SET_LEN_DEFAULT as _),
|
||||
data_set_count: Cell::new(1),
|
||||
value_range_min: Cell::new(0.),
|
||||
value_range_max: Cell::new(100.),
|
||||
scaling: Cell::new(NO_SCALING),
|
||||
only_scale_up: Cell::new(false),
|
||||
grid_visible: Cell::new(true),
|
||||
scroll: Cell::new(false),
|
||||
smooth_graphs: Cell::new(false),
|
||||
base_color: Cell::new(gdk::RGBA::new(0., 0., 0., 1.)),
|
||||
horizontal_line_count: Cell::new(9),
|
||||
vertical_line_count: Cell::new(6),
|
||||
|
||||
data_sets: Cell::new(vec![DataSetDescriptor {
|
||||
dashed: false,
|
||||
fill: true,
|
||||
visible: true,
|
||||
|
||||
data_set,
|
||||
max_all_time: 0.,
|
||||
}]),
|
||||
|
||||
scroll_offset: Cell::new(0.),
|
||||
prev_size: Cell::new((0, 0)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GraphWidget {
|
||||
fn set_value_range_min(&self, min: f32) {
|
||||
if self.scaling.get() == NORMALIZED_SCALING {
|
||||
self.value_range_min.set(0.);
|
||||
return;
|
||||
}
|
||||
|
||||
self.value_range_min.set(min);
|
||||
}
|
||||
|
||||
fn set_value_range_max(&self, max: f32) {
|
||||
if self.scaling.get() == NORMALIZED_SCALING {
|
||||
self.value_range_max.set(1.);
|
||||
return;
|
||||
}
|
||||
|
||||
self.value_range_max.set(max);
|
||||
}
|
||||
|
||||
fn set_scaling(&self, scaling: i32) {
|
||||
match scaling {
|
||||
NO_SCALING => {
|
||||
self.scaling.set(NO_SCALING);
|
||||
let mut data_sets = self.data_sets.take();
|
||||
|
||||
for values in data_sets.iter_mut() {
|
||||
for value in values.data_set.iter_mut() {
|
||||
*value =
|
||||
value.clamp(self.value_range_min.get(), self.value_range_max.get());
|
||||
}
|
||||
}
|
||||
|
||||
self.data_sets.set(data_sets);
|
||||
}
|
||||
AUTO_SCALING..=AUTO_POW2_SCALING => {
|
||||
self.scaling.set(scaling);
|
||||
|
||||
let mut data_sets = self.data_sets.take();
|
||||
|
||||
for values in data_sets.iter_mut() {
|
||||
for value in values.data_set.iter_mut() {
|
||||
*value = value.max(self.value_range_min.get());
|
||||
}
|
||||
}
|
||||
|
||||
self.data_sets.set(data_sets);
|
||||
}
|
||||
NORMALIZED_SCALING => {
|
||||
self.value_range_min.set(0.);
|
||||
self.value_range_max.set(1.);
|
||||
self.scaling.set(NORMALIZED_SCALING);
|
||||
}
|
||||
_ => self.scaling.set(NO_SCALING),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_data_points(&self, count: u32) {
|
||||
if self.data_points.take() != count {
|
||||
let mut data_points = self.data_sets.take();
|
||||
for values in data_points.iter_mut() {
|
||||
if count == (values.data_set.len() as u32) {
|
||||
continue;
|
||||
}
|
||||
// we need to truncate from the correct side
|
||||
values.data_set.reverse();
|
||||
values.data_set.resize(count as _, 0.);
|
||||
values.data_set.reverse();
|
||||
}
|
||||
self.data_sets.set(data_points);
|
||||
}
|
||||
self.data_points.set(count);
|
||||
}
|
||||
|
||||
fn set_data_sets(&self, count: u32) {
|
||||
let mut data_points = self.data_sets.take();
|
||||
data_points.resize(
|
||||
count as _,
|
||||
DataSetDescriptor {
|
||||
dashed: false,
|
||||
fill: true,
|
||||
visible: true,
|
||||
|
||||
data_set: vec![0.; self.data_points.get() as _],
|
||||
max_all_time: 0.,
|
||||
},
|
||||
);
|
||||
self.data_sets.set(data_points);
|
||||
|
||||
self.data_set_count.set(count);
|
||||
}
|
||||
|
||||
fn set_horizontal_line_count(&self, count: u32) {
|
||||
if self.horizontal_line_count.get() != count {
|
||||
self.horizontal_line_count.set(count);
|
||||
self.obj().upcast_ref::<super::GraphWidget>().queue_draw();
|
||||
}
|
||||
}
|
||||
|
||||
fn set_vertical_line_count(&self, count: u32) {
|
||||
if self.vertical_line_count.get() != count {
|
||||
self.vertical_line_count.set(count);
|
||||
self.obj().upcast_ref::<super::GraphWidget>().queue_draw();
|
||||
}
|
||||
}
|
||||
|
||||
fn set_smooth_graphs(&self, smooth: bool) {
|
||||
if self.smooth_graphs.get() != smooth {
|
||||
self.smooth_graphs.set(smooth);
|
||||
self.obj().upcast_ref::<super::GraphWidget>().queue_draw();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GraphWidget {
|
||||
#[inline]
|
||||
fn draw_outline(&self, snapshot: &Snapshot, bounds: &gsk::RoundedRect, color: &gdk::RGBA) {
|
||||
let stroke_color = gdk::RGBA::new(color.red(), color.green(), color.blue(), 1.);
|
||||
snapshot.append_border(&bounds, &[1.; 4], &[stroke_color.clone(); 4]);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn draw_grid(
|
||||
&self,
|
||||
snapshot: &Snapshot,
|
||||
width: f32,
|
||||
height: f32,
|
||||
scale_factor: f64,
|
||||
data_point_count: usize,
|
||||
color: &gdk::RGBA,
|
||||
) {
|
||||
let scale_factor = scale_factor as f32;
|
||||
let color = gdk::RGBA::new(color.red(), color.green(), color.blue(), 51. / 256.);
|
||||
|
||||
let stroke = Stroke::new(1.);
|
||||
|
||||
// Draw horizontal lines
|
||||
let horizontal_line_count = self.obj().horizontal_line_count() + 1;
|
||||
|
||||
let col_width = width - scale_factor;
|
||||
let col_height = height / horizontal_line_count as f32;
|
||||
|
||||
for i in 1..horizontal_line_count {
|
||||
let path_builder = PathBuilder::new();
|
||||
path_builder.move_to(scale_factor / 2., col_height * i as f32);
|
||||
path_builder.line_to(col_width, col_height * i as f32);
|
||||
snapshot.append_stroke(&path_builder.to_path(), &stroke, &color);
|
||||
}
|
||||
|
||||
// Draw vertical lines
|
||||
let mut vertical_line_count = self.obj().vertical_line_count() + 1;
|
||||
|
||||
let col_width = width / vertical_line_count as f32;
|
||||
let col_height = height - scale_factor;
|
||||
|
||||
let x_offset = if self.obj().scroll() {
|
||||
vertical_line_count += 1;
|
||||
|
||||
let mut x_offset = self.scroll_offset.get();
|
||||
x_offset += width / data_point_count as f32;
|
||||
x_offset %= col_width;
|
||||
self.scroll_offset.set(x_offset);
|
||||
|
||||
x_offset
|
||||
} else {
|
||||
0.
|
||||
};
|
||||
|
||||
for i in 1..vertical_line_count {
|
||||
let path_builder = PathBuilder::new();
|
||||
path_builder.move_to(col_width * i as f32 - x_offset, scale_factor / 2.);
|
||||
path_builder.line_to(col_width * i as f32 - x_offset, col_height);
|
||||
snapshot.append_stroke(&path_builder.to_path(), &stroke, &color);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn plot_values(
|
||||
&self,
|
||||
snapshot: &Snapshot,
|
||||
width: f32,
|
||||
height: f32,
|
||||
scale_factor: f64,
|
||||
data_points: &mut DataSetDescriptor,
|
||||
color: &gdk::RGBA,
|
||||
) {
|
||||
let scale_factor = scale_factor as f32;
|
||||
let stroke_color = gdk::RGBA::new(color.red(), color.green(), color.blue(), 1.);
|
||||
let fill_color = gdk::RGBA::new(color.red(), color.green(), color.blue(), 100. / 256.);
|
||||
|
||||
let stroke = Stroke::new(1.);
|
||||
|
||||
let val_max = self.value_range_max.get() - self.value_range_min.get();
|
||||
let val_min = 0.;
|
||||
|
||||
let spacing_x = width / (data_points.data_set.len() - 1) as f32;
|
||||
|
||||
let mut points: Vec<(f32, f32)> = if self.scaling.get() != NORMALIZED_SCALING {
|
||||
(0..)
|
||||
.map(|x| x as f32)
|
||||
.zip(
|
||||
data_points
|
||||
.data_set
|
||||
.iter()
|
||||
.map(|x| *x - self.value_range_min.get()),
|
||||
)
|
||||
.skip_while(|(_, y)| *y <= scale_factor)
|
||||
.collect()
|
||||
} else {
|
||||
let mut min = self.value_range_min.get();
|
||||
let mut max = self.value_range_max.get();
|
||||
for value in data_points.data_set.iter() {
|
||||
if *value < min {
|
||||
min = *value;
|
||||
}
|
||||
if *value > max {
|
||||
max = *value;
|
||||
}
|
||||
}
|
||||
|
||||
if data_points.max_all_time < max {
|
||||
data_points.max_all_time = max;
|
||||
}
|
||||
|
||||
if self.only_scale_up.get() {
|
||||
max = data_points.max_all_time;
|
||||
}
|
||||
|
||||
(0..)
|
||||
.map(|x| x as f32)
|
||||
.zip(data_points.data_set.iter().map(|x| {
|
||||
let downscale_factor = max - min;
|
||||
if downscale_factor == 0. {
|
||||
0.
|
||||
} else {
|
||||
(*x - min) / downscale_factor
|
||||
}
|
||||
}))
|
||||
.collect()
|
||||
};
|
||||
|
||||
for (x, y) in &mut points {
|
||||
*x = *x * spacing_x;
|
||||
*y = height - ((y.clamp(val_min, val_max) / val_max) * (height));
|
||||
}
|
||||
|
||||
if !points.is_empty() {
|
||||
let startindex;
|
||||
let (mut x, mut y);
|
||||
let pointlen = points.len();
|
||||
|
||||
if pointlen < data_points.data_set.len() {
|
||||
(x, y) = (
|
||||
(data_points.data_set.len() - pointlen - 1) as f32 * spacing_x,
|
||||
height,
|
||||
);
|
||||
startindex = 0;
|
||||
} else {
|
||||
(x, y) = points[0];
|
||||
startindex = 1;
|
||||
}
|
||||
let path_builder = PathBuilder::new();
|
||||
path_builder.move_to(x, y);
|
||||
|
||||
let smooth = self.smooth_graphs.get();
|
||||
|
||||
for i in startindex..pointlen {
|
||||
(x, y) = points[i];
|
||||
if smooth {
|
||||
let (lastx, lasty);
|
||||
if i > 0 {
|
||||
(lastx, lasty) = points[i - 1];
|
||||
} else {
|
||||
(lastx, lasty) = (x - spacing_x, height);
|
||||
}
|
||||
|
||||
path_builder.cubic_to(
|
||||
lastx + spacing_x / 2f32,
|
||||
lasty,
|
||||
lastx + spacing_x / 2f32,
|
||||
y,
|
||||
x,
|
||||
y,
|
||||
);
|
||||
} else {
|
||||
path_builder.line_to(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure to close out the path
|
||||
path_builder.line_to(points[pointlen - 1].0, height);
|
||||
path_builder.line_to(points[0].0, height);
|
||||
path_builder.close();
|
||||
|
||||
let path = path_builder.to_path();
|
||||
|
||||
if data_points.fill {
|
||||
snapshot.append_fill(&path, FillRule::Winding, &fill_color);
|
||||
}
|
||||
|
||||
if data_points.dashed {
|
||||
stroke.set_dash(&[5., 5.]);
|
||||
}
|
||||
|
||||
snapshot.append_stroke(&path, &stroke, &stroke_color);
|
||||
}
|
||||
}
|
||||
|
||||
fn render(&self, snapshot: &Snapshot, width: f32, height: f32, scale_factor: f64) {
|
||||
let base_color = self.base_color.get();
|
||||
|
||||
let radius = graphene::Size::new(GRAPH_RADIUS, GRAPH_RADIUS);
|
||||
let bounds = gsk::RoundedRect::new(
|
||||
graphene::Rect::new(0., 0., width, height),
|
||||
radius,
|
||||
radius,
|
||||
radius,
|
||||
radius,
|
||||
);
|
||||
|
||||
snapshot.push_rounded_clip(&bounds);
|
||||
|
||||
if self.obj().grid_visible() {
|
||||
self.draw_grid(
|
||||
snapshot,
|
||||
width,
|
||||
height,
|
||||
scale_factor,
|
||||
self.obj().data_points() as _,
|
||||
&base_color,
|
||||
);
|
||||
}
|
||||
|
||||
let mut data_sets = self.data_sets.take();
|
||||
for values in &mut data_sets {
|
||||
if !values.visible {
|
||||
continue;
|
||||
}
|
||||
|
||||
self.plot_values(snapshot, width, height, scale_factor, values, &base_color);
|
||||
}
|
||||
self.data_sets.set(data_sets);
|
||||
|
||||
snapshot.pop();
|
||||
|
||||
self.draw_outline(snapshot, &bounds, &base_color);
|
||||
}
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for GraphWidget {
|
||||
const NAME: &'static str = "GraphWidget";
|
||||
type Type = super::GraphWidget;
|
||||
type ParentType = gtk::Widget;
|
||||
}
|
||||
|
||||
impl ObjectImpl for GraphWidget {
|
||||
fn properties() -> &'static [ParamSpec] {
|
||||
Self::derived_properties()
|
||||
}
|
||||
|
||||
fn signals() -> &'static [Signal] {
|
||||
use std::sync::OnceLock;
|
||||
static SIGNALS: OnceLock<Vec<Signal>> = OnceLock::new();
|
||||
SIGNALS.get_or_init(|| vec![Signal::builder("resize").build()])
|
||||
}
|
||||
|
||||
fn set_property(&self, id: usize, value: &Value, pspec: &ParamSpec) {
|
||||
self.derived_set_property(id, value, pspec);
|
||||
}
|
||||
|
||||
fn property(&self, id: usize, pspec: &ParamSpec) -> Value {
|
||||
self.derived_property(id, pspec)
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetImpl for GraphWidget {
|
||||
fn realize(&self) {
|
||||
self.parent_realize();
|
||||
}
|
||||
|
||||
fn snapshot(&self, snapshot: &Snapshot) {
|
||||
use glib::g_critical;
|
||||
|
||||
let this = self.obj();
|
||||
|
||||
let native = match this.native() {
|
||||
Some(native) => native,
|
||||
None => {
|
||||
g_critical!("MissionCenter::GraphWidget", "Failed to get native");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let surface = match native.surface() {
|
||||
Some(surface) => surface,
|
||||
None => {
|
||||
g_critical!("MissionCenter::GraphWidget", "Failed to get surface");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let (prev_width, prev_height) = self.prev_size.get();
|
||||
let (width, height) = (this.width(), this.height());
|
||||
|
||||
if prev_width != width || prev_height != height {
|
||||
this.emit_by_name::<()>("resize", &[]);
|
||||
self.prev_size.set((width, height));
|
||||
}
|
||||
|
||||
self.render(snapshot, width as f32, height as f32, surface.scale());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct GraphWidget(ObjectSubclass<imp::GraphWidget>)
|
||||
@extends gtk::Widget,
|
||||
@implements gtk::Buildable;
|
||||
}
|
||||
|
||||
impl GraphWidget {
|
||||
pub fn new() -> Self {
|
||||
let this: Self = unsafe {
|
||||
glib::Object::new_internal(GraphWidget::static_type(), &mut [])
|
||||
.downcast()
|
||||
.unwrap()
|
||||
};
|
||||
this
|
||||
}
|
||||
|
||||
pub fn set_dashed(&self, index: usize, dashed: bool) {
|
||||
let mut data = self.imp().data_sets.take();
|
||||
if index < data.len() {
|
||||
data[index].dashed = dashed;
|
||||
}
|
||||
self.imp().data_sets.set(data);
|
||||
}
|
||||
|
||||
pub fn set_filled(&self, index: usize, filled: bool) {
|
||||
let mut data = self.imp().data_sets.take();
|
||||
if index < data.len() {
|
||||
data[index].fill = filled;
|
||||
}
|
||||
self.imp().data_sets.set(data);
|
||||
}
|
||||
|
||||
pub fn set_data_visible(&self, index: usize, visible: bool) {
|
||||
let mut data = self.imp().data_sets.take();
|
||||
if index < data.len() {
|
||||
data[index].visible = visible;
|
||||
}
|
||||
self.imp().data_sets.set(data);
|
||||
}
|
||||
|
||||
pub fn add_data_point(&self, index: usize, mut value: f32) {
|
||||
let mut data = self.imp().data_sets.take();
|
||||
|
||||
if index >= data.len() {
|
||||
self.imp().data_sets.set(data);
|
||||
return;
|
||||
}
|
||||
|
||||
if value.is_infinite() || value.is_nan() {
|
||||
value = self.value_range_min();
|
||||
}
|
||||
|
||||
if value.is_subnormal() {
|
||||
value = 0.;
|
||||
}
|
||||
|
||||
if self.scaling() == NO_SCALING {
|
||||
value = value.clamp(self.value_range_min(), self.value_range_max());
|
||||
} else if self.scaling() == AUTO_SCALING || self.scaling() == AUTO_POW2_SCALING {
|
||||
value = value.max(self.value_range_min());
|
||||
}
|
||||
|
||||
data[index].data_set.push(value);
|
||||
data[index].data_set.remove(0);
|
||||
|
||||
if self.scaling() == AUTO_SCALING || self.scaling() == AUTO_POW2_SCALING {
|
||||
self.scale(&mut data, value);
|
||||
}
|
||||
|
||||
self.imp().data_sets.set(data);
|
||||
|
||||
if self.is_visible() {
|
||||
self.queue_draw();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn data(&self, index: usize) -> Option<Vec<f32>> {
|
||||
let imp = self.imp();
|
||||
|
||||
let data = imp.data_sets.take();
|
||||
let result = if index < data.len() {
|
||||
Some(data[index].data_set.clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
imp.data_sets.set(data);
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
pub fn set_data(&self, index: usize, mut values: Vec<f32>) {
|
||||
let imp = self.imp();
|
||||
let mut data = imp.data_sets.take();
|
||||
|
||||
if index < data.len() {
|
||||
values.truncate(data[index].data_set.len());
|
||||
data[index].data_set = values;
|
||||
|
||||
for x in &mut data[index].data_set {
|
||||
if x.is_infinite() || x.is_nan() {
|
||||
*x = self.value_range_min();
|
||||
}
|
||||
|
||||
if x.is_subnormal() {
|
||||
*x = 0.;
|
||||
}
|
||||
|
||||
if self.scaling() == NO_SCALING {
|
||||
*x = x.clamp(self.value_range_min(), self.value_range_max());
|
||||
} else if self.scaling() == AUTO_SCALING || self.scaling() == AUTO_POW2_SCALING {
|
||||
*x = x.max(self.value_range_min());
|
||||
}
|
||||
}
|
||||
|
||||
if self.scaling() == AUTO_SCALING || self.scaling() == AUTO_POW2_SCALING {
|
||||
if let Some(max) = data[index]
|
||||
.data_set
|
||||
.iter()
|
||||
.map(|x| *x)
|
||||
.max_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal))
|
||||
{
|
||||
self.scale(&mut data, max);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
imp.data_sets.set(data);
|
||||
}
|
||||
|
||||
pub fn max_all_time(&self, index: usize) -> Option<f32> {
|
||||
let imp = self.imp();
|
||||
|
||||
let mut result = None;
|
||||
|
||||
let data = imp.data_sets.take();
|
||||
if index < data.len() {
|
||||
result = Some(data[index].max_all_time);
|
||||
}
|
||||
imp.data_sets.set(data);
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn scale(&self, data: &mut Vec<DataSetDescriptor>, value: f32) {
|
||||
fn round_up_to_next_power_of_two(num: u64) -> u64 {
|
||||
if num == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let mut n = num - 1;
|
||||
n |= n >> 1;
|
||||
n |= n >> 2;
|
||||
n |= n >> 4;
|
||||
n |= n >> 8;
|
||||
n |= n >> 16;
|
||||
|
||||
n + 1
|
||||
}
|
||||
|
||||
let min_value = self.value_range_min();
|
||||
let max_norm = self.value_range_max() - min_value;
|
||||
let value = value - min_value;
|
||||
|
||||
let mut max_y = value.max(max_norm);
|
||||
|
||||
let mut value_max = value;
|
||||
for data_set in data.iter() {
|
||||
for value in data_set.data_set.iter() {
|
||||
if value_max < (*value - min_value) {
|
||||
value_max = *value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while value_max < max_y {
|
||||
max_y /= 2.;
|
||||
}
|
||||
if value_max > max_y {
|
||||
max_y *= 2.;
|
||||
}
|
||||
|
||||
max_y += min_value;
|
||||
if self.scaling() == AUTO_POW2_SCALING {
|
||||
max_y = max_y.round();
|
||||
if max_y > 0. {
|
||||
max_y = round_up_to_next_power_of_two(max_y as u64) as f32;
|
||||
}
|
||||
}
|
||||
|
||||
if max_y > self.value_range_min() {
|
||||
if (max_y < self.value_range_max()) && self.only_scale_up() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.set_value_range_max(max_y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GraphWidget {
|
||||
#[inline]
|
||||
pub fn no_scaling() -> i32 {
|
||||
NO_SCALING
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn auto_scaling() -> i32 {
|
||||
AUTO_SCALING
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn auto_pow2_scaling() -> i32 {
|
||||
AUTO_POW2_SCALING
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn normalized_scaling() -> i32 {
|
||||
NORMALIZED_SCALING
|
||||
}
|
||||
}
|
57
src/main.rs
Normal file
57
src/main.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
use gtk::prelude::*;
|
||||
use gtk::{self, glib, Application};
|
||||
mod graph_widget;
|
||||
|
||||
const APP_ID: &str = "com.chloechristine.ftt2midi";
|
||||
|
||||
fn main() -> glib::ExitCode {
|
||||
// Create a new application
|
||||
let app = Application::builder().application_id(APP_ID).build();
|
||||
|
||||
// Connect to the "activate" signal of 'ftt2midi'
|
||||
app.connect_activate(build_ui);
|
||||
|
||||
// Run the application
|
||||
app.run()
|
||||
}
|
||||
|
||||
fn build_ui(app: &Application) {
|
||||
let ui_src = include_str!("resources/main_window.ui");
|
||||
println!("{}", ui_src);
|
||||
let builder = gtk::Builder::from_string(ui_src);
|
||||
|
||||
// Bind to UI elements defined in template
|
||||
let window = builder
|
||||
.object::<gtk::ApplicationWindow>("main_window")
|
||||
.expect("Couldn't bind to window :(");
|
||||
|
||||
let main_grid = builder
|
||||
.object::<gtk::Grid>("main_grid")
|
||||
.expect("Couldn't bind to main_grid");
|
||||
|
||||
let input_selector = builder
|
||||
.object::<gtk::MenuButton>("input_selector")
|
||||
.expect("Couldn't bind to input_selector");
|
||||
|
||||
let midi_device_selector = builder
|
||||
.object::<gtk::MenuButton>("midi_device_selector")
|
||||
.expect("Couldn't bind to midi_device_selector");
|
||||
|
||||
let start_button = builder
|
||||
.object::<gtk::Button>("start_button")
|
||||
.expect("Couldn't bind to start_button");
|
||||
|
||||
let fft_graph = graph_widget::GraphWidget::new();
|
||||
fft_graph.set_height_request(500);
|
||||
fft_graph.set_width_request(500);
|
||||
main_grid.attach(&fft_graph, 0, 0, 1, 1);
|
||||
//main_grid.attach_next_to(&fft_graph, Some(&control_deck), gtk::PositionType::Top, 1, 0);
|
||||
|
||||
//start_button.connect_clicked()
|
||||
|
||||
|
||||
window.set_application(Some(app));
|
||||
|
||||
// Present the window
|
||||
window.present();
|
||||
}
|
147
src/resources/graph.ui
Normal file
147
src/resources/graph.ui
Normal file
@@ -0,0 +1,147 @@
|
||||
<interface>
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
<template class="PerformancePageCpu" parent="GtkBox">
|
||||
<property name="margin-bottom">10</property>
|
||||
<property name="orientation">1</property>
|
||||
<child>
|
||||
<object class="GtkWindowHandle">
|
||||
<property name="child">
|
||||
<object class="GtkBox" id="description">
|
||||
<property name="orientation">1</property>
|
||||
<property name="spacing">7</property>
|
||||
<property name="hexpand">true</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="spacing">20</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<style>
|
||||
<class name="title-1"/>
|
||||
</style>
|
||||
<property name="hexpand">true</property>
|
||||
<property name="halign">1</property>
|
||||
<property name="label" translatable="yes">CPU</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="cpu_name">
|
||||
<style>
|
||||
<class name="title-3"/>
|
||||
</style>
|
||||
<property name="halign">2</property>
|
||||
<property name="ellipsize">2</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<style>
|
||||
<class name="caption"/>
|
||||
</style>
|
||||
<property name="label" translatable="yes">Utilization over </property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="graph_max_duration">
|
||||
<style>
|
||||
<class name="caption"/>
|
||||
</style>
|
||||
<property name="hexpand">true</property>
|
||||
<property name="halign">1</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<style>
|
||||
<class name="caption"/>
|
||||
</style>
|
||||
<property name="label" translatable="yes">100%</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkGrid" id="usage_graphs">
|
||||
<property name="row-spacing">7</property>
|
||||
<property name="column-spacing">7</property>
|
||||
<property name="vexpand">true</property>
|
||||
<property name="hexpand">true</property>
|
||||
<property name="height-request">100</property>
|
||||
<property name="width-request">100</property>
|
||||
<property name="row-homogeneous">true</property>
|
||||
<property name="column-homogeneous">true</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkPopoverMenu" id="context_menu">
|
||||
<property name="has-arrow">false</property>
|
||||
<property name="menu-model">context_menu_model</property>
|
||||
</object>
|
||||
</child>
|
||||
</template>
|
||||
<menu id="context_menu_model">
|
||||
<section>
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">Change G_raph To</attribute>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Overall U_tilization</attribute>
|
||||
<attribute name="action">graph.overall</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Logical _Processors</attribute>
|
||||
<attribute name="action">graph.all-processors</attribute>
|
||||
</item>
|
||||
</submenu>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Show Kernel Times</attribute>
|
||||
<attribute name="action">graph.kernel_times</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Graph _Summary View</attribute>
|
||||
<attribute name="action">graph.summary</attribute>
|
||||
</item>
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">_View</attribute>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">CP_U</attribute>
|
||||
<attribute name="action">graph.cpu</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">_Memory</attribute>
|
||||
<attribute name="action">graph.memory</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">_Drive</attribute>
|
||||
<attribute name="action">graph.disk</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">_Network</attribute>
|
||||
<attribute name="action">graph.network</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">_GPU</attribute>
|
||||
<attribute name="action">graph.gpu</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">_Fan</attribute>
|
||||
<attribute name="action">graph.fan</attribute>
|
||||
</item>
|
||||
</submenu>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">_Copy</attribute>
|
||||
<attribute name="action">graph.copy</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</menu>
|
||||
</interface>
|
66
src/resources/main_window.cmb
Normal file
66
src/resources/main_window.cmb
Normal file
@@ -0,0 +1,66 @@
|
||||
<?xml version='1.0' encoding='UTF-8' standalone='no'?>
|
||||
<!DOCTYPE cambalache-project SYSTEM "cambalache-project.dtd">
|
||||
<cambalache-project version="0.93.0" target_tk="gtk-4.0">
|
||||
<ui>
|
||||
(1,None,"main_window.ui","main_window.ui",None,None,"Chloe Fontenot",None,None,None,None)
|
||||
</ui>
|
||||
<ui_library>
|
||||
(1,"gtk","4.6",None)
|
||||
</ui_library>
|
||||
<object>
|
||||
(1,1,"GtkApplicationWindow","main_window",None,None,None,None,0,None,None),
|
||||
(1,3,"GtkGrid","control_deck",9,None,"center",None,0,None,None),
|
||||
(1,4,"GtkLabel",None,3,None,None,None,0,None,None),
|
||||
(1,5,"GtkLabel",None,3,None,None,None,1,None,None),
|
||||
(1,6,"GtkMenuButton","input_selector",3,None,None,None,2,None,None),
|
||||
(1,7,"GtkMenuButton","midi_device_selector",3,None,None,None,3,None,None),
|
||||
(1,8,"GtkButton","start_button",3,None,None,None,4,None,None),
|
||||
(1,9,"GtkGrid","main_grid",1,None,None,None,0,None,None)
|
||||
</object>
|
||||
<object_property>
|
||||
(1,1,"GtkApplicationWindow","show-menubar","True",None,None,None,None,None,None,None,None,None),
|
||||
(1,3,"GtkWidget","halign","center",None,None,None,None,None,None,None,None,None),
|
||||
(1,3,"GtkWidget","valign","center",None,None,None,None,None,None,None,None,None),
|
||||
(1,4,"GtkLabel","label","Audio Source:",None,None,None,None,None,None,None,None,None),
|
||||
(1,4,"GtkWidget","margin-end","20",None,None,None,None,None,None,None,None,None),
|
||||
(1,4,"GtkWidget","margin-start","20",None,None,None,None,None,None,None,None,None),
|
||||
(1,5,"GtkLabel","label","MIDI Output Device:",None,None,None,None,None,None,None,None,None),
|
||||
(1,5,"GtkWidget","margin-end","20",None,None,None,None,None,None,None,None,None),
|
||||
(1,5,"GtkWidget","margin-start","20",None,None,None,None,None,None,None,None,None),
|
||||
(1,6,"GtkMenuButton","active","True",None,None,None,None,None,None,None,None,None),
|
||||
(1,6,"GtkMenuButton","always-show-arrow","True",None,None,None,None,None,None,None,None,None),
|
||||
(1,6,"GtkMenuButton","icon-name","multimedia-volume-control",None,None,None,None,None,None,None,None,None),
|
||||
(1,7,"GtkMenuButton","active","True",None,None,None,None,None,None,None,None,None),
|
||||
(1,7,"GtkMenuButton","always-show-arrow","True",None,None,None,None,None,None,None,None,None),
|
||||
(1,8,"GtkButton","label","Start",None,None,None,None,None,None,None,None,None),
|
||||
(1,8,"GtkWidget","can-target","False",None,None,None,None,None,None,None,None,None),
|
||||
(1,8,"GtkWidget","sensitive","False",None,None,None,None,None,None,None,None,None),
|
||||
(1,9,"GtkWidget","halign","baseline-center",None,None,None,None,None,None,None,None,None),
|
||||
(1,9,"GtkWidget","hexpand","True",None,None,None,None,None,None,None,None,None),
|
||||
(1,9,"GtkWidget","hexpand-set","True",None,None,None,None,None,None,None,None,None),
|
||||
(1,9,"GtkWidget","valign","baseline-center",None,None,None,None,None,None,None,None,None),
|
||||
(1,9,"GtkWidget","vexpand","True",None,None,None,None,None,None,None,None,None),
|
||||
(1,9,"GtkWidget","vexpand-set","True",None,None,None,None,None,None,None,None,None)
|
||||
</object_property>
|
||||
<object_layout_property>
|
||||
(1,3,4,"GtkGridLayoutChild","column","0",None,None,None,None),
|
||||
(1,3,4,"GtkGridLayoutChild","column-span","1",None,None,None,None),
|
||||
(1,3,4,"GtkGridLayoutChild","row","0",None,None,None,None),
|
||||
(1,3,4,"GtkGridLayoutChild","row-span","1",None,None,None,None),
|
||||
(1,3,5,"GtkGridLayoutChild","column","0",None,None,None,None),
|
||||
(1,3,5,"GtkGridLayoutChild","column-span","1",None,None,None,None),
|
||||
(1,3,5,"GtkGridLayoutChild","row","1",None,None,None,None),
|
||||
(1,3,5,"GtkGridLayoutChild","row-span","1",None,None,None,None),
|
||||
(1,3,6,"GtkGridLayoutChild","column","1",None,None,None,None),
|
||||
(1,3,6,"GtkGridLayoutChild","column-span","1",None,None,None,None),
|
||||
(1,3,6,"GtkGridLayoutChild","row","0",None,None,None,None),
|
||||
(1,3,6,"GtkGridLayoutChild","row-span","1",None,None,None,None),
|
||||
(1,3,7,"GtkGridLayoutChild","column","1",None,None,None,None),
|
||||
(1,3,7,"GtkGridLayoutChild","column-span","1",None,None,None,None),
|
||||
(1,3,7,"GtkGridLayoutChild","row","1",None,None,None,None),
|
||||
(1,3,7,"GtkGridLayoutChild","row-span","1",None,None,None,None),
|
||||
(1,3,8,"GtkGridLayoutChild","column-span","2",None,None,None,None),
|
||||
(1,3,8,"GtkGridLayoutChild","row","2",None,None,None,None),
|
||||
(1,9,3,"GtkGridLayoutChild","row","1",None,None,None,None)
|
||||
</object_layout_property>
|
||||
</cambalache-project>
|
92
src/resources/main_window.ui
Normal file
92
src/resources/main_window.ui
Normal file
@@ -0,0 +1,92 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<!-- Created with Cambalache 0.93.0 -->
|
||||
<interface>
|
||||
<!-- interface-name main_window.ui -->
|
||||
<!-- interface-authors Chloe Fontenot -->
|
||||
<requires lib="gtk" version="4.6"/>
|
||||
<object class="GtkApplicationWindow" id="main_window">
|
||||
<property name="show-menubar">True</property>
|
||||
<child>
|
||||
<object class="GtkGrid" id="main_grid">
|
||||
<property name="halign">baseline-center</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="hexpand-set">True</property>
|
||||
<property name="valign">baseline-center</property>
|
||||
<property name="vexpand">True</property>
|
||||
<property name="vexpand-set">True</property>
|
||||
<child type="center">
|
||||
<object class="GtkGrid" id="control_deck">
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="label">Audio Source:</property>
|
||||
<property name="margin-end">20</property>
|
||||
<property name="margin-start">20</property>
|
||||
<layout>
|
||||
<property name="column">0</property>
|
||||
<property name="column-span">1</property>
|
||||
<property name="row">0</property>
|
||||
<property name="row-span">1</property>
|
||||
</layout>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="label">MIDI Output Device:</property>
|
||||
<property name="margin-end">20</property>
|
||||
<property name="margin-start">20</property>
|
||||
<layout>
|
||||
<property name="column">0</property>
|
||||
<property name="column-span">1</property>
|
||||
<property name="row">1</property>
|
||||
<property name="row-span">1</property>
|
||||
</layout>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkMenuButton" id="input_selector">
|
||||
<property name="active">True</property>
|
||||
<property name="always-show-arrow">True</property>
|
||||
<property name="icon-name">multimedia-volume-control</property>
|
||||
<layout>
|
||||
<property name="column">1</property>
|
||||
<property name="column-span">1</property>
|
||||
<property name="row">0</property>
|
||||
<property name="row-span">1</property>
|
||||
</layout>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkMenuButton" id="output_midi_device_selector">
|
||||
<property name="active">True</property>
|
||||
<property name="always-show-arrow">True</property>
|
||||
<layout>
|
||||
<property name="column">1</property>
|
||||
<property name="column-span">1</property>
|
||||
<property name="row">1</property>
|
||||
<property name="row-span">1</property>
|
||||
</layout>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="start_button">
|
||||
<property name="can-target">False</property>
|
||||
<property name="label">Start</property>
|
||||
<layout>
|
||||
<property name="column">0</property>
|
||||
<property name="column-span">2</property>
|
||||
<property name="row">2</property>
|
||||
</layout>
|
||||
</object>
|
||||
</child>
|
||||
<layout>
|
||||
<property name="column">0</property>
|
||||
<property name="row">1</property>
|
||||
</layout>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
Reference in New Issue
Block a user