lavavu-osmesa 1.9.9__cp313-cp313-manylinux_2_28_x86_64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lavavu/LavaVuPython.py +561 -0
- lavavu/_LavaVuPython.cpython-313-x86_64-linux-gnu.so +0 -0
- lavavu/__init__.py +15 -0
- lavavu/__main__.py +12 -0
- lavavu/amalgamate.py +15 -0
- lavavu/aserver.py +359 -0
- lavavu/control.py +1731 -0
- lavavu/convert.py +888 -0
- lavavu/dict.json +2528 -0
- lavavu/font.bin +0 -0
- lavavu/html/LavaVu-amalgamated.css +282 -0
- lavavu/html/OK-min.js +99 -0
- lavavu/html/baseviewer.js +307 -0
- lavavu/html/control.css +104 -0
- lavavu/html/control.js +340 -0
- lavavu/html/dat-gui-light-theme.css +68 -0
- lavavu/html/dat.gui.min.js +2 -0
- lavavu/html/draw.js +2259 -0
- lavavu/html/drawbox.js +1039 -0
- lavavu/html/emscripten-template.js +184 -0
- lavavu/html/emscripten.css +92 -0
- lavavu/html/favicon.ico +0 -0
- lavavu/html/gl-matrix-min.js +47 -0
- lavavu/html/gui.css +25 -0
- lavavu/html/menu.js +615 -0
- lavavu/html/server.js +226 -0
- lavavu/html/stats.min.js +5 -0
- lavavu/html/styles.css +58 -0
- lavavu/html/webview-template.html +43 -0
- lavavu/html/webview.html +43 -0
- lavavu/lavavu.py +6200 -0
- lavavu/osmesa/LavaVuPython.py +561 -0
- lavavu/osmesa/_LavaVuPython.cpython-313-x86_64-linux-gnu.so +0 -0
- lavavu/osmesa/__init__.py +0 -0
- lavavu/points.py +191 -0
- lavavu/server.py +343 -0
- lavavu/shaders/default.frag +14 -0
- lavavu/shaders/default.vert +17 -0
- lavavu/shaders/fontShader.frag +20 -0
- lavavu/shaders/fontShader.vert +18 -0
- lavavu/shaders/lineShader.frag +39 -0
- lavavu/shaders/lineShader.vert +26 -0
- lavavu/shaders/pointShader.frag +127 -0
- lavavu/shaders/pointShader.vert +53 -0
- lavavu/shaders/triShader.frag +153 -0
- lavavu/shaders/triShader.vert +49 -0
- lavavu/shaders/volumeShader.frag +400 -0
- lavavu/shaders/volumeShader.vert +5 -0
- lavavu/tracers.py +207 -0
- lavavu/vutils.py +211 -0
- lavavu_osmesa-1.9.9.dist-info/METADATA +323 -0
- lavavu_osmesa-1.9.9.dist-info/RECORD +65 -0
- lavavu_osmesa-1.9.9.dist-info/WHEEL +5 -0
- lavavu_osmesa-1.9.9.dist-info/entry_points.txt +2 -0
- lavavu_osmesa-1.9.9.dist-info/licenses/LICENSE.md +179 -0
- lavavu_osmesa-1.9.9.dist-info/top_level.txt +1 -0
- lavavu_osmesa.libs/libLLVM-17-51492e70.so +0 -0
- lavavu_osmesa.libs/libOSMesa-f6a8f160.so.8.0.0 +0 -0
- lavavu_osmesa.libs/libdrm-b0291a67.so.2.4.0 +0 -0
- lavavu_osmesa.libs/libffi-3a37023a.so.6.0.2 +0 -0
- lavavu_osmesa.libs/libglapi-520b284c.so.0.0.0 +0 -0
- lavavu_osmesa.libs/libpcre2-8-516f4c9d.so.0.7.1 +0 -0
- lavavu_osmesa.libs/libselinux-d0805dcb.so.1 +0 -0
- lavavu_osmesa.libs/libtinfo-3a2cb85b.so.6.1 +0 -0
- lavavu_osmesa.libs/libzstd-76b78bac.so.1.4.4 +0 -0
@@ -0,0 +1,307 @@
|
|
1
|
+
//Base functions to communicate between emscripten WebGL2 viewer and IPython/Javascript page
|
2
|
+
|
3
|
+
//Init standalong gui menu handler
|
4
|
+
var hideBoxTimer;
|
5
|
+
function initBase(dict, defaultcmaps) {
|
6
|
+
var c = document.getElementById("canvas");
|
7
|
+
c.style.display = 'block';
|
8
|
+
|
9
|
+
//console.log("INITBOX: " + el.id);
|
10
|
+
var viewer = new BaseViewer();
|
11
|
+
window.viewer = viewer;
|
12
|
+
|
13
|
+
//Command callback function
|
14
|
+
viewer.command = function(cmds) {window.commands.push(cmds);}
|
15
|
+
|
16
|
+
//Data dict and colourmap names from passed strings
|
17
|
+
window.dictionary = viewer.dict = JSON.parse(dict);
|
18
|
+
window.defaultcolourmaps = viewer.defaultcolourmaps = JSON.parse(defaultcmaps);
|
19
|
+
|
20
|
+
document.addEventListener('mousemove', mouseover);
|
21
|
+
|
22
|
+
//Touch events
|
23
|
+
c.addEventListener("touchstart", touchHandler, true);
|
24
|
+
c.addEventListener("touchmove", touchHandler, true);
|
25
|
+
c.addEventListener("touchend", touchHandler, true);
|
26
|
+
|
27
|
+
return viewer;
|
28
|
+
}
|
29
|
+
|
30
|
+
function mouseover() {
|
31
|
+
//GUI elements to show on mouseover
|
32
|
+
if (window.viewer.gui)
|
33
|
+
window.viewer.gui.domElement.style.display = "block";
|
34
|
+
if (hideBoxTimer) clearTimeout(hideBoxTimer);
|
35
|
+
hideBoxTimer = setTimeout(function () { hideMenu(null, window.viewer.gui);}, 1000 );
|
36
|
+
}
|
37
|
+
|
38
|
+
//This object holds the viewer details and calls the renderers
|
39
|
+
function BaseViewer() {
|
40
|
+
this.vis = {};
|
41
|
+
this.vis.objects = [];
|
42
|
+
this.vis.colourmaps = [];
|
43
|
+
this.canvas = undefined;
|
44
|
+
|
45
|
+
//Non-persistant settings
|
46
|
+
this.mode = 'Rotate';
|
47
|
+
}
|
48
|
+
|
49
|
+
BaseViewer.prototype.reload = function() {
|
50
|
+
this.command('reload');
|
51
|
+
}
|
52
|
+
|
53
|
+
BaseViewer.prototype.sendState = function(reload) {
|
54
|
+
//Remove the cam data
|
55
|
+
this.vis.exported = true;
|
56
|
+
this.vis.reload = reload;
|
57
|
+
this.command(JSON.stringify(this.vis));
|
58
|
+
}
|
59
|
+
|
60
|
+
function Merge(obj1, obj2) {
|
61
|
+
for (var p in obj2) {
|
62
|
+
try {
|
63
|
+
//console.log(p + " ==> " + obj2[p].constructor);
|
64
|
+
// Property in destination object set; update its value.
|
65
|
+
if (!obj2.hasOwnProperty(p)) continue;
|
66
|
+
if (obj2[p].constructor == Object || obj2[p].constructor == Array) {
|
67
|
+
obj1[p] = Merge(obj1[p], obj2[p]);
|
68
|
+
} else {
|
69
|
+
//Just copy
|
70
|
+
obj1[p] = obj2[p];
|
71
|
+
}
|
72
|
+
} catch(e) {
|
73
|
+
// Property in destination object not set; create it and set its value.
|
74
|
+
obj1[p] = obj2[p];
|
75
|
+
}
|
76
|
+
}
|
77
|
+
|
78
|
+
//Clear any keys in obj1 that are not in obj2
|
79
|
+
for (var p in obj1) {
|
80
|
+
if (!obj1.hasOwnProperty(p)) continue; //Ignore built in keys
|
81
|
+
if (p in obj2) continue;
|
82
|
+
if (typeof(obj1[p]) != 'object') {
|
83
|
+
//Just delete
|
84
|
+
delete obj1[p]
|
85
|
+
}
|
86
|
+
}
|
87
|
+
|
88
|
+
return obj1;
|
89
|
+
}
|
90
|
+
|
91
|
+
BaseViewer.prototype.toString = function(nocam, reload) {
|
92
|
+
|
93
|
+
this.vis.exported = true;
|
94
|
+
this.vis.reload = reload ? true : false;
|
95
|
+
|
96
|
+
if (nocam) return JSON.stringify(this.vis);
|
97
|
+
//Export with 2 space indentation
|
98
|
+
return JSON.stringify(this.vis, undefined, 2);
|
99
|
+
}
|
100
|
+
|
101
|
+
BaseViewer.prototype.exportFile = function() {
|
102
|
+
window.open('data:text/plain;base64,' + window.btoa(this.toString(false, true)));
|
103
|
+
}
|
104
|
+
|
105
|
+
BaseViewer.prototype.loadFile = function(source) {
|
106
|
+
//Skip update to rotate/translate etc if in process of updating
|
107
|
+
//if (document.mouse.isdown) return;
|
108
|
+
//console.log("LOADING: \n" + source);
|
109
|
+
if (source.length < 3) {
|
110
|
+
console.log('Invalid source data, ignoring');
|
111
|
+
console.log(source);
|
112
|
+
console.log(BaseViewer.prototype.loadFile.caller);
|
113
|
+
return; //Invalid
|
114
|
+
}
|
115
|
+
|
116
|
+
if (!this.vis)
|
117
|
+
this.vis = {};
|
118
|
+
|
119
|
+
//Parse data
|
120
|
+
var src = {};
|
121
|
+
try {
|
122
|
+
src = JSON.parse(source);
|
123
|
+
} catch(e) {
|
124
|
+
console.log(source);
|
125
|
+
console.log("Parse Error: " + e);
|
126
|
+
return;
|
127
|
+
}
|
128
|
+
|
129
|
+
//Before merge, delete all colourmap data if changed
|
130
|
+
for (var c in this.vis.colourmaps) {
|
131
|
+
//Name or colour count mismatch? delete so can be recreated
|
132
|
+
if (this.vis.colourmaps[c].name != src.colourmaps[c].name ||
|
133
|
+
this.vis.colourmaps[c].colours.length != src.colourmaps[c].colours.length) {
|
134
|
+
//Delete the colourmap folder for this map, will be re-created
|
135
|
+
if (this.cgui && this.cgui.__folders[this.vis.colourmaps[c].name]) {
|
136
|
+
this.cgui.removeFolder(this.cgui.__folders[this.vis.colourmaps[c].name]);
|
137
|
+
this.cgui.__folders[this.vis.colourmaps[c].name] = undefined;
|
138
|
+
}
|
139
|
+
//Clear all the colours so new data will replace in merge
|
140
|
+
this.vis.colourmaps[c].colours = undefined;
|
141
|
+
}
|
142
|
+
}
|
143
|
+
|
144
|
+
//Merge keys - preserves original objects for gui access
|
145
|
+
Merge(this.vis, src);
|
146
|
+
|
147
|
+
//No model loaded? Prevents errors so default are loaded
|
148
|
+
if (!this.vis.properties)
|
149
|
+
this.vis.properties = {};
|
150
|
+
|
151
|
+
//Create UI - disable by omitting dat.gui.min.js
|
152
|
+
//(If menu doesn't exist or is open, update immediately
|
153
|
+
// otherwise, wait until clicked/opened as update is slow)
|
154
|
+
if (!this.gui || !this.gui.closed)
|
155
|
+
this.menu();
|
156
|
+
else
|
157
|
+
this.reloadgui = true;
|
158
|
+
}
|
159
|
+
|
160
|
+
BaseViewer.prototype.menu = function() {
|
161
|
+
//Create UI - disable by omitting dat.gui.min.js
|
162
|
+
//This is slow! Don't call while animating
|
163
|
+
var viewer = this;
|
164
|
+
var changefn = function(value, reload) {
|
165
|
+
//console.log(JSON.stringify(Object.keys(this)));
|
166
|
+
//console.log(value);
|
167
|
+
if (reload == undefined) {
|
168
|
+
reload = true;
|
169
|
+
if (this.property && viewer.dict[this.property])
|
170
|
+
{
|
171
|
+
//Get reload level from prop dict
|
172
|
+
//console.log(this.property + " REDRAW: " + viewer.dict[this.property].redraw);
|
173
|
+
reload = viewer.dict[this.property].redraw;
|
174
|
+
}
|
175
|
+
}
|
176
|
+
|
177
|
+
//Sync state and reload
|
178
|
+
viewer.sendState(reload);
|
179
|
+
};
|
180
|
+
|
181
|
+
if (!this.gui || this.gui.closed) {
|
182
|
+
//Re-create from scratch if closed or not present
|
183
|
+
createMenu(this, changefn, 2);
|
184
|
+
} else {
|
185
|
+
updateMenu(this, changefn);
|
186
|
+
}
|
187
|
+
this.reloadgui = false;
|
188
|
+
}
|
189
|
+
|
190
|
+
BaseViewer.prototype.syncRotation = function(rot) {
|
191
|
+
if (rot)
|
192
|
+
this.vis.views[0].rotate = rot;
|
193
|
+
rstr = '' + this.getRotationString();
|
194
|
+
that = this;
|
195
|
+
window.requestAnimationFrame(function() {that.command(rstr);});
|
196
|
+
}
|
197
|
+
|
198
|
+
BaseViewer.prototype.getRotationString = function() {
|
199
|
+
//Return current rotation quaternion as string
|
200
|
+
var q = this.vis.views[0].rotate;
|
201
|
+
return 'rotation ' + q[0] + ' ' + q[1] + ' ' + q[2] + ' ' + q[3];
|
202
|
+
}
|
203
|
+
|
204
|
+
BaseViewer.prototype.getTranslationString = function() {
|
205
|
+
return 'translation ' + this.translate[0] + ' ' + this.translate[1] + ' ' + this.translate[2];
|
206
|
+
}
|
207
|
+
|
208
|
+
BaseViewer.prototype.reset = function() {
|
209
|
+
this.command('reset');
|
210
|
+
}
|
211
|
+
|
212
|
+
//Basic touch event handling
|
213
|
+
function touchHandler(event)
|
214
|
+
{
|
215
|
+
var mouse = event.target.mouse;
|
216
|
+
if (!mouse)
|
217
|
+
event.target.mouse = mouse = {"identifier" : -1};
|
218
|
+
|
219
|
+
if (window.viewer.gui)
|
220
|
+
mouseover();
|
221
|
+
|
222
|
+
switch(event.type)
|
223
|
+
{
|
224
|
+
case "touchstart":
|
225
|
+
if (!mouse.touchmax || mouse.touchmax < event.touches.length) {
|
226
|
+
mouse.touchmax = event.touches.length;
|
227
|
+
//console.log("touchmax: " + mouse.touchmax);
|
228
|
+
if (event.touches.length == 2)
|
229
|
+
mouse.scaling = 0;
|
230
|
+
}
|
231
|
+
break;
|
232
|
+
case "touchmove":
|
233
|
+
//console.log("touchmax: " + mouse.touchmax + " == " + event.touches.length);
|
234
|
+
if (event.touches.length == mouse.touchmax) {
|
235
|
+
var down = false;
|
236
|
+
var touch;
|
237
|
+
//Get the touch to track, always use the same id
|
238
|
+
if (mouse.identifier == -1) {
|
239
|
+
mouse.identifier = event.touches[0].identifier;
|
240
|
+
touch = event.touches[0];
|
241
|
+
down = true;
|
242
|
+
} else {
|
243
|
+
//Find logged touch
|
244
|
+
for (var t=0; t<event.touches.length; t++) {
|
245
|
+
//console.log(t + " / " + event.touches.length + " : " + event.touches[t].identifier + ' === ' + mouse.identifier)
|
246
|
+
if (event.touches[t].identifier === mouse.identifier) {
|
247
|
+
touch = event.touches[t];
|
248
|
+
break;
|
249
|
+
}
|
250
|
+
}
|
251
|
+
}
|
252
|
+
|
253
|
+
var x = touch.pageX;
|
254
|
+
var y = touch.pageY;
|
255
|
+
|
256
|
+
//Delayed mouse-down event post (need to wait until all of multiple touches registered)
|
257
|
+
if (down)
|
258
|
+
window.viewer.command('mouse mouse=down,button=' + (event.touches.length > 1 ? 2 : 0) + ',x=' + x + ',y=' + y);
|
259
|
+
|
260
|
+
if (event.touches.length == 3) {
|
261
|
+
//Translate on drag with 3 fingers(equivalent to drag with right mouse)
|
262
|
+
window.viewer.command('mouse mouse=move,button=2,x=' + x + ',y=' + y);
|
263
|
+
|
264
|
+
} else if (event.touches.length == 2) {
|
265
|
+
var pinch = 0;
|
266
|
+
if (mouse.scaling != null && event.touches.length == 2) {
|
267
|
+
var dist = Math.sqrt(
|
268
|
+
(event.touches[0].pageX-event.touches[1].pageX) * (event.touches[0].pageX-event.touches[1].pageX) +
|
269
|
+
(event.touches[0].pageY-event.touches[1].pageY) * (event.touches[0].pageY-event.touches[1].pageY));
|
270
|
+
|
271
|
+
if (mouse.scaling > 0)
|
272
|
+
pinch = (dist - mouse.scaling);
|
273
|
+
else
|
274
|
+
mouse.scaling = dist;
|
275
|
+
//console.log("DIST " + dist + " SCALING " + mouse.scaling + " PINCH " + pinch);
|
276
|
+
}
|
277
|
+
|
278
|
+
//Pinch to scale? If not sufficient scaling, interpret as translate drag
|
279
|
+
if (Math.abs(pinch) > 50)
|
280
|
+
window.viewer.command('zoom ' + (pinch * 0.0005));
|
281
|
+
else
|
282
|
+
//Also translate on drag with 2 fingers(equivalent to drag with right mouse)
|
283
|
+
window.viewer.command('mouse mouse=move,button=2,x=' + x + ',y=' + y);
|
284
|
+
|
285
|
+
} else if (event.touches.length == 1) {
|
286
|
+
//Rotate
|
287
|
+
window.viewer.command('mouse mouse=move,button=0,x=' + x + ',y=' + y);
|
288
|
+
}
|
289
|
+
}
|
290
|
+
break;
|
291
|
+
case "touchend":
|
292
|
+
if (event.touches.length == mouse.touchmax) {
|
293
|
+
mouse.scaling = null;
|
294
|
+
window.viewer.command('mouse mouse=up,button=' + (event.touches.length > 1 ? 2 : 0));
|
295
|
+
}
|
296
|
+
mouse.identifier = -1;
|
297
|
+
mouse.touchmax = 0;
|
298
|
+
break;
|
299
|
+
default:
|
300
|
+
return;
|
301
|
+
}
|
302
|
+
|
303
|
+
event.preventDefault();
|
304
|
+
return false;
|
305
|
+
}
|
306
|
+
|
307
|
+
|
lavavu/html/control.css
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
/* Notebook control stylesheet */
|
2
|
+
.lvctrl input { display: inline; }
|
3
|
+
.lvctrl input[type=range] { width: 200px; border: 0px;}
|
4
|
+
.lvctrl input[type=button] { width: 100px; border: 1px solid #999; border-radius: 3px; background: transparent; margin-right: 3px; height: 20px;}
|
5
|
+
.lvctrl input[type=number] { width: 105px; height: 26px; border-radius: 0; border: 1px dotted #999;}
|
6
|
+
.lvctrl pre { display: inline; }
|
7
|
+
.lvctrl p { color: gray; margin: 0px}
|
8
|
+
.lvctrl label { margin: 0px}
|
9
|
+
|
10
|
+
.lvtab { border: 1px solid #999; padding: 5px; }
|
11
|
+
.lvtabbar { width:100%; overflow:hidden; margin-top: 5px; }
|
12
|
+
.lvtabbar .lvtabbar-item { padding:4px 8px; float:left; width:auto; border:none; outline:none; display:block; }
|
13
|
+
.lvtabbar .dropdown-hover,.lvtabbar .dropdown-click { position:static; float:left; }
|
14
|
+
.lvtabbar .lvbutton { white-space:normal; }
|
15
|
+
|
16
|
+
.lvbutton:hover { color:#000!important; background-color:#ccc!important; border: 1px solid #999; border-bottom: 0; }
|
17
|
+
.lvbutton { border: 1px solid #999; border-bottom: 0; border-radius: 5px 5px 0px 0px; margin-right: 3px; display:inline-block; outline:0; padding:4px 8px; vertical-align:middle; overflow:hidden; text-decoration:none; color:inherit; background-color:inherit; text-align:center; cursor:pointer; white-space:nowrap; }
|
18
|
+
|
19
|
+
.lvseltab,.hover-lvseltab:hover { color:#fff!important; background-color:#555!important; }
|
20
|
+
|
21
|
+
/* Range styling: http://danielstern.ca/range.css/?ref=css-tricks#/ */
|
22
|
+
input[type=range] {
|
23
|
+
-webkit-appearance: none;
|
24
|
+
width: 100%;
|
25
|
+
}
|
26
|
+
input[type=range]:focus {
|
27
|
+
outline: none;
|
28
|
+
}
|
29
|
+
input[type=range]::-webkit-slider-runnable-track {
|
30
|
+
width: 100%;
|
31
|
+
height: 3px; /* Larger track in chrome as can only click exactly on it */
|
32
|
+
cursor: pointer;
|
33
|
+
background: #d7d7d7;
|
34
|
+
border-radius: 0px;
|
35
|
+
border: 0px solid #7f7f7f;
|
36
|
+
}
|
37
|
+
input[type=range]::-webkit-slider-thumb {
|
38
|
+
border: 0.5px solid #7f7f7f;
|
39
|
+
height: 12px;
|
40
|
+
width: 12px;
|
41
|
+
border-radius: 8px;
|
42
|
+
background: #ffffff;
|
43
|
+
cursor: pointer;
|
44
|
+
-webkit-appearance: none;
|
45
|
+
margin-top: -5px;
|
46
|
+
}
|
47
|
+
input[type=range]:focus::-webkit-slider-runnable-track {
|
48
|
+
background: #949494;
|
49
|
+
margin-top: -3px;
|
50
|
+
}
|
51
|
+
input[type=range]::-moz-range-track {
|
52
|
+
width: 100%;
|
53
|
+
height: 2px;
|
54
|
+
cursor: pointer;
|
55
|
+
background: #d7d7d7;
|
56
|
+
border-radius: 0px;
|
57
|
+
border: 0px solid #7f7f7f;
|
58
|
+
}
|
59
|
+
input[type=range]::-moz-range-thumb {
|
60
|
+
border: 0.5px solid #7f7f7f;
|
61
|
+
height: 12px;
|
62
|
+
width: 12px;
|
63
|
+
border-radius: 8px;
|
64
|
+
background: #ffffff;
|
65
|
+
cursor: pointer;
|
66
|
+
}
|
67
|
+
input[type=range]::-ms-track {
|
68
|
+
width: 100%;
|
69
|
+
height: 2px;
|
70
|
+
cursor: pointer;
|
71
|
+
background: transparent;
|
72
|
+
border-color: transparent;
|
73
|
+
color: transparent;
|
74
|
+
}
|
75
|
+
input[type=range]::-ms-fill-lower {
|
76
|
+
background: #cacaca;
|
77
|
+
border: 0px solid #7f7f7f;
|
78
|
+
border-radius: 0px;
|
79
|
+
}
|
80
|
+
input[type=range]::-ms-fill-upper {
|
81
|
+
background: #d7d7d7;
|
82
|
+
border: 0px solid #7f7f7f;
|
83
|
+
border-radius: 0px;
|
84
|
+
}
|
85
|
+
input[type=range]::-ms-thumb {
|
86
|
+
border: 0.5px solid #7f7f7f;
|
87
|
+
height: 12px;
|
88
|
+
width: 12px;
|
89
|
+
border-radius: 8px;
|
90
|
+
background: #ffffff;
|
91
|
+
cursor: pointer;
|
92
|
+
height: 2px;
|
93
|
+
}
|
94
|
+
input[type=range]:focus::-ms-fill-lower {
|
95
|
+
background: #d7d7d7;
|
96
|
+
}
|
97
|
+
input[type=range]:focus::-ms-fill-upper {
|
98
|
+
background: #949494;
|
99
|
+
}
|
100
|
+
|
101
|
+
.resizer { display:flex; margin:0; padding:0; resize:both; overflow:hidden }
|
102
|
+
.resizer > .resized { flex-grow:1; margin:0; padding:0; border:0 }
|
103
|
+
|
104
|
+
|