This commit is contained in:
xsghetti 2024-04-11 00:21:35 -04:00
parent 1f8cb3c145
commit 610604e80f
253 changed files with 27055 additions and 44 deletions

View file

@ -0,0 +1,106 @@
const { Gtk } = imports.gi;
const Lang = imports.lang;
import Widget from 'resource:///com/github/Aylur/ags/widget.js'
import * as Utils from 'resource:///com/github/Aylur/ags/utils.js'
// -- Styling --
// min-height for diameter
// min-width for trough stroke
// padding for space between trough and progress
// margin for space between widget and parent
// background-color for trough color
// color for progress color
// -- Usage --
// font size for progress value (0-100px) (hacky i know, but i want animations)
export const AnimatedCircProg = ({
initFrom = 0,
initTo = 0,
initAnimTime = 2900,
initAnimPoints = 1,
extraSetup = () => { },
...rest
}) => Widget.DrawingArea({
...rest,
css: `${initFrom != initTo ? 'font-size: ' + initFrom + 'px; transition: ' + initAnimTime + 'ms linear;' : ''}`,
setup: (area) => {
const styleContext = area.get_style_context();
const width = styleContext.get_property('min-height', Gtk.StateFlags.NORMAL);
const height = styleContext.get_property('min-height', Gtk.StateFlags.NORMAL);
const padding = styleContext.get_padding(Gtk.StateFlags.NORMAL).left;
const marginLeft = styleContext.get_margin(Gtk.StateFlags.NORMAL).left;
const marginRight = styleContext.get_margin(Gtk.StateFlags.NORMAL).right;
const marginTop = styleContext.get_margin(Gtk.StateFlags.NORMAL).top;
const marginBottom = styleContext.get_margin(Gtk.StateFlags.NORMAL).bottom;
area.set_size_request(width + marginLeft + marginRight, height + marginTop + marginBottom);
area.connect('draw', Lang.bind(area, (area, cr) => {
const styleContext = area.get_style_context();
const width = styleContext.get_property('min-height', Gtk.StateFlags.NORMAL);
const height = styleContext.get_property('min-height', Gtk.StateFlags.NORMAL);
const padding = styleContext.get_padding(Gtk.StateFlags.NORMAL).left;
const marginLeft = styleContext.get_margin(Gtk.StateFlags.NORMAL).left;
const marginRight = styleContext.get_margin(Gtk.StateFlags.NORMAL).right;
const marginTop = styleContext.get_margin(Gtk.StateFlags.NORMAL).top;
const marginBottom = styleContext.get_margin(Gtk.StateFlags.NORMAL).bottom;
area.set_size_request(width + marginLeft + marginRight, height + marginTop + marginBottom);
const progressValue = styleContext.get_property('font-size', Gtk.StateFlags.NORMAL) / 100.0;
const bg_stroke = styleContext.get_property('min-width', Gtk.StateFlags.NORMAL);
const fg_stroke = bg_stroke - padding;
const radius = Math.min(width, height) / 2.0 - Math.max(bg_stroke, fg_stroke) / 2.0;
const center_x = width / 2.0 + marginLeft;
const center_y = height / 2.0 + marginTop;
const start_angle = -Math.PI / 2.0;
const end_angle = start_angle + (2 * Math.PI * progressValue);
const start_x = center_x + Math.cos(start_angle) * radius;
const start_y = center_y + Math.sin(start_angle) * radius;
const end_x = center_x + Math.cos(end_angle) * radius;
const end_y = center_y + Math.sin(end_angle) * radius;
// Draw background
const background_color = styleContext.get_property('background-color', Gtk.StateFlags.NORMAL);
cr.setSourceRGBA(background_color.red, background_color.green, background_color.blue, background_color.alpha);
cr.arc(center_x, center_y, radius, 0, 2 * Math.PI);
cr.setLineWidth(bg_stroke);
cr.stroke();
if (progressValue == 0) return;
// Draw progress
const color = styleContext.get_property('color', Gtk.StateFlags.NORMAL);
cr.setSourceRGBA(color.red, color.green, color.blue, color.alpha);
cr.arc(center_x, center_y, radius, start_angle, end_angle);
cr.setLineWidth(fg_stroke);
cr.stroke();
// Draw rounded ends for progress arcs
cr.setLineWidth(0);
cr.arc(start_x, start_y, fg_stroke / 2, 0, 0 - 0.01);
cr.fill();
cr.arc(end_x, end_y, fg_stroke / 2, 0, 0 - 0.01);
cr.fill();
}));
// Init animation
if (initFrom != initTo) {
area.css = `font-size: ${initFrom}px; transition: ${initAnimTime}ms linear;`;
Utils.timeout(20, () => {
area.css = `font-size: ${initTo}px;`;
}, area)
const transitionDistance = initTo - initFrom;
const oneStep = initAnimTime / initAnimPoints;
area.css = `
font-size: ${initFrom}px;
transition: ${oneStep}ms linear;
`;
for (let i = 0; i < initAnimPoints; i++) {
Utils.timeout(Math.max(10, i * oneStep), () => {
if(!area) return;
area.css = `${initFrom != initTo ? 'font-size: ' + (initFrom + (transitionDistance / initAnimPoints * (i + 1))) + 'px;' : ''}`;
});
}
}
else area.css = 'font-size: 0px;';
extraSetup(area);
},
})

View file

