// This is a web-riented fiddle, it is designed to be published server-side.
// It accepts parameter in url, ex "?city=Paris"
// The goal is to show a perfect use-case for server-side rendering with SkiaSharp.
// For desktop/mobile you would remove fiddle api usage and set city via code.
// Weather — a card built the way a phone app would do it.
var city = Fiddle.Query.Text("city", "Novi Sad");
// ---------- condition art (inline svg: the symbols font has no weather glyphs) ----------
string Sun(string fill) =>
"<circle cx='50' cy='50' r='19' fill='" + fill + "'/>" +
"<g stroke='" + fill + "' stroke-width='7' stroke-linecap='round'>" +
"<path d='M50 8 v12'/><path d='M50 80 v12'/><path d='M8 50 h12'/><path d='M80 50 h12'/>" +
"<path d='M20 20 l9 9'/><path d='M71 71 l9 9'/><path d='M80 20 l-9 9'/><path d='M29 71 l-9 9'/></g>";
string Cloud(string fill, double dy) =>
"<g transform='translate(0," + dy.ToString(System.Globalization.CultureInfo.InvariantCulture) + ")'>" +
"<circle cx='38' cy='52' r='18' fill='" + fill + "'/>" +
"<circle cx='60' cy='46' r='23' fill='" + fill + "'/>" +
"<rect x='26' y='52' width='52' height='22' rx='11' fill='" + fill + "'/></g>";
string Drops(string fill) =>
"<g fill='" + fill + "'>" +
"<path d='M36 78 l5 9 a5.5 5.5 0 1 1 -10 0 z'/>" +
"<path d='M52 82 l5 9 a5.5 5.5 0 1 1 -10 0 z'/>" +
"<path d='M68 78 l5 9 a5.5 5.5 0 1 1 -10 0 z'/></g>";
string Flakes(string fill) =>
"<g stroke='" + fill + "' stroke-width='4' stroke-linecap='round'>" +
"<path d='M38 80 v10'/><path d='M33 85 h10'/><path d='M56 84 v10'/><path d='M51 89 h10'/>" +
"<path d='M74 80 v10'/><path d='M69 85 h10'/></g>";
string Bolt(string fill) =>
"<path d='M56 70 L40 94 h11 l-5 16 20 -26 h-11 z' fill='" + fill + "'/>";
string Icon(string kind)
{
var body = kind switch
{
"clear" => Sun("#FFC94A"),
"partly" => Sun("#FFC94A") + Cloud("#E2E8F0", 8),
"cloudy" => Cloud("#CBD5E1", 0) + Cloud("#94A3B8", 14),
"fog" => Cloud("#CBD5E1", 0) +
"<g stroke='#94A3B8' stroke-width='5' stroke-linecap='round'><path d='M28 84 h44'/><path d='M34 94 h32'/></g>",
"rain" => Cloud("#B6C6DA", 0) + Drops("#7DD3FC"),
"snow" => Cloud("#DCE7F3", 0) + Flakes("#BAE6FD"),
"storm" => Cloud("#A6B4C8", 0) + Bolt("#FBBF24"),
_ => Cloud("#CBD5E1", 0),
};
return "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 120'>" + body + "</svg>";
}
(string Kind, string Words) Describe(int code) => code switch
{
0 => ("clear", "Clear sky"),
1 => ("partly", "Mainly clear"),
2 => ("partly", "Partly cloudy"),
3 => ("cloudy", "Overcast"),
45 or 48 => ("fog", "Fog"),
51 or 53 or 55 or 56 or 57 => ("rain", "Drizzle"),
61 or 63 or 65 or 66 or 67 => ("rain", "Rain"),
71 or 73 or 75 or 77 => ("snow", "Snow"),
80 or 81 or 82 => ("rain", "Rain showers"),
85 or 86 => ("snow", "Snow showers"),
95 or 96 or 99 => ("storm", "Thunderstorm"),
_ => ("cloudy", "Cloudy"),
};
(string Top, string Bottom) Sky(string kind) => kind switch
{
"clear" => ("#2E6FD9", "#7FB2F0"),
"partly" => ("#37699E", "#8FB6D8"),
"rain" => ("#2B3F55", "#5C7690"),
"snow" => ("#4A5F7C", "#93A9C4"),
"storm" => ("#2A2A44", "#5B5480"),
"fog" => ("#4A5568", "#94A3B8"),
_ => ("#33465E", "#6B819B"),
};
SkiaGradient SkyGradient(string kind)
{
var sky = Sky(kind);
return new SkiaGradient
{
Type = GradientType.Linear,
StartXRatio = 0, StartYRatio = 0, EndXRatio = 0.35f, EndYRatio = 1,
Colors = new List<Color> { Color.Parse(sky.Top), Color.Parse(sky.Bottom) },
};
}
SkiaLabel lblCity = null, lblWords = null, lblTemp = null, lblRange = null;
SkiaLabel lblFeels = null, lblWind = null, lblHum = null, lblNote = null, unitC = null, unitF = null;
SkiaShape unitPill = null, refreshChip = null, card = null;
SkiaSvg icon = null;
var hourLabels = new SkiaLabel[6];
var hourTemps = new SkiaLabel[6];
var hourIcons = new SkiaSvg[6];
bool fahrenheit = false, haveData = false;
double cNow = 0, cHi = 0, cLo = 0, cFeels = 0, wind = 0, humidity = 0;
int codeNow = 0;
var hours = new List<(string Hour, double C, int Code)>();
// +21°, -4°, 0°
string Deg(double celsius)
{
var v = fahrenheit ? celsius * 9 / 5 + 32 : celsius;
var r = Math.Round(v);
return $"{(r > 0 ? "+" : "")}{r:0}°";
}
void Render()
{
if (!haveData) return;
var (kind, words) = Describe(codeNow);
card.FillGradient = SkyGradient(kind);
icon.SvgString = Icon(kind);
icon.Opacity = 1; // placeholders start dimmed
lblWords.Text = words;
lblTemp.Text = Deg(cNow);
lblRange.Text = $"H {Deg(cHi)} L {Deg(cLo)}";
lblFeels.Text = Deg(cFeels);
lblWind.Text = $"{wind:0} km/h";
lblHum.Text = $"{humidity:0}%";
lblNote.Text = "Refresh";
unitC.TextColor = fahrenheit ? Color.Parse("#99FFFFFF") : Colors.White;
unitF.TextColor = fahrenheit ? Colors.White : Color.Parse("#99FFFFFF");
for (int i = 0; i < 6 && i < hours.Count; i++)
{
hourLabels[i].Text = hours[i].Hour;
hourTemps[i].Text = Deg(hours[i].C);
hourIcons[i].SvgString = Icon(Describe(hours[i].Code).Kind);
hourIcons[i].Opacity = 1;
}
card.Update();
}
void LoadWeather()
{
lblNote.Text = "loading…";
lblNote.Update();
Fiddle.Load($"https://geocoding-api.open-meteo.com/v1/search?name={Uri.EscapeDataString(city)}&count=1&language=en", geo =>
{
if (!geo.TryGetProperty("results", out var results) || results.GetArrayLength() == 0)
{
lblNote.Text = $"unknown city: {city}";
lblNote.Update();
return;
}
var place = results[0];
lblCity.Text = place.GetProperty("name").GetString();
var lat = place.GetProperty("latitude").GetDouble();
var lon = place.GetProperty("longitude").GetDouble();
// Invariant() must wrap the interpolated part alone — concatenating first makes it a plain string
var url = FormattableString.Invariant($"https://api.open-meteo.com/v1/forecast?latitude={lat}&longitude={lon}")
+ "¤t=temperature_2m,apparent_temperature,relative_humidity_2m,wind_speed_10m,weather_code"
+ "&daily=temperature_2m_max,temperature_2m_min"
+ "&hourly=temperature_2m,weather_code&forecast_days=2&timezone=auto";
Fiddle.Load(url, w =>
{
var cur = w.GetProperty("current");
cNow = cur.GetProperty("temperature_2m").GetDouble();
cFeels = cur.GetProperty("apparent_temperature").GetDouble();
humidity = cur.GetProperty("relative_humidity_2m").GetDouble();
wind = cur.GetProperty("wind_speed_10m").GetDouble();
codeNow = cur.GetProperty("weather_code").GetInt32();
var daily = w.GetProperty("daily");
cHi = daily.GetProperty("temperature_2m_max")[0].GetDouble();
cLo = daily.GetProperty("temperature_2m_min")[0].GetDouble();
hours.Clear();
var hourly = w.GetProperty("hourly");
var times = hourly.GetProperty("time");
var temps = hourly.GetProperty("temperature_2m");
var codes = hourly.GetProperty("weather_code");
var nowHour = cur.GetProperty("time").GetString();
var start = 0;
for (int i = 0; i < times.GetArrayLength(); i++)
if (string.CompareOrdinal(times[i].GetString(), nowHour) > 0) { start = i; break; }
for (int i = start; i < start + 6 && i < times.GetArrayLength(); i++)
hours.Add((times[i].GetString().Substring(11, 2), temps[i].GetDouble(), codes[i].GetInt32()));
haveData = true;
Render();
}, err => { lblNote.Text = "weather: " + err; lblNote.Update(); });
}, err => { lblNote.Text = "geocoding: " + err; lblNote.Update(); });
}
// one hourly column — placeholders are the same shape as the real thing
SkiaControl HourCell(int i) => new SkiaLayout
{
Type = LayoutType.Column,
Spacing = 6,
WidthRequest = 44,
HorizontalOptions = LayoutOptions.Start,
Children =
{
new SkiaLabel("--") { FontSize = 12, TextColor = Color.Parse("#B0FFFFFF"), HorizontalOptions = LayoutOptions.Center }.Assign(out hourLabels[i]),
new SkiaSvg { SvgString = Icon("cloudy"), Opacity = 0.45, WidthRequest = 26, HeightRequest = 30, HorizontalOptions = LayoutOptions.Center }.Assign(out hourIcons[i]),
new SkiaLabel("--°") { FontSize = 13, FontWeight = 600, TextColor = Colors.White, HorizontalOptions = LayoutOptions.Center }.Assign(out hourTemps[i]),
}
};
SkiaControl Detail(string caption, out SkiaLabel value) => new SkiaLayout
{
Type = LayoutType.Column,
Spacing = 3,
WidthRequest = 86,
HorizontalOptions = LayoutOptions.Start,
Children =
{
new SkiaLabel(caption) { FontSize = 11, TextColor = Color.Parse("#99FFFFFF"), HorizontalOptions = LayoutOptions.Center },
new SkiaLabel("--") { FontSize = 16, FontWeight = 600, TextColor = Colors.White, HorizontalOptions = LayoutOptions.Center }.Assign(out value),
}
};
card = new SkiaShape
{
CornerRadius = 30,
WidthRequest = 320,
HeightRequest = 536, // fixed: the card never resizes when data arrives
Padding = new Thickness(22, 20, 22, 16),
HorizontalOptions = LayoutOptions.Center,
VerticalOptions = LayoutOptions.Center,
BackgroundColor = Color.Parse("#33465E"),
FillGradient = SkyGradient("partly"), // something to look at while loading
Children = new List<SkiaControl>
{
new SkiaLayout
{
Type = LayoutType.Column,
Spacing = 0,
HorizontalOptions = LayoutOptions.Fill,
Children = new List<SkiaControl>
{
new SkiaLayer
{
HeightRequest = 30,
HorizontalOptions = LayoutOptions.Fill,
Children = new List<SkiaControl>
{
new SkiaLabel(city)
{
FontSize = 21, FontFamily = "FontTextTitle", TextColor = Colors.White,
HorizontalOptions = LayoutOptions.Start, VerticalOptions = LayoutOptions.Center,
}.Assign(out lblCity),
new SkiaShape
{
CornerRadius = 13, WidthRequest = 74, HeightRequest = 26,
BackgroundColor = Color.Parse("#26FFFFFF"),
HorizontalOptions = LayoutOptions.End, VerticalOptions = LayoutOptions.Center,
Children = new List<SkiaControl>
{
new SkiaLabel("°C")
{
FontSize = 13, FontWeight = 700, TextColor = Colors.White,
HorizontalOptions = LayoutOptions.Start, VerticalOptions = LayoutOptions.Center,
Margin = new Thickness(12, 0, 0, 0),
}.Assign(out unitC),
new SkiaLabel("°F")
{
FontSize = 13, FontWeight = 700, TextColor = Color.Parse("#99FFFFFF"),
HorizontalOptions = LayoutOptions.End, VerticalOptions = LayoutOptions.Center,
Margin = new Thickness(0, 0, 12, 0),
}.Assign(out unitF),
}
}.Assign(out unitPill),
}
},
new SkiaSvg
{
SvgString = Icon("cloudy"),
Opacity = 0.5,
WidthRequest = 104, HeightRequest = 118,
HorizontalOptions = LayoutOptions.Center,
Margin = new Thickness(0, 6, 0, 0),
}.Assign(out icon),
new SkiaLabel("—")
{
FontSize = 15, TextColor = Color.Parse("#CCFFFFFF"),
HorizontalOptions = LayoutOptions.Center,
}.Assign(out lblWords),
new SkiaLabel("--°")
{
FontSize = 64, FontFamily = "FontTextTitle", TextColor = Colors.White,
HorizontalOptions = LayoutOptions.Center, Margin = new Thickness(0, 2, 0, 0),
}.Assign(out lblTemp),
new SkiaLabel("H --° L --°")
{
FontSize = 14, TextColor = Color.Parse("#CCFFFFFF"),
HorizontalOptions = LayoutOptions.Center, Margin = new Thickness(0, 0, 0, 14),
}.Assign(out lblRange),
new SkiaShape
{
CornerRadius = 18,
BackgroundColor = Color.Parse("#1FFFFFFF"),
HorizontalOptions = LayoutOptions.Fill,
Padding = new Thickness(6, 10),
Children = new List<SkiaControl>
{
new SkiaLayout
{
Type = LayoutType.Row,
Spacing = 0,
HorizontalOptions = LayoutOptions.Center,
Children = Enumerable.Range(0, 6).Select(i => HourCell(i)).ToList()
}
}
},
new SkiaLayout
{
Type = LayoutType.Row,
Spacing = 0,
HorizontalOptions = LayoutOptions.Center,
Margin = new Thickness(0, 14, 0, 0),
Children = new List<SkiaControl>
{
Detail("FEELS LIKE", out lblFeels),
Detail("WIND", out lblWind),
Detail("HUMIDITY", out lblHum),
}
},
// refresh on its own chip: a card-wide tap handler would swallow the pill's taps
new SkiaShape
{
CornerRadius = 14, WidthRequest = 104, HeightRequest = 28,
BackgroundColor = Color.Parse("#26FFFFFF"),
HorizontalOptions = LayoutOptions.Center,
Margin = new Thickness(0, 14, 0, 0),
Children = new List<SkiaControl>
{
new SkiaLabel("loading…")
{
FontSize = 12, FontWeight = 600, TextColor = Colors.White,
}.Center().Assign(out lblNote),
}
}.Assign(out refreshChip),
}
}
}
};
unitPill.OnTapped(me => { fahrenheit = !fahrenheit; Render(); });
refreshChip.OnTapped(me => { Fiddle.Emit("refresh", new { city }); LoadWeather(); });
LoadWeather();
return card;