├── .gitignore ├── cpb-splash.png ├── tests ├── Makefile ├── demo.vala └── menu.glade ├── .travis.yml ├── README.md └── circular-progress-bar.vala /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.c 3 | *.swp 4 | /tests/demo 5 | cpb-splash.xcf 6 | -------------------------------------------------------------------------------- /cpb-splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phastmike/vala-circular-progress-bar/HEAD/cpb-splash.png -------------------------------------------------------------------------------- /tests/Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | valac -o demo ../circular-progress-bar.vala demo.vala --pkg gtk4 --pkg pangocairo -X -lm 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: jammy 2 | 3 | language: c 4 | 5 | compiler: gcc 6 | 7 | before_install: 8 | - sudo add-apt-repository --yes ppa:vala-team 9 | - sudo apt-get update --quiet 10 | - sudo apt-get install --yes --force-yes valac libglib2.0-bin libglib2.0-dev libgtk-4-dev libgee-0.8-dev 11 | 12 | script: 13 | - cd tests 14 | - make 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://app.travis-ci.com/phastmike/vala-circular-progress-bar.svg?branch=master)](https://app.travis-ci.com/github/phastmike/vala-circular-progress-bar) 2 | 3 | # CircularProgressBar 4 | A Circular progress bar Gtk.Widget, implemented in **Vala**. Main goal was to implement a simple circular progress bar for a UI Dashboard. 5 | 6 | It now supports Gtk4, the previous version targeted Gtk3 and now resides in the `gtk3` branch. 7 | 8 | ![Visual examples](/cpb-splash.png "Some visual examples") 9 | 10 | ## Properties 11 | - Font 12 | - Percentage 13 | - Progress line width 14 | - Toggle center fill 15 | - Line Cap (as in Cairo.LineCap) 16 | - Fill colors (Center, Radius and Progress) 17 | - Toggle fullscale progress (with different fill color) 18 | 19 | ### Notes regarding Line width 20 | 21 | When setting `line_width` as zero, the progress will be rendered as a pie, fully filled and radius fill will be ignored. 22 | 23 | ## Requirements 24 | This widget should build and run on systems with: 25 | - Gtk+ >= 3.18 26 | - Valac >= 0.30 27 | 28 | Probably it will work with previous minor versions. 29 | 30 | ## Use 31 | To use this widget you will only need to copy the file `circular-progress-bar.vala` onto your project and include it in your build system. The class is defined on the `CircularProgressWidgets` namespace and you can check it's use on the file `demo.vala` included on the tests folder in this repository. 32 | 33 | The following build instructions are used to build the demo. 34 | 35 | ## Building and running 36 | Clone this repository: 37 | 38 | $ git clone https://github.com/phastmike/vala-circular-progress-bar 39 | 40 | then cd into it and run `make`: 41 | 42 | $ cd vala-circular-progress-bar/tests 43 | $ make 44 | 45 | After building you can run the demo program in the tests folder by typing: 46 | 47 | $ ./demo 48 | 49 | #### Demo 50 | 51 | The demo/test application allows you to tweak some of the properties in order the have a quick visual feedback of the applied values. The horizontal scales control the progress bar percentage evolution and the width, in pixels, of the progress bar segment. The buttons bellow the scales, toggle the line cap, center filling (on/off) and progress bar as fully filled (on/off). 52 | 53 | ## Future work 54 | 55 | Although the use of the widget is somehow controlled or static, some improvements could be made if some dynamic behaviour is needed, will list them as a ToDo list: 56 | 57 | - [ ] improve resizing and properties relation 58 | - [x] ~~add color selection to the demo application~~ 59 | - [ ] add font selection to the demo application 60 | 61 | ## Applications using this circular progress bar 62 | - Elementary OS applet: [date-countdown](https://github.com/rickybas/date-countdown) 63 | - Elementary OS app: [Optimizer](https://github.com/hannesschulze/optimizer) (based on) 64 | - Elementary OS app: [Planner](https://github.com/alainm23/planner) (project progress) 65 | - Elkowars Wacky Widgets: [Eww](https://github.com/elkowar/eww) (served as [inspiration for their circular progress bar](https://github.com/elkowar/eww/issues/233)) 66 | -------------------------------------------------------------------------------- /tests/demo.vala: -------------------------------------------------------------------------------- 1 | /* -*- Mode: Vala; indent-tabs-mode: nil; c-basic-offset: 4; tab-width: 4 -*- */ 2 | /* vim: set tabstop=4 softtabstop=4 shiftwidth=4 expandtab : */ 3 | /* 4 | * demo.vala 5 | * 6 | * Test CircularProgressBar visually. 7 | * Tweak some settings/properties and check the results. 8 | * 9 | * José Miguel Fonte 10 | */ 11 | 12 | using CircularProgressWidgets; 13 | 14 | private static string convert_color_component_to_string (double color_component) { 15 | return "%.2x".printf ((uint) (color_component * 255)); 16 | } 17 | 18 | private static string convert_rgba_to_webcolor (Gdk.RGBA c) { 19 | var red = convert_color_component_to_string (c.red); 20 | var green = convert_color_component_to_string (c.green); 21 | var blue = convert_color_component_to_string (c.blue); 22 | 23 | return "#" + red + green + blue; 24 | } 25 | 26 | int main (string[] args) { 27 | //Gtk.init (ref args); 28 | var app = new Gtk.Application ("circular.progress.bar.demo", GLib.ApplicationFlags.FLAGS_NONE); 29 | 30 | app.activate.connect (() => { 31 | var pbar = new CircularProgressBar (); 32 | //pbar.margin = 6; 33 | 34 | var builder = new Gtk.Builder.from_file ("menu.glade"); 35 | 36 | var window = builder.get_object ("window1") as Gtk.ApplicationWindow; 37 | window.set_application(app); 38 | var viewp = builder.get_object ("viewport1") as Gtk.Viewport; 39 | var s_progr = builder.get_object ("scale1") as Gtk.Scale; 40 | var s_linew = builder.get_object ("scale2") as Gtk.Scale; 41 | 42 | var colorb1 = builder.get_object ("colorbutton1") as Gtk.ColorButton; 43 | var colorb2 = builder.get_object ("colorbutton2") as Gtk.ColorButton; 44 | var colorb3 = builder.get_object ("colorbutton3") as Gtk.ColorButton; 45 | 46 | var button_cap = builder.get_object ("togglebutton3") as Gtk.ToggleButton; 47 | var button_center_filled = builder.get_object ("togglebutton1") as Gtk.ToggleButton; 48 | var button_radius_filled = builder.get_object ("togglebutton2") as Gtk.ToggleButton; 49 | 50 | button_center_filled.bind_property ("active", pbar, "center_filled", BindingFlags.DEFAULT); 51 | button_radius_filled.bind_property ("active", pbar, "radius_filled", BindingFlags.DEFAULT); 52 | 53 | var pbar_w = builder.get_object ("width") as Gtk.Label; 54 | var pbar_h = builder.get_object ("height") as Gtk.Label; 55 | pbar_w.set_use_markup (true); 56 | pbar_h.set_use_markup (true); 57 | 58 | var linew_adj = builder.get_object ("adjustment2") as Gtk.Adjustment; 59 | 60 | viewp.set_child (pbar); 61 | //window.set_child(pbar); 62 | 63 | // Set default tooltips 64 | // Color representation mismatch on default and after change. FIXME! 65 | button_cap.set_tooltip_text (pbar.line_cap.to_string ()); 66 | colorb1.set_tooltip_text (pbar.center_fill_color); 67 | colorb2.set_tooltip_text (pbar.radius_fill_color); 68 | colorb3.set_tooltip_text (pbar.progress_fill_color); 69 | 70 | var color = Gdk.RGBA(); 71 | 72 | color.parse (pbar.center_fill_color); 73 | colorb1.set_rgba (color); 74 | 75 | color.parse (pbar.radius_fill_color); 76 | colorb2.set_rgba (color); 77 | 78 | color.parse (pbar.progress_fill_color); 79 | colorb3.set_rgba (color); 80 | 81 | colorb1.color_set.connect (() => { 82 | var c = colorb1.get_rgba (); 83 | pbar.center_fill_color = c.to_string (); 84 | colorb1.set_tooltip_text (convert_rgba_to_webcolor (c)); 85 | }); 86 | 87 | colorb2.color_set.connect ((new_color) => { 88 | var c = colorb2.get_rgba (); 89 | pbar.radius_fill_color = c.to_string (); 90 | colorb2.set_tooltip_text (convert_rgba_to_webcolor (c)); 91 | }); 92 | 93 | colorb3.color_set.connect ((new_color) => { 94 | var c = colorb3.get_rgba (); 95 | pbar.progress_fill_color = c.to_string (); 96 | colorb3.set_tooltip_text (convert_rgba_to_webcolor (c)); 97 | }); 98 | 99 | button_cap.toggled.connect (() => { 100 | if (pbar.line_cap == Cairo.LineCap.ROUND) { 101 | pbar.line_cap = Cairo.LineCap.BUTT; 102 | } else { 103 | pbar.line_cap = Cairo.LineCap.ROUND; 104 | } 105 | 106 | button_cap.set_tooltip_text (pbar.line_cap.to_string ()); 107 | }); 108 | 109 | s_progr.value_changed.connect (() => { 110 | pbar.percentage = s_progr.get_value (); 111 | }); 112 | 113 | s_linew.value_changed.connect (() => { 114 | pbar.line_width = ((int) s_linew.get_value ()); 115 | }); 116 | 117 | window.notify["default-width"].connect ((s,p) => { 118 | var wstr = "%d".printf (window.default_width); 119 | pbar_w.set_markup (wstr); 120 | var w = window.default_width; 121 | var h = window.default_height; 122 | linew_adj.set_upper ((double) int.min (w, h) / 2); 123 | s_linew.queue_draw (); 124 | }); 125 | 126 | window.notify["default-height"].connect ((s,p) => { 127 | var hstr = "%d".printf (window.default_height); 128 | pbar_h.set_markup (hstr); 129 | var w = window.default_width; 130 | var h = window.default_height; 131 | linew_adj.set_upper ((double) int.min (w, h) / 2); 132 | s_linew.queue_draw (); 133 | }); 134 | window.present (); 135 | }); 136 | 137 | return app.run(args); 138 | } 139 | -------------------------------------------------------------------------------- /circular-progress-bar.vala: -------------------------------------------------------------------------------- 1 | /* -*- Mode: Vala; indent-tabs-mode: nil; c-basic-offset: 4; tab-width: 4 -*- */ 2 | /* vim: set tabstop=4 softtabstop=4 shiftwidth=4 expandtab : */ 3 | /* 4 | * CircularProgressBar.vala 5 | * 6 | * Custom Gtk.Widget to provide a circular progress bar.i 7 | * This is the Gtk4 version, it extends/subclasses Gtk.DrawingArea. 8 | * 9 | * Line width as 0 turns the widget into a pie. 10 | * 11 | * FIXME: Text size is hardcoded. 12 | * 13 | * José Miguel Fonte 14 | */ 15 | 16 | using Gtk; 17 | using Cairo; 18 | 19 | namespace CircularProgressWidgets { 20 | public class CircularProgressBar : Gtk.DrawingArea { 21 | private int _line_width; 22 | private double _percentage; 23 | private string _center_fill_color; 24 | private string _radius_fill_color; 25 | private string _progress_fill_color; 26 | 27 | [Description(nick = "Center Fill", blurb = "Center Fill toggle")] 28 | public bool center_filled {set; get; default = false;} 29 | 30 | [Description(nick = "Radius Fill", blurb = "Radius Fill toggle")] 31 | public bool radius_filled {set; get; default = false;} 32 | 33 | [Description(nick = "Font", blurb = "Font description without size, just the font name")] 34 | public string font {set; get; default = "URW Gothic";} 35 | 36 | [Description(nick = "Line Cap", blurb = "Line Cap for stroke as in Cairo.LineCap")] 37 | public Cairo.LineCap line_cap {set; get; default = Cairo.LineCap.BUTT;} 38 | 39 | [Description(nick = "Inside circle fill color", blurb = "Center pad fill color (Check Gdk.RGBA parse method)")] 40 | public string center_fill_color { 41 | get { 42 | return _center_fill_color; 43 | } 44 | set { 45 | var color = Gdk.RGBA (); 46 | if (color.parse (value)) { 47 | _center_fill_color = value; 48 | } 49 | } 50 | } 51 | 52 | [Description(nick = "Circular radius fill color", blurb = "The circular pad fill color (Check GdkRGBA parse method)")] 53 | public string radius_fill_color { 54 | get { 55 | return _radius_fill_color; 56 | } 57 | set { 58 | var color = Gdk.RGBA (); 59 | if (color.parse (value)) { 60 | _radius_fill_color = value; 61 | } 62 | } 63 | } 64 | 65 | [Description(nick = "Progress fill color", blurb = "Progress line color (Check GdkRGBA parse method)")] 66 | public string progress_fill_color { 67 | get { 68 | return _progress_fill_color;; 69 | } 70 | set { 71 | var color = Gdk.RGBA (); 72 | if (color.parse (value)) { 73 | _progress_fill_color = value; 74 | } 75 | } 76 | } 77 | 78 | [Description(nick = "Circle width", blurb = "The circle radius line width")] 79 | public int line_width { 80 | get { 81 | return _line_width; 82 | } 83 | set { 84 | if (value < 0) { 85 | _line_width = 0; 86 | } else if (value > calculate_radius ()) { 87 | _line_width = calculate_radius (); 88 | } else { 89 | _line_width = value; 90 | } 91 | } 92 | } 93 | 94 | [Description(nick = "Percentage/Value", blurb = "The percentage value [0.0 ... 1.0]")] 95 | public double percentage { 96 | get { 97 | return _percentage; 98 | } 99 | set { 100 | if (value > 1.0) { 101 | _percentage = 1.0; 102 | } else if (value < 0.0) { 103 | _percentage = 0.0; 104 | } else { 105 | _percentage = value; 106 | } 107 | } 108 | } 109 | 110 | construct { 111 | _line_width = 1; 112 | _percentage = 0; 113 | _center_fill_color = "#adadad"; 114 | _radius_fill_color = "#d3d3d3"; 115 | _progress_fill_color = "#4a90d9"; 116 | } 117 | 118 | public CircularProgressBar () { 119 | set_draw_func(draw); 120 | 121 | notify.connect (() => { 122 | queue_draw (); 123 | }); 124 | } 125 | 126 | private int calculate_radius () { 127 | return int.min (get_allocated_width () / 2, get_allocated_height () / 2) - 1; 128 | } 129 | 130 | public override Gtk.SizeRequestMode get_request_mode () { 131 | return Gtk.SizeRequestMode.CONSTANT_SIZE; 132 | } 133 | 134 | public void draw (DrawingArea da, Cairo.Context cr, int width, int height) { 135 | int w,h; 136 | int delta; 137 | Gdk.RGBA color; 138 | Pango.Layout layout; 139 | Pango.FontDescription desc; 140 | 141 | cr.save (); 142 | 143 | color = Gdk.RGBA (); 144 | 145 | var center_x = get_allocated_width () / 2; 146 | var center_y = get_allocated_height () / 2; 147 | var radius = calculate_radius (); 148 | 149 | if (radius - line_width < 0) { 150 | delta = 0; 151 | line_width = radius; 152 | } else { 153 | delta = radius - (line_width / 2); 154 | } 155 | 156 | color = Gdk.RGBA (); 157 | cr.set_line_cap (line_cap); 158 | cr.set_line_width (line_width); 159 | 160 | // Center Fill 161 | if (center_filled == true) { 162 | cr.arc (center_x, center_y, delta, 0, 2 * Math.PI); 163 | color.parse (center_fill_color); 164 | Gdk.cairo_set_source_rgba (cr, color); 165 | cr.fill (); 166 | } 167 | 168 | // Radius Fill 169 | if (radius_filled == true) { 170 | cr.arc (center_x, center_y, delta, 0, 2 * Math.PI); 171 | color.parse (radius_fill_color); 172 | Gdk.cairo_set_source_rgba (cr, color); 173 | cr.stroke (); 174 | } 175 | 176 | // Progress/Percentage Fill 177 | if (percentage > 0) { 178 | color.parse (progress_fill_color); 179 | Gdk.cairo_set_source_rgba (cr, color); 180 | 181 | if (line_width == 0) { 182 | cr.move_to (center_x, center_y); 183 | cr.arc (center_x, 184 | center_y, 185 | delta+1, 186 | 1.5 * Math.PI, 187 | (1.5 + percentage * 2 ) * Math.PI); 188 | cr.fill (); 189 | } else { 190 | cr.arc (center_x, 191 | center_y, 192 | delta, 193 | 1.5 * Math.PI, 194 | (1.5 + percentage * 2 ) * Math.PI); 195 | cr.stroke (); 196 | } 197 | } 198 | 199 | // Textual information 200 | var context = get_style_context (); 201 | context.save (); 202 | // FIXME: Gtk4 has changes in the styles that need to be reviewed 203 | // For now we get the text color from the defaut context. 204 | color = context.get_color (); 205 | Gdk.cairo_set_source_rgba (cr, color); 206 | 207 | // Percentage 208 | layout = Pango.cairo_create_layout (cr); 209 | layout.set_text ("%d".printf ((int) (percentage * 100.0)), -1); 210 | desc = Pango.FontDescription.from_string (font + " 24"); 211 | layout.set_font_description (desc); 212 | Pango.cairo_update_layout (cr, layout); 213 | layout.get_size (out w, out h); 214 | cr.move_to (center_x - ((w / Pango.SCALE) / 2), center_y - 27 ); 215 | Pango.cairo_show_layout (cr, layout); 216 | 217 | // Units indicator (percentage) 218 | layout.set_text ("PERCENT", -1); 219 | desc = Pango.FontDescription.from_string (font + " 8"); 220 | layout.set_font_description (desc); 221 | Pango.cairo_update_layout (cr, layout); 222 | layout.get_size (out w, out h); 223 | cr.move_to (center_x - ((w / Pango.SCALE) / 2), center_y + 13); 224 | Pango.cairo_show_layout (cr, layout); 225 | context.restore (); 226 | cr.restore (); 227 | } 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /tests/menu.glade: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 1 7 | 0.01 8 | 10 9 | 10 | 11 | 12 | 100 13 | 1 14 | 1 15 | 10 16 | 17 | 18 | 19 | False 20 | 21 | 22 | 23 | 26 | 27 | 28 | 29 | 30 | 31 | True 32 | False 33 | vertical 34 | 6 35 | false 36 | 37 | 38 | True 39 | False 40 | True 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | True 49 | False 50 | 51 | 52 | 53 | 54 | vertical 55 | True 56 | False 57 | center 58 | end 59 | 60 | 61 | False 62 | start 63 | Line width 64 | 65 | 66 | 67 | 68 | True 69 | True 70 | True 71 | adjustment2 72 | 1 73 | 0 74 | bottom 75 | true 76 | 77 | 78 | 79 | 80 | True 81 | False 82 | start 83 | Percentage 84 | 85 | 86 | 87 | 88 | True 89 | True 90 | True 91 | adjustment1 92 | 1 93 | 2 94 | bottom 95 | true 96 | 97 | 98 | 99 | 100 | False 101 | 102 | 103 | False 104 | Center 105 | 106 | 107 | 108 | 109 | False 110 | Radius 111 | 112 | 113 | 114 | 115 | True 116 | False 117 | Progress 118 | 119 | 120 | 121 | 122 | 123 | 124 | True 125 | False 126 | 127 | 128 | Fill 129 | True 130 | True 131 | True 132 | 133 | 134 | 135 | 136 | Fill 137 | True 138 | True 139 | True 140 | 141 | 142 | 143 | 144 | LineCap 145 | True 146 | True 147 | True 148 | 149 | 150 | 153 | 154 | 155 | 156 | 157 | True 158 | False 159 | 160 | 161 | True 162 | True 163 | True 164 | 165 | 166 | 167 | 168 | True 169 | True 170 | True 171 | 172 | 173 | 174 | 175 | True 176 | True 177 | True 178 | 179 | 180 | 183 | 184 | 185 | 186 | 187 | True 188 | False 189 | Progress bar keeps the aspect ratio so the smallest size it's the importan value. 190 | True 191 | 192 | 193 | True 194 | False 195 | <b>W x H</b> 196 | True 197 | 198 | 199 | 200 | 201 | True 202 | False 203 | 80 204 | 205 | 206 | 207 | 208 | True 209 | False 210 | 80 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | --------------------------------------------------------------------------------