@ -0,0 +1,71 @@
const { Gtk } = imports.gi;
const Lang = imports.lang;
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
// min-height/min-width for height/width
// background-color/color for background/indicator color
// padding for pad of indicator
// font-size for selected index (0-based)
export const NavigationIndicator = ({count, vertical, ...props}) => Widget.DrawingArea({
...props,
setup: (area) => {
const styleContext = area.get_style_context();
const width = Math.max(styleContext.get_property('min-width', Gtk.StateFlags.NORMAL), area.get_allocated_width());
const height = Math.max(styleContext.get_property('min-height', Gtk.StateFlags.NORMAL), area.get_allocated_height());
area.set_size_request(width, height);
area.connect('draw', Lang.bind(area, (area, cr) => {
const styleContext = area.get_style_context();
const width = Math.max(styleContext.get_property('min-width', Gtk.StateFlags.NORMAL), area.get_allocated_width());
const height = Math.max(styleContext.get_property('min-height', Gtk.StateFlags.NORMAL), area.get_allocated_height());
// console.log('allocated width/height:', area.get_allocated_width(), '/', area.get_allocated_height())
area.set_size_request(width, height);
const paddingLeft = styleContext.get_padding(Gtk.StateFlags.NORMAL).left;
const paddingRight = styleContext.get_padding(Gtk.StateFlags.NORMAL).right;
const paddingTop = styleContext.get_padding(Gtk.StateFlags.NORMAL).top;
const paddingBottom = styleContext.get_padding(Gtk.StateFlags.NORMAL).bottom;
const selectedCell = styleContext.get_property('font-size', Gtk.StateFlags.NORMAL);
let cellWidth = width;
let cellHeight = height;
if (vertical) cellHeight /= count;
else cellWidth /= count;
const indicatorWidth = cellWidth - paddingLeft - paddingRight;
const indicatorHeight = cellHeight - paddingTop - paddingBottom;
const background_color = styleContext.get_property('background-color', Gtk.StateFlags.NORMAL);
const color = styleContext.get_property('color', Gtk.StateFlags.NORMAL);
cr.setLineWidth(2);
// Background
cr.setSourceRGBA(background_color.red, background_color.green, background_color.blue, background_color.alpha);
cr.rectangle(0, 0, width, height);
cr.fill();
// The indicator line
cr.setSourceRGBA(color.red, color.green, color.blue, color.alpha);
if (vertical) {
cr.rectangle(paddingLeft, paddingTop + cellHeight * selectedCell + indicatorWidth / 2, indicatorWidth, indicatorHeight - indicatorWidth);
cr.stroke();
cr.rectangle(paddingLeft, paddingTop + cellHeight * selectedCell + indicatorWidth / 2, indicatorWidth, indicatorHeight - indicatorWidth);
cr.fill();
cr.arc(paddingLeft + indicatorWidth / 2, paddingTop + cellHeight * selectedCell + indicatorWidth / 2, indicatorWidth / 2, Math.PI, 2 * Math.PI);
cr.fill();
cr.arc(paddingLeft + indicatorWidth / 2, paddingTop + cellHeight * selectedCell + indicatorHeight - indicatorWidth / 2, indicatorWidth / 2, 0, Math.PI);
cr.fill();
}
else {
cr.rectangle(paddingLeft + cellWidth * selectedCell + indicatorHeight / 2, paddingTop, indicatorWidth - indicatorHeight, indicatorHeight);
cr.stroke();
cr.rectangle(paddingLeft + cellWidth * selectedCell + indicatorHeight / 2, paddingTop, indicatorWidth - indicatorHeight, indicatorHeight);
cr.fill();
cr.arc(paddingLeft + cellWidth * selectedCell + indicatorHeight / 2, paddingTop + indicatorHeight / 2, indicatorHeight / 2, 0.5 * Math.PI, 1.5 * Math.PI);
cr.fill();
cr.arc(paddingLeft + cellWidth * selectedCell + indicatorWidth - indicatorHeight / 2, paddingTop + indicatorHeight / 2, indicatorHeight / 2, -0.5 * Math.PI, 0.5 * Math.PI);
cr.fill();
}
}))
},
})

View file

@ -0,0 +1,50 @@
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
const { Gtk } = imports.gi;
const Lang = imports.lang;
export const RoundedCorner = (place, props) => Widget.DrawingArea({
...props,
hpack: place.includes('left') ? 'start' : 'end',
vpack: place.includes('top') ? 'start' : 'end',
setup: (widget) => Utils.timeout(1, () => {
const c = widget.get_style_context().get_property('background-color', Gtk.StateFlags.NORMAL);
const r = widget.get_style_context().get_property('border-radius', Gtk.StateFlags.NORMAL);
widget.set_size_request(r, r);
widget.connect('draw', Lang.bind(widget, (widget, cr) => {
const c = widget.get_style_context().get_property('background-color', Gtk.StateFlags.NORMAL);
const r = widget.get_style_context().get_property('border-radius', Gtk.StateFlags.NORMAL);
// const borderColor = widget.get_style_context().get_property('color', Gtk.StateFlags.NORMAL);
// const borderWidth = widget.get_style_context().get_border(Gtk.StateFlags.NORMAL).left; // ur going to write border-width: something anyway
widget.set_size_request(r, r);
switch (place) {
case 'topleft':
cr.arc(r, r, r, Math.PI, 3 * Math.PI / 2);
cr.lineTo(0, 0);
break;
case 'topright':
cr.arc(0, r, r, 3 * Math.PI / 2, 2 * Math.PI);
cr.lineTo(r, 0);
break;
case 'bottomleft':
cr.arc(r, 0, r, Math.PI / 2, Math.PI);
cr.lineTo(0, r);
break;
case 'bottomright':
cr.arc(0, 0, r, 0, Math.PI / 2);
cr.lineTo(r, r);
break;
}
cr.closePath();
cr.setSourceRGBA(c.red, c.green, c.blue, c.alpha);
cr.fill();
// cr.setLineWidth(borderWidth);
// cr.setSourceRGBA(borderColor.red, borderColor.green, borderColor.blue, borderColor.alpha);
// cr.stroke();
}));
}),
});

View file

@ -0,0 +1,49 @@
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
const { Gtk } = imports.gi;
const Lang = imports.lang;
export const AnimatedSlider = ({
className,
value,
...rest
}) => {
return Widget.DrawingArea({
className: `${className}`,
setup: (self) => {
self.connect('draw', Lang.bind(self, (self, cr) => {
const styleContext = self.get_style_context();
const allocatedWidth = self.get_allocated_width();
const allocatedHeight = self.get_allocated_height();
console.log(allocatedHeight, allocatedWidth)
const minWidth = styleContext.get_property('min-width', Gtk.StateFlags.NORMAL);
const minHeight = styleContext.get_property('min-height', Gtk.StateFlags.NORMAL);
const radius = styleContext.get_property('border-radius', Gtk.StateFlags.NORMAL);
const bg = styleContext.get_property('background-color', Gtk.StateFlags.NORMAL);
const fg = styleContext.get_property('color', Gtk.StateFlags.NORMAL);
const value = styleContext.get_property('font-size', Gtk.StateFlags.NORMAL) / 100;
self.set_size_request(-1, minHeight);
const width = allocatedHeight;
const height = minHeight;
cr.arc(radius, radius, radius, -1 * Math.PI, -0.5 * Math.PI); // Top-left
cr.arc(width - radius, radius, radius, -0.5 * Math.PI, 0); // Top-right
cr.arc(width - radius, height - radius, radius, 0, 0.5 * Math.PI); // Bottom-left
cr.arc(radius, height - radius, radius, 0.5 * Math.PI, 1 * Math.PI); // Bottom-right
cr.setSourceRGBA(bg.red, bg.green, bg.blue, bg.alpha);
cr.closePath();
cr.fill();
// const valueWidth = width * value;
// cr.arc(radius, radius, radius, -1 * Math.PI, -0.5 * Math.PI); // Top-left
// cr.arc(valueWidth - radius, radius, radius, -0.5 * Math.PI, 0); // Top-right
// cr.arc(valueWidth - radius, height - radius, radius, 0, 0.5 * Math.PI); // Bottom-left
// cr.arc(radius, height - radius, radius, 0.5 * Math.PI, 1 * Math.PI); // Bottom-right
// cr.setSourceRGBA(fg.red, fg.green, fg.blue, fg.alpha);
// cr.closePath();
// cr.fill();
}));
},
...rest,
})
}

View file

@ -0,0 +1,185 @@
import Variable from 'resource:///com/github/Aylur/ags/variable.js';
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
import { MaterialIcon } from './materialicon.js';
import { setupCursorHover } from '../.widgetutils/cursorhover.js';
const { Box, Button, Label, Revealer } = Widget;
export const ConfigToggle = ({
icon, name, desc = '', initValue,
expandWidget = true,
onChange = () => { }, extraSetup = () => { },
...rest
}) => {
const enabled = Variable(initValue);
const toggleIcon = Label({
className: `icon-material txt-bold ${enabled.value ? '' : 'txt-poof'}`,
label: `${enabled.value ? 'check' : ''}`,
setup: (self) => self.hook(enabled, (self) => {
self.toggleClassName('switch-fg-toggling-false', false);
if (!enabled.value) {
self.label = '';
self.toggleClassName('txt-poof', true);
}
else Utils.timeout(1, () => {
toggleIcon.label = 'check';
toggleIcon.toggleClassName('txt-poof', false);
})
}),
})
const toggleButtonIndicator = Box({
className: `switch-fg ${enabled.value ? 'switch-fg-true' : ''}`,
vpack: 'center',
hpack: 'start',
homogeneous: true,
children: [toggleIcon,],
setup: (self) => self.hook(enabled, (self) => {
self.toggleClassName('switch-fg-true', enabled.value);
}),
});
const toggleButton = Box({
hpack: 'end',
className: `switch-bg ${enabled.value ? 'switch-bg-true' : ''}`,
homogeneous: true,
children: [toggleButtonIndicator],
setup: (self) => self.hook(enabled, (self) => {
self.toggleClassName('switch-bg-true', enabled.value);
}),
});
const widgetContent = Box({
tooltipText: desc,
className: 'txt spacing-h-5 configtoggle-box',
children: [
(icon !== undefined ? MaterialIcon(icon, 'norm') : null),
(name !== undefined ? Label({
className: 'txt txt-small',
label: name,
}) : null),
expandWidget ? Box({ hexpand: true }) : null,
toggleButton,
]
});
const interactionWrapper = Button({
attribute: {
toggle: (newValue) => {
enabled.value = !enabled.value;
onChange(interactionWrapper, enabled.value);
}
},
child: widgetContent,
onClicked: (self) => self.attribute.toggle(self),
setup: (self) => {
setupCursorHover(self);
self.connect('pressed', () => { // mouse down
toggleIcon.toggleClassName('txt-poof', true);
toggleIcon.toggleClassName('switch-fg-true', false);
if (!enabled.value) toggleIcon.toggleClassName('switch-fg-toggling-false', true);
});
extraSetup(self)
},
...rest,
});
interactionWrapper.enabled = enabled;
return interactionWrapper;
}
export const ConfigSegmentedSelection = ({
icon, name, desc = '',
options = [{ name: 'Option 1', value: 0 }, { name: 'Option 2', value: 1 }],
initIndex = 0,
onChange,
...rest
}) => {
let lastSelected = initIndex;
let value = options[initIndex].value;
const widget = Box({
tooltipText: desc,
className: 'segment-container',
// homogeneous: true,
children: options.map((option, id) => {
const selectedIcon = Revealer({
revealChild: id == initIndex,
transition: 'slide_right',
transitionDuration: userOptions.animations.durationSmall,
child: MaterialIcon('check', 'norm')
});
return Button({
setup: setupCursorHover,
className: `segment-btn ${id == initIndex ? 'segment-btn-enabled' : ''}`,
child: Box({
hpack: 'center',
className: 'spacing-h-5',
children: [
selectedIcon,
Label({
label: option.name,
})
]
}),
onClicked: (self) => {
value = option.value;
const kids = widget.get_children();
kids[lastSelected].toggleClassName('segment-btn-enabled', false);
kids[lastSelected].get_children()[0].get_children()[0].revealChild = false;
lastSelected = id;
self.toggleClassName('segment-btn-enabled', true);
selectedIcon.revealChild = true;
onChange(option.value, option.name);
}
})
}),
...rest,
});
return widget;
}
export const ConfigMulipleSelection = ({
icon, name, desc = '',
optionsArr = [
[{ name: 'Option 1', value: 0 }, { name: 'Option 2', value: 1 }],
[{ name: 'Option 3', value: 0 }, { name: 'Option 4', value: 1 }],
],
initIndex = [0, 0],
onChange,
...rest
}) => {
let lastSelected = initIndex;
let value = optionsArr[initIndex[0]][initIndex[1]].value;
const widget = Box({
tooltipText: desc,
className: 'multipleselection-container spacing-v-3',
vertical: true,
children: optionsArr.map((options, grp) => {
return Box({
className: 'spacing-h-5',
hpack: 'center',
children: options.map((option, id) => {
return Button({
setup: setupCursorHover,
className: `multipleselection-btn ${id == initIndex[1] && grp == initIndex[0] ? 'multipleselection-btn-enabled' : ''}`,
label: option.name,
onClicked: (self) => {
const kidsg = widget.get_children();
const kids = kidsg.flatMap(widget => widget.get_children());
kids.forEach(kid => {
kid.toggleClassName('multipleselection-btn-enabled', false);
});
lastSelected = id;
self.toggleClassName('multipleselection-btn-enabled', true);
onChange(option.value, option.name);
}
})
}),
})
}),
...rest,
});
return widget;
}
export const ConfigGap = ({ vertical = true, size = 5, ...rest }) => Box({
className: `gap-${vertical ? 'v' : 'h'}-${size}`,
...rest,
})

View file

@ -0,0 +1,7 @@
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
export const MaterialIcon = (icon, size, props = {}) => Widget.Label({
className: `icon-material txt-${size}`,
label: icon,
...props,
})

View file

@ -0,0 +1,448 @@
// This file is for the actual widget for each single notification
const { GLib, Gdk, Gtk } = imports.gi;
import Widget from 'resource:///com/github/Aylur/ags/widget.js'
import * as Utils from 'resource:///com/github/Aylur/ags/utils.js'
const { Box, EventBox, Icon, Overlay, Label, Button, Revealer } = Widget;
import { MaterialIcon } from './materialicon.js';
import { setupCursorHover } from "../.widgetutils/cursorhover.js";
import { AnimatedCircProg } from "./cairo_circularprogress.js";
function guessMessageType(summary) {
const str = summary.toLowerCase();
if (str.includes('reboot')) return 'restart_alt';
if (str.includes('recording')) return 'screen_record';
if (str.includes('battery') || summary.includes('power')) return 'power';
if (str.includes('screenshot')) return 'screenshot_monitor';
if (str.includes('welcome')) return 'waving_hand';
if (str.includes('time')) return 'scheduleb';
if (str.includes('installed')) return 'download';
if (str.includes('update')) return 'update';
if (str.startsWith('file')) return 'folder_copy';
return 'chat';
}
function exists(widget) {
return widget !== null;
}
const NotificationIcon = (notifObject) => {
// { appEntry, appIcon, image }, urgency = 'normal'
if (notifObject.image) {
return Box({
valign: Gtk.Align.CENTER,
hexpand: false,
className: 'notif-icon',
css: `
background-image: url("${notifObject.image}");
background-size: auto 100%;
background-repeat: no-repeat;
background-position: center;
`,
});
}
let icon = 'NO_ICON';
if (Utils.lookUpIcon(notifObject.appIcon))
icon = notifObject.appIcon;
if (Utils.lookUpIcon(notifObject.appEntry))
icon = notifObject.appEntry;
return Box({
vpack: 'center',
hexpand: false,
className: `notif-icon notif-icon-material-${notifObject.urgency}`,
homogeneous: true,
children: [
(icon != 'NO_ICON' ?
Icon({
vpack: 'center',
icon: icon,
})
:
MaterialIcon(`${notifObject.urgency == 'critical' ? 'release_alert' : guessMessageType(notifObject.summary.toLowerCase())}`, 'hugerass', {
hexpand: true,
})
)
],
});
};
export default ({
notifObject,
isPopup = false,
props = {},
} = {}) => {
const popupTimeout = notifObject.timeout || (notifObject.urgency == 'critical' ? 8000 : 3000);
const command = (isPopup ?
() => notifObject.dismiss() :
() => notifObject.close()
)
const destroyWithAnims = () => {
widget.sensitive = false;
notificationBox.setCss(middleClickClose);
Utils.timeout(userOptions.animations.durationSmall, () => {
if (wholeThing) wholeThing.revealChild = false;
}, wholeThing);
Utils.timeout(userOptions.animations.durationSmall * 2, () => {
command();
if (wholeThing) {
wholeThing.destroy();
wholeThing = null;
}
}, wholeThing);
}
const widget = EventBox({
onHover: (self) => {
self.window.set_cursor(Gdk.Cursor.new_from_name(display, 'grab'));
if (!wholeThing.attribute.hovered)
wholeThing.attribute.hovered = true;
},
onHoverLost: (self) => {
self.window.set_cursor(null);
if (wholeThing.attribute.hovered)
wholeThing.attribute.hovered = false;
if (isPopup) {
command();
}
},
onMiddleClick: (self) => {
destroyWithAnims();
},
setup: (self) => {
self.on("button-press-event", () => {
wholeThing.attribute.held = true;
notificationContent.toggleClassName(`${isPopup ? 'popup-' : ''}notif-clicked-${notifObject.urgency}`, true);
Utils.timeout(800, () => {
if (wholeThing?.attribute.held) {
Utils.execAsync(['wl-copy', `${notifObject.body}`]).catch(print);
notifTextSummary.label = notifObject.summary + " (copied)";
Utils.timeout(3000, () => notifTextSummary.label = notifObject.summary)
}
})
}).on("button-release-event", () => {
wholeThing.attribute.held = false;
notificationContent.toggleClassName(`${isPopup ? 'popup-' : ''}notif-clicked-${notifObject.urgency}`, false);
})
}
});
let wholeThing = Revealer({
attribute: {
'close': undefined,
'destroyWithAnims': destroyWithAnims,
'dragging': false,
'held': false,
'hovered': false,
'id': notifObject.id,
},
revealChild: false,
transition: 'slide_down',
transitionDuration: userOptions.animations.durationLarge,
child: Box({ // Box to make sure css-based spacing works
homogeneous: true,
}),
});
const display = Gdk.Display.get_default();
const notifTextPreview = Revealer({
transition: 'slide_down',
transitionDuration: userOptions.animations.durationSmall,
revealChild: true,
child: Label({
xalign: 0,
className: `txt-smallie notif-body-${notifObject.urgency}`,
useMarkup: true,
xalign: 0,
justify: Gtk.Justification.LEFT,
maxWidthChars: 24,
truncate: 'end',
label: notifObject.body.split("\n")[0],
}),
});
const notifTextExpanded = Revealer({
transition: 'slide_up',
transitionDuration: userOptions.animations.durationSmall,
revealChild: false,
child: Box({
vertical: true,
className: 'spacing-v-10',
children: [
Label({
xalign: 0,
className: `txt-smallie notif-body-${notifObject.urgency}`,
useMarkup: true,
xalign: 0,
justify: Gtk.Justification.LEFT,
maxWidthChars: 24,
wrap: true,
label: notifObject.body,
}),
Box({
className: 'notif-actions spacing-h-5',
children: [
Button({
hexpand: true,
className: `notif-action notif-action-${notifObject.urgency}`,
onClicked: () => destroyWithAnims(),
setup: setupCursorHover,
child: Label({
label: 'Close',
}),
}),
...notifObject.actions.map(action => Widget.Button({
hexpand: true,
className: `notif-action notif-action-${notifObject.urgency}`,
onClicked: () => notifObject.invoke(action.id),
setup: setupCursorHover,
child: Label({
label: action.label,
}),
}))
],
})
]
}),
});
const notifIcon = Box({
vpack: 'start',
homogeneous: true,
children: [
Overlay({
child: NotificationIcon(notifObject),
overlays: isPopup ? [AnimatedCircProg({
className: `notif-circprog-${notifObject.urgency}`,
vpack: 'center', hpack: 'center',
initFrom: (isPopup ? 100 : 0),
initTo: 0,
initAnimTime: popupTimeout,
})] : [],
}),
]
});
let notifTime = '';
const messageTime = GLib.DateTime.new_from_unix_local(notifObject.time);
if (messageTime.get_day_of_year() == GLib.DateTime.new_now_local().get_day_of_year())
notifTime = messageTime.format(userOptions.time.format);
else if (messageTime.get_day_of_year() == GLib.DateTime.new_now_local().get_day_of_year() - 1)
notifTime = 'Yesterday';
else
notifTime = messageTime.format(userOptions.time.dateFormat);
const notifTextSummary = Label({
xalign: 0,
className: 'txt-small txt-semibold titlefont',
justify: Gtk.Justification.LEFT,
hexpand: true,
maxWidthChars: 24,
truncate: 'end',
ellipsize: 3,
useMarkup: notifObject.summary.startsWith('<'),
label: notifObject.summary,
});
const notifTextBody = Label({
vpack: 'center',
justification: 'right',
className: 'txt-smaller txt-semibold',
label: notifTime,
});
const notifText = Box({
valign: Gtk.Align.CENTER,
vertical: true,
hexpand: true,
children: [
Box({
children: [
notifTextSummary,
notifTextBody,
]
}),
notifTextPreview,
notifTextExpanded,
]
});
const notifExpandButton = Button({
vpack: 'start',
className: 'notif-expand-btn',
onClicked: (self) => {
if (notifTextPreview.revealChild) { // Expanding...
notifTextPreview.revealChild = false;
notifTextExpanded.revealChild = true;
self.child.label = 'expand_less';
expanded = true;
}
else {
notifTextPreview.revealChild = true;
notifTextExpanded.revealChild = false;
self.child.label = 'expand_more';
expanded = false;
}
},
child: MaterialIcon('expand_more', 'norm', {
vpack: 'center',
}),
setup: setupCursorHover,
});
const notificationContent = Box({
...props,
className: `${isPopup ? 'popup-' : ''}notif-${notifObject.urgency} spacing-h-10`,
children: [
notifIcon,
Box({
className: 'spacing-h-5',
children: [
notifText,
notifExpandButton,
]
})
]
})
// Gesture stuff
const gesture = Gtk.GestureDrag.new(widget);
var initDirX = 0;
var initDirVertical = -1; // -1: unset, 0: horizontal, 1: vertical
var expanded = false;
// in px
const startMargin = 0;
const MOVE_THRESHOLD = 10;
const DRAG_CONFIRM_THRESHOLD = 100;
// in rem
const maxOffset = 10.227;
const endMargin = 20.455;
const disappearHeight = 6.818;
const leftAnim1 = `transition: ${userOptions.animations.durationSmall}ms cubic-bezier(0.05, 0.7, 0.1, 1);
margin-left: -${Number(maxOffset + endMargin)}rem;
margin-right: ${Number(maxOffset + endMargin)}rem;
opacity: 0;`;
const rightAnim1 = `transition: ${userOptions.animations.durationSmall}ms cubic-bezier(0.05, 0.7, 0.1, 1);
margin-left: ${Number(maxOffset + endMargin)}rem;
margin-right: -${Number(maxOffset + endMargin)}rem;
opacity: 0;`;
const middleClickClose = `transition: ${userOptions.animations.durationSmall}ms cubic-bezier(0.85, 0, 0.15, 1);
margin-left: ${Number(maxOffset + endMargin)}rem;
margin-right: -${Number(maxOffset + endMargin)}rem;
opacity: 0;`;
const notificationBox = Box({
attribute: {
'leftAnim1': leftAnim1,
'rightAnim1': rightAnim1,
'middleClickClose': middleClickClose,
'ready': false,
},
homogeneous: true,
children: [notificationContent],
setup: (self) => self
.hook(gesture, self => {
var offset_x = gesture.get_offset()[1];
var offset_y = gesture.get_offset()[2];
// Which dir?
if (initDirVertical == -1) {
if (Math.abs(offset_y) > MOVE_THRESHOLD)
initDirVertical = 1;
if (initDirX == 0 && Math.abs(offset_x) > MOVE_THRESHOLD) {
initDirVertical = 0;
initDirX = (offset_x > 0 ? 1 : -1);
}
}
// Horizontal drag
if (initDirVertical == 0 && offset_x > MOVE_THRESHOLD) {
if (initDirX < 0)
self.setCss(`margin-left: 0px; margin-right: 0px;`);
else
self.setCss(`
margin-left: ${Number(offset_x + startMargin - MOVE_THRESHOLD)}px;
margin-right: -${Number(offset_x + startMargin - MOVE_THRESHOLD)}px;
`);
}
else if (initDirVertical == 0 && offset_x < -MOVE_THRESHOLD) {
if (initDirX > 0)
self.setCss(`margin-left: 0px; margin-right: 0px;`);
else {
offset_x = Math.abs(offset_x);
self.setCss(`
margin-right: ${Number(offset_x + startMargin - MOVE_THRESHOLD)}px;
margin-left: -${Number(offset_x + startMargin - MOVE_THRESHOLD)}px;
`);
}
}
// Update dragging
wholeThing.attribute.dragging = Math.abs(offset_x) > MOVE_THRESHOLD;
if (Math.abs(offset_x) > MOVE_THRESHOLD ||
Math.abs(offset_y) > MOVE_THRESHOLD) wholeThing.attribute.held = false;
widget.window?.set_cursor(Gdk.Cursor.new_from_name(display, 'grabbing'));
// Vertical drag
if (initDirVertical == 1 && offset_y > MOVE_THRESHOLD && !expanded) {
notifTextPreview.revealChild = false;
notifTextExpanded.revealChild = true;
expanded = true;
notifExpandButton.child.label = 'expand_less';
}
else if (initDirVertical == 1 && offset_y < -MOVE_THRESHOLD && expanded) {
notifTextPreview.revealChild = true;
notifTextExpanded.revealChild = false;
expanded = false;
notifExpandButton.child.label = 'expand_more';
}
}, 'drag-update')
.hook(gesture, self => {
if (!self.attribute.ready) {
wholeThing.revealChild = true;
self.attribute.ready = true;
return;
}
const offset_h = gesture.get_offset()[1];
if (Math.abs(offset_h) > DRAG_CONFIRM_THRESHOLD && offset_h * initDirX > 0) {
if (offset_h > 0) {
self.setCss(rightAnim1);
widget.sensitive = false;
}
else {
self.setCss(leftAnim1);
widget.sensitive = false;
}
Utils.timeout(userOptions.animations.durationSmall, () => {
if (wholeThing) wholeThing.revealChild = false;
}, wholeThing);
Utils.timeout(userOptions.animations.durationSmall * 2, () => {
command();
if (wholeThing) {
wholeThing.destroy();
wholeThing = null;
}
}, wholeThing);
}
else {
self.setCss(`transition: margin 200ms cubic-bezier(0.05, 0.7, 0.1, 1), opacity 200ms cubic-bezier(0.05, 0.7, 0.1, 1);
margin-left: ${startMargin}px;
margin-right: ${startMargin}px;
margin-bottom: unset; margin-top: unset;
opacity: 1;`);
if (widget.window)
widget.window.set_cursor(Gdk.Cursor.new_from_name(display, 'grab'));
wholeThing.attribute.dragging = false;
}
initDirX = 0;
initDirVertical = -1;
}, 'drag-end')
,
})
widget.add(notificationBox);
wholeThing.child.children = [widget];
if (isPopup) Utils.timeout(popupTimeout, () => {
if (wholeThing) {
wholeThing.revealChild = false;
Utils.timeout(userOptions.animations.durationSmall, () => {
if (wholeThing) {
wholeThing.destroy();
wholeThing = null;
}
command();
}, wholeThing);
}
})
return wholeThing;
}

View file

@ -0,0 +1,298 @@
import App from 'resource:///com/github/Aylur/ags/app.js';
import Audio from 'resource:///com/github/Aylur/ags/service/audio.js';
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
import { MaterialIcon } from './materialicon.js';
import Bluetooth from 'resource:///com/github/Aylur/ags/service/bluetooth.js';
import Network from 'resource:///com/github/Aylur/ags/service/network.js';
import Notifications from 'resource:///com/github/Aylur/ags/service/notifications.js';
import { languages } from './statusicons_languages.js';
// A guessing func to try to support langs not listed in data/languages.js
function isLanguageMatch(abbreviation, word) {
const lowerAbbreviation = abbreviation.toLowerCase();
const lowerWord = word.toLowerCase();
let j = 0;
for (let i = 0; i < lowerWord.length; i++) {
if (lowerWord[i] === lowerAbbreviation[j]) {
j++;
}
if (j === lowerAbbreviation.length) {
return true;
}
}
return false;
}
export const MicMuteIndicator = () => Widget.Revealer({
transition: 'slide_left',
transitionDuration: userOptions.animations.durationSmall,
revealChild: false,
setup: (self) => self.hook(Audio, (self) => {
self.revealChild = Audio.microphone?.stream?.isMuted;
}),
child: MaterialIcon('mic_off', 'norm'),
});
export const NotificationIndicator = (notifCenterName = 'sideright') => {
const widget = Widget.Revealer({
transition: 'slide_left',
transitionDuration: userOptions.animations.durationSmall,
revealChild: false,
setup: (self) => self
.hook(Notifications, (self, id) => {
if (!id || Notifications.dnd) return;
if (!Notifications.getNotification(id)) return;
self.revealChild = true;
}, 'notified')
.hook(App, (self, currentName, visible) => {
if (visible && currentName === notifCenterName) {
self.revealChild = false;
}
})
,
child: Widget.Box({
children: [
MaterialIcon('notifications', 'norm'),
Widget.Label({
className: 'txt-small titlefont',
attribute: {
unreadCount: 0,
update: (self) => self.label = `${self.attribute.unreadCount}`,
},
setup: (self) => self
.hook(Notifications, (self, id) => {
if (!id || Notifications.dnd) return;
if (!Notifications.getNotification(id)) return;
self.attribute.unreadCount++;
self.attribute.update(self);
}, 'notified')
.hook(App, (self, currentName, visible) => {
if (visible && currentName === notifCenterName) {
self.attribute.unreadCount = 0;
self.attribute.update(self);
}
})
,
})
]
})
});
return widget;
}
export const BluetoothIndicator = () => Widget.Stack({
transition: 'slide_up_down',
transitionDuration: userOptions.animations.durationSmall,
children: {
'false': Widget.Label({ className: 'txt-norm icon-material', label: 'bluetooth_disabled' }),
'true': Widget.Label({ className: 'txt-norm icon-material', label: 'bluetooth' }),
},
setup: (self) => self
.hook(Bluetooth, stack => {
stack.shown = String(Bluetooth.enabled);
})
,
});
const BluetoothDevices = () => Widget.Box({
className: 'spacing-h-5',
setup: self => self.hook(Bluetooth, self => {
self.children = Bluetooth.connected_devices.map((device) => {
return Widget.Box({
className: 'bar-bluetooth-device spacing-h-5',
vpack: 'center',
tooltipText: device.name,
children: [
Widget.Icon(`${device.iconName}-symbolic`),
(device.batteryPercentage ? Widget.Label({
className: 'txt-smallie',
label: `${device.batteryPercentage}`,
setup: (self) => {
self.hook(device, (self) => {
self.label = `${device.batteryPercentage}`;
}, 'notify::batteryPercentage')
}
}) : null),
]
});
});
self.visible = Bluetooth.connected_devices.length > 0;
}, 'notify::connected-devices'),
})
const NetworkWiredIndicator = () => Widget.Stack({
transition: 'slide_up_down',
transitionDuration: userOptions.animations.durationSmall,
children: {
'fallback': SimpleNetworkIndicator(),
'unknown': Widget.Label({ className: 'txt-norm icon-material', label: 'wifi_off' }),
'disconnected': Widget.Label({ className: 'txt-norm icon-material', label: 'signal_wifi_off' }),
'connected': Widget.Label({ className: 'txt-norm icon-material', label: 'lan' }),
'connecting': Widget.Label({ className: 'txt-norm icon-material', label: 'settings_ethernet' }),
},
setup: (self) => self.hook(Network, stack => {
if (!Network.wired)
return;
const { internet } = Network.wired;
if (['connecting', 'connected'].includes(internet))
stack.shown = internet;
else if (Network.connectivity !== 'full')
stack.shown = 'disconnected';
else
stack.shown = 'fallback';
}),
});
const SimpleNetworkIndicator = () => Widget.Icon({
setup: (self) => self.hook(Network, self => {
const icon = Network[Network.primary || 'wifi']?.iconName;
self.icon = icon || '';
self.visible = icon;
}),
});
const NetworkWifiIndicator = () => Widget.Stack({
transition: 'slide_up_down',
transitionDuration: userOptions.animations.durationSmall,
children: {
'disabled': Widget.Label({ className: 'txt-norm icon-material', label: 'wifi_off' }),
'disconnected': Widget.Label({ className: 'txt-norm icon-material', label: 'signal_wifi_off' }),
'connecting': Widget.Label({ className: 'txt-norm icon-material', label: 'settings_ethernet' }),
'0': Widget.Label({ className: 'txt-norm icon-material', label: 'signal_wifi_0_bar' }),
'1': Widget.Label({ className: 'txt-norm icon-material', label: 'network_wifi_1_bar' }),
'2': Widget.Label({ className: 'txt-norm icon-material', label: 'network_wifi_2_bar' }),
'3': Widget.Label({ className: 'txt-norm icon-material', label: 'network_wifi_3_bar' }),
'4': Widget.Label({ className: 'txt-norm icon-material', label: 'signal_wifi_4_bar' }),
},
setup: (self) => self.hook(Network, (stack) => {
if (!Network.wifi) {
return;
}
if (Network.wifi.internet == 'connected') {
stack.shown = String(Math.ceil(Network.wifi.strength / 25));
}
else if (["disconnected", "connecting"].includes(Network.wifi.internet)) {
stack.shown = Network.wifi.internet;
}
}),
});
export const NetworkIndicator = () => Widget.Stack({
transition: 'slide_up_down',
transitionDuration: userOptions.animations.durationSmall,
children: {
'fallback': SimpleNetworkIndicator(),
'wifi': NetworkWifiIndicator(),
'wired': NetworkWiredIndicator(),
},
setup: (self) => self.hook(Network, stack => {
if (!Network.primary) {
stack.shown = 'wifi';
return;
}
const primary = Network.primary || 'fallback';
if (['wifi', 'wired'].includes(primary))
stack.shown = primary;
else
stack.shown = 'fallback';
}),
});
const HyprlandXkbKeyboardLayout = async ({ useFlag } = {}) => {
try {
const Hyprland = (await import('resource:///com/github/Aylur/ags/service/hyprland.js')).default;
var initLangs = [];
var languageStackArray = [];
var currentKeyboard;
const updateCurrentKeyboards = () => {
currentKeyboard = JSON.parse(Utils.exec('hyprctl -j devices')).keyboards
.find(device => device.name === 'at-translated-set-2-keyboard');
if (currentKeyboard) {
initLangs = currentKeyboard.layout.split(',').map(lang => lang.trim());
}
languageStackArray = Array.from({ length: initLangs.length }, (_, i) => {
const lang = languages.find(lang => lang.layout == initLangs[i]);
// if (!lang) return [
// initLangs[i],
// Widget.Label({ label: initLangs[i] })
// ];
// return [
// lang.layout,
// Widget.Label({ label: (useFlag ? lang.flag : lang.layout) })
// ];
// Object
if (!lang) return {
[initLangs[i]]: Widget.Label({ label: initLangs[i] })
};
return {
[lang.layout]: Widget.Label({ label: (useFlag ? lang.flag : lang.layout) })
};
});
};
updateCurrentKeyboards();
const widgetRevealer = Widget.Revealer({
transition: 'slide_left',
transitionDuration: userOptions.animations.durationSmall,
revealChild: languageStackArray.length > 1,
});
const widgetKids = {
...languageStackArray.reduce((obj, lang) => {
return { ...obj, ...lang };
}, {}),
'undef': Widget.Label({ label: '?' }),
}
const widgetContent = Widget.Stack({
transition: 'slide_up_down',
transitionDuration: userOptions.animations.durationSmall,
children: widgetKids,
setup: (self) => self.hook(Hyprland, (stack, kbName, layoutName) => {
if (!kbName) {
return;
}
var lang = languages.find(lang => layoutName.includes(lang.name));
if (lang) {
widgetContent.shown = lang.layout;
}
else { // Attempt to support langs not listed
lang = languageStackArray.find(lang => isLanguageMatch(lang[0], layoutName));
if (!lang) stack.shown = 'undef';
else stack.shown = lang[0];
}
}, 'keyboard-layout'),
});
widgetRevealer.child = widgetContent;
return widgetRevealer;
} catch {
return null;
}
}
const OptionalKeyboardLayout = async () => {
try {
return await HyprlandXkbKeyboardLayout({ useFlag: userOptions.appearance.keyboardUseFlag });
} catch {
return null;
}
};
const optionalKeyboardLayoutInstance = await OptionalKeyboardLayout();
export const StatusIcons = (props = {}) => Widget.Box({
...props,
child: Widget.Box({
className: 'spacing-h-15',
children: [
MicMuteIndicator(),
optionalKeyboardLayoutInstance,
NotificationIndicator(),
NetworkIndicator(),
Widget.Box({
className: 'spacing-h-5',
children: [BluetoothIndicator(), BluetoothDevices()]
})
]
})
});

View file

@ -0,0 +1,62 @@
// For keyboard layout in statusicons.js
// This list is not exhaustive. It just includes known/possible languages of users of my dotfiles
// Add your language here if you use multi-lang xkb input. Else, ignore
// Note that something like "French (Canada)" should go before "French"
// and "English (US)" should go before "English"
export const languages = [
{
layout: 'us',
name: 'English (US)',
flag: '🇺🇸'
},
{
layout: 'ru',
name: 'Russian',
flag: '🇷🇺',
},
{
layout: 'pl',
name: 'Polish',
flag: '🇷🇵🇵🇱',
},
{
layout: 'ro',
name: 'Romanian',
flag: '🇷🇴',
},
{
layout: 'ca',
name: 'French (Canada)',
flag: '🇫🇷',
},
{
layout: 'fr',
name: 'French',
flag: '🇫🇷',
},
{
layout: 'tr',
name: 'Turkish',
flag: '🇹🇷',
},
{
layout: 'jp',
name: 'Japanese',
flag: '🇯🇵',
},
{
layout: 'cn',
name: 'Chinese',
flag: '🇨🇳',
},
{
layout: 'vn',
name: 'Vietnamese',
flag: '🇻🇳',
},
{
layout: 'undef',
name: 'Undefined',
flag: '🧐',
},
]

View file

@ -0,0 +1,274 @@
import Variable from 'resource:///com/github/Aylur/ags/variable.js';
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
const { Box, Button, EventBox, Label, Overlay, Stack } = Widget;
import { MaterialIcon } from './materialicon.js';
import { NavigationIndicator } from './cairo_navigationindicator.js';
import { setupCursorHover } from '../.widgetutils/cursorhover.js';
import { DoubleRevealer } from '../.widgethacks/advancedrevealers.js';
export const TabContainer = ({ icons, names, children, className = '', setup = () => { }, ...rest }) => {
const shownIndex = Variable(0);
let previousShownIndex = 0;
const count = Math.min(icons.length, names.length, children.length);
const tabs = Box({
homogeneous: true,
children: Array.from({ length: count }, (_, i) => Button({ // Tab button
className: 'tab-btn',
onClicked: () => shownIndex.value = i,
setup: setupCursorHover,
child: Box({
hpack: 'center',
vpack: 'center',
className: 'spacing-h-5 txt-small',
children: [
MaterialIcon(icons[i], 'norm'),
Label({
label: names[i],
})
]
})
})),
setup: (self) => self.hook(shownIndex, (self) => {
self.children[previousShownIndex].toggleClassName('tab-btn-active', false);
self.children[shownIndex.value].toggleClassName('tab-btn-active', true);
previousShownIndex = shownIndex.value;
}),
});
const tabIndicatorLine = Box({
hexpand: true,
vertical: true,
homogeneous: true,
setup: (self) => self.hook(shownIndex, (self) => {
self.children[0].css = `font-size: ${shownIndex.value}px;`;
}),
children: [NavigationIndicator({
className: 'tab-indicator',
count: count,
css: `font-size: ${shownIndex.value}px;`,
})],
});
const tabSection = Box({
homogeneous: true,
children: [EventBox({
onScrollUp: () => mainBox.prevTab(),
onScrollDown: () => mainBox.nextTab(),
child: Box({
vertical: true,
hexpand: true,
children: [
tabs,
tabIndicatorLine
]
})
})]
});
const contentStack = Stack({
transition: 'slide_left_right',
children: children.reduce((acc, currentValue, index) => {
acc[index] = currentValue;
return acc;
}, {}),
setup: (self) => self.hook(shownIndex, (self) => {
self.shown = `${shownIndex.value}`;
}),
});
const mainBox = Box({
attribute: {
children: children,
shown: shownIndex,
names: names,
},
vertical: true,
className: `spacing-v-5 ${className}`,
setup: (self) => {
self.pack_start(tabSection, false, false, 0);
self.pack_end(contentStack, true, true, 0);
setup(self);
},
...rest,
});
mainBox.nextTab = () => shownIndex.value = Math.min(shownIndex.value + 1, count - 1);
mainBox.prevTab = () => shownIndex.value = Math.max(shownIndex.value - 1, 0);
mainBox.cycleTab = () => shownIndex.value = (shownIndex.value + 1) % count;
return mainBox;
}
export const IconTabContainer = ({
iconWidgets, names, children, className = '',
setup = () => { }, onChange = () => { },
tabsHpack = 'center', tabSwitcherClassName = '',
...rest
}) => {
const shownIndex = Variable(0);
let previousShownIndex = 0;
const count = Math.min(iconWidgets.length, names.length, children.length);
const tabs = Box({
homogeneous: true,
hpack: tabsHpack,
className: `spacing-h-5 ${tabSwitcherClassName}`,
children: iconWidgets.map((icon, i) => Button({
className: 'tab-icon',
tooltipText: names[i],
child: icon,
setup: setupCursorHover,
onClicked: () => shownIndex.value = i,
})),
setup: (self) => self.hook(shownIndex, (self) => {
self.children[previousShownIndex].toggleClassName('tab-icon-active', false);
self.children[shownIndex.value].toggleClassName('tab-icon-active', true);
previousShownIndex = shownIndex.value;
}),
});
const tabSection = Box({
homogeneous: true,
children: [EventBox({
onScrollUp: () => mainBox.prevTab(),
onScrollDown: () => mainBox.nextTab(),
child: Box({
vertical: true,
hexpand: true,
children: [
tabs,
]
})
})]
});
const contentStack = Stack({
transition: 'slide_left_right',
children: children.reduce((acc, currentValue, index) => {
acc[index] = currentValue;
return acc;
}, {}),
setup: (self) => self.hook(shownIndex, (self) => {
self.shown = `${shownIndex.value}`;
}),
});
const mainBox = Box({
attribute: {
children: children,
shown: shownIndex,
names: names,
},
vertical: true,
className: `spacing-v-5 ${className}`,
setup: (self) => {
self.pack_start(tabSection, false, false, 0);
self.pack_end(contentStack, true, true, 0);
setup(self);
self.hook(shownIndex, (self) => onChange(self, shownIndex.value));
},
...rest,
});
mainBox.nextTab = () => shownIndex.value = Math.min(shownIndex.value + 1, count - 1);
mainBox.prevTab = () => shownIndex.value = Math.max(shownIndex.value - 1, 0);
mainBox.cycleTab = () => shownIndex.value = (shownIndex.value + 1) % count;
mainBox.shown = shownIndex;
return mainBox;
}
export const ExpandingIconTabContainer = ({
icons, names, children, className = '',
setup = () => { }, onChange = () => { },
tabsHpack = 'center', tabSwitcherClassName = '',
...rest
}) => {
const shownIndex = Variable(0);
let previousShownIndex = 0;
const count = Math.min(icons.length, names.length, children.length);
const tabs = Box({
hpack: tabsHpack,
className: `spacing-h-5 ${tabSwitcherClassName}`,
children: icons.map((icon, i) => {
const tabIcon = MaterialIcon(icon, 'norm', { hexpand: true });
const tabName = DoubleRevealer({
transition1: 'slide_right',
transition2: 'crossfade',
duration1: 0,
duration2: 0,
// duration1: userOptions.animations.durationSmall,
// duration2: userOptions.animations.durationSmall,
child: Label({
className: 'margin-left-5 txt-small',
label: names[i],
}),
revealChild: i === shownIndex.value,
})
const button = Button({
className: 'tab-icon-expandable',
tooltipText: names[i],
child: Box({
homogeneous: true,
children: [Box({
hpack: 'center',
children: [
tabIcon,
tabName,
]
})],
}),
setup: setupCursorHover,
onClicked: () => shownIndex.value = i,
});
button.toggleFocus = (value) => {
tabIcon.hexpand = !value;
button.toggleClassName('tab-icon-expandable-active', value);
tabName.toggleRevealChild(value);
}
return button;
}),
setup: (self) => self.hook(shownIndex, (self) => {
self.children[previousShownIndex].toggleFocus(false);
self.children[shownIndex.value].toggleFocus(true);
previousShownIndex = shownIndex.value;
}),
});
const tabSection = Box({
homogeneous: true,
children: [EventBox({
onScrollUp: () => mainBox.prevTab(),
onScrollDown: () => mainBox.nextTab(),
child: Box({
vertical: true,
hexpand: true,
children: [
tabs,
]
})
})]
});
const contentStack = Stack({
transition: 'slide_left_right',
children: children.reduce((acc, currentValue, index) => {
acc[index] = currentValue;
return acc;
}, {}),
setup: (self) => self.hook(shownIndex, (self) => {
self.shown = `${shownIndex.value}`;
}),
});
const mainBox = Box({
attribute: {
children: children,
shown: shownIndex,
names: names,
},
vertical: true,
className: `spacing-v-5 ${className}`,
setup: (self) => {
self.pack_start(tabSection, false, false, 0);
self.pack_end(contentStack, true, true, 0);
setup(self);
self.hook(shownIndex, (self) => onChange(self, shownIndex.value));
},
...rest,
});
mainBox.nextTab = () => shownIndex.value = Math.min(shownIndex.value + 1, count - 1);
mainBox.prevTab = () => shownIndex.value = Math.max(shownIndex.value - 1, 0);
mainBox.cycleTab = () => shownIndex.value = (shownIndex.value + 1) % count;
mainBox.shown = shownIndex;
return mainBox;
}